mirror of
https://github.com/quartz-community/tag-page.git
synced 2026-07-22 02:50:25 +00:00
feat: exclude unlisted pages from tag discovery and listings
Respect the file.data.unlisted convention in both the tag discovery loop (virtual tag page generation in pageType.generate) and the tag listing itself (allPagesWithTag and the root-index tag set builder in TagContent). Unlisted pages no longer contribute tags, and no longer appear in any tag listing. A tag that exists only on unlisted pages will not cause a virtual tag page to be generated at all. Adds 4 unit tests for pageType.generate covering tag discovery from listed pages, unlisted exclusion, shared tags across listed and unlisted pages, and tag-only-on-unlisted-pages suppression.
This commit is contained in:
parent
13036ce028
commit
21bd3be046
8 changed files with 107 additions and 11 deletions
|
|
@ -9,4 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
|
||||
### Added
|
||||
|
||||
- Respect the `file.data.unlisted` convention in tag discovery (virtual tag page generation) and tag listings (`TagContent.tsx`). Unlisted pages are never used to discover tags and never appear in any tag listing. A tag that exists only on unlisted pages will not cause a tag page to be generated.
|
||||
- Unit tests for tag discovery covering the unlisted-filter behavior.
|
||||
|
||||
- Initial Quartz community plugin template.
|
||||
|
|
|
|||
7
dist/components/index.js
vendored
7
dist/components/index.js
vendored
|
|
@ -2761,6 +2761,9 @@ function concatenateResources(...resources) {
|
|||
const result = resources.filter((r) => r !== void 0).flat();
|
||||
return result.length === 0 ? void 0 : result;
|
||||
}
|
||||
function isListed(file) {
|
||||
return file.unlisted !== true;
|
||||
}
|
||||
var TagContent_default = ((opts) => {
|
||||
const options = { ...defaultOptions, ...opts };
|
||||
const TagContent = (props) => {
|
||||
|
|
@ -2772,7 +2775,7 @@ var TagContent_default = ((opts) => {
|
|||
throw new Error(`Component "TagContent" tried to render a non-tag page: ${slug2}`);
|
||||
}
|
||||
const tag = simplifySlug(slug2.slice("tags/".length));
|
||||
const allPagesWithTag = (t) => allFiles.filter(
|
||||
const allPagesWithTag = (t) => allFiles.filter(isListed).filter(
|
||||
(file) => (file.frontmatter?.tags ?? []).flatMap(getAllSegmentPrefixes).includes(t)
|
||||
);
|
||||
const hastRoot = tree;
|
||||
|
|
@ -2782,7 +2785,7 @@ var TagContent_default = ((opts) => {
|
|||
if (tag === "/") {
|
||||
const tags = [
|
||||
...new Set(
|
||||
allFiles.flatMap((data) => data.frontmatter?.tags ?? []).flatMap(getAllSegmentPrefixes)
|
||||
allFiles.filter(isListed).flatMap((data) => data.frontmatter?.tags ?? []).flatMap(getAllSegmentPrefixes)
|
||||
)
|
||||
].sort((a, b) => a.localeCompare(b));
|
||||
const tagItemMap = /* @__PURE__ */ new Map();
|
||||
|
|
|
|||
2
dist/components/index.js.map
vendored
2
dist/components/index.js.map
vendored
File diff suppressed because one or more lines are too long
9
dist/index.js
vendored
9
dist/index.js
vendored
|
|
@ -2745,6 +2745,9 @@ function concatenateResources(...resources) {
|
|||
const result = resources.filter((r) => r !== void 0).flat();
|
||||
return result.length === 0 ? void 0 : result;
|
||||
}
|
||||
function isListed(file) {
|
||||
return file.unlisted !== true;
|
||||
}
|
||||
var TagContent_default = ((opts) => {
|
||||
const options = { ...defaultOptions, ...opts };
|
||||
const TagContent = (props) => {
|
||||
|
|
@ -2756,7 +2759,7 @@ var TagContent_default = ((opts) => {
|
|||
throw new Error(`Component "TagContent" tried to render a non-tag page: ${slug2}`);
|
||||
}
|
||||
const tag = simplifySlug(slug2.slice("tags/".length));
|
||||
const allPagesWithTag = (t) => allFiles.filter(
|
||||
const allPagesWithTag = (t) => allFiles.filter(isListed).filter(
|
||||
(file) => (file.frontmatter?.tags ?? []).flatMap(getAllSegmentPrefixes).includes(t)
|
||||
);
|
||||
const hastRoot = tree;
|
||||
|
|
@ -2766,7 +2769,7 @@ var TagContent_default = ((opts) => {
|
|||
if (tag === "/") {
|
||||
const tags = [
|
||||
...new Set(
|
||||
allFiles.flatMap((data) => data.frontmatter?.tags ?? []).flatMap(getAllSegmentPrefixes)
|
||||
allFiles.filter(isListed).flatMap((data) => data.frontmatter?.tags ?? []).flatMap(getAllSegmentPrefixes)
|
||||
)
|
||||
].sort((a, b) => a.localeCompare(b));
|
||||
const tagItemMap = /* @__PURE__ */ new Map();
|
||||
|
|
@ -2844,7 +2847,7 @@ var TagPage = (opts) => ({
|
|||
priority: 10,
|
||||
match: tagMatcher,
|
||||
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 tags = new Set(
|
||||
allFiles.flatMap((data) => data.frontmatter?.tags ?? []).flatMap(getAllSegmentPrefixes)
|
||||
|
|
|
|||
2
dist/index.js.map
vendored
2
dist/index.js.map
vendored
File diff suppressed because one or more lines are too long
|
|
@ -32,12 +32,17 @@ function concatenateResources(
|
|||
interface PageFileData {
|
||||
slug?: string;
|
||||
filePath?: string;
|
||||
unlisted?: boolean;
|
||||
frontmatter?: { title?: string; tags?: string[]; cssclasses?: string[] };
|
||||
description?: unknown;
|
||||
htmlAst?: Root;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
function isListed(file: PageFileData): boolean {
|
||||
return file.unlisted !== true;
|
||||
}
|
||||
|
||||
export default ((opts?: Partial<TagContentOptions>) => {
|
||||
const options: TagContentOptions = { ...defaultOptions, ...opts };
|
||||
|
||||
|
|
@ -53,9 +58,11 @@ export default ((opts?: Partial<TagContentOptions>) => {
|
|||
|
||||
const tag = simplifySlug(slug.slice("tags/".length) as FullSlug);
|
||||
const allPagesWithTag = (t: string) =>
|
||||
(allFiles as PageFileData[]).filter((file) =>
|
||||
(file.frontmatter?.tags ?? []).flatMap(getAllSegmentPrefixes).includes(t),
|
||||
);
|
||||
(allFiles as PageFileData[])
|
||||
.filter(isListed)
|
||||
.filter((file) =>
|
||||
(file.frontmatter?.tags ?? []).flatMap(getAllSegmentPrefixes).includes(t),
|
||||
);
|
||||
|
||||
const hastRoot = tree as Root;
|
||||
const content = hastRoot.children.length === 0 ? fd.description : htmlToJsx(hastRoot);
|
||||
|
|
@ -67,6 +74,7 @@ export default ((opts?: Partial<TagContentOptions>) => {
|
|||
const tags = [
|
||||
...new Set(
|
||||
(allFiles as PageFileData[])
|
||||
.filter(isListed)
|
||||
.flatMap((data) => data.frontmatter?.tags ?? [])
|
||||
.flatMap(getAllSegmentPrefixes),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -25,7 +25,9 @@ export const TagPage: QuartzPageTypePlugin<TagPageOptions> = (opts) => ({
|
|||
priority: 10,
|
||||
match: tagMatcher,
|
||||
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 tags: Set<string> = new Set(
|
||||
|
|
|
|||
77
test/pageType.test.ts
Normal file
77
test/pageType.test.ts
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import type { ProcessedContent } from "@quartz-community/types";
|
||||
import { TagPage } from "../src/pageType";
|
||||
|
||||
function makeProcessedContent(
|
||||
slug: string,
|
||||
opts: { tags?: string[]; unlisted?: boolean } = {},
|
||||
): ProcessedContent {
|
||||
const data: Record<string, unknown> = {
|
||||
slug,
|
||||
relativePath: `${slug}.md`,
|
||||
frontmatter: { title: slug, tags: opts.tags ?? [] },
|
||||
};
|
||||
if (opts.unlisted) data.unlisted = true;
|
||||
const vfile = { data } as unknown as ProcessedContent[1];
|
||||
return [{ type: "root", children: [] }, vfile];
|
||||
}
|
||||
|
||||
function generate(
|
||||
plugin: ReturnType<typeof TagPage>,
|
||||
content: ProcessedContent[],
|
||||
): { slug: string; title: string }[] {
|
||||
const generateFn = plugin.generate;
|
||||
if (!generateFn) throw new Error("TagPage.generate is undefined");
|
||||
const pages = generateFn({
|
||||
content,
|
||||
cfg: { locale: "en-US" } as never,
|
||||
ctx: {} as never,
|
||||
});
|
||||
return pages.map((p) => ({ slug: String(p.slug), title: p.title }));
|
||||
}
|
||||
|
||||
describe("TagPage pageType generate", () => {
|
||||
it("discovers tags from listed pages", () => {
|
||||
const plugin = TagPage();
|
||||
const virtualPages = generate(plugin, [
|
||||
makeProcessedContent("notes/a", { tags: ["public", "alpha"] }),
|
||||
makeProcessedContent("notes/b", { tags: ["public"] }),
|
||||
]);
|
||||
const slugs = virtualPages.map((p) => p.slug);
|
||||
expect(slugs).toContain("tags/public");
|
||||
expect(slugs).toContain("tags/alpha");
|
||||
expect(slugs).toContain("tags/index");
|
||||
});
|
||||
|
||||
it("does NOT discover tags from unlisted pages", () => {
|
||||
const plugin = TagPage();
|
||||
const virtualPages = generate(plugin, [
|
||||
makeProcessedContent("notes/public", { tags: ["visible"] }),
|
||||
makeProcessedContent("notes/secret", { tags: ["hidden"], unlisted: true }),
|
||||
]);
|
||||
const slugs = virtualPages.map((p) => p.slug);
|
||||
expect(slugs).toContain("tags/visible");
|
||||
expect(slugs).not.toContain("tags/hidden");
|
||||
});
|
||||
|
||||
it("discovers tag only for listed pages even when the unlisted page shares a tag", () => {
|
||||
const plugin = TagPage();
|
||||
const virtualPages = generate(plugin, [
|
||||
makeProcessedContent("notes/a", { tags: ["shared"] }),
|
||||
makeProcessedContent("notes/b", { tags: ["shared"], unlisted: true }),
|
||||
]);
|
||||
const slugs = virtualPages.map((p) => p.slug);
|
||||
expect(slugs).toContain("tags/shared");
|
||||
});
|
||||
|
||||
it("ignores a tag that only exists on unlisted pages", () => {
|
||||
const plugin = TagPage();
|
||||
const virtualPages = generate(plugin, [
|
||||
makeProcessedContent("notes/public", { tags: ["listed"] }),
|
||||
makeProcessedContent("notes/secret", { tags: ["secret-only"], unlisted: true }),
|
||||
]);
|
||||
const slugs = virtualPages.map((p) => p.slug);
|
||||
expect(slugs).toContain("tags/listed");
|
||||
expect(slugs).not.toContain("tags/secret-only");
|
||||
});
|
||||
});
|
||||
Loading…
Reference in a new issue