diff --git a/src/content/config.ts b/src/content/config.ts
index a5ac255..dc035c6 100644
--- a/src/content/config.ts
+++ b/src/content/config.ts
@@ -11,7 +11,19 @@ const postsCollection = defineCollection({
image: z.string().optional().default(''),
// Extended Settings
lang: z.string().optional().default(''),
- slug: z.string().optional().default(''),
+ slug: z.string()
+ .optional()
+ .default('')
+ .refine(
+ (slug) => {
+ if (!slug)
+ return true
+ return /^[\w\-]*$/.test(slug)
+ },
+ {
+ message: 'Slug can only contain letters, numbers, hyphens and underscores',
+ },
+ ),
toc: z.boolean().optional().default(false),
pin: z.boolean().optional().default(false),
draft: z.boolean().optional().default(false),
diff --git a/src/pages/[lang]/index.astro b/src/pages/[lang]/index.astro
index 87d2755..def533c 100644
--- a/src/pages/[lang]/index.astro
+++ b/src/pages/[lang]/index.astro
@@ -21,7 +21,7 @@ const posts = await getPosts()
{pinnedPosts.map(post => (
-
-
+
{post.data.title}
@@ -35,7 +35,7 @@ const posts = await getPosts()
{posts.map(post => (
-
-
+
{post.data.title}
diff --git a/src/pages/[lang]/posts/[slug].astro b/src/pages/[lang]/posts/[slug].astro
index 1d1eae2..e65d6a7 100644
--- a/src/pages/[lang]/posts/[slug].astro
+++ b/src/pages/[lang]/posts/[slug].astro
@@ -1,13 +1,23 @@
---
import { themeConfig } from '@/config'
import Layout from '@/layouts/Layout.astro'
+import { checkSlugDuplication } from '@/utils/content.config'
import { getCollection } from 'astro:content'
export async function getStaticPaths() {
const posts = await getCollection('posts')
+
+ const duplicates = await checkSlugDuplication(posts)
+ if (duplicates.length > 0) {
+ throw new Error(`Slug conflicts found:\n${duplicates.join('\n')}`)
+ }
+
return themeConfig.global.moreLocale.flatMap(lang =>
posts.map(post => ({
- params: { lang, slug: post.slug },
+ params: {
+ lang,
+ slug: post.data.slug || post.slug,
+ },
props: { post },
})),
)
diff --git a/src/pages/[lang]/rss.xml.ts b/src/pages/[lang]/rss.xml.ts
index b6e511e..408cee3 100644
--- a/src/pages/[lang]/rss.xml.ts
+++ b/src/pages/[lang]/rss.xml.ts
@@ -65,7 +65,7 @@ export async function GET({ params }: APIContext) {
pubDate: post.data.published,
description: post.data.description || getExcerpt(post.body),
// Generate absolute URL with language prefix
- link: new URL(`${lang}/posts/${post.slug}/`, url).toString(),
+ link: new URL(`${lang}/posts/${post.data.slug || post.slug}/`, url).toString(),
// Convert markdown content to HTML, allowing img tags
content: post.body
? sanitizeHtml(parser.render(post.body), {
diff --git a/src/pages/[lang]/tags/[tags].astro b/src/pages/[lang]/tags/[tags].astro
index 3319886..aaac175 100644
--- a/src/pages/[lang]/tags/[tags].astro
+++ b/src/pages/[lang]/tags/[tags].astro
@@ -32,7 +32,7 @@ const allTags = await getAllTags()
diff --git a/src/pages/index.astro b/src/pages/index.astro
index a28ceb8..0433832 100644
--- a/src/pages/index.astro
+++ b/src/pages/index.astro
@@ -13,7 +13,7 @@ const pinnedPosts = await getPinnedPosts()
{pinnedPosts.map(post => (
-
-
+
{post.data.title}
@@ -27,7 +27,7 @@ const pinnedPosts = await getPinnedPosts()
{posts.map(post => (
-
-
+
{post.data.title}
diff --git a/src/pages/posts/[slug].astro b/src/pages/posts/[slug].astro
index 7576075..da1714d 100644
--- a/src/pages/posts/[slug].astro
+++ b/src/pages/posts/[slug].astro
@@ -1,11 +1,20 @@
---
import Layout from '@/layouts/Layout.astro'
+import { checkSlugDuplication } from '@/utils/content.config'
import { getCollection } from 'astro:content'
export async function getStaticPaths() {
const posts = await getCollection('posts')
+
+ const duplicates = await checkSlugDuplication(posts)
+ if (duplicates.length > 0) {
+ throw new Error(`Slug conflicts found:\n${duplicates.join('\n')}`)
+ }
+
return posts.map(post => ({
- params: { slug: post.slug },
+ params: {
+ slug: post.data.slug || post.slug,
+ },
props: { post },
}))
}
diff --git a/src/pages/rss.xml.ts b/src/pages/rss.xml.ts
index 7cde56d..11a07e3 100644
--- a/src/pages/rss.xml.ts
+++ b/src/pages/rss.xml.ts
@@ -42,7 +42,7 @@ export async function GET() {
pubDate: post.data.published,
description: post.data.description || getExcerpt(post.body),
// Generate absolute URL for post
- link: new URL(`posts/${post.slug}/`, url).toString(),
+ link: new URL(`posts/${post.data.slug || post.slug}/`, url).toString(),
// Convert markdown content to HTML, allowing img tags
content: post.body
? sanitizeHtml(parser.render(post.body), {
diff --git a/src/pages/tags/[tags].astro b/src/pages/tags/[tags].astro
index 068dd92..4703bf9 100644
--- a/src/pages/tags/[tags].astro
+++ b/src/pages/tags/[tags].astro
@@ -28,7 +28,7 @@ const allTags = await getAllTags()
diff --git a/src/utils/content.config.ts b/src/utils/content.config.ts
index 7cad6f6..ab37b0a 100644
--- a/src/utils/content.config.ts
+++ b/src/utils/content.config.ts
@@ -7,6 +7,31 @@ export type Post = CollectionEntry<'posts'>
export type PostData = Post['data']
export type PostsGroupByYear = Map
+// Check if the slug is duplicated under the same language.
+export async function checkSlugDuplication(posts: Post[]): Promise {
+ const slugMap = new Map>() // Map>
+ const duplicates: string[] = []
+
+ posts.forEach((post) => {
+ const lang = post.data.lang || ''
+ const slug = post.data.slug || post.slug
+
+ if (!slugMap.has(lang)) {
+ slugMap.set(lang, new Set())
+ }
+
+ const slugSet = slugMap.get(lang)!
+ if (slugSet.has(slug)) {
+ duplicates.push(`Duplicate slug "${slug}" found in language "${lang || 'default'}"`)
+ }
+ else {
+ slugSet.add(slug)
+ }
+ })
+
+ return duplicates
+}
+
// Get all posts except drafts (include pinned)
export async function getPosts(lang?: string) {
const defaultLocale = themeConfig.global.locale