a .md file starting with _ is a fragment, not a bundle

Found by serving the include evidence: `tools.md` beside a bundle's index was
itself scanned as a bundle, so a file meant only to be included took a URL of its
own, appeared in its section's listing, and turned the including bundle into a
one-member series. The real binary showed the phantom series nav; no test would
have, because every fixture happened to name its partials differently.

The rule mirrors the one directories already have, `_index` excepted since that
names its directory. `{{< include file="_tools.md" >}}` is now the shape to write.
This commit is contained in:
2026-08-01 02:23:35 +06:00
parent 7102cf0d44
commit 5e574b038f
4 changed files with 55 additions and 4 deletions
+16
View File
@@ -84,6 +84,8 @@ func Scan(fsys fs.FS) ([]Bundle, error) {
return nil
case !strings.HasSuffix(p, ".md"):
return nil
case isPartial(path.Base(p)):
return nil
}
data, err := fs.ReadFile(fsys, p)
if err != nil {
@@ -258,6 +260,20 @@ func isLangTag(s string) bool {
return true
}
// isPartial reports whether a filename is a fragment rather than a bundle of its own.
//
// An underscore prefix, the same mark a directory already uses, with `_index` excepted because that names
// the directory it sits in. Without this a file meant only to be included would also be a bundle: it would
// take a URL, appear in its section's listing, and turn its bundle into a one-member series
// (content-model.md).
func isPartial(base string) bool {
name := strings.TrimSuffix(base, ".md")
if i := strings.LastIndex(name, "."); i > 0 && isLangTag(name[i+1:]) {
name = name[:i]
}
return strings.HasPrefix(name, "_") && name != "_index"
}
// skipDir reports whether a directory is not content: hidden, or underscore-prefixed.
func skipDir(base string) bool {
return strings.HasPrefix(base, ".") && base != "." || strings.HasPrefix(base, "_")
+30
View File
@@ -4,6 +4,7 @@ import (
"io/fs"
"os"
"path/filepath"
"sort"
"testing"
"testing/fstest"
)
@@ -173,3 +174,32 @@ func TestTagSlugPreservesScriptAndFoldsCase(t *testing.T) {
}
}
}
func TestAnUnderscoreFileIsAPartialNotABundle(t *testing.T) {
// A file meant only to be included must not also be a bundle: it would take a URL, show up in its
// section's listing, and make its bundle look like a one-member series (content-model.md).
fsys := fstest.MapFS{
"content/pages/about/index.md": {Data: []byte("---\ntitle: About\n---\nx\n")},
"content/pages/about/_tools.md": {Data: []byte("A fragment.\n")},
"content/pages/about/_notes.bn.md": {Data: []byte("একটি অংশ।\n")},
"content/pages/_index.md": {Data: []byte("---\ntitle: Pages\n---\n")},
}
bundles, err := Scan(fsys)
if err != nil {
t.Fatal(err)
}
var keys []string
for _, b := range bundles {
keys = append(keys, b.Key)
}
sort.Strings(keys)
want := []string{"pages", "pages/about"}
if len(keys) != len(want) {
t.Fatalf("scanned %v, want %v — _index still names its directory", keys, want)
}
for i := range want {
if keys[i] != want[i] {
t.Fatalf("scanned %v, want %v", keys, want)
}
}
}