Metadata-Version: 2.3
Name: site-blocks
Version: 0.1.0
Summary: Shared rendering primitives, chrome templates, and theme CSS for lucascorcodilos.com and its sibling sites
Requires-Dist: jinja2>=3.1.6
Requires-Dist: markdown-it-py>=3.0.0
Requires-Dist: mdit-py-plugins>=0.4.1
Requires-Dist: pyyaml>=6.0.2
Requires-Dist: pygments>=2.18.0
Requires-Python: >=3.13
Description-Content-Type: text/markdown

# 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:

```python
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:

```python
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

```bash
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.

```bash
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.

```bash
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.
