mirror of
https://github.com/jackyzha0/quartz.git
synced 2025-12-27 06:44:07 -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.
|
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
|
## 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.
|
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 })`
|
- 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 })`
|
- 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.
|
- 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 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.
|
- 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`
|
- 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:
|
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.
|
- `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
|
## API
|
||||||
|
|
||||||
|
|||||||
@ -8,7 +8,8 @@ import { i18n } from "../../i18n"
|
|||||||
import { QuartzPluginData } from "../../plugins/vfile"
|
import { QuartzPluginData } from "../../plugins/vfile"
|
||||||
import { ComponentChildren } from "preact"
|
import { ComponentChildren } from "preact"
|
||||||
import { concatenateResources } from "../../util/resources"
|
import { concatenateResources } from "../../util/resources"
|
||||||
import { trieFromAllFiles } from "../../util/ctx"
|
import { BuildTimeTrieData, trieFromAllFiles } from "../../util/ctx"
|
||||||
|
import { FileTrieNode } from "../../util/fileTrie"
|
||||||
|
|
||||||
interface FolderContentOptions {
|
interface FolderContentOptions {
|
||||||
/**
|
/**
|
||||||
@ -31,63 +32,67 @@ export default ((opts?: Partial<FolderContentOptions>) => {
|
|||||||
const { tree, fileData, allFiles, cfg } = props
|
const { tree, fileData, allFiles, cfg } = props
|
||||||
|
|
||||||
const trie = (props.ctx.trie ??= trieFromAllFiles(allFiles))
|
const trie = (props.ctx.trie ??= trieFromAllFiles(allFiles))
|
||||||
const folder = trie.findNode(fileData.slug!.split("/"))
|
let folder : FileTrieNode<BuildTimeTrieData> | undefined
|
||||||
if (!folder) {
|
if (!fileData.isGlobalFolder) {
|
||||||
return null
|
folder = trie.findNode(fileData.slug!.split("/"))
|
||||||
|
if (!folder) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const allPagesInFolder: QuartzPluginData[] =
|
const allPagesInFolder: QuartzPluginData[] = fileData.isGlobalFolder
|
||||||
folder.children
|
? allFiles
|
||||||
.map((node) => {
|
: folder!.children
|
||||||
// regular file, proceed
|
.map((node) => {
|
||||||
if (node.data) {
|
// regular file, proceed
|
||||||
return node.data
|
if (node.data) {
|
||||||
}
|
return node.data
|
||||||
|
}
|
||||||
|
|
||||||
if (node.isFolder && options.showSubfolders) {
|
if (node.isFolder && options.showSubfolders) {
|
||||||
// folders that dont have data need synthetic files
|
// folders that dont have data need synthetic files
|
||||||
const getMostRecentDates = (): QuartzPluginData["dates"] => {
|
const getMostRecentDates = (): QuartzPluginData["dates"] => {
|
||||||
let maybeDates: QuartzPluginData["dates"] | undefined = undefined
|
let maybeDates: QuartzPluginData["dates"] | undefined = undefined
|
||||||
for (const child of node.children) {
|
for (const child of node.children) {
|
||||||
if (child.data?.dates) {
|
if (child.data?.dates) {
|
||||||
// compare all dates and assign to maybeDates if its more recent or its not set
|
// compare all dates and assign to maybeDates if its more recent or its not set
|
||||||
if (!maybeDates) {
|
if (!maybeDates) {
|
||||||
maybeDates = { ...child.data.dates }
|
maybeDates = { ...child.data.dates }
|
||||||
} else {
|
} else {
|
||||||
if (child.data.dates.created > maybeDates.created) {
|
if (child.data.dates.created > maybeDates.created) {
|
||||||
maybeDates.created = child.data.dates.created
|
maybeDates.created = child.data.dates.created
|
||||||
}
|
}
|
||||||
|
|
||||||
if (child.data.dates.modified > maybeDates.modified) {
|
if (child.data.dates.modified > maybeDates.modified) {
|
||||||
maybeDates.modified = child.data.dates.modified
|
maybeDates.modified = child.data.dates.modified
|
||||||
}
|
}
|
||||||
|
|
||||||
if (child.data.dates.published > maybeDates.published) {
|
if (child.data.dates.published > maybeDates.published) {
|
||||||
maybeDates.published = child.data.dates.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 {
|
return {
|
||||||
slug: node.slug,
|
slug: node.slug,
|
||||||
dates: getMostRecentDates(),
|
dates: getMostRecentDates(),
|
||||||
frontmatter: {
|
frontmatter: {
|
||||||
title: node.displayName,
|
title: node.displayName,
|
||||||
tags: [],
|
tags: [],
|
||||||
},
|
},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
})
|
.filter((page) => page !== undefined) ?? []
|
||||||
.filter((page) => page !== undefined) ?? []
|
|
||||||
const cssClasses: string[] = fileData.frontmatter?.cssclasses ?? []
|
const cssClasses: string[] = fileData.frontmatter?.cssclasses ?? []
|
||||||
const classes = cssClasses.join(" ")
|
const classes = cssClasses.join(" ")
|
||||||
const listProps = {
|
const listProps = {
|
||||||
|
|||||||
@ -20,8 +20,16 @@ import { write } from "./helpers"
|
|||||||
import { i18n, TRANSLATIONS } from "../../i18n"
|
import { i18n, TRANSLATIONS } from "../../i18n"
|
||||||
import { BuildCtx } from "../../util/ctx"
|
import { BuildCtx } from "../../util/ctx"
|
||||||
import { StaticResources } from "../../util/resources"
|
import { StaticResources } from "../../util/resources"
|
||||||
|
|
||||||
interface FolderPageOptions extends FullPageLayout {
|
interface FolderPageOptions extends FullPageLayout {
|
||||||
sort?: (f1: QuartzPluginData, f2: QuartzPluginData) => number
|
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(
|
async function* processFolderInfo(
|
||||||
@ -63,7 +71,19 @@ function computeFolderInfo(
|
|||||||
folders: Set<SimpleSlug>,
|
folders: Set<SimpleSlug>,
|
||||||
content: ProcessedContent[],
|
content: ProcessedContent[],
|
||||||
locale: keyof typeof TRANSLATIONS,
|
locale: keyof typeof TRANSLATIONS,
|
||||||
|
userOpts?: Partial<FolderPageOptions>,
|
||||||
): Record<SimpleSlug, ProcessedContent> {
|
): 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
|
// Create default folder descriptions
|
||||||
const folderInfo: Record<SimpleSlug, ProcessedContent> = Object.fromEntries(
|
const folderInfo: Record<SimpleSlug, ProcessedContent> = Object.fromEntries(
|
||||||
[...folders].map((folder) => [
|
[...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
|
// Update with actual content if available
|
||||||
for (const [tree, file] of content) {
|
for (const [tree, file] of content) {
|
||||||
const slug = stripSlashes(simplifySlug(file.data.slug!)) as SimpleSlug
|
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)
|
yield* processFolderInfo(ctx, folderInfo, allFiles, opts, resources)
|
||||||
},
|
},
|
||||||
async *partialEmit(ctx, content, resources, changeEvents) {
|
async *partialEmit(ctx, content, resources, changeEvents) {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user