fixed invalid post title breaking links

This commit is contained in:
2026-05-09 05:59:56 +02:00
parent bd18b96846
commit 801df057e0
6 changed files with 23 additions and 16 deletions
+14 -8
View File
@@ -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,
+1 -1
View File
@@ -16,7 +16,7 @@ const formattedDate = new Date(date).toLocaleDateString('en-US', {
});
---
<a href={`/posts/${slug}`} class="group block">
<a href={`/posts/${encodeURIComponent(slug)}`} class="group block">
<article class="glass p-5 md:p-8 transition-colors hover:bg-surface0/80 flex flex-col md:flex-row justify-between md:items-center gap-4 md:gap-6">
<div class="flex-1 min-w-0">
<div class="flex flex-wrap items-center gap-x-3 gap-y-1 mb-2 text-xs text-subtext0">
@@ -78,7 +78,7 @@ export default function Dashboard() {
<p className="text-xs text-subtext0" style={{ color: 'var(--subtext0)' }}>/posts/{post.slug}</p>
</div>
<div className="flex gap-3">
<a href={`/admin/editor?edit=${post.slug}`} className="p-2 text-blue hover:bg-blue/10 rounded transition-colors" title="Edit">
<a href={`/admin/editor?edit=${encodeURIComponent(post.slug)}`} className="p-2 text-blue hover:bg-blue/10 rounded transition-colors" title="Edit">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z"/><path d="m15 5 4 4"/></svg>
</a>
<button onClick={() => handleDelete(post.slug)} className="p-2 text-red hover:bg-red/10 rounded transition-colors" title="Delete">
@@ -218,7 +218,7 @@ export default function Editor({ editSlug }: Props) {
.map(t => t.trim())
.filter(Boolean);
try {
await savePost({
const saved = await savePost({
slug,
old_slug: originalSlug || null,
date,
@@ -228,7 +228,8 @@ export default function Editor({ editSlug }: Props) {
content,
});
showAlertMsg('Post saved!', 'success');
setOriginalSlug(slug);
if (saved?.slug && saved.slug !== slug) setSlug(saved.slug);
setOriginalSlug(saved?.slug ?? slug);
} catch (e) {
showAlertMsg(e instanceof ApiError ? `Error: ${e.message}` : 'Failed to connect to server.', 'error');
}
@@ -274,7 +275,7 @@ export default function Editor({ editSlug }: Props) {
</button>
{originalSlug && (
<a
href={`/posts/${originalSlug}`}
href={`/posts/${encodeURIComponent(originalSlug)}`}
target="_blank"
rel="noreferrer"
className="bg-blue text-crust font-bold py-3 px-8 rounded-lg hover:bg-sky transition-all transform hover:scale-105 whitespace-nowrap inline-flex items-center justify-center gap-2"
+1 -1
View File
@@ -51,7 +51,7 @@ export const GET: APIRoute = async ({ site }) => {
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 => ` <category>${escapeXml(t)}</category>`).join('\n');
+2 -2
View File
@@ -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';
</div>
<a
id="edit-link"
href={`/admin/editor?edit=${post.slug}`}
href={`/admin/editor?edit=${encodeURIComponent(post.slug)}`}
class={`bg-surface0 hover:bg-surface1 text-blue px-3 py-1.5 md:px-4 md:py-2 rounded border border-surface1 transition-colors inline-flex items-center gap-2 text-sm md:text-base self-start ${isAdmin ? '' : 'hidden'}`}
>
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="md:w-4 md:h-4"><path d="M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z"/><path d="m15 5 4 4"/></svg>