This document describes how to organize blog content for deterministic URL generation.
All blog posts must follow this structure:
content/blog/
└── {collection}/ # Collection folder (becomes the URL slug)
└── index.md # Required: exactly one markdown file
content/blog/
├── playlist-reinvent-2019/
│ └── index.md # URL: /playlist-reinvent-2019/
├── my-first-post/
│ └── index.md # URL: /my-first-post/
└── getting-started-aws/
└── index.md # URL: /getting-started-aws/
| Rule | Good Example | Bad Example |
|---|---|---|
| Use lowercase | my-post/ |
My-Post/ |
| Use hyphens for spaces | my-first-post/ |
my first post/ |
| ASCII characters only | cafe/ |
café/ |
| No special characters | my-post/ |
my_post!/ |
| No numbers at start | post-2024/ |
2024-post/ (ok but avoid) |
The URL slug is derived directly from the collection folder name:
- The folder name is converted to lowercase
- Spaces are replaced with hyphens
- Non-ASCII characters are transliterated (e.g.,
é→e) - Special characters are removed
- Consecutive hyphens are collapsed
- Leading/trailing hyphens are trimmed
| Folder Name | Generated Slug |
|---|---|
my-post |
/my-post/ |
My-Post |
/my-post/ |
My First Post |
/my-first-post/ |
café-guide |
/cafe-guide/ |
post--with--hyphens |
/post-with-hyphens/ |
Before creating a post, preview what the URL will be:
npm run slug:preview -- "content/blog/my-new-post/index.md"
# Output: /my-new-post/Check that all content files match the canonical manifest:
npm run verify-slugsWhen you intentionally add or rename posts, update the manifest:
npm run slug:update-manifestThen commit the updated manifest file.
-
Create the collection folder:
mkdir -p content/blog/my-new-topic
-
Create index.md with frontmatter:
--- title: "My New Topic" date: 2025-01-15 description: "A brief description of the post" --- Your content here...
-
Preview the slug (optional):
npm run slug:preview -- "content/blog/my-new-topic/index.md" -
Update the manifest:
npm run slug:update-manifest
-
Commit your changes:
git add content/blog/my-new-topic/ git add specs/004-preserve-slugs/slug-manifest.json git commit -m "feat: add my-new-topic post"
The verify-slugs command runs automatically before builds:
- Build fails if any slug doesn't match the manifest
- Build fails if folder structure is invalid
- Build fails if slug collision is detected (two posts with same slug)
Your post folder is nested too deeply. Move it to content/blog/{collection}/index.md.
# Wrong
content/blog/2024/january/my-post/index.md
# Correct
content/blog/my-post/index.mdEach post folder must contain exactly one index.md file.
# Wrong
content/blog/my-post/
└── post.md
# Correct
content/blog/my-post/
└── index.mdThe generated slug doesn't match the canonical manifest. Either:
- Unintentional change: Restore the original folder name
- Intentional change: Update manifest with
npm run slug:update-manifest
Two posts resolve to the same URL. Rename one of the conflicting folders.
# Collision example
content/blog/my-post/index.md # → /my-post/
content/blog/My-Post/index.md # → /my-post/ (collision!)