site-blocks
A small shared layer for hand-authored static sites built with Python and Jinja2.
It holds the parts that are the same whatever the site is about: a set of presentational dataclasses and the partials that render them, the page chrome, a design-token stylesheet, and a Markdown pipeline with wikilinks, backlinks, footnotes and syntax highlighting.
It deliberately does not hold the parts that make a site itself. How content is discovered, what a document's frontmatter must contain, what pages get emitted and how they are laid out all stay in the site that needs them.
What is in here
| Module | Purpose |
|---|---|
elements.py |
Presentational primitives (HTML, P, Bullets, Dropdown, Card). Sites subclass these. |
render.py |
Jinja environment construction and page writing. |
assets.py |
Concatenates the theme CSS and copies shipped JS into a site's output. |
frontmatter.py |
Split, require, coerce and publish-gate a content file's YAML block. |
markdown.py |
Markdown to HTML: anchors, footnotes, task lists, tables, Pygments. |
links.py |
[[Wikilinks]], resolution across content types, and the backlink index. |
figures.py |
```figure fences to Plotly markup, with static/js/figures.js. |
slug.py, errors.py |
Slugs, and the one exception type the pipeline raises. |
templates/ |
Eight chrome partials: head, header, main, and the accordion set. |
css/ |
Four opt-in stylesheets: tokens, base, chrome, components. |
Using it from a site
Add the dependency:
uv add https://github.com/lcorcodilos/site-blocks.git
Build an environment with the site's own templates taking precedence:
from pathlib import Path
from site_blocks.render import make_environment, render_page, package_templates
env = make_environment(
template_dirs=[Path("templates"), package_templates()],
site_title="...",
base_url="https://...",
nav_items=[{"label": "About", "href": "/about/"}],
)
render_page(env, "home.html", Path("dist/index.html"), page_title="Home")
template_dirs is a search path, resolved left to right: a template name is
looked up in each directory in turn and the first match wins. A name present
in an earlier directory therefore shadows the same name in a later one, and
nothing is merged.
Page structure
main.html is a skeleton, not a base class to extend directly. It owns the
document, the head, the header and the accordion script, and exposes two
blocks:
layout— everything between the header and the scriptsscripts— per-page script tags, for loading something heavy only where it is needed
A site provides one template filling layout with its own page structure, and
its page templates extend that.
CSS
The four stylesheets are opt-in. A site picks the ones it wants and
concatenates its own main.css last, so it can override any token:
from site_blocks.assets import build_css
build_css(
out_path=Path("dist/css/main.css"),
include=["tokens", "base", "chrome", "components"],
site_css=Path("css/main.css"),
)
tokens.css declares custom property names; values are overridable because
the site's stylesheet comes last in the concatenation.
Selectors here are class-based (
.site-header, notheader). A site rule written against a bare type selector will lose to one of these on specificity, silently. Scope site overrides to the same class.
Development
uv sync # install, including dev dependencies
uv run pytest # run the tests
uv run ruff check . # lint
uv run ruff format . # format
Tools
tools/verify_build.py and tools/verify_css.py compare a built site tree
against a snapshot taken before a change, so a refactor can be shown to have
altered nothing a reader would notice.
python tools/verify_build.py <golden-dir> <new-dir>
python tools/verify_css.py <golden.css> <new.css>
See their docstrings for what each does and does not prove — in particular,
verify_css.py shows no rule was lost or altered, but not that the cascade is
unchanged. Reordering can flip which of two matching rules wins, and only
comparing computed styles in a browser will catch that.
Releases
Tag a commit and push the tag; the release workflow builds the wheel and sdist and attaches them to a GitHub release.
uv version 0.2.0
git commit -am "Release 0.2.0" && git push
git tag 0.2.0 && git push origin 0.2.0
Tags are bare X.Y.Z with no leading v, and must match the version in
pyproject.toml exactly; the workflow fails if they do not.
A second job in the same workflow then uploads to PyPI via trusted publishing. It does not rebuild: it downloads the artifacts the first job attached to the release, so what reaches PyPI is the file that was linted and tested.
Both jobs are in one workflow on purpose. Events raised by GITHUB_TOKEN do
not start new workflow runs, so a separate workflow keyed on
release: published would never fire -- and would fail silently, since a
workflow that does not run reports nothing.
Metadata
Release files for site-blocks 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| site_blocks-0.1.0.tar.gz | 25.3 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| site_blocks-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 58.2 kB
Release files / site_blocks-0.1.0.tar.gz
| Download URL | site_blocks-0.1.0.tar.gz |
|---|---|
| Size | 25.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
b0ad342933746167fdba7fccfb642fddeee3a4de696f5727cea8cc5769df7638
|
|
BLAKE2b-256 checksum How to use checksums |
c52cb718cf8dcc4c36e3da332dc2d5cc6dc2bf4ed0c2031ba441a6bde1c82f44
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.12.13 {"installer":{"name":"uv","version":"0.12.13","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 14, 2026.
Transparency logRelease files / site_blocks-0.1.0-py3-none-any.whl
| Download URL | site_blocks-0.1.0-py3-none-any.whl |
|---|---|
| Size | 32.9 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
5abf2b42537830aa2dbfbdea24af1aa8ba0bae4b6c1f3f8f9e8565f95f314b57
|
|
BLAKE2b-256 checksum How to use checksums |
883036b022db1eb7c82eb1d2caa1cfbd7557d7d4375cc94bd57e260e63f366a9
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.12.13 {"installer":{"name":"uv","version":"0.12.13","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 14, 2026.
Transparency log