Skip to content

Commit 8945da5

Browse files
authored
fix: add support for unlisted pages (#106)
1 parent 32efb47 commit 8945da5

5 files changed

Lines changed: 141 additions & 5 deletions

File tree

docs/Configuration.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,7 @@ Per-page options set in YAML frontmatter:
206206
| `author` | `str` | Page author (used in Atom feed, overrides site-level feed author) |
207207
| `subtitle` | `str` | Subtitle shown below the page title, in folder indexes, and tag indexes |
208208
| `show_index` | `bool` | For `index.md` files: render page content + auto-generated folder listing |
209+
| `unlisted` | `bool` | Hide page from sidebar navigation and folder indexes (still accessible by URL) |
209210

210211
## CLI Overrides
211212

src/rockgarden/nav/folder_index.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,9 @@ def generate_folder_indexes(
8888
for folder_path in sorted(folders):
8989
if _should_hide(folder_path, config.hide):
9090
continue
91+
index_page = existing_indexes.get(folder_path)
92+
if index_page and index_page.frontmatter.get("unlisted", False):
93+
continue
9194

9295
children = _get_folder_children(
9396
folder_path, pages, config, clean_urls, base_path
@@ -204,7 +207,9 @@ def _get_folder_children(
204207
if len(parts) == 1:
205208
if parts[0] == "index":
206209
continue
207-
if _should_hide(page.slug, config.hide):
210+
if _should_hide(page.slug, config.hide) or page.frontmatter.get(
211+
"unlisted", False
212+
):
208213
continue
209214

210215
modified = None
@@ -229,6 +234,11 @@ def _get_folder_children(
229234
if subfolder_path not in seen_subfolders:
230235
if _should_hide(subfolder_path, config.hide):
231236
continue
237+
subfolder_index = folder_index_pages.get(subfolder_path)
238+
if subfolder_index and subfolder_index.frontmatter.get(
239+
"unlisted", False
240+
):
241+
continue
232242

233243
seen_subfolders.add(subfolder_path)
234244
label = resolve_label(subfolder_path, subfolder, config.labels)

src/rockgarden/nav/tree.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,9 @@ def build_nav_tree(
124124
tree: dict[str, dict] = {}
125125

126126
for page in pages:
127-
if _should_hide(page.slug, config.hide):
127+
if _should_hide(page.slug, config.hide) or page.frontmatter.get(
128+
"unlisted", False
129+
):
128130
continue
129131

130132
parts = page.slug.split("/")
@@ -136,7 +138,10 @@ def build_nav_tree(
136138
current_path_parts.append(part)
137139
folder_path = "/".join(current_path_parts)
138140

139-
if _should_hide(folder_path, config.hide):
141+
folder_page = folder_pages.get(folder_path)
142+
if _should_hide(folder_path, config.hide) or (
143+
folder_page and folder_page.frontmatter.get("unlisted", False)
144+
):
140145
break
141146

142147
if part not in current:
@@ -148,8 +153,7 @@ def build_nav_tree(
148153
"_original_name": original_name,
149154
}
150155
current = current[part]["_children"]
151-
152-
if not _should_hide(page.slug, config.hide):
156+
else:
153157
page_name = parts[-1]
154158
if page_name != "index":
155159
current[page_name] = {

tests/test_folder_index.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -269,3 +269,52 @@ def test_folder_note_not_in_children(self):
269269
assert "City of Fairshore" not in child_titles
270270
assert "The Salty Dog" in child_titles
271271
assert "Market Square" in child_titles
272+
273+
274+
class TestUnlistedFolderIndex:
275+
"""Test that unlisted pages are excluded from folder index children."""
276+
277+
def test_unlisted_page_hidden_from_folder_children(self):
278+
pages = [
279+
make_page("docs/visible", "Visible"),
280+
make_page("docs/secret", "Secret", unlisted=True),
281+
]
282+
indexes = generate_folder_indexes(pages)
283+
docs = next(fi for fi in indexes if fi.slug == "docs/index")
284+
child_titles = [c.title for c in docs.children]
285+
assert "Visible" in child_titles
286+
assert "Secret" not in child_titles
287+
288+
def test_unlisted_folder_hidden_from_parent_index(self):
289+
"""Folder with unlisted index page not shown in parent's children."""
290+
pages = [
291+
make_page("docs/public", "Public"),
292+
Page(
293+
source_path=Path("/vault/docs/secret/index.md"),
294+
slug="docs/secret/index",
295+
frontmatter={"title": "Secret Folder", "unlisted": True},
296+
content="",
297+
),
298+
make_page("docs/secret/inner", "Inner Page"),
299+
]
300+
indexes = generate_folder_indexes(pages)
301+
docs = next(fi for fi in indexes if fi.slug == "docs/index")
302+
child_titles = [c.title for c in docs.children]
303+
assert "Public" in child_titles
304+
assert "Secret Folder" not in child_titles
305+
assert "secret" not in [c.title.lower() for c in docs.children]
306+
307+
def test_unlisted_folder_not_in_generated_indexes(self):
308+
"""Folder with unlisted index should not get a generated folder index."""
309+
pages = [
310+
Page(
311+
source_path=Path("/vault/secret/index.md"),
312+
slug="secret/index",
313+
frontmatter={"title": "Secret", "unlisted": True},
314+
content="",
315+
),
316+
make_page("secret/inner", "Inner"),
317+
]
318+
indexes = generate_folder_indexes(pages)
319+
index_slugs = [fi.slug for fi in indexes]
320+
assert "secret/index" not in index_slugs

tests/test_nav_tree.py

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -523,3 +523,75 @@ def test_folder_note_not_duplicate_child(self):
523523
assert "Fairshore" not in child_labels
524524
assert "The Salty Dog" in child_labels
525525
assert "Market Square" in child_labels
526+
527+
528+
class TestUnlistedPages:
529+
"""Test that unlisted frontmatter hides pages from nav."""
530+
531+
def test_unlisted_page_hidden_from_nav(self):
532+
pages = [
533+
make_page("about", "About"),
534+
Page(
535+
source_path=Path("/vault/secret.md"),
536+
slug="secret",
537+
frontmatter={"title": "Secret", "unlisted": True},
538+
content="",
539+
),
540+
]
541+
tree = build_nav_tree(pages)
542+
labels = [c.label for c in tree.children]
543+
assert "About" in labels
544+
assert "Secret" not in labels
545+
546+
def test_unlisted_page_folder_still_shown(self):
547+
pages = [
548+
Page(
549+
source_path=Path("/vault/docs/hidden.md"),
550+
slug="docs/hidden",
551+
frontmatter={"title": "Hidden", "unlisted": True},
552+
content="",
553+
),
554+
make_page("docs/visible", "Visible"),
555+
]
556+
tree = build_nav_tree(pages)
557+
docs = tree.children[0]
558+
assert docs.is_folder
559+
child_labels = [c.label for c in docs.children]
560+
assert "Visible" in child_labels
561+
assert "Hidden" not in child_labels
562+
563+
def test_unlisted_false_still_shown(self):
564+
pages = [
565+
Page(
566+
source_path=Path("/vault/page.md"),
567+
slug="page",
568+
frontmatter={"title": "Page", "unlisted": False},
569+
content="",
570+
),
571+
]
572+
tree = build_nav_tree(pages)
573+
labels = [c.label for c in tree.children]
574+
assert "Page" in labels
575+
576+
def test_unlisted_folder_hidden_from_nav(self):
577+
"""Folder with unlisted index page should not appear in nav."""
578+
pages = [
579+
Page(
580+
source_path=Path("/vault/secret/index.md"),
581+
slug="secret/index",
582+
frontmatter={"title": "Secret", "unlisted": True},
583+
content="",
584+
),
585+
Page(
586+
source_path=Path("/vault/secret/details.md"),
587+
slug="secret/details",
588+
frontmatter={"title": "Details"},
589+
content="",
590+
),
591+
make_page("public", "Public"),
592+
]
593+
tree = build_nav_tree(pages)
594+
labels = [c.label for c in tree.children]
595+
assert "Public" in labels
596+
assert "secret" not in [c.label.lower() for c in tree.children]
597+
assert "Details" not in labels

0 commit comments

Comments
 (0)