Skip to content

Commit bf90ca5

Browse files
committed
og images fixes with a fallback
1 parent 1bf9519 commit bf90ca5

File tree

5 files changed

+640
-25
lines changed

5 files changed

+640
-25
lines changed

scripts/public/rss.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
<link>https://piyushmehta.com</link>
66
<description>Articles and tutorials on React.js, web development, and software engineering by Piyush Mehta.</description>
77
<language>en-us</language>
8-
<lastBuildDate>Wed, 27 Aug 2025 16:27:26 GMT</lastBuildDate>
8+
<lastBuildDate>Wed, 27 Aug 2025 16:34:35 GMT</lastBuildDate>
99
<generator>Static RSS Generator</generator>
1010
<copyright>Copyright 2025 Piyush Mehta. All rights reserved.</copyright>
1111
<managingEditor>hello@piyushmehta.com (Piyush Mehta)</managingEditor>

src/components/SEO.astro

Lines changed: 42 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ generateTwitterImageUrl,
99
optimizeKeywords,
1010
sanitizeDescription,
1111
validateImageForSocialSharing
12-
} from '../utils/seo-optimization';
12+
} from '../utils/og-generator';
1313
1414
export interface Props {
1515
title: string;
@@ -54,20 +54,47 @@ const baseUrl = Astro.url.origin || Astro.site?.toString() || 'https://piyushmeh
5454
const optimizedDescription = sanitizeDescription(description);
5555
const canonicalUrl = canonical || generateCanonicalUrl(Astro.url.pathname, baseUrl);
5656
57-
// Handle image metadata with full OG Protocol compliance and generated image fallback
58-
const imageMetadata: ImageMetadata = extractImageMetadata(
59-
image,
60-
baseUrl,
61-
{
62-
title,
63-
description: optimizedDescription,
64-
type,
65-
publishedTime,
66-
tags,
67-
template: ogTemplate,
68-
theme: ogTheme,
69-
}
70-
);
57+
// Handle image metadata with full OG Protocol compliance
58+
// Prioritize existing images over generated ones for better social media compatibility
59+
let imageMetadata: ImageMetadata;
60+
61+
if (image && typeof image === 'string') {
62+
// Use the provided image URL directly
63+
imageMetadata = {
64+
url: image.startsWith('http') ? image : `${baseUrl}${image}`,
65+
secureUrl: image.startsWith('http') ? image : `${baseUrl}${image}`,
66+
type: 'image/jpeg',
67+
width: 1200,
68+
height: 630,
69+
alt: `${title} - ${author}`,
70+
};
71+
} else if (image && typeof image === 'object' && image.url) {
72+
// Use the provided image object
73+
const imageUrl = image.url.startsWith('http') ? image.url : `${baseUrl}${image.url}`;
74+
imageMetadata = {
75+
url: imageUrl,
76+
secureUrl: imageUrl,
77+
alt: image.alt || `${title} - ${author}`,
78+
width: image.width || 1200,
79+
height: image.height || 630,
80+
type: image.type || 'image/jpeg',
81+
};
82+
} else {
83+
// Fallback to generated OG image only when no image is provided
84+
imageMetadata = extractImageMetadata(
85+
null, // No image provided
86+
baseUrl,
87+
{
88+
title,
89+
description: optimizedDescription,
90+
type,
91+
publishedTime,
92+
tags,
93+
template: ogTemplate,
94+
theme: ogTheme,
95+
}
96+
);
97+
}
7198
7299
// Generate Twitter-optimized image URL
73100
const twitterImageUrl = generateTwitterImageUrl({

src/pages/api/og-enhanced.ts

Lines changed: 79 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,24 @@ import satori from "satori";
77

88
export const prerender = false;
99

10-
export const GET: APIRoute = async ({ url }): Promise<Response> => {
10+
11+
12+
export const GET: APIRoute = async ({ url, request }): Promise<Response> => {
13+
const searchParams = new URL(url).searchParams;
14+
const title = searchParams.get('title') || 'Piyush Mehta';
15+
const description = searchParams.get('description') || 'Software Engineer & Tech Speaker';
16+
const template = searchParams.get('template') || 'modern';
17+
1118
try {
12-
const searchParams = new URL(url).searchParams;
13-
const title = searchParams.get('title') || 'Piyush Mehta';
14-
const description = searchParams.get('description') || 'Software Engineer & Tech Speaker';
15-
const template = searchParams.get('template') || 'modern';
19+
20+
// Check if this is a social media crawler
21+
const userAgent = request.headers.get('user-agent') || '';
22+
const isSocialCrawler = /facebook|twitter|linkedin|whatsapp|telegram|discord|slack/i.test(userAgent);
23+
24+
// For social media crawlers, add extra delay to ensure proper rendering
25+
if (isSocialCrawler) {
26+
await new Promise(resolve => setTimeout(resolve, 100));
27+
}
1628

1729
// Load fonts
1830
const fontPath = join(process.cwd(), "InterVariable.ttf");
@@ -257,15 +269,75 @@ export const GET: APIRoute = async ({ url }): Promise<Response> => {
257269
const pngData = resvg.render();
258270
const pngBuffer = pngData.asPng();
259271

272+
// Different caching strategies for social crawlers vs regular users
273+
const cacheControl = isSocialCrawler
274+
? 'public, max-age=86400, s-maxage=86400' // 24 hours for crawlers
275+
: 'public, max-age=31536000, immutable'; // 1 year for regular users
276+
260277
return new Response(new Uint8Array(pngBuffer), {
261278
headers: {
262279
'Content-Type': 'image/png',
263280
'Content-Length': pngBuffer.length.toString(),
264-
'Cache-Control': 'public, max-age=31536000, immutable',
281+
'Cache-Control': cacheControl,
282+
'Access-Control-Allow-Origin': '*',
283+
'Access-Control-Allow-Methods': 'GET',
284+
'Access-Control-Allow-Headers': 'Content-Type',
285+
'X-Robots-Tag': 'noindex',
286+
// Additional headers for social media crawlers
287+
...(isSocialCrawler && {
288+
'X-Crawler-Friendly': 'true',
289+
'Link': `<${url}>; rel="canonical"`,
290+
}),
265291
},
266292
});
267293
} catch (error) {
268294
console.error('Error generating OG image:', error);
269-
return new Response('Error generating OG image', { status: 500 });
295+
296+
// Return a fallback static image for social media crawlers
297+
try {
298+
const fallbackImagePath = join(process.cwd(), "public/images/social.jpg");
299+
const fallbackImage = fs.readFileSync(fallbackImagePath);
300+
301+
return new Response(fallbackImage, {
302+
headers: {
303+
'Content-Type': 'image/jpeg',
304+
'Content-Length': fallbackImage.length.toString(),
305+
'Cache-Control': 'public, max-age=86400',
306+
'Access-Control-Allow-Origin': '*',
307+
'Access-Control-Allow-Methods': 'GET',
308+
'Access-Control-Allow-Headers': 'Content-Type',
309+
'X-Fallback-Image': 'true',
310+
},
311+
});
312+
} catch (fallbackError) {
313+
console.error('Fallback image error:', fallbackError);
314+
315+
// Create a simple fallback SVG image as last resort
316+
const fallbackSvg = `
317+
<svg width="1200" height="630" xmlns="http://www.w3.org/2000/svg">
318+
<rect width="1200" height="630" fill="#1f2347"/>
319+
<text x="600" y="300" text-anchor="middle" fill="#f9fafb" font-family="Arial" font-size="48" font-weight="bold">
320+
${title ? title.substring(0, 50) + (title.length > 50 ? '...' : '') : 'Piyush Mehta'}
321+
</text>
322+
<text x="600" y="360" text-anchor="middle" fill="#d1d5db" font-family="Arial" font-size="24">
323+
piyushmehta.com
324+
</text>
325+
</svg>
326+
`;
327+
328+
const resvg = new Resvg(fallbackSvg);
329+
const pngData = resvg.render();
330+
const pngBuffer = pngData.asPng();
331+
332+
return new Response(new Uint8Array(pngBuffer), {
333+
headers: {
334+
'Content-Type': 'image/png',
335+
'Content-Length': pngBuffer.length.toString(),
336+
'Cache-Control': 'public, max-age=3600', // 1 hour cache for error fallbacks
337+
'Access-Control-Allow-Origin': '*',
338+
'X-Emergency-Fallback': 'true',
339+
},
340+
});
341+
}
270342
}
271343
};

0 commit comments

Comments
 (0)