ui: update Open Graph and Twitter card images for various pages to enhance social sharing

This commit is contained in:
Dries Augustyns
2026-05-04 12:32:51 +02:00
parent 29883ffc71
commit 4b5815c1d6
40 changed files with 492 additions and 27 deletions
@@ -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<string>('');
@@ -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"
+8 -1
View File
@@ -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'}]}
/>
+146
View File
@@ -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(
(
<div
style={{
width: '1200px',
height: '630px',
display: 'flex',
flexDirection: 'column',
justifyContent: 'space-between',
padding: '64px 72px',
backgroundColor: '#ffffff',
fontFamily: '"Bricolage Grotesque"',
position: 'relative',
overflow: 'hidden',
}}
>
{/* Ghost P watermark */}
<div
style={{
position: 'absolute',
bottom: '-60px',
right: '-30px',
opacity: 0.035,
display: 'flex',
}}
>
<svg width="480" height="480" viewBox="0 0 1080 1080" fill="none">
<path d={PLUNK_P_PATH} fill="#000000" />
</svg>
</div>
{/* Top row: logo + URL */}
<div style={{display: 'flex', justifyContent: 'space-between', alignItems: 'center'}}>
<div style={{display: 'flex', alignItems: 'center', gap: '10px'}}>
<svg width="30" height="30" viewBox="0 0 1080 1080" fill="none">
<path d={PLUNK_P_PATH} fill="#171717" />
</svg>
<span
style={{
fontSize: '22px',
fontWeight: 800,
color: '#171717',
letterSpacing: '-0.03em',
}}
>
Plunk
</span>
</div>
<span style={{fontSize: '15px', color: '#a3a3a3', letterSpacing: '0.01em'}}>
useplunk.com
</span>
</div>
{/* Main content */}
<div style={{display: 'flex', flexDirection: 'column', gap: '18px', maxWidth: '920px'}}>
{tag ? (
<div
style={{
display: 'flex',
alignItems: 'center',
padding: '6px 14px',
backgroundColor: '#f4f4f5',
borderRadius: '6px',
fontSize: '15px',
color: '#52525b',
fontWeight: 600,
width: 'fit-content',
letterSpacing: '-0.01em',
}}
>
{tag}
</div>
) : null}
<div
style={{
fontSize: `${fontSize}px`,
fontWeight: 800,
color: '#0a0a0a',
lineHeight: 1.08,
letterSpacing: '-0.04em',
}}
>
{title}
</div>
{description ? (
<div
style={{
fontSize: '22px',
color: '#737373',
lineHeight: 1.5,
fontWeight: 400,
letterSpacing: '-0.01em',
}}
>
{description}
</div>
) : null}
</div>
{/* Bottom spacer */}
<div style={{display: 'flex'}} />
</div>
),
{
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));
});
}
@@ -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."
/>
<meta property="og:image" content="https://www.useplunk.com/api/og?title=Email+Editor&tag=Feature" />
<meta property="twitter:card" content="summary_large_image" />
<meta property="twitter:image" content="https://www.useplunk.com/api/og?title=Email+Editor&tag=Feature" />
</Head>
<Navbar />
@@ -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."
/>
<meta property="og:image" content="https://www.useplunk.com/api/og?title=Inbound+Email+Processing&tag=Feature" />
<meta property="twitter:card" content="summary_large_image" />
<meta property="twitter:image" content="https://www.useplunk.com/api/og?title=Inbound+Email+Processing&tag=Feature" />
</Head>
<Navbar />
@@ -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."
/>
<meta property="og:image" content="https://www.useplunk.com/api/og?title=Audience+Segmentation&tag=Feature" />
<meta property="twitter:card" content="summary_large_image" />
<meta property="twitter:image" content="https://www.useplunk.com/api/og?title=Audience+Segmentation&tag=Feature" />
</Head>
<Navbar />
+3
View File
@@ -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."
/>
<meta property="og:image" content="https://www.useplunk.com/api/og?title=SMTP+Email+Sending&tag=Feature" />
<meta property="twitter:card" content="summary_large_image" />
<meta property="twitter:image" content="https://www.useplunk.com/api/og?title=SMTP+Email+Sending&tag=Feature" />
</Head>
<Navbar />
@@ -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."
/>
<meta property="og:image" content="https://www.useplunk.com/api/og?title=Email+Workflow+Automation&tag=Feature" />
<meta property="twitter:card" content="summary_large_image" />
<meta property="twitter:image" content="https://www.useplunk.com/api/og?title=Email+Workflow+Automation&tag=Feature" />
</Head>
<Navbar />
+1 -1
View File
@@ -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}],
}}
/>
+8
View File
@@ -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',
},
],
}}
/>
<Script
+8
View File
@@ -79,6 +79,14 @@ export default function Pricing() {
title: 'Plunk Pricing | The Open-Source Email Platform',
description:
'Transparent email pricing at $0.001 per email with no contact limits. Free plan includes 1,000 emails/month. No hidden fees.',
images: [
{
url: 'https://www.useplunk.com/api/og?title=Simple%2C+Honest+Pricing&description=%240.001+per+email.+No+contact+limits.+Free+to+start.',
width: 1200,
height: 630,
alt: 'Plunk Pricing',
},
],
}}
additionalMetaTags={[{property: 'title', content: 'Plunk Pricing | The Open-Source Email Platform'}]}
/>
+1 -1
View File
@@ -64,7 +64,7 @@ export default function ToolsIndex() {
description:
'Free email developer tools: spam checker, email address validator, and markdown-to-email converter. No sign-up required.',
url: 'https://www.useplunk.com/tools',
images: [{url: 'https://www.useplunk.com/assets/card.png', alt: 'Plunk Email Tools'}],
images: [{url: 'https://www.useplunk.com/api/og?title=Free+Email+Developer+Tools&tag=Tool', alt: 'Plunk Email Tools', width: 1200, height: 630}],
}}
/>
@@ -54,7 +54,7 @@ export default function MarkdownToEmail() {
description:
'Convert Markdown to email-safe HTML instantly — handles inline styles, table layouts, and compatibility across email clients. Free, no sign-up.',
url: 'https://www.useplunk.com/tools/markdown-to-email',
images: [{url: 'https://www.useplunk.com/assets/card.png', alt: 'Plunk Email HTML Converter'}],
images: [{url: 'https://www.useplunk.com/api/og?title=Markdown+to+Email+HTML+Converter&tag=Tool', alt: 'Plunk Markdown to Email', width: 1200, height: 630}],
}}
/>
@@ -238,7 +238,7 @@ export default function SpamCheckerPage() {
title: 'Email Spam Checker | Free Subject Line & Content Spam Test | Plunk',
description: 'Free spam checker. Test your email subject line and content for factors that cause emails to land in spam folders.',
url: 'https://www.useplunk.com/tools/spam-checker',
images: [{url: 'https://www.useplunk.com/assets/card.png', alt: 'Plunk Email Spam Checker'}],
images: [{url: 'https://www.useplunk.com/api/og?title=Free+Email+Spam+Checker&tag=Tool', alt: 'Plunk Spam Checker', width: 1200, height: 630}],
}}
/>
@@ -65,7 +65,7 @@ export default function VerifyEmailPage() {
title: 'Email Validator | Free Email Address Verification Tool | Plunk',
description: 'Free email validator. Check email addresses for validity, typos, disposable domains, and MX records. Verify any email instantly.',
url: 'https://www.useplunk.com/tools/verify-email',
images: [{url: 'https://www.useplunk.com/assets/card.png', alt: 'Plunk Email Validator Tool'}],
images: [{url: 'https://www.useplunk.com/api/og?title=Free+Email+Address+Validator&tag=Tool', alt: 'Plunk Email Validator Tool', width: 1200, height: 630}],
}}
/>
+1 -1
View File
@@ -61,7 +61,7 @@ export default function ActiveCampaignComparison() {
description:
"Powerful email automation without ActiveCampaign's complexity and cost. Open-source, developer-friendly, pay-as-you-go pricing.",
url: 'https://www.useplunk.com/vs/activecampaign',
images: [{url: 'https://www.useplunk.com/assets/card.png', alt: 'Plunk vs ActiveCampaign'}],
images: [{url: 'https://www.useplunk.com/api/og?title=ActiveCampaign+Alternative%3A+Open-Source+%26+Affordable&tag=Comparison', alt: 'Plunk vs ActiveCampaign', width: 1200, height: 630}],
}}
/>
+1 -1
View File
@@ -61,7 +61,7 @@ export default function BentoComparison() {
description:
'Open-source alternative to Bento. API-first email platform without CRM bloat. Self-hostable, pay-as-you-go pricing.',
url: 'https://www.useplunk.com/vs/bento',
images: [{url: 'https://www.useplunk.com/assets/card.png', alt: 'Plunk vs Bento'}],
images: [{url: 'https://www.useplunk.com/api/og?title=Bento+Alternative%3A+Developer-First+Email+Platform&tag=Comparison', alt: 'Plunk vs Bento', width: 1200, height: 630}],
}}
/>
+1 -1
View File
@@ -63,7 +63,7 @@ export default function BrevoComparison() {
description:
"Done with Brevo's contact-based pricing? Plunk is open-source, self-hostable, and charges per email not per contact.",
url: 'https://www.useplunk.com/vs/brevo',
images: [{url: 'https://www.useplunk.com/assets/card.png', alt: 'Plunk vs Brevo'}],
images: [{url: 'https://www.useplunk.com/api/og?title=Brevo+Alternative%3A+Developer-First+%26+Open+Source&tag=Comparison', alt: 'Plunk vs Brevo', width: 1200, height: 630}],
}}
/>
+1 -1
View File
@@ -64,7 +64,7 @@ export default function ConvertkitComparison() {
description:
'Developer-first alternative to ConvertKit. Pay per email instead of per subscriber. Open-source, self-hostable, with powerful automation.',
url: 'https://www.useplunk.com/vs/convertkit',
images: [{url: 'https://www.useplunk.com/assets/card.png', alt: 'Plunk vs ConvertKit'}],
images: [{url: 'https://www.useplunk.com/api/og?title=ConvertKit+Alternative+for+Developers&tag=Comparison', alt: 'Plunk vs ConvertKit', width: 1200, height: 630}],
}}
/>
+1 -1
View File
@@ -59,7 +59,7 @@ export default function CustomerioComparison() {
description:
"Frustrated with Customer.io's complexity and cost? Plunk is open-source with simple workflow automation and transparent pay-as-you-go pricing.",
url: 'https://www.useplunk.com/vs/customerio',
images: [{url: 'https://www.useplunk.com/assets/card.png', alt: 'Plunk vs Customer.io'}],
images: [{url: 'https://www.useplunk.com/api/og?title=Customer.io+Alternative%3A+Open-Source+%26+Developer-Focused&tag=Comparison', alt: 'Plunk vs Customer.io', width: 1200, height: 630}],
}}
/>
+1 -1
View File
@@ -99,7 +99,7 @@ export default function CompetitorsIndex() {
description:
'Open-source alternative to Resend, Mailgun, and SendGrid. Transactional + marketing emails in one self-hostable platform at $0.001/email.',
url: 'https://www.useplunk.com/vs',
images: [{url: 'https://www.useplunk.com/assets/card.png', alt: 'Plunk vs Competitors'}],
images: [{url: 'https://www.useplunk.com/api/og?title=Best+Email+Platform+Alternatives&tag=Comparison', alt: 'Plunk vs Competitors', width: 1200, height: 630}],
}}
/>
+1 -1
View File
@@ -61,7 +61,7 @@ export default function KlaviyoComparison() {
description:
"E-commerce email automation without Klaviyo's price tag. Pay-as-you-go pricing, open-source, self-hostable.",
url: 'https://www.useplunk.com/vs/klaviyo',
images: [{url: 'https://www.useplunk.com/assets/card.png', alt: 'Plunk vs Klaviyo'}],
images: [{url: 'https://www.useplunk.com/api/og?title=Klaviyo+Alternative%3A+Affordable+E-commerce+Email&tag=Comparison', alt: 'Plunk vs Klaviyo', width: 1200, height: 630}],
}}
/>
+1 -1
View File
@@ -63,7 +63,7 @@ export default function LoopsComparison() {
description:
'Need more control than Loops offers? Plunk is open-source and self-hostable with pay-as-you-go pricing and no contact limits.',
url: 'https://www.useplunk.com/vs/loops',
images: [{url: 'https://www.useplunk.com/assets/card.png', alt: 'Plunk vs Loops'}],
images: [{url: 'https://www.useplunk.com/api/og?title=Loops+Alternative%3A+Open+Source+%26+Self-Hostable&tag=Comparison', alt: 'Plunk vs Loops', width: 1200, height: 630}],
}}
/>
+1 -1
View File
@@ -64,7 +64,7 @@ export default function MailchimpComparison() {
description:
'Plunk is Mailchimp for developers: code-first email platform with modern API. Pay per email, not per contact.',
url: 'https://www.useplunk.com/vs/mailchimp',
images: [{url: 'https://www.useplunk.com/assets/card.png', alt: 'Plunk vs Mailchimp'}],
images: [{url: 'https://www.useplunk.com/api/og?title=Mailchimp+Alternative+for+Developers&tag=Comparison', alt: 'Plunk vs Mailchimp', width: 1200, height: 630}],
}}
/>
+1 -1
View File
@@ -61,7 +61,7 @@ export default function MailerliteComparison() {
description:
'The truly developer-first MailerLite alternative. Open-source, self-hostable, pay per email not per subscriber.',
url: 'https://www.useplunk.com/vs/mailerlite',
images: [{url: 'https://www.useplunk.com/assets/card.png', alt: 'Plunk vs MailerLite'}],
images: [{url: 'https://www.useplunk.com/api/og?title=MailerLite+Alternative%3A+Open-Source+%26+Self-Hostable&tag=Comparison', alt: 'Plunk vs MailerLite', width: 1200, height: 630}],
}}
/>
+1 -1
View File
@@ -59,7 +59,7 @@ export default function MailgunComparison() {
description:
'Switching from Mailgun? Plunk is open-source with pay-as-you-go pricing, marketing campaigns, and workflow automation — no subscription tiers.',
url: 'https://www.useplunk.com/vs/mailgun',
images: [{url: 'https://www.useplunk.com/assets/card.png', alt: 'Plunk vs Mailgun'}],
images: [{url: 'https://www.useplunk.com/api/og?title=Mailgun+Alternative%3A+Open-Source+with+Marketing+%26+Automation&tag=Comparison', alt: 'Plunk vs Mailgun', width: 1200, height: 630}],
}}
/>
+1 -1
View File
@@ -60,7 +60,7 @@ export default function PostmarkComparison() {
description:
'Postmark starts at $15/month for 10k emails. Plunk charges $0.001/email — no subscription, no minimums. Plus marketing campaigns and open-source code.',
url: 'https://www.useplunk.com/vs/postmark',
images: [{url: 'https://www.useplunk.com/assets/card.png', alt: 'Plunk vs Postmark'}],
images: [{url: 'https://www.useplunk.com/api/og?title=Postmark+Alternative%3A+Pay-As-You-Go+vs+Fixed+Pricing&tag=Comparison', alt: 'Plunk vs Postmark', width: 1200, height: 630}],
}}
/>
+1 -1
View File
@@ -59,7 +59,7 @@ export default function ResendComparison() {
description:
'Resend uses fixed subscription pricing. Plunk is pay-as-you-go at $0.001/email — open-source, self-hostable, with marketing and automation included.',
url: 'https://www.useplunk.com/vs/resend',
images: [{url: 'https://www.useplunk.com/assets/card.png', alt: 'Plunk vs Resend'}],
images: [{url: 'https://www.useplunk.com/api/og?title=Open-Source+Resend+Alternative&tag=Comparison', alt: 'Plunk vs Resend', width: 1200, height: 630}],
}}
/>
+1 -1
View File
@@ -63,7 +63,7 @@ export default function SendGridComparison() {
description:
"Tired of SendGrid's pricing tiers and complexity? Plunk is open-source with pay-as-you-go pricing at $0.001/email and no lock-in.",
url: 'https://www.useplunk.com/vs/sendgrid',
images: [{url: 'https://www.useplunk.com/assets/card.png', alt: 'Plunk vs SendGrid'}],
images: [{url: 'https://www.useplunk.com/api/og?title=SendGrid+Alternative%3A+Open+Source+%26+Pay-As-You-Go&tag=Comparison', alt: 'Plunk vs SendGrid', width: 1200, height: 630}],
}}
/>
Binary file not shown.
Binary file not shown.
+2 -2
View File
@@ -21,7 +21,7 @@ function Document({locale}: {locale: string}) {
property="og:description"
content="Manage your email campaigns, contacts, and analytics with Plunk - the open-source email platform."
/>
<meta property="og:image" content="/assets/card.png" />
<meta property="og:image" content="https://next-app.useplunk.com/api/og?title=Email%20Platform%20Dashboard" />
{/* Twitter */}
<meta property="twitter:card" content="summary_large_image" />
@@ -30,7 +30,7 @@ function Document({locale}: {locale: string}) {
property="twitter:description"
content="Manage your email campaigns, contacts, and analytics with Plunk - the open-source email platform."
/>
<meta property="twitter:image" content="/assets/card.png" />
<meta property="twitter:image" content="https://next-app.useplunk.com/api/og?title=Email%20Platform%20Dashboard" />
{/* Fonts */}
<link rel="preconnect" href="https://fonts.googleapis.com" />
+127
View File
@@ -0,0 +1,127 @@
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) || 'Email Platform Dashboard';
const description = (req.query.description 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(
(
<div
style={{
width: '1200px',
height: '630px',
display: 'flex',
flexDirection: 'column',
justifyContent: 'space-between',
padding: '64px 72px',
backgroundColor: '#ffffff',
fontFamily: '"Bricolage Grotesque"',
position: 'relative',
overflow: 'hidden',
}}
>
{/* Ghost P watermark */}
<div
style={{
position: 'absolute',
bottom: '-60px',
right: '-30px',
opacity: 0.035,
display: 'flex',
}}
>
<svg width="480" height="480" viewBox="0 0 1080 1080" fill="none">
<path d={PLUNK_P_PATH} fill="#000000" />
</svg>
</div>
{/* Top row: logo + URL */}
<div style={{display: 'flex', justifyContent: 'space-between', alignItems: 'center'}}>
<div style={{display: 'flex', alignItems: 'center', gap: '10px'}}>
<svg width="30" height="30" viewBox="0 0 1080 1080" fill="none">
<path d={PLUNK_P_PATH} fill="#171717" />
</svg>
<span
style={{
fontSize: '22px',
fontWeight: 800,
color: '#171717',
letterSpacing: '-0.03em',
}}
>
Plunk
</span>
</div>
<span style={{fontSize: '15px', color: '#a3a3a3', letterSpacing: '0.01em'}}>
next-app.useplunk.com
</span>
</div>
{/* Main content */}
<div style={{display: 'flex', flexDirection: 'column', gap: '18px', maxWidth: '920px'}}>
<div
style={{
fontSize: `${fontSize}px`,
fontWeight: 800,
color: '#0a0a0a',
lineHeight: 1.08,
letterSpacing: '-0.04em',
}}
>
{title}
</div>
{description ? (
<div
style={{
fontSize: '22px',
color: '#737373',
lineHeight: 1.5,
fontWeight: 400,
letterSpacing: '-0.01em',
}}
>
{description}
</div>
) : null}
</div>
{/* Bottom spacer */}
<div style={{display: 'flex'}} />
</div>
),
{
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));
});
}
+13
View File
@@ -48,8 +48,21 @@ export async function generateMetadata(props: {params: Promise<{slug?: string[]}
const page = source.getPage(params.slug);
if (!page) notFound();
const ogUrl = new URL('https://docs.useplunk.com/api/og');
ogUrl.searchParams.set('title', page.data.title);
if (page.data.description) {
ogUrl.searchParams.set('description', page.data.description);
}
return {
title: page.data.title,
description: page.data.description,
openGraph: {
images: [{url: ogUrl.toString(), width: 1200, height: 630}],
},
twitter: {
card: 'summary_large_image',
images: [ogUrl.toString()],
},
};
}
+138
View File
@@ -0,0 +1,138 @@
import {readFileSync} from 'fs';
import {join} from 'path';
import {ImageResponse} from 'next/og';
import type {NextRequest} from 'next/server';
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 async function GET(req: NextRequest) {
const {searchParams} = new URL(req.url);
const title = searchParams.get('title') || 'Documentation';
const description = searchParams.get('description') || '';
const fontData = loadFont();
const titleLength = title.length;
const fontSize = titleLength < 35 ? 72 : titleLength < 55 ? 60 : titleLength < 75 ? 50 : 42;
return new ImageResponse(
(
<div
style={{
width: '1200px',
height: '630px',
display: 'flex',
flexDirection: 'column',
justifyContent: 'space-between',
padding: '64px 72px',
backgroundColor: '#ffffff',
fontFamily: '"Bricolage Grotesque"',
position: 'relative',
overflow: 'hidden',
}}
>
{/* Ghost P watermark */}
<div
style={{
position: 'absolute',
bottom: '-60px',
right: '-30px',
opacity: 0.035,
display: 'flex',
}}
>
<svg width="480" height="480" viewBox="0 0 1080 1080" fill="none">
<path d={PLUNK_P_PATH} fill="#000000" />
</svg>
</div>
{/* Top row: logo + section tag */}
<div style={{display: 'flex', justifyContent: 'space-between', alignItems: 'center'}}>
<div style={{display: 'flex', alignItems: 'center', gap: '10px'}}>
<svg width="30" height="30" viewBox="0 0 1080 1080" fill="none">
<path d={PLUNK_P_PATH} fill="#171717" />
</svg>
<span
style={{
fontSize: '22px',
fontWeight: 800,
color: '#171717',
letterSpacing: '-0.03em',
}}
>
Plunk
</span>
</div>
<div
style={{
display: 'flex',
alignItems: 'center',
padding: '6px 14px',
backgroundColor: '#f4f4f5',
borderRadius: '6px',
fontSize: '15px',
color: '#52525b',
fontWeight: 600,
letterSpacing: '-0.01em',
}}
>
Documentation
</div>
</div>
{/* Main content */}
<div style={{display: 'flex', flexDirection: 'column', gap: '18px', maxWidth: '920px'}}>
<div
style={{
fontSize: `${fontSize}px`,
fontWeight: 800,
color: '#0a0a0a',
lineHeight: 1.08,
letterSpacing: '-0.04em',
}}
>
{title}
</div>
{description ? (
<div
style={{
fontSize: '22px',
color: '#737373',
lineHeight: 1.5,
fontWeight: 400,
letterSpacing: '-0.01em',
}}
>
{description}
</div>
) : null}
</div>
{/* Bottom: URL */}
<div style={{display: 'flex', justifyContent: 'flex-end'}}>
<span style={{fontSize: '15px', color: '#a3a3a3', letterSpacing: '0.01em'}}>
docs.useplunk.com
</span>
</div>
</div>
),
{
width: 1200,
height: 630,
fonts: [
{
name: 'Bricolage Grotesque',
data: fontData,
style: 'normal',
weight: 800,
},
],
},
);
}
+2 -2
View File
@@ -32,7 +32,7 @@ export default function Layout({children}: {children: ReactNode}) {
property="og:description"
content="Documentation for Plunk, the open-source email platform. Learn how to integrate Plunk into your application and manage your email communications."
/>
<meta property="og:image" content="https://docs.useplunk.com/assets/card.png" />
<meta property="og:image" content="https://docs.useplunk.com/api/og?title=Documentation" />
{/* Twitter */}
<meta property="twitter:card" content="summary_large_image" />
@@ -42,7 +42,7 @@ export default function Layout({children}: {children: ReactNode}) {
property="twitter:description"
content="Documentation for Plunk, the open-source email platform. Learn how to integrate Plunk into your application and manage your email communications."
/>
<meta property="twitter:image" content="https://docs.useplunk.com/assets/card.png" />
<meta property="twitter:image" content="https://docs.useplunk.com/api/og?title=Documentation" />
{/* Fonts */}
<link rel="preconnect" href="https://fonts.googleapis.com" />
Binary file not shown.
Binary file not shown.