Content Collections
Content collections are SPRAG's markdown-backed route scaffold. They create a route tree, a matching app/content/<name>/ directory, and the static_paths wiring needed for static builds.
What you get
Run:
sprag add content posts
SPRAG scaffolds:
app/content/posts/
getting-started.md
app/content_support.py
app/routes/posts/
page.py
server.py
web.py
[...segments]/
page.py
server.py
web.py
app/content/posts/holds your markdown files.app/routes/posts/page.pyserves the collection index at/posts.app/routes/posts/[...segments]/page.pyserves individual markdown documents.app/content_support.pyadds reusable helpers for loading markdown, looking up a document, and generatingstatic_paths.
When to use this vs a hand-rolled dynamic route
Use a content collection when:
- your route tree is backed by markdown or filesystem content
- you want static path expansion generated from those files
- you want a quick docs/blog/guides scaffold without writing the route plumbing yourself
Use a hand-rolled [slug] or [...segments] route when:
- the route reads from a live API or database
- path discovery depends on runtime data rather than files in the repo
- you need custom loading rules that do not map cleanly to a markdown tree
What sprag add content wires for you
The generated article route uses content_static_paths() automatically:
from app.content_support import content_static_paths
def posts_static_paths():
return content_static_paths("posts", base_url="/posts")
That helper walks app/content/posts/, turns each markdown document into a URL path, and feeds those params into the catch-all route's static_paths=....
Base URLs
base_url is the route prefix for the collection inside your app. Use it when the markdown folder name and the public route do not match, or when you want to keep the prefix in one shared constant.
from sprag import join_url, load_markdown_tree
POSTS_BASE_URL = join_url("/", "posts")
def posts_collection():
return load_markdown_tree(CONTENT_ROOT / "posts", base_url=POSTS_BASE_URL)
join_url() is the first-class helper for composing route prefixes and URL segments. It normalizes slashes, so join_url("/posts", "hello-world") returns /posts/hello-world.
For static deployments under a host path such as GitHub Pages, keep author-facing links root-relative (/posts, /static/...). SPRAG rewrites rendered internal href, src, and action attributes to relative URLs during the static build so the generated HTML works from nested pages and path-prefixed hosts.
The generated article controller also exposes a ready-to-render payload:
docfor the current markdown documentdocsfor the full collectionroute_pathandcurrent_pathfor navigation/UIcollection_pathso the scaffold can point you to the source folder
End-to-end flow
1. Scaffold the collection:
sprag add content posts
2. Add a markdown file such as app/content/posts/hello-world.md.
3. Confirm the routes:
sprag routes
You should see /posts and /posts/[...segments].
4. Build the static site:
sprag build static
SPRAG expands the collection through static_paths and emits the corresponding HTML files under dist/.
Authoring notes
- Collection names may be nested, such as
docs/guides, which scaffolds both nested routes and nested content folders. - The scaffold creates document-mode pages because markdown content is usually server-rendered first.
app/content_support.pyis created once; latersprag add content ...runs reuse it.
If you already know you need a custom controller contract or live-service-backed routing, start from Routes instead.