diff --git a/docs/features/folder and tag listings.md b/docs/features/folder and tag listings.md index 3190709d3..9fda4639d 100644 --- a/docs/features/folder and tag listings.md +++ b/docs/features/folder and tag listings.md @@ -18,6 +18,10 @@ By default, Quartz will title the page `Folder: ` and no descriptio For example, for the folder `content/posts`, you can add another file `content/posts/index.md` to add a specific description for it. +## Global Folder Listings (Optional) + +After configuring in the [[FolderPage]] plugin, Quartz can generate a virtual global folder under the website root for all the user-created public pages, sorted based on folder page sorting rules. This can be used in conjunction with [[features/recent-notes | Recent Notes]]'s 'See more' link feature for site navigation. + ## Tag Listings Quartz will also create an index page for each unique tag in your vault and render a list of all notes with that tag. diff --git a/docs/features/recent notes.md b/docs/features/recent notes.md index 75406e504..88aa38c45 100644 --- a/docs/features/recent notes.md +++ b/docs/features/recent notes.md @@ -11,6 +11,7 @@ Quartz can generate a list of recent notes based on some filtering and sorting c - Changing the number of recent notes: pass in an additional parameter to `Component.RecentNotes({ limit: 5 })` - Display the note's tags (defaults to true): `Component.RecentNotes({ showTags: false })` - Show a 'see more' link: pass in an additional parameter to `Component.RecentNotes({ linkToMore: "tags/components" })`. This field should be a full slug to a page that exists. + - See [[folder-and-tag-listings | Folder and Tag Listings]] for more information on the virtual global folder page you may link to. - Customize filtering: pass in an additional parameter to `Component.RecentNotes({ filter: someFilterFunction })`. The filter function should be a function that has the signature `(f: QuartzPluginData) => boolean`. - Customize sorting: pass in an additional parameter to `Component.RecentNotes({ sort: someSortFunction })`. By default, Quartz will sort by date and then tie break lexographically. The sort function should be a function that has the signature `(f1: QuartzPluginData, f2: QuartzPluginData) => number`. See `byDateAndAlphabetical` in `quartz/components/PageList.tsx` for an example. - Component: `quartz/components/RecentNotes.tsx` diff --git a/docs/plugins/FolderPage.md b/docs/plugins/FolderPage.md index 45cfa1574..03878e8e8 100644 --- a/docs/plugins/FolderPage.md +++ b/docs/plugins/FolderPage.md @@ -16,6 +16,9 @@ The pages are displayed using the `defaultListPageLayout` in `quartz.layouts.ts` This plugin accepts the following configuration options: - `sort`: A function of type `(f1: QuartzPluginData, f2: QuartzPluginData) => number{:ts}` used to sort entries. Defaults to sorting by date and tie-breaking on lexographical order. +- `globalFolderTitle`: If set, the title of a virtual global folder under website root that lists all user-created public posts. This can be used in conjunction with [[features/recent-notes | Recent Notes]]'s 'See more' link feature for site navigation. + + As an example, if it's set to "All Posts", a global folder page will be created on `/all-posts/`. An exception is thrown on build if it conflicts with an existing folder. ## API diff --git a/quartz/components/pages/FolderContent.tsx b/quartz/components/pages/FolderContent.tsx index afd4f5d7e..b5c6aef9a 100644 --- a/quartz/components/pages/FolderContent.tsx +++ b/quartz/components/pages/FolderContent.tsx @@ -8,7 +8,8 @@ import { i18n } from "../../i18n" import { QuartzPluginData } from "../../plugins/vfile" import { ComponentChildren } from "preact" import { concatenateResources } from "../../util/resources" -import { trieFromAllFiles } from "../../util/ctx" +import { BuildTimeTrieData, trieFromAllFiles } from "../../util/ctx" +import { FileTrieNode } from "../../util/fileTrie" interface FolderContentOptions { /** @@ -31,63 +32,67 @@ export default ((opts?: Partial) => { const { tree, fileData, allFiles, cfg } = props const trie = (props.ctx.trie ??= trieFromAllFiles(allFiles)) - const folder = trie.findNode(fileData.slug!.split("/")) - if (!folder) { - return null + let folder : FileTrieNode | undefined + if (!fileData.isGlobalFolder) { + folder = trie.findNode(fileData.slug!.split("/")) + if (!folder) { + return null + } } - const allPagesInFolder: QuartzPluginData[] = - folder.children - .map((node) => { - // regular file, proceed - if (node.data) { - return node.data - } + const allPagesInFolder: QuartzPluginData[] = fileData.isGlobalFolder + ? allFiles + : folder!.children + .map((node) => { + // regular file, proceed + if (node.data) { + return node.data + } - if (node.isFolder && options.showSubfolders) { - // folders that dont have data need synthetic files - const getMostRecentDates = (): QuartzPluginData["dates"] => { - let maybeDates: QuartzPluginData["dates"] | undefined = undefined - for (const child of node.children) { - if (child.data?.dates) { - // compare all dates and assign to maybeDates if its more recent or its not set - if (!maybeDates) { - maybeDates = { ...child.data.dates } - } else { - if (child.data.dates.created > maybeDates.created) { - maybeDates.created = child.data.dates.created - } + if (node.isFolder && options.showSubfolders) { + // folders that dont have data need synthetic files + const getMostRecentDates = (): QuartzPluginData["dates"] => { + let maybeDates: QuartzPluginData["dates"] | undefined = undefined + for (const child of node.children) { + if (child.data?.dates) { + // compare all dates and assign to maybeDates if its more recent or its not set + if (!maybeDates) { + maybeDates = { ...child.data.dates } + } else { + if (child.data.dates.created > maybeDates.created) { + maybeDates.created = child.data.dates.created + } - if (child.data.dates.modified > maybeDates.modified) { - maybeDates.modified = child.data.dates.modified - } + if (child.data.dates.modified > maybeDates.modified) { + maybeDates.modified = child.data.dates.modified + } - if (child.data.dates.published > maybeDates.published) { - maybeDates.published = child.data.dates.published + if (child.data.dates.published > maybeDates.published) { + maybeDates.published = child.data.dates.published + } } } } + return ( + maybeDates ?? { + created: new Date(), + modified: new Date(), + published: new Date(), + } + ) } - return ( - maybeDates ?? { - created: new Date(), - modified: new Date(), - published: new Date(), - } - ) - } - return { - slug: node.slug, - dates: getMostRecentDates(), - frontmatter: { - title: node.displayName, - tags: [], - }, + return { + slug: node.slug, + dates: getMostRecentDates(), + frontmatter: { + title: node.displayName, + tags: [], + }, + } } - } - }) - .filter((page) => page !== undefined) ?? [] + }) + .filter((page) => page !== undefined) ?? [] const cssClasses: string[] = fileData.frontmatter?.cssclasses ?? [] const classes = cssClasses.join(" ") const listProps = { diff --git a/quartz/plugins/emitters/folderPage.tsx b/quartz/plugins/emitters/folderPage.tsx index f9b181dff..8400ae8ae 100644 --- a/quartz/plugins/emitters/folderPage.tsx +++ b/quartz/plugins/emitters/folderPage.tsx @@ -20,8 +20,16 @@ import { write } from "./helpers" import { i18n, TRANSLATIONS } from "../../i18n" import { BuildCtx } from "../../util/ctx" import { StaticResources } from "../../util/resources" + interface FolderPageOptions extends FullPageLayout { sort?: (f1: QuartzPluginData, f2: QuartzPluginData) => number + /** + * If set, generates a virtual global folder page with the given title + * at the root of the site containing all non-generated posts. + * + * Make sure the folder name does not conflict with existing absolute paths. + */ + globalFolderTitle?: string } async function* processFolderInfo( @@ -63,7 +71,19 @@ function computeFolderInfo( folders: Set, content: ProcessedContent[], locale: keyof typeof TRANSLATIONS, + userOpts?: Partial, ): Record { + // Fail fast if global folder slug conflicts with existing folders + const globalFolderSlug = userOpts?.globalFolderTitle?.toLowerCase() + .replaceAll(" ", "-") as SimpleSlug ?? null + if (globalFolderSlug) { + if (folders.has(globalFolderSlug)) { + throw new Error( + `Global folder path "${globalFolderSlug}" conflicts with existing folder's.`, + ) + } + } + // Create default folder descriptions const folderInfo: Record = Object.fromEntries( [...folders].map((folder) => [ @@ -78,6 +98,18 @@ function computeFolderInfo( ]), ) + // Add metadata for the global folder + if (globalFolderSlug) { + folderInfo[globalFolderSlug] = defaultProcessedContent({ + slug: joinSegments(globalFolderSlug, "index") as FullSlug, + frontmatter: { + title: userOpts?.globalFolderTitle!, + tags: [], + }, + isGlobalFolder: true, + }) + } + // Update with actual content if available for (const [tree, file] of content) { const slug = stripSlashes(simplifySlug(file.data.slug!)) as SimpleSlug @@ -142,7 +174,7 @@ export const FolderPage: QuartzEmitterPlugin> = (user }), ) - const folderInfo = computeFolderInfo(folders, content, cfg.locale) + const folderInfo = computeFolderInfo(folders, content, cfg.locale, userOpts) yield* processFolderInfo(ctx, folderInfo, allFiles, opts, resources) }, async *partialEmit(ctx, content, resources, changeEvents) {