mirror of
https://github.com/jackyzha0/quartz.git
synced 2025-12-24 21:34:06 -06:00
feat(RecentNotes): Virtual global folder for public posts
This commit is contained in:
parent
c238dd16d9
commit
e68749d6e4
@ -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.
|
||||
|
||||
@ -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`
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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 = {
|
||||
|
||||
@ -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) {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user