this repo has no description
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

Add SVG logo, favicons, OG images, and social meta tags

SVG logo (mark + text variants), full favicon set (SVG, PNG, Apple,
Android/PWA via web manifest), and per-page OpenGraph images for all
public routes. Unified ogMeta() helper replaces ad-hoc meta arrays
with correct property attributes for Bluesky/social card compatibility.

+204 -33
+1
.gitignore
··· 1 + .DS_Store 1 2 /target 2 3 appview/_build/ 3 4 appview/deps/
+119
tools/generate-og-images.mjs
··· 1 + #!/usr/bin/env node 2 + import { execFileSync } from "node:child_process"; 3 + import { writeFileSync, mkdirSync } from "node:fs"; 4 + import { join } from "node:path"; 5 + 6 + const PUBLIC = join(import.meta.dirname, "../web/public"); 7 + const OG_DIR = join(PUBLIC, "og"); 8 + mkdirSync(OG_DIR, { recursive: true }); 9 + 10 + const BG = "#f4f0e8"; 11 + const TEXT = "#1C1408"; 12 + const MUTED = "#5C4A2E"; 13 + const GOLD = "#9A7840"; 14 + const WIDTH = 1200; 15 + const HEIGHT = 630; 16 + 17 + const LOGO_MARK_LG = ` 18 + <rect x="0" y="0" width="90" height="90" rx="7" fill="${GOLD}" fill-opacity="0.7" /> 19 + <rect x="24" y="24" width="90" height="90" rx="7" fill="${GOLD}" fill-opacity="0.2" stroke="${GOLD}" stroke-opacity="0.45" stroke-width="3.5" /> 20 + `; 21 + 22 + const LOGO_MARK_SM = ` 23 + <rect x="0" y="0" width="60" height="60" rx="5" fill="${GOLD}" fill-opacity="0.7" /> 24 + <rect x="16" y="16" width="60" height="60" rx="5" fill="${GOLD}" fill-opacity="0.2" stroke="${GOLD}" stroke-opacity="0.45" stroke-width="2.5" /> 25 + `; 26 + 27 + function escapeXml(s) { 28 + return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;"); 29 + } 30 + 31 + function wrapText(text, maxChars) { 32 + const words = text.split(" "); 33 + const lines = []; 34 + let current = ""; 35 + for (const word of words) { 36 + if (current.length + word.length + 1 > maxChars) { 37 + lines.push(current); 38 + current = word; 39 + } else { 40 + current = current ? `${current} ${word}` : word; 41 + } 42 + } 43 + if (current) lines.push(current); 44 + return lines; 45 + } 46 + 47 + function baseSvg() { 48 + return `<svg xmlns="http://www.w3.org/2000/svg" width="${WIDTH}" height="${HEIGHT}" viewBox="0 0 ${WIDTH} ${HEIGHT}"> 49 + <rect width="${WIDTH}" height="${HEIGHT}" fill="${BG}" /> 50 + <g transform="translate(${(WIDTH - 114) / 2}, 110)"> 51 + ${LOGO_MARK_LG} 52 + </g> 53 + <text x="${WIDTH / 2}" y="330" text-anchor="middle" 54 + font-family="Cormorant Garamond, Georgia, serif" font-size="120" font-weight="500" 55 + letter-spacing="0.05em" fill="${TEXT}">Opake</text> 56 + <text x="${WIDTH / 2}" y="400" text-anchor="middle" 57 + font-family="Inter, Helvetica, sans-serif" font-size="34" fill="${MUTED}">Your data, freely shared, privately kept</text> 58 + </svg>`; 59 + } 60 + 61 + function docSvg(title, description) { 62 + const titleLines = wrapText(escapeXml(title), 22); 63 + const descLines = wrapText(escapeXml(description), 45); 64 + 65 + const titleY = 290; 66 + const titleMarkup = titleLines 67 + .map((line, i) => `<tspan x="100" dy="${i === 0 ? 0 : 95}">${line}</tspan>`) 68 + .join(""); 69 + const descStartY = titleY + titleLines.length * 95 + 36; 70 + const descMarkup = descLines 71 + .map((line, i) => `<tspan x="100" dy="${i === 0 ? 0 : 42}">${line}</tspan>`) 72 + .join(""); 73 + 74 + return `<svg xmlns="http://www.w3.org/2000/svg" width="${WIDTH}" height="${HEIGHT}" viewBox="0 0 ${WIDTH} ${HEIGHT}"> 75 + <rect width="${WIDTH}" height="${HEIGHT}" fill="${BG}" /> 76 + <g transform="translate(100, 70)"> 77 + ${LOGO_MARK_SM} 78 + </g> 79 + <text x="186" y="115" 80 + font-family="Cormorant Garamond, Georgia, serif" font-size="42" font-weight="500" 81 + letter-spacing="0.05em" fill="${TEXT}">Opake</text> 82 + <text y="${titleY}" 83 + font-family="Cormorant Garamond, Georgia, serif" font-size="84" font-weight="400" 84 + fill="${TEXT}">${titleMarkup}</text> 85 + <text y="${descStartY}" 86 + font-family="Inter, Helvetica, sans-serif" font-size="32" fill="${MUTED}">${descMarkup}</text> 87 + </svg>`; 88 + } 89 + 90 + const DOCS = [ 91 + { slug: "getting-started", title: "Getting Started", description: "Set up your cabinet, create your first encrypted file, and explore the interface." }, 92 + { slug: "at-protocol", title: "AT Protocol", description: "The open standard powering Opake — identity, data portability, and federation." }, 93 + { slug: "encryption-keys", title: "Encryption & Keys", description: "How end-to-end encryption works in Opake and how your keys are managed." }, 94 + { slug: "sharing-dids", title: "Sharing & DIDs", description: "Share files using decentralised identifiers without a central authority." }, 95 + { slug: "keyrings", title: "Keyrings & Groups", description: "Manage secure group sharing for families, teams, and research groups." }, 96 + { slug: "pairing", title: "Multi-Device Magic", description: "Securely transfer your identity keypair to new devices using your PDS as a relay." }, 97 + { slug: "glossary", title: "Glossary", description: "A quick-hit reference for the terminology and acronyms we use in Opake." }, 98 + { slug: "faq", title: "FAQ", description: "Common questions about privacy, security, and how Opake compares to alternatives." }, 99 + ]; 100 + 101 + function renderPng(svgContent, outputPath) { 102 + const tmpSvg = `${outputPath}.tmp.svg`; 103 + writeFileSync(tmpSvg, svgContent); 104 + execFileSync("npx", ["sharp-cli", "-i", tmpSvg, "-o", outputPath, "resize", String(WIDTH), String(HEIGHT)], { 105 + cwd: join(import.meta.dirname, "../web"), 106 + stdio: "pipe", 107 + }); 108 + execFileSync("rm", [tmpSvg]); 109 + } 110 + 111 + console.log("Generating base OG image..."); 112 + renderPng(baseSvg(), join(OG_DIR, "default.png")); 113 + 114 + for (const doc of DOCS) { 115 + console.log(`Generating OG image for ${doc.slug}...`); 116 + renderPng(docSvg(doc.title, doc.description), join(OG_DIR, `${doc.slug}.png`)); 117 + } 118 + 119 + console.log("Done!");
+1
web/.env.development
··· 1 1 VITE_APPVIEW_URL=http://localhost:6100 2 + VITE_SITE_URL=http://localhost:3000
web/public/apple-touch-icon.png

This is a binary file and will not be displayed.

web/public/favicon-32.png

This is a binary file and will not be displayed.

+4
web/public/favicon.svg
··· 1 + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 28 28"> 2 + <rect x="0" y="0" width="22" height="22" rx="2" fill="#9A7840" fill-opacity="0.7" /> 3 + <rect x="6" y="6" width="22" height="22" rx="2" fill="#9A7840" fill-opacity="0.2" stroke="#9A7840" stroke-opacity="0.45" stroke-width="1.5" /> 4 + </svg>
web/public/icon-192.png

This is a binary file and will not be displayed.

web/public/icon-512.png

This is a binary file and will not be displayed.

web/public/og/at-protocol.png

This is a binary file and will not be displayed.

web/public/og/default.png

This is a binary file and will not be displayed.

web/public/og/encryption-keys.png

This is a binary file and will not be displayed.

web/public/og/faq.png

This is a binary file and will not be displayed.

web/public/og/getting-started.png

This is a binary file and will not be displayed.

web/public/og/glossary.png

This is a binary file and will not be displayed.

web/public/og/keyrings.png

This is a binary file and will not be displayed.

web/public/og/pairing.png

This is a binary file and will not be displayed.

web/public/og/sharing-dids.png

This is a binary file and will not be displayed.

+7
web/public/opake-logo-text.svg
··· 1 + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 28"> 2 + <!-- Mark --> 3 + <rect x="0" y="0" width="22" height="22" rx="2" fill="#9A7840" fill-opacity="0.7" /> 4 + <rect x="6" y="6" width="22" height="22" rx="2" fill="#9A7840" fill-opacity="0.2" stroke="#9A7840" stroke-opacity="0.45" stroke-width="1.5" /> 5 + <!-- Wordmark --> 6 + <text x="38" y="22" font-family="Cormorant Garamond, Cormorant, Georgia, serif" font-size="22" font-weight="500" letter-spacing="0.05em" fill="#1C1408">Opake</text> 7 + </svg>
+4
web/public/opake-logo.svg
··· 1 + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 28 28" width="28" height="28"> 2 + <rect x="0" y="0" width="22" height="22" rx="2" fill="#9A7840" fill-opacity="0.7" /> 3 + <rect x="6" y="6" width="22" height="22" rx="2" fill="#9A7840" fill-opacity="0.2" stroke="#9A7840" stroke-opacity="0.45" stroke-width="1.5" /> 4 + </svg>
+11
web/public/site.webmanifest
··· 1 + { 2 + "name": "Opake", 3 + "short_name": "Opake", 4 + "icons": [ 5 + { "src": "/icon-192.png", "sizes": "192x192", "type": "image/png" }, 6 + { "src": "/icon-512.png", "sizes": "512x512", "type": "image/png" } 7 + ], 8 + "theme_color": "#f4f0e8", 9 + "background_color": "#f4f0e8", 10 + "display": "standalone" 11 + }
+23
web/src/lib/og-meta.ts
··· 1 + const SITE_URL = (import.meta.env.VITE_SITE_URL as string | undefined) ?? "https://opake.app"; 2 + 3 + interface OgMeta { 4 + readonly title: string; 5 + readonly description: string; 6 + readonly image?: string; 7 + } 8 + 9 + export function ogMeta({ title, description, image }: OgMeta) { 10 + const ogImage = image ? `${SITE_URL}${image}` : `${SITE_URL}/og/default.png`; 11 + 12 + return [ 13 + { title }, 14 + { name: "description", content: description }, 15 + { property: "og:title", content: title }, 16 + { property: "og:description", content: description }, 17 + { property: "og:image", content: ogImage }, 18 + { name: "twitter:title", content: title }, 19 + { name: "twitter:description", content: description }, 20 + { name: "twitter:image", content: ogImage }, 21 + { name: "twitter:card", content: "summary_large_image" }, 22 + ]; 23 + }
+7 -3
web/src/routes/__root.tsx
··· 35 35 type="font/woff2" 36 36 crossOrigin="" 37 37 /> 38 + <link rel="icon" type="image/svg+xml" href="/favicon.svg" /> 39 + <link rel="icon" type="image/png" sizes="32x32" href="/favicon-32.png" /> 40 + <link rel="apple-touch-icon" href="/apple-touch-icon.png" /> 41 + <link rel="manifest" href="/site.webmanifest" /> 42 + <meta name="theme-color" content="#f4f0e8" /> 38 43 <link rel="stylesheet" href={css} /> 39 44 <HeadContent /> 40 45 </head> ··· 106 111 head: () => ({ 107 112 meta: [ 108 113 { title: "Opake" }, 109 - { name: "og:site_name", content: "Opake" }, 110 - { name: "og:type", content: "website" }, 111 - { name: "twitter:card", content: "summary" }, 114 + { property: "og:site_name", content: "Opake" }, 115 + { property: "og:type", content: "website" }, 112 116 ], 113 117 }), 114 118 component: RootLayout,
+6 -4
web/src/routes/_public/docs/$slug.tsx
··· 2 2 import { createFileRoute } from "@tanstack/react-router"; 3 3 import { MdxContent } from "@/components/content/MdxProvider"; 4 4 import { findDoc } from "@/lib/docs-registry"; 5 + import { ogMeta } from "@/lib/og-meta"; 5 6 6 7 import GettingStarted from "@/content/docs/getting-started.mdx"; 7 8 import AtProtocol from "@/content/docs/at-protocol.mdx"; ··· 48 49 head: ({ params }) => { 49 50 const doc = findDoc(params.slug); 50 51 return { 51 - meta: [ 52 - { title: doc ? `${doc.title} — Opake` : "Docs — Opake" }, 53 - { name: "description", content: doc?.description ?? "" }, 54 - ], 52 + meta: ogMeta({ 53 + title: doc ? `${doc.title} — Opake` : "Docs — Opake", 54 + description: doc?.description ?? "", 55 + image: doc ? `/og/${params.slug}.png` : undefined, 56 + }), 55 57 }; 56 58 }, 57 59 component: DocChapterPage,
+5 -7
web/src/routes/_public/docs/index.tsx
··· 1 1 import { createFileRoute } from "@tanstack/react-router"; 2 + import { ogMeta } from "@/lib/og-meta"; 2 3 import { MdxContent } from "@/components/content/MdxProvider"; 3 4 import DocsIndexContent from "@/content/docs/index.mdx"; 4 5 ··· 12 13 13 14 export const Route = createFileRoute("/_public/docs/")({ 14 15 head: () => ({ 15 - meta: [ 16 - { title: "The Opaque Handbook — Opake" }, 17 - { 18 - name: "description", 19 - content: "Everything you need to get the most out of Opake.", 20 - }, 21 - ], 16 + meta: ogMeta({ 17 + title: "The Opaque Handbook — Opake", 18 + description: "Everything you need to get the most out of Opake.", 19 + }), 22 20 }), 23 21 component: DocsIndexPage, 24 22 });
+6 -4
web/src/routes/_public/faq.tsx
··· 1 1 import { createFileRoute } from "@tanstack/react-router"; 2 + import { ogMeta } from "@/lib/og-meta"; 2 3 import { MdxContent } from "@/components/content/MdxProvider"; 3 4 import FaqContent from "@/content/faq.mdx"; 4 5 ··· 12 13 13 14 export const Route = createFileRoute("/_public/faq")({ 14 15 head: () => ({ 15 - meta: [ 16 - { title: "FAQ — Opake" }, 17 - { name: "description", content: "Frequently asked questions about Opake." }, 18 - ], 16 + meta: ogMeta({ 17 + title: "FAQ — Opake", 18 + description: "Frequently asked questions about Opake.", 19 + image: "/og/faq.png", 20 + }), 19 21 }), 20 22 component: FaqPage, 21 23 });
+5 -8
web/src/routes/_public/index.tsx
··· 1 1 import { createFileRoute, Link } from "@tanstack/react-router"; 2 2 import { ArrowRightIcon } from "@phosphor-icons/react"; 3 + import { ogMeta } from "@/lib/og-meta"; 3 4 import { 4 5 HeroSection, 5 6 HeroHeadline, ··· 151 152 152 153 export const Route = createFileRoute("/_public/")({ 153 154 head: () => ({ 154 - meta: [ 155 - { title: "Opake — Your data, freely shared, privately kept" }, 156 - { name: "description", content: DESCRIPTION }, 157 - { name: "og:title", content: "Opake — Your data, freely shared, privately kept" }, 158 - { name: "og:description", content: DESCRIPTION }, 159 - { name: "twitter:title", content: "Opake — Your data, freely shared, privately kept" }, 160 - { name: "twitter:description", content: DESCRIPTION }, 161 - ], 155 + meta: ogMeta({ 156 + title: "Opake — Your data, freely shared, privately kept", 157 + description: DESCRIPTION, 158 + }), 162 159 }), 163 160 component: LandingPage, 164 161 });
+5 -7
web/src/routes/_public/troubleshooting.tsx
··· 1 1 import { createFileRoute } from "@tanstack/react-router"; 2 + import { ogMeta } from "@/lib/og-meta"; 2 3 import { MdxContent } from "@/components/content/MdxProvider"; 3 4 import TroubleshootingContent from "@/content/troubleshooting.mdx"; 4 5 ··· 12 13 13 14 export const Route = createFileRoute("/_public/troubleshooting")({ 14 15 head: () => ({ 15 - meta: [ 16 - { title: "Troubleshooting — Opake" }, 17 - { 18 - name: "description", 19 - content: "Common issues and solutions for Opake.", 20 - }, 21 - ], 16 + meta: ogMeta({ 17 + title: "Troubleshooting — Opake", 18 + description: "Common issues and solutions for Opake.", 19 + }), 22 20 }), 23 21 component: TroubleshootingPage, 24 22 });