Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
287 changes: 179 additions & 108 deletions app/api/og.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,34 @@
import appsData from '../src/data/apps/apps.json';
import postsData from '../src/data/posts/posts.json';

export const config = {
runtime: 'edge',
};

// Default OG tags
// Types
type PathParts = {
countryId: string;
section?: string;
slug?: string;
};

type OgMetadata = {
title: string;
description: string;
image: string;
type: 'article' | 'website';
};

// Constants
const BASE_URL = 'https://policyengine.org';

const DEFAULT_OG = {
title: 'PolicyEngine',
description:
'Free, open-source tools to understand tax and benefit policies. Calculate your taxes and benefits, or analyze policy reforms.',
image: 'https://policyengine.org/assets/logos/policyengine/teal.png',
};

// Static page OG configs
const STATIC_PAGES: Record<string, { title: string; description: string }> = {
research: {
title: 'Research',
Expand All @@ -31,22 +49,33 @@ const STATIC_PAGES: Record<string, { title: string; description: string }> = {
},
};

// Generate HTML with Open Graph meta tags
function generateOgHtml(
title: string,
description: string,
image: string,
url: string,
type: string = 'article'
): string {
const siteName = 'PolicyEngine';
const twitterHandle = '@ThePolicyEngine';
// Helper functions
function parsePathParts(pathname: string): PathParts | null {
const parts = pathname.split('/').filter(Boolean);
if (parts.length < 1) {
return null;
}

return {
countryId: parts[0],
section: parts[1],
slug: parts[2],
};
}

const escapeHtml = (str: string) =>
str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
function escapeHtml(str: string): string {
return str
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
}

const safeTitle = escapeHtml(title);
const safeDescription = escapeHtml(description);
function generateOgHtml(metadata: OgMetadata, url: string): string {
const siteName = 'PolicyEngine';
const twitterHandle = '@ThePolicyEngine';
const safeTitle = escapeHtml(metadata.title);
const safeDescription = escapeHtml(metadata.description);

return `<!DOCTYPE html>
<html lang="en">
Expand All @@ -57,15 +86,15 @@ function generateOgHtml(
<meta name="description" content="${safeDescription}" />
<meta property="og:title" content="${safeTitle}" />
<meta property="og:description" content="${safeDescription}" />
<meta property="og:image" content="${image}" />
<meta property="og:image" content="${metadata.image}" />
<meta property="og:url" content="${url}" />
<meta property="og:type" content="${type}" />
<meta property="og:type" content="${metadata.type}" />
<meta property="og:site_name" content="${siteName}" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:site" content="${twitterHandle}" />
<meta name="twitter:title" content="${safeTitle}" />
<meta name="twitter:description" content="${safeDescription}" />
<meta name="twitter:image" content="${image}" />
<meta name="twitter:image" content="${metadata.image}" />
</head>
<body>
<h1>${safeTitle}</h1>
Expand All @@ -75,102 +104,144 @@ function generateOgHtml(
</html>`;
}

export default async function handler(request: Request) {
const url = new URL(request.url);
const pathname = url.searchParams.get('path') || '/';
const pathParts = pathname.split('/').filter(Boolean);

const baseUrl = 'https://policyengine.org';
const fullUrl = `${baseUrl}${pathname}`;

let html: string;

if (pathParts.length < 1) {
html = generateOgHtml(
DEFAULT_OG.title,
DEFAULT_OG.description,
DEFAULT_OG.image,
baseUrl,
'website'
);
return new Response(html, {
status: 200,
headers: { 'Content-Type': 'text/html', 'Cache-Control': 'public, max-age=3600' },
});
function createOgResponse(html: string): Response {
return new Response(html, {
status: 200,
headers: { 'Content-Type': 'text/html', 'Cache-Control': 'public, max-age=3600' },
});
}

function getImageUrl(imageName: string | undefined): string {
return imageName ? `${BASE_URL}/assets/posts/${imageName}` : DEFAULT_OG.image;
}

// Content handlers
function findPostBySlug(slug: string): any {
return postsData.find((p: { filename: string }) => {
const filenameWithoutExt = p.filename.substring(0, p.filename.indexOf('.'));
return filenameWithoutExt.toLowerCase().replace(/_/g, '-') === slug;
});
}

function findAppBySlugAndCountry(slug: string, countryId: string): any {
return appsData.find(
(a: { slug: string; countryId: string }) => a.slug === slug && a.countryId === countryId
);
}

function handleBlogPost(parts: PathParts, fullUrl: string): Response | null {
if (parts.section !== 'research' || !parts.slug) {
return null;
}

const countryId = pathParts[0];
const section = pathParts[1];
const slug = pathParts[2];

// Blog post: /:countryId/research/:slug
if (section === 'research' && slug) {
try {
const postsResponse = await fetch(`${baseUrl}/data/posts.json`);
if (postsResponse.ok) {
const posts = await postsResponse.json();
const post = posts.find((p: { filename: string }) => {
const filenameWithoutExt = p.filename.substring(0, p.filename.indexOf('.'));
return filenameWithoutExt.toLowerCase().replace(/_/g, '-') === slug;
});

if (post) {
const imageUrl = post.image ? `${baseUrl}/assets/posts/${post.image}` : DEFAULT_OG.image;
html = generateOgHtml(post.title, post.description, imageUrl, fullUrl, 'article');
return new Response(html, {
status: 200,
headers: { 'Content-Type': 'text/html', 'Cache-Control': 'public, max-age=3600' },
});
}
}
} catch {
// Fall through to default
}
const post = findPostBySlug(parts.slug);
if (!post) {
return null;
}

// Static pages
const staticPage = STATIC_PAGES[section];
if (staticPage && !slug) {
html = generateOgHtml(
staticPage.title,
staticPage.description,
DEFAULT_OG.image,
fullUrl,
'website'
);
return new Response(html, {
status: 200,
headers: { 'Content-Type': 'text/html', 'Cache-Control': 'public, max-age=3600' },
});
const metadata: OgMetadata = {
title: post.title,
description: post.description,
image: getImageUrl(post.image),
type: 'article',
};

return createOgResponse(generateOgHtml(metadata, fullUrl));
}

function handleApp(parts: PathParts, fullUrl: string): Response | null {
if (!parts.section || parts.slug) {
return null;
}
if (STATIC_PAGES[parts.section]) {
return null;
}

const app = findAppBySlugAndCountry(parts.section, parts.countryId);
if (!app) {
return null;
}

const metadata: OgMetadata = {
title: app.title,
description: app.description,
image: getImageUrl(app.image),
type: 'website',
};

return createOgResponse(generateOgHtml(metadata, fullUrl));
}

function handleStaticPage(parts: PathParts, fullUrl: string): Response | null {
if (!parts.section || parts.slug) {
return null;
}

const staticPage = STATIC_PAGES[parts.section];
if (!staticPage) {
return null;
}

// Country homepage
if (pathParts.length === 1) {
const countryName =
countryId === 'uk' ? 'UK' : countryId === 'us' ? 'US' : countryId.toUpperCase();
html = generateOgHtml(
`PolicyEngine ${countryName}`,
DEFAULT_OG.description,
DEFAULT_OG.image,
fullUrl,
'website'
);
return new Response(html, {
status: 200,
headers: { 'Content-Type': 'text/html', 'Cache-Control': 'public, max-age=3600' },
});
const metadata: OgMetadata = {
title: staticPage.title,
description: staticPage.description,
image: DEFAULT_OG.image,
type: 'website',
};

return createOgResponse(generateOgHtml(metadata, fullUrl));
}

function handleCountryHomepage(parts: PathParts, fullUrl: string): Response | null {
if (parts.section) {
return null;
}

// Default fallback
html = generateOgHtml(
DEFAULT_OG.title,
DEFAULT_OG.description,
DEFAULT_OG.image,
fullUrl,
'website'
const countryName =
parts.countryId === 'uk'
? 'UK'
: parts.countryId === 'us'
? 'US'
: parts.countryId.toUpperCase();

const metadata: OgMetadata = {
title: `PolicyEngine ${countryName}`,
description: DEFAULT_OG.description,
image: DEFAULT_OG.image,
type: 'website',
};

return createOgResponse(generateOgHtml(metadata, fullUrl));
}

function handleDefault(fullUrl: string): Response {
const metadata: OgMetadata = {
title: DEFAULT_OG.title,
description: DEFAULT_OG.description,
image: DEFAULT_OG.image,
type: 'website',
};

return createOgResponse(generateOgHtml(metadata, fullUrl));
}

// Main handler
export default async function handler(request: Request) {
const url = new URL(request.url);
const pathname = url.searchParams.get('path') || '/';
const parts = parsePathParts(pathname);
const fullUrl = `${BASE_URL}${pathname}`;

if (!parts) {
return handleDefault(fullUrl);
}

// Try each handler in order
return (
handleBlogPost(parts, fullUrl) ||
handleApp(parts, fullUrl) ||
handleStaticPage(parts, fullUrl) ||
handleCountryHomepage(parts, fullUrl) ||
handleDefault(fullUrl)
);
return new Response(html, {
status: 200,
headers: { 'Content-Type': 'text/html', 'Cache-Control': 'public, max-age=3600' },
});
}
Loading
Loading