mirror of
https://github.com/reonokiy/blog.nokiy.net.git
synced 2025-06-16 19:51:07 +02:00
feat: add custom slug validation and handling for posts
- Enhance content configuration with slug validation to ensure only valid characters are used - Update post routing to support custom slugs with fallback to default slug - Implement slug duplication check to prevent conflicts across different languages - Modify various page components to use custom or default slugs in URL generation
This commit is contained in:
parent
ee35006f7c
commit
a26031d490
10 changed files with 67 additions and 11 deletions
|
@ -11,7 +11,19 @@ const postsCollection = defineCollection({
|
||||||
image: z.string().optional().default(''),
|
image: z.string().optional().default(''),
|
||||||
// Extended Settings
|
// Extended Settings
|
||||||
lang: z.string().optional().default(''),
|
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),
|
toc: z.boolean().optional().default(false),
|
||||||
pin: z.boolean().optional().default(false),
|
pin: z.boolean().optional().default(false),
|
||||||
draft: z.boolean().optional().default(false),
|
draft: z.boolean().optional().default(false),
|
||||||
|
|
|
@ -21,7 +21,7 @@ const posts = await getPosts()
|
||||||
<ul>
|
<ul>
|
||||||
{pinnedPosts.map(post => (
|
{pinnedPosts.map(post => (
|
||||||
<li>
|
<li>
|
||||||
<a href={`/${lang}/posts/${post.slug}/`}>
|
<a href={`/${lang}/posts/${post.data.slug || post.slug}/`}>
|
||||||
{post.data.title}
|
{post.data.title}
|
||||||
<time>({post.data.published.toISOString().split('T')[0]})</time>
|
<time>({post.data.published.toISOString().split('T')[0]})</time>
|
||||||
</a>
|
</a>
|
||||||
|
@ -35,7 +35,7 @@ const posts = await getPosts()
|
||||||
<ul>
|
<ul>
|
||||||
{posts.map(post => (
|
{posts.map(post => (
|
||||||
<li>
|
<li>
|
||||||
<a href={`/${lang}/posts/${post.slug}/`}>
|
<a href={`/${lang}/posts/${post.data.slug || post.slug}/`}>
|
||||||
{post.data.title}
|
{post.data.title}
|
||||||
<time>({post.data.published.toISOString().split('T')[0]})</time>
|
<time>({post.data.published.toISOString().split('T')[0]})</time>
|
||||||
</a>
|
</a>
|
||||||
|
|
|
@ -1,13 +1,23 @@
|
||||||
---
|
---
|
||||||
import { themeConfig } from '@/config'
|
import { themeConfig } from '@/config'
|
||||||
import Layout from '@/layouts/Layout.astro'
|
import Layout from '@/layouts/Layout.astro'
|
||||||
|
import { checkSlugDuplication } from '@/utils/content.config'
|
||||||
import { getCollection } from 'astro:content'
|
import { getCollection } from 'astro:content'
|
||||||
|
|
||||||
export async function getStaticPaths() {
|
export async function getStaticPaths() {
|
||||||
const posts = await getCollection('posts')
|
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 =>
|
return themeConfig.global.moreLocale.flatMap(lang =>
|
||||||
posts.map(post => ({
|
posts.map(post => ({
|
||||||
params: { lang, slug: post.slug },
|
params: {
|
||||||
|
lang,
|
||||||
|
slug: post.data.slug || post.slug,
|
||||||
|
},
|
||||||
props: { post },
|
props: { post },
|
||||||
})),
|
})),
|
||||||
)
|
)
|
||||||
|
|
|
@ -65,7 +65,7 @@ export async function GET({ params }: APIContext) {
|
||||||
pubDate: post.data.published,
|
pubDate: post.data.published,
|
||||||
description: post.data.description || getExcerpt(post.body),
|
description: post.data.description || getExcerpt(post.body),
|
||||||
// Generate absolute URL with language prefix
|
// 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
|
// Convert markdown content to HTML, allowing img tags
|
||||||
content: post.body
|
content: post.body
|
||||||
? sanitizeHtml(parser.render(post.body), {
|
? sanitizeHtml(parser.render(post.body), {
|
||||||
|
|
|
@ -32,7 +32,7 @@ const allTags = await getAllTags()
|
||||||
<ul>
|
<ul>
|
||||||
{posts.map(post => (
|
{posts.map(post => (
|
||||||
<li>
|
<li>
|
||||||
<a href={`/${lang}/posts/${post.slug}/`}>{post.data.title}</a>
|
<a href={`/${lang}/posts/${post.data.slug || post.slug}/`}>{post.data.title}</a>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
|
@ -13,7 +13,7 @@ const pinnedPosts = await getPinnedPosts()
|
||||||
<ul>
|
<ul>
|
||||||
{pinnedPosts.map(post => (
|
{pinnedPosts.map(post => (
|
||||||
<li>
|
<li>
|
||||||
<a href={`/posts/${post.slug}/`}>
|
<a href={`/posts/${post.data.slug || post.slug}/`}>
|
||||||
{post.data.title}
|
{post.data.title}
|
||||||
<time>({post.data.published.toISOString().split('T')[0]})</time>
|
<time>({post.data.published.toISOString().split('T')[0]})</time>
|
||||||
</a>
|
</a>
|
||||||
|
@ -27,7 +27,7 @@ const pinnedPosts = await getPinnedPosts()
|
||||||
<ul>
|
<ul>
|
||||||
{posts.map(post => (
|
{posts.map(post => (
|
||||||
<li>
|
<li>
|
||||||
<a href={`/posts/${post.slug}/`}>
|
<a href={`/posts/${post.data.slug || post.slug}/`}>
|
||||||
{post.data.title}
|
{post.data.title}
|
||||||
<time>({post.data.published.toISOString().split('T')[0]})</time>
|
<time>({post.data.published.toISOString().split('T')[0]})</time>
|
||||||
</a>
|
</a>
|
||||||
|
|
|
@ -1,11 +1,20 @@
|
||||||
---
|
---
|
||||||
import Layout from '@/layouts/Layout.astro'
|
import Layout from '@/layouts/Layout.astro'
|
||||||
|
import { checkSlugDuplication } from '@/utils/content.config'
|
||||||
import { getCollection } from 'astro:content'
|
import { getCollection } from 'astro:content'
|
||||||
|
|
||||||
export async function getStaticPaths() {
|
export async function getStaticPaths() {
|
||||||
const posts = await getCollection('posts')
|
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 => ({
|
return posts.map(post => ({
|
||||||
params: { slug: post.slug },
|
params: {
|
||||||
|
slug: post.data.slug || post.slug,
|
||||||
|
},
|
||||||
props: { post },
|
props: { post },
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
|
@ -42,7 +42,7 @@ export async function GET() {
|
||||||
pubDate: post.data.published,
|
pubDate: post.data.published,
|
||||||
description: post.data.description || getExcerpt(post.body),
|
description: post.data.description || getExcerpt(post.body),
|
||||||
// Generate absolute URL for post
|
// 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
|
// Convert markdown content to HTML, allowing img tags
|
||||||
content: post.body
|
content: post.body
|
||||||
? sanitizeHtml(parser.render(post.body), {
|
? sanitizeHtml(parser.render(post.body), {
|
||||||
|
|
|
@ -28,7 +28,7 @@ const allTags = await getAllTags()
|
||||||
<ul>
|
<ul>
|
||||||
{posts.map(post => (
|
{posts.map(post => (
|
||||||
<li>
|
<li>
|
||||||
<a href={`/posts/${post.slug}/`}>{post.data.title}</a>
|
<a href={`/posts/${post.data.slug || post.slug}/`}>{post.data.title}</a>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
|
@ -7,6 +7,31 @@ export type Post = CollectionEntry<'posts'>
|
||||||
export type PostData = Post['data']
|
export type PostData = Post['data']
|
||||||
export type PostsGroupByYear = Map<number, Post[]>
|
export type PostsGroupByYear = Map<number, Post[]>
|
||||||
|
|
||||||
|
// Check if the slug is duplicated under the same language.
|
||||||
|
export async function checkSlugDuplication(posts: Post[]): Promise<string[]> {
|
||||||
|
const slugMap = new Map<string, Set<string>>() // Map<lang, Set<slug>>
|
||||||
|
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)
|
// Get all posts except drafts (include pinned)
|
||||||
export async function getPosts(lang?: string) {
|
export async function getPosts(lang?: string) {
|
||||||
const defaultLocale = themeConfig.global.locale
|
const defaultLocale = themeConfig.global.locale
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue