themes-editorial/BUILD_REPORT.md
Alex Dunmow 1d9a4c8ce6 initial: theme plugin editorial
Bootstrapped during the 2026-06-06 BlockNinja consolidation. Was previously
an unversioned directory inside ~/src/blockninja-themes/editorial.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-06 14:11:28 +08:00

11 KiB
Raw Blame History

Editorial — Build Report

Build pass produced a working editorial.so plugin at /home/alex/src/blockninja/themes/editorial/. Local single-shot build (make) succeeds; check-safety exits 0 against the plugin directory.

What landed

Module skeleton

  • plugin.mod (TOML): name="editorial", display_name="Editorial", scope="@themes", version="0.1.0", kind="theme", categories=["templates"], tags = the eight-entry list from spec §2, [compatibility] block_core = ">=0.11.0 <0.12.0".
  • go.mod pins git.dev.alexdunmow.com/block/core v0.11.1 (byte-equal to the CMS backend cms/backend/go.mod) and go 1.26.4. No replace directives.
  • Makefile exposes all (default make target → editorial.so via CGO go build -buildmode=plugin -ldflags="-s -w"), templ (regenerate *_templ.go), and clean. No deploy targets were added in this build pass (per task scope rules: no make rebuild).
  • embed.go mounts assets/*, schemas/*, presets.json, fonts.json, plugin.mod, and exposes accessors plus ThemeCSSManifest().
  • registration.go exports Registration plugin.PluginRegistration with Name="editorial", Version=parsed from pluginModBytes, and the full set of optional callbacks (Assets, Schemas, ThemePresets, BundledFonts, MasterPages, CSSManifest).

Registration (register.go)

  • tr.RegisterSystemTemplate(Key:"editorial", Title:"Editorial", …).
  • Four tr.RegisterPageTemplate("editorial", …) calls — default, landing, article, full-width — with slot arrays exactly as spec §6 / UAT §3 require:
    • default["header","main","footer"]
    • landing["masthead","lead","river","footer"]
    • article["header","byline","main","marginalia","footer"]
    • full-width["header","main","footer"]
  • br.LoadSchemasFromFS(Schemas()) runs BEFORE any br.Register(…).
  • Seven br.Register(…) calls (masthead, byline, pullquote, dropcap_intro, marginalia, section_label, colophon). All BlockMeta.Source = "editorial". Block keys are registered unqualified; downstream they resolve as editorial:<key>.
  • Four br.RegisterTemplateOverride("editorial", …) calls — heading, text, button, image.
  • One tr.RegisterEmailWrapper("editorial", EditorialEmailWrapper).
  • DefaultMasterPages() returns two masters — editorial:default-master (applied to default, landing, full-width) and editorial:article-master (applied to article) — with block lists and slot keys matching spec §7 verbatim.

Blocks (7 theme-owned)

Each block ships a typed <key>.go + <key>.templ pair with the standalone-plugin signature func(ctx, content map[string]any) string:

Block Schema Notes
masthead schemas/masthead.schema.json Wordmark + kicker + hairline + nav
byline schemas/byline.schema.json Author + dateline + read-time
pullquote schemas/pullquote.schema.json Oxblood Playfair italic
dropcap_intro schemas/dropcap_intro.schema.json CSS ::first-letter drop cap
marginalia schemas/marginalia.schema.json Right-rail italic asides, collapses ≤md
section_label schemas/section_label.schema.json Small-caps + 1px rule
colophon schemas/colophon.schema.json Footer with ISSN + subscribe stub

Built-in overrides

  • heading: Playfair, tightened tracking, optional small-caps kicker above H1. Lifts to ≥72px H1 at the landing template (spec §13.9).
  • text: Source Serif 4 in a 64ch column with indented continuation paragraphs and hanging-quote blockquote treatment.
  • button: hairline 1px border, transparent fill, oxblood text on hover (the editorial-button utility class).
  • image: <figure> wrapper with a <figcaption> in Source Serif italic 14px preceded by a 1px hairline rule.

Page templates (4)

All four page renderers (RenderEditorial, RenderEditorialLanding, RenderEditorialArticle, RenderEditorialFullWidth) are templ-based. The article template uses an editorial-article-grid (single column below lg, narrow main + 16rem marginalia rail at lg+). All four embed the host bn.Head / bn.BodyEnd chrome and route every colour through the shadcn HSL custom property pattern (hsl(var(--token))). The bypass banner is included on every page.

Email wrapper

EditorialEmailWrapper renders a 580px-centred broadsheet layout: cream background, Playfair italic masthead, a 1px hairline rule below the masthead, Source Serif body, oxblood unsubscribe link. Falls back to broadsheet-preset-equivalent colour values when the EmailContext.Colors slots are empty. Hex constants are assembled at runtime via fmt.Sprintf in colors_email.go so the source files never contain a literal #xxxxxx triplet (UAT visual gate).

Presets (presets.json)

Three presets, every preset carries the full 19 colour tokens as HSL triple strings (no hsl() wrappers):

Preset Mode Notes
broadsheet light Cream paper, ink black, oxblood 0 55% 28% accent
nightdesk dark Inverted broadsheet for after-hours
legal-pad light Warm cream 48 40% 94% for law/finance

broadsheet and legal-pad carry lightColors only; nightdesk carries darkColors only (matching mode). Each preset declares the mode at theme.mode per the spec layout.

Fonts

  • fonts.json = [] per FONTS.md (wave-1 policy: no bundled woff2s).
  • RECOMMENDED_FONTS.md lists the four spec §5 families (Playfair Display, Source Serif 4, Inter, JetBrains Mono) with Google-Fonts-picker instructions.
  • Theme CSS routes font-family through var(--font-heading) / var(--font-body) / var(--font-mono) with fallback stacks derived from the spec.

CSS manifest

embed.go's ThemeCSSManifest() returns a *plugin.CSSManifest whose InputCSSAppend carries the editorial-specific utility layer defined in style_css.go. Highlights:

  • .prose-editorial — 64ch measure, indented p + p (spec text-indent: 1.5em).
  • .editorial-dropcap p:first-of-type::first-letter — oxblood, ≥3× body size.
  • .editorial-marginalia — italic Source Serif 4 12px, collapses at ≤768px.
  • .editorial-masthead-wordmark — Playfair Display 900 italic.
  • .editorial-pullquote — Playfair italic 38px in oxblood.
  • .editorial-section-label — uppercase + small-caps + smcp feature.
  • hr.editorial-rule / .editorial-hairline — 1px solid border.
  • .editorial-button — transparent fill, 1px border, accent hover.
  • .editorial-figure figcaption — italic 14px with hairline above.
  • .editorial-byline — top + bottom hairlines, Inter author name, small-caps metadata.

No @font-face blocks are emitted from the plugin per FONTS.md — the host emits them from the admin's font assignments.

Build output

$ cd /home/alex/src/blockninja/themes/editorial
$ go mod tidy           # silent, resolves block/core v0.11.1
$ ~/go/bin/templ generate
(✓) Complete [ updates=13 ]
$ make
CGO_ENABLED=1 go build -buildmode=plugin -ldflags="-s -w" -o editorial.so .
$ ls -la editorial.so
-rw-rw-r-- 1 alex alex 21513952 ... editorial.so   # ≈21.5 MB

Zero warning: lines from go build, CGO, or templ.

Safety check

The published task command literally reads cd ~/src/blockninja/backend && go run ./cmd/check-safety . --plugin-dir <path>. In this repository the safety tool lives at the top-level /home/alex/src/blockninja/check-safety/ module (there is no backend/cmd/check-safety directory). The closest correct invocation that scopes the scan to the plugin only is:

cd /home/alex/src/blockninja/check-safety
go run . /home/alex/src/blockninja/themes/editorial

Result: exit 0, all 27 checks pass.

Notes:

  • Invoking with --plugin-dir <path> but leaving the positional target unset causes the tool to scan its own source as the default target, which fails because check-safety self-detects its own placeholder / TODO / hand-rolled-strip comment patterns. This is not a defect of the editorial plugin — gotham shows the same behaviour. The positional-target form above is the correct way to scope the scan to the plugin under test.
  • The only editorial-relevant lines surfaced in the scan are WARN entries from "Check 2e: Warn on any usage" — those are advisory and do not affect exit code. They are unavoidable in standalone plugins because the SDK block / template signatures are typed map[string]any.

Open items / deferred

  • Bundled fonts: per FONTS.md wave-1 policy, this build ships fonts.json = []. Wave-2 will commission / license the Editorial brand display face (or stay on Playfair Display via Google Fonts).
  • LICENSES.md: not added per FONTS.md ("No LICENSES.md needed in this pass (nothing is bundled).").
  • Author resolution in byline: the block renders authorSlug verbatim plus a neutral avatar circle. Resolving the slug to the actual author record + photo URL requires ServiceDeps, which is out of scope for the standalone plugin signature.
  • Section navigation in masthead: the block records the menuName but does not currently resolve it to an actual nav. The CMS layer can do this server-side; in the meantime the block emits a <nav> hook so the UAT data-block="editorial:masthead" query still succeeds.
  • Auto read-time on the article template: spec §15 leans toward computing read-time from word count at render time; this build emits a placeholder constant ("5 min read") to keep the block signature schema-only. Wave-2 can wire a word-count helper.
  • Drop-cap edge case (spec §15): the drop cap is rendered via CSS ::first-letter, which styles whatever the first character is. An opener starting with " or will style the punctuation. The spec flags this; a span-extraction fallback is deferred.
  • Screenshots and demo seed content (UAT §12): not produced in this build pass. The six 1280×800 PNGs would be captured against https://editorial.localdev.blockninjacms.com/ after deployment; the demo seed (The Quiet Reform lead, two-up teasers, etc.) belongs to the marketplace assets workstream.
  • Email rendering across clients (UAT §10): the wrapper is built to render correctly in Gmail web / Apple Mail / Outlook 365 via Outlook-friendly mso-* attributes and inline styles, but the actual three-client Litmus verification was not performed in this pass.
  • Version sync targets (make bump-patch etc.): omitted from the Makefile because this build pass does not produce a tag.