Skip to content

feat(theme): Jinja theming engine with bundled themes - #322

Draft
Rl0007 wants to merge 29 commits into
developfrom
feat/themes
Draft

feat(theme): Jinja theming engine with bundled themes#322
Rl0007 wants to merge 29 commits into
developfrom
feat/themes

Conversation

@Rl0007

Copy link
Copy Markdown
Collaborator

Adds a site-wide Jinja theming engine for the public event pages, ported in from the standalone frappe_themes + buzz_themes apps so buzz stands alone.

  • One setting drives it: Buzz Settings.default_theme. Empty means the renderer declines and nothing changes — the engine is inert until a theme is chosen.
  • Routes ship as data via an idempotent patch; they previously existed only in one dev site's database.
  • Three bundled themes: Buzz Events (ticket stub), Sketchbook (handwritten), Stickerpack (neo-brutalist).
  • 38 tests, each proved to fail against a mutated implementation.

DocTypes and the module are Buzz *-prefixed because both names are globally unique in Frappe and would otherwise clash wherever the old apps are co-installed.

Theme templates are private; only built CSS and scripts are published under public/themes/. An earlier symlink layout served the raw Jinja — data model, filters and all — at /assets/.

Rl0007 added 13 commits August 8, 2026 09:44
Ports the frappe_themes engine (page renderer, Jinja theme loader, theme
inheritance chain, route table) into buzz/theme/.
DocTypes are renamed to Buzz Theme / Buzz Theme Settings / Buzz Themed
Route because DocType names are globally unique in Frappe - a module does
not namespace them - so the old names would clash wherever frappe_themes
is also installed.
Theme resolution becomes: ?preview_theme= (dev) -> Buzz Event.theme on
event-scoped routes -> Buzz Settings.default_theme -> nothing.
can_render() now matches the route before resolving the theme, because the
event is only known once the event_route group has been captured. The
resolved context is stashed on frappe.local so theme_asset_url() and
theme_config() cannot diverge from it and serve another theme's assets.
Drops is_active and the active_theme.json manifest: Buzz Settings is the
single source of truth for the site default, and the manifest's only reader
was the Vite plugin, which is not part of this port.
Moves buzz_events_theme, sketchbook_theme and events_theme into buzz, with
the public/themes symlinks that make their assets servable.
The route table previously existed only in one dev site's database, so the
engine shipped inert; a patch now seeds it idempotently. importable_doctypes
is what makes the theme records themselves import on migrate - the exported
folder is otherwise only an export target, never an import source.
Theme scripts join the existing vendored-JS exclusions: they are Alpine
browser globals and minified vendor bundles, like buzz/public/js/lib.
Jinja methods are registered in one global namespace keyed by function name,
so buzz's theme_asset_url silently lost to the identically-named helper in
frappe_themes wherever both apps are installed. Every themed page rendered
with empty asset URLs. Renaming to buzz_theme_asset_url/buzz_theme_config
gives the doctype rename the co-installability it was chosen for.
The not-found guard was an output node in a template that extends another,
and Jinja discards those - only statements run. The throw never fired, so a
missing event fell through to get_cached_doc(..., None), which raised
DoesNotExistError carrying a doctype and got converted to 403 Not Permitted
for guests instead of 404.
Ports the sticker-pack prototype onto the live theme engine: dot-grid ground,
3px borders with offset shadows, accent-cycled rotated cards and date stickers.
The prototype's accent_bg/accent_fg/rot_class were Python globals registered
in its render.py, which Frappe's Jinja has no equivalent of, so they are
reimplemented as macros in components/macros/accents.html. Tailwind is built
locally rather than pulled from the Play CDN.
Events Theme is removed: it shipped two pages that extend a base.html it did
not have, so it could never render standalone.
Drops Buzz Event.theme. Theming an individual event let a listing page and
the events it linked to render in different design languages, so clicking a
card visibly changed the design mid-journey. Hosted event platforms attach
the theme to the organizer or workspace for this reason and let an event vary
only its content, never its templates.
With nothing request-dependent left to resolve, can_render() checks the theme
first and returns early when none is set, and the frappe.local hand-off that
kept the Jinja helpers in step with the renderer is no longer needed - both
now read the same cached lookup.
Removing a field drops its DocField but never the column, so the stale values
survived every migrate. Also invalidates the cached column list the DDL does
not touch - otherwise running workers keep building SQL for a column the
table no longer has.
…ndling
Split bundled themes into private templates (buzz/themes/<slug>) and public
assets (buzz/public/themes/<slug>), replacing the committed symlinks that
exposed every page/component template, its Jinja queries and any sibling .py
page controller at /assets/buzz/themes/<slug>/...
Also:
- reject theme names outside ^[A-Za-z0-9 _-]+$ and containment-check every
path before rename/rmtree/makedirs; frappe.scrub keeps path separators
- never remove a standard theme's shipped folders on record delete
- gate after_insert scaffolding on developer_mode so a read-only apps volume
cannot break the insert
- make scaffold_theme_settings POST-only and check write permission
- refuse reserved first path segments for dynamic pages so a theme cannot
shadow login/desk/api
- stop the seed patch re-enabling dynamic_pages_enabled on every re-run
- batch the sponsor tier lookup on the event detail page
- log boot-data failures, memoize compiled routes per request, drop dead code
Module Def names are globally unique the same way DocType names are, and
'Theme' was too generic to own. The pre-model-sync patch creates the new
module and repoints the doctypes before their JSONs sync, because
DocType.module is a Link and the sync would otherwise fail to import them.
Covers the regressions this engine has already had: asset URLs resolving to
the wrong theme (which fails as a broken stylesheet, not an error), path
traversal, on_trash deleting shipped app source, and reserved paths being
hijacked by a theme page.
Also renames the asset fallback from base_ to active_: names is child-first,
so the fallback URL is the active theme, not the root ancestor.
@greptile-apps

Copy link
Copy Markdown
Contributor

Too many files changed for review (111 files, 100 file limit).

Bypass the limit by tagging @greptile-apps to review.

@Rl0007Rl0007 added the wiki On merge, add/update a docs page in the bwh.tech/buzz wiki label Aug 8, 2026
@Rl0007
Rl0007 marked this pull request as draft August 8, 2026 10:45
Rl0007and others added 14 commits August 8, 2026 16:28
The three bundled themes each shipped a byte-identical copy of
lucide.min.js and alpine.min.js, plus a shared theme.js/components.js
pair duplicated between sketchbook and stickerpack.
buzz_theme_asset_url() already walks the parent_theme chain child-first
and serves the first theme that has the file, so a new asset-only
"Buzz Base Theme" parent removes the duplicates with no engine change.
buzz_events_theme keeps its own theme.js/components.js, which genuinely
differ, and still overrides the parent.
The three per-theme package.json/yarn.lock become one at buzz/themes/,
so there is a single node_modules instead of three. Each build keeps its
own working directory so Tailwind's automatic source detection stays
scoped to one theme. The separate lockfiles had drifted apart
(4.3.0/4.3.0/4.3.3); consolidating normalises all three on 4.3.3, which
only shifts Tailwind's own preflight defaults — the emitted utility class
set is unchanged for every theme.
The template reader reaches open() only through find_theme_file or the
loader's own containment check, so the flagged path is already proven to sit
inside a theme folder; annotated with the reasoning semgrep asks for.
frappe-ui beta.37 declares Combobox's emit as (...args: unknown[]), which a
narrowly typed handler is not assignable to, so PhoneInput narrows at the
boundary instead.
Sketchbook's footer row was a non-wrapping flex row, so it pushed the page
36px wider than a 390px viewport. Stickerpack's theme toggle is fixed to the
top-right, which sits in the gutter beside the centred shell on desktop but
lands on top of the header's sign-in link once the gutter is gone.
staticmap.openstreetmap.de is NXDOMAIN - the service was withdrawn - so every
venue with coordinates rendered a broken image in all three themes. The
official openstreetmap.org embed needs a bounding box rather than a centre
point, derived here from the venue's latitude and longitude.
Every bundled theme hardcoded one customer's identity (brand text, og tags,
mailto, social links), so any site installing buzz rendered someone else's
name. Themed pages now get frappe's own website context and read the brand,
og image, footer links, copyright and address from Website Settings, falling
back cleanly when a field is unset.
Also wires up the per-theme settings mechanism that already existed but was
unused: Stickerpack Theme Settings drives the homepage hero and featured
heading through buzz_theme_config().
Ports the site's frappe/builder home, event and category pages into the Jinja
theme system as a fourth bundled theme, with light and dark modes.
Colours are frappe-ui's own semantic tokens (surface / ink / outline), resolved
from frappe-ui/tailwind/generated/colors.json, so themed pages and the Vue
dashboard share one grey ramp.
Adds Buzz Theme Nav Link so header links are editable from Desk; its validate()
allowlists http/https because the URL is rendered straight into href.
Component CSS is wrapped in @layer components: tokens.css is imported unlayered,
where it would otherwise outrank every Tailwind utility regardless of specificity.
Frappe writes every patch into Patch Log as applied WITHOUT running it when a
site is installed, so a fresh site came up with the theme records but zero
themed routes, and the log entry then blocked the patch from ever re-running.
Only /home resolved, by accident, through dynamic pages.
Moves the route list and seeding into buzz_theme_settings as DEFAULT_ROUTES and
seed_default_routes(), called from after_install, on_migrate and the patch alike.
Seeding is idempotent on url_pattern, so custom routes survive.
Drops the dead ^events$ default: no bundled theme ships pages/events.html.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Rl0007and others added 2 commits August 18, 2026 08:52
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Renders themes with jinja autoescape on: frappe's shared env has it off, which
made every {{ }} in a theme an xss sink for fields frappe does not sanitize on
save. Deliberate html now says |safe.
- reject dot-dot and non-word segments in dynamic page paths, so an encoded
..%2f can no longer walk out of pages/ and render any partial
- inherit requires_auth from the route that owns a template, so a gated page
is not reachable by its file path once dynamic pages are on
- never render venue.google_maps_embed_code or category.icon_svg: both are Code
fields, which frappe deliberately leaves unsanitized. maps validate the embed
url, categories name an icon
- keep route captures out of form_dict, let website settings failures raise
rather than log per request, refresh the cached jinja overlay's globals
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

wikiOn merge, add/update a docs page in the bwh.tech/buzz wiki

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@Rl0007