Skip to main content

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 scripts
  • scripts — 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, not header). 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)

Source distribution for site-blocks 0.1.0
File Size Uploaded
site_blocks-0.1.0.tar.gz 25.3 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for site-blocks 0.1.0
File Interpreter ABI Platform
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 log

Release 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

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page