Skip to main content

splent_feature_theme

The public shell of a SPLENT product (public_base.html), the design tokens it is drawn with, a library of composable blocks, and the Appearance and Menus editors of the admin panel.

The theme never hardcodes a product name, palette or font. The product gives its identity through configuration, and its palette through THEME_TOKENS or a skin feature; the admin panel can change both at runtime. What this feature provides and requires is listed in the [tool.splent.contract] section of pyproject.toml, which splent feature:contract --write keeps in sync with the source.

The public shell

public_base.html is the website's page skeleton. A page names its shell rather than the file, {% extends shell('public') %}, which resolves to public_base.html unless the product's SPLENT_SHELL_PUBLIC names another layout. In order, the shell draws:

  • a <head> with the brand font, the tokens as an inline :root block, then every stylesheet the asset registry holds for the public shell, by order;
  • the site-header: the brand (logo, name or both), the main nav, the language switcher, and the far right of the nav;
  • the breadcrumb trail, the flashed messages, then the hero and the content;
  • the site-footer, with the footer links and the social links;
  • the registered scripts.

It never hardcodes a product name, palette or font. Pages fill the Jinja blocks title (the page's own name: the shell writes " · " into <title> and og:title), head, hero, content and scripts, and any feature adds markup through these slots (get_template_hooks), with no edit to the shell:

Slot Where
layout.head End of <head>, after the registered stylesheets
layout.body_start Right after <body> opens
layout.nav Inside the main <nav>, after the feature entries
layout.nav.end Far right of the <nav>, after the language switcher (the account control lives here)
layout.hero Before the hero block
layout.footer Inside the footer's social list
layout.scripts End of <body>, after the registered scripts

The theme fills none of the public slots itself. A hook meant for one page checks request.endpoint and returns "" elsewhere, and a page that steps aside only when a feature contributes uses render_template_hooks(name), which returns the slot's joined output, empty when nobody contributed (the home's home.hero works this way, see splent_feature_public).

The shell links whichever icons the product ships in its static folder (favicon.ico, favicon.svg, favicon-32.png, favicon-192.png, favicon-512.png, apple-touch-icon.png), and emits og:image only when the product ships static/og.png.

Design tokens

The look of the public shell comes from a small set of tokens, whose neutral defaults are DEFAULT_TOKENS in tokens.py: primary, primary_contrast, accent, bg, surface, text, heading, muted, border, radius, container, font_body, font_heading, font_display and font_url. Each becomes a CSS custom property, --brand-<key> with _ as - (primary_contrast is --brand-primary-contrast). font_url is the exception: the address of a font stylesheet, not a variable.

The default font_url is theme:inter: Inter, at the weights 400 to 700, served by the theme itself. frontend/package.json takes the font from npm (@fontsource-variable/inter, OFL-1.1), and the build writes the stylesheet of the fonts entry (assets/css/inter.css) and the font files, with the licence beside them (assets/inter.LICENSE.txt), into assets/dist. The rules and the files are the ones Google Fonts serves for family=Inter:wght@400;500;600;700, byte for byte, so the public shell looks the same and no page asks another site for its face.

get_tokens(app) merges three sources, highest first:

Source Which tokens Wins when
The Appearance editor's brand_primary, brand_accent, brand_bg, brand_surface, brand_text, brand_heading Those six colours The value is not empty
app.config["THEME_TOKENS"], the product's or its skin's Any token The value is not None
DEFAULT_TOKENS All Always, as the base

tokens_to_css renders the result as the :root { --brand-*: ... } block of the shell's <head>. The theme registers public.css at order 0, feature stylesheets sit around 100 and skins at 200, so the cascade is deterministic. public.css reads every colour, font and radius from a --brand-* variable, never a literal, so reskinning a product is a change of tokens, not of CSS. A few tokens have no default and only a skin sets them: the flashed messages' borders read --brand-success, --brand-warning and --brand-danger, with neutral fallbacks.

Skins

A skin is a light feature with no domain logic, models or routes. It sets THEME_TOKENS in its init_feature and registers its own stylesheet at order 200, so it cascades last and inherits every --brand-* variable:

def init_feature(app):
    from splent_framework.assets.asset_registry import register_asset

    app.config["THEME_TOKENS"] = OCEAN_TOKENS
    register_asset(
        "css", "skin_ocean.assets", order=200, subfolder="css", filename="skin_ocean.css"
    )

init_feature runs after the product's configuration, so with a skin installed the product's own THEME_TOKENS are replaced whole; such a product adjusts its colours in the Appearance editor. Installing or removing a skin changes the whole public aesthetic with no edit to the theme or the product's templates. A skin may also override a block template through [tool.splent.refinement].

What templates get

inject_context_vars gives every template:

Variable What it is
theme_tokens The merged tokens
theme_tokens_css Their :root block
render_block(name, **context) Renders templates/blocks/<name>.html as Markup, so it is inserted unescaped
auto_breadcrumb(page_title, mode) The trail derived from the address, the page title and the composed nav, or None where the mode hides it
site_title(page_title) The document title, the page's name then the site's
page_shows_flash(content, message) Whether the page already shows a flashed message (below)
site The product's identity, nav and locales (below)

The blocks are hero and breadcrumb, and they pair with the component classes of public.css (.block-hero, .card-grid, .feature-grid, .cta-banner, .countdown, .logo-wall, .gallery-grid...), so the look stays the same across products and skins.

{{ render_block('hero', title=site.name, subtitle=site.tagline) }}

The site object

The theme builds one site object for templates. Its name, tagline and logo come from the Appearance editor first (site_name, site_tagline, site_logo), then from app.config; the rest from app.config alone, so a product sets them in its [tool.splent.config] (or a config.py of its own, which wins over the environment).

Config key site.* Used for
SITE_NAME name The brand name, the title, the footer. Falls back to SPLENT_APP, then Site
SITE_TAGLINE tagline The meta description, the hero's subtitle
SITE_LOGO logo The brand logo, a path inside the product's static folder
SITE_SOCIAL social The footer's social links, {network, href} each
SITE_HERO_EYEBROW, SITE_HERO_ACTIONS hero_eyebrow, hero_actions The line above the home's hero title, and its buttons ({label, href, class})
SITE_HIGHLIGHTS_TITLE, SITE_HIGHLIGHTS highlights_title, highlights The highlight grid of the home ({icon, title, text} each)
SITE_CTA cta The home's closing call to action ({title, text, href, button})
SITE_EVENT event The countdown and event line of the home's hero ({edition, dates, venue, iso}), when no feature fills home.hero
SITE_SPONSORS, SITE_SPONSORS_TITLE sponsors, sponsors_title A sponsor logo wall, for a product without a partners feature
SITE_GALLERY, SITE_GALLERY_TITLE gallery, gallery_title An image gallery of static paths, for a product without media
SITE_NAV nav The header nav, only when no feature registered an entry and no Menus override exists

SITE_SPONSORS_TITLE and SITE_GALLERY_TITLE default to "Patrocinadores" and "Galería". site.footer_nav has no config key: it is the row of custom links the Menus editor stores. site.locale and site.locales (BABEL_SUPPORTED_LOCALES) drive the language switcher, which appears only when more than one locale is supported.

Each content feature declares its public nav entry in init_feature with register_nav_item(key, label, href, order) from splent_framework.nav.nav_registry, so the menu follows the product's derivation. The Menus editor (/admin/menus) stores an override in the settings store, key site_nav, a JSON list in menu order that reorders, hides or relabels those entries and adds custom links:

[
  {"type": "feature", "key": "projects", "visible": true},
  {"type": "feature", "key": "team", "label": "Our team", "visible": false},
  {"type": "custom", "label": "Old site", "href": "https://example.org"}
]

compose_nav reconciles both at render time (nav.py):

  • the override drives order, visibility and labels; the entries of features no longer installed are dropped, and registered entries the override does not list are appended;
  • without an override the registered entries render by order, and without any, SITE_NAV;
  • labels go through gettext, so they follow the active locale;
  • a custom link keeps only an http, https, mailto, / or # address, so no javascript: or data: link renders;
  • an entry with children is drawn as a dropdown, and stays a link itself.

The shell marks at most one entry current, with is-active and aria-current="page": an entry matches when the request's path is its address or lies below it, the longest address wins, and the home entry (/) matches only the home.

The footer has its own row of custom links from the same editor, under site_footer_nav ({label, href} in order, at most 20), drawn before the social links when not empty.

Flashed messages

The shell shows what a feature says with Flask's flash() in a container site-flashes block between the header and the hero, one <div class="alert alert-<kind>"> each:

Category passed to flash() Shown as Announced
success, info, warning That kind Politely (role="status")
danger, error danger At once (role="alert")
Anything else info Politely

A page that shows its messages in a box of its own (a contact form, a login page on the public layout) does not get them twice: the shell renders the page first and leaves out any message that is the whole text of an element of it. The same words inside a sentence do not count.

Breadcrumbs

The shell draws a trail under the header on its own: Home, then every level of the address, named by the nav entry that points at it (so a rename in the Menus editor renames the crumb) or by the segment itself, a slug turned into words, then the page's name. SITE_BREADCRUMBS, which the Appearance editor can change, says where: deep (the default) only on pages two levels down or more, all on every page but the home, off never. A page that passes an explicit breadcrumb list gets exactly that list.

Scripts every page gets

The theme registers them at order 0, so no content feature ships its own:

  • countdown.js drives <div class="countdown" data-target="<ISO date>"> with data-d, data-h, data-m and data-s spans, so anything that announces a date gets a live timer.
  • lightbox.js publishes the sticky header's height as --site-header-offset, and opens over the darkened page every image of rich text (.prose), anything marked data-lightbox, and every image of a gallery (.gallery-grid or data-lightbox-gallery), with previous and next, keyboard and swipe navigation, and a slideshow inside a gallery (data-slideshow-ms sets its pace). A thumbnail wrapped in a link to the full-size file opens that file, never in a new tab, so it degrades to a plain link. The control labels come from the data-lightbox-* attributes of <body>. A page drives it through window.splentLightbox (open(img, {play}), next(), prev(), refresh(), close()); a gallery that grows during the slideshow listens for splent:lightbox:end on its grid, appends the next page and calls refresh() then next().
  • code_blocks.js labels each <pre><code class="language-..."> with its language and adds a copy button.

The 404 page, robots.txt and the language

The theme answers the 404 itself, in the public shell with the product's skin and the language in force: its handler is registered from init_feature, after the framework's, so it replaces the bare 404.html older products ship. The Appearance editor edits its title, text and button (site_404_title, site_404_text, site_404_cta), and a product that wants a page of its own shadows errors/404.html in its templates.

/robots.txt lets every crawler in. /lang/<code> stores the reader's language in the session and sends them back to the page they came from, when it is on this site, else to the home.

Menu entries

The editors register two entries in the Administration section of the application shell's menu registry. Neither names roles: each takes the ones its screen asks for (routes.PANEL_ROLES, administrators), so nobody is offered a door that answers 403.

Key Label Order Icon Opens
theme_appearance Appearance 40 sliders theme.admin_appearance
theme_menus Menus 50 menu theme.admin_menus

A product hides or reorders them by key in [tool.splent.layout.menu].

# the product's pyproject.toml
[tool.splent.layout.menu]
hide = ["theme_menus"]

The layout.authenticated_sidebar hooks that drew the same two entries stay for one minor, for the shells that still render slots. A shell draws the registry or the slots, never both.

The Appearance editor

/admin/appearance, for administrators, edits the site's name, tagline, logo, header brand, logo height, breadcrumbs, the 404 page's copy and the six colours of the tokens. Each field is stored in the settings store under its key in small letters (site_name for SITE_NAME), the runtime parameters of the theme, so param('theme', 'SITE_NAME') returns what the editor stored too (framework 1.23 and later).

A Save stores only what the administrator changed, as the settings panel does: a field posted as it was shown writes nothing, and one set back to what the product says clears the override that was there. The form shows what is in force for a field nobody set in the panel (the skin's colours, the header and breadcrumb choices of the product's environment), so pressing Save does not freeze them into the database, and a later release of the skin can still change the colours nobody chose.

Configuration

Declared in [tool.splent.config], with their types in [tool.splent.config_schema]. A product overrides them in its own [tool.splent.config]. The runtime ones are also the Appearance editor's, which wins over all of them where it has a value.

Key Type Bind Default What it does
SITE_NAME str runtime the package name What the site calls itself in the header, the browser tab and the footer.
SITE_TAGLINE str runtime empty The line under the name.
SITE_LOGO str runtime empty The header logo, a path inside the product's static folder. Empty draws no logo.
SITE_BRAND_MODE enum runtime both How the header brand renders: logo, text or both.
SITE_LOGO_SIZE int, 16 to 200 runtime 40 The logo height in pixels.
SITE_BREADCRUMBS enum runtime deep Where the automatic breadcrumb trail appears: deep, all or off.
THEME_TOKENS map derivation the theme's defaults The palette and fonts, a JSON object of token name to CSS value.

The keys of The site object other than these are read from app.config and have no schema.

THEME_TOKENS takes the names of DEFAULT_TOKENS in tokens.py and goes over them, so a product writes only what it changes. An empty font_url loads no web font, and the fonts named in font_body, font_heading and font_display then come from the reader's system. Any other address is linked as it is: a skin that names a Google Fonts stylesheet loads it from there.

# the product's pyproject.toml
[tool.splent.config]
SITE_NAME = "uvlhub"
SITE_LOGO = "media/logos/default.svg"
SITE_BRAND_MODE = "logo"
THEME_TOKENS = '{"primary": "#287FB9", "font_url": "", "font_body": "system-ui, sans-serif"}'

A THEME_TOKENS that is not valid JSON, is not an object, names a token the theme does not have or gives one a value that is not a string stops the boot with a message that says which, instead of leaving the default palette with nothing in the log. A skin that sets THEME_TOKENS in its init_feature replaces the product's whole.

SITE_LOGO and THEME_TOKENS are read from the environment from theme 0.14.0; before it only a product's config.py could set them.

It requires settings, whose store keeps the editors' values.

Use it in a product

splent feature:add splent_feature_theme

Develop

splent feature:test splent_feature_theme
splent feature:contract splent_feature_theme

Metadata

Release files for splent-feature-theme 0.16.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 splent-feature-theme 0.16.0
File Size Uploaded Provenance Actions
splent_feature_theme-0.16.0.tar.gz 311.7 kB Without provenance Details for splent_feature_theme-0.16.0.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for splent-feature-theme 0.16.0
File Interpreter ABI Platform Actions
splent_feature_theme-0.16.0-py3-none-any.whl Python 3 none any Details for splent_feature_theme-0.16.0-py3-none-any.whl

Total release size: 627.6 kB

Release files / splent_feature_theme-0.16.0.tar.gz

Download URL splent_feature_theme-0.16.0.tar.gz
Size 311.7 kB
Tags Source
SHA-256 checksum
How to use checksums
fdf5609f21b5c6ef4a4d1cacc9592c8c1b8f37988fc67dbf5c9456aa96caea1b
BLAKE2b-256 checksum
How to use checksums
88058f0d5b2d4f30efe71baace6b4c04f98ff6b902febcadf5947f03f8dc19d7
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.1.0 CPython/3.13.16

Release files / splent_feature_theme-0.16.0-py3-none-any.whl

Download URL splent_feature_theme-0.16.0-py3-none-any.whl
Size 315.9 kB
Tags Python 3
SHA-256 checksum
How to use checksums
43b6e23d5610c630ba1c4a65448fbb33b3a5623293c9115072803edc5f5d13b3
BLAKE2b-256 checksum
How to use checksums
5c76a9fab089b0a0e08bfcdc471e6b73c05e7cd6258ed913a9648bca2b3f1212
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.1.0 CPython/3.13.16

Security for splent-feature-theme 0.16.0

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.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

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