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:rootblock, then every stylesheet the asset registry holds for the public shell, byorder; - 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.
The main nav and the footer links
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 nojavascript:ordata:link renders; - an entry with
childrenis 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.jsdrives<div class="countdown" data-target="<ISO date>">withdata-d,data-h,data-manddata-sspans, so anything that announces a date gets a live timer.lightbox.jspublishes the sticky header's height as--site-header-offset, and opens over the darkened page every image of rich text (.prose), anything markeddata-lightbox, and every image of a gallery (.gallery-gridordata-lightbox-gallery), with previous and next, keyboard and swipe navigation, and a slideshow inside a gallery (data-slideshow-mssets 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 thedata-lightbox-*attributes of<body>. A page drives it throughwindow.splentLightbox(open(img, {play}),next(),prev(),refresh(),close()); a gallery that grows during the slideshow listens forsplent:lightbox:endon its grid, appends the next page and callsrefresh()thennext().code_blocks.jslabels 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)
| 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)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| 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.