A pass over the content at startup writes three widths per picture into a cache outside the site root, named by the source's content hash and the width (ADR-0042). Idempotent by construction: a rerun stats and skips, an edited picture takes a new name, and nothing stale can be served under an old one. Restarting the evidence site made 0 derivatives the second time, as it should. Ahead of the request rather than during it, because resampling is felt and there is no page cache yet to hide it. Outside the site root, because the engine reads that directory and must not leave generated files in somebody's content git — a lost cache costs one startup pass and no correctness. Markup now carries the original as src, the derivatives as srcset closed by the original at its own width, and width/height from the original — which retires most of the latent row about the output floor; only a gallery's alt is still empty, and a filename cannot supply that. Two things the work itself decided: `Fragment.Items` became `Fragment.Pictures`, ADR-0037's own revisit trigger. Items had one consumer, so widening it beat adding a second list beside it. "A browser can show it" and "we can resample it" are different questions, and conflating them nearly deleted content: an SVG has no decoder here, so a single predicate would have dropped SVGs from galleries silently. Undecodable and unsupported pictures are now rendered as they are, without a size or a srcset.
157 lines
6.4 KiB
Go
157 lines
6.4 KiB
Go
package web
|
|
|
|
import (
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
"testing/fstest"
|
|
|
|
"khosra/internal/content"
|
|
"khosra/internal/render"
|
|
)
|
|
|
|
func slugHandler(t *testing.T, fsys fstest.MapFS) http.Handler {
|
|
t.Helper()
|
|
bundles, err := content.Scan(fsys)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
r, err := render.New(nil, content.Settings{}, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return Handler(content.NewSite(bundles), r, fsys, nil, content.Settings{})
|
|
}
|
|
|
|
func TestASlugRenamesTheAddressInEveryLanguage(t *testing.T) {
|
|
h := slugHandler(t, fstest.MapFS{
|
|
"content/posts/hello-world.md": {Data: []byte("---\ntitle: Hello\nslug: ekti-post\n---\nEnglish.\n")},
|
|
"content/posts/hello-world.bn.md": {Data: []byte("---\ntitle: একটি পোস্ট\n---\nবাংলা।\n")},
|
|
})
|
|
for _, path := range []string{"/posts/ekti-post/", "/bn/posts/ekti-post/"} {
|
|
rec := httptest.NewRecorder()
|
|
h.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, path, nil))
|
|
if rec.Code != http.StatusOK {
|
|
t.Errorf("GET %s = %d, want 200 — a slug renames every language (ADR-0035)", path, rec.Code)
|
|
}
|
|
}
|
|
// The old path is gone: the engine serves the new one only, and an author who wants both writes an alias.
|
|
rec := httptest.NewRecorder()
|
|
h.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/posts/hello-world/", nil))
|
|
if rec.Code != http.StatusNotFound {
|
|
t.Errorf("the derived path = %d, want 404", rec.Code)
|
|
}
|
|
// Canonical and hreflang must name the new address, not the key.
|
|
rec = httptest.NewRecorder()
|
|
h.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/bn/posts/ekti-post/", nil))
|
|
body := rec.Body.String()
|
|
if !strings.Contains(body, `rel="canonical" href="/bn/posts/ekti-post/"`) {
|
|
t.Errorf("canonical should name the served address:\n%s", body)
|
|
}
|
|
if strings.Contains(body, "hello-world") {
|
|
t.Errorf("nothing the engine emits should still say hello-world:\n%s", body)
|
|
}
|
|
}
|
|
|
|
func TestAnAliasCanKeepTheOldPathWorking(t *testing.T) {
|
|
h := slugHandler(t, fstest.MapFS{
|
|
"content/posts/hello-world.md": {Data: []byte("---\ntitle: Hello\nslug: ekti-post\naliases: [posts/hello-world]\n---\nx\n")},
|
|
})
|
|
rec := httptest.NewRecorder()
|
|
h.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/posts/hello-world/", nil))
|
|
if rec.Code != http.StatusMovedPermanently {
|
|
t.Fatalf("got %d, want 301 — this is how a rename keeps its promise (ADR-0008)", rec.Code)
|
|
}
|
|
if loc := rec.Header().Get("Location"); loc != "/posts/ekti-post/" {
|
|
t.Errorf("Location = %q, want the new address", loc)
|
|
}
|
|
}
|
|
|
|
func TestListingsAndSitemapsUseTheSluggedAddress(t *testing.T) {
|
|
fsys := fstest.MapFS{
|
|
"content/posts/hello-world.md": {Data: []byte("---\ntitle: Hello\ndate: 2026-03-01\nslug: ekti-post\n---\nx\n")},
|
|
"content/posts/plain.md": {Data: []byte("---\ntitle: Plain\ndate: 2026-02-01\n---\nx\n")},
|
|
}
|
|
bundles, err := content.Scan(fsys)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
settings := content.Settings{Base: "https://khosra.example"}
|
|
r, err := render.New(nil, settings, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
h := Handler(content.NewSite(bundles), r, fsys, nil, settings)
|
|
|
|
rec := httptest.NewRecorder()
|
|
h.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/posts/", nil))
|
|
if body := rec.Body.String(); !strings.Contains(body, `href="/posts/ekti-post/"`) || strings.Contains(body, "hello-world") {
|
|
t.Errorf("a listing must link the address, not the key:\n%s", body)
|
|
}
|
|
rec = httptest.NewRecorder()
|
|
h.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/sitemap.xml", nil))
|
|
if body := rec.Body.String(); !strings.Contains(body, "/posts/ekti-post/") || strings.Contains(body, "hello-world") {
|
|
t.Errorf("a sitemap that disagrees with what is served is worse than none:\n%s", body)
|
|
}
|
|
}
|
|
|
|
func TestAmbiguousOrCollidingSlugsAreDropped(t *testing.T) {
|
|
// Both cases keep the derived path rather than picking a winner, the same rule aliases follow.
|
|
h := slugHandler(t, fstest.MapFS{
|
|
// Variants disagree.
|
|
"content/posts/a.md": {Data: []byte("---\ntitle: A\nslug: one\n---\nx\n")},
|
|
"content/posts/a.bn.md": {Data: []byte("---\ntitle: ক\nslug: two\n---\nx\n")},
|
|
// Slug lands on a bundle that already exists.
|
|
"content/posts/b.md": {Data: []byte("---\ntitle: B\nslug: taken\n---\nx\n")},
|
|
"content/posts/taken.md": {Data: []byte("---\ntitle: Taken\n---\nx\n")},
|
|
})
|
|
for path, want := range map[string]int{
|
|
"/posts/a/": http.StatusOK, // kept its derived path
|
|
"/posts/one/": http.StatusNotFound, // neither declared slug won
|
|
"/posts/two/": http.StatusNotFound,
|
|
"/posts/b/": http.StatusOK,
|
|
"/posts/taken/": http.StatusOK, // the bundle already there keeps its URL
|
|
} {
|
|
rec := httptest.NewRecorder()
|
|
h.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, path, nil))
|
|
if rec.Code != want {
|
|
t.Errorf("GET %s = %d, want %d", path, rec.Code, want)
|
|
}
|
|
}
|
|
rec := httptest.NewRecorder()
|
|
h.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/posts/taken/", nil))
|
|
if body := rec.Body.String(); !strings.Contains(body, "<h1>Taken</h1>") {
|
|
t.Errorf("the bundle already there must still be the one served:\n%s", body)
|
|
}
|
|
}
|
|
|
|
func TestASeriesSurvivesItsLandingBeingSlugged(t *testing.T) {
|
|
// The reason a slug does not touch the key: membership is the directory (ADR-0033), so renaming the
|
|
// landing page's address must not orphan its chapters.
|
|
h := slugHandler(t, fstest.MapFS{
|
|
"content/comics/the-long-monsoon/_index.md": {Data: []byte("---\ntitle: The Long Monsoon\nslug: monsoon\n---\nx\n")},
|
|
"content/comics/the-long-monsoon/first-rain.md": {Data: []byte("---\ntitle: First Rain\norder: 10\n---\nx\n")},
|
|
"content/comics/the-long-monsoon/the-flood.md": {Data: []byte("---\ntitle: The Flood\norder: 20\n---\nx\n")},
|
|
})
|
|
rec := httptest.NewRecorder()
|
|
h.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/comics/monsoon/", nil))
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("the landing page = %d, want 200 at its new address", rec.Code)
|
|
}
|
|
body := rec.Body.String()
|
|
for _, want := range []string{"First Rain", "The Flood"} {
|
|
if !strings.Contains(body, want) {
|
|
t.Errorf("the archive lost %q — a slug must not orphan a series:\n%s", want, body)
|
|
}
|
|
}
|
|
rec = httptest.NewRecorder()
|
|
h.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/comics/the-long-monsoon/the-flood/", nil))
|
|
if rec.Code != http.StatusOK {
|
|
t.Errorf("a chapter = %d, want 200: chapters keep their own addresses", rec.Code)
|
|
}
|
|
if body := rec.Body.String(); !strings.Contains(body, `href="/comics/monsoon/"`) {
|
|
t.Errorf("the chapter should link its series at the slugged address:\n%s", body)
|
|
}
|
|
}
|