Skip to content

feat: v3.14.0 — dedicated-route polish (closes #75)#76

Merged
vnykmshr merged 5 commits into
mainfrom
feat/v3.14.0
May 18, 2026
Merged

feat: v3.14.0 — dedicated-route polish (closes #75)#76
vnykmshr merged 5 commits into
mainfrom
feat/v3.14.0

Conversation

@vnykmshr
Copy link
Copy Markdown
Collaborator

v3.13.0 made dedicated-route content (/about, /p/:slug) structurally separate via the predicate. v3.14.0 makes it operationally first-class — pages become discoverable (sitemap + /p index + footer link), /about joins the sitemap, and the /about reach section consolidates AMA + mailto into one section voiced by the existing v3.11.0 AMA_PAGE_* env vars (closes #75).

Summary

  • Phase 1 (81ba14f): Repository.GetPages() + ArticleServiceInterface.GetPages() — published type:page articles, natural insertion order. 8-file interface ripple + 3 downstream test mocks.
  • Phase 2 (21662af): All 15 hardcoded /writing/<slug> literals across 5 files now route through articlepkg.CanonicalURLFor. RSS + JSON Feed + Sitemap all uniformly honor the dedicated-route predicate. Sitemap gains static entries for /about and /p plus a pages section. Dead seo.Helper.GenerateSitemap removed (no production call sites; feed.Service.GenerateSitemap is the live impl).
  • Phase 3 (4fbe152): /p handler + pages.html (compact list, case-insensitive alphabetical sort in the handler) + footer link + template integration test guarding against the strict-typed-template silent-truncation trap. README content-type count corrected to five; docs/design.md + project CLAUDE.md counts refreshed.
  • Phase 4 (c8d7461): /about consolidates the AMA promo + mailto-only contact into one .about-reach card. Operator voice unified across /ama overlay and /about reach via AMA_PAGE_HEADING/INTRO/SUBMIT_LABEL. ShowAbout gains a doc-comment enumeration of the full template-data contract.

Test plan

  • Lint clean: golangci-lint cache clean && make lint
  • make test-race green
  • Coverage ≥ 45% (currently 63.3%)
  • curl /sitemap.xml/about + /p + each /p/<slug> visible, no /writing/<page-slug> leak
  • curl /p — alphabetical list; HEAD /p 200
  • curl /about — reach section renders both AMA + mailto cards by default
  • Playwright sweep: nav from footer link to /p, click into a page, back-nav

Closes #75.

vnykmshr added 4 commits May 17, 2026 23:06
GetPages is the only list-shaped accessor that returns dedicated-route
content. Filters !Draft && Type == TypePage; preserves natural insertion
order (date-desc, matching siblings) — alphabetical sort belongs in the
/p index handler. Powers /p index (Phase 3) and sitemap pages section
(Phase 2).

Interface ripple touches 11 files: Repository + 4 Service shapes
(internal Service interface + CompositeService impl + ServiceAdapter
delegation + ArticleServiceInterface) + 5 downstream mocks
(FileSystemRepository / CachedRepository real impls + mockRepository in
service_test + MockArticleService in handlers_test + TestArticleService
in article_test + mockArticleService in feed_test + MockArticleService
in seo/service_test).

Test mocks return canned data per CLAUDE.md — only the repository-layer
mock filters by type (since the unit-under-test IS the filter); all
handler-layer mocks are passthrough.
…r; predicate-correct sitemap

15 hardcoded /writing/<slug> literals replaced across 5 files: feed.go
(5 sites in RSS Link/GUID, JSON Feed id/url, sitemap loc), compose.go
(5 redirect+JSON-response sites via new canonicalSlugPath helper),
taxonomy_handler.go (2 collectionSchema URLs), post_handler.go (2 sites
— canonicalPath + BlogPosting URL), seo_helper.go (1 breadcrumb in the
non-dedicated-route branch). Verified: grep '/writing/' across non-test
internal/ returns only 2 legitimate sites — predicate.go (inside
CanonicalURLFor itself) + command.go (route registration).

Sitemap gains explicit static entries for /about (0.5, yearly) and /p
(0.5, monthly), plus a pages section iterating GetPages() with each
page at /p/<slug> (0.5, yearly). /about was silently absent before
because GetAllArticles→GetPublished excludes dedicated-route content
via the predicate; pages were absent for the same reason — both now
surface explicitly. Sitemap URL count grows from 4-static+articles to
6-static+articles+pages.

Removes dead seo.Helper.GenerateSitemap (and orphan URLSet/URL types,
unused xml+sort imports) — no production call sites, only test code.
feed.Service.GenerateSitemap (served at /sitemap.xml) is the live
implementation. SEOServiceInterface narrowed accordingly. The
GenerateSitemap-error path test on disabled helper now exercises
GenerateRobotsTxt for the same disabled-fail assertion.

Extracts ogTypeWebsite constant to close the goconst regression
exposed by the file shrinking (3 "website" occurrences within seo
package: 2 in seo.go, 1 in service_test.go).
Adds the /p reader-facing index that lists published type:page articles
sorted alphabetically by title (case-insensitive). Sort is a presentation
concern in the handler; the repository returns natural insertion order
(date-desc) for symmetry with sibling list methods. New pages.html
template uses only binary or/not/eq per the markgo strict-typed
template gotcha (CLAUDE.md). Empty state copy when no pages exist.
Footer link slots between /categories and /about.

Template integration test in services package renders pages-content
against the real embedded web.Assets template with empty/single/many
inputs — guards against silent mid-render truncation that handler
status==200 checks would miss. Handler tests verify alphabetical sort
(zoo/alpha/Beta → alpha/Beta/Zoo), canonicalPath = /p, pageCount, and
HEAD support.

README content-type count corrected to five (the v3.13.0 page type was
never reflected). design.md "Three Streams" heading dropped in favor of
"Content Types"; pages discoverability sentence now mentions /p index,
footer link, sitemap, search. Project CLAUDE.md route count refreshed
~41 → ~65 (counting HEAD pairs from registerGET), handler count 11→13
(AMA+Upload), CSS 20→23, JS modules 10→19, templates 17→19.

Smoke-tested end-to-end: GET /p with 2 type:page articles renders
sorted, HEAD /p returns 200, /p/<slug> still renders, sitemap.xml has
/about + /p static entries + /p/<slug> page entries, footer link
visible on home.
Closes #75. The /about page's adjacent AMA promo + mailto-only contact
sections collapse into a single .about-reach card with two affordance
columns. AMA copy now flows from the existing v3.11.0 AMA_PAGE_HEADING /
AMA_PAGE_INTRO / AMA_SUBMIT_LABEL env vars so operators voice /ama and
/about consistently. SMTP-backed contact form (when has_contact_form)
stays untouched as a separate section — out of scope here.

ShowAbout gains a doc-comment enumeration of the full template-data
contract (identity / bio / social / contact / reach groups) — pays the
debt of the implicit data["..."] convention that previously had no
single source of truth.

AMA half always renders on /about, matching pre-v3.14.0 behavior: getEnv
treats empty as unset and falls back to the non-empty default, so
clearing AMA_PAGE_HEADING in .env can't hide it. Hiding the AMA half
requires overriding about.html via TEMPLATES_PATH; documented in
configuration.md alongside the doubled-context note on AMA_PAGE_* vars.

Tests restructured per the LastData inspection pattern (handlers_test.go
MockTemplateService) — previous status==200 assertions couldn't verify
new env-driven keys reached the template. Three rows: defaults, no
email, custom AMA copy.

docs/configuration.md: 3 stale claims corrected — Pages › Sitemap
("pages are not currently emitted" → "pages are emitted with canonical
URLs"), Pages › Future enhancements (drop /p index + auto-nav bullets
since v3.14.0 ships /p with footer link), AMA Copy table (extend
descriptions to note doubled context).
@vnykmshr
Copy link
Copy Markdown
Collaborator Author

Code review

Found 1 issue:

  1. web/static/css/pages.css uses an undefined design token (var(--spacing-lg)main.css :root defines --spacing-1 through --spacing-20 but no --spacing-lg, so it resolves to empty and .pages-index padding collapses to zero). Same file also hardcodes rem and timing values (1.25rem, 0.15s ease, 1.15rem, 0.25rem, 0.95rem) where tokens exist (--spacing-5, --transition-fast, --font-size-xl, --spacing-1, etc.). Project CLAUDE.md "Component CSS: tokens only" — sibling component CSS files (about.css, taxonomy.css) follow this discipline.

.pages-index {
padding: var(--spacing-lg) 0;
}
.pages-list {
list-style: none;
padding: 0;
margin: 0;
max-width: 42rem;
margin-left: auto;
margin-right: auto;
}
.pages-list-item {
border-bottom: 1px solid var(--color-border);
}
.pages-list-item:last-child {
border-bottom: none;
}
.pages-list-link {
display: block;
padding: 1.25rem 0;
text-decoration: none;
color: inherit;
transition: color 0.15s ease;
}
.pages-list-title {
display: block;
font-size: 1.15rem;
font-weight: 600;
margin-bottom: 0.25rem;
}
.pages-list-excerpt {
display: block;
color: var(--color-text-muted);
font-size: 0.95rem;
line-height: 1.5;
}
.pages-list-link:hover .pages-list-title,
.pages-list-link:focus .pages-list-title {
color: var(--color-primary);
}

Three issues from the automated review pass:

1. pages.css used undefined --spacing-lg token (collapsing .pages-index
   padding to zero) and hardcoded rem/timing values where design tokens
   exist. Replaced with --spacing-5, --transition-fast, --font-size-lg,
   --font-size-base, --spacing-1. Closes the CLAUDE.md "Component CSS:
   tokens only" violation.

2. compose.go HandleEdit redirected via canonicalSlugPath(slug) which
   passed a synthetic empty-Type article to CanonicalURLFor — wrong for
   existing type:page articles edited via /compose/edit/:slug. Lookup
   now hits articleService.GetArticleBySlug first; falls back to the
   empty-Type synthetic only if reload failed. Removes the needless 301
   round-trip for page authors. canonicalSlugPath becomes the method
   canonicalPathForSlug on ComposeHandler.

3. about.html previously gated the entire about-reach section on
   `not .has_contact_form`, hiding the AMA card from operators with
   full SMTP configured. Doc comment claimed "AMA always renders" but
   template contradicted it. Restructured: AMA card now renders
   unconditionally inside about-reach; only the mailto card stays
   gated on `has_contact && not has_contact_form`. id="contact" stays
   on about-reach (the primary /contact landing target); the SMTP form
   section gets id="contact-form" to deduplicate. Test gains a
   regression-guard row for the SMTP-configured case.
@vnykmshr vnykmshr merged commit 6292c84 into main May 18, 2026
6 checks passed
@vnykmshr vnykmshr deleted the feat/v3.14.0 branch May 18, 2026 03:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(about): env-driven /about reach section, unified AMA + contact

1 participant