feat: optional series pages defined in a site-wide series.yml (#107) - #108
harrydayexe wants to merge 5 commits into
Conversation
Automated reviewI read the full diff plus the surrounding generator, outputter, server, watcher, template, and CLI code, ran the suite (all green), and exercised the feature end-to-end through the The design is solid and matches the issue closely: opt-in via
Agent responseAll three points addressed; pushed as 1. Empty meta description / dead I went with the un-shadowing fix rather than the reviewer's "simplest" suggestion of setting both fields to 2. Series with slug I did not extend the same check to tags — it's pre-existing behaviour outside this PR, and changing it would turn currently-building sites into failing ones as a side effect of a series PR. 3. Unknown-key test passing for the wrong reason — fixed. One unrelated observation while testing: Posted by rain. Both halves were written by Claude Code; treat them as a starting point for human review, not a substitute for it. |
Series are named, ordered collections of posts defined in one site-wide YAML file rather than in post front matter, so renaming a series or reordering its parts is a single-file edit. A post belongs to at most one series and the order is explicit, not by date. The feature is opt-in: it is off unless a series file is supplied via config.WithSeriesFile or --series-file. Every rule the file must satisfy is a hard error naming the series and the offending value, so a typo fails generation rather than silently dropping content. - pkg/config: SeriesFile type + WithSeriesFile, and WatchFiles + WithWatchFile so the watcher can react to a non-markdown file - pkg/generator: parse and validate the file, render series and series-index pages, attach PostPageData.Series, expose both on GeneratedBlog - pkg/models: SeriesInfo, SeriesIndexPageData, SeriesPageData, PostSeries, BaseData.SeriesEnabled, and an exported Slugify for deriving series slugs - pkg/outputter: write series/ only when series pages were generated - pkg/server: register /series routes only when series content exists - pkg/templates: series and series-index pages, a post series box, and a nav link, all guarded so output with series disabled is byte-identical - CLI: shared --series-file flag on generate and serve, failing fast when the path cannot be read, and watched in serve --watch Refs #107
Adds a README section covering the format and all three ways to enable it (CLI, Docker, library), doc.go entries for WithSeriesFile and WithWatchFile, the new page paths, and the validation rules. The just serve recipes now pass --series-file so the example posts demonstrate the feature. Also extracts Generator.parsePosts so the parsing step can be reused. Refs #107
Covers the acceptance cases from the issue: the disabled default, an empty series list, file order beating date order, derived and explicit slugs, every validation error, the post page's position/prev/next fields, paths under a blog root and with HTML paths, independence from --disable-tags, the outputter's series directory, the server's /series routes, and serve reloading a reordered or newly invalid series file. Refs #107
addlicense checks .yml files, so the example series file needs the same MPL header the other YAML files carry.
…ex slug SeriesPageData.Description shadowed the embedded BaseData.Description that partials/head.tmpl renders, so a series with no description in the series file produced an empty meta description, og:description and JSON-LD description, and seriesDescription's fallback never reached a page. Rename the page-level field to SeriesDescription so the two no longer collide: the body still shows only an author-written description, while the meta tags get the fallback. A series whose slug resolved to "index" was also silently lost: the outputter writes series/index.html for the series index after the per-series pages, so the series page was overwritten with no error, while serve happily served it. Reject that slug during validation, in the same style as the slug-collision error. Also tighten the unknown-key validation test, which asserted only on "post" and so would still have passed if KnownFields(true) were dropped and the series failed for lacking posts instead. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
36e125e to
117f864
Compare
Closes #107
Note
This pull request was written by rain running Claude Code. It has not been reviewed by a human. CI and an automated review pass run before it is handed over.
Issue
#107: feat: optional series pages defined in a site-wide series.yml
What the agent reports
Pushed. Here's the handover.
What I built
Optional series pages driven by one site-wide YAML file, opt-in via
--series-file/config.WithSeriesFile(fsys, path).Config (
pkg/config) —SeriesFile{FS, Path}+WithSeriesFile, embedded inGeneratorandDirectoryWriterand applied in the option chains (the server forwards it throughcfg.Gen). AlsoWatchFiles+WithWatchFile, a watcher option for reacting to a single non-markdown file.Generator (
pkg/generator/series.go) — reads the file through thefs.FS, decodes withyaml.v3+KnownFields(true), and validates: missing/empty name, missing/empty posts, unknown filename, post in two series, post twice in one series, colliding slugs, empty-after-slugify slug, malformed YAML, missing top-levelserieskey, unknown keys. Every message names the series and the offending value. Posts resolve bySourcePath(filename), not slug. Rendersseries.tmpl/series-index.tmplinto newGeneratedBlog.Series/.SeriesIndex, and attachesPostPageData.Series(models.PostSeries: name, slug, description, path, ordered posts, 1-based position, total, prev/next).Outputter / server / watcher / CLI —
series/written only when series pages exist and never under--raw;/seriesand/series/{slug}registered only when series content exists (both clean and.htmlURLs);serve --watchwatches the series file explicitly;--series-fileis validated up front and fails with the existingFlagErrorstyle.Templates — new series and series-index pages, a "Part N of M" box with prev/next on post pages, and a
SeriesEnabled-gated nav link. Trim markers keep disabled output unchanged.Docs & example — README section (CLI/Docker/library),
pkg/config/doc.go,pkg/generator/doc.go,pkg/models/doc.go,ExampleWithSeriesFile, a three-part example series plusdocs/example-posts/series.yml, andjust run-serve/run-imagenow pass the flag.Verification
go build ./...,go vet ./...,go test ./...,go test -race ./...,gofmt -l .,addlicense -check ./— all clean. I also generated the example site with and without the flag: with series disabled, the output is byte-identical to the same command run againstmain(only the three newly added example posts show up in listings;posts/getting-started-with-go.htmldiffs empty). I could not run a liveservesmoke test — the sandbox denied the command — so routing is covered byhttptesttests instead.Worth a close look
HTMLPaths. The issue's prose saysBaseData.Pathshould be/series.html, but the file written isseries/index.html, so that path would 404. I followed the existing tags-index convention instead:/series/index.html(the single-series path is/series/{slug}.htmlas specified). This is the one deliberate deviation from the issue text; the existing comment inpagePathexplains the same reasoning for tags.sitemap.xml. The issue enumerates outputs and doesn't mention the sitemap, so I kept scope tight — but it's a plausible follow-up.field post not found in type generator.seriesDefinition), which leaks the internal type name. It names the bad key, which is what rule 14 requires, but it could be prettified.models.Slugifyis newly exported (was unexportedslugify) so series slugs use the identical rules as post titles.pages/series.tmplfails generation once series are enabled, with the template named. Open questions 2 (reject unknown keys) and 3 (file order) follow the issue's suggestions.Changelog (#108)
✨ New Features
🐛 Bug Fixes
📚 Documentation
🧪 Tests
🧹 Chores