diff --git a/apps/landing/public/fonts/bricolage-grotesque-800.ttf b/apps/landing/public/fonts/bricolage-grotesque-800.ttf new file mode 100644 index 0000000..2fe4741 Binary files /dev/null and b/apps/landing/public/fonts/bricolage-grotesque-800.ttf differ diff --git a/apps/landing/public/fonts/bricolage-grotesque-800.woff2 b/apps/landing/public/fonts/bricolage-grotesque-800.woff2 new file mode 100644 index 0000000..9dfde6f Binary files /dev/null and b/apps/landing/public/fonts/bricolage-grotesque-800.woff2 differ diff --git a/apps/landing/src/components/guides/GuideLayout.tsx b/apps/landing/src/components/guides/GuideLayout.tsx index cf73bed..f214249 100644 --- a/apps/landing/src/components/guides/GuideLayout.tsx +++ b/apps/landing/src/components/guides/GuideLayout.tsx @@ -27,9 +27,12 @@ export function GuideLayout({ readTime, children, canonical, - ogImage = 'https://www.useplunk.com/assets/card.png', + ogImage, faqs, }: GuideLayoutProps) { + const resolvedOgImage = + ogImage || + `https://www.useplunk.com/api/og?title=${encodeURIComponent(title)}&description=${encodeURIComponent(description)}&tag=Guide`; const [headings, setHeadings] = useState<{id: string; text: string; level: number}[]>([]); const [activeId, setActiveId] = useState(''); @@ -93,7 +96,7 @@ export function GuideLayout({ description: description, url: canonical, type: 'article', - images: [{url: ogImage, alt: title}], + images: [{url: resolvedOgImage, alt: title, width: 1200, height: 630}], article: { publishedTime: lastUpdated, modifiedTime: lastUpdated, @@ -106,7 +109,7 @@ export function GuideLayout({ type="Article" url={canonical || ''} title={title} - images={[ogImage]} + images={[resolvedOgImage]} datePublished={lastUpdated} dateModified={lastUpdated} authorName="Plunk" diff --git a/apps/landing/src/pages/_app.tsx b/apps/landing/src/pages/_app.tsx index 86874eb..2665c0e 100644 --- a/apps/landing/src/pages/_app.tsx +++ b/apps/landing/src/pages/_app.tsx @@ -82,7 +82,14 @@ export default function WithProviders(props: AppProps) { title: 'Plunk | The Open-Source Email Platform', description: 'Open-source email automation platform with workflows, segments, and developer API. Scale from 0 to millions of emails at $0.001 per email. Self-hostable and privacy-first.', - images: [{url: 'https://www.useplunk.com/assets/card.png', alt: 'Plunk'}], + images: [ + { + url: `https://www.useplunk.com/api/og?title=${encodeURIComponent('The Open-Source Email Platform')}`, + width: 1200, + height: 630, + alt: 'Plunk', + }, + ], }} additionalMetaTags={[{property: 'title', content: 'Plunk | The Open-Source Email Platform'}]} /> diff --git a/apps/landing/src/pages/api/og.tsx b/apps/landing/src/pages/api/og.tsx new file mode 100644 index 0000000..f0d4f0e --- /dev/null +++ b/apps/landing/src/pages/api/og.tsx @@ -0,0 +1,146 @@ +import {readFileSync} from 'fs'; +import {join} from 'path'; + +import {ImageResponse} from 'next/og'; +import type {NextApiRequest, NextApiResponse} from 'next'; + +const PLUNK_P_PATH = + 'M955 296.539C955 357.181 939.314 412.83 907.942 463.484C876.57 514.138 830.938 555.517 771.046 587.622C711.155 619.727 639.855 638.633 557.147 644.34L511.158 902.248C493.333 1001.42 444.849 1051 365.707 1051C322.214 1051 281.929 1038.16 244.853 1012.47C208.49 986.791 179.257 947.551 157.154 894.757C135.051 841.963 124 777.04 124 699.988C124 555.161 147.172 432.449 193.517 331.854C240.575 230.546 303.319 154.922 381.749 104.981C460.892 54.327 547.878 29 642.707 29C709.728 29 766.412 40.7717 812.757 64.3152C859.815 87.8586 895.108 119.963 918.637 160.629C942.879 200.582 955 245.885 955 296.539ZM576.398 534.114C722.562 515.565 795.645 439.584 795.645 306.171C795.645 259.084 779.959 220.915 748.587 191.664C717.928 161.699 670.157 146.717 605.274 146.717C531.835 146.717 467.665 169.904 412.764 216.278C358.577 262.651 316.51 327.217 286.564 409.976C257.331 492.021 242.714 585.838 242.714 691.427C242.714 735.66 246.992 774.9 255.548 809.145C264.817 843.39 276.225 870.143 289.772 889.406C304.032 907.956 317.579 917.23 330.413 917.23C348.238 917.23 361.785 892.617 371.054 843.39L406.347 641.13L405.633 646.299L411.708 611.502L416.534 583.854L420.128 561.451L424.529 534.114C433.798 478.466 446.988 403.912 464.1 310.451C468.378 286.194 478.004 269.072 492.977 259.084C508.663 248.382 526.844 243.031 547.521 243.031C571.05 243.031 587.806 247.669 597.788 256.943C608.483 265.505 613.83 279.417 613.83 298.68C613.83 310.095 613.117 319.369 611.691 326.504C597.908 407.581 590.181 453.037 576.398 534.114Z'; + +function loadFont(): Buffer { + return readFileSync(join(process.cwd(), 'public/fonts/bricolage-grotesque-800.ttf')); +} + +export default function handler(req: NextApiRequest, res: NextApiResponse) { + const title = (req.query.title as string) || 'The Open-Source Email Platform'; + const description = (req.query.description as string) || ''; + const tag = (req.query.tag as string) || ''; + + const fontData = loadFont(); + + const titleLength = title.length; + const fontSize = titleLength < 35 ? 72 : titleLength < 55 ? 60 : titleLength < 75 ? 50 : 42; + + const imageResponse = new ImageResponse( + ( +
+ {/* Ghost P watermark */} +
+ + + +
+ + {/* Top row: logo + URL */} +
+
+ + + + + Plunk + +
+ + useplunk.com + +
+ + {/* Main content */} +
+ {tag ? ( +
+ {tag} +
+ ) : null} +
+ {title} +
+ {description ? ( +
+ {description} +
+ ) : null} +
+ + {/* Bottom spacer */} +
+
+ ), + { + width: 1200, + height: 630, + fonts: [ + { + name: 'Bricolage Grotesque', + data: fontData, + style: 'normal', + weight: 800, + }, + ], + }, + ); + + res.setHeader('Content-Type', 'image/png'); + res.setHeader('Cache-Control', 'public, max-age=86400, stale-while-revalidate=604800'); + imageResponse.arrayBuffer().then((buffer) => { + res.end(Buffer.from(buffer)); + }); +} diff --git a/apps/landing/src/pages/features/email-editor.tsx b/apps/landing/src/pages/features/email-editor.tsx index 4987843..68f3422 100644 --- a/apps/landing/src/pages/features/email-editor.tsx +++ b/apps/landing/src/pages/features/email-editor.tsx @@ -79,6 +79,9 @@ export default function EmailEditorFeature() { property="og:description" content="The email editor that speaks both languages. Switch seamlessly between visual and code editing, preview with real data, and create templates that work everywhere." /> + + + diff --git a/apps/landing/src/pages/features/inbound-email.tsx b/apps/landing/src/pages/features/inbound-email.tsx index 7a21ef3..68067b7 100644 --- a/apps/landing/src/pages/features/inbound-email.tsx +++ b/apps/landing/src/pages/features/inbound-email.tsx @@ -78,6 +78,9 @@ export default function InboundEmailFeature() { property="og:description" content="Receive emails at your custom domain and automatically process them. Capture leads, create support tickets, and trigger workflows from incoming emails." /> + + + diff --git a/apps/landing/src/pages/features/segments.tsx b/apps/landing/src/pages/features/segments.tsx index 61ae9f6..775b8c9 100644 --- a/apps/landing/src/pages/features/segments.tsx +++ b/apps/landing/src/pages/features/segments.tsx @@ -93,6 +93,9 @@ export default function SegmentsFeature() { property="og:description" content="Create dynamic and static segments to organize your contacts. Filter by behavior, attributes, and engagement." /> + + + diff --git a/apps/landing/src/pages/features/smtp.tsx b/apps/landing/src/pages/features/smtp.tsx index d75f140..db0787a 100644 --- a/apps/landing/src/pages/features/smtp.tsx +++ b/apps/landing/src/pages/features/smtp.tsx @@ -89,6 +89,9 @@ export default function SMTPFeature() { property="og:description" content="Send emails via SMTP or API. Works with any email client or application. Secure TLS/SSL connections with automatic domain validation and full tracking." /> + + + diff --git a/apps/landing/src/pages/features/workflows.tsx b/apps/landing/src/pages/features/workflows.tsx index e4cebc9..c33e6e2 100644 --- a/apps/landing/src/pages/features/workflows.tsx +++ b/apps/landing/src/pages/features/workflows.tsx @@ -78,6 +78,9 @@ export default function WorkflowsFeature() { property="og:description" content="Build sophisticated email automation workflows with visual no-code builder. Create event-driven sequences, conditional branching, and time-based delays." /> + + + diff --git a/apps/landing/src/pages/guides/index.tsx b/apps/landing/src/pages/guides/index.tsx index f2fa777..03d92dd 100644 --- a/apps/landing/src/pages/guides/index.tsx +++ b/apps/landing/src/pages/guides/index.tsx @@ -112,7 +112,7 @@ export default function GuidesIndex() { description: 'Learn email best practices, authentication (DKIM, SPF, DMARC), deliverability optimization, and more.', url: 'https://www.useplunk.com/guides', - images: [{url: 'https://www.useplunk.com/assets/card.png', alt: 'Plunk Guides'}], + images: [{url: 'https://www.useplunk.com/api/og?title=Email+Guides+for+Developers&tag=Guide', alt: 'Plunk Guides', width: 1200, height: 630}], }} /> diff --git a/apps/landing/src/pages/index.tsx b/apps/landing/src/pages/index.tsx index 6ae379c..37cb6c4 100644 --- a/apps/landing/src/pages/index.tsx +++ b/apps/landing/src/pages/index.tsx @@ -162,6 +162,14 @@ export default function Index() { title: 'Plunk — Open-Source Transactional Email Platform', description: 'Send transactional emails, run marketing campaigns, and automate workflows — all open-source and self-hostable. $0.001/email, no contact limits.', + images: [ + { + url: 'https://www.useplunk.com/api/og?title=The+Open-Source+Email+Platform', + width: 1200, + height: 630, + alt: 'Plunk — The Open-Source Email Platform', + }, + ], }} />