Skip to content

Commit 88cffac

Browse files
JacobCoffeeclaude
andcommitted
change post URLs from /blog/slug to /YYYY/MM/slug
Preserves the Blogger-era year/month URL structure as the canonical path so that blog.python.org/2026/02/python-3150-alpha-6/ works natively. Redirects from .html legacy URLs now just strip the extension at the same path instead of mapping to a completely different route. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 701f400 commit 88cffac

File tree

11 files changed

+352
-332
lines changed

11 files changed

+352
-332
lines changed

keystatic.config.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ export default config({
122122
format: { contentField: "content" },
123123
columns: ["publishDate", "author"],
124124
entryLayout: "content",
125-
previewUrl: "/blog/{slug}",
125+
// previewUrl requires computed year/month — not supported by Keystatic template syntax
126126
schema: {
127127
title: fields.slug({ name: { label: "Title" } }),
128128
publishDate: fields.date({ label: "Publish Date", validation: { isRequired: true } }),

scripts/migrate-blogger.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -460,9 +460,12 @@ async function migrate() {
460460
const mdxContent = `${frontmatter}\n\n${markdown}\n`;
461461
fs.writeFileSync(path.join(postDir, "index.md"), mdxContent, "utf-8");
462462

463-
// Track redirect: legacy path → new path
463+
// Track redirect: legacy .html path → route path (publishDate year/month + slug)
464464
if (legacyFilename) {
465-
redirects[legacyFilename] = `/blog/${slug}`;
465+
const pd = new Date(publishDate);
466+
const y = pd.getUTCFullYear();
467+
const m = String(pd.getUTCMonth() + 1).padStart(2, "0");
468+
redirects[legacyFilename] = `/${y}/${m}/${slug}`;
466469
}
467470

468471
migrated++;

src/components/BlogPostCard.astro

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
---
2-
import { formatDate, slugify, withBase } from "../lib/utils";
2+
import { formatDate, postUrl, slugify, withBase } from "../lib/utils";
33
44
interface Props {
55
slug: string;
@@ -18,7 +18,7 @@ const { slug, title, publishDate, author, description, tags, showEditLink = true
1818
<article class="post-card group relative px-5 py-4">
1919
<div class="flex items-start justify-between gap-4">
2020
<div class="min-w-0 flex-1">
21-
<a href={withBase(`/blog/${slug}`)} class="block">
21+
<a href={withBase(postUrl(slug, publishDate))} class="block">
2222
<h3 class="text-base font-semibold leading-snug text-zinc-900 transition-colors group-hover:text-[#306998] dark:text-zinc-100 dark:group-hover:text-[#ffd43b]" style="font-family: var(--font-display);">
2323
{title}
2424
</h3>

src/components/CommandPalette.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { useCallback, useEffect, useRef, useState } from "react";
55

66
interface PostEntry {
77
id: string;
8+
url: string;
89
title: string;
910
description: string;
1011
author: string;
@@ -292,7 +293,7 @@ export default function CommandPalette({ open: controlledOpen, onOpenChange }: P
292293
<Command.Item
293294
key={post.id}
294295
value={`post-${post.id}`}
295-
onSelect={() => navigate(withBase(`/blog/${post.id}`))}
296+
onSelect={() => navigate(withBase(post.url))}
296297
>
297298
<div className="cmdk-post-icon" aria-hidden="true">
298299
<ArticleIcon />

src/components/Footer.astro

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -57,12 +57,9 @@ const year = new Date().getFullYear();
5757
<p class="text-xs text-zinc-400 dark:text-zinc-500">
5858
&copy; {year} Python Software Foundation
5959
</p>
60-
<div class="flex items-center gap-1 text-xs text-zinc-400 dark:text-zinc-500">
61-
<span>Powered by</span>
62-
<a href="https://astro.build" class="underline decoration-zinc-300 underline-offset-2 hover:text-zinc-600 dark:decoration-zinc-600 dark:hover:text-zinc-300">Astro</a>
63-
<span>&</span>
64-
<a href="https://keystatic.com" class="underline decoration-zinc-300 underline-offset-2 hover:text-zinc-600 dark:decoration-zinc-600 dark:hover:text-zinc-300">Keystatic</a>
65-
</div>
60+
<p class="text-xs text-zinc-400 dark:text-zinc-500">
61+
<a href="https://creativecommons.org/licenses/by-nc-sa/3.0/" rel="noopener noreferrer" class="underline decoration-zinc-300 underline-offset-2 hover:text-zinc-600 dark:decoration-zinc-600 dark:hover:text-zinc-300">CC BY-NC-SA 3.0</a>
62+
</p>
6663
</div>
6764
</div>
6865
</footer>

src/data/redirects.json

Lines changed: 305 additions & 305 deletions
Large diffs are not rendered by default.

src/lib/utils.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,16 @@ export function withBase(path: string): string {
2424
return `${base}${path.startsWith("/") ? path : `/${path}`}`;
2525
}
2626

27+
/**
28+
* Returns the URL path for a blog post: /YYYY/MM/slug
29+
*/
30+
export function postUrl(slug: string, publishDate: string | Date): string {
31+
const d = typeof publishDate === "string" ? new Date(publishDate) : publishDate;
32+
const year = d.getUTCFullYear();
33+
const month = String(d.getUTCMonth() + 1).padStart(2, "0");
34+
return `/${year}/${month}/${slug}`;
35+
}
36+
2737
export function slugify(text: string): string {
2838
return text
2939
.normalize("NFD")
Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,29 @@
11
---
2-
import type { GetStaticPaths } from "astro";
3-
import type { CollectionEntry } from "astro:content";
4-
import BlogPostLayout from "../../layouts/BlogPostLayout.astro";
2+
import type { InferGetStaticPropsType } from "astro";
3+
import BlogPostLayout from "../../../layouts/BlogPostLayout.astro";
54
import { getCollection, render } from "astro:content";
65
76
export const prerender = true;
87
9-
export const getStaticPaths: GetStaticPaths = async () => {
8+
export async function getStaticPaths() {
109
const posts = await getCollection("posts");
1110
return posts
1211
.filter((p) => p.data.published)
13-
.map((post) => ({
14-
params: { slug: post.id },
15-
props: { post },
16-
}));
17-
};
12+
.map((post) => {
13+
const d = post.data.publishDate;
14+
return {
15+
params: {
16+
year: String(d.getUTCFullYear()),
17+
month: String(d.getUTCMonth() + 1).padStart(2, "0"),
18+
slug: post.id,
19+
},
20+
props: { post },
21+
};
22+
});
23+
}
1824
19-
const { post } = Astro.props as { post: CollectionEntry<"posts"> };
25+
type Props = InferGetStaticPropsType<typeof getStaticPaths>;
26+
const { post } = Astro.props;
2027
const { Content, remarkPluginFrontmatter } = await render(post);
2128
const references = (remarkPluginFrontmatter?.references ?? []) as Array<{ type: string; label: string; url: string }>;
2229
---

src/pages/index.astro

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ export const prerender = true;
33
44
import BaseLayout from "../layouts/BaseLayout.astro";
55
import PythonLogo from "../components/PythonLogo.astro";
6-
import { formatDate, withBase } from "../lib/utils";
6+
import { formatDate, postUrl, withBase } from "../lib/utils";
77
import { getCollection } from "astro:content";
88
99
const allPosts = await getCollection("posts");
@@ -51,7 +51,7 @@ const authors = new Set(posts.map((p) => p.data.author));
5151
<span class="text-xs font-bold uppercase tracking-[0.15em] text-[#306998] dark:text-[#ffd43b]">Latest</span>
5252
</div>
5353

54-
<a href={withBase(`/blog/${featured.id}`)} class="group block">
54+
<a href={withBase(postUrl(featured.id, featured.data.publishDate))} class="group block">
5555
<h1
5656
class="max-w-4xl text-3xl font-extrabold leading-[1.15] tracking-tight text-zinc-900 transition-colors group-hover:text-[#306998] dark:text-zinc-100 dark:group-hover:text-[#ffd43b] sm:text-4xl lg:text-5xl"
5757
style="font-family: var(--font-display); letter-spacing: -0.02em;"
@@ -121,7 +121,7 @@ const authors = new Set(posts.map((p) => p.data.author));
121121
<div class="grid gap-px overflow-hidden rounded-xl border border-zinc-200 bg-zinc-200 dark:border-zinc-800 dark:bg-zinc-800 sm:grid-cols-2">
122122
{recent.map((post) => (
123123
<article class="post-card group relative bg-white p-5 dark:bg-[#0f1117]">
124-
<a href={withBase(`/blog/${post.id}`)} class="block">
124+
<a href={withBase(postUrl(post.id, post.data.publishDate))} class="block">
125125
<h3 class="text-[0.9375rem] font-semibold leading-snug text-zinc-900 transition-colors group-hover:text-[#306998] dark:text-zinc-100 dark:group-hover:text-[#ffd43b]" style="font-family: var(--font-display);">
126126
{post.data.title}
127127
</h3>

src/pages/rss.xml.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import rss from "@astrojs/rss";
22
import type { APIContext } from "astro";
33
import { getCollection } from "astro:content";
4-
import { withBase } from "../lib/utils";
4+
import { postUrl, withBase } from "../lib/utils";
55

66
export const prerender = true;
77

@@ -20,7 +20,7 @@ export async function GET(context: APIContext) {
2020
title: post.data.title,
2121
pubDate: post.data.publishDate,
2222
description: post.data.description ?? "",
23-
link: withBase(`/blog/${post.id}/`),
23+
link: withBase(`${postUrl(post.id, post.data.publishDate)}/`),
2424
author: post.data.author,
2525
categories: post.data.tags,
2626
})),

0 commit comments

Comments
 (0)