This commit is contained in:
2026-01-14 17:40:22 +01:00
parent a9b2c89636
commit 5247cdc5e9
8 changed files with 489 additions and 9 deletions

View File

@@ -22,9 +22,19 @@ import 'prismjs/components/prism-markdown';
interface Props {
title: string;
description?: string;
image?: string;
keywords?: string[];
canonicalUrl?: string;
}
const { title, description = "Technical problem solver's blog - practical insights and learning notes" } = Astro.props;
const { title, description = "Technical problem solver's blog - practical insights and learning notes", image, keywords, canonicalUrl } = Astro.props;
const siteUrl = 'https://mintel.me';
const currentUrl = canonicalUrl || (new URL(Astro.request.url)).pathname;
const fullUrl = `${siteUrl}${currentUrl}`;
const slug = currentUrl.split('/').filter(Boolean).pop() || 'home';
const ogImage = image || `${siteUrl}/api/og/${slug}.svg`;
const twitterImage = image || `${siteUrl}/api/og/${slug}.svg`;
const keywordsString = keywords ? keywords.join(', ') : '';
---
<!DOCTYPE html>
@@ -34,7 +44,24 @@ const { title, description = "Technical problem solver's blog - practical insigh
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{title} | Marc Mintel</title>
<meta name="description" content={description} />
{keywordsString && <meta name="keywords" content={keywordsString} />}
<meta name="generator" content={Astro.generator} />
<link rel="canonical" href={fullUrl} />
<!-- Open Graph / Facebook -->
<meta property="og:type" content="website" />
<meta property="og:url" content={fullUrl} />
<meta property="og:title" content={`${title} | Marc Mintel`} />
<meta property="og:description" content={description} />
<meta property="og:image" content={ogImage} />
<meta property="og:site_name" content="Marc Mintel" />
<!-- Twitter -->
<meta property="twitter:card" content="summary_large_image" />
<meta property="twitter:url" content={fullUrl} />
<meta property="twitter:title" content={`${title} | Marc Mintel`} />
<meta property="twitter:description" content={description} />
<meta property="twitter:image" content={twitterImage} />
<!-- Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">

View File

@@ -0,0 +1,72 @@
import type { APIRoute } from 'astro';
import { blogPosts } from '../../../data/blogPosts';
export async function getStaticPaths() {
const paths = blogPosts.map(post => ({
params: { slug: post.slug }
}));
// Add home page
paths.push({ params: { slug: 'home' } });
return paths;
}
export const GET: APIRoute = async ({ params }) => {
const slug = params.slug;
let title: string;
let description: string;
// Handle home page
if (slug === 'home') {
title = 'Marc Mintel';
description = 'Technical problem solver\'s blog - practical insights and learning notes';
} else {
// Find the blog post
const post = blogPosts.find(p => p.slug === slug);
// Default content if no post found
title = (post?.title || 'Marc Mintel').replace(/[<>&'"]/g, '');
description = (post?.description || 'Technical problem solver\'s blog - practical insights and learning notes').slice(0, 100).replace(/[<>&'"]/g, '');
}
// Create SVG with typographic design matching the site
const svg = `<svg width="1200" height="630" xmlns="http://www.w3.org/2000/svg">
<defs>
<style>
.title { font-family: system-ui, -apple-system, sans-serif; font-size: 48px; font-weight: 700; fill: #1e293b; letter-spacing: -0.025em; }
.description { font-family: system-ui, -apple-system, sans-serif; font-size: 24px; font-weight: 400; fill: #64748b; }
.branding { font-family: system-ui, -apple-system, sans-serif; font-size: 18px; font-weight: 500; fill: #94a3b8; }
</style>
</defs>
<!-- Background -->
<rect width="1200" height="630" fill="#ffffff"/>
<!-- Title -->
<text x="60" y="200" class="title">
${title}
</text>
<!-- Description -->
<text x="60" y="280" class="description">
${description}
</text>
<!-- Site branding -->
<text x="60" y="580" class="branding">
mintel.me
</text>
<!-- Decorative accent -->
<rect x="1000" y="60" width="120" height="4" fill="#3b82f6" rx="2"/>
</svg>`;
return new Response(svg, {
headers: {
'Content-Type': 'image/svg+xml',
'Cache-Control': 'public, max-age=31536000, immutable',
},
});
};

View File

@@ -33,7 +33,12 @@ const showFileExamples = post.tags?.some(tag =>
);
---
<BaseLayout title={post.title} description={post.description}>
<BaseLayout
title={post.title}
description={post.description}
keywords={post.tags}
canonicalUrl={`/blog/${post.slug}`}
/>
<!-- Top navigation bar with back button and clap counter -->
<nav class="fixed top-0 left-0 right-0 z-40 bg-white/80 backdrop-blur-md border-b border-slate-200 transition-all duration-300" id="top-nav">
<div class="max-w-4xl mx-auto px-6 py-4 flex items-center justify-between">
@@ -266,6 +271,33 @@ const showFileExamples = post.tags?.some(tag =>
</section>
</main>
<!-- Structured Data for SEO -->
<script type="application/ld+json" set:html={JSON.stringify({
"@context": "https://schema.org",
"@type": "BlogPosting",
"headline": post.title,
"description": post.description,
"author": {
"@type": "Person",
"name": "Marc Mintel",
"url": "https://mintel.me"
},
"publisher": {
"@type": "Person",
"name": "Marc Mintel",
"url": "https://mintel.me"
},
"datePublished": post.date,
"dateModified": post.date,
"mainEntityOfPage": {
"@type": "WebPage",
"@id": `https://mintel.me/blog/${post.slug}`
},
"url": `https://mintel.me/blog/${post.slug}`,
"keywords": post.tags.join(", "),
"articleSection": post.tags[0] || "Technology"
})} />
<script>
// Reading progress bar with smooth gradient
function updateReadingProgress() {