diff --git a/quartz.config.ts b/quartz.config.ts
index 46aff1b7a..a8410ddd5 100644
--- a/quartz.config.ts
+++ b/quartz.config.ts
@@ -72,20 +72,21 @@ const config: QuartzConfig = {
],
filters: [ExternalPlugin.RemoveDrafts()],
emitters: [
- Plugin.AliasRedirects(),
+ ExternalPlugin.AliasRedirects(),
Plugin.ComponentResources(),
- Plugin.ContentIndex({
+ ExternalPlugin.ContentIndex({
enableSiteMap: true,
enableRSS: true,
}),
Plugin.Assets(),
Plugin.Static(),
- Plugin.Favicon(),
+ ExternalPlugin.Favicon(),
Plugin.PageTypes.PageTypeDispatcher({
defaults: layout.defaults,
byPageType: layout.byPageType,
}),
- Plugin.CustomOgImages(),
+ ExternalPlugin.CustomOgImages(),
+ ExternalPlugin.CNAME(),
],
pageTypes: [
ExternalPlugin.ContentPage(),
@@ -124,6 +125,11 @@ const config: QuartzConfig = {
"github:quartz-community/roam",
"github:quartz-community/remove-draft",
"github:quartz-community/explicit-publish",
+ "github:quartz-community/alias-redirects",
+ "github:quartz-community/cname",
+ "github:quartz-community/favicon",
+ "github:quartz-community/content-index",
+ "github:quartz-community/og-image",
],
}
diff --git a/quartz.lock.json b/quartz.lock.json
index 1e0f3597a..0e73a7b76 100644
--- a/quartz.lock.json
+++ b/quartz.lock.json
@@ -186,6 +186,36 @@
"resolved": "https://github.com/quartz-community/explicit-publish.git",
"commit": "f870ac6a13d297c0f4451986c7f7042f006c3b38",
"installedAt": "2026-02-13T22:28:08.774Z"
+ },
+ "alias-redirects": {
+ "source": "github:quartz-community/alias-redirects",
+ "resolved": "https://github.com/quartz-community/alias-redirects.git",
+ "commit": "e890973e16a4365a989306ccc9fb9c4591128897",
+ "installedAt": "2026-02-13T22:53:31.585Z"
+ },
+ "cname": {
+ "source": "github:quartz-community/cname",
+ "resolved": "https://github.com/quartz-community/cname.git",
+ "commit": "1d2cf8aba20e1687c9e54fc40ce20048e55648f5",
+ "installedAt": "2026-02-13T22:53:45.670Z"
+ },
+ "favicon": {
+ "source": "github:quartz-community/favicon",
+ "resolved": "https://github.com/quartz-community/favicon.git",
+ "commit": "319c6ac41e56c63f2d2f503c78fc18e59e70bd8d",
+ "installedAt": "2026-02-13T22:53:55.256Z"
+ },
+ "content-index": {
+ "source": "github:quartz-community/content-index",
+ "resolved": "https://github.com/quartz-community/content-index.git",
+ "commit": "2eb7bfd0dc1e6a625542eadb470009e77990b056",
+ "installedAt": "2026-02-13T22:59:01.330Z"
+ },
+ "og-image": {
+ "source": "github:quartz-community/og-image",
+ "resolved": "https://github.com/quartz-community/og-image.git",
+ "commit": "d68d1fba330d97661c3b4a5df73149c4e20e7c56",
+ "installedAt": "2026-02-13T22:59:01.745Z"
}
}
}
diff --git a/quartz/components/Head.tsx b/quartz/components/Head.tsx
index 23183ca8c..0c1a20468 100644
--- a/quartz/components/Head.tsx
+++ b/quartz/components/Head.tsx
@@ -4,7 +4,7 @@ import { CSSResourceToStyleElement, JSResourceToScriptElement } from "../util/re
import { googleFontHref, googleFontSubsetHref } from "../util/theme"
import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types"
import { unescapeHTML } from "../util/escape"
-import { CustomOgImagesEmitterName } from "../plugins/emitters/ogImage"
+import { CustomOgImagesEmitterName } from "../../.quartz/plugins"
export default (() => {
const Head: QuartzComponent = ({
cfg,
diff --git a/quartz/plugins/emitters/aliases.ts b/quartz/plugins/emitters/aliases.ts
deleted file mode 100644
index 9cb9bd576..000000000
--- a/quartz/plugins/emitters/aliases.ts
+++ /dev/null
@@ -1,55 +0,0 @@
-import { FullSlug, isRelativeURL, resolveRelative, simplifySlug } from "../../util/path"
-import { QuartzEmitterPlugin } from "../types"
-import { write } from "./helpers"
-import { BuildCtx } from "../../util/ctx"
-import { VFile } from "vfile"
-import path from "path"
-
-async function* processFile(ctx: BuildCtx, file: VFile) {
- const ogSlug = simplifySlug(file.data.slug!)
-
- for (const aliasTarget of file.data.aliases ?? []) {
- const aliasTargetSlug = (
- isRelativeURL(aliasTarget)
- ? path.normalize(path.join(ogSlug, "..", aliasTarget))
- : aliasTarget
- ) as FullSlug
-
- const redirUrl = resolveRelative(aliasTargetSlug, ogSlug)
- yield write({
- ctx,
- content: `
-
-
-
- ${ogSlug}
-
-
-
-
-
-
- `,
- slug: aliasTargetSlug,
- ext: ".html",
- })
- }
-}
-
-export const AliasRedirects: QuartzEmitterPlugin = () => ({
- name: "AliasRedirects",
- async *emit(ctx, content) {
- for (const [_tree, file] of content) {
- yield* processFile(ctx, file)
- }
- },
- async *partialEmit(ctx, _content, _resources, changeEvents) {
- for (const changeEvent of changeEvents) {
- if (!changeEvent.file) continue
- if (changeEvent.type === "add" || changeEvent.type === "change") {
- // add new ones if this file still exists
- yield* processFile(ctx, changeEvent.file)
- }
- }
- },
-})
diff --git a/quartz/plugins/emitters/cname.ts b/quartz/plugins/emitters/cname.ts
deleted file mode 100644
index 64fdbab68..000000000
--- a/quartz/plugins/emitters/cname.ts
+++ /dev/null
@@ -1,34 +0,0 @@
-import { QuartzEmitterPlugin } from "../types"
-import { write } from "./helpers"
-import { styleText } from "util"
-import { FullSlug } from "../../util/path"
-
-export function extractDomainFromBaseUrl(baseUrl: string) {
- const url = new URL(`https://${baseUrl}`)
- return url.hostname
-}
-
-export const CNAME: QuartzEmitterPlugin = () => ({
- name: "CNAME",
- async emit(ctx) {
- if (!ctx.cfg.configuration.baseUrl) {
- console.warn(
- styleText("yellow", "CNAME emitter requires `baseUrl` to be set in your configuration"),
- )
- return []
- }
- const content = extractDomainFromBaseUrl(ctx.cfg.configuration.baseUrl)
- if (!content) {
- return []
- }
-
- const path = await write({
- ctx,
- content,
- slug: "CNAME" as FullSlug,
- ext: "",
- })
- return [path]
- },
- async *partialEmit() {},
-})
diff --git a/quartz/plugins/emitters/contentIndex.tsx b/quartz/plugins/emitters/contentIndex.tsx
deleted file mode 100644
index 56392b358..000000000
--- a/quartz/plugins/emitters/contentIndex.tsx
+++ /dev/null
@@ -1,174 +0,0 @@
-import { Root } from "hast"
-import { GlobalConfiguration } from "../../cfg"
-import { getDate } from "../../components/Date"
-import { escapeHTML } from "../../util/escape"
-import { FilePath, FullSlug, SimpleSlug, joinSegments, simplifySlug } from "../../util/path"
-import { QuartzEmitterPlugin } from "../types"
-import { toHtml } from "hast-util-to-html"
-import { write } from "./helpers"
-import { i18n } from "../../i18n"
-
-export type ContentIndexMap = Map
-export type ContentDetails = {
- slug: FullSlug
- filePath: FilePath
- title: string
- links: SimpleSlug[]
- tags: string[]
- content: string
- richContent?: string
- date?: Date
- description?: string
-}
-
-interface Options {
- enableSiteMap: boolean
- enableRSS: boolean
- rssLimit?: number
- rssFullHtml: boolean
- rssSlug: string
- includeEmptyFiles: boolean
-}
-
-const defaultOptions: Options = {
- enableSiteMap: true,
- enableRSS: true,
- rssLimit: 10,
- rssFullHtml: false,
- rssSlug: "index",
- includeEmptyFiles: true,
-}
-
-function generateSiteMap(cfg: GlobalConfiguration, idx: ContentIndexMap): string {
- const base = cfg.baseUrl ?? ""
- const createURLEntry = (slug: SimpleSlug, content: ContentDetails): string => `
- https://${joinSegments(base, encodeURI(slug))}
- ${content.date && `${content.date.toISOString()}`}
- `
- const urls = Array.from(idx)
- .map(([slug, content]) => createURLEntry(simplifySlug(slug), content))
- .join("")
- return `${urls}`
-}
-
-function generateRSSFeed(cfg: GlobalConfiguration, idx: ContentIndexMap, limit?: number): string {
- const base = cfg.baseUrl ?? ""
-
- const createURLEntry = (slug: SimpleSlug, content: ContentDetails): string => `-
- ${escapeHTML(content.title)}
- https://${joinSegments(base, encodeURI(slug))}
- https://${joinSegments(base, encodeURI(slug))}
-
- ${content.date?.toUTCString()}
-
`
-
- const items = Array.from(idx)
- .sort(([_, f1], [__, f2]) => {
- if (f1.date && f2.date) {
- return f2.date.getTime() - f1.date.getTime()
- } else if (f1.date && !f2.date) {
- return -1
- } else if (!f1.date && f2.date) {
- return 1
- }
-
- return f1.title.localeCompare(f2.title)
- })
- .map(([slug, content]) => createURLEntry(simplifySlug(slug), content))
- .slice(0, limit ?? idx.size)
- .join("")
-
- return `
-
-
- ${escapeHTML(cfg.pageTitle)}
- https://${base}
- ${!!limit ? i18n(cfg.locale).pages.rss.lastFewNotes({ count: limit }) : i18n(cfg.locale).pages.rss.recentNotes} on ${escapeHTML(
- cfg.pageTitle,
- )}
- Quartz -- quartz.jzhao.xyz
- ${items}
-
- `
-}
-
-export const ContentIndex: QuartzEmitterPlugin> = (opts) => {
- opts = { ...defaultOptions, ...opts }
- return {
- name: "ContentIndex",
- async *emit(ctx, content) {
- const cfg = ctx.cfg.configuration
- const linkIndex: ContentIndexMap = new Map()
- for (const [tree, file] of content) {
- const slug = file.data.slug!
- const date = getDate(ctx.cfg.configuration, file.data) ?? new Date()
- if (opts?.includeEmptyFiles || (file.data.text && file.data.text !== "")) {
- linkIndex.set(slug, {
- slug,
- filePath: file.data.relativePath!,
- title: file.data.frontmatter?.title!,
- links: file.data.links ?? [],
- tags: file.data.frontmatter?.tags ?? [],
- content: file.data.text ?? "",
- richContent: opts?.rssFullHtml
- ? escapeHTML(toHtml(tree as Root, { allowDangerousHtml: true }))
- : undefined,
- date: date,
- description: file.data.description ?? "",
- })
- }
- }
-
- if (opts?.enableSiteMap) {
- yield write({
- ctx,
- content: generateSiteMap(cfg, linkIndex),
- slug: "sitemap" as FullSlug,
- ext: ".xml",
- })
- }
-
- if (opts?.enableRSS) {
- yield write({
- ctx,
- content: generateRSSFeed(cfg, linkIndex, opts.rssLimit),
- slug: (opts?.rssSlug ?? "index") as FullSlug,
- ext: ".xml",
- })
- }
-
- const fp = joinSegments("static", "contentIndex") as FullSlug
- const simplifiedIndex = Object.fromEntries(
- Array.from(linkIndex).map(([slug, content]) => {
- // remove description and from content index as nothing downstream
- // actually uses it. we only keep it in the index as we need it
- // for the RSS feed
- delete content.description
- delete content.date
- return [slug, content]
- }),
- )
-
- yield write({
- ctx,
- content: JSON.stringify(simplifiedIndex),
- slug: fp,
- ext: ".json",
- })
- },
- externalResources: (ctx) => {
- if (opts?.enableRSS) {
- return {
- additionalHead: [
- ,
- ],
- }
- }
- },
- }
-}
diff --git a/quartz/plugins/emitters/favicon.ts b/quartz/plugins/emitters/favicon.ts
deleted file mode 100644
index b05f9309d..000000000
--- a/quartz/plugins/emitters/favicon.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-import sharp from "sharp"
-import { joinSegments, QUARTZ, FullSlug } from "../../util/path"
-import { QuartzEmitterPlugin } from "../types"
-import { write } from "./helpers"
-import { BuildCtx } from "../../util/ctx"
-
-export const Favicon: QuartzEmitterPlugin = () => ({
- name: "Favicon",
- async *emit({ argv }) {
- const iconPath = joinSegments(QUARTZ, "static", "icon.png")
-
- const faviconContent = sharp(iconPath).resize(48, 48).toFormat("png")
-
- yield write({
- ctx: { argv } as BuildCtx,
- slug: "favicon" as FullSlug,
- ext: ".ico",
- content: faviconContent,
- })
- },
- async *partialEmit() {},
-})
diff --git a/quartz/plugins/emitters/index.ts b/quartz/plugins/emitters/index.ts
index 08fee89ed..bba0892b8 100644
--- a/quartz/plugins/emitters/index.ts
+++ b/quartz/plugins/emitters/index.ts
@@ -1,8 +1,3 @@
-export { ContentIndex as ContentIndex } from "./contentIndex"
-export { AliasRedirects } from "./aliases"
export { Assets } from "./assets"
export { Static } from "./static"
-export { Favicon } from "./favicon"
export { ComponentResources } from "./componentResources"
-export { CNAME } from "./cname"
-export { CustomOgImages } from "./ogImage"
diff --git a/quartz/plugins/emitters/ogImage.tsx b/quartz/plugins/emitters/ogImage.tsx
deleted file mode 100644
index 813d9348c..000000000
--- a/quartz/plugins/emitters/ogImage.tsx
+++ /dev/null
@@ -1,182 +0,0 @@
-import { QuartzEmitterPlugin } from "../types"
-import { i18n } from "../../i18n"
-import { unescapeHTML } from "../../util/escape"
-import { FullSlug, getFileExtension, isAbsoluteURL, joinSegments, QUARTZ } from "../../util/path"
-import { ImageOptions, SocialImageOptions, defaultImage, getSatoriFonts } from "../../util/og"
-import sharp from "sharp"
-import satori, { SatoriOptions } from "satori"
-import { loadEmoji, getIconCode } from "../../util/emoji"
-import { Readable } from "stream"
-import { write } from "./helpers"
-import { BuildCtx } from "../../util/ctx"
-import { QuartzPluginData } from "../vfile"
-import fs from "node:fs/promises"
-import { styleText } from "util"
-
-const defaultOptions: SocialImageOptions = {
- colorScheme: "lightMode",
- width: 1200,
- height: 630,
- imageStructure: defaultImage,
- excludeRoot: false,
-}
-
-/**
- * Generates social image (OG/twitter standard) and saves it as `.webp` inside the public folder
- * @param opts options for generating image
- */
-async function generateSocialImage(
- { cfg, description, fonts, title, fileData }: ImageOptions,
- userOpts: SocialImageOptions,
-): Promise {
- const { width, height } = userOpts
- const iconPath = joinSegments(QUARTZ, "static", "icon.png")
- let iconBase64: string | undefined = undefined
- try {
- const iconData = await fs.readFile(iconPath)
- iconBase64 = `data:image/png;base64,${iconData.toString("base64")}`
- } catch (err) {
- console.warn(styleText("yellow", `Warning: Could not find icon at ${iconPath}`))
- }
-
- const imageComponent = userOpts.imageStructure({
- cfg,
- userOpts,
- title,
- description,
- fonts,
- fileData,
- iconBase64,
- })
-
- const svg = await satori(imageComponent, {
- width,
- height,
- fonts,
- loadAdditionalAsset: async (languageCode: string, segment: string) => {
- if (languageCode === "emoji") {
- return await loadEmoji(getIconCode(segment))
- }
-
- return languageCode
- },
- })
-
- return sharp(Buffer.from(svg)).webp({ quality: 40 })
-}
-
-async function processOgImage(
- ctx: BuildCtx,
- fileData: QuartzPluginData,
- fonts: SatoriOptions["fonts"],
- fullOptions: SocialImageOptions,
-) {
- const cfg = ctx.cfg.configuration
- const slug = fileData.slug!
- const titleSuffix = cfg.pageTitleSuffix ?? ""
- const title =
- (fileData.frontmatter?.title ?? i18n(cfg.locale).propertyDefaults.title) + titleSuffix
- const description =
- fileData.frontmatter?.socialDescription ??
- fileData.frontmatter?.description ??
- unescapeHTML(fileData.description?.trim() ?? i18n(cfg.locale).propertyDefaults.description)
-
- const stream = await generateSocialImage(
- {
- title,
- description,
- fonts,
- cfg,
- fileData,
- },
- fullOptions,
- )
-
- return write({
- ctx,
- content: stream,
- slug: `${slug}-og-image` as FullSlug,
- ext: ".webp",
- })
-}
-
-export const CustomOgImagesEmitterName = "CustomOgImages"
-export const CustomOgImages: QuartzEmitterPlugin> = (userOpts) => {
- const fullOptions = { ...defaultOptions, ...userOpts }
-
- return {
- name: CustomOgImagesEmitterName,
- getQuartzComponents() {
- return []
- },
- async *emit(ctx, content, _resources) {
- const cfg = ctx.cfg.configuration
- const headerFont = cfg.theme.typography.header
- const bodyFont = cfg.theme.typography.body
- const fonts = await getSatoriFonts(headerFont, bodyFont)
-
- for (const [_tree, vfile] of content) {
- if (vfile.data.frontmatter?.socialImage !== undefined) continue
- yield processOgImage(ctx, vfile.data, fonts, fullOptions)
- }
- },
- async *partialEmit(ctx, _content, _resources, changeEvents) {
- const cfg = ctx.cfg.configuration
- const headerFont = cfg.theme.typography.header
- const bodyFont = cfg.theme.typography.body
- const fonts = await getSatoriFonts(headerFont, bodyFont)
-
- // find all slugs that changed or were added
- for (const changeEvent of changeEvents) {
- if (!changeEvent.file) continue
- if (changeEvent.file.data.frontmatter?.socialImage !== undefined) continue
- if (changeEvent.type === "add" || changeEvent.type === "change") {
- yield processOgImage(ctx, changeEvent.file.data, fonts, fullOptions)
- }
- }
- },
- externalResources: (ctx) => {
- if (!ctx.cfg.configuration.baseUrl) {
- return {}
- }
-
- const baseUrl = ctx.cfg.configuration.baseUrl
- return {
- additionalHead: [
- (pageData) => {
- const isRealFile = pageData.filePath !== undefined
- let userDefinedOgImagePath = pageData.frontmatter?.socialImage
-
- if (userDefinedOgImagePath) {
- userDefinedOgImagePath = isAbsoluteURL(userDefinedOgImagePath)
- ? userDefinedOgImagePath
- : `https://${baseUrl}/static/${userDefinedOgImagePath}`
- }
-
- const generatedOgImagePath = isRealFile
- ? `https://${baseUrl}/${pageData.slug!}-og-image.webp`
- : undefined
- const defaultOgImagePath = `https://${baseUrl}/static/og-image.png`
- const ogImagePath = userDefinedOgImagePath ?? generatedOgImagePath ?? defaultOgImagePath
- const ogImageMimeType = `image/${getFileExtension(ogImagePath) ?? "png"}`
- return (
- <>
- {!userDefinedOgImagePath && (
- <>
-
-
- >
- )}
-
-
-
-
-
- >
- )
- },
- ],
- }
- },
- }
-}
diff --git a/quartz/util/fileTrie.ts b/quartz/util/fileTrie.ts
index 9e6706f4a..f4a4e789b 100644
--- a/quartz/util/fileTrie.ts
+++ b/quartz/util/fileTrie.ts
@@ -1,4 +1,4 @@
-import { ContentDetails } from "../plugins/emitters/contentIndex"
+import { ContentDetails } from "../../.quartz/plugins"
import { FullSlug, joinSegments } from "./path"
interface FileTrieData {