From 8c19d26cfd35f6b360432910ca05a42a5d9a5414 Mon Sep 17 00:00:00 2001 From: radishzzz Date: Sat, 18 Jan 2025 02:25:00 +0000 Subject: [PATCH] feat: enhance internationalization support with dynamic routing --- astro.config.ts | 3 + pnpm-lock.yaml | 38 ++++++------ src/config/index.ts | 1 - src/pages/[lang]/about.astro | 7 +++ src/pages/[lang]/index.astro | 8 +-- src/pages/[lang]/posts/[slug].astro | 11 ++-- src/pages/[lang]/rss.xml.ts | 86 ++++++++++++++++++++++++++++ src/pages/[lang]/tags/[tags].astro | 16 ++++-- src/pages/[lang]/tags/index.astro | 10 +++- src/pages/rss.xml.ts | 16 ++++-- src/types/index.d.ts | 1 - src/utils/content.config.ts | 89 ++++++++++++++--------------- 12 files changed, 199 insertions(+), 87 deletions(-) create mode 100644 src/pages/[lang]/rss.xml.ts diff --git a/astro.config.ts b/astro.config.ts index b003d41..dcd7bde 100644 --- a/astro.config.ts +++ b/astro.config.ts @@ -43,6 +43,9 @@ export default defineConfig({ codes, })), defaultLocale: locale, + routing: { + prefixDefaultLocale: false, + }, }, integrations: [ partytown({ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 25d2c0c..1ab1d49 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2736,8 +2736,8 @@ packages: peerDependencies: eslint: ^8.57.0 || ^9.0.0 - eslint-plugin-jsdoc@50.6.1: - resolution: {integrity: sha512-UWyaYi6iURdSfdVVqvfOs2vdCVz0J40O/z/HTsv2sFjdjmdlUI/qlKLOTmwbPQ2tAfQnE5F9vqx+B+poF71DBQ==} + eslint-plugin-jsdoc@50.6.2: + resolution: {integrity: sha512-n7GNZ4czMAAbDg7DsDA7PvHo1IPIUwAXYmxTx6j/hTlXbt5V0x5q/kGkiJ7s4wA9SpB/yaiK8jF7CO237lOLew==} engines: {node: '>=18'} peerDependencies: eslint: ^7.0.0 || ^8.0.0 || ^9.0.0 @@ -2930,8 +2930,8 @@ packages: fastq@1.18.0: resolution: {integrity: sha512-QKHXPW0hD8g4UET03SdOdunzSouc9N4AuHdsX8XNcTsuz+yYFILVNIX4l9yHABMhiEI9Db0JTTIpu0wB+Y1QQw==} - fdir@6.4.2: - resolution: {integrity: sha512-KnhMXsKSPZlAhp7+IjUkRZKPb4fUyccpDrdFXbi4QL1qkmFh9kVY09Yox+n4MaOb3lHZ1Tv829C3oaaXoMYPDQ==} + fdir@6.4.3: + resolution: {integrity: sha512-PMXmW2y1hDDfTSRc9gaXIuCCRpuoz3Kaz8cUelp3smouvfT632ozg2vrT6lJsHKKOF59YLbOGfAWGUcKEfRMQw==} peerDependencies: picomatch: ^3 || ^4 peerDependenciesMeta: @@ -3545,8 +3545,8 @@ packages: jsonfile@6.1.0: resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==} - katex@0.16.20: - resolution: {integrity: sha512-jjuLaMGD/7P8jUTpdKhA9IoqnH+yMFB3sdAFtq5QdAqeP2PjiSbnC3EaguKPNtv6dXXanHxp1ckwvF4a86LBig==} + katex@0.16.21: + resolution: {integrity: sha512-XvqR7FgOHtWupfMiigNzmh+MgUVmDGU2kXZm899ZkPfcuoPuFxyHmXsgATDpFZDAXCI8tvinaVcDo8PIIJSo4A==} hasBin: true keyv@4.5.4: @@ -4222,8 +4222,8 @@ packages: pathe@1.1.2: resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==} - pathe@2.0.1: - resolution: {integrity: sha512-6jpjMpOth5S9ITVu5clZ7NOgHNsv5vRQdheL9ztp2vZmM6fRbLvyua1tiBIL4lk8SAe3ARzeXEly6siXCjDHDw==} + pathe@2.0.2: + resolution: {integrity: sha512-15Ztpk+nov8DR524R4BF7uEuzESgzUEAV4Ah7CUMNGXdE5ELuvxElxGXndBl32vMSsWa1jpNf22Z+Er3sKwq+w==} perfect-debounce@1.0.0: resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==} @@ -5755,7 +5755,7 @@ snapshots: eslint-plugin-antfu: 2.7.0(eslint@9.18.0(jiti@2.4.2)) eslint-plugin-command: 2.1.0(eslint@9.18.0(jiti@2.4.2)) eslint-plugin-import-x: 4.6.1(eslint@9.18.0(jiti@2.4.2))(typescript@5.7.3) - eslint-plugin-jsdoc: 50.6.1(eslint@9.18.0(jiti@2.4.2)) + eslint-plugin-jsdoc: 50.6.2(eslint@9.18.0(jiti@2.4.2)) eslint-plugin-jsonc: 2.18.2(eslint@9.18.0(jiti@2.4.2)) eslint-plugin-n: 17.15.1(eslint@9.18.0(jiti@2.4.2)) eslint-plugin-no-only-tests: 3.3.0 @@ -8838,7 +8838,7 @@ snapshots: eslint-flat-config-utils@1.0.0: dependencies: - pathe: 2.0.1 + pathe: 2.0.2 eslint-import-resolver-node@0.3.9: dependencies: @@ -8910,7 +8910,7 @@ snapshots: - supports-color - typescript - eslint-plugin-jsdoc@50.6.1(eslint@9.18.0(jiti@2.4.2)): + eslint-plugin-jsdoc@50.6.2(eslint@9.18.0(jiti@2.4.2)): dependencies: '@es-joy/jsdoccomment': 0.49.0 are-docs-informative: 0.0.2 @@ -9204,7 +9204,7 @@ snapshots: dependencies: reusify: 1.0.4 - fdir@6.4.2(picomatch@4.0.2): + fdir@6.4.3(picomatch@4.0.2): optionalDependencies: picomatch: 4.0.2 @@ -9898,7 +9898,7 @@ snapshots: optionalDependencies: graceful-fs: 4.2.11 - katex@0.16.20: + katex@0.16.21: dependencies: commander: 8.3.0 @@ -10446,7 +10446,7 @@ snapshots: dependencies: '@types/katex': 0.16.7 devlop: 1.1.0 - katex: 0.16.20 + katex: 0.16.21 micromark-factory-space: 2.0.1 micromark-util-character: 2.1.1 micromark-util-symbol: 2.0.1 @@ -10669,7 +10669,7 @@ snapshots: mlly@1.7.4: dependencies: acorn: 8.14.0 - pathe: 2.0.1 + pathe: 2.0.2 pkg-types: 1.3.1 ufo: 1.5.4 @@ -10910,7 +10910,7 @@ snapshots: pathe@1.1.2: {} - pathe@2.0.1: {} + pathe@2.0.2: {} perfect-debounce@1.0.0: {} @@ -10936,7 +10936,7 @@ snapshots: dependencies: confbox: 0.1.8 mlly: 1.7.4 - pathe: 2.0.1 + pathe: 2.0.2 pluralize@8.0.0: {} @@ -11347,7 +11347,7 @@ snapshots: '@types/katex': 0.16.7 hast-util-from-html-isomorphic: 2.0.0 hast-util-to-text: 4.0.2 - katex: 0.16.20 + katex: 0.16.21 unist-util-visit-parents: 6.0.1 vfile: 6.0.3 @@ -12026,7 +12026,7 @@ snapshots: tinyglobby@0.2.10: dependencies: - fdir: 6.4.2(picomatch@4.0.2) + fdir: 6.4.3(picomatch@4.0.2) picomatch: 4.0.2 to-regex-range@5.0.1: diff --git a/src/config/index.ts b/src/config/index.ts index 866e929..505f88d 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -33,7 +33,6 @@ export const themeConfig: ThemeConfig = { locale: 'zh', // zh, zh-tw, ja, en, es, ru, default locale setting moreLocale: ['zh-tw', 'ja', 'en', 'es', 'ru'], // ['zh-tw', 'ja', 'en', 'es', 'ru'], not fill in the default locale code again font: 'sans', // sans, serif, choose the font style for posts - rss: true, // true, false, whether to enable RSS toc: true, // true, false, whether to enable table of contents in posts }, // GLOBAL SETTINGS >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> END diff --git a/src/pages/[lang]/about.astro b/src/pages/[lang]/about.astro index e2b29d8..f33fc26 100644 --- a/src/pages/[lang]/about.astro +++ b/src/pages/[lang]/about.astro @@ -1,5 +1,12 @@ --- +import { themeConfig } from '@/config' import Layout from '@/layouts/Layout.astro' + +export function getStaticPaths() { + return themeConfig.global.moreLocale.map(lang => ({ + params: { lang }, + })) +} --- diff --git a/src/pages/[lang]/index.astro b/src/pages/[lang]/index.astro index aa58087..87d2755 100644 --- a/src/pages/[lang]/index.astro +++ b/src/pages/[lang]/index.astro @@ -1,12 +1,12 @@ --- +import { themeConfig } from '@/config' import Layout from '@/layouts/Layout.astro' import { getPinnedPosts, getPosts } from '@/utils/content.config' export function getStaticPaths() { - return [ - { params: { lang: 'zh' } }, - { params: { lang: 'en' } }, - ] + return themeConfig.global.moreLocale.map(lang => ({ + params: { lang }, + })) } const { lang } = Astro.params diff --git a/src/pages/[lang]/posts/[slug].astro b/src/pages/[lang]/posts/[slug].astro index 28928f3..1d1eae2 100644 --- a/src/pages/[lang]/posts/[slug].astro +++ b/src/pages/[lang]/posts/[slug].astro @@ -1,13 +1,16 @@ --- +import { themeConfig } from '@/config' import Layout from '@/layouts/Layout.astro' import { getCollection } from 'astro:content' export async function getStaticPaths() { const posts = await getCollection('posts') - return posts.map(post => ({ - params: { slug: post.slug }, - props: { post }, - })) + return themeConfig.global.moreLocale.flatMap(lang => + posts.map(post => ({ + params: { lang, slug: post.slug }, + props: { post }, + })), + ) } const { post } = Astro.props diff --git a/src/pages/[lang]/rss.xml.ts b/src/pages/[lang]/rss.xml.ts new file mode 100644 index 0000000..987b1f4 --- /dev/null +++ b/src/pages/[lang]/rss.xml.ts @@ -0,0 +1,86 @@ +import type { APIContext } from 'astro' +import type { CollectionEntry } from 'astro:content' +import themeConfig from '@/config' +import rss from '@astrojs/rss' +import { getCollection } from 'astro:content' +import MarkdownIt from 'markdown-it' +import sanitizeHtml from 'sanitize-html' + +const parser = new MarkdownIt() +const { title, description, url } = themeConfig.site +const { locale: defaultLocale, moreLocale } = themeConfig.global +const followConfig = themeConfig.seo?.follow + +// Extract first 100 chars from content as description +function getExcerpt(content: string): string { + const plainText = sanitizeHtml( + parser.render(content), + { + allowedTags: [], + allowedAttributes: {}, + }, + ) + + return `${plainText.slice(0, 100).trim()}...` +} + +// Return 404 response for invalid language routes +function return404() { + return new Response(null, { + status: 404, + statusText: 'Not found', + }) +} + +// Add getStaticPaths for dynamic routes +export function getStaticPaths() { + return moreLocale.map(lang => ({ params: { lang } })) +} + +// Generate RSS feed for non-default languages +export async function GET({ params }: APIContext) { + const lang = params.lang as string + + // Only generate RSS for valid non-default languages + if (!moreLocale.includes(lang)) { + return return404() + } + + // Get posts for specific language (include universal posts) + const posts = await getCollection( + 'posts', + ({ data }: { data: CollectionEntry<'posts'>['data'] }) => + (!data.draft && (data.lang === lang || data.lang === '')), + ) + + return rss({ + title: `${title} (${lang})`, + description, + site: url, + stylesheet: '/rss/styles.xsl', + // Map posts to RSS items with language-specific URLs + items: posts.map(post => ({ + title: post.data.title, + pubDate: post.data.published, + description: post.data.description || getExcerpt(post.body), + link: `/${lang}/posts/${post.slug}/`, + content: sanitizeHtml( + parser.render(post.body), + { + allowedTags: sanitizeHtml.defaults.allowedTags.concat(['img']), + }, + ), + })), + // Add language and follow challenge info + customData: ` + ${lang} + ${followConfig?.feedID && followConfig?.userID + ? ` + ${followConfig.feedID} + ${followConfig.userID} + ` + : '' + } + `.trim(), + }) +} diff --git a/src/pages/[lang]/tags/[tags].astro b/src/pages/[lang]/tags/[tags].astro index 068dd92..3319886 100644 --- a/src/pages/[lang]/tags/[tags].astro +++ b/src/pages/[lang]/tags/[tags].astro @@ -1,15 +1,19 @@ --- +import { themeConfig } from '@/config' import Layout from '@/layouts/Layout.astro' import { getAllTags, getPostsByTag } from '@/utils/content.config' export async function getStaticPaths() { const tags = await getAllTags() - return tags.map(tag => ({ - params: { tags: tag }, - props: { tags: tag }, - })) + return themeConfig.global.moreLocale.flatMap(lang => + tags.map(tag => ({ + params: { lang, tags: tag }, + props: { tags: tag }, + })), + ) } +const { lang } = Astro.params const { tags } = Astro.props const posts = await getPostsByTag(tags) const allTags = await getAllTags() @@ -18,7 +22,7 @@ const allTags = await getAllTags()
{allTags.map(tag => ( - + {tag} ))} @@ -28,7 +32,7 @@ const allTags = await getAllTags() diff --git a/src/pages/[lang]/tags/index.astro b/src/pages/[lang]/tags/index.astro index ab600e1..b759350 100644 --- a/src/pages/[lang]/tags/index.astro +++ b/src/pages/[lang]/tags/index.astro @@ -1,14 +1,22 @@ --- +import { themeConfig } from '@/config' import Layout from '@/layouts/Layout.astro' import { getAllTags } from '@/utils/content.config' +export function getStaticPaths() { + return themeConfig.global.moreLocale.map(lang => ({ + params: { lang }, + })) +} + +const { lang } = Astro.params const allTags = await getAllTags() ---
{allTags.map(tag => ( - + {tag} ))} diff --git a/src/pages/rss.xml.ts b/src/pages/rss.xml.ts index b6a1d0e..6ee9c62 100644 --- a/src/pages/rss.xml.ts +++ b/src/pages/rss.xml.ts @@ -24,11 +24,13 @@ function getExcerpt(content: string): string { return `${plainText.slice(0, 100).trim()}...` } -// Generate RSS feed -export async function GET(_context: APIContext) { +// Generate RSS feed for default language +export async function GET() { + // Only handle posts for default language const posts = await getCollection( 'posts', - ({ data }: { data: CollectionEntry<'posts'>['data'] }) => !data.draft, + ({ data }: { data: CollectionEntry<'posts'>['data'] }) => + (!data.draft && (data.lang === locale || data.lang === '')), ) return rss({ @@ -36,6 +38,7 @@ export async function GET(_context: APIContext) { description, site: url, stylesheet: '/rss/styles.xsl', + // Map posts to RSS items items: posts.map((post: CollectionEntry<'posts'>) => ({ title: post.data.title, pubDate: post.data.published, @@ -48,13 +51,14 @@ export async function GET(_context: APIContext) { }, ), })), + // Add language and follow challenge info customData: ` ${locale} ${followConfig?.feedID && followConfig?.userID ? ` - ${followConfig.feedID} - ${followConfig.userID} - ` + ${followConfig.feedID} + ${followConfig.userID} + ` : '' } `.trim(), diff --git a/src/types/index.d.ts b/src/types/index.d.ts index 76d0198..1ea7a05 100644 --- a/src/types/index.d.ts +++ b/src/types/index.d.ts @@ -29,7 +29,6 @@ export interface ThemeConfig { locale: typeof langPath[number] moreLocale: typeof langPath[number][] font: string - rss: boolean toc: boolean } diff --git a/src/utils/content.config.ts b/src/utils/content.config.ts index a40ee7c..7cad6f6 100644 --- a/src/utils/content.config.ts +++ b/src/utils/content.config.ts @@ -1,17 +1,25 @@ import type { CollectionEntry } from 'astro:content' +import themeConfig from '@/config' +import { langPath } from '@/utils/ui' import { getCollection } from 'astro:content' export type Post = CollectionEntry<'posts'> export type PostData = Post['data'] export type PostsGroupByYear = Map -// Get all posts and sort by publish date -export async function getPosts() { +// Get all posts except drafts (include pinned) +export async function getPosts(lang?: string) { + const defaultLocale = themeConfig.global.locale + const currentLang = lang || defaultLocale + const posts = await getCollection( 'posts', ({ data }: Post) => { - const shouldInclude = (import.meta.env.DEV || !data.draft) && !data.pin - return shouldInclude + const shouldInclude = import.meta.env.DEV || !data.draft + if (!langPath.includes(currentLang)) { + return shouldInclude && data.lang === '' + } + return shouldInclude && (data.lang === currentLang || data.lang === '') }, ) @@ -20,27 +28,23 @@ export async function getPosts() { ) } +// Get all posts except drafts (not pinned) +export async function getRegularPosts(lang?: string) { + const posts = await getPosts(lang) + return posts.filter(post => !post.data.pin) +} + // Get pinned posts -export async function getPinnedPosts() { - const posts = await getCollection( - 'posts', - ({ data }: Post) => { - const shouldInclude = (import.meta.env.DEV || !data.draft) && data.pin - return shouldInclude - }, - ) - - return posts.sort((a: Post, b: Post) => - b.data.published.valueOf() - a.data.published.valueOf(), - ) +export async function getPinnedPosts(lang?: string) { + const posts = await getPosts(lang) + return posts.filter(post => post.data.pin) } -// Group posts by year and sort -export async function getPostsByYear(): Promise { - const posts = await getPosts() +// Get posts group by year (not pinned) +export async function getPostsByYear(lang?: string): Promise { + const posts = await getRegularPosts(lang) const yearMap = new Map() - // Group by year posts.forEach((post: Post) => { const year = post.data.published.getFullYear() if (!yearMap.has(year)) { @@ -49,7 +53,6 @@ export async function getPostsByYear(): Promise { yearMap.get(year)?.push(post) }) - // Sort posts within each year yearMap.forEach((yearPosts) => { yearPosts.sort((a: Post, b: Post) => { const aDate = a.data.published @@ -66,9 +69,23 @@ export async function getPostsByYear(): Promise { return new Map([...yearMap.entries()].sort((a, b) => b[0] - a[0])) } -// Group posts by tags -export async function sortPostsByTags() { - const posts = await getPosts() +// Get all tags +export async function getAllTags(lang?: string) { + const posts = await getPosts(lang) + const tags = new Set() + + posts.forEach((post: Post) => { + post.data.tags?.forEach((tag: string) => + tags.add(tag), + ) + }) + + return Array.from(tags) +} + +// Get posts group by each tag +export async function getPostsGroupByTags(lang?: string) { + const posts = await getRegularPosts(lang) const tagMap = new Map() posts.forEach((post: Post) => { @@ -85,28 +102,10 @@ export async function sortPostsByTags() { return tagMap } -// Get posts by specific tag -export async function getPostsByTag(tag: string) { - const posts = await getPosts() - +// Get all posts by one tag +export async function getPostsByTag(tag: string, lang?: string) { + const posts = await getRegularPosts(lang) return posts.filter((post: Post) => post.data.tags?.includes(tag), ) } - -// Get all tags list -export async function getAllTags() { - const posts = await getCollection( - 'posts', - ({ data }: Post) => import.meta.env.DEV || !data.draft, - ) - const tags = new Set() - - posts.forEach((post: Post) => { - post.data.tags?.forEach((tag: string) => - tags.add(tag), - ) - }) - - return Array.from(tags) -}