diff --git a/quartz.config.ts b/quartz.config.ts index b3db3d60d..de17574fa 100644 --- a/quartz.config.ts +++ b/quartz.config.ts @@ -18,6 +18,7 @@ const config: QuartzConfig = { locale: "en-US", baseUrl: "quartz.jzhao.xyz", ignorePatterns: ["private", "templates", ".obsidian"], + unlistedPatterns: [], defaultDateType: "modified", theme: { fontOrigin: "googleFonts", diff --git a/quartz/cfg.ts b/quartz/cfg.ts index 57dff5c75..12d3e3457 100644 --- a/quartz/cfg.ts +++ b/quartz/cfg.ts @@ -62,6 +62,8 @@ export interface GlobalConfiguration { analytics: Analytics /** Glob patterns to not search */ ignorePatterns: string[] + /** Glob patterns to mark files as unlisted (hidden from listings but still accessible via direct link) */ + unlistedPatterns?: string[] /** Whether to use created, modified, or published as the default type of date */ defaultDateType: ValidDateType /** Base URL to use for CNAME files, sitemaps, and RSS feeds that require an absolute URL. diff --git a/quartz/components/Breadcrumbs.tsx b/quartz/components/Breadcrumbs.tsx index 5144a314d..17a32104e 100644 --- a/quartz/components/Breadcrumbs.tsx +++ b/quartz/components/Breadcrumbs.tsx @@ -50,7 +50,7 @@ export default ((opts?: Partial) => { displayClass, ctx, }: QuartzComponentProps) => { - const trie = (ctx.trie ??= trieFromAllFiles(allFiles)) + const trie = (ctx.trie ??= trieFromAllFiles(allFiles, ctx.cfg)) const slugParts = fileData.slug!.split("/") const pathNodes = trie.ancestryChain(slugParts) diff --git a/quartz/components/pages/FolderContent.tsx b/quartz/components/pages/FolderContent.tsx index afd4f5d7e..16cfad202 100644 --- a/quartz/components/pages/FolderContent.tsx +++ b/quartz/components/pages/FolderContent.tsx @@ -30,7 +30,7 @@ export default ((opts?: Partial) => { const FolderContent: QuartzComponent = (props: QuartzComponentProps) => { const { tree, fileData, allFiles, cfg } = props - const trie = (props.ctx.trie ??= trieFromAllFiles(allFiles)) + const trie = (props.ctx.trie ??= trieFromAllFiles(allFiles, props.ctx.cfg)) const folder = trie.findNode(fileData.slug!.split("/")) if (!folder) { return null diff --git a/quartz/plugins/emitters/contentIndex.tsx b/quartz/plugins/emitters/contentIndex.tsx index 56392b358..138831358 100644 --- a/quartz/plugins/emitters/contentIndex.tsx +++ b/quartz/plugins/emitters/contentIndex.tsx @@ -7,6 +7,7 @@ import { QuartzEmitterPlugin } from "../types" import { toHtml } from "hast-util-to-html" import { write } from "./helpers" import { i18n } from "../../i18n" +import { isUnlisted } from "../filters/unlisted" export type ContentIndexMap = Map export type ContentDetails = { @@ -102,6 +103,11 @@ export const ContentIndex: QuartzEmitterPlugin> = (opts) => { for (const [tree, file] of content) { const slug = file.data.slug! const date = getDate(ctx.cfg.configuration, file.data) ?? new Date() + + if(isUnlisted(file.data, cfg)) { + continue + } + if (opts?.includeEmptyFiles || (file.data.text && file.data.text !== "")) { linkIndex.set(slug, { slug, diff --git a/quartz/plugins/emitters/contentPage.tsx b/quartz/plugins/emitters/contentPage.tsx index c3410ecc3..b67cff207 100644 --- a/quartz/plugins/emitters/contentPage.tsx +++ b/quartz/plugins/emitters/contentPage.tsx @@ -14,6 +14,7 @@ import { BuildCtx } from "../../util/ctx" import { Node } from "unist" import { StaticResources } from "../../util/resources" import { QuartzPluginData } from "../vfile" +import { isUnlisted } from "../filters/unlisted" async function processContent( ctx: BuildCtx, @@ -74,7 +75,7 @@ export const ContentPage: QuartzEmitterPlugin> = (userOp ] }, async *emit(ctx, content, resources) { - const allFiles = content.map((c) => c[1].data) + const allFiles = content.map((c) => c[1].data).filter(f => !isUnlisted(f, ctx.cfg.configuration)) let containsIndex = false for (const [tree, file] of content) { @@ -98,7 +99,7 @@ export const ContentPage: QuartzEmitterPlugin> = (userOp } }, async *partialEmit(ctx, content, resources, changeEvents) { - const allFiles = content.map((c) => c[1].data) + const allFiles = content.map((c) => c[1].data).filter(f => !isUnlisted(f, ctx.cfg.configuration)) // find all slugs that changed or were added const changedSlugs = new Set() diff --git a/quartz/plugins/emitters/folderPage.tsx b/quartz/plugins/emitters/folderPage.tsx index f9b181dff..ef610a5bd 100644 --- a/quartz/plugins/emitters/folderPage.tsx +++ b/quartz/plugins/emitters/folderPage.tsx @@ -20,6 +20,7 @@ import { write } from "./helpers" import { i18n, TRANSLATIONS } from "../../i18n" import { BuildCtx } from "../../util/ctx" import { StaticResources } from "../../util/resources" +import { isUnlisted } from "../filters/unlisted" interface FolderPageOptions extends FullPageLayout { sort?: (f1: QuartzPluginData, f2: QuartzPluginData) => number } @@ -129,7 +130,7 @@ export const FolderPage: QuartzEmitterPlugin> = (user ] }, async *emit(ctx, content, resources) { - const allFiles = content.map((c) => c[1].data) + const allFiles = content.map((c) => c[1].data).filter(f => !isUnlisted(f, ctx.cfg.configuration)) const cfg = ctx.cfg.configuration const folders: Set = new Set( @@ -146,7 +147,7 @@ export const FolderPage: QuartzEmitterPlugin> = (user yield* processFolderInfo(ctx, folderInfo, allFiles, opts, resources) }, async *partialEmit(ctx, content, resources, changeEvents) { - const allFiles = content.map((c) => c[1].data) + const allFiles = content.map((c) => c[1].data).filter(f => !isUnlisted(f, ctx.cfg.configuration)) const cfg = ctx.cfg.configuration // Find all folders that need to be updated based on changed files diff --git a/quartz/plugins/emitters/tagPage.tsx b/quartz/plugins/emitters/tagPage.tsx index 5f238932d..04d2363db 100644 --- a/quartz/plugins/emitters/tagPage.tsx +++ b/quartz/plugins/emitters/tagPage.tsx @@ -12,6 +12,7 @@ import { write } from "./helpers" import { i18n, TRANSLATIONS } from "../../i18n" import { BuildCtx } from "../../util/ctx" import { StaticResources } from "../../util/resources" +import { isUnlisted } from "../filters/unlisted" interface TagPageOptions extends FullPageLayout { sort?: (f1: QuartzPluginData, f2: QuartzPluginData) => number @@ -122,7 +123,7 @@ export const TagPage: QuartzEmitterPlugin> = (userOpts) ] }, async *emit(ctx, content, resources) { - const allFiles = content.map((c) => c[1].data) + const allFiles = content.map((c) => c[1].data).filter(f => !isUnlisted(f, ctx.cfg.configuration)) const cfg = ctx.cfg.configuration const [tags, tagDescriptions] = computeTagInfo(allFiles, content, cfg.locale) @@ -131,7 +132,7 @@ export const TagPage: QuartzEmitterPlugin> = (userOpts) } }, async *partialEmit(ctx, content, resources, changeEvents) { - const allFiles = content.map((c) => c[1].data) + const allFiles = content.map((c) => c[1].data).filter(f => !isUnlisted(f, ctx.cfg.configuration)) const cfg = ctx.cfg.configuration // Find all tags that need to be updated based on changed files diff --git a/quartz/plugins/filters/unlisted.ts b/quartz/plugins/filters/unlisted.ts new file mode 100644 index 000000000..07d6e0856 --- /dev/null +++ b/quartz/plugins/filters/unlisted.ts @@ -0,0 +1,27 @@ +import { minimatch } from "minimatch" +import { QuartzPluginData } from "../vfile" +import { GlobalConfiguration } from "../../cfg" + +export function isUnlisted( + fileData: QuartzPluginData, + cfg: GlobalConfiguration, + unlistedPatterns?: string[] +): boolean { + const unlistedFlag: boolean = + fileData?.frontmatter?.unlisted === true || + fileData?.frontmatter?.unlisted === "true" + + if (unlistedFlag) return true + + const patterns = unlistedPatterns ?? cfg.unlistedPatterns + if (patterns && patterns.length > 0 && fileData.slug) { + const slug = fileData.slug as string + for (const pattern of patterns) { + if (minimatch(slug, pattern)) { + return true + } + } + } + + return false +} \ No newline at end of file diff --git a/quartz/plugins/transformers/frontmatter.ts b/quartz/plugins/transformers/frontmatter.ts index 1103900c5..6bd04c782 100644 --- a/quartz/plugins/transformers/frontmatter.ts +++ b/quartz/plugins/transformers/frontmatter.ts @@ -146,6 +146,7 @@ declare module "vfile" { socialDescription: string publish: boolean | string draft: boolean | string + unlisted: boolean | string lang: string enableToc: string cssclasses: string[] diff --git a/quartz/util/ctx.ts b/quartz/util/ctx.ts index 80115ec27..d1eb4a5ba 100644 --- a/quartz/util/ctx.ts +++ b/quartz/util/ctx.ts @@ -1,4 +1,5 @@ import { QuartzConfig } from "../cfg" +import { isUnlisted } from "../plugins/filters/unlisted" import { QuartzPluginData } from "../plugins/vfile" import { FileTrieNode } from "./fileTrie" import { FilePath, FullSlug } from "./path" @@ -31,10 +32,16 @@ export interface BuildCtx { incremental: boolean } -export function trieFromAllFiles(allFiles: QuartzPluginData[]): FileTrieNode { +export function trieFromAllFiles( + allFiles: QuartzPluginData[], + cfg?: QuartzConfig +): FileTrieNode { const trie = new FileTrieNode([]) allFiles.forEach((file) => { if (file.frontmatter) { + if (cfg && isUnlisted(file, cfg.configuration)) { + return + } trie.add({ ...file, slug: file.slug!,