- Add sitemap.ts and robots.ts - pageMetadata() helper: consistent title, canonical URL, Open Graph, Twitter card and feed alternate on every page - Generated OG images: a site card, and per post either the cover image or a branded title card (pre-rendered at build) - BlogPosting JSON-LD on post pages - Fail fast with a clear error when SITE_URL is missing Content loading: - Validate post frontmatter at build time and normalize dates to ISO (fixes invalid <time datetime> values) - Add draft: true support (drafts only render in development) - Cache markdown rendering so metadata and page share one render - Replace the three copy-pasted about/projects/setup pages with a shared MarkdownPage component
67 lines
2 KiB
TypeScript
67 lines
2 KiB
TypeScript
import { AUTHOR, SITE_NAME, SITE_URL } from '@/app/metadata'
|
|
import { getAllPostsMeta } from '@/lib/posts'
|
|
import type { PostMeta } from '@/types/Post.type'
|
|
|
|
export const dynamic = 'force-static'
|
|
|
|
const lastModified = (p: PostMeta) => new Date(p.updated ?? p.date)
|
|
|
|
function escapeXml(str: string) {
|
|
return str
|
|
.replace(/&/g, '&')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>')
|
|
.replace(/"/g, '"')
|
|
.replace(/'/g, ''')
|
|
}
|
|
|
|
export async function GET(req: Request) {
|
|
const posts = getAllPostsMeta()
|
|
|
|
const updated =
|
|
posts.length > 0
|
|
? new Date(Math.max(...posts.map((p) => lastModified(p).getTime()))).toISOString()
|
|
: new Date().toISOString()
|
|
|
|
const entries = posts
|
|
.map((p) => {
|
|
const url = `${SITE_URL}/posts/${p.slug}`
|
|
const date = new Date(p.date).toISOString()
|
|
const modified = lastModified(p).toISOString()
|
|
return `
|
|
<entry>
|
|
<title>${escapeXml(p.title)}</title>
|
|
<link href="${url}" rel="alternate" type="text/html" />
|
|
<id>${url}</id>
|
|
<published>${date}</published>
|
|
<updated>${modified}</updated>
|
|
<content type="text/html" src="${url}" />
|
|
<summary>${escapeXml(p.description)}</summary>
|
|
</entry>`
|
|
})
|
|
.join('')
|
|
|
|
const feed = `<?xml version="1.0" encoding="UTF-8"?>
|
|
<feed xmlns="http://www.w3.org/2005/Atom">
|
|
<id>${SITE_URL}/</id>
|
|
<title>${escapeXml(SITE_NAME)}</title>
|
|
<updated>${updated}</updated>
|
|
<author>
|
|
<name>${escapeXml(AUTHOR.name)}
|
|
<uri>${AUTHOR.url}</uri></name>
|
|
</author>
|
|
<link href="${SITE_URL}/feed.xml" rel="self" type="application/atom+xml" />
|
|
<link href="${SITE_URL}" rel="alternate" type="text/html" />
|
|
<rights>© ${new Date().getFullYear()} ${escapeXml(AUTHOR.name)}</rights>
|
|
${entries}
|
|
</feed>`
|
|
|
|
const accept = req.headers.get('accept') || ''
|
|
const contentType = accept.includes('application/atom+xml')
|
|
? 'application/atom+xml; charset=utf-8'
|
|
: 'application/xml; charset=utf-8'
|
|
|
|
return new Response(feed, {
|
|
headers: { 'Content-Type': contentType },
|
|
})
|
|
}
|