From f994301fbb4f36902cad759c6233845326a35fdf Mon Sep 17 00:00:00 2001 From: gbanyan Date: Thu, 20 Nov 2025 17:59:56 +0800 Subject: [PATCH] Add RSS feed, sitemap, robots.txt, and code syntax highlighting MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- app/feed.xml/route.ts | 63 +++++++++ app/layout.tsx | 5 + app/robots.ts | 14 ++ app/sitemap.ts | 68 ++++++++++ contentlayer.config.ts | 11 ++ package-lock.json | 284 +++++++++++++++++++++++++++++++++++++++++ package.json | 2 + styles/globals.css | 66 ++++++++++ 8 files changed, 513 insertions(+) create mode 100644 app/feed.xml/route.ts create mode 100644 app/robots.ts create mode 100644 app/sitemap.ts diff --git a/app/feed.xml/route.ts b/app/feed.xml/route.ts new file mode 100644 index 0000000..ed7c3fe --- /dev/null +++ b/app/feed.xml/route.ts @@ -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 = ` + + + ${escapeXml(siteConfig.name)} + ${siteUrl} + ${escapeXml(siteConfig.description)} + ${siteConfig.defaultLocale.replace('_', '-')} + ${new Date().toUTCString()} + + ${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 ` + + ${escapeXml(post.title)} + ${postUrl} + ${postUrl} + ${escapeXml(post.description || post.custom_excerpt || post.title)} + ${post.body?.html ? `` : ''} + ${pubDate} + ${post.authors?.map((author) => `${escapeXml(author)}`).join('\n ') || ''} + ${post.tags?.map((tag) => `${escapeXml(tag)}`).join('\n ') || ''} + `; + }) + .join('')} + +`; + + 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, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); +} diff --git a/app/layout.tsx b/app/layout.tsx index 2918654..6ed8038 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -34,6 +34,11 @@ export const metadata: Metadata = { }, icons: { icon: '/favicon.png' + }, + alternates: { + types: { + 'application/rss+xml': `${siteConfig.url}/feed.xml` + } } }; diff --git a/app/robots.ts b/app/robots.ts new file mode 100644 index 0000000..d7c5f4b --- /dev/null +++ b/app/robots.ts @@ -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`, + }; +} diff --git a/app/sitemap.ts b/app/sitemap.ts new file mode 100644 index 0000000..9f8e0ee --- /dev/null +++ b/app/sitemap.ts @@ -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]; +} diff --git a/contentlayer.config.ts b/contentlayer.config.ts index ad5949e..18552e0 100644 --- a/contentlayer.config.ts +++ b/contentlayer.config.ts @@ -3,6 +3,7 @@ import { visit } from 'unist-util-visit'; import rehypeSlug from 'rehype-slug'; import rehypeAutolinkHeadings from 'rehype-autolink-headings'; import remarkGfm from 'remark-gfm'; +import rehypePrettyCode from 'rehype-pretty-code'; export const Post = defineDocumentType(() => ({ name: 'Post', @@ -86,6 +87,16 @@ export default makeSource({ markdown: { remarkPlugins: [remarkGfm], rehypePlugins: [ + [ + rehypePrettyCode, + { + theme: { + dark: 'github-dark', + light: 'github-light', + }, + keepBackground: false, + }, + ], rehypeSlug, [rehypeAutolinkHeadings, { behavior: 'wrap' }], /** diff --git a/package-lock.json b/package-lock.json index bb87d58..877543e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,8 +26,10 @@ "react": "^19.2.0", "react-dom": "^19.2.0", "rehype-autolink-headings": "^7.1.0", + "rehype-pretty-code": "^0.14.1", "rehype-slug": "^6.0.0", "remark-gfm": "^4.0.1", + "shiki": "^3.15.0", "tailwind-merge": "^3.4.0", "unist-util-visit": "^5.0.0" }, @@ -2667,6 +2669,73 @@ "dev": true, "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": { "version": "1.4.0-beta.0", "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_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": { "version": "1.24.0", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.0.tgz", @@ -6121,6 +6202,44 @@ "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": { "version": "3.0.0", "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" } }, + "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": { "version": "3.1.3", "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" } }, + "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": { "version": "0.25.1", "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", @@ -8714,6 +8863,23 @@ "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": { "version": "1.119.0", "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==", "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": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz", @@ -9383,6 +9567,30 @@ "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": { "version": "1.5.4", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", @@ -9422,6 +9630,41 @@ "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": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/rehype-recma/-/rehype-recma-1.0.0.tgz", @@ -9912,6 +10155,23 @@ "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": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", @@ -11125,6 +11385,20 @@ "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": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", @@ -11139,6 +11413,16 @@ "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": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/package.json b/package.json index c18c3e1..8029783 100644 --- a/package.json +++ b/package.json @@ -33,8 +33,10 @@ "react": "^19.2.0", "react-dom": "^19.2.0", "rehype-autolink-headings": "^7.1.0", + "rehype-pretty-code": "^0.14.1", "rehype-slug": "^6.0.0", "remark-gfm": "^4.0.1", + "shiki": "^3.15.0", "tailwind-merge": "^3.4.0", "unist-util-visit": "^5.0.0" }, diff --git a/styles/globals.css b/styles/globals.css index bc1025e..2025e90 100644 --- a/styles/globals.css +++ b/styles/globals.css @@ -359,4 +359,70 @@ body { .pagefind-ui__search-input:focus { @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; } \ No newline at end of file