Categories
Learn how to categorize posts, arrange them in order, and group them under one path using Categories.
Updated Aug 04, 2026
On this page
A category is a flat label with a name, a slug derived from that name, and an optional weight. There is no nesting and no per-post override: a post belongs to at most one category, and every post is offered the same list. Unlike a tag, a category changes the post's URL. Use a category when the path should say where a post lives, and tags for every other label.
Create a category
- Open Categories in the sidebar.
- Click New category.
- Type a name, up to 64 characters. plym derives the slug:
Getting Startedbecomesgetting-started. - Set Weight, or leave it blank.
- Click Add category.
You cannot type the slug. Rename the category to change it.
The API does the same thing:
curl -X POST http://localhost:9173/api/categories \
-H "Authorization: Bearer $PLYM_TOKEN" \
-H 'Content-Type: application/json' \
-d '{"name": "Getting Started", "weight": 10}'
{"id": 4, "name": "Getting Started", "slug": "getting-started", "weight": 10}
Assign a category to a post
Pick one from Category in the editor sidebar. The default is Uncategorised, which serves the post at the blog root.
path is the blog-root-relative URL and rendered_path is the file plym wrote for it. The category_id for Uncategorised posts is null.
Categories are global
A category exists once, for the whole site. There is no way to scope one to a section, an author, a template, or a user.
Two consequences follow. Renaming a category rewrites the URL of every published post in it, and deleting one is blocked while any post still uses it.
Weight
Weight orders the category list, lowest first. Categories with no weight sort last, alphabetically by name.
It orders the Categories screen and the GET /api/categories response. Weight changes no URL, and the bundled template renders no category listing, so on a default install it has no visible effect on the public site. A custom template reads post.category.weight from the render context and can group and order with it.
Paths change with the category
Assigning, changing, or clearing a category on a published post moves its URL immediately. plym deletes the old rendered file and writes a new one at the new path.
| Change | Previous URL | URL after the change |
|---|---|---|
Publish introduction, uncategorised |
— | /introduction |
| Assign Getting Started | /introduction returns 404 |
/getting-started/introduction |
| Rename to Quickstart | /getting-started/introduction returns 404 |
/quickstart/introduction |
| Clear the category | /quickstart/introduction returns 404 |
/introduction |
plym issues no redirect. The previous URL returns 404 the moment the move completes — not a 301, not a 308. Every inbound link, bookmark, and indexed result pointing at the old path breaks. This is architecture decision (partly due to SEO) and not a technical limitation.
Treat a category change on a published post as a URL change. Update your internal links, set the old URL as a redirect at your CDN or reverse proxy if the traffic matters, and let search engines re-crawl /sitemap.xml, which already lists the new path.
Draft posts have no rendered file, so moving them between categories costs nothing. Categorise before you publish where you can.
Rules
| Rule | Failure |
|---|---|
| Names are unique | 409 categories.conflict |
| A category slug may not equal an existing post slug | 409 categories.conflict |
A name may not resolve to a reserved segment: admin, api, health, mcp, media, page, plym-admin, plym-docs, static, webfonts |
400 categories.reserved_name |
| A name must have a URL-safe form | 400 categories.invalid_name |
| A category with posts assigned cannot be deleted | 409 categories.in_use |
The last rule has no override. Reassign or delete every post in a category before you delete the category itself; the error message tells you how many are left.
FAQ
Can I move 200 posts into a category at once? Not in one call. PATCH /api/posts/{id} takes one post at a time; loop over the ids. Each published post is re-rendered as it moves.