Add RSS feed, sitemap, robots.txt, and code syntax highlighting

Implements essential blog features:

1. RSS Feed (/feed.xml)
   - Latest 20 posts with full content
   - Proper XML escaping and CDATA sections
   - Includes tags, authors, and descriptions
   - Auto-discovery link in HTML head

2. Sitemap (/sitemap.xml)
   - All posts, pages, and tag pages
   - Proper lastModified dates and priorities
   - Automatic generation via Next.js built-in support

3. Robots.txt (/robots.txt)
   - Allow all crawlers
   - Disallow API and admin routes
   - Links to sitemap for better SEO

4. Code Syntax Highlighting
   - Using rehype-pretty-code + Shiki
   - GitHub Dark/Light themes based on user preference
   - Line numbers for all code blocks
   - Support for highlighted lines
   - Inline code styling
   - Code title support

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-20 17:59:56 +08:00
parent dd3f553282
commit f994301fbb
8 changed files with 513 additions and 0 deletions

63
app/feed.xml/route.ts Normal file
View File

@@ -0,0 +1,63 @@
import { allPosts } from 'contentlayer2/generated';
import { siteConfig } from '@/lib/config';
export async function GET() {
const sortedPosts = allPosts
.filter((post) => post.status === 'published')
.sort((a, b) => {
const dateA = a.published_at ? new Date(a.published_at).getTime() : 0;
const dateB = b.published_at ? new Date(b.published_at).getTime() : 0;
return dateB - dateA;
})
.slice(0, 20); // Latest 20 posts
const siteUrl = process.env.NEXT_PUBLIC_SITE_URL || 'http://localhost:3000';
const rss = `<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
<channel>
<title>${escapeXml(siteConfig.name)}</title>
<link>${siteUrl}</link>
<description>${escapeXml(siteConfig.description)}</description>
<language>${siteConfig.defaultLocale.replace('_', '-')}</language>
<lastBuildDate>${new Date().toUTCString()}</lastBuildDate>
<atom:link href="${siteUrl}/feed.xml" rel="self" type="application/rss+xml"/>
${sortedPosts
.map((post) => {
const postUrl = `${siteUrl}${post.url}`;
const pubDate = post.published_at
? new Date(post.published_at).toUTCString()
: new Date(post.created_at || Date.now()).toUTCString();
return `
<item>
<title>${escapeXml(post.title)}</title>
<link>${postUrl}</link>
<guid isPermaLink="true">${postUrl}</guid>
<description>${escapeXml(post.description || post.custom_excerpt || post.title)}</description>
${post.body?.html ? `<content:encoded><![CDATA[${post.body.html}]]></content:encoded>` : ''}
<pubDate>${pubDate}</pubDate>
${post.authors?.map((author) => `<author>${escapeXml(author)}</author>`).join('\n ') || ''}
${post.tags?.map((tag) => `<category>${escapeXml(tag)}</category>`).join('\n ') || ''}
</item>`;
})
.join('')}
</channel>
</rss>`;
return new Response(rss, {
headers: {
'Content-Type': 'application/xml; charset=utf-8',
'Cache-Control': 'public, max-age=3600, s-maxage=3600',
},
});
}
function escapeXml(unsafe: string): string {
return unsafe
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&apos;');
}

View File

@@ -34,6 +34,11 @@ export const metadata: Metadata = {
}, },
icons: { icons: {
icon: '/favicon.png' icon: '/favicon.png'
},
alternates: {
types: {
'application/rss+xml': `${siteConfig.url}/feed.xml`
}
} }
}; };

14
app/robots.ts Normal file
View File

@@ -0,0 +1,14 @@
import { MetadataRoute } from 'next';
export default function robots(): MetadataRoute.Robots {
const siteUrl = process.env.NEXT_PUBLIC_SITE_URL || 'http://localhost:3000';
return {
rules: {
userAgent: '*',
allow: '/',
disallow: ['/api/', '/_next/', '/admin/'],
},
sitemap: `${siteUrl}/sitemap.xml`,
};
}

68
app/sitemap.ts Normal file
View File

@@ -0,0 +1,68 @@
import { MetadataRoute } from 'next';
import { allPosts, allPages } from 'contentlayer2/generated';
export default function sitemap(): MetadataRoute.Sitemap {
const siteUrl = process.env.NEXT_PUBLIC_SITE_URL || 'http://localhost:3000';
// Homepage
const homepage = {
url: siteUrl,
lastModified: new Date(),
changeFrequency: 'daily' as const,
priority: 1,
};
// Blog listing page
const blogPage = {
url: `${siteUrl}/blog`,
lastModified: new Date(),
changeFrequency: 'daily' as const,
priority: 0.9,
};
// Tags page
const tagsPage = {
url: `${siteUrl}/tags`,
lastModified: new Date(),
changeFrequency: 'weekly' as const,
priority: 0.7,
};
// All blog posts
const posts = allPosts
.filter((post) => post.status === 'published')
.map((post) => ({
url: `${siteUrl}${post.url}`,
lastModified: new Date(post.updated_at || post.published_at || post.created_at || Date.now()),
changeFrequency: 'weekly' as const,
priority: 0.8,
}));
// All pages
const pages = allPages
.filter((page) => page.status === 'published')
.map((page) => ({
url: `${siteUrl}${page.url}`,
lastModified: new Date(page.updated_at || page.published_at || page.created_at || Date.now()),
changeFrequency: 'monthly' as const,
priority: 0.6,
}));
// All unique tags
const allTags = Array.from(
new Set(
allPosts
.filter((post) => post.status === 'published' && post.tags)
.flatMap((post) => post.tags || [])
)
);
const tagPages = allTags.map((tag) => ({
url: `${siteUrl}/tags/${encodeURIComponent(tag.toLowerCase().replace(/\s+/g, '-'))}`,
lastModified: new Date(),
changeFrequency: 'weekly' as const,
priority: 0.5,
}));
return [homepage, blogPage, tagsPage, ...posts, ...pages, ...tagPages];
}

View File

@@ -3,6 +3,7 @@ import { visit } from 'unist-util-visit';
import rehypeSlug from 'rehype-slug'; import rehypeSlug from 'rehype-slug';
import rehypeAutolinkHeadings from 'rehype-autolink-headings'; import rehypeAutolinkHeadings from 'rehype-autolink-headings';
import remarkGfm from 'remark-gfm'; import remarkGfm from 'remark-gfm';
import rehypePrettyCode from 'rehype-pretty-code';
export const Post = defineDocumentType(() => ({ export const Post = defineDocumentType(() => ({
name: 'Post', name: 'Post',
@@ -86,6 +87,16 @@ export default makeSource({
markdown: { markdown: {
remarkPlugins: [remarkGfm], remarkPlugins: [remarkGfm],
rehypePlugins: [ rehypePlugins: [
[
rehypePrettyCode,
{
theme: {
dark: 'github-dark',
light: 'github-light',
},
keepBackground: false,
},
],
rehypeSlug, rehypeSlug,
[rehypeAutolinkHeadings, { behavior: 'wrap' }], [rehypeAutolinkHeadings, { behavior: 'wrap' }],
/** /**

284
package-lock.json generated
View File

@@ -26,8 +26,10 @@
"react": "^19.2.0", "react": "^19.2.0",
"react-dom": "^19.2.0", "react-dom": "^19.2.0",
"rehype-autolink-headings": "^7.1.0", "rehype-autolink-headings": "^7.1.0",
"rehype-pretty-code": "^0.14.1",
"rehype-slug": "^6.0.0", "rehype-slug": "^6.0.0",
"remark-gfm": "^4.0.1", "remark-gfm": "^4.0.1",
"shiki": "^3.15.0",
"tailwind-merge": "^3.4.0", "tailwind-merge": "^3.4.0",
"unist-util-visit": "^5.0.0" "unist-util-visit": "^5.0.0"
}, },
@@ -2667,6 +2669,73 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@shikijs/core": {
"version": "3.15.0",
"resolved": "https://registry.npmjs.org/@shikijs/core/-/core-3.15.0.tgz",
"integrity": "sha512-8TOG6yG557q+fMsSVa8nkEDOZNTSxjbbR8l6lF2gyr6Np+jrPlslqDxQkN6rMXCECQ3isNPZAGszAfYoJOPGlg==",
"license": "MIT",
"dependencies": {
"@shikijs/types": "3.15.0",
"@shikijs/vscode-textmate": "^10.0.2",
"@types/hast": "^3.0.4",
"hast-util-to-html": "^9.0.5"
}
},
"node_modules/@shikijs/engine-javascript": {
"version": "3.15.0",
"resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-3.15.0.tgz",
"integrity": "sha512-ZedbOFpopibdLmvTz2sJPJgns8Xvyabe2QbmqMTz07kt1pTzfEvKZc5IqPVO/XFiEbbNyaOpjPBkkr1vlwS+qg==",
"license": "MIT",
"dependencies": {
"@shikijs/types": "3.15.0",
"@shikijs/vscode-textmate": "^10.0.2",
"oniguruma-to-es": "^4.3.3"
}
},
"node_modules/@shikijs/engine-oniguruma": {
"version": "3.15.0",
"resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-3.15.0.tgz",
"integrity": "sha512-HnqFsV11skAHvOArMZdLBZZApRSYS4LSztk2K3016Y9VCyZISnlYUYsL2hzlS7tPqKHvNqmI5JSUJZprXloMvA==",
"license": "MIT",
"dependencies": {
"@shikijs/types": "3.15.0",
"@shikijs/vscode-textmate": "^10.0.2"
}
},
"node_modules/@shikijs/langs": {
"version": "3.15.0",
"resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-3.15.0.tgz",
"integrity": "sha512-WpRvEFvkVvO65uKYW4Rzxs+IG0gToyM8SARQMtGGsH4GDMNZrr60qdggXrFOsdfOVssG/QQGEl3FnJ3EZ+8w8A==",
"license": "MIT",
"dependencies": {
"@shikijs/types": "3.15.0"
}
},
"node_modules/@shikijs/themes": {
"version": "3.15.0",
"resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-3.15.0.tgz",
"integrity": "sha512-8ow2zWb1IDvCKjYb0KiLNrK4offFdkfNVPXb1OZykpLCzRU6j+efkY+Y7VQjNlNFXonSw+4AOdGYtmqykDbRiQ==",
"license": "MIT",
"dependencies": {
"@shikijs/types": "3.15.0"
}
},
"node_modules/@shikijs/types": {
"version": "3.15.0",
"resolved": "https://registry.npmjs.org/@shikijs/types/-/types-3.15.0.tgz",
"integrity": "sha512-BnP+y/EQnhihgHy4oIAN+6FFtmfTekwOLsQbRw9hOKwqgNy8Bdsjq8B05oAt/ZgvIWWFrshV71ytOrlPfYjIJw==",
"license": "MIT",
"dependencies": {
"@shikijs/vscode-textmate": "^10.0.2",
"@types/hast": "^3.0.4"
}
},
"node_modules/@shikijs/vscode-textmate": {
"version": "10.0.2",
"resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-10.0.2.tgz",
"integrity": "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==",
"license": "MIT"
},
"node_modules/@shuding/opentype.js": { "node_modules/@shuding/opentype.js": {
"version": "1.4.0-beta.0", "version": "1.4.0-beta.0",
"resolved": "https://registry.npmjs.org/@shuding/opentype.js/-/opentype.js-1.4.0-beta.0.tgz", "resolved": "https://registry.npmjs.org/@shuding/opentype.js/-/opentype.js-1.4.0-beta.0.tgz",
@@ -4620,6 +4689,18 @@
"node": ">=10.0.0" "node": ">=10.0.0"
} }
}, },
"node_modules/entities": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz",
"integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=0.12"
},
"funding": {
"url": "https://github.com/fb55/entities?sponsor=1"
}
},
"node_modules/es-abstract": { "node_modules/es-abstract": {
"version": "1.24.0", "version": "1.24.0",
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.0.tgz", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.0.tgz",
@@ -6121,6 +6202,44 @@
"node": ">= 0.4" "node": ">= 0.4"
} }
}, },
"node_modules/hast-util-from-html": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/hast-util-from-html/-/hast-util-from-html-2.0.3.tgz",
"integrity": "sha512-CUSRHXyKjzHov8yKsQjGOElXy/3EKpyX56ELnkHH34vDVw1N1XSQ1ZcAvTyAPtGqLTuKP/uxM+aLkSPqF/EtMw==",
"license": "MIT",
"dependencies": {
"@types/hast": "^3.0.0",
"devlop": "^1.1.0",
"hast-util-from-parse5": "^8.0.0",
"parse5": "^7.0.0",
"vfile": "^6.0.0",
"vfile-message": "^4.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/hast-util-from-parse5": {
"version": "8.0.3",
"resolved": "https://registry.npmjs.org/hast-util-from-parse5/-/hast-util-from-parse5-8.0.3.tgz",
"integrity": "sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg==",
"license": "MIT",
"dependencies": {
"@types/hast": "^3.0.0",
"@types/unist": "^3.0.0",
"devlop": "^1.0.0",
"hastscript": "^9.0.0",
"property-information": "^7.0.0",
"vfile": "^6.0.0",
"vfile-location": "^5.0.0",
"web-namespaces": "^2.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/hast-util-heading-rank": { "node_modules/hast-util-heading-rank": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/hast-util-heading-rank/-/hast-util-heading-rank-3.0.0.tgz", "resolved": "https://registry.npmjs.org/hast-util-heading-rank/-/hast-util-heading-rank-3.0.0.tgz",
@@ -6147,6 +6266,19 @@
"url": "https://opencollective.com/unified" "url": "https://opencollective.com/unified"
} }
}, },
"node_modules/hast-util-parse-selector": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-4.0.0.tgz",
"integrity": "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==",
"license": "MIT",
"dependencies": {
"@types/hast": "^3.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/hast-util-to-estree": { "node_modules/hast-util-to-estree": {
"version": "3.1.3", "version": "3.1.3",
"resolved": "https://registry.npmjs.org/hast-util-to-estree/-/hast-util-to-estree-3.1.3.tgz", "resolved": "https://registry.npmjs.org/hast-util-to-estree/-/hast-util-to-estree-3.1.3.tgz",
@@ -6251,6 +6383,23 @@
"url": "https://opencollective.com/unified" "url": "https://opencollective.com/unified"
} }
}, },
"node_modules/hastscript": {
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/hastscript/-/hastscript-9.0.1.tgz",
"integrity": "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==",
"license": "MIT",
"dependencies": {
"@types/hast": "^3.0.0",
"comma-separated-tokens": "^2.0.0",
"hast-util-parse-selector": "^4.0.0",
"property-information": "^7.0.0",
"space-separated-tokens": "^2.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/hermes-estree": { "node_modules/hermes-estree": {
"version": "0.25.1", "version": "0.25.1",
"resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz",
@@ -8714,6 +8863,23 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/oniguruma-parser": {
"version": "0.12.1",
"resolved": "https://registry.npmjs.org/oniguruma-parser/-/oniguruma-parser-0.12.1.tgz",
"integrity": "sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w==",
"license": "MIT"
},
"node_modules/oniguruma-to-es": {
"version": "4.3.3",
"resolved": "https://registry.npmjs.org/oniguruma-to-es/-/oniguruma-to-es-4.3.3.tgz",
"integrity": "sha512-rPiZhzC3wXwE59YQMRDodUwwT9FZ9nNBwQQfsd1wfdtlKEyCdRV0avrTcSZ5xlIvGRVPd/cx6ZN45ECmS39xvg==",
"license": "MIT",
"dependencies": {
"oniguruma-parser": "^0.12.1",
"regex": "^6.0.1",
"regex-recursion": "^6.0.2"
}
},
"node_modules/oo-ascii-tree": { "node_modules/oo-ascii-tree": {
"version": "1.119.0", "version": "1.119.0",
"resolved": "https://registry.npmjs.org/oo-ascii-tree/-/oo-ascii-tree-1.119.0.tgz", "resolved": "https://registry.npmjs.org/oo-ascii-tree/-/oo-ascii-tree-1.119.0.tgz",
@@ -8870,6 +9036,24 @@
"integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/parse-numeric-range": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/parse-numeric-range/-/parse-numeric-range-1.3.0.tgz",
"integrity": "sha512-twN+njEipszzlMJd4ONUYgSfZPDxgHhT9Ahed5uTigpQn90FggW4SA/AIPq/6a149fTbE9qBEcSwE3FAEp6wQQ==",
"license": "ISC"
},
"node_modules/parse5": {
"version": "7.3.0",
"resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz",
"integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==",
"license": "MIT",
"dependencies": {
"entities": "^6.0.0"
},
"funding": {
"url": "https://github.com/inikulin/parse5?sponsor=1"
}
},
"node_modules/pascal-case": { "node_modules/pascal-case": {
"version": "3.1.2", "version": "3.1.2",
"resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz", "resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz",
@@ -9383,6 +9567,30 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/regex": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/regex/-/regex-6.0.1.tgz",
"integrity": "sha512-uorlqlzAKjKQZ5P+kTJr3eeJGSVroLKoHmquUj4zHWuR+hEyNqlXsSKlYYF5F4NI6nl7tWCs0apKJ0lmfsXAPA==",
"license": "MIT",
"dependencies": {
"regex-utilities": "^2.3.0"
}
},
"node_modules/regex-recursion": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/regex-recursion/-/regex-recursion-6.0.2.tgz",
"integrity": "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==",
"license": "MIT",
"dependencies": {
"regex-utilities": "^2.3.0"
}
},
"node_modules/regex-utilities": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/regex-utilities/-/regex-utilities-2.3.0.tgz",
"integrity": "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==",
"license": "MIT"
},
"node_modules/regexp.prototype.flags": { "node_modules/regexp.prototype.flags": {
"version": "1.5.4", "version": "1.5.4",
"resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz",
@@ -9422,6 +9630,41 @@
"url": "https://opencollective.com/unified" "url": "https://opencollective.com/unified"
} }
}, },
"node_modules/rehype-parse": {
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/rehype-parse/-/rehype-parse-9.0.1.tgz",
"integrity": "sha512-ksCzCD0Fgfh7trPDxr2rSylbwq9iYDkSn8TCDmEJ49ljEUBxDVCzCHv7QNzZOfODanX4+bWQ4WZqLCRWYLfhag==",
"license": "MIT",
"dependencies": {
"@types/hast": "^3.0.0",
"hast-util-from-html": "^2.0.0",
"unified": "^11.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/rehype-pretty-code": {
"version": "0.14.1",
"resolved": "https://registry.npmjs.org/rehype-pretty-code/-/rehype-pretty-code-0.14.1.tgz",
"integrity": "sha512-IpG4OL0iYlbx78muVldsK86hdfNoht0z63AP7sekQNW2QOTmjxB7RbTO+rhIYNGRljgHxgVZoPwUl6bIC9SbjA==",
"license": "MIT",
"dependencies": {
"@types/hast": "^3.0.4",
"hast-util-to-string": "^3.0.0",
"parse-numeric-range": "^1.3.0",
"rehype-parse": "^9.0.0",
"unified": "^11.0.5",
"unist-util-visit": "^5.0.0"
},
"engines": {
"node": ">=18"
},
"peerDependencies": {
"shiki": "^1.0.0 || ^2.0.0 || ^3.0.0"
}
},
"node_modules/rehype-recma": { "node_modules/rehype-recma": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/rehype-recma/-/rehype-recma-1.0.0.tgz", "resolved": "https://registry.npmjs.org/rehype-recma/-/rehype-recma-1.0.0.tgz",
@@ -9912,6 +10155,23 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/shiki": {
"version": "3.15.0",
"resolved": "https://registry.npmjs.org/shiki/-/shiki-3.15.0.tgz",
"integrity": "sha512-kLdkY6iV3dYbtPwS9KXU7mjfmDm25f5m0IPNFnaXO7TBPcvbUOY72PYXSuSqDzwp+vlH/d7MXpHlKO/x+QoLXw==",
"license": "MIT",
"peer": true,
"dependencies": {
"@shikijs/core": "3.15.0",
"@shikijs/engine-javascript": "3.15.0",
"@shikijs/engine-oniguruma": "3.15.0",
"@shikijs/langs": "3.15.0",
"@shikijs/themes": "3.15.0",
"@shikijs/types": "3.15.0",
"@shikijs/vscode-textmate": "^10.0.2",
"@types/hast": "^3.0.4"
}
},
"node_modules/side-channel": { "node_modules/side-channel": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
@@ -11125,6 +11385,20 @@
"url": "https://opencollective.com/unified" "url": "https://opencollective.com/unified"
} }
}, },
"node_modules/vfile-location": {
"version": "5.0.3",
"resolved": "https://registry.npmjs.org/vfile-location/-/vfile-location-5.0.3.tgz",
"integrity": "sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==",
"license": "MIT",
"dependencies": {
"@types/unist": "^3.0.0",
"vfile": "^6.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/vfile-message": { "node_modules/vfile-message": {
"version": "4.0.3", "version": "4.0.3",
"resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz",
@@ -11139,6 +11413,16 @@
"url": "https://opencollective.com/unified" "url": "https://opencollective.com/unified"
} }
}, },
"node_modules/web-namespaces": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz",
"integrity": "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/which": { "node_modules/which": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",

View File

@@ -33,8 +33,10 @@
"react": "^19.2.0", "react": "^19.2.0",
"react-dom": "^19.2.0", "react-dom": "^19.2.0",
"rehype-autolink-headings": "^7.1.0", "rehype-autolink-headings": "^7.1.0",
"rehype-pretty-code": "^0.14.1",
"rehype-slug": "^6.0.0", "rehype-slug": "^6.0.0",
"remark-gfm": "^4.0.1", "remark-gfm": "^4.0.1",
"shiki": "^3.15.0",
"tailwind-merge": "^3.4.0", "tailwind-merge": "^3.4.0",
"unist-util-visit": "^5.0.0" "unist-util-visit": "^5.0.0"
}, },

View File

@@ -360,3 +360,69 @@ body {
.pagefind-ui__search-input:focus { .pagefind-ui__search-input:focus {
@apply ring-2 ring-blue-500 dark:ring-blue-400; @apply ring-2 ring-blue-500 dark:ring-blue-400;
} }
/* Code Syntax Highlighting Styles (rehype-pretty-code) */
.prose pre {
@apply overflow-x-auto rounded-lg border border-slate-200 dark:border-slate-700;
padding: 1rem 1.2rem;
margin: 1.5rem 0;
background-color: #f8fafc;
}
.dark .prose pre {
background-color: #0f172a;
}
.prose pre > code {
@apply grid;
counter-reset: line;
font-size: 0.9em;
line-height: 1.7;
}
.prose pre > code > [data-line] {
padding: 0 1rem;
border-left: 2px solid transparent;
}
.prose pre > code > [data-line]::before {
counter-increment: line;
content: counter(line);
display: inline-block;
width: 1.5rem;
margin-right: 1.5rem;
text-align: right;
color: #94a3b8;
user-select: none;
}
.dark .prose pre > code > [data-line]::before {
color: #475569;
}
/* Highlighted lines */
.prose pre > code > [data-highlighted-line] {
background-color: rgba(59, 130, 246, 0.1);
border-left-color: rgb(59, 130, 246);
}
.dark .prose pre > code > [data-highlighted-line] {
background-color: rgba(96, 165, 250, 0.15);
border-left-color: rgb(96, 165, 250);
}
/* Inline code */
.prose :not(pre) > code {
@apply rounded bg-slate-100 px-1.5 py-0.5 text-sm font-semibold text-slate-800 dark:bg-slate-800 dark:text-slate-200;
white-space: nowrap;
}
/* Code title (if specified in markdown: ```js title="example.js") */
.prose [data-rehype-pretty-code-title] {
@apply rounded-t-lg border border-b-0 border-slate-200 bg-slate-100 px-4 py-2 text-sm font-semibold text-slate-700 dark:border-slate-700 dark:bg-slate-800 dark:text-slate-300;
margin-bottom: 0;
}
.prose [data-rehype-pretty-code-title] + pre {
@apply mt-0 rounded-t-none;
}