feat: convert to codeless .bnp — templates + manifest, Go/templ deleted (WO-WZ-021)

Convert the Editorial theme from a Go+templ wasm plugin to a fully codeless
.bnp: 7 blocks + 4 built-in overrides + email wrapper transcribed templ→pongo2
.ninjatpl (byte-faithful, parity-verified 55/55 identical), 4 page templates,
blocks/blocks.yaml (fully-qualified editorial:<key>), master_pages.json, and
manifest.yaml (system/page templates, template_overrides, email_wrapper, css
input_css_append). Schemas moved schemas/→blocks/. All Go, templ, go.mod/go.sum
deleted. plugin.mod bumped 0.1.2→0.2.0, [compatibility] dropped. Makefile
rewritten to codeless targets.

Gates: ninja plugin build → (codeless, no plugin.wasm); ninja plugin verify → OK;
check-safety → OK.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Alex Dunmow 2026-07-04 15:27:31 +08:00
parent 6c9426f69c
commit efbfaea74c
74 changed files with 550 additions and 4178 deletions

View File

@ -1,26 +1,27 @@
# editorial — build & publish helpers (wasm .bnp workflow) # editorial — build & publish helpers (CODELESS .bnp workflow, WO-WZ-021)
# #
# The theme compiles to a wasip1 reactor-mode wasm module packed into a .bnp # This theme is a CODELESS .bnp: pure declaration the host runs — blocks
# artifact (`ninja plugin build`). The legacy .so path is retired (WO-WZ-015): # (blocks/blocks.yaml + .ninjatpl), page/system templates, template overrides,
# the CMS is wasm-native and can no longer load .so plugins. Distribution is # an email wrapper, master pages, presets, fonts and CSS in manifest.yaml. No
# publish-to-registry -> InstallFromRegistry (hot-load, no restart). # Go, no templ, no plugin.wasm, no block/core dependency. `ninja plugin build`
# classifies the repo as codeless (no Go at the root) and packs the artifact
# with manifest.pb (codeless: true) and NO plugin.wasm.
# #
# Usage: # Usage:
# make build # compile wasm + pack editorial-<version>.bnp # make build # pack editorial-<version>.bnp (codeless — no wasm)
# make verify # validate the built .bnp (layout/abi/name/size) # make verify # validate the built .bnp (loader-identical checks)
# make archive-check # prove a clean `git archive HEAD` compiles to wasm # make archive-check # prove a clean `git archive HEAD` still builds codeless
# make publish # ninja plugin publish --bnp (to the active registry) # make publish # ninja plugin publish --bnp (to the active registry)
# make templ # regenerate templ Go files
# make clean # remove build artefacts # make clean # remove build artefacts
.PHONY: build verify archive-check publish templ clean help .PHONY: build verify archive-check publish clean help
PLUGIN_NAME := editorial PLUGIN_NAME := editorial
NINJA := ninja NINJA := ninja
BNP := $(PLUGIN_NAME)-$(shell grep '^version' plugin.mod | sed 's/.*"\(.*\)"/\1/').bnp BNP := $(PLUGIN_NAME)-$(shell grep '^version' plugin.mod | sed 's/.*"\(.*\)"/\1/').bnp
# Compile to wasm and pack the .bnp (GOOS=wasip1 GOARCH=wasm, DESCRIBE probe, # Pack the codeless .bnp (manifest.pb synthesized from plugin.mod + manifest.yaml
# tar.zst of plugin.wasm + plugin.mod + manifest.pb + schemas/ + assets/). # + blocks/ + templates/; no wasm compile, no DESCRIBE probe).
build: build:
$(NINJA) plugin build --dir . $(NINJA) plugin build --dir .
@ -28,12 +29,12 @@ build:
verify: verify:
$(NINJA) plugin verify $(BNP) $(NINJA) plugin verify $(BNP)
# Prove a clean `git archive HEAD` (what publish ships) compiles to wasm. # Prove a clean `git archive HEAD` (what publish ships) still packs codeless.
archive-check: archive-check:
@T=$$(mktemp -d /tmp/archive-check-$(PLUGIN_NAME).XXXXXX) && \ @T=$$(mktemp -d /tmp/archive-check-$(PLUGIN_NAME).XXXXXX) && \
trap 'rm -rf "$$T"' EXIT && \ trap 'rm -rf "$$T"' EXIT && \
git archive HEAD | tar -x -C "$$T" && \ git archive HEAD | tar -x -C "$$T" && \
cd "$$T" && GOOS=wasip1 GOARCH=wasm go build -buildmode=c-shared -o "$$T/check.wasm" . && \ cd "$$T" && $(NINJA) plugin build --dir . && \
echo "archive-check OK" echo "archive-check OK"
# Publish the prebuilt .bnp to the active registry. # Publish the prebuilt .bnp to the active registry.
@ -41,19 +42,14 @@ archive-check:
publish: build publish: build
$(NINJA) plugin publish --bnp $(BNP) $(NINJA) plugin publish --bnp $(BNP)
# Regenerate templ Go files locally.
templ:
templ generate
# Remove build artefacts. # Remove build artefacts.
clean: clean:
rm -f $(PLUGIN_NAME)-*.bnp $(PLUGIN_NAME).so plugin.wasm rm -f $(PLUGIN_NAME)-*.bnp plugin.wasm
help: help:
@echo "Targets:" @echo "Targets:"
@echo " build Compile wasm + pack the .bnp" @echo " build Pack the codeless .bnp (no wasm)"
@echo " verify Validate the built .bnp (loader-identical checks)" @echo " verify Validate the built .bnp (loader-identical checks)"
@echo " archive-check Prove a clean git archive HEAD compiles to wasm" @echo " archive-check Prove a clean git archive HEAD packs codeless"
@echo " publish Publish the .bnp to the active registry" @echo " publish Publish the .bnp to the active registry"
@echo " templ Regenerate templ Go files"
@echo " clean Remove build artefacts" @echo " clean Remove build artefacts"

48
blocks/blocks.yaml Normal file
View File

@ -0,0 +1,48 @@
# Editorial theme block definitions (codeless .bnp, WO-WZ-021).
# Keys are fully qualified (editorial:<key>) because the codeless loader
# registers definitions verbatim — there is no PluginBlockRegistry prefixing
# in the codeless path (unlike the wasm/templ path). The qualified keys match
# the hardcoded data-block attributes and the master_pages.json references.
blocks:
- key: editorial:masthead
title: Masthead
description: Newspaper-style masthead with Playfair wordmark and hairline rule
category: navigation
schema: masthead.schema.json
template: masthead.ninjatpl
- key: editorial:byline
title: Byline
description: Author byline with optional photo, dateline, and read time
category: theme
schema: byline.schema.json
template: byline.ninjatpl
- key: editorial:pullquote
title: Pull Quote
description: Large oxblood pull quote in Playfair italic
category: theme
schema: pullquote.schema.json
template: pullquote.ninjatpl
- key: editorial:dropcap_intro
title: Drop Cap Intro
description: Opening paragraph with an oxblood Playfair drop cap
category: theme
schema: dropcap_intro.schema.json
template: dropcap_intro.ninjatpl
- key: editorial:marginalia
title: Marginalia Rail
description: Right-rail italic notes for longform articles
category: theme
schema: marginalia.schema.json
template: marginalia.ninjatpl
- key: editorial:section_label
title: Section Label
description: Small-caps section label with optional hairline divider
category: theme
schema: section_label.schema.json
template: section_label.ninjatpl
- key: editorial:colophon
title: Colophon
description: Editorial footer with ISSN, subscribe stub, and link list
category: navigation
schema: colophon.schema.json
template: colophon.ninjatpl

1
blocks/byline.ninjatpl Normal file
View File

@ -0,0 +1 @@
<div class="editorial-column" data-block="editorial:byline"><div class="editorial-byline">{% if showPhoto != "false" and showPhoto != false and authorSlug %}<div class="editorial-byline-photo" aria-hidden="true"></div>{% endif %}<div class="flex-1">{% if authorSlug %}<div class="editorial-byline-author">By {{ authorSlug }}</div>{% else %}<div class="editorial-byline-author">By Editorial Staff</div>{% endif %}<div class="editorial-byline-meta">{% if dateline %}<span>{{ dateline }}</span>{% endif %}{% if showReadTime != "false" and showReadTime != false %}{% if dateline %}<span class="px-1">&middot;</span>{% endif %}<span>5 min read</span>{% endif %}</div></div></div></div>

1
blocks/colophon.ninjatpl Normal file
View File

@ -0,0 +1 @@
<div class="editorial-wide editorial-colophon" data-block="editorial:colophon">{% if showSignup == true or showSignup == "true" %}<div class="mb-4"><div class="editorial-section-label mb-1">Subscribe</div><form class="flex gap-2 max-w-md"><input type="email" placeholder="you@example.com" class="flex-1 px-3 py-1 border editorial-hairline" aria-label="Email address"><button type="submit" class="editorial-button">Subscribe</button></form></div>{% endif %}{% if links %}<nav aria-label="Footer"><ul class="flex flex-wrap gap-x-4 gap-y-1">{% for link in links %}<li><a href="{{ link.url|default:"#" }}" class="editorial-button" style="border:0;padding:0;letter-spacing:0.05em;">{{ link.text }}</a></li>{% endfor %}</ul></nav>{% endif %}{% if issn %}<div class="mt-3"><span class="editorial-colophon-issn">ISSN {{ issn }}</span></div>{% endif %}</div>

View File

@ -0,0 +1 @@
{% if lead %}<div class="editorial-column editorial-dropcap prose-editorial" data-block="editorial:dropcap_intro">{{ lead|safe }}</div>{% endif %}

View File

@ -0,0 +1 @@
<div class="editorial-marginalia" data-block="editorial:marginalia">{% if items %}{% for note in items %}<aside>{% if note.anchor %}<span class="editorial-byline-meta">{{ note.anchor }}</span>{% endif %}{% if note.note %}{{ note.note|safe }}{% endif %}</aside>{% endfor %}{% else %}<aside class="text-xs opacity-60"></aside>{% endif %}</div>

1
blocks/masthead.ninjatpl Normal file
View File

@ -0,0 +1 @@
<div class="editorial-wide" data-block="editorial:masthead">{% if compact == true or compact == "true" %}<div class="flex items-center justify-between py-3"><a href="/" class="editorial-masthead-wordmark text-2xl">{{ title|default:"Editorial" }}</a>{% if menuName %}<span class="editorial-masthead-kicker">{{ menuName }}</span>{% endif %}</div><hr class="editorial-masthead-rule">{% else %}<div class="text-center py-6">{% if kicker %}<div class="editorial-masthead-kicker mb-2">{{ kicker }}</div>{% endif %}<a href="/" class="editorial-masthead-wordmark inline-block text-5xl md:text-6xl">{{ title|default:"Editorial" }}</a><hr class="editorial-masthead-rule mt-4">{% if menuName %}<nav class="editorial-section-label mt-3" aria-label="Sections"><span class="text-xs">Sections: {{ menuName }}</span></nav>{% endif %}</div>{% endif %}</div>

View File

@ -0,0 +1 @@
{% if quote %}<aside class="editorial-column" data-block="editorial:pullquote"><blockquote class="editorial-pullquote">{{ quote|safe }}{% if attribution %}<cite class="editorial-pullquote-attribution">{{ attribution }}</cite>{% endif %}</blockquote></aside>{% endif %}

View File

@ -0,0 +1 @@
{% if label %}<div class="editorial-column" data-block="editorial:section_label"><div class="editorial-section-label">{{ label }}</div>{% if divider != "false" and divider != false %}<hr class="editorial-section-label-rule">{% endif %}</div>{% endif %}

View File

@ -1,25 +0,0 @@
package main
import (
"bytes"
"context"
)
// EditorialButtonBlock overrides the built-in `button` block for the
// editorial template. The editorial button is hairline-only: 1px border,
// transparent fill, oxblood text on hover (see .editorial-button CSS).
//
// Content shape: { "text", "url", "ariaLabel" }.
func EditorialButtonBlock(ctx context.Context, content map[string]any) string {
text := getString(content, "text")
url := getString(content, "url")
ariaLabel := getString(content, "ariaLabel")
if text == "" {
return ""
}
var buf bytes.Buffer
_ = editorialButtonComponent(text, url, ariaLabel).Render(ctx, &buf)
return buf.String()
}

View File

@ -1,15 +0,0 @@
package main
// editorialButtonComponent renders an editorial-styled button. The element is
// always <button> (UAT §13.11 measures computed style on <button>); when a URL
// is supplied a same-element wrapper anchor is emitted around the button so
// the existing tab order and keyboard semantics remain intact.
templ editorialButtonComponent(text, url, ariaLabel string) {
if url != "" {
<a href={ templ.SafeURL(linkHref(url)) } class="inline-block" aria-label={ ariaLabel }>
<button type="button" class="editorial-button">{ text }</button>
</a>
} else {
<button type="button" class="editorial-button" aria-label={ ariaLabel }>{ text }</button>
}
}

View File

@ -1,116 +0,0 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.1020
package main
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
// editorialButtonComponent renders an editorial-styled button. The element is
// always <button> (UAT §13.11 measures computed style on <button>); when a URL
// is supplied a same-element wrapper anchor is emitted around the button so
// the existing tab order and keyboard semantics remain intact.
func editorialButtonComponent(text, url, ariaLabel string) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
if url != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<a href=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var2 templ.SafeURL
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(linkHref(url)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `button_override.templ`, Line: 9, Col: 40}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "\" class=\"inline-block\" aria-label=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.ResolveAttributeValue(ariaLabel)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `button_override.templ`, Line: 9, Col: 86}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var3)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "\"><button type=\"button\" class=\"editorial-button\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(text)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `button_override.templ`, Line: 10, Col: 56}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "</button></a>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<button type=\"button\" class=\"editorial-button\" aria-label=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.ResolveAttributeValue(ariaLabel)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `button_override.templ`, Line: 13, Col: 71}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var5)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var6 string
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(text)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `button_override.templ`, Line: 13, Col: 80}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "</button>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
return nil
})
}
var _ = templruntime.GeneratedTemplate

View File

@ -1,56 +0,0 @@
package main
import (
"bytes"
"context"
"git.dev.alexdunmow.com/block/core/blocks"
)
// BylineBlockMeta describes the Editorial byline block: author name, optional
// avatar, dateline, and read time.
// Registered as "editorial:byline" at runtime.
var BylineBlockMeta = blocks.BlockMeta{
Key: "byline",
Title: "Byline",
Description: "Author byline with optional photo, dateline, and read time",
Source: "editorial",
Category: blocks.CategoryTheme,
}
// BylineData carries parsed byline content.
//
// Author resolution: this plugin does not resolve the slug to an actual author
// record (that would require ServiceDeps and a database call). Instead the
// authorSlug is rendered verbatim in the byline label. The CMS layer can
// replace the slug with the resolved name when integrating with the author
// directory.
type BylineData struct {
AuthorSlug string
ShowPhoto bool
ShowReadTime bool
Dateline string
}
// BylineBlock renders the article byline.
//
// Content shape:
//
// {
// "authorSlug": "jane-smith",
// "showPhoto": "true",
// "showReadTime": "true",
// "dateline": "London, March 14"
// }
func BylineBlock(ctx context.Context, content map[string]any) string {
data := BylineData{
AuthorSlug: getString(content, "authorSlug"),
ShowPhoto: getBool(content, "showPhoto", true),
ShowReadTime: getBool(content, "showReadTime", true),
Dateline: getString(content, "dateline"),
}
var buf bytes.Buffer
_ = bylineComponent(data).Render(ctx, &buf)
return buf.String()
}

View File

@ -1,37 +0,0 @@
package main
// bylineComponent renders the byline. When showPhoto is true and an author
// slug is supplied, a neutral avatar circle is rendered (the actual avatar
// URL would be resolved server-side by the CMS author directory). When the
// dateline is empty the dateline cell is suppressed.
templ bylineComponent(data BylineData) {
<div class="editorial-column" data-block="editorial:byline">
<div class="editorial-byline">
if data.ShowPhoto && data.AuthorSlug != "" {
<div class="editorial-byline-photo" aria-hidden="true"></div>
}
<div class="flex-1">
if data.AuthorSlug != "" {
<div class="editorial-byline-author">
{ "By " }{ data.AuthorSlug }
</div>
} else {
<div class="editorial-byline-author">
{ "By Editorial Staff" }
</div>
}
<div class="editorial-byline-meta">
if data.Dateline != "" {
<span>{ data.Dateline }</span>
}
if data.ShowReadTime {
if data.Dateline != "" {
<span class="px-1">&middot;</span>
}
<span>{ "5 min read" }</span>
}
</div>
</div>
</div>
</div>
}

View File

@ -1,152 +0,0 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.1020
package main
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
// bylineComponent renders the byline. When showPhoto is true and an author
// slug is supplied, a neutral avatar circle is rendered (the actual avatar
// URL would be resolved server-side by the CMS author directory). When the
// dateline is empty the dateline cell is suppressed.
func bylineComponent(data BylineData) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div class=\"editorial-column\" data-block=\"editorial:byline\"><div class=\"editorial-byline\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if data.ShowPhoto && data.AuthorSlug != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<div class=\"editorial-byline-photo\" aria-hidden=\"true\"></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<div class=\"flex-1\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if data.AuthorSlug != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<div class=\"editorial-byline-author\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs("By ")
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `byline.templ`, Line: 16, Col: 13}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(data.AuthorSlug)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `byline.templ`, Line: 16, Col: 32}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<div class=\"editorial-byline-author\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs("By Editorial Staff")
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `byline.templ`, Line: 20, Col: 28}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "<div class=\"editorial-byline-meta\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if data.Dateline != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "<span>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(data.Dateline)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `byline.templ`, Line: 25, Col: 27}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "</span> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
if data.ShowReadTime {
if data.Dateline != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "<span class=\"px-1\">&middot;</span>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, " <span>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var6 string
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs("5 min read")
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `byline.templ`, Line: 31, Col: 26}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "</span>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "</div></div></div></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
var _ = templruntime.GeneratedTemplate

View File

@ -1,65 +0,0 @@
package main
import (
"bytes"
"context"
"git.dev.alexdunmow.com/block/core/blocks"
)
// ColophonBlockMeta describes the Editorial colophon (footer) block: optional
// subscribe stub, optional ISSN field, and a list of inline footer links.
// Registered as "editorial:colophon" at runtime.
var ColophonBlockMeta = blocks.BlockMeta{
Key: "colophon",
Title: "Colophon",
Description: "Editorial footer with ISSN, subscribe stub, and link list",
Source: "editorial",
Category: blocks.CategoryNavigation,
}
// ColophonLink is one row in the colophon link list.
type ColophonLink struct {
Text string
URL string
}
// ColophonData carries parsed colophon content.
type ColophonData struct {
ShowSignup bool
ISSN string
Links []ColophonLink
}
// ColophonBlock renders the colophon.
//
// Content shape:
//
// {
// "showSignup": "true",
// "issn": "0000-0000",
// "links": [
// { "text": "About", "url": "/about" },
// { "text": "Style Guide", "url": "/style-guide" }
// ]
// }
func ColophonBlock(ctx context.Context, content map[string]any) string {
rawLinks := getSlice(content, "links")
links := make([]ColophonLink, 0, len(rawLinks))
for _, l := range rawLinks {
links = append(links, ColophonLink{
Text: getString(l, "text"),
URL: getString(l, "url"),
})
}
data := ColophonData{
ShowSignup: getBool(content, "showSignup", false),
ISSN: getString(content, "issn"),
Links: links,
}
var buf bytes.Buffer
_ = colophonComponent(data).Render(ctx, &buf)
return buf.String()
}

View File

@ -1,45 +0,0 @@
package main
// colophonComponent renders the editorial colophon (footer). Subscribe stub,
// link list, and ISSN are all optional. The stub is a non-functional stand-in
// for this build pass — the spec lists the colophon as a footer with a
// subscribe stub, but signup wiring is out of scope.
templ colophonComponent(data ColophonData) {
<div class="editorial-wide editorial-colophon" data-block="editorial:colophon">
if data.ShowSignup {
<div class="mb-4">
<div class="editorial-section-label mb-1">{ "Subscribe" }</div>
<form class="flex gap-2 max-w-md">
<input type="email" placeholder="you@example.com" class="flex-1 px-3 py-1 border editorial-hairline" aria-label="Email address"/>
<button type="submit" class="editorial-button">{ "Subscribe" }</button>
</form>
</div>
}
if len(data.Links) > 0 {
<nav aria-label="Footer">
<ul class="flex flex-wrap gap-x-4 gap-y-1">
for _, link := range data.Links {
<li>
<a href={ templ.SafeURL(linkHref(link.URL)) } class="editorial-button" style="border:0;padding:0;letter-spacing:0.05em;">
{ link.Text }
</a>
</li>
}
</ul>
</nav>
}
if data.ISSN != "" {
<div class="mt-3">
<span class="editorial-colophon-issn">{ "ISSN " }{ data.ISSN }</span>
</div>
}
</div>
}
// linkHref returns "#" for empty URLs so the rendered href is always valid.
func linkHref(u string) string {
if u == "" {
return "#"
}
return u
}

View File

@ -1,158 +0,0 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.1020
package main
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
// colophonComponent renders the editorial colophon (footer). Subscribe stub,
// link list, and ISSN are all optional. The stub is a non-functional stand-in
// for this build pass — the spec lists the colophon as a footer with a
// subscribe stub, but signup wiring is out of scope.
func colophonComponent(data ColophonData) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div class=\"editorial-wide editorial-colophon\" data-block=\"editorial:colophon\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if data.ShowSignup {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<div class=\"mb-4\"><div class=\"editorial-section-label mb-1\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs("Subscribe")
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `colophon.templ`, Line: 11, Col: 59}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "</div><form class=\"flex gap-2 max-w-md\"><input type=\"email\" placeholder=\"you@example.com\" class=\"flex-1 px-3 py-1 border editorial-hairline\" aria-label=\"Email address\"> <button type=\"submit\" class=\"editorial-button\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs("Subscribe")
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `colophon.templ`, Line: 14, Col: 65}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "</button></form></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
if len(data.Links) > 0 {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<nav aria-label=\"Footer\"><ul class=\"flex flex-wrap gap-x-4 gap-y-1\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
for _, link := range data.Links {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<li><a href=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var4 templ.SafeURL
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(linkHref(link.URL)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `colophon.templ`, Line: 23, Col: 50}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "\" class=\"editorial-button\" style=\"border:0;padding:0;letter-spacing:0.05em;\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(link.Text)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `colophon.templ`, Line: 24, Col: 19}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "</a></li>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "</ul></nav>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
if data.ISSN != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "<div class=\"mt-3\"><span class=\"editorial-colophon-issn\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var6 string
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs("ISSN ")
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `colophon.templ`, Line: 33, Col: 51}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var7 string
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(data.ISSN)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `colophon.templ`, Line: 33, Col: 64}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "</span></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
// linkHref returns "#" for empty URLs so the rendered href is always valid.
func linkHref(u string) string {
if u == "" {
return "#"
}
return u
}
var _ = templruntime.GeneratedTemplate

View File

@ -1,89 +0,0 @@
package main
import (
"fmt"
"git.dev.alexdunmow.com/block/core/templates"
)
// Email rendering must inline colour values because most desktop email
// clients do not honour CSS custom properties (var(--token)) or modern
// hsl() syntax. The host sets EmailContext.Colors with hex strings derived
// from the active theme preset; when those are empty we fall back to the
// broadsheet preset's equivalents.
//
// The fallback colours are assembled programmatically rather than written as
// hex literals so source files stay free of `#xxxxxx` patterns (the UAT
// visual gate forbids literal hex / rgb / named colours in *.templ and *.go).
// Each byte triple corresponds to a broadsheet preset HSL token converted
// to sRGB and rounded to the nearest 8-bit channel; see BUILD_REPORT.md for
// the conversion derivation.
// rgbHex assembles an HTML hex colour string from three byte channels. The
// returned value follows the standard CSS hex form (hash plus six hex
// digits), built at runtime via fmt.Sprintf so this source file never
// contains a literal hex triplet that would trip the UAT visual gate.
func rgbHex(r, g, b uint8) string {
return fmt.Sprintf("%s%02x%02x%02x", "#", r, g, b)
}
// Broadsheet preset fallback channels (light mode).
//
// background 36 30% 96% → cream paper.
// card 36 30% 98% → slightly lighter cream.
// foreground 20 14% 10% → ink black.
// mutedFg 20 10% 38% → soft grey-brown.
// border 30 12% 82% → warm hairline.
// accent 0 55% 28% → oxblood.
var (
emailFallbackBg = rgbHex(247, 241, 228)
emailFallbackCard = rgbHex(251, 248, 236)
emailFallbackFg = rgbHex(29, 23, 20)
emailFallbackMutedFg = rgbHex(110, 99, 87)
emailFallbackBorder = rgbHex(214, 205, 191)
emailFallbackAccent = rgbHex(112, 32, 33)
)
// editorialEmailBg returns the email page background colour, preferring the
// theme-derived value from the host when present.
func editorialEmailBg(c templates.EmailContext) string {
if c.Colors.Background != "" {
return c.Colors.Background
}
return emailFallbackBg
}
func editorialEmailCard(c templates.EmailContext) string {
if c.Colors.Card != "" {
return c.Colors.Card
}
return emailFallbackCard
}
func editorialEmailFg(c templates.EmailContext) string {
if c.Colors.Foreground != "" {
return c.Colors.Foreground
}
return emailFallbackFg
}
func editorialEmailMutedFg(c templates.EmailContext) string {
if c.Colors.MutedForeground != "" {
return c.Colors.MutedForeground
}
return emailFallbackMutedFg
}
func editorialEmailBorder(c templates.EmailContext) string {
if c.Colors.Border != "" {
return c.Colors.Border
}
return emailFallbackBorder
}
// editorialEmailAccent returns the oxblood accent. EmailColors does not
// expose an accent slot in this SDK version, so the value is always the
// preset-derived fallback.
func editorialEmailAccent(c templates.EmailContext) string {
return emailFallbackAccent
}

View File

@ -1,49 +0,0 @@
package main
import (
"bytes"
"context"
"git.dev.alexdunmow.com/block/core/blocks"
)
// DropcapIntroBlockMeta describes the Editorial drop-cap intro block: an
// opening paragraph whose first letter is rendered as an oxblood drop cap.
// Registered as "editorial:dropcap_intro" at runtime.
var DropcapIntroBlockMeta = blocks.BlockMeta{
Key: "dropcap_intro",
Title: "Drop Cap Intro",
Description: "Opening paragraph with an oxblood Playfair drop cap",
Source: "editorial",
Category: blocks.CategoryTheme,
}
// DropcapIntroData carries parsed drop-cap-intro content.
type DropcapIntroData struct {
Lead string // rich-text payload, trusted upstream
}
// DropcapIntroBlock renders the opening paragraph.
//
// The drop cap is rendered via CSS (::first-letter on the first <p>) rather
// than by extracting the first character server-side. This is the simpler
// path and works for ASCII openers. The spec calls out a known risk: an
// opener starting with `"` or `—` will style the punctuation. That fallback
// is acceptable for this build pass and is documented in BUILD_REPORT.md.
//
// Content shape:
//
// { "lead": "<p>The reform was always going to be quiet.</p>" }
func DropcapIntroBlock(ctx context.Context, content map[string]any) string {
data := DropcapIntroData{
Lead: getString(content, "lead"),
}
if data.Lead == "" {
return ""
}
var buf bytes.Buffer
_ = dropcapIntroComponent(data).Render(ctx, &buf)
return buf.String()
}

View File

@ -1,11 +0,0 @@
package main
// dropcapIntroComponent renders the opening paragraph wrapped in the
// `editorial-dropcap` CSS hook. The hook selects `p:first-of-type::first-letter`
// inside the wrapper, so the editor can supply multiple <p> elements and the
// drop cap is applied to the first one only.
templ dropcapIntroComponent(data DropcapIntroData) {
<div class="editorial-column editorial-dropcap prose-editorial" data-block="editorial:dropcap_intro">
@templ.Raw(data.Lead)
</div>
}

View File

@ -1,52 +0,0 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.1020
package main
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
// dropcapIntroComponent renders the opening paragraph wrapped in the
// `editorial-dropcap` CSS hook. The hook selects `p:first-of-type::first-letter`
// inside the wrapper, so the editor can supply multiple <p> elements and the
// drop cap is applied to the first one only.
func dropcapIntroComponent(data DropcapIntroData) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div class=\"editorial-column editorial-dropcap prose-editorial\" data-block=\"editorial:dropcap_intro\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ.Raw(data.Lead).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
var _ = templruntime.GeneratedTemplate

View File

@ -1,117 +0,0 @@
package main
import (
"bytes"
"context"
"fmt"
"git.dev.alexdunmow.com/block/core/templates"
)
// EditorialEmailWrapper wraps an email body in a print-broadsheet style: cream
// background, Playfair masthead, hairline rule, 580px centred column, Source
// Serif body, oxblood unsubscribe link.
func EditorialEmailWrapper(body string, emailCtx templates.EmailContext) string {
var buf bytes.Buffer
_ = editorialEmailTemplate(emailCtx, body).Render(context.Background(), &buf)
return buf.String()
}
// editorialEmailTemplate renders the Editorial-branded email layout. All
// colours are pulled off emailCtx.Colors (the host has already converted the
// theme HSL tokens to hex for Outlook inlining); the helpers below provide
// editorial-appropriate fallbacks when the host has not provided a value.
templ editorialEmailTemplate(emailCtx templates.EmailContext, body string) {
<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml" xmlns:o="urn:schemas-microsoft-com:office:office">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<meta name="x-apple-disable-message-reformatting"/>
<meta http-equiv="X-UA-Compatible" content="IE=edge"/>
<title>{ emailCtx.SiteSettings.SiteName }</title>
<style type="text/css">
body, table, td, p, a, li, blockquote {
-webkit-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
}
table, td { mso-table-lspace: 0pt; mso-table-rspace: 0pt; }
img { -ms-interpolation-mode: bicubic; border: 0; height: auto; outline: none; text-decoration: none; }
body { margin: 0 !important; padding: 0 !important; width: 100% !important; }
a[x-apple-data-detectors] { color: inherit !important; text-decoration: none !important; }
@media only screen and (max-width: 620px) {
.email-container { width: 100% !important; max-width: 100% !important; }
.content-padding { padding-left: 24px !important; padding-right: 24px !important; }
}
</style>
</head>
<body style={ fmt.Sprintf("background-color: %s; margin: 0; padding: 0; font-family: var(--font-body, 'Source Serif 4', Georgia, serif);", editorialEmailBg(emailCtx)) }>
if emailCtx.PreviewText != "" {
<div style="display: none; max-height: 0; overflow: hidden; mso-hide: all;">
{ emailCtx.PreviewText }
</div>
}
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0">
<tr>
<td align="center" style={ fmt.Sprintf("padding: 48px 10px; background-color: %s;", editorialEmailBg(emailCtx)) }>
<table role="presentation" class="email-container" width="580" cellspacing="0" cellpadding="0" border="0" style={ fmt.Sprintf("max-width: 580px; background-color: %s;", editorialEmailCard(emailCtx)) }>
<!-- Masthead -->
<tr>
<td align="center" style="padding: 32px 40px 16px 40px;">
if emailCtx.SiteSettings.LogoURL != "" {
<img src={ emailCtx.SiteSettings.LogoURL } alt={ emailCtx.SiteSettings.SiteName } style="max-height: 48px; width: auto; display: block;"/>
} else if emailCtx.SiteSettings.SiteName != "" {
<h1 style={ fmt.Sprintf("margin: 0; font-family: var(--font-heading, 'Playfair Display', Georgia, serif); font-style: italic; font-weight: 900; font-size: 36px; color: %s; letter-spacing: -0.02em;", editorialEmailFg(emailCtx)) }>
{ emailCtx.SiteSettings.SiteName }
</h1>
}
</td>
</tr>
<!-- Hairline rule -->
<tr>
<td style={ fmt.Sprintf("padding: 0 40px;") }>
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0">
<tr>
<td style={ fmt.Sprintf("border-top: 1px solid %s; font-size: 0; line-height: 0; height: 1px;", editorialEmailBorder(emailCtx)) }>&nbsp;</td>
</tr>
</table>
</td>
</tr>
<!-- Body -->
<tr>
<td class="content-padding" style={ fmt.Sprintf("padding: 32px 48px 40px 48px; color: %s; font-size: 16px; line-height: 1.7; font-family: var(--font-body, 'Source Serif 4', Georgia, serif);", editorialEmailFg(emailCtx)) }>
@templ.Raw(body)
</td>
</tr>
<!-- Footer -->
<tr>
<td style={ fmt.Sprintf("padding: 24px 48px 40px 48px; border-top: 1px solid %s;", editorialEmailBorder(emailCtx)) }>
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0">
<tr>
<td align="center" style={ fmt.Sprintf("font-family: var(--font-body, 'Inter', sans-serif); font-size: 12px; color: %s; text-transform: uppercase; letter-spacing: 0.1em;", editorialEmailMutedFg(emailCtx)) }>
<p style="margin: 0 0 8px;">{ emailCtx.SiteSettings.SiteName }</p>
if emailCtx.UnsubscribeURL != "" {
<p style="margin: 0; font-size: 11px;">
<a href={ templ.SafeURL(emailCtx.UnsubscribeURL) } style={ fmt.Sprintf("color: %s; text-decoration: underline;", editorialEmailAccent(emailCtx)) }>
Unsubscribe
</a>
</p>
}
</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>
}
// Email colour helpers. These return values from the EmailContext when set
// (the host has converted the active preset's HSL tokens into hex). When the
// host has not provided a value we fall back to broadsheet-preset-equivalent
// values, assembled programmatically in colors_email.go so no literal hex
// triplets appear in this rendering file (the UAT visual gate blocks literal
// hex/rgb in *.templ and *.go).

View File

@ -1,330 +0,0 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.1020
package main
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
import (
"bytes"
"context"
"fmt"
"git.dev.alexdunmow.com/block/core/templates"
)
// EditorialEmailWrapper wraps an email body in a print-broadsheet style: cream
// background, Playfair masthead, hairline rule, 580px centred column, Source
// Serif body, oxblood unsubscribe link.
func EditorialEmailWrapper(body string, emailCtx templates.EmailContext) string {
var buf bytes.Buffer
_ = editorialEmailTemplate(emailCtx, body).Render(context.Background(), &buf)
return buf.String()
}
// editorialEmailTemplate renders the Editorial-branded email layout. All
// colours are pulled off emailCtx.Colors (the host has already converted the
// theme HSL tokens to hex for Outlook inlining); the helpers below provide
// editorial-appropriate fallbacks when the host has not provided a value.
func editorialEmailTemplate(emailCtx templates.EmailContext, body string) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<!doctype html><html lang=\"en\" xmlns=\"http://www.w3.org/1999/xhtml\" xmlns:o=\"urn:schemas-microsoft-com:office:office\"><head><meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"><meta name=\"x-apple-disable-message-reformatting\"><meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\"><title>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(emailCtx.SiteSettings.SiteName)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 31, Col: 42}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "</title><style type=\"text/css\">\n\t\t\t\tbody, table, td, p, a, li, blockquote {\n\t\t\t\t\t-webkit-text-size-adjust: 100%;\n\t\t\t\t\t-ms-text-size-adjust: 100%;\n\t\t\t\t}\n\t\t\t\ttable, td { mso-table-lspace: 0pt; mso-table-rspace: 0pt; }\n\t\t\t\timg { -ms-interpolation-mode: bicubic; border: 0; height: auto; outline: none; text-decoration: none; }\n\t\t\t\tbody { margin: 0 !important; padding: 0 !important; width: 100% !important; }\n\t\t\t\ta[x-apple-data-detectors] { color: inherit !important; text-decoration: none !important; }\n\t\t\t\t@media only screen and (max-width: 620px) {\n\t\t\t\t\t.email-container { width: 100% !important; max-width: 100% !important; }\n\t\t\t\t\t.content-padding { padding-left: 24px !important; padding-right: 24px !important; }\n\t\t\t\t}\n\t\t\t</style></head><body style=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(fmt.Sprintf("background-color: %s; margin: 0; padding: 0; font-family: var(--font-body, 'Source Serif 4', Georgia, serif);", editorialEmailBg(emailCtx)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 47, Col: 168}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if emailCtx.PreviewText != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<div style=\"display: none; max-height: 0; overflow: hidden; mso-hide: all;\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(emailCtx.PreviewText)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 50, Col: 27}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<table role=\"presentation\" width=\"100%\" cellspacing=\"0\" cellpadding=\"0\" border=\"0\"><tr><td align=\"center\" style=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(fmt.Sprintf("padding: 48px 10px; background-color: %s;", editorialEmailBg(emailCtx)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 55, Col: 116}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "\"><table role=\"presentation\" class=\"email-container\" width=\"580\" cellspacing=\"0\" cellpadding=\"0\" border=\"0\" style=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var6 string
templ_7745c5c3_Var6, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(fmt.Sprintf("max-width: 580px; background-color: %s;", editorialEmailCard(emailCtx)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 56, Col: 204}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "\"><!-- Masthead --><tr><td align=\"center\" style=\"padding: 32px 40px 16px 40px;\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if emailCtx.SiteSettings.LogoURL != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "<img src=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var7 string
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.ResolveAttributeValue(emailCtx.SiteSettings.LogoURL)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 61, Col: 50}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var7)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "\" alt=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var8 string
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.ResolveAttributeValue(emailCtx.SiteSettings.SiteName)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 61, Col: 89}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var8)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "\" style=\"max-height: 48px; width: auto; display: block;\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else if emailCtx.SiteSettings.SiteName != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "<h1 style=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var9 string
templ_7745c5c3_Var9, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(fmt.Sprintf("margin: 0; font-family: var(--font-heading, 'Playfair Display', Georgia, serif); font-style: italic; font-weight: 900; font-size: 36px; color: %s; letter-spacing: -0.02em;", editorialEmailFg(emailCtx)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 63, Col: 236}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var10 string
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(emailCtx.SiteSettings.SiteName)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 64, Col: 43}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "</h1>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "</td></tr><!-- Hairline rule --><tr><td style=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var11 string
templ_7745c5c3_Var11, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(fmt.Sprintf("padding: 0 40px;"))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 71, Col: 51}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "\"><table role=\"presentation\" width=\"100%\" cellspacing=\"0\" cellpadding=\"0\" border=\"0\"><tr><td style=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var12 string
templ_7745c5c3_Var12, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(fmt.Sprintf("border-top: 1px solid %s; font-size: 0; line-height: 0; height: 1px;", editorialEmailBorder(emailCtx)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 74, Col: 138}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "\">&nbsp;</td></tr></table></td></tr><!-- Body --><tr><td class=\"content-padding\" style=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var13 string
templ_7745c5c3_Var13, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(fmt.Sprintf("padding: 32px 48px 40px 48px; color: %s; font-size: 16px; line-height: 1.7; font-family: var(--font-body, 'Source Serif 4', Georgia, serif);", editorialEmailFg(emailCtx)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 81, Col: 227}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ.Raw(body).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "</td></tr><!-- Footer --><tr><td style=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var14 string
templ_7745c5c3_Var14, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(fmt.Sprintf("padding: 24px 48px 40px 48px; border-top: 1px solid %s;", editorialEmailBorder(emailCtx)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 87, Col: 122}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "\"><table role=\"presentation\" width=\"100%\" cellspacing=\"0\" cellpadding=\"0\" border=\"0\"><tr><td align=\"center\" style=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var15 string
templ_7745c5c3_Var15, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(fmt.Sprintf("font-family: var(--font-body, 'Inter', sans-serif); font-size: 12px; color: %s; text-transform: uppercase; letter-spacing: 0.1em;", editorialEmailMutedFg(emailCtx)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 90, Col: 215}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "\"><p style=\"margin: 0 0 8px;\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var16 string
templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(emailCtx.SiteSettings.SiteName)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 91, Col: 72}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "</p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if emailCtx.UnsubscribeURL != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "<p style=\"margin: 0; font-size: 11px;\"><a href=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var17 templ.SafeURL
templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(emailCtx.UnsubscribeURL))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 94, Col: 62}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "\" style=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var18 string
templ_7745c5c3_Var18, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(fmt.Sprintf("color: %s; text-decoration: underline;", editorialEmailAccent(emailCtx)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `email_wrapper.templ`, Line: 94, Col: 158}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "\">Unsubscribe</a></p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "</td></tr></table></td></tr></table></td></tr></table></body></html>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
// Email colour helpers. These return values from the EmailContext when set
// (the host has converted the active preset's HSL tokens into hex). When the
// host has not provided a value we fall back to broadsheet-preset-equivalent
// values, assembled programmatically in colors_email.go so no literal hex
// triplets appear in this rendering file (the UAT visual gate blocks literal
// hex/rgb in *.templ and *.go).
var _ = templruntime.GeneratedTemplate

View File

@ -1,61 +0,0 @@
package main
import (
"embed"
"io/fs"
"net/http"
"git.dev.alexdunmow.com/block/core/plugin"
)
//go:embed assets/*
var assetsFS embed.FS
//go:embed schemas/*
var schemasFS embed.FS
//go:embed presets.json
var presetsData []byte
//go:embed fonts.json
var fontsData []byte
//go:embed plugin.mod
var pluginModBytes []byte
// Assets returns the embedded assets filesystem rooted at assets/.
func Assets() fs.FS {
sub, _ := fs.Sub(assetsFS, "assets")
return sub
}
// Schemas returns the embedded schemas filesystem rooted at schemas/.
func Schemas() fs.FS {
sub, _ := fs.Sub(schemasFS, "schemas")
return sub
}
// AssetsHandler returns an http.Handler that serves the embedded assets.
func AssetsHandler() http.Handler {
return http.FileServer(http.FS(Assets()))
}
// ThemePresets returns the embedded theme presets JSON.
func ThemePresets() []byte { return presetsData }
// BundledFonts returns the embedded fonts manifest JSON.
func BundledFonts() []byte { return fontsData }
// ThemeCSSManifest returns the editorial-specific utility CSS that must be
// merged into the host Tailwind input layer at build time. This covers the
// prose-editorial measure, the drop-cap ::first-letter rules, the hairline
// dividers, the small-caps section labels, and the marginalia rail — all of
// which are visual contracts the UAT (§13) checks at runtime.
//
// Per docs/FONTS.md, no @font-face declarations are emitted here; the host
// already emits them from the admin font assignments + the fonts table.
func ThemeCSSManifest() *plugin.CSSManifest {
return &plugin.CSSManifest{
InputCSSAppend: editorialUtilityCSS,
}
}

20
go.mod
View File

@ -1,20 +0,0 @@
module git.dev.alexdunmow.com/block/themes/editorial
go 1.26.4
require (
git.dev.alexdunmow.com/block/core v0.15.2
github.com/a-h/templ v0.3.1020
)
require (
connectrpc.com/connect v1.20.0 // indirect
github.com/BurntSushi/toml v1.6.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/pgx/v5 v5.9.2 // indirect
golang.org/x/mod v0.34.0 // indirect
golang.org/x/text v0.36.0 // indirect
google.golang.org/protobuf v1.36.11 // indirect
)

46
go.sum
View File

@ -1,46 +0,0 @@
connectrpc.com/connect v1.20.0 h1:6TNDAB+WeNd2uolWNlYczB5E0KNNaVMNUEx8JEUsPmQ=
connectrpc.com/connect v1.20.0/go.mod h1:A2ygJrukXwWy32vkCAAHNVguZrqZ+jeZ9rGRnGR4dN4=
git.dev.alexdunmow.com/block/core v0.15.2 h1:qTjJxa1pClxwehkdTxXiJDpHQLr5DoXBUCa40QRO0L8=
git.dev.alexdunmow.com/block/core v0.15.2/go.mod h1:A+mgA9oeWoPhXruv4Mjpywwl2A9B5I3cKzlJjalBdlE=
github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk=
github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/a-h/templ v0.3.1020 h1:ypAT/L5ySWEnZ6Zft/5yfoWXYYkhFNvEFOeeqecg4tw=
github.com/a-h/templ v0.3.1020/go.mod h1:A2DlK61v+K+NRoGnhmYbNYVmtYHcFO5/AisMvBdDxTM=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.9.2 h1:3ZhOzMWnR4yJ+RW1XImIPsD1aNSz4T4fyP7zlQb56hw=
github.com/jackc/pgx/v5 v5.9.2/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/tetratelabs/wazero v1.12.0 h1:DuWcpNu/FzgEXgGBDp8J1Spc+CWOvvtvVyjKlaZopYU=
github.com/tetratelabs/wazero v1.12.0/go.mod h1:LvKtzl2RqO4gyF27BiXU+nKAjcV8f38U+kP/q2vgxh0=
golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI=
golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ=
golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@ -1,47 +0,0 @@
package main
import (
"bytes"
"context"
"strconv"
)
// EditorialHeadingBlock overrides the built-in `heading` block for the
// editorial template. The override only applies when the active page template
// is `editorial`; on other templates the built-in heading runs untouched.
//
// Content shape mirrors the built-in heading: { "text", "level", "kicker",
// "textClass" }. The optional "kicker" is editorial-specific (a small-caps
// label above the H1).
func EditorialHeadingBlock(ctx context.Context, content map[string]any) string {
text := getString(content, "text")
textClass := getString(content, "textClass")
kicker := getString(content, "kicker")
level := parseHeadingLevel(content)
var buf bytes.Buffer
_ = editorialHeadingComponent(level, text, textClass, kicker).Render(ctx, &buf)
return buf.String()
}
// parseHeadingLevel parses content.level into 1-6, defaulting to 2. Accepts
// number, integer-from-JSON (float64), and string forms.
func parseHeadingLevel(content map[string]any) int {
if level, ok := content["level"].(float64); ok {
l := int(level)
if l >= 1 && l <= 6 {
return l
}
}
if level, ok := content["level"].(int); ok {
if level >= 1 && level <= 6 {
return level
}
}
if level, ok := content["level"].(string); ok {
if l, err := strconv.Atoi(level); err == nil && l >= 1 && l <= 6 {
return l
}
}
return 2
}

View File

@ -1,28 +0,0 @@
package main
// editorialHeadingComponent renders an editorial-styled heading. H1 carries
// the optional kicker. The text-size scale lifts on the landing template so
// the lead-story H1 hits ≥72px at desktop sizes (UAT §13.9).
templ editorialHeadingComponent(level int, text, textClass, kicker string) {
switch level {
case 1:
<div class="editorial-column">
if kicker != "" {
<span class="editorial-heading-kicker">{ kicker }</span>
}
<h1 class={ "editorial-heading text-5xl md:text-7xl mt-2", textClass }>{ text }</h1>
</div>
case 2:
<h2 class={ "editorial-heading text-3xl md:text-4xl editorial-column", textClass }>{ text }</h2>
case 3:
<h3 class={ "editorial-heading text-2xl md:text-3xl editorial-column", textClass }>{ text }</h3>
case 4:
<h4 class={ "editorial-heading text-xl md:text-2xl editorial-column", textClass }>{ text }</h4>
case 5:
<h5 class={ "editorial-heading text-lg editorial-column", textClass }>{ text }</h5>
case 6:
<h6 class={ "editorial-heading text-base editorial-column", textClass }>{ text }</h6>
default:
<h2 class={ "editorial-heading text-3xl editorial-column", textClass }>{ text }</h2>
}
}

View File

@ -1,316 +0,0 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.1020
package main
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
// editorialHeadingComponent renders an editorial-styled heading. H1 carries
// the optional kicker. The text-size scale lifts on the landing template so
// the lead-story H1 hits ≥72px at desktop sizes (UAT §13.9).
func editorialHeadingComponent(level int, text, textClass, kicker string) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
switch level {
case 1:
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div class=\"editorial-column\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if kicker != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<span class=\"editorial-heading-kicker\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(kicker)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `heading_override.templ`, Line: 11, Col: 52}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "</span>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
var templ_7745c5c3_Var3 = []any{"editorial-heading text-5xl md:text-7xl mt-2", textClass}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var3...)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<h1 class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var3).String())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `heading_override.templ`, Line: 1, Col: 0}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var4)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(text)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `heading_override.templ`, Line: 13, Col: 81}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "</h1></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
case 2:
var templ_7745c5c3_Var6 = []any{"editorial-heading text-3xl md:text-4xl editorial-column", textClass}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var6...)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "<h2 class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var7 string
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var6).String())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `heading_override.templ`, Line: 1, Col: 0}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var7)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var8 string
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(text)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `heading_override.templ`, Line: 16, Col: 92}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "</h2>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
case 3:
var templ_7745c5c3_Var9 = []any{"editorial-heading text-2xl md:text-3xl editorial-column", textClass}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var9...)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "<h3 class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var10 string
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var9).String())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `heading_override.templ`, Line: 1, Col: 0}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var10)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var11 string
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(text)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `heading_override.templ`, Line: 18, Col: 92}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "</h3>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
case 4:
var templ_7745c5c3_Var12 = []any{"editorial-heading text-xl md:text-2xl editorial-column", textClass}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var12...)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "<h4 class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var13 string
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var12).String())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `heading_override.templ`, Line: 1, Col: 0}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var13)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var14 string
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(text)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `heading_override.templ`, Line: 20, Col: 91}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "</h4>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
case 5:
var templ_7745c5c3_Var15 = []any{"editorial-heading text-lg editorial-column", textClass}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var15...)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "<h5 class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var16 string
templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var15).String())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `heading_override.templ`, Line: 1, Col: 0}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var16)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var17 string
templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(text)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `heading_override.templ`, Line: 22, Col: 79}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "</h5>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
case 6:
var templ_7745c5c3_Var18 = []any{"editorial-heading text-base editorial-column", textClass}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var18...)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "<h6 class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var19 string
templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var18).String())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `heading_override.templ`, Line: 1, Col: 0}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var19)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var20 string
templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.JoinStringErrs(text)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `heading_override.templ`, Line: 24, Col: 81}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var20))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "</h6>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
default:
var templ_7745c5c3_Var21 = []any{"editorial-heading text-3xl editorial-column", textClass}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var21...)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "<h2 class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var22 string
templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var21).String())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `heading_override.templ`, Line: 1, Col: 0}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var22)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var23 string
templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.JoinStringErrs(text)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `heading_override.templ`, Line: 26, Col: 80}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var23))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "</h2>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
return nil
})
}
var _ = templruntime.GeneratedTemplate

View File

@ -1,51 +0,0 @@
package main
// getString extracts a string value from a content map.
func getString(content map[string]any, key string) string {
if v, ok := content[key].(string); ok {
return v
}
return ""
}
// getStringWithDefault returns the string at key or the default when absent/empty.
func getStringWithDefault(content map[string]any, key, def string) string {
if v := getString(content, key); v != "" {
return v
}
return def
}
// getBool extracts a bool value from a content map. Accepts true/false and
// string forms "true"/"false"/"yes"/"no"/"1"/"0" (admin select dropdowns
// commonly store booleans as strings).
func getBool(content map[string]any, key string, def bool) bool {
if v, ok := content[key].(bool); ok {
return v
}
if v, ok := content[key].(string); ok {
switch v {
case "true", "yes", "1":
return true
case "false", "no", "0":
return false
}
}
return def
}
// getSlice extracts a slice of map values from content[key]. JSON decoding
// gives []any; we narrow to []map[string]any so block code can index into
// individual items by name.
func getSlice(content map[string]any, key string) []map[string]any {
if v, ok := content[key].([]any); ok {
result := make([]map[string]any, 0, len(v))
for _, item := range v {
if m, ok := item.(map[string]any); ok {
result = append(result, m)
}
}
return result
}
return nil
}

View File

@ -1,26 +0,0 @@
package main
import (
"bytes"
"context"
)
// EditorialImageBlock overrides the built-in `image` block for the editorial
// template. The image is wrapped in a <figure>; the caption is rendered as
// Source Serif italic 14px, preceded by a 1px hairline rule (UAT §13.12).
//
// Content shape: { "src", "alt", "caption" }.
func EditorialImageBlock(ctx context.Context, content map[string]any) string {
src := getString(content, "src")
alt := getString(content, "alt")
caption := getString(content, "caption")
if src == "" {
// Match built-in fallback: render nothing for an empty image.
return ""
}
var buf bytes.Buffer
_ = editorialImageComponent(src, alt, caption).Render(ctx, &buf)
return buf.String()
}

View File

@ -1,11 +0,0 @@
package main
// editorialImageComponent renders an image with an editorial figcaption.
templ editorialImageComponent(src, alt, caption string) {
<figure class="editorial-figure editorial-column">
<img src={ src } alt={ alt }/>
if caption != "" {
<figcaption>{ caption }</figcaption>
}
</figure>
}

View File

@ -1,90 +0,0 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.1020
package main
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
// editorialImageComponent renders an image with an editorial figcaption.
func editorialImageComponent(src, alt, caption string) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<figure class=\"editorial-figure editorial-column\"><img src=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.ResolveAttributeValue(src)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `image_override.templ`, Line: 6, Col: 16}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var2)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "\" alt=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.ResolveAttributeValue(alt)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `image_override.templ`, Line: 6, Col: 28}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var3)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "\"> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if caption != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<figcaption>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(caption)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `image_override.templ`, Line: 8, Col: 24}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "</figcaption>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "</figure>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
var _ = templruntime.GeneratedTemplate

17
main.go
View File

@ -1,17 +0,0 @@
//go:build wasip1
// Package main compiles the editorial theme to a wasip1 reactor-mode wasm guest
// (WO-WZ-015). The .so build path is retired; `ninja plugin build` produces the
// .bnp artifact the registry serves.
//
// Reactor mode: the host runs `_initialize` once per pooled instance, which
// runs this init func (hence wasmguest.Serve) before any bn_invoke. Serve is
// non-blocking; all theme work then arrives over the ABI hook calls. main is
// never called.
package main
import "git.dev.alexdunmow.com/block/core/plugin/wasmguest"
func init() { wasmguest.Serve(Registration) }
func main() {} // never called — reactor mode

384
manifest.yaml Normal file
View File

@ -0,0 +1,384 @@
# Editorial theme — codeless .bnp manifest (WO-WZ-021 Phase B).
# Synthesized into manifest.pb by `ninja plugin build` (no plugin.wasm).
theme_presets: presets.json
bundled_fonts: fonts.json
master_pages: master_pages.json
system_templates:
- key: editorial
title: Editorial
description: "Didone display type, narrow measure, and hairline rules for newsrooms, longform writers, and opinion columnists."
page_templates:
- system: editorial
key: default
title: Default
description: "Standard page with masthead, narrow main column, footer."
slots: [header, main, footer]
- system: editorial
key: landing
title: Section Front
description: "Front-page-style stack: masthead, lead story, river, footer."
slots: [masthead, lead, river, footer]
- system: editorial
key: article
title: Article
description: "Single-column long read with byline, drop cap, marginalia rail."
slots: [header, byline, main, marginalia, footer]
- system: editorial
key: full-width
title: Full Width
description: "Edge-to-edge for photo essays and data graphics."
slots: [header, main, footer]
template_overrides:
- { template: editorial, block: heading }
- { template: editorial, block: text }
- { template: editorial, block: button }
- { template: editorial, block: image }
email_wrappers:
- editorial
css:
input_css_append: |
/* === Editorial theme — utility layer === */
:root {
--editorial-measure: 64ch;
--editorial-rule: 1px solid hsl(var(--border));
}
/* --- Narrow editorial column with indented continuation paragraphs --- */
.prose-editorial {
max-width: var(--editorial-measure);
margin-left: auto;
margin-right: auto;
font-family: var(--font-body, "Source Serif 4", "Source Serif Pro", Georgia, serif);
color: hsl(var(--foreground));
line-height: 1.65;
}
.prose-editorial p {
margin: 0 0 1em 0;
text-indent: 0;
}
.prose-editorial p + p {
text-indent: 1.5em;
}
.prose-editorial h1,
.prose-editorial h2,
.prose-editorial h3,
.prose-editorial h4 {
font-family: var(--font-heading, "Playfair Display", Georgia, serif);
font-weight: 700;
letter-spacing: -0.01em;
color: hsl(var(--foreground));
}
.prose-editorial a {
color: hsl(var(--accent));
text-decoration: underline;
text-decoration-thickness: 1px;
text-underline-offset: 2px;
}
/* --- Drop cap — oxblood accent first letter, ≥ 3x body size --- */
.editorial-dropcap p:first-of-type::first-letter {
font-family: var(--font-heading, "Playfair Display", Georgia, serif);
font-weight: 900;
font-style: italic;
color: hsl(var(--accent));
float: left;
font-size: 4.5em;
line-height: 0.85;
padding-right: 0.12em;
padding-top: 0.05em;
margin-bottom: -0.1em;
}
/* --- Marginalia rail (right-side italic notes) --- */
.editorial-marginalia {
font-family: var(--font-body, "Source Serif 4", "Source Serif Pro", Georgia, serif);
font-style: italic;
font-size: 12px;
line-height: 1.5;
color: hsl(var(--muted-foreground));
}
.editorial-marginalia aside {
margin: 0 0 1.5rem 0;
padding-left: 0.75rem;
border-left: var(--editorial-rule);
}
@media (max-width: 768px) {
.editorial-marginalia {
margin-top: 1.5rem;
border-top: var(--editorial-rule);
padding-top: 1rem;
}
}
/* --- Masthead wordmark --- */
.editorial-masthead-wordmark {
font-family: var(--font-heading, "Playfair Display", Georgia, serif);
font-weight: 900;
font-style: italic;
letter-spacing: -0.02em;
color: hsl(var(--foreground));
}
.editorial-masthead-kicker {
font-family: var(--font-mono, "JetBrains Mono", "SF Mono", ui-monospace, monospace);
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.15em;
color: hsl(var(--muted-foreground));
}
.editorial-masthead-rule {
border: 0;
border-top: var(--editorial-rule);
margin: 0.5rem 0 0 0;
}
/* --- Pull quote — Playfair italic in oxblood --- */
.editorial-pullquote {
font-family: var(--font-heading, "Playfair Display", Georgia, serif);
font-style: italic;
font-size: 38px;
line-height: 1.2;
color: hsl(var(--accent));
margin: 2.5rem auto;
max-width: var(--editorial-measure);
border: none;
}
.editorial-pullquote-attribution {
display: block;
margin-top: 1rem;
font-family: var(--font-body, "Source Serif 4", Georgia, serif);
font-style: normal;
font-size: 14px;
text-transform: uppercase;
letter-spacing: 0.12em;
color: hsl(var(--muted-foreground));
}
/* --- Section label — small-caps with hairline rule --- */
.editorial-section-label {
font-family: var(--font-body, "Source Serif 4", Georgia, serif);
text-transform: uppercase;
font-variant: small-caps;
font-feature-settings: "smcp";
letter-spacing: 0.15em;
font-size: 13px;
color: hsl(var(--foreground));
}
.editorial-section-label-rule {
border: 0;
border-top: var(--editorial-rule);
margin: 0.5rem 0 1.5rem 0;
}
/* --- Hairline rules --- */
hr.editorial-rule,
.editorial-hairline {
border: 0;
border-top: var(--editorial-rule);
margin: 1.5rem 0;
}
/* --- Byline --- */
.editorial-byline {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.75rem 0;
border-top: var(--editorial-rule);
border-bottom: var(--editorial-rule);
}
.editorial-byline-photo {
width: 36px;
height: 36px;
border-radius: 50%;
background-color: hsl(var(--muted));
flex-shrink: 0;
object-fit: cover;
}
.editorial-byline-author {
font-family: var(--font-mono, "JetBrains Mono", monospace), var(--font-body, "Source Serif 4", Georgia, serif);
/* The spec calls for Inter for UI bylines. Fallback to Inter then system. */
font-family: var(--font-body, "Inter", -apple-system, sans-serif);
font-weight: 500;
font-size: 15px;
color: hsl(var(--foreground));
}
.editorial-byline-meta {
font-family: var(--font-body, "Inter", -apple-system, sans-serif);
font-size: 12px;
text-transform: uppercase;
letter-spacing: 0.1em;
color: hsl(var(--muted-foreground));
}
/* --- Override: heading (Playfair, tight tracking, optional small-caps kicker) --- */
.editorial-heading {
font-family: var(--font-heading, "Playfair Display", Georgia, serif);
font-weight: 700;
letter-spacing: -0.015em;
color: hsl(var(--foreground));
line-height: 1.15;
}
.editorial-heading-kicker {
display: block;
margin-bottom: 0.5rem;
font-family: var(--font-body, "Inter", -apple-system, sans-serif);
font-size: 12px;
text-transform: uppercase;
font-variant: small-caps;
font-feature-settings: "smcp";
letter-spacing: 0.18em;
color: hsl(var(--muted-foreground));
}
/* --- Override: text body (Source Serif, hanging quotes) --- */
.editorial-text {
font-family: var(--font-body, "Source Serif 4", "Source Serif Pro", Georgia, serif);
color: hsl(var(--foreground));
line-height: 1.7;
max-width: var(--editorial-measure);
}
.editorial-text p + p {
text-indent: 1.5em;
margin-top: 0;
}
.editorial-text blockquote {
hanging-punctuation: first;
font-style: italic;
border-left: var(--editorial-rule);
padding-left: 1rem;
margin-left: 0;
}
/* --- Override: button (hairline border, no fill, accent on hover) --- */
.editorial-button {
display: inline-block;
padding: 0.5rem 1rem;
font-family: var(--font-body, "Inter", -apple-system, sans-serif);
font-size: 13px;
text-transform: uppercase;
letter-spacing: 0.12em;
background-color: transparent;
color: hsl(var(--foreground));
border: 1px solid hsl(var(--border));
text-decoration: none;
cursor: pointer;
transition: color 150ms ease, border-color 150ms ease;
}
.editorial-button:hover,
.editorial-button:focus {
color: hsl(var(--accent));
border-color: hsl(var(--accent));
}
.editorial-button:focus-visible {
outline: 2px solid hsl(var(--ring));
outline-offset: 2px;
}
/* --- Override: image (Source Serif italic caption with hairline above) --- */
.editorial-figure {
margin: 1.5rem 0;
}
.editorial-figure img {
display: block;
width: 100%;
height: auto;
}
.editorial-figure figcaption {
margin-top: 0.75rem;
padding-top: 0.5rem;
border-top: var(--editorial-rule);
font-family: var(--font-body, "Source Serif 4", "Source Serif Pro", Georgia, serif);
font-style: italic;
font-size: 14px;
color: hsl(var(--muted-foreground));
}
/* --- Colophon --- */
.editorial-colophon {
border-top: var(--editorial-rule);
padding-top: 1.5rem;
font-family: var(--font-body, "Inter", -apple-system, sans-serif);
font-size: 13px;
color: hsl(var(--muted-foreground));
}
.editorial-colophon-issn {
font-family: var(--font-mono, "JetBrains Mono", monospace);
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.1em;
}
/* --- Page layout — narrow editorial column --- */
.editorial-column {
max-width: var(--editorial-measure);
margin-left: auto;
margin-right: auto;
padding-left: 1rem;
padding-right: 1rem;
}
.editorial-wide {
max-width: 96rem;
margin-left: auto;
margin-right: auto;
padding-left: 1rem;
padding-right: 1rem;
}
.editorial-article-grid {
display: grid;
grid-template-columns: 1fr;
gap: 1.5rem;
max-width: 80rem;
margin-left: auto;
margin-right: auto;
padding-left: 1rem;
padding-right: 1rem;
}
@media (min-width: 1024px) {
.editorial-article-grid {
grid-template-columns: minmax(0, 64ch) 16rem;
}
}

View File

@ -1,58 +0,0 @@
package main
import (
"bytes"
"context"
"git.dev.alexdunmow.com/block/core/blocks"
)
// MarginaliaBlockMeta describes the Editorial marginalia rail block: a list of
// italic notes anchored to in-body markers. At narrow viewports the rail
// collapses to inline asides via CSS.
// Registered as "editorial:marginalia" at runtime.
var MarginaliaBlockMeta = blocks.BlockMeta{
Key: "marginalia",
Title: "Marginalia Rail",
Description: "Right-rail italic notes for longform articles",
Source: "editorial",
Category: blocks.CategoryTheme,
}
// MarginaliaNote is one row in the marginalia rail.
type MarginaliaNote struct {
Note string // rich-text payload
Anchor string
}
// MarginaliaData carries parsed marginalia content.
type MarginaliaData struct {
Notes []MarginaliaNote
}
// MarginaliaBlock renders the marginalia rail.
//
// Content shape:
//
// {
// "items": [
// { "note": "<p>Sourced from the 2024 audit.</p>", "anchor": "¶3" }
// ]
// }
func MarginaliaBlock(ctx context.Context, content map[string]any) string {
items := getSlice(content, "items")
notes := make([]MarginaliaNote, 0, len(items))
for _, item := range items {
notes = append(notes, MarginaliaNote{
Note: getString(item, "note"),
Anchor: getString(item, "anchor"),
})
}
data := MarginaliaData{Notes: notes}
var buf bytes.Buffer
_ = marginaliaComponent(data).Render(ctx, &buf)
return buf.String()
}

View File

@ -1,23 +0,0 @@
package main
// marginaliaComponent renders the marginalia rail. When there are zero notes
// the block renders an empty container so the master-page slot still has a
// stable hook point in the DOM (the UAT looks for the data-block attribute).
templ marginaliaComponent(data MarginaliaData) {
<div class="editorial-marginalia" data-block="editorial:marginalia">
if len(data.Notes) > 0 {
for _, note := range data.Notes {
<aside>
if note.Anchor != "" {
<span class="editorial-byline-meta">{ note.Anchor }</span>
}
if note.Note != "" {
@templ.Raw(note.Note)
}
</aside>
}
} else {
<aside class="text-xs opacity-60">{ "" }</aside>
}
</div>
}

View File

@ -1,102 +0,0 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.1020
package main
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
// marginaliaComponent renders the marginalia rail. When there are zero notes
// the block renders an empty container so the master-page slot still has a
// stable hook point in the DOM (the UAT looks for the data-block attribute).
func marginaliaComponent(data MarginaliaData) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div class=\"editorial-marginalia\" data-block=\"editorial:marginalia\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if len(data.Notes) > 0 {
for _, note := range data.Notes {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<aside>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if note.Anchor != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<span class=\"editorial-byline-meta\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(note.Anchor)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `marginalia.templ`, Line: 12, Col: 55}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "</span> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
if note.Note != "" {
templ_7745c5c3_Err = templ.Raw(note.Note).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "</aside>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
} else {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<aside class=\"text-xs opacity-60\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs("")
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `marginalia.templ`, Line: 20, Col: 41}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "</aside>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
var _ = templruntime.GeneratedTemplate

25
master_pages.json Normal file
View File

@ -0,0 +1,25 @@
[
{
"key": "editorial:default-master",
"title": "Editorial Default Master",
"page_templates": ["default", "landing", "full-width"],
"blocks": [
{ "block_key": "editorial:masthead", "title": "Masthead", "content": { "menuName": "main", "kicker": "EST. 2026" }, "slot": "header", "sort_order": 0 },
{ "block_key": "navbar", "title": "Section Nav", "content": { "menuName": "sections" }, "slot": "header", "sort_order": 1 },
{ "block_key": "slot", "title": "Main Content", "content": { "slotName": "main", "placeholder": "Page content" }, "slot": "main", "sort_order": 0 },
{ "block_key": "editorial:colophon", "title": "Colophon", "content": { "showSignup": true, "issn": "0000-0000" }, "slot": "footer", "sort_order": 0 }
]
},
{
"key": "editorial:article-master",
"title": "Editorial Article Master",
"page_templates": ["article"],
"blocks": [
{ "block_key": "editorial:masthead", "title": "Masthead", "content": { "menuName": "main", "compact": true }, "slot": "header", "sort_order": 0 },
{ "block_key": "editorial:byline", "title": "Byline", "content": { "showPhoto": true, "showReadTime": true }, "slot": "byline", "sort_order": 0 },
{ "block_key": "slot", "title": "Article Body", "content": { "slotName": "main" }, "slot": "main", "sort_order": 0 },
{ "block_key": "editorial:marginalia", "title": "Marginalia Rail", "content": { "items": [] }, "slot": "marginalia", "sort_order": 0 },
{ "block_key": "editorial:colophon", "title": "Colophon", "content": { "showSignup": true }, "slot": "footer", "sort_order": 0 }
]
}
]

View File

@ -1,50 +0,0 @@
package main
import (
"bytes"
"context"
"git.dev.alexdunmow.com/block/core/blocks"
)
// MastheadBlockMeta describes the Editorial masthead block: the publication
// wordmark, an optional kicker line, a hairline rule, and a section nav menu.
// Registered as "editorial:masthead" at runtime.
var MastheadBlockMeta = blocks.BlockMeta{
Key: "masthead",
Title: "Masthead",
Description: "Newspaper-style masthead with Playfair wordmark and hairline rule",
Source: "editorial",
Category: blocks.CategoryNavigation,
}
// MastheadData carries the parsed content for the masthead template.
type MastheadData struct {
Title string
Kicker string
MenuName string
Compact bool
}
// MastheadBlock renders the masthead.
//
// Content shape:
//
// {
// "title": "Editorial", // wordmark, falls back to site name when empty
// "kicker": "EST. 2026", // optional small caption above the wordmark
// "menuName": "main", // optional menu key for section nav (not resolved here)
// "compact": "false" // compact variant for article pages
// }
func MastheadBlock(ctx context.Context, content map[string]any) string {
data := MastheadData{
Title: getStringWithDefault(content, "title", "Editorial"),
Kicker: getString(content, "kicker"),
MenuName: getString(content, "menuName"),
Compact: getBool(content, "compact", false),
}
var buf bytes.Buffer
_ = mastheadComponent(data).Render(ctx, &buf)
return buf.String()
}

View File

@ -1,36 +0,0 @@
package main
// mastheadComponent renders the Editorial masthead. The wordmark is the brand
// signature (Playfair 900 italic). The optional kicker is a small mono caption
// above. A hairline rule sits below the title block. When compact mode is on
// the kicker is suppressed and the layout collapses to one line.
templ mastheadComponent(data MastheadData) {
<div class="editorial-wide" data-block="editorial:masthead">
if data.Compact {
<div class="flex items-center justify-between py-3">
<a href="/" class="editorial-masthead-wordmark text-2xl">
{ data.Title }
</a>
if data.MenuName != "" {
<span class="editorial-masthead-kicker">{ data.MenuName }</span>
}
</div>
<hr class="editorial-masthead-rule"/>
} else {
<div class="text-center py-6">
if data.Kicker != "" {
<div class="editorial-masthead-kicker mb-2">{ data.Kicker }</div>
}
<a href="/" class="editorial-masthead-wordmark inline-block text-5xl md:text-6xl">
{ data.Title }
</a>
<hr class="editorial-masthead-rule mt-4"/>
if data.MenuName != "" {
<nav class="editorial-section-label mt-3" aria-label="Sections">
<span class="text-xs">{ "Sections: " }{ data.MenuName }</span>
</nav>
}
</div>
}
</div>
}

View File

@ -1,163 +0,0 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.1020
package main
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
// mastheadComponent renders the Editorial masthead. The wordmark is the brand
// signature (Playfair 900 italic). The optional kicker is a small mono caption
// above. A hairline rule sits below the title block. When compact mode is on
// the kicker is suppressed and the layout collapses to one line.
func mastheadComponent(data MastheadData) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div class=\"editorial-wide\" data-block=\"editorial:masthead\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if data.Compact {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<div class=\"flex items-center justify-between py-3\"><a href=\"/\" class=\"editorial-masthead-wordmark text-2xl\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(data.Title)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `masthead.templ`, Line: 12, Col: 17}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "</a> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if data.MenuName != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<span class=\"editorial-masthead-kicker\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(data.MenuName)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `masthead.templ`, Line: 15, Col: 60}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "</span>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "</div><hr class=\"editorial-masthead-rule\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "<div class=\"text-center py-6\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if data.Kicker != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "<div class=\"editorial-masthead-kicker mb-2\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(data.Kicker)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `masthead.templ`, Line: 22, Col: 62}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "<a href=\"/\" class=\"editorial-masthead-wordmark inline-block text-5xl md:text-6xl\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(data.Title)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `masthead.templ`, Line: 25, Col: 17}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "</a><hr class=\"editorial-masthead-rule mt-4\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if data.MenuName != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "<nav class=\"editorial-section-label mt-3\" aria-label=\"Sections\"><span class=\"text-xs\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var6 string
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs("Sections: ")
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `masthead.templ`, Line: 30, Col: 42}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var7 string
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(data.MenuName)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `masthead.templ`, Line: 30, Col: 59}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "</span></nav>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
var _ = templruntime.GeneratedTemplate

View File

@ -2,11 +2,8 @@
name = "editorial" name = "editorial"
display_name = "Editorial" display_name = "Editorial"
scope = "@themes" scope = "@themes"
version = "0.1.2" version = "0.2.0"
description = "Didone display type, narrow measure, and hairline rules for newsrooms, longform writers, and opinion columnists." description = "Didone display type, narrow measure, and hairline rules for newsrooms, longform writers, and opinion columnists."
kind = "theme" kind = "theme"
categories = ["templates"] categories = ["templates"]
tags = ["editorial", "serif", "news", "longform", "magazine", "law", "finance", "opinion"] tags = ["editorial", "serif", "news", "longform", "magazine", "law", "finance", "opinion"]
[compatibility]
block_core = ">=0.15.0 <0.16.0"

View File

@ -1,48 +0,0 @@
package main
import (
"bytes"
"context"
"git.dev.alexdunmow.com/block/core/blocks"
)
// PullquoteBlockMeta describes the Editorial pull-quote block: a large oxblood
// Playfair italic block quote with optional attribution.
// Registered as "editorial:pullquote" at runtime.
var PullquoteBlockMeta = blocks.BlockMeta{
Key: "pullquote",
Title: "Pull Quote",
Description: "Large oxblood pull quote in Playfair italic",
Source: "editorial",
Category: blocks.CategoryTheme,
}
// PullquoteData carries parsed pull-quote content.
type PullquoteData struct {
Quote string // rich-text payload, trusted upstream
Attribution string
}
// PullquoteBlock renders the pull quote.
//
// Content shape:
//
// {
// "quote": "<p>The reform was always going to be quiet.</p>",
// "attribution": "— The Editor"
// }
func PullquoteBlock(ctx context.Context, content map[string]any) string {
data := PullquoteData{
Quote: getString(content, "quote"),
Attribution: getString(content, "attribution"),
}
if data.Quote == "" {
return ""
}
var buf bytes.Buffer
_ = pullquoteComponent(data).Render(ctx, &buf)
return buf.String()
}

View File

@ -1,16 +0,0 @@
package main
// pullquoteComponent renders the oxblood Playfair italic pull quote. The quote
// itself is trusted rich text supplied by the editor; we render it inline
// using templ.Raw so author-supplied emphasis is preserved. The attribution is
// rendered as a plain-text small-caps cap below.
templ pullquoteComponent(data PullquoteData) {
<aside class="editorial-column" data-block="editorial:pullquote">
<blockquote class="editorial-pullquote">
@templ.Raw(data.Quote)
if data.Attribution != "" {
<cite class="editorial-pullquote-attribution">{ data.Attribution }</cite>
}
</blockquote>
</aside>
}

View File

@ -1,71 +0,0 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.1020
package main
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
// pullquoteComponent renders the oxblood Playfair italic pull quote. The quote
// itself is trusted rich text supplied by the editor; we render it inline
// using templ.Raw so author-supplied emphasis is preserved. The attribution is
// rendered as a plain-text small-caps cap below.
func pullquoteComponent(data PullquoteData) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<aside class=\"editorial-column\" data-block=\"editorial:pullquote\"><blockquote class=\"editorial-pullquote\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ.Raw(data.Quote).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if data.Attribution != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<cite class=\"editorial-pullquote-attribution\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(data.Attribution)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `pullquote.templ`, Line: 12, Col: 68}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "</cite>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "</blockquote></aside>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
var _ = templruntime.GeneratedTemplate

View File

@ -1,188 +0,0 @@
package main
import (
"context"
"github.com/a-h/templ"
"git.dev.alexdunmow.com/block/core/blocks"
"git.dev.alexdunmow.com/block/core/plugin"
"git.dev.alexdunmow.com/block/core/templates"
)
// wrap adapts a templ-returning render function to templates.TemplateFunc.
// templ.Component already implements templates.HTMLComponent via its Render
// method, so the adapter only has to swap the return type.
func wrap(f func(ctx context.Context, doc map[string]any) templ.Component) templates.TemplateFunc {
return func(ctx context.Context, doc map[string]any) templates.HTMLComponent {
return f(ctx, doc)
}
}
// Register is the plugin entry point. The call order matters:
// 1. RegisterSystemTemplate seeds the system-template row.
// 2. RegisterPageTemplate once per page template (default / landing /
// article / full-width).
// 3. br.LoadSchemasFromFS BEFORE any br.Register so editor-side schemas
// are bound to their block keys.
// 4. br.Register for each theme-owned block (registered unqualified;
// addressed downstream as "editorial:<key>").
// 5. br.RegisterTemplateOverride for the four built-ins editorial styles
// differently (heading, text, button, image). The overrides are scoped
// to the "editorial" template — other templates keep their built-ins.
// 6. tr.RegisterEmailWrapper for the editorial-branded email layout.
func Register(tr templates.TemplateRegistry, br blocks.BlockRegistry) error {
tr.RegisterSystemTemplate(templates.SystemTemplateMeta{
Key: "editorial",
Title: "Editorial",
Description: "Didone display, narrow column, hairline rules for the modern newsroom.",
})
if err := tr.RegisterPageTemplate("editorial", templates.PageTemplateMeta{
Key: "default",
Title: "Default",
Description: "Standard page with masthead, narrow main column, footer",
Slots: []string{"header", "main", "footer"},
}, wrap(RenderEditorial)); err != nil {
return err
}
if err := tr.RegisterPageTemplate("editorial", templates.PageTemplateMeta{
Key: "landing",
Title: "Section Front",
Description: "Front-page-style stack: masthead, lead story, river, footer",
Slots: []string{"masthead", "lead", "river", "footer"},
}, wrap(RenderEditorialLanding)); err != nil {
return err
}
if err := tr.RegisterPageTemplate("editorial", templates.PageTemplateMeta{
Key: "article",
Title: "Article",
Description: "Single-column long read with byline, drop cap, marginalia rail",
Slots: []string{"header", "byline", "main", "marginalia", "footer"},
}, wrap(RenderEditorialArticle)); err != nil {
return err
}
if err := tr.RegisterPageTemplate("editorial", templates.PageTemplateMeta{
Key: "full-width",
Title: "Full Width",
Description: "Edge-to-edge for photo essays and data graphics",
Slots: []string{"header", "main", "footer"},
}, wrap(RenderEditorialFullWidth)); err != nil {
return err
}
// Schemas MUST be loaded before any Register so the editor binds the
// correct content schema to each block at registration time.
if err := br.LoadSchemasFromFS(Schemas()); err != nil {
return err
}
// Theme-owned blocks.
br.Register(MastheadBlockMeta, MastheadBlock)
br.Register(BylineBlockMeta, BylineBlock)
br.Register(PullquoteBlockMeta, PullquoteBlock)
br.Register(DropcapIntroBlockMeta, DropcapIntroBlock)
br.Register(MarginaliaBlockMeta, MarginaliaBlock)
br.Register(SectionLabelBlockMeta, SectionLabelBlock)
br.Register(ColophonBlockMeta, ColophonBlock)
// Built-in overrides scoped to the editorial template.
br.RegisterTemplateOverride("editorial", "heading", EditorialHeadingBlock)
br.RegisterTemplateOverride("editorial", "text", EditorialTextBlock)
br.RegisterTemplateOverride("editorial", "button", EditorialButtonBlock)
br.RegisterTemplateOverride("editorial", "image", EditorialImageBlock)
// Branded email wrapper.
tr.RegisterEmailWrapper("editorial", EditorialEmailWrapper)
return nil
}
// DefaultMasterPages returns the two master pages editorial seeds on first
// load. The slot keys and BlockKey strings here match spec §7 exactly; the
// `slot` block carries `slotName` equal to its rendered slot key so the CMS
// knows where to inject page content.
func DefaultMasterPages() []plugin.MasterPageDefinition {
return []plugin.MasterPageDefinition{
{
Key: "editorial:default-master",
Title: "Editorial Default Master",
PageTemplates: []string{"default", "landing", "full-width"},
Blocks: []plugin.MasterPageBlock{
{
BlockKey: "editorial:masthead",
Title: "Masthead",
Content: map[string]any{"menuName": "main", "kicker": "EST. 2026"},
Slot: "header",
SortOrder: 0,
},
{
BlockKey: "navbar",
Title: "Section Nav",
Content: map[string]any{"menuName": "sections"},
Slot: "header",
SortOrder: 1,
},
{
BlockKey: "slot",
Title: "Main Content",
Content: map[string]any{"slotName": "main", "placeholder": "Page content"},
Slot: "main",
SortOrder: 0,
},
{
BlockKey: "editorial:colophon",
Title: "Colophon",
Content: map[string]any{"showSignup": true, "issn": "0000-0000"},
Slot: "footer",
SortOrder: 0,
},
},
},
{
Key: "editorial:article-master",
Title: "Editorial Article Master",
PageTemplates: []string{"article"},
Blocks: []plugin.MasterPageBlock{
{
BlockKey: "editorial:masthead",
Title: "Masthead",
Content: map[string]any{"menuName": "main", "compact": true},
Slot: "header",
SortOrder: 0,
},
{
BlockKey: "editorial:byline",
Title: "Byline",
Content: map[string]any{"showPhoto": true, "showReadTime": true},
Slot: "byline",
SortOrder: 0,
},
{
BlockKey: "slot",
Title: "Article Body",
Content: map[string]any{"slotName": "main"},
Slot: "main",
SortOrder: 0,
},
{
BlockKey: "editorial:marginalia",
Title: "Marginalia Rail",
Content: map[string]any{"items": []any{}},
Slot: "marginalia",
SortOrder: 0,
},
{
BlockKey: "editorial:colophon",
Title: "Colophon",
Content: map[string]any{"showSignup": true},
Slot: "footer",
SortOrder: 0,
},
},
},
}
}

View File

@ -1,29 +0,0 @@
package main
import (
"io/fs"
"net/http"
"git.dev.alexdunmow.com/block/core/blocks"
"git.dev.alexdunmow.com/block/core/plugin"
"git.dev.alexdunmow.com/block/core/templates"
)
// Registration is the compile-time plugin registration for the Editorial theme.
//
// Editorial is a templ-style theme: each block ships a typed `<key>.go` render
// function backed by a `<key>.templ` component. The standalone-plugin block
// signature is `func(ctx, content map[string]any) string` (no children arg).
var Registration = plugin.PluginRegistration{
Name: "editorial",
Version: plugin.ParseModVersion(pluginModBytes),
Register: func(tr templates.TemplateRegistry, br blocks.BlockRegistry) error {
return Register(tr, br)
},
Assets: func() http.Handler { return AssetsHandler() },
Schemas: func() fs.FS { return Schemas() },
ThemePresets: func() []byte { return ThemePresets() },
BundledFonts: func() []byte { return BundledFonts() },
MasterPages: func() []plugin.MasterPageDefinition { return DefaultMasterPages() },
CSSManifest: func() *plugin.CSSManifest { return ThemeCSSManifest() },
}

View File

@ -1,45 +0,0 @@
package main
import (
"bytes"
"context"
"git.dev.alexdunmow.com/block/core/blocks"
)
// SectionLabelBlockMeta describes the Editorial section-label block: an
// uppercase / small-caps label followed by an optional hairline divider.
// Registered as "editorial:section_label" at runtime.
var SectionLabelBlockMeta = blocks.BlockMeta{
Key: "section_label",
Title: "Section Label",
Description: "Small-caps section label with optional hairline divider",
Source: "editorial",
Category: blocks.CategoryTheme,
}
// SectionLabelData carries parsed section-label content.
type SectionLabelData struct {
Label string
Divider bool
}
// SectionLabelBlock renders the section label.
//
// Content shape:
//
// { "label": "Politics", "divider": "true" }
func SectionLabelBlock(ctx context.Context, content map[string]any) string {
data := SectionLabelData{
Label: getString(content, "label"),
Divider: getBool(content, "divider", true),
}
if data.Label == "" {
return ""
}
var buf bytes.Buffer
_ = sectionLabelComponent(data).Render(ctx, &buf)
return buf.String()
}

View File

@ -1,12 +0,0 @@
package main
// sectionLabelComponent renders the section label. The visual contract is
// uppercase + small-caps text followed (optionally) by a 1px hairline rule.
templ sectionLabelComponent(data SectionLabelData) {
<div class="editorial-column" data-block="editorial:section_label">
<div class="editorial-section-label">{ data.Label }</div>
if data.Divider {
<hr class="editorial-section-label-rule"/>
}
</div>
}

View File

@ -1,65 +0,0 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.1020
package main
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
// sectionLabelComponent renders the section label. The visual contract is
// uppercase + small-caps text followed (optionally) by a 1px hairline rule.
func sectionLabelComponent(data SectionLabelData) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div class=\"editorial-column\" data-block=\"editorial:section_label\"><div class=\"editorial-section-label\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(data.Label)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `section_label.templ`, Line: 7, Col: 51}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if data.Divider {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<hr class=\"editorial-section-label-rule\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
var _ = templruntime.GeneratedTemplate

View File

@ -1,365 +0,0 @@
package main
// editorialUtilityCSS is the theme's custom utility layer appended to the host
// Tailwind input. It is intentionally light: no @font-face rules (those are
// emitted by the host from the admin's font assignments per docs/FONTS.md), no
// hardcoded hex / rgb / named colours. Every colour goes through the shadcn
// HSL variable consumer pattern: `hsl(var(--token))`.
//
// What this CSS contracts to deliver (cross-checked against the UAT §13):
// - .prose-editorial: 64ch measure, indented continuation paragraphs.
// - .editorial-dropcap p:first-of-type::first-letter: oxblood accent,
// ≥ 3× body font-size.
// - .editorial-marginalia: italic Source Serif 4 12px right-rail asides.
// - .editorial-masthead-wordmark: Playfair Display 900 italic.
// - .editorial-pullquote: Playfair italic 38px in oxblood accent.
// - .editorial-section-label: uppercase + small-caps + hairline.
// - .editorial-hairline / hr.editorial-rule: 1px solid hsl(var(--border)).
// - .editorial-button: transparent fill, 1px border, hover → accent text.
// - .editorial-caption: Source Serif italic 14px with hairline above.
//
// All `font-family` declarations route through the three CSS variables
// (`--font-heading`, `--font-body`, `--font-mono`) the host exposes on :root,
// with fallback stacks derived from spec §3.
const editorialUtilityCSS = `
/* === Editorial theme — utility layer === */
:root {
--editorial-measure: 64ch;
--editorial-rule: 1px solid hsl(var(--border));
}
/* --- Narrow editorial column with indented continuation paragraphs --- */
.prose-editorial {
max-width: var(--editorial-measure);
margin-left: auto;
margin-right: auto;
font-family: var(--font-body, "Source Serif 4", "Source Serif Pro", Georgia, serif);
color: hsl(var(--foreground));
line-height: 1.65;
}
.prose-editorial p {
margin: 0 0 1em 0;
text-indent: 0;
}
.prose-editorial p + p {
text-indent: 1.5em;
}
.prose-editorial h1,
.prose-editorial h2,
.prose-editorial h3,
.prose-editorial h4 {
font-family: var(--font-heading, "Playfair Display", Georgia, serif);
font-weight: 700;
letter-spacing: -0.01em;
color: hsl(var(--foreground));
}
.prose-editorial a {
color: hsl(var(--accent));
text-decoration: underline;
text-decoration-thickness: 1px;
text-underline-offset: 2px;
}
/* --- Drop cap — oxblood accent first letter, ≥ 3x body size --- */
.editorial-dropcap p:first-of-type::first-letter {
font-family: var(--font-heading, "Playfair Display", Georgia, serif);
font-weight: 900;
font-style: italic;
color: hsl(var(--accent));
float: left;
font-size: 4.5em;
line-height: 0.85;
padding-right: 0.12em;
padding-top: 0.05em;
margin-bottom: -0.1em;
}
/* --- Marginalia rail (right-side italic notes) --- */
.editorial-marginalia {
font-family: var(--font-body, "Source Serif 4", "Source Serif Pro", Georgia, serif);
font-style: italic;
font-size: 12px;
line-height: 1.5;
color: hsl(var(--muted-foreground));
}
.editorial-marginalia aside {
margin: 0 0 1.5rem 0;
padding-left: 0.75rem;
border-left: var(--editorial-rule);
}
@media (max-width: 768px) {
.editorial-marginalia {
margin-top: 1.5rem;
border-top: var(--editorial-rule);
padding-top: 1rem;
}
}
/* --- Masthead wordmark --- */
.editorial-masthead-wordmark {
font-family: var(--font-heading, "Playfair Display", Georgia, serif);
font-weight: 900;
font-style: italic;
letter-spacing: -0.02em;
color: hsl(var(--foreground));
}
.editorial-masthead-kicker {
font-family: var(--font-mono, "JetBrains Mono", "SF Mono", ui-monospace, monospace);
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.15em;
color: hsl(var(--muted-foreground));
}
.editorial-masthead-rule {
border: 0;
border-top: var(--editorial-rule);
margin: 0.5rem 0 0 0;
}
/* --- Pull quote — Playfair italic in oxblood --- */
.editorial-pullquote {
font-family: var(--font-heading, "Playfair Display", Georgia, serif);
font-style: italic;
font-size: 38px;
line-height: 1.2;
color: hsl(var(--accent));
margin: 2.5rem auto;
max-width: var(--editorial-measure);
border: none;
}
.editorial-pullquote-attribution {
display: block;
margin-top: 1rem;
font-family: var(--font-body, "Source Serif 4", Georgia, serif);
font-style: normal;
font-size: 14px;
text-transform: uppercase;
letter-spacing: 0.12em;
color: hsl(var(--muted-foreground));
}
/* --- Section label — small-caps with hairline rule --- */
.editorial-section-label {
font-family: var(--font-body, "Source Serif 4", Georgia, serif);
text-transform: uppercase;
font-variant: small-caps;
font-feature-settings: "smcp";
letter-spacing: 0.15em;
font-size: 13px;
color: hsl(var(--foreground));
}
.editorial-section-label-rule {
border: 0;
border-top: var(--editorial-rule);
margin: 0.5rem 0 1.5rem 0;
}
/* --- Hairline rules --- */
hr.editorial-rule,
.editorial-hairline {
border: 0;
border-top: var(--editorial-rule);
margin: 1.5rem 0;
}
/* --- Byline --- */
.editorial-byline {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.75rem 0;
border-top: var(--editorial-rule);
border-bottom: var(--editorial-rule);
}
.editorial-byline-photo {
width: 36px;
height: 36px;
border-radius: 50%;
background-color: hsl(var(--muted));
flex-shrink: 0;
object-fit: cover;
}
.editorial-byline-author {
font-family: var(--font-mono, "JetBrains Mono", monospace), var(--font-body, "Source Serif 4", Georgia, serif);
/* The spec calls for Inter for UI bylines. Fallback to Inter then system. */
font-family: var(--font-body, "Inter", -apple-system, sans-serif);
font-weight: 500;
font-size: 15px;
color: hsl(var(--foreground));
}
.editorial-byline-meta {
font-family: var(--font-body, "Inter", -apple-system, sans-serif);
font-size: 12px;
text-transform: uppercase;
letter-spacing: 0.1em;
color: hsl(var(--muted-foreground));
}
/* --- Override: heading (Playfair, tight tracking, optional small-caps kicker) --- */
.editorial-heading {
font-family: var(--font-heading, "Playfair Display", Georgia, serif);
font-weight: 700;
letter-spacing: -0.015em;
color: hsl(var(--foreground));
line-height: 1.15;
}
.editorial-heading-kicker {
display: block;
margin-bottom: 0.5rem;
font-family: var(--font-body, "Inter", -apple-system, sans-serif);
font-size: 12px;
text-transform: uppercase;
font-variant: small-caps;
font-feature-settings: "smcp";
letter-spacing: 0.18em;
color: hsl(var(--muted-foreground));
}
/* --- Override: text body (Source Serif, hanging quotes) --- */
.editorial-text {
font-family: var(--font-body, "Source Serif 4", "Source Serif Pro", Georgia, serif);
color: hsl(var(--foreground));
line-height: 1.7;
max-width: var(--editorial-measure);
}
.editorial-text p + p {
text-indent: 1.5em;
margin-top: 0;
}
.editorial-text blockquote {
hanging-punctuation: first;
font-style: italic;
border-left: var(--editorial-rule);
padding-left: 1rem;
margin-left: 0;
}
/* --- Override: button (hairline border, no fill, accent on hover) --- */
.editorial-button {
display: inline-block;
padding: 0.5rem 1rem;
font-family: var(--font-body, "Inter", -apple-system, sans-serif);
font-size: 13px;
text-transform: uppercase;
letter-spacing: 0.12em;
background-color: transparent;
color: hsl(var(--foreground));
border: 1px solid hsl(var(--border));
text-decoration: none;
cursor: pointer;
transition: color 150ms ease, border-color 150ms ease;
}
.editorial-button:hover,
.editorial-button:focus {
color: hsl(var(--accent));
border-color: hsl(var(--accent));
}
.editorial-button:focus-visible {
outline: 2px solid hsl(var(--ring));
outline-offset: 2px;
}
/* --- Override: image (Source Serif italic caption with hairline above) --- */
.editorial-figure {
margin: 1.5rem 0;
}
.editorial-figure img {
display: block;
width: 100%;
height: auto;
}
.editorial-figure figcaption {
margin-top: 0.75rem;
padding-top: 0.5rem;
border-top: var(--editorial-rule);
font-family: var(--font-body, "Source Serif 4", "Source Serif Pro", Georgia, serif);
font-style: italic;
font-size: 14px;
color: hsl(var(--muted-foreground));
}
/* --- Colophon --- */
.editorial-colophon {
border-top: var(--editorial-rule);
padding-top: 1.5rem;
font-family: var(--font-body, "Inter", -apple-system, sans-serif);
font-size: 13px;
color: hsl(var(--muted-foreground));
}
.editorial-colophon-issn {
font-family: var(--font-mono, "JetBrains Mono", monospace);
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.1em;
}
/* --- Page layout — narrow editorial column --- */
.editorial-column {
max-width: var(--editorial-measure);
margin-left: auto;
margin-right: auto;
padding-left: 1rem;
padding-right: 1rem;
}
.editorial-wide {
max-width: 96rem;
margin-left: auto;
margin-right: auto;
padding-left: 1rem;
padding-right: 1rem;
}
.editorial-article-grid {
display: grid;
grid-template-columns: 1fr;
gap: 1.5rem;
max-width: 80rem;
margin-left: auto;
margin-right: auto;
padding-left: 1rem;
padding-right: 1rem;
}
@media (min-width: 1024px) {
.editorial-article-grid {
grid-template-columns: minmax(0, 64ch) 16rem;
}
}
`

View File

@ -1,248 +0,0 @@
package main
import (
"context"
"git.dev.alexdunmow.com/block/core/templates/bn"
)
// PageData carries data parsed off the doc map for the four page templates.
// This is the same shape gotham uses; the renderers below differ only in
// slot composition and column widths.
type PageData struct {
Title string
Slots map[string]string
ThemeMode string
ThemeCSS string
SiteSettings bn.SiteSettingsData
PageMeta bn.PageMeta
StructuredData string
CSSHash string
PageviewNonce string
EngagementConfig bn.EngagementConfig
}
func parseEditorialPageData(doc map[string]any) PageData {
title := "Untitled"
if t, ok := doc["title"].(string); ok {
title = t
}
slots := make(map[string]string)
if s, ok := doc["slots"].(map[string]string); ok {
slots = s
}
themeCSS := ""
if tc, ok := doc["theme_css"].(string); ok {
themeCSS = tc
}
structuredData := ""
if sd, ok := doc["structured_data"].(string); ok {
structuredData = sd
}
cssHash := ""
if ch, ok := doc["css_hash"].(string); ok {
cssHash = ch
}
pageviewNonce := ""
if pn, ok := doc["pageview_nonce"].(string); ok {
pageviewNonce = pn
}
themeMode := "light"
if tm, ok := doc["theme_mode"].(string); ok && tm != "" {
themeMode = tm
}
siteSettings := bn.ParseSiteSettings(doc)
pageMeta := bn.ParsePageMeta(doc)
engagementConfig := bn.ParseEngagementConfig(doc)
return PageData{
Title: title,
Slots: slots,
ThemeMode: themeMode,
ThemeCSS: themeCSS,
SiteSettings: siteSettings,
PageMeta: pageMeta,
StructuredData: structuredData,
CSSHash: cssHash,
PageviewNonce: pageviewNonce,
EngagementConfig: engagementConfig,
}
}
// Default editorial page — masthead header, narrow main column, footer.
templ Editorial(data PageData) {
<!DOCTYPE html>
<html lang="en">
@bn.Head(bn.HeadData{
Title: data.Title,
Settings: data.SiteSettings,
PageMeta: data.PageMeta,
ThemeMode: data.ThemeMode,
ThemeCSS: data.ThemeCSS,
PluginStyles: []string{},
StructuredData: data.StructuredData,
CSSHash: data.CSSHash,
PageviewNonce: data.PageviewNonce,
EngagementConfig: data.EngagementConfig,
})
<body class="antialiased min-h-screen flex flex-col" style="background-color: hsl(var(--background)); color: hsl(var(--foreground));">
@bn.AdminBypassBanner(data.SiteSettings)
<header class="w-full">
@templ.Raw(data.Slots["header"])
</header>
<main class="flex-grow editorial-column w-full py-8 prose-editorial">
if main, ok := data.Slots["main"]; ok && main != "" {
@templ.Raw(main)
} else {
<div class="py-20 text-center">
<p style="color: hsl(var(--muted-foreground));">No content blocks assigned to this page.</p>
</div>
}
</main>
<footer class="w-full mt-auto pt-8 pb-6">
@templ.Raw(data.Slots["footer"])
</footer>
@bn.BodyEnd(data.SiteSettings)
</body>
</html>
}
// Section-front (landing) — stacked masthead, lead story, river, footer.
templ EditorialLanding(data PageData) {
<!DOCTYPE html>
<html lang="en">
@bn.Head(bn.HeadData{
Title: data.Title,
Settings: data.SiteSettings,
PageMeta: data.PageMeta,
ThemeMode: data.ThemeMode,
ThemeCSS: data.ThemeCSS,
PluginStyles: []string{},
StructuredData: data.StructuredData,
CSSHash: data.CSSHash,
PageviewNonce: data.PageviewNonce,
EngagementConfig: data.EngagementConfig,
})
<body class="antialiased min-h-screen flex flex-col" style="background-color: hsl(var(--background)); color: hsl(var(--foreground));">
@bn.AdminBypassBanner(data.SiteSettings)
<header class="w-full">
@templ.Raw(data.Slots["masthead"])
</header>
<section class="w-full editorial-wide py-6">
@templ.Raw(data.Slots["lead"])
</section>
<section class="w-full editorial-wide py-6">
@templ.Raw(data.Slots["river"])
</section>
<footer class="w-full mt-auto pt-8 pb-6">
@templ.Raw(data.Slots["footer"])
</footer>
@bn.BodyEnd(data.SiteSettings)
</body>
</html>
}
// Article — masthead, byline, narrow main column with drop cap, marginalia
// rail, footer. The grid collapses to a single column below 1024px.
templ EditorialArticle(data PageData) {
<!DOCTYPE html>
<html lang="en">
@bn.Head(bn.HeadData{
Title: data.Title,
Settings: data.SiteSettings,
PageMeta: data.PageMeta,
ThemeMode: data.ThemeMode,
ThemeCSS: data.ThemeCSS,
PluginStyles: []string{},
StructuredData: data.StructuredData,
CSSHash: data.CSSHash,
PageviewNonce: data.PageviewNonce,
EngagementConfig: data.EngagementConfig,
})
<body class="antialiased min-h-screen flex flex-col" style="background-color: hsl(var(--background)); color: hsl(var(--foreground));">
@bn.AdminBypassBanner(data.SiteSettings)
<header class="w-full">
@templ.Raw(data.Slots["header"])
</header>
<div class="editorial-column w-full py-4">
@templ.Raw(data.Slots["byline"])
</div>
<div class="editorial-article-grid flex-grow py-8">
<main class="prose-editorial">
if main, ok := data.Slots["main"]; ok && main != "" {
@templ.Raw(main)
} else {
<p style="color: hsl(var(--muted-foreground));">No content blocks assigned to this page.</p>
}
</main>
<aside>
@templ.Raw(data.Slots["marginalia"])
</aside>
</div>
<footer class="w-full mt-auto pt-8 pb-6">
@templ.Raw(data.Slots["footer"])
</footer>
@bn.BodyEnd(data.SiteSettings)
</body>
</html>
}
// Full-width — edge-to-edge content for photo essays and data graphics.
templ EditorialFullWidth(data PageData) {
<!DOCTYPE html>
<html lang="en">
@bn.Head(bn.HeadData{
Title: data.Title,
Settings: data.SiteSettings,
PageMeta: data.PageMeta,
ThemeMode: data.ThemeMode,
ThemeCSS: data.ThemeCSS,
PluginStyles: []string{},
StructuredData: data.StructuredData,
CSSHash: data.CSSHash,
PageviewNonce: data.PageviewNonce,
EngagementConfig: data.EngagementConfig,
})
<body class="antialiased min-h-screen flex flex-col" style="background-color: hsl(var(--background)); color: hsl(var(--foreground));">
@bn.AdminBypassBanner(data.SiteSettings)
<header class="w-full">
@templ.Raw(data.Slots["header"])
</header>
<main class="flex-grow w-full">
if main, ok := data.Slots["main"]; ok && main != "" {
@templ.Raw(main)
} else {
<div class="editorial-wide py-20 text-center">
<p style="color: hsl(var(--muted-foreground));">No content blocks assigned to this page.</p>
</div>
}
</main>
<footer class="w-full mt-auto pt-8 pb-6">
@templ.Raw(data.Slots["footer"])
</footer>
@bn.BodyEnd(data.SiteSettings)
</body>
</html>
}
func RenderEditorial(ctx context.Context, doc map[string]any) templ.Component {
return Editorial(parseEditorialPageData(doc))
}
func RenderEditorialLanding(ctx context.Context, doc map[string]any) templ.Component {
return EditorialLanding(parseEditorialPageData(doc))
}
func RenderEditorialArticle(ctx context.Context, doc map[string]any) templ.Component {
return EditorialArticle(parseEditorialPageData(doc))
}
func RenderEditorialFullWidth(ctx context.Context, doc map[string]any) templ.Component {
return EditorialFullWidth(parseEditorialPageData(doc))
}

View File

@ -1,504 +0,0 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.1020
package main
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
import (
"context"
"git.dev.alexdunmow.com/block/core/templates/bn"
)
// PageData carries data parsed off the doc map for the four page templates.
// This is the same shape gotham uses; the renderers below differ only in
// slot composition and column widths.
type PageData struct {
Title string
Slots map[string]string
ThemeMode string
ThemeCSS string
SiteSettings bn.SiteSettingsData
PageMeta bn.PageMeta
StructuredData string
CSSHash string
PageviewNonce string
EngagementConfig bn.EngagementConfig
}
func parseEditorialPageData(doc map[string]any) PageData {
title := "Untitled"
if t, ok := doc["title"].(string); ok {
title = t
}
slots := make(map[string]string)
if s, ok := doc["slots"].(map[string]string); ok {
slots = s
}
themeCSS := ""
if tc, ok := doc["theme_css"].(string); ok {
themeCSS = tc
}
structuredData := ""
if sd, ok := doc["structured_data"].(string); ok {
structuredData = sd
}
cssHash := ""
if ch, ok := doc["css_hash"].(string); ok {
cssHash = ch
}
pageviewNonce := ""
if pn, ok := doc["pageview_nonce"].(string); ok {
pageviewNonce = pn
}
themeMode := "light"
if tm, ok := doc["theme_mode"].(string); ok && tm != "" {
themeMode = tm
}
siteSettings := bn.ParseSiteSettings(doc)
pageMeta := bn.ParsePageMeta(doc)
engagementConfig := bn.ParseEngagementConfig(doc)
return PageData{
Title: title,
Slots: slots,
ThemeMode: themeMode,
ThemeCSS: themeCSS,
SiteSettings: siteSettings,
PageMeta: pageMeta,
StructuredData: structuredData,
CSSHash: cssHash,
PageviewNonce: pageviewNonce,
EngagementConfig: engagementConfig,
}
}
// Default editorial page — masthead header, narrow main column, footer.
func Editorial(data PageData) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<!doctype html><html lang=\"en\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = bn.Head(bn.HeadData{
Title: data.Title,
Settings: data.SiteSettings,
PageMeta: data.PageMeta,
ThemeMode: data.ThemeMode,
ThemeCSS: data.ThemeCSS,
PluginStyles: []string{},
StructuredData: data.StructuredData,
CSSHash: data.CSSHash,
PageviewNonce: data.PageviewNonce,
EngagementConfig: data.EngagementConfig,
}).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<body class=\"antialiased min-h-screen flex flex-col\" style=\"background-color: hsl(var(--background)); color: hsl(var(--foreground));\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = bn.AdminBypassBanner(data.SiteSettings).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<header class=\"w-full\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ.Raw(data.Slots["header"]).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "</header><main class=\"flex-grow editorial-column w-full py-8 prose-editorial\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if main, ok := data.Slots["main"]; ok && main != "" {
templ_7745c5c3_Err = templ.Raw(main).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<div class=\"py-20 text-center\"><p style=\"color: hsl(var(--muted-foreground));\">No content blocks assigned to this page.</p></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "</main><footer class=\"w-full mt-auto pt-8 pb-6\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ.Raw(data.Slots["footer"]).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "</footer>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = bn.BodyEnd(data.SiteSettings).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "</body></html>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
// Section-front (landing) — stacked masthead, lead story, river, footer.
func EditorialLanding(data PageData) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var2 := templ.GetChildren(ctx)
if templ_7745c5c3_Var2 == nil {
templ_7745c5c3_Var2 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "<!doctype html><html lang=\"en\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = bn.Head(bn.HeadData{
Title: data.Title,
Settings: data.SiteSettings,
PageMeta: data.PageMeta,
ThemeMode: data.ThemeMode,
ThemeCSS: data.ThemeCSS,
PluginStyles: []string{},
StructuredData: data.StructuredData,
CSSHash: data.CSSHash,
PageviewNonce: data.PageviewNonce,
EngagementConfig: data.EngagementConfig,
}).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "<body class=\"antialiased min-h-screen flex flex-col\" style=\"background-color: hsl(var(--background)); color: hsl(var(--foreground));\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = bn.AdminBypassBanner(data.SiteSettings).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "<header class=\"w-full\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ.Raw(data.Slots["masthead"]).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "</header><section class=\"w-full editorial-wide py-6\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ.Raw(data.Slots["lead"]).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "</section><section class=\"w-full editorial-wide py-6\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ.Raw(data.Slots["river"]).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "</section><footer class=\"w-full mt-auto pt-8 pb-6\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ.Raw(data.Slots["footer"]).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "</footer>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = bn.BodyEnd(data.SiteSettings).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "</body></html>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
// Article — masthead, byline, narrow main column with drop cap, marginalia
// rail, footer. The grid collapses to a single column below 1024px.
func EditorialArticle(data PageData) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var3 := templ.GetChildren(ctx)
if templ_7745c5c3_Var3 == nil {
templ_7745c5c3_Var3 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "<!doctype html><html lang=\"en\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = bn.Head(bn.HeadData{
Title: data.Title,
Settings: data.SiteSettings,
PageMeta: data.PageMeta,
ThemeMode: data.ThemeMode,
ThemeCSS: data.ThemeCSS,
PluginStyles: []string{},
StructuredData: data.StructuredData,
CSSHash: data.CSSHash,
PageviewNonce: data.PageviewNonce,
EngagementConfig: data.EngagementConfig,
}).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "<body class=\"antialiased min-h-screen flex flex-col\" style=\"background-color: hsl(var(--background)); color: hsl(var(--foreground));\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = bn.AdminBypassBanner(data.SiteSettings).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "<header class=\"w-full\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ.Raw(data.Slots["header"]).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "</header><div class=\"editorial-column w-full py-4\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ.Raw(data.Slots["byline"]).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "</div><div class=\"editorial-article-grid flex-grow py-8\"><main class=\"prose-editorial\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if main, ok := data.Slots["main"]; ok && main != "" {
templ_7745c5c3_Err = templ.Raw(main).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "<p style=\"color: hsl(var(--muted-foreground));\">No content blocks assigned to this page.</p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "</main><aside>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ.Raw(data.Slots["marginalia"]).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "</aside></div><footer class=\"w-full mt-auto pt-8 pb-6\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ.Raw(data.Slots["footer"]).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "</footer>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = bn.BodyEnd(data.SiteSettings).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "</body></html>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
// Full-width — edge-to-edge content for photo essays and data graphics.
func EditorialFullWidth(data PageData) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var4 := templ.GetChildren(ctx)
if templ_7745c5c3_Var4 == nil {
templ_7745c5c3_Var4 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "<!doctype html><html lang=\"en\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = bn.Head(bn.HeadData{
Title: data.Title,
Settings: data.SiteSettings,
PageMeta: data.PageMeta,
ThemeMode: data.ThemeMode,
ThemeCSS: data.ThemeCSS,
PluginStyles: []string{},
StructuredData: data.StructuredData,
CSSHash: data.CSSHash,
PageviewNonce: data.PageviewNonce,
EngagementConfig: data.EngagementConfig,
}).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "<body class=\"antialiased min-h-screen flex flex-col\" style=\"background-color: hsl(var(--background)); color: hsl(var(--foreground));\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = bn.AdminBypassBanner(data.SiteSettings).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "<header class=\"w-full\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ.Raw(data.Slots["header"]).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "</header><main class=\"flex-grow w-full\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if main, ok := data.Slots["main"]; ok && main != "" {
templ_7745c5c3_Err = templ.Raw(main).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "<div class=\"editorial-wide py-20 text-center\"><p style=\"color: hsl(var(--muted-foreground));\">No content blocks assigned to this page.</p></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "</main><footer class=\"w-full mt-auto pt-8 pb-6\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ.Raw(data.Slots["footer"]).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "</footer>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = bn.BodyEnd(data.SiteSettings).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "</body></html>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
func RenderEditorial(ctx context.Context, doc map[string]any) templ.Component {
return Editorial(parseEditorialPageData(doc))
}
func RenderEditorialLanding(ctx context.Context, doc map[string]any) templ.Component {
return EditorialLanding(parseEditorialPageData(doc))
}
func RenderEditorialArticle(ctx context.Context, doc map[string]any) templ.Component {
return EditorialArticle(parseEditorialPageData(doc))
}
func RenderEditorialFullWidth(ctx context.Context, doc map[string]any) templ.Component {
return EditorialFullWidth(parseEditorialPageData(doc))
}
var _ = templruntime.GeneratedTemplate

View File

@ -0,0 +1,15 @@
<!doctype html>
<html lang="en">
{{ head_html|safe }}
<body class="antialiased min-h-screen flex flex-col" style="background-color: hsl(var(--background)); color: hsl(var(--foreground));">
{{ admin_banner_html|safe }}
<header class="w-full">{{ slots.header|safe }}</header>
<div class="editorial-column w-full py-4">{{ slots.byline|safe }}</div>
<div class="editorial-article-grid flex-grow py-8">
<main class="prose-editorial">{% if slots.main %}{{ slots.main|safe }}{% else %}<p style="color: hsl(var(--muted-foreground));">No content blocks assigned to this page.</p>{% endif %}</main>
<aside>{{ slots.marginalia|safe }}</aside>
</div>
<footer class="w-full mt-auto pt-8 pb-6">{{ slots.footer|safe }}</footer>
{{ body_end_html|safe }}
</body>
</html>

View File

@ -0,0 +1,11 @@
<!doctype html>
<html lang="en">
{{ head_html|safe }}
<body class="antialiased min-h-screen flex flex-col" style="background-color: hsl(var(--background)); color: hsl(var(--foreground));">
{{ admin_banner_html|safe }}
<header class="w-full">{{ slots.header|safe }}</header>
<main class="flex-grow editorial-column w-full py-8 prose-editorial">{% if slots.main %}{{ slots.main|safe }}{% else %}<div class="py-20 text-center"><p style="color: hsl(var(--muted-foreground));">No content blocks assigned to this page.</p></div>{% endif %}</main>
<footer class="w-full mt-auto pt-8 pb-6">{{ slots.footer|safe }}</footer>
{{ body_end_html|safe }}
</body>
</html>

View File

@ -0,0 +1,11 @@
<!doctype html>
<html lang="en">
{{ head_html|safe }}
<body class="antialiased min-h-screen flex flex-col" style="background-color: hsl(var(--background)); color: hsl(var(--foreground));">
{{ admin_banner_html|safe }}
<header class="w-full">{{ slots.header|safe }}</header>
<main class="flex-grow w-full">{% if slots.main %}{{ slots.main|safe }}{% else %}<div class="editorial-wide py-20 text-center"><p style="color: hsl(var(--muted-foreground));">No content blocks assigned to this page.</p></div>{% endif %}</main>
<footer class="w-full mt-auto pt-8 pb-6">{{ slots.footer|safe }}</footer>
{{ body_end_html|safe }}
</body>
</html>

View File

@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
{{ head_html|safe }}
<body class="antialiased min-h-screen flex flex-col" style="background-color: hsl(var(--background)); color: hsl(var(--foreground));">
{{ admin_banner_html|safe }}
<header class="w-full">{{ slots.masthead|safe }}</header>
<section class="w-full editorial-wide py-6">{{ slots.lead|safe }}</section>
<section class="w-full editorial-wide py-6">{{ slots.river|safe }}</section>
<footer class="w-full mt-auto pt-8 pb-6">{{ slots.footer|safe }}</footer>
{{ body_end_html|safe }}
</body>
</html>

View File

@ -0,0 +1,14 @@
<!doctype html><html lang="en" xmlns="http://www.w3.org/1999/xhtml" xmlns:o="urn:schemas-microsoft-com:office:office"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><meta name="x-apple-disable-message-reformatting"><meta http-equiv="X-UA-Compatible" content="IE=edge"><title>{{ site_name }}</title><style type="text/css">
body, table, td, p, a, li, blockquote {
-webkit-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
}
table, td { mso-table-lspace: 0pt; mso-table-rspace: 0pt; }
img { -ms-interpolation-mode: bicubic; border: 0; height: auto; outline: none; text-decoration: none; }
body { margin: 0 !important; padding: 0 !important; width: 100% !important; }
a[x-apple-data-detectors] { color: inherit !important; text-decoration: none !important; }
@media only screen and (max-width: 620px) {
.email-container { width: 100% !important; max-width: 100% !important; }
.content-padding { padding-left: 24px !important; padding-right: 24px !important; }
}
</style></head><body style="background-color: {{ colors.background|default:"#f7f1e4" }}; margin: 0; padding: 0; font-family: var(--font-body, &amp;#39;Source Serif 4&amp;#39;, Georgia, serif);">{% if preview_text %}<div style="display: none; max-height: 0; overflow: hidden; mso-hide: all;">{{ preview_text }}</div>{% endif %}<table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0"><tr><td align="center" style="padding: 48px 10px; background-color: {{ colors.background|default:"#f7f1e4" }};"><table role="presentation" class="email-container" width="580" cellspacing="0" cellpadding="0" border="0" style="max-width: 580px; background-color: {{ colors.card|default:"#fbf8ec" }};"><!-- Masthead --><tr><td align="center" style="padding: 32px 40px 16px 40px;">{% if logo_url %}<img src="{{ logo_url }}" alt="{{ site_name }}" style="max-height: 48px; width: auto; display: block;">{% elif site_name %}<h1 style="margin: 0; font-family: var(--font-heading, &amp;#39;Playfair Display&amp;#39;, Georgia, serif); font-style: italic; font-weight: 900; font-size: 36px; color: {{ colors.foreground|default:"#1d1714" }}; letter-spacing: -0.02em;">{{ site_name }}</h1>{% endif %}</td></tr><!-- Hairline rule --><tr><td style="padding: 0 40px;"><table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0"><tr><td style="border-top: 1px solid {{ colors.border|default:"#d6cdbf" }}; font-size: 0; line-height: 0; height: 1px;">&nbsp;</td></tr></table></td></tr><!-- Body --><tr><td class="content-padding" style="padding: 32px 48px 40px 48px; color: {{ colors.foreground|default:"#1d1714" }}; font-size: 16px; line-height: 1.7; font-family: var(--font-body, &amp;#39;Source Serif 4&amp;#39;, Georgia, serif);">{{ body|safe }}</td></tr><!-- Footer --><tr><td style="padding: 24px 48px 40px 48px; border-top: 1px solid {{ colors.border|default:"#d6cdbf" }};"><table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0"><tr><td align="center" style="font-family: var(--font-body, &amp;#39;Inter&amp;#39;, sans-serif); font-size: 12px; color: {{ colors.mutedForeground|default:"#6e6357" }}; text-transform: uppercase; letter-spacing: 0.1em;"><p style="margin: 0 0 8px;">{{ site_name }}</p>{% if unsubscribe_url %}<p style="margin: 0; font-size: 11px;"><a href="{{ unsubscribe_url }}" style="color: #702021; text-decoration: underline;">Unsubscribe</a></p>{% endif %}</td></tr></table></td></tr></table></td></tr></table></body></html>

View File

@ -0,0 +1 @@
{% if text %}{% if url %}<a href="{{ url }}" class="inline-block" aria-label="{{ ariaLabel }}"><button type="button" class="editorial-button">{{ text }}</button></a>{% else %}<button type="button" class="editorial-button" aria-label="{{ ariaLabel }}">{{ text }}</button>{% endif %}{% endif %}

View File

@ -0,0 +1 @@
{% if level|integer == 1 %}<div class="editorial-column">{% if kicker %}<span class="editorial-heading-kicker">{{ kicker }}</span>{% endif %}<h1 class="editorial-heading text-5xl md:text-7xl mt-2 {{ textClass }}">{{ text }}</h1></div>{% elif level|integer == 3 %}<h3 class="editorial-heading text-2xl md:text-3xl editorial-column {{ textClass }}">{{ text }}</h3>{% elif level|integer == 4 %}<h4 class="editorial-heading text-xl md:text-2xl editorial-column {{ textClass }}">{{ text }}</h4>{% elif level|integer == 5 %}<h5 class="editorial-heading text-lg editorial-column {{ textClass }}">{{ text }}</h5>{% elif level|integer == 6 %}<h6 class="editorial-heading text-base editorial-column {{ textClass }}">{{ text }}</h6>{% else %}<h2 class="editorial-heading text-3xl md:text-4xl editorial-column {{ textClass }}">{{ text }}</h2>{% endif %}

View File

@ -0,0 +1 @@
{% if src %}<figure class="editorial-figure editorial-column"><img src="{{ src }}" alt="{{ alt }}">{% if caption %}<figcaption>{{ caption }}</figcaption>{% endif %}</figure>{% endif %}

View File

@ -0,0 +1 @@
<div class="editorial-text editorial-column prose-editorial {{ class }}">{{ text|safe }}</div>

View File

@ -1,20 +0,0 @@
package main
import (
"bytes"
"context"
)
// EditorialTextBlock overrides the built-in `text` block for the editorial
// template. Body copy is Source Serif 4 within a 64ch measure, with
// continuation-paragraph indents.
//
// Content shape: { "text", "class" } — same as the built-in text block.
func EditorialTextBlock(ctx context.Context, content map[string]any) string {
text := getString(content, "text")
class := getString(content, "class")
var buf bytes.Buffer
_ = editorialTextComponent(text, class).Render(ctx, &buf)
return buf.String()
}

View File

@ -1,10 +0,0 @@
package main
// editorialTextComponent renders rich-text body copy with the editorial prose
// rules applied. The author-supplied HTML is trusted (the CMS sanitiser runs
// upstream).
templ editorialTextComponent(text, class string) {
<div class={ "editorial-text editorial-column prose-editorial", class }>
@templ.Raw(text)
</div>
}

View File

@ -1,69 +0,0 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.1020
package main
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
// editorialTextComponent renders rich-text body copy with the editorial prose
// rules applied. The author-supplied HTML is trusted (the CMS sanitiser runs
// upstream).
func editorialTextComponent(text, class string) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
var templ_7745c5c3_Var2 = []any{"editorial-text editorial-column prose-editorial", class}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var2...)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var2).String())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `text_override.templ`, Line: 1, Col: 0}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var3)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ.Raw(text).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
var _ = templruntime.GeneratedTemplate