The generator also now serves these docs as a Hugo documentation website from:
/
/docs/
/docs/reference/
Run the documentation website locally with:
npm run docs:server
Build the documentation website to disk with:
npm run docs:build
The docs website uses the dedicated config profile in:
config/docs-site/config.yaml
The shortcode manifest is intended to become the MCP-facing inventory for agents editing websites.
Features
Container Boundary
The shared Hugo/editor theme is not itself a hosted K3s service.
The only containerized runtime in this repo is the local markdown bridge used
by the editor:
Containerfile.local-bridge
container/README.md
Keep that bridge local-only. Hosted generator runtime belongs in
generator-api, not in the theme repo.
Canonical Root Tokens
The theme now emits a canonical :root token surface from params.style.tokens.* in
assets/css/variables.css.
Current emitted groups:
params.style.tokens.color.* -> --color-*
params.style.tokens.space.* -> --space-*
params.style.tokens.radius.* -> --radius-*
params.style.tokens.shadow.* -> --shadow-*
params.style.tokens.section.* -> --section-*
params.style.tokens.region.* -> --region-*
params.style.tokens.width.* -> width-oriented names such as --content-max-width, --section-max-width, --media-width, --aside-width
params.style.tokens.type.* -> --font-*, --text-size-*, --line-height-* where applicable
Legacy site.Params.style.* variables are still emitted too, so migration can happen
incrementally rather than as a flag day.
The first scoped-surface bridge is now live too: assets/css/variables.css
rebinds the shared palette names --bg, --fg, --fg-bold, --fg-light,
--border, --accent, --hover-accent, --icon, and --fig-bg on header, footer,
aside, and #article-nav. New consumers should prefer those shared scoped
names and only fall back to legacy header_* / footer_* / aside_* /
article_nav_* variables while the migration is still in progress.
Those scoped rebindings must use non-cyclic root fallbacks like
--surface-root-bg, not self-referential patterns like
--bg: var(--footer_bg, var(--bg)), because Firefox drops those cycles.
Scoped palette surfaces keep a readability fallback too: if a site explicitly sets a dark
footer_bg equal to the global invert_bg but does not provide matching footer_fg /
footer_icon values, the footer inherits the invert foreground/icon/border family by
default instead of reusing the normal light-surface text colors.
Widgets are part of the same scoped-token architecture, not exceptions to it. The site theme should
own the base tokens, each widget root should alias them into local --widget-* variables (or
widget-family aliases like --booking-*), then the widget should reset global component leakage
such as shared button styles and rebuild its own controls explicitly with flex/grid and explicit
spacing. Widget internals should consume only those local aliases, so each site inherits branding
automatically and can override the widget safely without forking shared SCSS. See
widget-integration-plan.md.
Sites can now separate their general accent from their interaction/hover accent through
hover_accent and scoped overrides such as footer_hover_accent. Shared hover consumers like nav
links, ordinary links, buttons, and icons should prefer --hover-accent with --accent as the
fallback.
Normal footer rendering should still consume the scoped footer_* tokens directly. Footer
icons/titles/borders should not read footer_invert_* unless a footer-specific invert/glass
treatment is explicitly intended.
Head Loading Guardrails
The shared head pipeline should have exactly two authoritative entrypoints:
layouts/partials/head/css.html for stylesheet linking
layouts/partials/head/import/runtime-core.html for the critical JS/editor runtime chain
Site overrides should not hand-roll the core runtime tags for prefetch, Vim, cursor presence,
Nowtype, or edit mode. Keep those imports routed through runtime-core and extend via site
head/import/*.html overrides instead.
Guardrails:
use the shared .CDN import context for asset URLs; do not hardcode mixed /cdn/... and helper-built paths inside the same chain
register shared optional CDN assets through params.cdnAssets in the generator config rather than hardcoding one-off <script>/<link> tags in site head forks
the canonical shared asset source is now cdn/; do not place shared CDN/runtime assets under theme static/
keep cdn/editor/ trimmed to runtime/source files; do not leave nested git metadata, node_modules/, or packaged Electron outputs there
local hugo server now resolves shared assets through params.devCdnBase (falling back to params.cdnBase); use that instead of expecting a theme-local /cdn/ tree
in local development, head/css.html should link /css/style.css and /css/brochure.css directly, but append generator-owned ?v=... cache-busters through helpers/asset-url.html; keep fingerprinted asset links for non-development builds
for inline module bootstraps that need shared asset URLs, prefer an adjacent application/json config node over embedding quoted import specifiers directly in JS source; this avoids broken import("\"/cdn/...\"") output in templated module scripts like PhotoSwipe and Mermaid
prefer tiny site-specific head/import overrides over full site head.html forks whenever possible
do not create per-site layouts/ theme forks in individual website repos; shared rendering changes belong in generator/layouts/
do not create per-site theme copies or symlinks like themes/quantalumin/generator in site repos; run Hugo against the shared theme instead, for example with --themesDir /home/henry/Code
the SPA prefetch runtime rebases relative src / href / poster / data / srcset URLs inside fetched main/aside content before swapping DOM, so bundle-local media like petru.jpg keep working after client-side navigation
when debugging, verify the rendered page head includes the canonical runtime chain:
scope the same names at header, main, footer, aside, and section level
treat banner, spotlight, and wrapper as legacy presets over a smaller shared section model
migrate from SCSS to CSS incrementally, not in a single rewrite
keep the shared SCSS breakpoint map scalar; tuple/range values can compile into invalid
media queries and silently skip responsive rules
the shared main.scss build now goes through Dart Sass in
layouts/partials/head/css.html
rather than deprecated libsass, so modern CSS math and shared component styles compile
reliably during development and production builds
The shared breakpoint contract lives in:
assets/scss/main.scss
assets/scss/libs/_breakpoints.scss
Tuple breakpoints are interpreted as (max, min) ranges. The mixin now emits
valid <=, >=, and exact-range media queries from those tuples, so mobile
section, gallery, and hero rules actually apply at the intended breakpoints.
Page Title Header
When the shared title contract decides a page should render the automatic title block,
layouts/partials/main.html
should emit that block as a semantic page-level <header class="content ql-page-header">
inside the title section, not as a plain .content div. That preserves the shared layout
while keeping the authored page structure meaningful for tooling and downstream consumers.
Skill Meter Widget
The skill-meter shortcode now renders through a shared partial and shared SCSS/JS instead
of injecting inline <style> or a nested full <section> blob into markdown output.
Files:
layouts/shortcodes/skill-meter.html
layouts/partials/widgets/skill-meter.html
assets/scss/components/_skill-meter.scss
cdn/custom/skillMeter.js
Supported shortcode params:
widget
user
variant
title
eyebrow
subtext
class
Use variant="compact" when the meter should behave like a secondary landing-page card rather
than the dominant block on the page.
Quiz Widget
The quiz shortcode now provides a dedicated Hugo widget for authored quiz definitions instead
of forcing quiz content into generic prose editing.
Quiz source "quizzes/cell-basics.json" was not found as a page resource or readable file.
The shortcode resolves src against page resources first, then falls back to readable files.
Inline YAML/JSON is also supported when src is omitted.
In edit mode, file-backed quiz widgets now open through the dedicated Quiz Editor modal from the
rendered page or when their JSON/YAML source file is opened directly. Inline shortcode quiz bodies
remain raw-source only for now.
Intro Card Color Tokens
Intro-card sections now use shared surface-scoped foreground tokens for the card copy, rather
than relying on whatever the surrounding section text color happens to be.
Available surface tokens:
intro_card_fg
intro_card_title_fg
intro_card_meta_fg
intro_card_lead_fg
These can be authored at site, page-type, or page scope through the normal
params.style.tokens.surface.* / params.style.page_types.<scope>.tokens.surface.* /
page style.tokens.surface.* paths.
Markdown section attrs can also carry inline CSS custom properties directly on the section root,
for example:
The shared section renderer now preserves that style="" attribute alongside authored section
classes, which is useful for one-off token tuning without introducing site-local layout forks.
Starfield Section Effect
Sites that want the shared Three.js starfield effect should opt into the runtime through
params.cdnAssets, for example:
[params]
cdnAssets = ['/custom/starfield.js']
Then apply the effect as a normal section modifier:
Optional fly-in text can stay on the section itself via CSS custom properties in the same attr
line, for example --ql-starfield-text-1, --ql-starfield-text-2, and --ql-starfield-text-3.
By default the fly-in text uses the site body font; set --ql-starfield-text-font only when a
section needs a different family.
The moving stars default to small white points; only set --ql-starfield-node,
--ql-starfield-node-accent, --ql-starfield-node-size, or --ql-starfield-node-opacity when a
section explicitly wants a more pronounced secondary node layer.
That keeps the effect on the shared generator path without reviving old front-matter-driven
custom partials.
Team Block
Use a fenced team block or team shortcode for committee/staff/profile cards instead of
raw HTML grids. The widget renders the existing brochure card layout (.ql-team-grid /
.ql-team-card) from structured data, so the markdown stays readable and the editor has a
clearer object boundary to target later.
Example fenced block:
```team
- name: Petru Balan
role: Hall Manager
image: petru.jpg
alt: Portrait of Petru Balan.
- name: Helen Mullany
role: Chair
```
Supported fields per member:
name
role
image / src / photo
alt
bio
href / url / link
link_label
class
Text-only cards are allowed by omitting image.
In live edit mode, rendered team blocks are now treated as a structured object instead of a
raw HTML blob. Clicking a team grid opens a Team Editor modal that edits just that fenced
block in the page markdown and currently supports:
upload/replace member photos through the existing project image upload path
The current editor writes back to the fenced team block in the source markdown file. It does
not yet provide direct on-canvas drag/reorder handles.
Workspace Block
Use a fenced workspace block or workspace shortcode for shared collaboration spaces that mix
real people and agents on the same website-backed board. This keeps the collaboration surface
file-backed in markdown while giving the editor a clear object boundary to target.
Example fenced block:
```workspace
title: Generator Collaboration Room
summary: |
Shared workspace for people and agents collaborating through the website.
website: /workspace/
repo: https://forgejo.example.com/org/repo
chat: https://control.example.com/rooms/generator
participants:
- name: CTO
type: agent
role: Technical lead
company: quantalumin
department: leadership
summary: |
Coordinates architecture and delivery.
- name: Alice Example
type: person
role: Client representative
company: Example Studio
summary: |
Reviews copy, goals, and launch priorities.
```
Supported top-level fields:
title
summary
objective
website / site
repo / repository
chat / room
notes / docs
class
participants / members / people / collaborators
Supported fields per participant:
name / title
type / kind
role / position
company / org
department / team
status
image / src / photo
alt
summary / bio / body
href / url / link
link_label
website / site
workspace / board
session / thread
class
loading
In live edit mode, rendered workspace blocks are now treated as a structured object. Clicking a
workspace board opens a Workspace Editor modal that edits just that fenced block in the source
markdown and currently supports:
top-level workspace settings like title, summary, objective, website, repo, room, and notes
upload/replace participant photos through the existing project image upload path
Gallery Block
Use a fenced gallery block or gallery shortcode for brochure/gallery image grids instead of
raw HTML. The widget renders the existing brochure gallery layout (.ql-gallery-grid /
.ql-gallery-item) from structured data, so the markdown stays readable and the editor can treat
the block as one object boundary.
Example fenced block:
```gallery
- image: rawson-large-hall-full.jpg
alt: The large hall arranged for an event.
caption: The large hall set up for a formal gathering.
- image: rawson-field-from-large-hall.jpg
alt: The view from the hall towards the field outside.
caption: Views out from the hall across the green space.
```
Supported fields per item:
image / src / photo
alt
caption / text / body
class
loading
In live edit mode, rendered gallery blocks are treated as a structured object. Clicking a gallery
grid opens a Gallery Editor modal that edits just that fenced block in the page markdown and
currently supports:
add/remove images
move images up/down
edit image, alt, caption, class, loading
upload/replace gallery images through the existing project image upload path
On pages with pswpFigures: true, the shared PhotoSwipe boot still upgrades rendered figures
inside .ql-gallery-grid at runtime, so the gallery block keeps the old click-to-expand behavior
without needing raw HTML.
Stats Block
Use a fenced stats block or stats shortcode for metric and highlight cards instead of raw HTML.
The widget renders the brochure stat layout (.ql-stat-grid / .ql-stat-card) from structured
data, so the markdown stays readable and Hugo remains the render authority.
Example fenced block:
```stats {.ql-grid-4}
- value: Main Hall
label: Large event space
body: 22m x 9m with space for larger classes, parties and events.
class: featured
- value: From GBP 13/hr
label: Starting price
body: Room hire starts with Robins' Room and scales up by space.
```
Supported fields per item:
value
label
body
class
The block preserves source order in the rendered DOM and keeps the stat grid wrapped in a single
data-ql-editable="stats" outer element.
FAQ Block
Use a fenced faq block or faq shortcode for question-and-answer lists instead of raw HTML.
The widget renders a structured FAQ layout (.ql-faq-grid / .ql-faq-item / .ql-faq-answer)
from markdown data, so FAQ content stays readable while Hugo remains the render authority.
FAQ items now reuse the shared details.box.collapse hover/open treatment, so sites get the same
accent-corner hover behavior as collapse boxes without custom site CSS. FAQ summaries now use a
full-row click target, with the +/− glyph treated as a non-interactive indicator, and the
shared section rhythm also gives faq blocks the same automatic breathing room before following
button rows as other structured blocks.
Example fenced block:
```faq
- question: Can I book recurring sessions?
answer: Yes. Recurring hires are supported where availability allows.
- question: Is the hall accessible?
body: |
The main hall has step-free access and accessible toilet facilities.
class: highlighted
```
Supported fields per item:
question
answer / body
class
The block preserves source order in the rendered DOM and keeps the FAQ grid wrapped in a single
data-ql-editable="faq" outer element.
Contact Block
Use a fenced contact block or contact shortcode for profile/contact-card grids instead of raw
HTML. The widget renders the existing brochure contact layout (.ql-contact-grid /
.ql-contact-card / .ql-contact-profile) from structured data, so contact pages stay readable in
markdown and the editor can treat the block as one structured object.
Example fenced block:
```contact
- type: profile
name: Petru Balan
role: Hall Manager
image: petru.jpg
alt: Portrait of Petru Balan.
body: |
Petru handles day-to-day hall enquiries and supports regular users, party organisers and
prospective hirers.
- title: Contact details
body: |
**Telephone**<br>01444 300158
**Email**<br>[info@example.com](mailto:info@example.com)
```
Supported fields per item:
type / kind
title
name
role
image / src / photo
alt
body / bio / text / content
href / url / link
link_label
class
loading
Items with type: profile or an image render as .ql-contact-profile cards; items without an
image render as ordinary .ql-contact-card panels.
In live edit mode, rendered contact blocks are treated as a structured object. Clicking a contact
grid opens a Contact Editor modal that edits just that fenced block in the page markdown and
currently supports:
upload/replace contact profile images through the existing project image upload path
Testimonials Block
Use a fenced testimonials block or testimonials shortcode for quotation/testimonial grids
instead of raw HTML. The widget renders brochure quote cards (.ql-testimonial-grid /
.ql-card.ql-quote-card) from structured data, so homepage/social-proof sections stay readable in
markdown and the editor can treat the whole grid as one structured object.
Example fenced block:
```testimonials
- quote: The hall worked beautifully for our family celebration.
name: A Rawson hirer
role: Birthday party
- quote: Clear booking process and a really flexible local venue.
name: Community organiser
role: Weekly class
```
Supported fields per item:
quote
name
role
class
In live edit mode, rendered testimonials blocks are treated as a structured object. Clicking a
testimonials grid opens a Testimonials Editor modal that edits just that fenced block in the page
markdown and currently supports:
add/remove items
move items up/down
edit quote, name, role, class
Booking Block
Use a fenced booking block or booking shortcode for public reservation widgets backed by the
shared booking API instead of raw HTML forms or site-local embeds. The block renders a shared
.ql-booking-widget shell, fetches availability from the booking service, and redirects users into
the hosted payment checkout returned by the API. The public widget now also supports calendar-style
day, week, month, and year views so visitors can see what is on as well as what is
available to book. In edit mode, clicking the rendered widget opens Booking Editor, which lets
authors update the widget settings and room/resource list without dropping back to raw HTML.
The selected day agenda also renders public event cards with safe source/category/room metadata
from the booking API, so the same widget can act as a what’s-on surface as well as a reservation
flow. When a block omits provider or common booking URLs, the shared partial falls back to
site.Params.booking.* so sites can set shared booking defaults in Hugo config instead of
duplicating them in page content.
Example fenced block:
```booking
title: Book Rawson Hall
intro: Choose a room, pick a slot, and continue to secure payment.
view: month
charges_url: /charges
terms_url: /terms-and-conditions
privacy_url: /privacy-policy
resources:
- owner: rawson-main-hall
name: Main Hall
description: Large hall for parties, classes, and community events.
price_lookup_key: rawson.main-hall.standard
- owner: rawson-small-hall
name: Small Hall
description: Flexible room for meetings and smaller groups.
price_lookup_key: rawson.small-hall.standard
```
Supported top-level keys:
title
intro / description
provider
view (day, week, month, year, or legacy list; default month)
api_base_url
success_url
cancel_url
charges_url
terms_url
privacy_url
items / resources / rooms
Supported fields per resource item:
owner
name / title
description
price_lookup_key
class
For brochure sites served through sites-edge, prefer a same-origin shared base such as
/_platform/booking. The edge can then proxy and cache the public booking API without exposing a
cross-origin dependency from the page runtime.
Calendar Block
Use a fenced calendar block or calendar shortcode for a separate public what’s-on calendar
surface backed by the shared booking API. This block uses the same shared planner UI as the
booking widget, but it renders an event-focused agenda instead of a reservation form. It is the
preferred shared surface for whole-site or cross-room public schedules.
The shared booking/calendar runtime in
bookingWidget.js
must re-scan widgets after SPA swaps. The current contract is to listen for the shared spa:route
event from prefetch.js
instead of assuming a full page load.
Example fenced block:
```calendar
title: What’s on at Rawson Hall
intro: See public events across the hall, then jump into the booking flow when you want to reserve a room.
view: month
booking_url: /bookings/
booking_label: Book a room
owners:
- owner: rawson-main-hall
name: Main Hall
description: Parties, classes, and community events.
- owner: rawson-small-hall
name: Small Hall
description: Meetings, workshops, and smaller gatherings.
```
Supported top-level keys:
title
intro / description
view (day, week, month, year, or legacy list; default month)
api_base_url
booking_url
booking_label
owners / items / resources / rooms
Supported fields per owner/filter item:
owner
name / title
description
class
Platform Feed Block
Use a fenced platformfeed block or platformfeed shortcode for public same-origin widgets backed
by sites-edge path routes like /_platform/villagehall. This is the preferred pattern for
brochure-first sites that need a live news feed, bulletin board, or event list without coupling the
static site generator to the portal backend.
The shared runtime in
platformFeedWidget.js
listens for spa:route and ql:page-swapped, so these widgets continue to hydrate on client-side
navigation.
Example fenced block:
```platformfeed
title: Latest village news
intro: Notices and updates from the hall and the wider village.
kind: feed
api_base_url: /_platform/villagehall
site_slug: rawson-hall
category: village_hall
limit: 4
cta_url: https://portal.rawsonhallbolney.org/
cta_label: Open the community portal
```
```platformfeed
title: Hall documents
kind: documents
api_base_url: /_platform/villagehall
site_slug: rawson-hall
limit: 6
cta_url: /hall-documents/
cta_label: Browse hall documents
```
Supported top-level keys:
title
intro / description
kind (feed, events, or documents)
api_base_url
endpoint
site_slug
category
timeline
limit / item_limit
cta_url
cta_label
empty_text
class
Platform Portal Block
Use a fenced platformportal block or platformportal shortcode for a simple brochure-site CTA
that sends residents into the full VillageHall portal.
Example fenced block:
```platformportal
title: Residents’ portal
intro: Sign in to join the noticeboard, post updates, and manage member-only activity.
url: https://portal.rawsonhallbolney.org/
label: Open the portal
```
Supported top-level keys:
title
intro / description
url / portal_url
label / cta_label
class
Fundalumin Block
Use a fenced fundalumin block or fundalumin shortcode for shared funding discovery widgets
backed by the separate Fundalumin app. The block renders a scoped .ql-fundalumin-widget shell,
aliases the surrounding site tokens into local --widget-* variables, resets shared component
leakage inside the widget root, and then rebuilds the widget controls explicitly. That keeps the
widget on the shared generator path while still letting each site inherit its own branding via
Hugo-configured CSS variables. The shared runtime in
fundaluminWidget.js
also re-scans widgets after shared SPA swaps by listening for spa:route and ql:page-swapped,
so discovery pages keep working on client-side navigation instead of only on hard reloads.
Example fenced block:
```fundalumin
title: Fund a Future Luminary
intro: Support TutorLumin students and help turn donations into tuition credit.
app_base_url: http://127.0.0.1:8090
browse_url: http://127.0.0.1:8090/
browse_label: Open Fundalumin
apply_url: /enrol/
apply_label: Apply for support
wishlist_limit: 3
campaigns:
- slug: general-fund
label: TutorLumin Access Fund
description: Support tuition credit for students who need it most.
cta_label: Support this fund
- slug: mina-medicine
label: Mina's medicine goal
description: Help Mina continue her science studies with tuition support.
```
Supported top-level keys:
title
intro / description
app_base_url
browse_url
browse_label
apply_url
apply_label
wishlist_limit
campaigns / items / profiles
Supported fields per campaign item:
slug
label
description
url
cta_label
class
The first editor-side token inspector now reads from that canonical surface:
Site scope reads Hugo config-backed defaults via params.style.tokens.*
Page Type scope reads shared surface defaults via params.style.page_types.<scope>.tokens.surface.*
Page scope reads front matter overrides via style.tokens.surface.*
Section scope reads and writes the selected live section against the authored section ordinal (s0, s1, …) and owns the leading section attr block in markdown
Block scope currently targets section media attrs first (s0:media) and structured fenced blocks by kind + ordinal (team:0, gallery:1, contact:0, stats:0, testimonials:0, faq:0, booking:0, calendar:0, fundalumin:0, buttons:0), then falls back to explicit attr/fence-backed block ids when they exist
the current panel still keeps legacy section / region groups as compatibility only, while surface is the primary shared editing surface
Structured fenced blocks now also share one editor-side descriptor registry in
cdn/custom/toggleMarkdown.js for slash/backslash insert templates, editor labels, model
keys, parse/replace handlers, inspector renderers, modal routing, and preview targeting. New
blocks should extend that shared registry instead of adding more one-off switch branches.
Within that registry, the simpler YAML-list blocks faq, testimonials, and stats now also
share one list-editor engine for item add/move/remove/update and inspector rendering. The richer
media/profile blocks team, gallery, and contact now share the same card-list scaffolding and
list mutation helpers, while still keeping block-specific image/upload/autofill widgets on top.
In replace-main mode, that token editor now lives inside the first shared right-side inspector shell.
The shell exposes stable Content, Layout, Style, Data, and Advanced tabs for the current
live selection or page defaults. Style is still the strongest shared panel in this slice, but
Layout now also edits the existing section contract directly for normal section controls instead
of only launching the old popover.
Focused Nowtype section controls now reuse that same shared inspector too: the section Layout
button opens the shared inspector with an explicit section context, so Layout and Style still
work even when there is no live-page selection object.
The old floating Nowtype section-layout popover has now been removed. Section layout editing should
only route through the shared inspector, so the editor no longer carries a second layout-only UI
path beside the semantic inspector.
The next live section inspector slice is now in place in toggleMarkdown.js. For a selected live
section it now exposes the new semantic axes first:
flow
media_side
size
content_align
surface
layout_family as a compatibility preset
It then keeps width, spacing, padding, density, and the existing block layout preset path
in a separate Width And Rhythm card.
The first layout mutation still opens the focused replace-main section editor so the markdown buffer
stays authoritative, then applies the shared control from that same inspector rail. The write path
is now split more cleanly:
section-frame semantics like family, media side, size, spacing, padding, density, surface, and
content alignment write to the section body’s leading { ... } attr block
content width and block layout presets stay on the section body’s trailing --- { ... } content
attr block
frontmatter.sections[*].section remains editor compatibility only; the active Hugo render path now
follows the inline markdown attr blocks above as the canonical section-frame source.
Shared Navigation Ordering
Page ordering for navigation partials is centralized in
layouts/partials/nav/ordered-pages.html.
It is used by nav/nextprev, nav/book-index, and book TOC partials, and sorts by:
explicit front matter weight (when set),
numeric filename/dirname prefixes like 1-, 2-, 10-,
stable path/title/permalink tie-breakers.
PhotoSwipe Figure Links
Inline markdown/render-hook figures may link into PhotoSwipe, but promoted section-media
figures are intentionally non-PhotoSwipe.
Pages can opt plain gallery figures into PhotoSwipe by setting front matter like
pswpFigures: true (or pswpGallery: true). On those pages, the shared
PhotoSwipe boot upgrades figures inside .ql-gallery-grid into pswp-item
links at runtime before initializing the lightbox, so the visible page markup
can stay unchanged.
Fixed Chrome Shell
Sites can opt into a fixed header/footer shell via config:
When fixedFrame is enabled, the shared generator keeps <header> and
<footer> always visible, makes <main> the scroll container, and adds the
body class ql-shell-fixed-chrome. showArticleNav = false suppresses the
bottom #article-nav chrome without requiring a site-local layout fork.
The shared background audio player can also auto-close its initial minimised
bar after page load:
[params.bgfx]
player = trueautoCloseBarAfterMs = 3500
This only affects the initial idle bar state. It does not overwrite the saved
player mode in local storage, and the auto-close timer is cancelled on first
user interaction.
Markdown render-hook figure numbering now uses a page-global counter in
layouts/partials/fig/parse.html (.Page.Store) so captions do not reset when page content
is rendered in multiple chunks.
Section-media extraction is now opt-in: only images marked with {.fig}, {.figure},
or {.section-banner}
are treated as section figures. Plain markdown images stay inline in .content.
{#fig:...} shorthand is normalized to id="fig:..." during preprocessing, including
when the attribute block is written on the next line after the image or below caption lines.
Section-media figures are implicitly non-linking. .nolink / .no-link is still tolerated
on read for backward compatibility, but it is redundant and should not be written for new
section-media source.
Plain markdown image layout now resolves in this order:
explicit image layout from attrs/classes such as .fit, .left, .right, .centre, .main
explicit destination suffix such as 
otherwise a shared fallback:
fit on homepage/list/taxonomy pages
alternating left / right on ordinary single pages
The shared image render hook must tolerate pages rendered without a backing .Page.File object,
such as RSS and some indirect/module render contexts. When deriving figure/gallery ids, use
.Page.File.Path only when present and fall back to .Page.Path or .RelPermalink.
iframe-like markdown image targets (.html, .pdf, YouTube, and video files) default to fit
unless you explicitly choose another layout, so  behaves like an embed instead
of a floated prose image. Local and external .html targets now both use the shared iframe render
path. External URLs that expose an embed endpoint under /embed also use that iframe path, so
markdown like  stays on the plain ![]() authoring
flow instead of requiring raw HTML in content.
Homepage/list lead sections should be authored as ordinary first sections. If the first section is
.banner or .spotlight, it owns the visible page title while front matter title stays available
for SEO, nav, and feeds. The shared intro-card look is now a real section modifier, so authored
section attrs augment site defaults like fullscreen instead of replacing them. Shared
title ownership also now treats leading markdown section attr lines ({.wrapper ...}) as neutral,
so pages that begin with attrs followed by # Heading keep the content heading as the visible page
title instead of getting a duplicate template title.
Shared
content-only fullscreen banners and spotlights now size against the viewport minus a small chrome
offset (--fullscreen-content-offset) rather than a raw 100vh, so the hero sits higher and the
footer can remain visible on initial load.
Section/figure class ownership is now classifier-based. Figure attrs may still promote a small
legacy subset onto the section root when they clearly express section semantics:
Figure-only media tokens such as fit, left, right, main, and other media-layout controls stay
on the <figure>. This keeps legacy figure-led sections like  {.fig .fit .spotlight .style4 .fullscreen}
working as section-level spotlight style4 fullscreen blocks without leaking fit onto the section.
footer can still enter view on shorter laptop screens without site-specific overrides.
{.banner .intro-card}
<div class="kicker">Bolney village venue</div>
# Classes, parties, weddings and community events in the heart of Bolney
Hire bright, flexible spaces for fitness classes, children's parties, meetings, celebrations and village events.
{.lead}
For split layouts, use the same pattern with .spotlight:
---
{.spotlight .intro-card}
# Section title
DJ Chris Nova • Canterbury, UK • Quantum Physicist
One strong paragraph introducing the section.
{.lead}
The old .ql-hero, .ql-hero-card, .ql-kicker, .ql-hero-meta, and .ql-ink classes still work
as temporary compatibility aliases, but the shared SCSS section styles now own the look. Keep new
content on section modifiers and ordinary content classes rather than page-level hero wrappers.
The legacy fenced hero block is no longer the canonical authoring path.
Section-media figures render as direct section media with a layout class:
explicit .left / .right / .fit in the media attributes wins,
otherwise layout alternates left/right by figure order (same default behavior as markdown render-image parsing).
Wrapper SVG Margins
Wrappers can render decorative SVG side margins using section classes:
svg-margin or svg-margin-lines: vertical line motif on both sides,
svg-margin-motif: repeating square motif on both sides.
sectionMarginSvg resolves against page resources first (so border.svg in the same
bundle works), then falls back to a direct path/URL.
Preset + side controls:
sectionStyle: "svg-margin"sectionMarginPreset: "french-flag"# tricolor stripe presetsectionMarginSide: "left"# left | right | both (default both)
Current presets:
french-flag
orbitals-spdf (stacked s/p/d/f motif in blue/red/purple/green)
power-grid (animated green sci-fi wiring grid)
Preset framework:
sectionMarginPreset: "<slug>" maps to CSS class .svg-margin-preset-<slug>,
add new preset styles in assets/scss/components/_wrapper.scss under .wrapper.
Inheritance (unless overridden):
child pages inherit sectionStyle and sectionMargin* from their current section
(_index.md),
child front matter values override inherited ones,
set sectionStyle: "none" on a child page to disable section wrapper styling.
You can tune spacing/scale/opacity per section by setting CSS vars on that wrapper:
--ql-svg-margin-inset
--ql-svg-margin-strip-width
--ql-svg-margin-tile-height
--ql-svg-margin-opacity
--ql-svg-margin-svg (full custom SVG data URL)
Equivalent front matter vars are also supported:
sectionMarginInset
sectionMarginStripWidth
sectionMarginTileHeight
sectionMarginOpacity
Content Iframes
Banner and spotlight layouts no longer blanket-scale every descendant <iframe>.
Only figure-owned/media iframes keep the old scaled embed treatment. Raw content iframes
inside .content now retain their authored width and height, so embeds like booking diaries
and maps can fill the text column instead of being silently shrunk to half size.
Embed Shortcode
For richer iframe/embed layout control, use the figure-backed embed shortcode instead of
raw HTML:
Rawson Hall booking diary
This embed may be blocked by browser privacy settings.
If the shortcode src is a YouTube URL, local/external .html, .pdf, or a supported video
file, it still routes through the normal media pipeline. Otherwise it defaults to a real iframe
embed rendered inside the standard figure wrapper, so captions and layout classes behave like
other media.
Scoped Surface Widths
The theme now has a first shared width contract for page surfaces:
main uses --main-content-max-width
header uses --header-content-max-width
footer uses --footer-content-max-width
aside uses --aside-content-max-width
These default to the prose width token surface and expose a shared
--surface-content-max-width inside each region.
body.ql-page-type-homepage, body.ql-page-type-list,
body.ql-page-type-single, body.ql-page-type-landing, and
body.ql-page-type-detail now rebind main through the same shared surface
contract.
Current shared page scopes:
homepage
list
single
compatibility umbrella for all regular pages
landing
first-level singleton pages such as /about/, /contact/, /bookings/
detail
deeper regular pages such as /museum/chromatography/
main now exposes the primary page scope as data-ql-page-type, while <body>
keeps the compatibility ql-page-type-single class alongside ql-page-type-landing
or ql-page-type-detail where relevant.
The scoped main bridge now drives:
--main-content-max-width
--surface-content-max-width
--surface-section-padding-y
--surface-section-gap-y
--lead-max-width
That lets top-level brochure-like pages stay wider with roomier lead spacing,
while deeper detail pages can fall back to a tighter prose rhythm without per-site overrides.
Shared Section Spacing
Section spacing is now moving onto a shared rhythm contract instead of relying on
hardcoded utility exceptions.
Shared scoped spacing tokens now include:
--section-gap-before
--section-gap-after
--section-padding-top
--section-padding-bottom
--section-gap-tight
--section-gap-normal
--section-gap-roomy
--section-padding-y-tight
--section-padding-y-normal
--section-padding-y-roomy
main rebinds those through page scopes such as homepage, list, landing, and
detail, so ordinary section rhythm can be driven automatically by shared CSS.
The first rendered section inside main is then reset to --section-gap-before: 0,
so section spacing remains section-to-section rhythm rather than creating a blank band
between header and the first section.
Shared density defaults now also flow through main by page scope:
--surface-structured-gap
--surface-compact-padding-y
--surface-intro-card-gap
--surface-intro-card-padding-y
--surface-intro-card-padding-x
--surface-intro-card-max-width
--surface-intro-card-mobile-max-width
--surface-component-gap
--surface-card-padding-y
--surface-card-padding-x
--surface-note-padding-y
--surface-note-padding-x
--surface-chip-gap
--surface-chip-padding-y
--surface-chip-padding-x
--surface-team-card-gap
--surface-team-card-padding-y
--surface-team-card-padding-x
--surface-action-gap
--surface-embed-fixed-max-height-medium
--surface-embed-fixed-max-height-small
--surface-body-line-height
--surface-heading-line-height
--surface-paragraph-gap
--surface-heading-gap
--surface-nav-row-gap
--surface-nav-column-gap
--surface-nav-link-size
--surface-nav-link-line-height
--surface-nav-link-padding-x
--surface-nav-link-padding-y
That lets landing pages stay more brochure-like while detail pages tighten rhythm and intro-card
surface sizing automatically, without page-specific CSS. Brochure-style components such as
grids, cards, notes, fact chips, action rows, team cards, fixed-height embeds, paragraph rhythm,
heading rhythm, and header/menu density now consume the same scoped density surface.
Global params.style.tokens.surface.nav_link_size feeds the page-type nav defaults, so the shared
header size can be changed once at framework level or overridden once per site without repeating
the same override for homepage, list, landing, and detail.
Header/menu sizing now binds through the header surface itself, not the main page-type surface.
That keeps ordinary header links and button-like CTA items on the same shared nav token path.
Headers now also have a first shared contract layer in params.header.*, with legacy
params.style.theme.header = "style1|style2|style3" still supported as compatibility presets.
The new contract is:
layout = inline | split | stacked | centered
logo_position = left | center | hidden | title
nav_align = start | center | end | space-between | space-evenly
density = compact | normal | roomy
surface = solid | glass | transparent
sticky = true | false
cta_mode = inline | separate | button
layouts/partials/page/header-contract.html maps those settings onto shared header classes and
data attributes. Semantic header layout classes are now the primary runtime path, while
style1 / style2 / style3, params.chrome.stickyHeader, and the legacy fixed header class
continue to work only as compatibility inputs into the same contract.
The shared inspector now exposes the first semantic Header card in the Layout tab when no live
section is selected. That card edits site and page header settings through the local bridge,
using:
/ops/header/read
/ops/header/set
/ops/header/unset
Those bridge ops currently target params.header.* in the site config and page front matter
header.*, preserving markdown/config-as-source rather than introducing a separate header editor
store.
The rendered header shell now reads that contract once in layouts/partials/header.html and passes
the semantic view model down to the logo and breadcrumb partials. Those partials prefer
params.header.* first and only fall back to legacy HeaderNav.* fields when the semantic values
are not present yet.
The local editor bridge now exposes that shared scoped model directly through a first-class
surface token group in /ops/token/catalog. New editor token work should target:
global params.style.tokens.surface.*
local --surface-* section/block overrides in markdown attr styles
Legacy section / region token groups remain readable and writable for compatibility, but they
are no longer the preferred editor path.
Compatibility aliases still work on wrapper, banner, and spotlight, but now only
by rebinding the shared spacing vars:
.squeeze
.squeeze-top
.squeeze-bottom
.pad
.pad-top
.pad-bottom
New canonical spacing modifiers are:
.space-none
.space-tight
.space-normal
.space-roomy
.pad-none
.pad-tight
.pad-normal
.pad-roomy
Automatic adjacency tightening now lives in shared section SCSS for common structured-block
transitions such as:
grid -> buttons
grid -> note
note -> grid
buttons -> note
heading/lead/kicker -> grid/note/buttons
figure-led media -> content
Automatic compacting now also handles common short sections such as:
a single note / facts block / buttons row
a heading plus one semantic module
short update sections like heading + paragraph or heading + note
short banner/content-only intro sections with exactly one heading and one paragraph
banner.style1 responsive padding now also respects the same shared --section-padding-* surface
variables as wrapper.style1, so compact section heuristics and page/site token overrides work the
same way across the two main content-only section presets.
That keeps the same spacing behavior for both normal Hugo output and the JS editor’s live page
surface without duplicating spacing rules in JavaScript.
The intended rule is:
automatic rhythm from page scope + section flow + adjacency first
canonical .space-* / .pad-* only when a page genuinely needs an exception
legacy .squeeze* / .pad* aliases only as compatibility bridges
You can check that authored source has stopped relying on the legacy aliases with:
You can also verify that page-scope hooks are still emitted correctly with:
node scripts/check-page-scope-contract.mjs
For normal content sections, wrapper consumes that width contract instead of hardcoding
everything to --size_inner. Content-only banner and spotlight sections now consume the
same shared surface width by default, and still honor the same width classes when authored
explicitly. Media-bearing banner and spotlight sections keep their split layout geometry.
This keeps today’s presets compatible while the section model is generalized beyond the old
banner/spotlight/wrapper split. Markdown section content can opt into a clearer width modifier
by ending a section with an inline content attr block:
Some section body...
--- {.width-wide}
Current width modifiers:
.width-prose
.width-medium
.width-narrow
.width-wide
.width-full
This is the intended path for pages like booking diaries, maps, floorplans, and other content
that should be wider than the normal prose column without forcing a different section family.
Live Edit Metadata
The public page HTML should keep the live edit contract minimal.
For ordinary section / content / media nodes, live DOM should carry only the identity and
state markers needed for selection:
data-ql-section-id
data-ql-block-id
data-ql-editable
transient edit-session markers like data-ql-edit-attached and data-ql-live-selected
The richer editor node contract belongs in #editorData, not repeated across every live node:
ObjectId
NodeKind
Role
Capabilities
InteractionProfile
SchemaKeys
PersistenceAdapter
SourcePath
Structured special blocks like team and buttons should also stay lean in the public DOM and
identify themselves through data-ql-editable, with richer meaning coming from the editor runtime
and the source text rather than repeated public node-contract attributes.
Editorial Hero Backgrounds
You can apply abstract geometric hero backgrounds to wrappers:
> [!style]- Title (collapsible, closed by default)
> ![style] Title (legacy alias, still supported)
> ![style]+ Title (legacy alias)
> ![style]- Title (legacy alias)
Behavior:
title is on the marker line,
subsequent lines are rendered as content.
Example:
> [!warning]+ Read this first
> Back up your data before running the migration.
> This step cannot be undone.
Supported notice styles:
note
todo
info
tip
warning
caution
important
Icon override:
add a block attribute after the quote, e.g. {icon="skull-crossbones"},
add block classes via attributes (e.g. {.my-callout}) to style notice wrappers,
icon names resolve through your icon partial conventions: Custom first, then Font Awesome (solid/, regular/, brands/).
Example with custom alert type:
> [!action] Action
> Style of action buttons like Mermaid zoom or block code copy-to-clipboard.
{icon="skull-crossbones"}
Compatibility notes:
GitHub-style alerts (> [!NOTE]) still render.
Local Markdown Workspace
The site markdown overlay now supports:
file navigator,
in-browser markdown editing with Nowtype,
markdown round-trip for inline emphasis, links, and citations,
relative markdown src/href assets in Nowtype are resolved against the current page markdown path (so page-local figures render in edit mode),
bare relative image filenames in page bundles stay page-local during editor fallback repair (for example henry.jpg in about/index.md stays under /about/ and no longer degrades to /<name>),
the integration now passes the current markdown path into Nowtype via NT_SetFilePath(...) before NT_SetMarkdown(...), so runtime image preview candidates can resolve against the current file instead of guessing from the page root,
in replace-main edit mode, image figures get preview decoration (figure-inline + left/right layout + generated gallery id) and empty captions show a non-destructive preview label,
in edit mode, image previews are rendered as direct <img> nodes (no pswp-item/data-pswp-* link wrapping) so clicks can be used for editor figure actions,
figure captions render with italicized figure titles and use : before caption text when present; caption editing shows a Caption: helper label,
block-level raw HTML in markdown is now preserved as rendered, non-text Nowtype blocks instead of leaking literal source into the live editor surface, so authored structures like Rawson ql-facts / ql-grid-* blocks stay visually aligned with the webpage while text around them remains editable,
fenced structured blocks that Hugo already renders as real objects, such as team, gallery, stats, contact, and buttons blocks, are preserved in live in-place editing by reusing the original Hugo-rendered DOM as a non-text preview instead of leaking raw fence source into the visible page,
live in-place editing suppresses synthetic empty figure captions, so uncaptioned webpage figures no longer sprout preview-only labels like Figure 1,
content-only banner / spotlight sections whose content is just a direct fit figure or iframe embed now drop their inline content padding on narrow/mobile layouts, so shared embeds like Rawson /bookings/ can fill the section width instead of collapsing into a narrow inset card,
image Choose now opens a searchable picker modal of existing project image files (while still allowing typed path/URL apply),
Goldmark figure classes in {...} are applied onto Nowtype preview figures (including section markers like .section-banner),
figure placement remains figure-owned; canonical placement classes are position-* (for example .position-upper, .position-88), while legacy figure-position-* is still accepted on read for compatibility, and both forms are applied directly from the <figure> class list without needing section-level promotion,
content-only sections now strip figure-only section tokens such as orient-*, figure-position-*, and *-figure-fade-in during section class resolution, while home lead sections and explicitly authored fullscreen / halfscreen still keep their screen-size tokens,
same-line image Goldmark attrs like  {.fig .no-link} are normalized to the two-line editor form before entering Nowtype, so section-media figures round-trip the same way as  followed by {.fig .no-link},
preprocess now normalizes over-escaped typography entities inside heading tags after RenderString, so headings and body text do not diverge on smart apostrophes/quotes (for example Robins’ Room),
replace-main edit mode now consumes Hugo-emitted per-section #editorData for resolved section-media placement and layout (where, type, classes, layout, caption, sectionMedia) so banner/section figures are promoted using Hugo’s decisions rather than markdown-shape guesses alone,
replace-main loading/error states keep the top toolbar visible, and the same toolbar now remains available in live-page attach mode instead of disappearing after the initial bridge-backed page/file load,
bibliography preload now stat()-checks common ref.bib candidates before trying to read them, which avoids noisy missing-file bridge requests during editor startup,
callout round-trip + live callout rendering in editor for > [!type][+/-] Title,
Goldmark-style attributes for ids/classes/params ({#id .class key=value}),
hover Attrs popover in Nowtype for editing Goldmark id, classes, style, and extra params,
region presets now include optional wrapped layouts (region-wrap-left / region-wrap-right) so surrounding paragraphs can flow around selected note/figure regions,
editable replace-main page title ([data-page-title="true"]) bridged to front matter title,
PDF mode in Nowtype with a centered editable A4 viewport (A4 ratio + nt.print margins), page-step controls (prev/next), LaTeX book/article mode toggle, chapter title page in book mode, route-aware print actions (Chapter / Part / Book), scoped multi-file editing in PDF mode for part/book routes, and a dedicated print-preview document composed from the paginated PDF editor DOM rather than the browser’s generic flowing print layout,
polling-based external-change detection,
live page refresh via SPA navigation,
prefetched SPA HTML is revalidated against sitemap lastmod before reuse, and localhost navigation no longer reuses ready-cached page HTML after a rebuild, so stale route markup does not survive a Hugo rebuild and keep old image or iframe states alive until a manual reload.
Use this four-process setup as the default regression test harness when checking:
edit-mode startup and toolbar visibility,
live page editing/attach behavior,
bridge path resolution,
multi-site isolation between page roots and bridge roots.
When window.QLMarkdownEditor.localApiBase is omitted in localhost mode, the editor now infers the local bridge from the site port for the standard multi-site matrix:
1313 -> 8787
1314 -> 8787
1315 -> 8788
That keeps Rawson and TutorLumin isolated during local regression runs even when the site override does not emit a site-specific localApiBase.
For source-backed local preview on a non-localhost host such as .qlocal, set these site params explicitly:
params.qlMarkdownEditor.defaultMode = "local" to force the browser onto the localhost bridge instead of WebDAV
params.qlMarkdownEditor.localApiBase = "http://localhost:8787" (or another explicit bridge URL)
params.qlMarkdownEditor.autoEnterEditMode = true to open the bridge surface on page load
params.qlMarkdownEditor.replaceMainInitialView = "focused" to start in the bridge-rendered replace-main view instead of the live-page attach mode
That combination keeps the page body sourced from the markdown bridge, so local markdown edits can appear immediately without waiting for another Hugo rebuild.
The local bridge now exposes GET /project/list (used by the image picker via kind=image).
Bridge contract status:
Implemented high-level ops:
set_local_token
unset_local_token
set_global_token
unset_global_token
read_token_catalog
read_global_tokens
read_local_tokens
set_attrs
clear_attrs
update_menu_item
create_page
insert_section
set_column_span
move_block
GET /capabilities returns the authoritative implemented op list in ops plus opEndpoints and coarse supports.* flags.
token read/catalog endpoints:
GET /ops/token/catalog
GET /ops/token/read-global
GET /ops/token/read-local?path=<markdown-path>&nodeId=<id>&scope=<section|block>&kind=<kind>
replace-main style inspector groundwork:
toggleMarkdown.js includes the first design-token inspector runtime and bridge wiring
replace-main now also exposes an initial shared right-side inspector shell with Content, Layout, Style, Data, and Advanced tabs
focused section Layout actions now route into that shared inspector instead of a separate popover, and the layout tab includes Density alongside family, width, spacing, padding, surface, and alignment
live selection now opens explicit section / block scopes instead of a single generic local bucket
the first panel slice now treats surface as the primary shared token group for width, spacing, density, typography, and nav
legacy section / region groups still render, but only as compatibility groups after surface
non-local scope targeting now distinguishes:
site
page type
page
rather than a single global bucket
replace-main now also includes a first preview-width control on the existing toolbar (Desktop / Tablet / Phone) that constrains the live Nowtype paper width without introducing a second renderer
Attr/token mutation constraints:
POST /ops/token/set-local / unset-local accept target.scope:
section resolves the authored section by ordinal (s0, s1, …) and reads/writes its leading section attr block, creating it when needed
block currently resolves section media first (s0:media) and structured fenced blocks by kind + ordinal (team:0, gallery:0, contact:0, stats:0, testimonials:0, faq:0, booking:0, calendar:0, fundalumin:0, buttons:0), then explicit attr/fence-backed block ids
POST /ops/token/unset-global removes one scoped token assignment from:
params.style.tokens.<group>.<key> for site scope
params.style.page_types.<scope>.tokens.<group>.<key> for page-type scope
style.tokens.<group>.<key> in markdown front matter for page scope
POST /ops/attrs/set only mutates an existing Goldmark attr block found by target.path + target.nodeId
POST /ops/attrs/clear only clears id, classes, and CSS custom properties in an existing Goldmark attr block
attrs/set payload shape:
target: { path, nodeId }
payload.id: optional string, replaces #id
payload.classes: optional string array, replaces the full class list
payload.style: optional object of CSS custom properties to set
attrs/clear payload shape:
target: { path, nodeId }
payload.id: true removes #id
payload.classes: true removes all classes, or payload.classes: ["name"] removes only the listed classes
payload.style: true removes the full style="..." attr, or payload.style: ["--token"] removes only the listed CSS custom properties
Planned ops such as the remaining page/section/menu/site routes are documented in editor-plan.md but are not yet implemented.
The editor family is intentionally converging on three shared Nowtype-backed surfaces rather than separate rendering authorities:
page WYSIWYG editor
single-page PDF editor
double-page spread PDF editor
Those are editing surfaces over the same Hugo/markdown source contract, so shared token scopes and section semantics should work across all three rather than being re-implemented per mode.
The prioritized product roadmap for making that feel more like a SquareSpace/Figma-class editor is in
docs/editor-product-roadmap.md.
GET /project/watch is available in local mode for project-root polling/SSE workflows
replace-main edit mode still falls back cleanly when watch is unavailable
The browser overlay will default to local mode on localhost.
By default, edit mode uses editorPresentation: "replace-main": it hides page <main> and mounts the Nowtype workspace in its place.
That replace-main surface uses embedded Nowtype (nowtype.min.js) with web-demo-compatible DOM anchors (#toolbar, #backbox, #paper, #render_div) and a minimal page-editor UI (current file context + editor area, no file-tree workbench, no iframe).
inline and popup presentations remain optional configuration modes.
The live-main attach path now uses Hugo-emitted data-ql-* identities and the formal section/content/media node contract (ObjectId, NodeKind, Role, Capabilities, InteractionProfile, SchemaKeys, PersistenceAdapter) to index the real rendered <main> while edit mode is active.
In normal edit mode, clicking a live content block now opens true in-place Nowtype editing on that visible page content instead of falling back to a second full-page editor shell. Section media and section-chrome layout edits still route through the focused replace-main tools. The old live action panel (Text / Media / Layout / Tokens / Cancel) is no longer part of the normal workflow.
Browser smoke now treats in-place content editing as the required contract on the local anchor pages (1314/about, 1315/, 1315/about-us/, 1315/facilities/). The facilities route also asserts that preserved raw HTML blocks stay rendered instead of leaking literal source text and that live in-place figures do not show synthetic empty captions. Live text sessions now preserve the original click point when placing the caret, and section structure/media controls are intentionally suppressed during an active live text session so stale section snapshots cannot clobber newer layout mutations. Remaining in-place caveats are deeper session rebasing if those controls are later re-enabled, empty-section UX polish, and richer editing for currently atomic raw-HTML blocks.
PDF helper logic now has a dedicated runtime helper module at /cdn/custom/nowtypePdfCore.js, loaded before toggleMarkdown.js so PDF sizing/scope/page-role rules are centralized.
PDF mode is available from the Nowtype toolbar and keeps the page centered while fixing page height to the viewport; LaTeX toggles book/article behavior, print scope follows the currently opened route (index.md => Book, top-level section _index.md => Part, deeper pages => Chapter), entering PDF mode on Part/Book routes loads an editable scoped document spanning all markdown files in that scope, an optional rendered ToC page sequence can be toggled from the toolbar and clicked to jump to headings, and figures that would cross a page boundary are shrunk/centered only at clean scale steps (0.95/0.9/0.85) or moved to the next page boundary. The print action now opens a separate print-ready document built from the same paginated PDF editor DOM (single-page or 2-up spread), so “Save to PDF” uses the editor’s composed page shells instead of reverting to the browser’s normal flowing print view.
For TeX/PDF output, SVG figures rendered through layouts/_default/_markup/render-image.tex now apply a doubled font context to the pdf_tex overlay layer, so LaTeX labels inside SVG figures render at roughly 2x their prior size.
For HTML output, inline SVG figures rendered through layouts/partials/figure/svg.html now scale the injected KaTeX <foreignObject> wrapper to roughly 2x as well, so $...$ / $$...$$ labels inside SVG figures stay visually consistent with the PDF path.
Browser Runtime Logging
For local debugging, the repo includes a small Playwright script that launches Firefox,
opens your local site, and writes browser console/page/request failures to browser.log
in the current working directory.
Default target:
http://127.0.0.1:1313
Start your site first, for example from the site repo:
cd ~/Projects/Research
./hugo dev --hugo-only
Then, from this repo:
npm run browser:log
Useful variants:
npm run browser:log:headed to watch Firefox
BROWSER_LOG_URL=http://127.0.0.1:1314 npm run browser:log for a non-default port
node scripts/browser-log.js http://127.0.0.1:1313 to pass the URL directly
npm run browser:smoke:pdf for a quick PDF-editor smoke (click-to-type focus, --- section page breaks, spread navigation)
BROWSER_SMOKE_URL=http://127.0.0.1:1313/2-methodology/ npm run browser:smoke:pdf to target a specific page
npm run browser:export:pdf -- --url http://127.0.0.1:1313/ --output /tmp/book.pdf to capture the current route through the editor’s print-ready PDF path
npm run browser:export:pdf -- --site-root /home/henry/Projects/Research/PhD --output /home/henry/Projects/Research/PhD/build/pdf/browser/phd-double-spread.pdf to auto-start the PhD site + local bridge and export the full book as a 2-up spread PDF
The export script drives the existing editor workflow rather than a second renderer:
it opens the page on localhost,
enters edit mode,
switches to PDF mode,
enforces the requested book/article, paper, font, ToC, and 2-up spread settings,
opens the same print-ready popup used by the UI,
and saves that popup with Chromium page.pdf().
When --site-root is provided, the script starts both:
This smoke is intentionally stronger than a visibility check. After clicking the first live content block, it now asserts:
an active editor surface exists (in_place or safe replace_main fallback),
the visible content does not disappear into a hidden live mount,
a typed probe token changes NT_GetMarkdown() in the active session.
This is the current guard against the “click text and it goes blank instead of becoming editable” regression.
check-section-class-contract.mjs is the shared render-side guard for section/media ownership. It verifies that content-only sections do not leak media-only tokens like halfscreen, orient-*, or *-figure-fade-in, while real media sections still retain the section-level size/orientation tokens the shared layout CSS expects.
check-figure-position-contract.mjs is the shared guard for figure-owned placement. It verifies that canonical position-* classes remain on <figure> elements, do not leak back onto section roots, and compile to direct figure.position-* CSS selectors.
check-head-asset-contract.mjs is the shared head/runtime guard. It verifies that local pages emit
the expected stylesheet/runtime chain and that the linked assets actually return 200:
/css/style.css
fingerprinted variables.css
/css/brochure.css where brochure mode is enabled
prefetch/prefetch.js
editor/nowtype.min.js
custom/toggleMarkdown.js
check-responsive-layout.mjs is the shared mobile/layout guard. It checks Rawson and TutorLumin at a phone viewport and currently asserts:
no horizontal page overflow,
mobile header nav stays on rows instead of collapsing into a column,
shared fit figures/iframe embeds fill the content width on routes like /bookings/ and /hall-layout/,
Chris homepage fixed chrome still leaves the footer visible on first load.
check-structured-block-editors.mjs is the shared modal-editor smoke for structured brochure blocks.
It currently opens and closes the live page editors for:
team
gallery
contact
testimonials
faq
booking
check-tutorlumin-student-platform.mjs is the integrated TutorLumin smoke for the current
student-facing shared widgets. It verifies:
/fundalumin/ renders live Fundalumin campaign cards and wishlist items,
/calendar/ renders the shared public timetable with room filters and date cells,
/enrol/ renders the shared booking widget with a real week-strip and bookable slots,
optional --reserve mode can also create a real local hosted checkout session through
booking + payments
check-pswp-spa.mjs is the shared PhotoSwipe + SPA regression. It verifies the real failure path:
navigate to /gallery/ through the SPA
confirm structured gallery items still carry data-pswp-width / data-pswp-height
confirm the first image click opens PhotoSwipe instead of falling through to the raw image URL
The responsive smoke now prefers a sandbox-safe Chromium launch (--no-sandbox --disable-setuid-sandbox --disable-dev-shm-usage) before falling back to Firefox, so local CI/sandbox runs do not depend on Firefox surviving large Rawson pages.
Structured gallery blocks now render PhotoSwipe-ready anchors server-side, and the shared PhotoSwipe init treats bare .pswp-item links as valid children with inferred dimensions. That keeps the first gallery click working after SPA navigation instead of depending on late client-side wrapping.
The shared header.style1 mobile contract now also treats #special as a full-row CTA on narrower widths, and layouts/partials/logo.html no longer emits an empty .logo anchor when a site does not actually define a logo/title/home-icon payload.
The shared section contract now distinguishes three render flows:
content_only: no section media, so figure-only section tokens are stripped; homepage lead sections and explicitly authored fullscreen / halfscreen can still keep screen-size section tokens
figure_led: section media exists but uses figure-led layouts like .fit, so split-screen size/orientation defaults are stripped unless the authored section attrs explicitly request split semantics
split: section media participates in split banner/spotlight layout, so section-level halfscreen/fullscreen and orient-* tokens remain canonical
Rendered section roots now also expose that shared flow as a single hook class:
ql-flow-content-only
ql-flow-figure-led
ql-flow-split
Rendered section roots now also carry a neutral shared shell class:
ql-section
That neutral shell is where the shared framework now hangs ordinary section rhythm and content-only behavior, so canonical width/spacing/fullscreen handling is no longer duplicated separately in every legacy family selector.
The first neutral section-axis slice is now also emitted directly on rendered section roots and in
#editorData. Every resolved section now carries:
flow
media_side
size
content_align
surface
layout_family
Those values currently surface as:
data-ql-flow
data-ql-media-side
data-ql-size
data-ql-content-align
data-ql-section-surface
data-ql-layout-family
This is intentionally additive. banner / spotlight / wrapper classes still render exactly as
before, but Hugo now emits a smaller semantic contract that the editor and future SCSS can move to
without rediscovering section meaning from legacy class bundles.
The first additive SCSS migration is now also in place for the smallest safe slice:
data-ql-flow="figure_led"
data-ql-size="full" | "half"
data-ql-section-surface="intro_card"
Those hooks now alias the existing shared figure_led, full/half, and intro-card selectors in
_wrapper.scss, _banner.scss, and _spotlight.scss. Legacy classes still remain in place as the
compatibility path while the rest of the section geometry migrates.
Explicitly authored section attrs now also count as an intentional section frame, even when the section body is empty. That keeps effect-only sections such as a pure .fx-starfield banner renderable after a --- split, instead of silently discarding the section or treating the attr line itself as paragraph content.
Shared viewport metrics now come from cdn/custom/layoutMetrics.js, imported through head/import/custom.html. It publishes --ql-main-offset-top and --ql-main-viewport-height on :root, which the shared section/effect SCSS can use for “fill the rest of the viewport after the header” layouts without site-specific JS.
figure_led sections now preserve promoted or base layout families when present instead of being forced back to wrapper. Explicit section attrs also win over figure-led inference: if an authored section declares banner / spotlight with orient-left or orient-right, the shared contract keeps that section on the split-flow path even when the figure itself carries .fit. Shared wrapper styling still uses ql-flow-figure-led to widen figure-led sections like /hall-layout/ without changing prose-width behavior for ordinary inline figures on single pages.
Canonical intro-card sections and the temporary .ql-hero / .ql-hero-card compatibility path are now driven by shared intro-card mixins in assets/scss/main.scss, with banner / spotlight consuming that shared behavior instead of carrying separate copies of the same content-only rules.
For deeper browser diagnosis, window.__qlNowtypePdfSmoke.getEditorState() now exposes the current editor/runtime state used by the smoke harness:
presentation and editor kind
current file/current page file
live-main attach/focus flags
live-main index summary
current live-main selection summary
Open browser.log after the run and inspect the first errors.
WebDAV mode (production)
The theme now supports a minimal setup: set one Hugo param and include one partial.
If you need advanced options, you can still set global config before toggleMarkdown.js runs:
<script>
window.QLMarkdownEditor= {
defaultMode:"webdav",
webdavBase:"https://example.com/webdav/content/",
webdavAuth: { username:"user", password:"pass" },
nowtypePageSrc:"/cdn/editor/web.demo.html", // used as asset-base reference
nowtypeEnabled:true,
nowtypeSrc:"https://cdn.quantalumin.com/editor/nowtype.min.js" };
</script>
Optional cache-bust: set params.qlMarkdownEditor.assetVersion (or params.assetVersion) and the editor scripts will append ?v=... automatically. When no explicit version is set, the shared head chain now adds development-only ?v=... query params to local /css/... and /cdn/... assets through helpers/asset-url.html, so an ordinary refresh on localhost picks up fresh CSS/JS without needing a hard reload. The same version is still applied to the service-worker registration URL, and localhost/127.0.0.1 sessions automatically unregister old service workers and clear stale caches on first load so editor/runtime fixes are not masked by cached assets.
Mode can also be switched in the overlay UI.
Attributions
Font-Awesome icons detached from their repo for easier importing of this theme
Lula, unfortunately, doesn't have opposable thumbs
You didn't really think Lula would have personal website, did you?
SphereList (c) 2025 QuantaLumin
TutorLumin partners with QuantaLumin
You’re connecting to your QuantaLumin account on
members.quantalumin.com.