feat: exclude unlisted pages from folder discovery and listings

Respect the file.data.unlisted convention in both the folder discovery
loop (virtual folder index generation in pageType.generate) and the
folder listing itself (pagesFromAllFiles and pagesFromTrie in
FolderContent).

Unlisted pages no longer contribute folders or subfolders, and no longer
appear as entries inside any folder page listing. The foldersWithIndex
detection loop also skips unlisted pages so a user-authored unlisted
index file does not bypass the filter.

Exports pagesFromAllFiles so its filter logic can be tested directly.
Adds 5 unit tests covering unlisted exclusion, explicit unlisted: false
passthrough, subfolder grouping, and folder-index self-exclusion.
This commit is contained in:
saberzero1 2026-04-11 13:50:43 +02:00
parent 8dcc63d340
commit 1c2dce604c
No known key found for this signature in database
8 changed files with 86 additions and 10 deletions

View file

@ -9,4 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- Respect the `file.data.unlisted` convention in both folder discovery (virtual page generation) and folder listings (the page-list inside a folder index). Unlisted pages are never shown in folder listings and never contribute folder/subfolder entries.
- Exported `pagesFromAllFiles` helper for reuse and testing.
- Unit tests for `pagesFromAllFiles` covering the unlisted-filter behavior.
- Initial Quartz community plugin template.

View file

@ -2667,7 +2667,10 @@ function concatenateResources(...resources) {
function pagesFromTrie(folder, showSubfolders) {
return folder.children.map((node) => {
const nodeData = node.data;
if (nodeData) return nodeData;
if (nodeData) {
if (nodeData.unlisted === true) return void 0;
return nodeData;
}
if (node.isFolder && showSubfolders) {
return {
slug: node.slug,
@ -2683,6 +2686,7 @@ function pagesFromAllFiles(allFiles, folderSlug, showSubfolders) {
const directChildren = [];
const subfolderFiles = /* @__PURE__ */ new Map();
for (const file of allFiles) {
if (file.unlisted === true) continue;
const fileSlug = file.slug;
if (!fileSlug || !fileSlug.startsWith(folderPrefix)) continue;
const relativePath = fileSlug.slice(folderPrefix.length);

File diff suppressed because one or more lines are too long

12
dist/index.js vendored
View file

@ -2652,7 +2652,10 @@ function concatenateResources(...resources) {
function pagesFromTrie(folder, showSubfolders) {
return folder.children.map((node) => {
const nodeData = node.data;
if (nodeData) return nodeData;
if (nodeData) {
if (nodeData.unlisted === true) return void 0;
return nodeData;
}
if (node.isFolder && showSubfolders) {
return {
slug: node.slug,
@ -2668,6 +2671,7 @@ function pagesFromAllFiles(allFiles, folderSlug, showSubfolders) {
const directChildren = [];
const subfolderFiles = /* @__PURE__ */ new Map();
for (const file of allFiles) {
if (file.unlisted === true) continue;
const fileSlug = file.slug;
if (!fileSlug || !fileSlug.startsWith(folderPrefix)) continue;
const relativePath = fileSlug.slice(folderPrefix.length);
@ -2787,7 +2791,7 @@ var FolderPage = (opts) => {
priority: 10,
match: folderMatcher,
generate({ content, cfg }) {
const allFiles = content.map((c) => c[1].data);
const allFiles = content.map((c) => c[1].data).filter((d) => d?.unlisted !== true);
const locale = cfg?.locale ?? "en-US";
const folders = /* @__PURE__ */ new Set();
const folderDisplayNames = /* @__PURE__ */ new Map();
@ -2813,7 +2817,9 @@ var FolderPage = (opts) => {
}
const foldersWithIndex = /* @__PURE__ */ new Set();
for (const [, file] of content) {
const slug2 = file.data?.slug;
const data = file.data;
if (data?.unlisted === true) continue;
const slug2 = data?.slug;
if (slug2 && slug2.endsWith("/index")) {
const folder = slug2.slice(0, -"/index".length);
foldersWithIndex.add(folder);

2
dist/index.js.map vendored

File diff suppressed because one or more lines are too long

View file

@ -33,6 +33,7 @@ interface TrieNode {
type PageEntry = {
slug?: string;
unlisted?: boolean;
dates?: { created: Date; modified: Date; published: Date };
frontmatter?: { title?: string; tags?: string[] };
};
@ -48,7 +49,10 @@ function pagesFromTrie(folder: TrieNode, showSubfolders: boolean): PageEntry[] {
return folder.children
.map((node) => {
const nodeData = node.data as PageEntry | null;
if (nodeData) return nodeData;
if (nodeData) {
if (nodeData.unlisted === true) return undefined;
return nodeData;
}
if (node.isFolder && showSubfolders) {
return {
@ -62,7 +66,7 @@ function pagesFromTrie(folder: TrieNode, showSubfolders: boolean): PageEntry[] {
.filter((page): page is PageEntry => page !== undefined);
}
function pagesFromAllFiles(
export function pagesFromAllFiles(
allFiles: unknown[],
folderSlug: string,
showSubfolders: boolean,
@ -77,6 +81,7 @@ function pagesFromAllFiles(
const subfolderFiles = new Map<string, PageEntry[]>();
for (const file of allFiles as PageEntry[]) {
if (file.unlisted === true) continue;
const fileSlug = file.slug;
if (!fileSlug || !fileSlug.startsWith(folderPrefix)) continue;

View file

@ -41,7 +41,9 @@ export const FolderPage: QuartzPageTypePlugin<FolderPageOptions> = (opts) => {
priority: 10,
match: folderMatcher,
generate({ content, cfg }) {
const allFiles = content.map((c) => c[1].data);
const allFiles = content
.map((c) => c[1].data)
.filter((d) => (d as { unlisted?: unknown } | undefined)?.unlisted !== true);
const locale = (cfg as { locale?: string } | undefined)?.locale ?? "en-US";
const folders = new Set<string>();
@ -76,7 +78,9 @@ export const FolderPage: QuartzPageTypePlugin<FolderPageOptions> = (opts) => {
const foldersWithIndex = new Set<string>();
for (const [, file] of content) {
const slug = (file.data as { slug?: string } | undefined)?.slug;
const data = file.data as { slug?: string; unlisted?: unknown } | undefined;
if (data?.unlisted === true) continue;
const slug = data?.slug;
if (slug && slug.endsWith("/index")) {
const folder = slug.slice(0, -"/index".length);
foldersWithIndex.add(folder);

View file

@ -0,0 +1,53 @@
import { describe, expect, it } from "vitest";
import { pagesFromAllFiles } from "../src/components/FolderContent";
describe("pagesFromAllFiles", () => {
it("returns direct children of the requested folder", () => {
const allFiles = [
{ slug: "notes/a", frontmatter: { title: "A" } },
{ slug: "notes/b", frontmatter: { title: "B" } },
{ slug: "other/c", frontmatter: { title: "C" } },
];
const result = pagesFromAllFiles(allFiles, "notes/index", true);
expect(result.map((r) => r.slug).sort()).toEqual(["notes/a", "notes/b"]);
});
it("excludes unlisted pages from folder listings", () => {
const allFiles = [
{ slug: "notes/public", frontmatter: { title: "Public" } },
{ slug: "notes/secret", frontmatter: { title: "Secret" }, unlisted: true },
{ slug: "notes/another", frontmatter: { title: "Another" } },
];
const result = pagesFromAllFiles(allFiles, "notes/index", true);
const slugs = result.map((r) => r.slug);
expect(slugs).toContain("notes/public");
expect(slugs).toContain("notes/another");
expect(slugs).not.toContain("notes/secret");
});
it("includes explicit unlisted: false pages", () => {
const allFiles = [{ slug: "notes/forced", frontmatter: { title: "Forced" }, unlisted: false }];
const result = pagesFromAllFiles(allFiles, "notes/index", true);
expect(result.map((r) => r.slug)).toEqual(["notes/forced"]);
});
it("groups subfolders when showSubfolders is true", () => {
const allFiles = [
{ slug: "notes/top", frontmatter: { title: "Top" } },
{ slug: "notes/sub/page", frontmatter: { title: "SubPage" } },
];
const result = pagesFromAllFiles(allFiles, "notes/index", true);
const slugs = result.map((r) => r.slug).sort();
expect(slugs).toContain("notes/top");
expect(slugs.some((s) => s?.startsWith("notes/sub"))).toBe(true);
});
it("does not include the folder index itself", () => {
const allFiles = [
{ slug: "notes/index", frontmatter: { title: "Index" } },
{ slug: "notes/page", frontmatter: { title: "Page" } },
];
const result = pagesFromAllFiles(allFiles, "notes/index", true);
expect(result.map((r) => r.slug)).toEqual(["notes/page"]);
});
});