New Feature·Pushed May 1, 2026·S
Story URLs now include headline keywords for SEO
Story links now include a keyword-rich slug derived from the headline, replacing opaque IDs with readable URLs that search engines can parse.
Story URLs on the changelog site were opaque strings like [[code]]/stories/pr-2/[[/code]] or [[code]]/stories/commit-abc1234/[[/code]]. Search engines crawling these paths saw no semantic signal — no indication of what the story was about. Now every story gets a keyword-rich slug appended to its URL, derived from the headline text.
The slug is generated by converting the headline to lowercase, replacing non-alphanumeric characters with hyphens, and truncating at a word boundary within 80 characters. For example, a story about adding SEO-friendly path slugs gets the URL [[code]]/stories/test-stub/adding-seo-friendly-path-slugs/[[/code]]. The story ID remains part of the URL to ensure reliable routing even if headlines change or produce identical slugs.
This change touches the sitemap, canonical meta tags, OpenGraph metadata, and all internal navigation links across the site. URL construction is now centralized in a new helper module, replacing inline template strings scattered across components. The route structure was updated so the slug is a real route segment, enabling static generation of the new URL format.
Technical description
Story URLs on the changelog site were opaque IDs ([[code]]/stories/pr-2/[[/code]]), providing no semantic signal to search engines. This PR adds keyword-rich slugs derived from story headlines to the URL path.
The implementation introduces two new utility modules. [[code ref=1]]slugify[[/code]] converts headlines to URL-friendly slugs: lowercase, hyphens for non-alphanumeric characters, 80-character limit with truncation at a word boundary. [[code ref=2]]storyPath[[/code]] and its sibling helpers in [[code]]lib/urls.ts[[/code]] build the canonical URL shape [[code]]/stories/<id>/<slug>/[[/code]]. These replace inline template strings across the codebase.
The story route was migrated from [[code]][id]/page.tsx[[/code]] to [[code]][id]/[slug]/page.tsx[[/code]], and similarly for the OpenGraph image route. Both [[code ref=3]][slug]/page.tsx[[/code]] and [[code ref=4]][slug]/opengraph-image.tsx[[/code]] receive the slug as a route parameter. The [[code]]generateStaticParams[[/code]] function now returns [[code]]{id, slug}[[/code]] pairs for static generation.
All internal navigation was updated: [[code ref=5]]PRFeedItem[[/code]], [[code ref=6]]FixesBrief[[/code]], [[code ref=7]]HousekeepingDrawer[[/code]], and the sitemap all use [[code ref=9]]storyPath(story)[[/code]] instead of inline templates. SEO metadata in [[code ref=8]]lib/seo.ts[[/code]] now points canonical URLs and OpenGraph images to the slug-formatted paths.
````mermaid
graph LR
A[Story headline] --> B[[code]]slugify[[/code]]
B --> C[URL slug]
C --> D[[code]]storyPath[[/code]]
D --> E[Canonical URL]
D --> F[Og Image URL]
E --> G[sitemap.xml]
E --> H[canonical meta]
F --> I[og:image meta]
G --> J[crawler index]
H --> J
I --> K[social shares]
````
Files at a Glance:
- [[code]]lib/utils/slugify.ts[[/code]] — slug generation utility
- [[code]]lib/urls.ts[[/code]] — centralized URL builders
- [[code]]app/stories/[id]/[slug]/page.tsx[[/code]] — story page route (moved)
- [[code]]app/stories/[id]/[slug]/opengraph-image.tsx[[/code]] — og image route (moved)
- [[code]]lib/seo.ts[[/code]] — canonical and og metadata updated
- [[code]]app/sitemap.ts[[/code]] — sitemap entries updated
- [[code]]components/PRFeedItem.tsx[[/code]] — internal links updated
- [[code]]components/FixesBrief.tsx[[/code]] — internal links updated
- [[code]]components/HousekeepingDrawer.tsx[[/code]] — internal links updated
Categories
- New Feature (85%) — Primary purpose is adding SEO-friendly story URLs with keyword-rich slugs
- Refactoring (15%) — URL building logic centralized into helper functions, replacing inline template strings across multiple components