hide drafts and future-dated bundles; reveal them with -dev on

A draft is now not served at all, and neither is a bundle whose date has not
arrived. The filter sits in `Site.Lookup` and `Site.Run`, which is every path to a
bundle — so the files inside an unpublished bundle inherit its status for free,
which is what ADR-0024 asks for and what the asset route was written to allow. A
test asserts the 404 for the bundle *and* its picture, and that nothing leaks into
a listing, a feed or a sitemap.

The clock is read per request rather than at startup, so a scheduled post appears
exactly when its date arrives with nothing to restart and nothing to invalidate.
That first clock read created internal/content/clock.go, which is the only place
`verify.sh` allows `time.Now` — a render that depends on the time is worth being
able to find.

`-dev on` reveals both and reparses the theme before each render. Deliberately not
a bare boolean flag: turning unpublished work into public work should not be one
fumbled argument away. A reload that fails to parse leaves the working template set
in place, so a typo shows an error rather than replacing a good set with a broken one.
This commit is contained in:
Claude Opus 5
2026-07-31 12:47:44 +06:00
committed by bdeshi
parent b574800adb
commit ab882980ad
8 changed files with 203 additions and 14 deletions
+29 -1
View File
@@ -121,6 +121,12 @@ type Renderer struct {
files fs.FS
// settings are the site's declarations, constant for the life of the process.
settings content.Settings
// reload reparses the theme before each render, for `-dev`: editing a template should not need a restart.
// Off in a serving build, where parsing once is the point (conventions.md).
reload bool
// siteFS and extend are kept only so reload can rebuild what New built.
siteFS fs.FS
extend func(Partial) []goldmark.Extender
}
// Partial renders a named fragment. A feature under internal/ext is handed one of these at wiring time,
@@ -204,7 +210,8 @@ func New(siteFS fs.FS, settings content.Settings, extend func(Partial) []goldmar
if err != nil {
return nil, err
}
r := &Renderer{page: page, list: list, partials: partials, style: css, files: siteFS, settings: settings}
r := &Renderer{page: page, list: list, partials: partials, style: css, files: siteFS, settings: settings,
siteFS: siteFS, extend: extend}
// The typographer smooths quotes, dashes and ellipses in authored prose and leaves code spans alone,
// because it works on the parsed tree rather than the text. That is the only change the engine makes to
// an author's words (ADR-0034), and it is a parser option rather than a render transform, so it does
@@ -239,6 +246,24 @@ func (r *Renderer) absolute(path string) string {
return content.Absolute(r.settings.Base, path)
}
// Reload makes every render reparse the theme first. For `-dev` only: it trades the parse-once rule for the
// ability to edit a template and refresh.
func (r *Renderer) Reload() { r.reload = true }
// fresh reparses the theme when reloading, and reports a failure without disturbing the working renderer — a
// template with a typo in it should show an error page, not replace a good set with a broken one.
func (r *Renderer) fresh() error {
if !r.reload {
return nil
}
next, err := New(r.siteFS, r.settings, r.extend)
if err != nil {
return err
}
r.page, r.list, r.partials, r.style, r.md = next.page, next.list, next.partials, next.style, next.md
return nil
}
// Partial renders one named fragment. A missing template is an error the caller degrades on, never a
// failed request (extensions.md rule 5).
func (r *Renderer) Partial(name string, data Fragment) ([]byte, error) {
@@ -327,6 +352,9 @@ func (r *Renderer) Bundle(b content.Bundle, served string, variants []string, se
// Listing renders one page of a Query result for a section.
func (r *Renderer) Listing(section, lang string, all []content.Bundle, page int) ([]byte, error) {
if err := r.fresh(); err != nil {
return nil, err
}
l, window := r.paginate(section, lang, content.PageURL(section, lang, page), all, page,
func(p int) string { return content.PageURL(section, lang, p) })
for _, b := range window {