- Slugify tag/category static params so pre-rendered pages match links; disable dynamic params and return a real 404 via notFound() - Drop bogus generateStaticParams from the tags/categories index pages - Add a styled not-found page - Sort 'Recently updated' by last modification before slicing - Atom feed: correct rel=self URL, use updated dates, valid content type - Swap no-js -> js class in an inline head script before first paint - Markdown links: only route internal paths through next/link and tolerate missing hrefs Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
68 lines
2 KiB
TypeScript
68 lines
2 KiB
TypeScript
import { metadata } from '@/app/metadata'
|
|
import { getAllPostsMeta } from '@/lib/posts'
|
|
import type { PostMeta } from '@/types/Post.type'
|
|
|
|
export const dynamic = 'force-static'
|
|
|
|
const SITE_URL = process.env.SITE_URL
|
|
|
|
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(metadata.title!.toString())}</title>
|
|
<updated>${updated}</updated>
|
|
<author>
|
|
<name>${escapeXml(metadata.creator!.toString())}</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()} Frank Borisjuk </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 },
|
|
})
|
|
}
|