seo
This commit is contained in:
@@ -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">
|
||||
|
||||
72
src/pages/api/og/[...slug].svg.ts
Normal file
72
src/pages/api/og/[...slug].svg.ts
Normal 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',
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user