feat(RecentNotes): Virtual global folder for public posts

This commit is contained in:
Stephen Tse 2025-04-21 01:15:45 -07:00
parent c238dd16d9
commit e68749d6e4
5 changed files with 93 additions and 48 deletions

View File

@ -18,6 +18,10 @@ By default, Quartz will title the page `Folder: <folder name>` 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.

View File

@ -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`

View File

@ -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 `<baseUrl>/all-posts/`. An exception is thrown on build if it conflicts with an existing folder.
## API

View File

@ -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<FolderContentOptions>) => {
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<BuildTimeTrieData> | 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 = {

View File

@ -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<SimpleSlug>,
content: ProcessedContent[],
locale: keyof typeof TRANSLATIONS,
userOpts?: Partial<FolderPageOptions>,
): Record<SimpleSlug, ProcessedContent> {
// 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<SimpleSlug, ProcessedContent> = 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<Partial<FolderPageOptions>> = (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) {