From 801df057e0d511f2c39a9d95d43304464cf64c55 Mon Sep 17 00:00:00 2001 From: Nils Pukropp Date: Sat, 9 May 2026 05:59:56 +0200 Subject: [PATCH] fixed invalid post title breaking links --- backend/src/handlers/posts.rs | 22 ++++++++++++------- frontend/src/components/PostCard.astro | 2 +- .../src/components/react/admin/Dashboard.tsx | 2 +- .../src/components/react/admin/Editor.tsx | 7 +++--- frontend/src/pages/feed.xml.ts | 2 +- frontend/src/pages/posts/[slug].astro | 4 ++-- 6 files changed, 23 insertions(+), 16 deletions(-) diff --git a/backend/src/handlers/posts.rs b/backend/src/handlers/posts.rs index 2371989..27f7d7b 100644 --- a/backend/src/handlers/posts.rs +++ b/backend/src/handlers/posts.rs @@ -115,16 +115,22 @@ pub async fn create_post( return Err(AppError::Unauthorized); } - validate_slug(&payload.slug)?; + let slug = slug::slugify(&payload.slug); + if slug.is_empty() { + return Err(AppError::BadRequest( + "Slug is empty after normalization (try ASCII letters/numbers)".to_string(), + )); + } + validate_slug(&slug)?; if let Some(ref old) = payload.old_slug { validate_slug(old)?; } let posts_dir = state.data_dir.join("posts"); - let file_path = posts_dir.join(format!("{}.md", payload.slug)); + let file_path = posts_dir.join(format!("{}.md", slug)); if let Some(ref old_slug) = payload.old_slug { - if old_slug != &payload.slug { + if old_slug != &slug { let old_path = posts_dir.join(format!("{}.md", old_slug)); if old_path.exists() { if file_path.exists() { @@ -134,11 +140,11 @@ pub async fn create_post( } let _guard = state.post_lock.lock().await; fs::rename(&old_path, &file_path).map_err(|e| { - error!("Rename error from {} to {}: {}", old_slug, payload.slug, e); + error!("Rename error from {} to {}: {}", old_slug, slug, e); AppError::Internal("Rename error".to_string(), Some(e.to_string())) })?; drop(_guard); - info!("Renamed post from {} to {}", old_slug, payload.slug); + info!("Renamed post from {} to {}", old_slug, slug); } } } @@ -150,11 +156,11 @@ pub async fn create_post( draft: payload.draft, }; let contents = serialize_post(&meta, &payload.content)?; - write_post_atomic(&state, &payload.slug, &contents).await?; + write_post_atomic(&state, &slug, &contents).await?; - info!("Post saved: {}", payload.slug); + info!("Post saved: {}", slug); Ok(Json(PostDetail { - slug: payload.slug, + slug, date: meta.date, summary: meta.summary, tags: meta.tags, diff --git a/frontend/src/components/PostCard.astro b/frontend/src/components/PostCard.astro index a91d3d5..447bc20 100644 --- a/frontend/src/components/PostCard.astro +++ b/frontend/src/components/PostCard.astro @@ -16,7 +16,7 @@ const formattedDate = new Date(date).toLocaleDateString('en-US', { }); --- - +
diff --git a/frontend/src/components/react/admin/Dashboard.tsx b/frontend/src/components/react/admin/Dashboard.tsx index 5ac1f19..5fbcda7 100644 --- a/frontend/src/components/react/admin/Dashboard.tsx +++ b/frontend/src/components/react/admin/Dashboard.tsx @@ -78,7 +78,7 @@ export default function Dashboard() {

/posts/{post.slug}

- + {originalSlug && ( { const items = posts .filter(p => !p.draft) .map(p => { - const url = `${origin}/posts/${p.slug}`; + const url = `${origin}/posts/${encodeURIComponent(p.slug)}`; const description = p.summary || p.excerpt || ''; const pubDate = new Date(p.date).toUTCString(); const categories = p.tags.map(t => ` ${escapeXml(t)}`).join('\n'); diff --git a/frontend/src/pages/posts/[slug].astro b/frontend/src/pages/posts/[slug].astro index cee8fb6..a0c5434 100644 --- a/frontend/src/pages/posts/[slug].astro +++ b/frontend/src/pages/posts/[slug].astro @@ -24,7 +24,7 @@ let html = ''; let error = ''; try { - const response = await fetch(`${API_URL}/api/posts/${slug}`); + const response = await fetch(`${API_URL}/api/posts/${encodeURIComponent(slug ?? '')}`); if (response.ok) { post = await response.json(); html = renderMarkdown(post!.content); @@ -98,7 +98,7 @@ const isAdmin = Astro.cookies.get('admin_session')?.value === '1';