mirror of
https://github.com/jackyzha0/quartz.git
synced 2025-12-19 10:54:06 -06:00
Merge 84d6484350 into bacd19c4ea
This commit is contained in:
commit
9030bbf81f
@ -6,19 +6,39 @@ tags:
|
|||||||
|
|
||||||
This plugin emits HTML redirect pages for aliases and permalinks defined in the frontmatter of content files.
|
This plugin emits HTML redirect pages for aliases and permalinks defined in the frontmatter of content files.
|
||||||
|
|
||||||
For example, A `foo.md` has the following frontmatter
|
## Aliases
|
||||||
|
|
||||||
|
Aliases create redirects from alternative names to the actual page URL.
|
||||||
|
|
||||||
|
For example, a `foo.md` has the following frontmatter:
|
||||||
|
|
||||||
```md title="foo.md"
|
```md title="foo.md"
|
||||||
---
|
---
|
||||||
title: "Foo"
|
title: "Foo"
|
||||||
alias:
|
alias:
|
||||||
- "bar"
|
- "bar"
|
||||||
|
- "old-name"
|
||||||
---
|
---
|
||||||
```
|
```
|
||||||
|
|
||||||
The target `host.me/bar` will be redirected to `host.me/foo`
|
The URLs `host.me/bar` and `host.me/old-name` will be redirected to `host.me/foo`
|
||||||
|
|
||||||
Note that these are permanent redirect.
|
## Permalinks
|
||||||
|
|
||||||
|
Permalinks set the actual URL where the page will be accessed. When you set a permalink, the page is rendered at that URL instead of the default file path.
|
||||||
|
|
||||||
|
For example, a `my-note.md` has the following frontmatter:
|
||||||
|
|
||||||
|
```md title="my-note.md"
|
||||||
|
---
|
||||||
|
title: "My Note"
|
||||||
|
permalink: "/custom-path"
|
||||||
|
---
|
||||||
|
```
|
||||||
|
|
||||||
|
The page will be accessible at `host.me/custom-path` (not at `host.me/my-note`). A redirect will automatically be created from the original file path (`host.me/my-note`) to the permalink (`host.me/custom-path`).
|
||||||
|
|
||||||
|
Note that these are permanent redirects.
|
||||||
|
|
||||||
The emitter supports the following aliases:
|
The emitter supports the following aliases:
|
||||||
|
|
||||||
|
|||||||
@ -49,6 +49,7 @@ async function buildQuartz(argv: Argv, mut: Mutex, clientRefresh: () => void) {
|
|||||||
cfg,
|
cfg,
|
||||||
allSlugs: [],
|
allSlugs: [],
|
||||||
allFiles: [],
|
allFiles: [],
|
||||||
|
slugToPermalink: {},
|
||||||
incremental: false,
|
incremental: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -84,6 +85,13 @@ async function buildQuartz(argv: Argv, mut: Mutex, clientRefresh: () => void) {
|
|||||||
const parsedFiles = await parseMarkdown(ctx, filePaths)
|
const parsedFiles = await parseMarkdown(ctx, filePaths)
|
||||||
const filteredContent = filterContent(ctx, parsedFiles)
|
const filteredContent = filterContent(ctx, parsedFiles)
|
||||||
|
|
||||||
|
// Build slugToPermalink map after parsing
|
||||||
|
for (const [_tree, file] of filteredContent) {
|
||||||
|
if (file.data.permalinkSlug && file.data.slug) {
|
||||||
|
ctx.slugToPermalink[file.data.slug] = file.data.permalinkSlug
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
await emitContent(ctx, filteredContent)
|
await emitContent(ctx, filteredContent)
|
||||||
console.log(
|
console.log(
|
||||||
styleText("green", `Done processing ${markdownPaths.length} files in ${perf.timeSince()}`),
|
styleText("green", `Done processing ${markdownPaths.length} files in ${perf.timeSince()}`),
|
||||||
@ -261,6 +269,14 @@ async function rebuild(changes: ChangeEvent[], clientRefresh: () => void, buildD
|
|||||||
.map((file) => file.content),
|
.map((file) => file.content),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Rebuild slugToPermalink map
|
||||||
|
ctx.slugToPermalink = {}
|
||||||
|
for (const [_tree, file] of processedFiles) {
|
||||||
|
if (file.data.permalinkSlug && file.data.slug) {
|
||||||
|
ctx.slugToPermalink[file.data.slug] = file.data.permalinkSlug
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let emittedFiles = 0
|
let emittedFiles = 0
|
||||||
for (const emitter of cfg.plugins.emitters) {
|
for (const emitter of cfg.plugins.emitters) {
|
||||||
// Try to use partialEmit if available, otherwise assume the output is static
|
// Try to use partialEmit if available, otherwise assume the output is static
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types"
|
import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types"
|
||||||
import style from "./styles/backlinks.scss"
|
import style from "./styles/backlinks.scss"
|
||||||
import { resolveRelative, simplifySlug } from "../util/path"
|
import { resolveRelative, simplifySlug, getDisplaySlug } from "../util/path"
|
||||||
import { i18n } from "../i18n"
|
import { i18n } from "../i18n"
|
||||||
import { classNames } from "../util/lang"
|
import { classNames } from "../util/lang"
|
||||||
import OverflowListFactory from "./OverflowList"
|
import OverflowListFactory from "./OverflowList"
|
||||||
@ -35,7 +35,10 @@ export default ((opts?: Partial<BacklinksOptions>) => {
|
|||||||
{backlinkFiles.length > 0 ? (
|
{backlinkFiles.length > 0 ? (
|
||||||
backlinkFiles.map((f) => (
|
backlinkFiles.map((f) => (
|
||||||
<li>
|
<li>
|
||||||
<a href={resolveRelative(fileData.slug!, f.slug!)} class="internal">
|
<a
|
||||||
|
href={resolveRelative(getDisplaySlug(fileData), getDisplaySlug(f))}
|
||||||
|
class="internal"
|
||||||
|
>
|
||||||
{f.frontmatter?.title}
|
{f.frontmatter?.title}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types"
|
import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types"
|
||||||
import breadcrumbsStyle from "./styles/breadcrumbs.scss"
|
import breadcrumbsStyle from "./styles/breadcrumbs.scss"
|
||||||
import { FullSlug, SimpleSlug, resolveRelative, simplifySlug } from "../util/path"
|
import { FullSlug, SimpleSlug, resolveRelative, simplifySlug, getDisplaySlug } from "../util/path"
|
||||||
import { classNames } from "../util/lang"
|
import { classNames } from "../util/lang"
|
||||||
import { trieFromAllFiles } from "../util/ctx"
|
import { trieFromAllFiles } from "../util/ctx"
|
||||||
|
|
||||||
@ -59,7 +59,7 @@ export default ((opts?: Partial<BreadcrumbOptions>) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const crumbs: CrumbData[] = pathNodes.map((node, idx) => {
|
const crumbs: CrumbData[] = pathNodes.map((node, idx) => {
|
||||||
const crumb = formatCrumb(node.displayName, fileData.slug!, simplifySlug(node.slug))
|
const crumb = formatCrumb(node.displayName, getDisplaySlug(fileData), simplifySlug(node.slug))
|
||||||
if (idx === 0) {
|
if (idx === 0) {
|
||||||
crumb.displayName = options.rootName
|
crumb.displayName = options.rootName
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { FullSlug, isFolderPath, resolveRelative } from "../util/path"
|
import { FullSlug, isFolderPath, resolveRelative, getDisplaySlug } from "../util/path"
|
||||||
import { QuartzPluginData } from "../plugins/vfile"
|
import { QuartzPluginData } from "../plugins/vfile"
|
||||||
import { Date, getDate } from "./Date"
|
import { Date, getDate } from "./Date"
|
||||||
import { QuartzComponent, QuartzComponentProps } from "./types"
|
import { QuartzComponent, QuartzComponentProps } from "./types"
|
||||||
@ -78,7 +78,10 @@ export const PageList: QuartzComponent = ({ cfg, fileData, allFiles, limit, sort
|
|||||||
</p>
|
</p>
|
||||||
<div class="desc">
|
<div class="desc">
|
||||||
<h3>
|
<h3>
|
||||||
<a href={resolveRelative(fileData.slug!, page.slug!)} class="internal">
|
<a
|
||||||
|
href={resolveRelative(getDisplaySlug(fileData), getDisplaySlug(page))}
|
||||||
|
class="internal"
|
||||||
|
>
|
||||||
{title}
|
{title}
|
||||||
</a>
|
</a>
|
||||||
</h3>
|
</h3>
|
||||||
@ -88,7 +91,7 @@ export const PageList: QuartzComponent = ({ cfg, fileData, allFiles, limit, sort
|
|||||||
<li>
|
<li>
|
||||||
<a
|
<a
|
||||||
class="internal tag-link"
|
class="internal tag-link"
|
||||||
href={resolveRelative(fileData.slug!, `tags/${tag}` as FullSlug)}
|
href={resolveRelative(getDisplaySlug(fileData), `tags/${tag}` as FullSlug)}
|
||||||
>
|
>
|
||||||
{tag}
|
{tag}
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types"
|
import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types"
|
||||||
import { FullSlug, SimpleSlug, resolveRelative } from "../util/path"
|
import { FullSlug, SimpleSlug, resolveRelative, getDisplaySlug } from "../util/path"
|
||||||
import { QuartzPluginData } from "../plugins/vfile"
|
import { QuartzPluginData } from "../plugins/vfile"
|
||||||
import { byDateAndAlphabetical } from "./PageList"
|
import { byDateAndAlphabetical } from "./PageList"
|
||||||
import style from "./styles/recentNotes.scss"
|
import style from "./styles/recentNotes.scss"
|
||||||
@ -48,7 +48,10 @@ export default ((userOpts?: Partial<Options>) => {
|
|||||||
<div class="section">
|
<div class="section">
|
||||||
<div class="desc">
|
<div class="desc">
|
||||||
<h3>
|
<h3>
|
||||||
<a href={resolveRelative(fileData.slug!, page.slug!)} class="internal">
|
<a
|
||||||
|
href={resolveRelative(getDisplaySlug(fileData), getDisplaySlug(page))}
|
||||||
|
class="internal"
|
||||||
|
>
|
||||||
{title}
|
{title}
|
||||||
</a>
|
</a>
|
||||||
</h3>
|
</h3>
|
||||||
@ -64,7 +67,10 @@ export default ((userOpts?: Partial<Options>) => {
|
|||||||
<li>
|
<li>
|
||||||
<a
|
<a
|
||||||
class="internal tag-link"
|
class="internal tag-link"
|
||||||
href={resolveRelative(fileData.slug!, `tags/${tag}` as FullSlug)}
|
href={resolveRelative(
|
||||||
|
getDisplaySlug(fileData),
|
||||||
|
`tags/${tag}` as FullSlug,
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
{tag}
|
{tag}
|
||||||
</a>
|
</a>
|
||||||
@ -79,7 +85,7 @@ export default ((userOpts?: Partial<Options>) => {
|
|||||||
</ul>
|
</ul>
|
||||||
{opts.linkToMore && remaining > 0 && (
|
{opts.linkToMore && remaining > 0 && (
|
||||||
<p>
|
<p>
|
||||||
<a href={resolveRelative(fileData.slug!, opts.linkToMore)}>
|
<a href={resolveRelative(getDisplaySlug(fileData), opts.linkToMore)}>
|
||||||
{i18n(cfg.locale).components.recentNotes.seeRemainingMore({ remaining })}
|
{i18n(cfg.locale).components.recentNotes.seeRemainingMore({ remaining })}
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { FullSlug, resolveRelative } from "../util/path"
|
import { FullSlug, resolveRelative, getDisplaySlug } from "../util/path"
|
||||||
import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types"
|
import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types"
|
||||||
import { classNames } from "../util/lang"
|
import { classNames } from "../util/lang"
|
||||||
|
|
||||||
@ -8,7 +8,7 @@ const TagList: QuartzComponent = ({ fileData, displayClass }: QuartzComponentPro
|
|||||||
return (
|
return (
|
||||||
<ul class={classNames(displayClass, "tags")}>
|
<ul class={classNames(displayClass, "tags")}>
|
||||||
{tags.map((tag) => {
|
{tags.map((tag) => {
|
||||||
const linkDest = resolveRelative(fileData.slug!, `tags/${tag}` as FullSlug)
|
const linkDest = resolveRelative(getDisplaySlug(fileData), `tags/${tag}` as FullSlug)
|
||||||
return (
|
return (
|
||||||
<li>
|
<li>
|
||||||
<a href={linkDest} class="internal tag-link">
|
<a href={linkDest} class="internal tag-link">
|
||||||
|
|||||||
@ -1,7 +1,13 @@
|
|||||||
import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "../types"
|
import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "../types"
|
||||||
import style from "../styles/listPage.scss"
|
import style from "../styles/listPage.scss"
|
||||||
import { PageList, SortFn } from "../PageList"
|
import { PageList, SortFn } from "../PageList"
|
||||||
import { FullSlug, getAllSegmentPrefixes, resolveRelative, simplifySlug } from "../../util/path"
|
import {
|
||||||
|
FullSlug,
|
||||||
|
getAllSegmentPrefixes,
|
||||||
|
resolveRelative,
|
||||||
|
simplifySlug,
|
||||||
|
getDisplaySlug,
|
||||||
|
} from "../../util/path"
|
||||||
import { QuartzPluginData } from "../../plugins/vfile"
|
import { QuartzPluginData } from "../../plugins/vfile"
|
||||||
import { Root } from "hast"
|
import { Root } from "hast"
|
||||||
import { htmlToJsx } from "../../util/jsx"
|
import { htmlToJsx } from "../../util/jsx"
|
||||||
@ -75,7 +81,7 @@ export default ((opts?: Partial<TagContentOptions>) => {
|
|||||||
: htmlToJsx(contentPage.filePath!, root)
|
: htmlToJsx(contentPage.filePath!, root)
|
||||||
|
|
||||||
const tagListingPage = `/tags/${tag}` as FullSlug
|
const tagListingPage = `/tags/${tag}` as FullSlug
|
||||||
const href = resolveRelative(fileData.slug!, tagListingPage)
|
const href = resolveRelative(getDisplaySlug(fileData), tagListingPage)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@ -84,11 +84,13 @@ function createFileNode(currentSlug: FullSlug, node: FileTrieNode): HTMLLIElemen
|
|||||||
const clone = template.content.cloneNode(true) as DocumentFragment
|
const clone = template.content.cloneNode(true) as DocumentFragment
|
||||||
const li = clone.querySelector("li") as HTMLLIElement
|
const li = clone.querySelector("li") as HTMLLIElement
|
||||||
const a = li.querySelector("a") as HTMLAnchorElement
|
const a = li.querySelector("a") as HTMLAnchorElement
|
||||||
a.href = resolveRelative(currentSlug, node.slug)
|
// Use displaySlug if available (for permalink support), otherwise use slug
|
||||||
|
const linkSlug = (node.data as any)?.displaySlug ?? node.slug
|
||||||
|
a.href = resolveRelative(currentSlug, linkSlug)
|
||||||
a.dataset.for = node.slug
|
a.dataset.for = node.slug
|
||||||
a.textContent = node.displayName
|
a.textContent = node.displayName
|
||||||
|
|
||||||
if (currentSlug === node.slug) {
|
if (currentSlug === linkSlug || currentSlug === node.slug) {
|
||||||
a.classList.add("active")
|
a.classList.add("active")
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -115,7 +117,9 @@ function createFolderNode(
|
|||||||
// Replace button with link for link behavior
|
// Replace button with link for link behavior
|
||||||
const button = titleContainer.querySelector(".folder-button") as HTMLElement
|
const button = titleContainer.querySelector(".folder-button") as HTMLElement
|
||||||
const a = document.createElement("a")
|
const a = document.createElement("a")
|
||||||
a.href = resolveRelative(currentSlug, folderPath)
|
// Use displaySlug if available (for permalink support), otherwise use folderPath
|
||||||
|
const linkSlug = (node.data as any)?.displaySlug ?? folderPath
|
||||||
|
a.href = resolveRelative(currentSlug, linkSlug)
|
||||||
a.dataset.for = folderPath
|
a.dataset.for = folderPath
|
||||||
a.className = "folder-title"
|
a.className = "folder-title"
|
||||||
a.textContent = node.displayName
|
a.textContent = node.displayName
|
||||||
|
|||||||
@ -6,7 +6,9 @@ import { VFile } from "vfile"
|
|||||||
import path from "path"
|
import path from "path"
|
||||||
|
|
||||||
async function* processFile(ctx: BuildCtx, file: VFile) {
|
async function* processFile(ctx: BuildCtx, file: VFile) {
|
||||||
const ogSlug = simplifySlug(file.data.slug!)
|
// Use permalinkSlug if set, otherwise use the regular slug
|
||||||
|
const targetSlug = file.data.permalinkSlug ?? file.data.slug!
|
||||||
|
const ogSlug = simplifySlug(targetSlug)
|
||||||
|
|
||||||
for (const aliasTarget of file.data.aliases ?? []) {
|
for (const aliasTarget of file.data.aliases ?? []) {
|
||||||
const aliasTargetSlug = (
|
const aliasTargetSlug = (
|
||||||
|
|||||||
@ -11,6 +11,7 @@ import { i18n } from "../../i18n"
|
|||||||
export type ContentIndexMap = Map<FullSlug, ContentDetails>
|
export type ContentIndexMap = Map<FullSlug, ContentDetails>
|
||||||
export type ContentDetails = {
|
export type ContentDetails = {
|
||||||
slug: FullSlug
|
slug: FullSlug
|
||||||
|
displaySlug?: FullSlug
|
||||||
filePath: FilePath
|
filePath: FilePath
|
||||||
title: string
|
title: string
|
||||||
links: SimpleSlug[]
|
links: SimpleSlug[]
|
||||||
@ -46,7 +47,9 @@ function generateSiteMap(cfg: GlobalConfiguration, idx: ContentIndexMap): string
|
|||||||
${content.date && `<lastmod>${content.date.toISOString()}</lastmod>`}
|
${content.date && `<lastmod>${content.date.toISOString()}</lastmod>`}
|
||||||
</url>`
|
</url>`
|
||||||
const urls = Array.from(idx)
|
const urls = Array.from(idx)
|
||||||
.map(([slug, content]) => createURLEntry(simplifySlug(slug), content))
|
.map(([_, content]) =>
|
||||||
|
createURLEntry(simplifySlug(content.displaySlug ?? content.slug), content),
|
||||||
|
)
|
||||||
.join("")
|
.join("")
|
||||||
return `<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:xhtml="http://www.w3.org/1999/xhtml">${urls}</urlset>`
|
return `<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:xhtml="http://www.w3.org/1999/xhtml">${urls}</urlset>`
|
||||||
}
|
}
|
||||||
@ -74,7 +77,9 @@ function generateRSSFeed(cfg: GlobalConfiguration, idx: ContentIndexMap, limit?:
|
|||||||
|
|
||||||
return f1.title.localeCompare(f2.title)
|
return f1.title.localeCompare(f2.title)
|
||||||
})
|
})
|
||||||
.map(([slug, content]) => createURLEntry(simplifySlug(slug), content))
|
.map(([_, content]) =>
|
||||||
|
createURLEntry(simplifySlug(content.displaySlug ?? content.slug), content),
|
||||||
|
)
|
||||||
.slice(0, limit ?? idx.size)
|
.slice(0, limit ?? idx.size)
|
||||||
.join("")
|
.join("")
|
||||||
|
|
||||||
@ -100,11 +105,14 @@ export const ContentIndex: QuartzEmitterPlugin<Partial<Options>> = (opts) => {
|
|||||||
const cfg = ctx.cfg.configuration
|
const cfg = ctx.cfg.configuration
|
||||||
const linkIndex: ContentIndexMap = new Map()
|
const linkIndex: ContentIndexMap = new Map()
|
||||||
for (const [tree, file] of content) {
|
for (const [tree, file] of content) {
|
||||||
|
// Always use the original slug for tree structure
|
||||||
const slug = file.data.slug!
|
const slug = file.data.slug!
|
||||||
const date = getDate(ctx.cfg.configuration, file.data) ?? new Date()
|
const date = getDate(ctx.cfg.configuration, file.data) ?? new Date()
|
||||||
if (opts?.includeEmptyFiles || (file.data.text && file.data.text !== "")) {
|
if (opts?.includeEmptyFiles || (file.data.text && file.data.text !== "")) {
|
||||||
linkIndex.set(slug, {
|
linkIndex.set(slug, {
|
||||||
slug,
|
slug,
|
||||||
|
// Add displaySlug if permalink is set
|
||||||
|
displaySlug: file.data.permalinkSlug,
|
||||||
filePath: file.data.relativePath!,
|
filePath: file.data.relativePath!,
|
||||||
title: file.data.frontmatter?.title!,
|
title: file.data.frontmatter?.title!,
|
||||||
links: file.data.links ?? [],
|
links: file.data.links ?? [],
|
||||||
|
|||||||
@ -24,8 +24,10 @@ async function processContent(
|
|||||||
resources: StaticResources,
|
resources: StaticResources,
|
||||||
) {
|
) {
|
||||||
const slug = fileData.slug!
|
const slug = fileData.slug!
|
||||||
|
// Use permalinkSlug if set, otherwise use the regular slug
|
||||||
|
const renderSlug = fileData.permalinkSlug ?? slug
|
||||||
const cfg = ctx.cfg.configuration
|
const cfg = ctx.cfg.configuration
|
||||||
const externalResources = pageResources(pathToRoot(slug), resources)
|
const externalResources = pageResources(pathToRoot(renderSlug), resources)
|
||||||
const componentData: QuartzComponentProps = {
|
const componentData: QuartzComponentProps = {
|
||||||
ctx,
|
ctx,
|
||||||
fileData,
|
fileData,
|
||||||
@ -36,11 +38,11 @@ async function processContent(
|
|||||||
allFiles,
|
allFiles,
|
||||||
}
|
}
|
||||||
|
|
||||||
const content = renderPage(cfg, slug, componentData, opts, externalResources)
|
const content = renderPage(cfg, renderSlug, componentData, opts, externalResources)
|
||||||
return write({
|
return write({
|
||||||
ctx,
|
ctx,
|
||||||
content,
|
content,
|
||||||
slug,
|
slug: renderSlug,
|
||||||
ext: ".html",
|
ext: ".html",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@ -72,7 +72,7 @@ async function processOgImage(
|
|||||||
fullOptions: SocialImageOptions,
|
fullOptions: SocialImageOptions,
|
||||||
) {
|
) {
|
||||||
const cfg = ctx.cfg.configuration
|
const cfg = ctx.cfg.configuration
|
||||||
const slug = fileData.slug!
|
const slug = fileData.permalinkSlug ?? fileData.slug!
|
||||||
const titleSuffix = cfg.pageTitleSuffix ?? ""
|
const titleSuffix = cfg.pageTitleSuffix ?? ""
|
||||||
const title =
|
const title =
|
||||||
(fileData.frontmatter?.title ?? i18n(cfg.locale).propertyDefaults.title) + titleSuffix
|
(fileData.frontmatter?.title ?? i18n(cfg.locale).propertyDefaults.title) + titleSuffix
|
||||||
@ -154,7 +154,7 @@ export const CustomOgImages: QuartzEmitterPlugin<Partial<SocialImageOptions>> =
|
|||||||
}
|
}
|
||||||
|
|
||||||
const generatedOgImagePath = isRealFile
|
const generatedOgImagePath = isRealFile
|
||||||
? `https://${baseUrl}/${pageData.slug!}-og-image.webp`
|
? `https://${baseUrl}/${pageData.permalinkSlug ?? pageData.slug!}-og-image.webp`
|
||||||
: undefined
|
: undefined
|
||||||
const defaultOgImagePath = `https://${baseUrl}/static/og-image.png`
|
const defaultOgImagePath = `https://${baseUrl}/static/og-image.png`
|
||||||
const ogImagePath = userDefinedOgImagePath ?? generatedOgImagePath ?? defaultOgImagePath
|
const ogImagePath = userDefinedOgImagePath ?? generatedOgImagePath ?? defaultOgImagePath
|
||||||
|
|||||||
@ -87,12 +87,21 @@ export const FrontMatter: QuartzTransformerPlugin<Partial<Options>> = (userOpts)
|
|||||||
allSlugs.push(...file.data.aliases)
|
allSlugs.push(...file.data.aliases)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle permalink: it should be the actual URL where the page is rendered
|
||||||
|
// Keep the original slug for sidebar structure, use permalinkSlug for rendering
|
||||||
if (data.permalink != null && data.permalink.toString() !== "") {
|
if (data.permalink != null && data.permalink.toString() !== "") {
|
||||||
data.permalink = data.permalink.toString() as FullSlug
|
data.permalink = data.permalink.toString() as FullSlug
|
||||||
const aliases = file.data.aliases ?? []
|
const originalSlug = file.data.slug
|
||||||
aliases.push(data.permalink)
|
|
||||||
file.data.aliases = aliases
|
// Set the permalinkSlug - this is where the page will actually be rendered
|
||||||
|
file.data.permalinkSlug = data.permalink
|
||||||
allSlugs.push(data.permalink)
|
allSlugs.push(data.permalink)
|
||||||
|
|
||||||
|
// Create a redirect from the original file path to the permalink
|
||||||
|
if (originalSlug && originalSlug !== data.permalink) {
|
||||||
|
file.data.aliases = file.data.aliases ?? []
|
||||||
|
file.data.aliases.push(originalSlug)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const cssclasses = coerceToArray(coalesceAliases(data, ["cssclasses", "cssclass"]))
|
const cssclasses = coerceToArray(coalesceAliases(data, ["cssclasses", "cssclass"]))
|
||||||
@ -135,6 +144,7 @@ export const FrontMatter: QuartzTransformerPlugin<Partial<Options>> = (userOpts)
|
|||||||
declare module "vfile" {
|
declare module "vfile" {
|
||||||
interface DataMap {
|
interface DataMap {
|
||||||
aliases: FullSlug[]
|
aliases: FullSlug[]
|
||||||
|
permalinkSlug?: FullSlug
|
||||||
frontmatter: { [key: string]: unknown } & {
|
frontmatter: { [key: string]: unknown } & {
|
||||||
title: string
|
title: string
|
||||||
} & Partial<{
|
} & Partial<{
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import {
|
|||||||
simplifySlug,
|
simplifySlug,
|
||||||
splitAnchor,
|
splitAnchor,
|
||||||
transformLink,
|
transformLink,
|
||||||
|
resolveRelative,
|
||||||
} from "../../util/path"
|
} from "../../util/path"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import { visit } from "unist-util-visit"
|
import { visit } from "unist-util-visit"
|
||||||
@ -113,7 +114,7 @@ export const CrawlLinks: QuartzTransformerPlugin<Partial<Options>> = (userOpts)
|
|||||||
// WHATWG equivalent https://nodejs.dev/en/api/v18/url/#urlresolvefrom-to
|
// WHATWG equivalent https://nodejs.dev/en/api/v18/url/#urlresolvefrom-to
|
||||||
const url = new URL(dest, "https://base.com/" + stripSlashes(curSlug, true))
|
const url = new URL(dest, "https://base.com/" + stripSlashes(curSlug, true))
|
||||||
const canonicalDest = url.pathname
|
const canonicalDest = url.pathname
|
||||||
let [destCanonical, _destAnchor] = splitAnchor(canonicalDest)
|
let [destCanonical, destAnchor] = splitAnchor(canonicalDest)
|
||||||
if (destCanonical.endsWith("/")) {
|
if (destCanonical.endsWith("/")) {
|
||||||
destCanonical += "index"
|
destCanonical += "index"
|
||||||
}
|
}
|
||||||
@ -123,6 +124,14 @@ export const CrawlLinks: QuartzTransformerPlugin<Partial<Options>> = (userOpts)
|
|||||||
const simple = simplifySlug(full)
|
const simple = simplifySlug(full)
|
||||||
outgoing.add(simple)
|
outgoing.add(simple)
|
||||||
node.properties["data-slug"] = full
|
node.properties["data-slug"] = full
|
||||||
|
|
||||||
|
// If the target file has a permalink, rewrite the href to use it
|
||||||
|
if (ctx.slugToPermalink[full]) {
|
||||||
|
const permalinkSlug = ctx.slugToPermalink[full]
|
||||||
|
const currentSlug = file.data.permalinkSlug ?? file.data.slug!
|
||||||
|
const newHref = resolveRelative(currentSlug, permalinkSlug) + destAnchor
|
||||||
|
node.properties.href = newHref
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// rewrite link internals if prettylinks is on
|
// rewrite link internals if prettylinks is on
|
||||||
|
|||||||
@ -181,6 +181,7 @@ export async function parseMarkdown(ctx: BuildCtx, fps: FilePath[]): Promise<Pro
|
|||||||
argv: ctx.argv,
|
argv: ctx.argv,
|
||||||
allSlugs: ctx.allSlugs,
|
allSlugs: ctx.allSlugs,
|
||||||
allFiles: ctx.allFiles,
|
allFiles: ctx.allFiles,
|
||||||
|
slugToPermalink: ctx.slugToPermalink,
|
||||||
incremental: ctx.incremental,
|
incremental: ctx.incremental,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -27,6 +27,7 @@ export interface BuildCtx {
|
|||||||
cfg: QuartzConfig
|
cfg: QuartzConfig
|
||||||
allSlugs: FullSlug[]
|
allSlugs: FullSlug[]
|
||||||
allFiles: FilePath[]
|
allFiles: FilePath[]
|
||||||
|
slugToPermalink: Record<FullSlug, FullSlug>
|
||||||
trie?: FileTrieNode<BuildTimeTrieData>
|
trie?: FileTrieNode<BuildTimeTrieData>
|
||||||
incremental: boolean
|
incremental: boolean
|
||||||
}
|
}
|
||||||
|
|||||||
@ -173,6 +173,11 @@ export function resolveRelative(current: FullSlug, target: FullSlug | SimpleSlug
|
|||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Get the slug to use for linking to a page (uses permalink if available) */
|
||||||
|
export function getDisplaySlug(data: { slug?: FullSlug; permalinkSlug?: FullSlug }): FullSlug {
|
||||||
|
return data.permalinkSlug ?? data.slug ?? ("" as FullSlug)
|
||||||
|
}
|
||||||
|
|
||||||
export function splitAnchor(link: string): [string, string] {
|
export function splitAnchor(link: string): [string, string] {
|
||||||
let [fp, anchor] = link.split("#", 2)
|
let [fp, anchor] = link.split("#", 2)
|
||||||
if (fp.endsWith(".pdf")) {
|
if (fp.endsWith(".pdf")) {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user