mirror of
https://github.com/jackyzha0/quartz.git
synced 2026-03-23 06:25:41 -05:00
Migrate emitters to external plugins (alias-redirects, cname, favicon, content-index, og-image)
This commit is contained in:
parent
70ef4f9fd5
commit
6b0364e2e4
@ -72,20 +72,21 @@ const config: QuartzConfig = {
|
|||||||
],
|
],
|
||||||
filters: [ExternalPlugin.RemoveDrafts()],
|
filters: [ExternalPlugin.RemoveDrafts()],
|
||||||
emitters: [
|
emitters: [
|
||||||
Plugin.AliasRedirects(),
|
ExternalPlugin.AliasRedirects(),
|
||||||
Plugin.ComponentResources(),
|
Plugin.ComponentResources(),
|
||||||
Plugin.ContentIndex({
|
ExternalPlugin.ContentIndex({
|
||||||
enableSiteMap: true,
|
enableSiteMap: true,
|
||||||
enableRSS: true,
|
enableRSS: true,
|
||||||
}),
|
}),
|
||||||
Plugin.Assets(),
|
Plugin.Assets(),
|
||||||
Plugin.Static(),
|
Plugin.Static(),
|
||||||
Plugin.Favicon(),
|
ExternalPlugin.Favicon(),
|
||||||
Plugin.PageTypes.PageTypeDispatcher({
|
Plugin.PageTypes.PageTypeDispatcher({
|
||||||
defaults: layout.defaults,
|
defaults: layout.defaults,
|
||||||
byPageType: layout.byPageType,
|
byPageType: layout.byPageType,
|
||||||
}),
|
}),
|
||||||
Plugin.CustomOgImages(),
|
ExternalPlugin.CustomOgImages(),
|
||||||
|
ExternalPlugin.CNAME(),
|
||||||
],
|
],
|
||||||
pageTypes: [
|
pageTypes: [
|
||||||
ExternalPlugin.ContentPage(),
|
ExternalPlugin.ContentPage(),
|
||||||
@ -124,6 +125,11 @@ const config: QuartzConfig = {
|
|||||||
"github:quartz-community/roam",
|
"github:quartz-community/roam",
|
||||||
"github:quartz-community/remove-draft",
|
"github:quartz-community/remove-draft",
|
||||||
"github:quartz-community/explicit-publish",
|
"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",
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -186,6 +186,36 @@
|
|||||||
"resolved": "https://github.com/quartz-community/explicit-publish.git",
|
"resolved": "https://github.com/quartz-community/explicit-publish.git",
|
||||||
"commit": "f870ac6a13d297c0f4451986c7f7042f006c3b38",
|
"commit": "f870ac6a13d297c0f4451986c7f7042f006c3b38",
|
||||||
"installedAt": "2026-02-13T22:28:08.774Z"
|
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import { CSSResourceToStyleElement, JSResourceToScriptElement } from "../util/re
|
|||||||
import { googleFontHref, googleFontSubsetHref } from "../util/theme"
|
import { googleFontHref, googleFontSubsetHref } from "../util/theme"
|
||||||
import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types"
|
import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types"
|
||||||
import { unescapeHTML } from "../util/escape"
|
import { unescapeHTML } from "../util/escape"
|
||||||
import { CustomOgImagesEmitterName } from "../plugins/emitters/ogImage"
|
import { CustomOgImagesEmitterName } from "../../.quartz/plugins"
|
||||||
export default (() => {
|
export default (() => {
|
||||||
const Head: QuartzComponent = ({
|
const Head: QuartzComponent = ({
|
||||||
cfg,
|
cfg,
|
||||||
|
|||||||
@ -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: `
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en-us">
|
|
||||||
<head>
|
|
||||||
<title>${ogSlug}</title>
|
|
||||||
<link rel="canonical" href="${redirUrl}">
|
|
||||||
<meta name="robots" content="noindex">
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<meta http-equiv="refresh" content="0; url=${redirUrl}">
|
|
||||||
</head>
|
|
||||||
</html>
|
|
||||||
`,
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})
|
|
||||||
@ -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() {},
|
|
||||||
})
|
|
||||||
@ -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<FullSlug, ContentDetails>
|
|
||||||
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 => `<url>
|
|
||||||
<loc>https://${joinSegments(base, encodeURI(slug))}</loc>
|
|
||||||
${content.date && `<lastmod>${content.date.toISOString()}</lastmod>`}
|
|
||||||
</url>`
|
|
||||||
const urls = Array.from(idx)
|
|
||||||
.map(([slug, content]) => createURLEntry(simplifySlug(slug), content))
|
|
||||||
.join("")
|
|
||||||
return `<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:xhtml="http://www.w3.org/1999/xhtml">${urls}</urlset>`
|
|
||||||
}
|
|
||||||
|
|
||||||
function generateRSSFeed(cfg: GlobalConfiguration, idx: ContentIndexMap, limit?: number): string {
|
|
||||||
const base = cfg.baseUrl ?? ""
|
|
||||||
|
|
||||||
const createURLEntry = (slug: SimpleSlug, content: ContentDetails): string => `<item>
|
|
||||||
<title>${escapeHTML(content.title)}</title>
|
|
||||||
<link>https://${joinSegments(base, encodeURI(slug))}</link>
|
|
||||||
<guid>https://${joinSegments(base, encodeURI(slug))}</guid>
|
|
||||||
<description><![CDATA[ ${content.richContent ?? content.description} ]]></description>
|
|
||||||
<pubDate>${content.date?.toUTCString()}</pubDate>
|
|
||||||
</item>`
|
|
||||||
|
|
||||||
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 `<?xml version="1.0" encoding="UTF-8" ?>
|
|
||||||
<rss version="2.0">
|
|
||||||
<channel>
|
|
||||||
<title>${escapeHTML(cfg.pageTitle)}</title>
|
|
||||||
<link>https://${base}</link>
|
|
||||||
<description>${!!limit ? i18n(cfg.locale).pages.rss.lastFewNotes({ count: limit }) : i18n(cfg.locale).pages.rss.recentNotes} on ${escapeHTML(
|
|
||||||
cfg.pageTitle,
|
|
||||||
)}</description>
|
|
||||||
<generator>Quartz -- quartz.jzhao.xyz</generator>
|
|
||||||
${items}
|
|
||||||
</channel>
|
|
||||||
</rss>`
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ContentIndex: QuartzEmitterPlugin<Partial<Options>> = (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: [
|
|
||||||
<link
|
|
||||||
rel="alternate"
|
|
||||||
type="application/rss+xml"
|
|
||||||
title="RSS Feed"
|
|
||||||
href={`https://${ctx.cfg.configuration.baseUrl}/index.xml`}
|
|
||||||
/>,
|
|
||||||
],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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() {},
|
|
||||||
})
|
|
||||||
@ -1,8 +1,3 @@
|
|||||||
export { ContentIndex as ContentIndex } from "./contentIndex"
|
|
||||||
export { AliasRedirects } from "./aliases"
|
|
||||||
export { Assets } from "./assets"
|
export { Assets } from "./assets"
|
||||||
export { Static } from "./static"
|
export { Static } from "./static"
|
||||||
export { Favicon } from "./favicon"
|
|
||||||
export { ComponentResources } from "./componentResources"
|
export { ComponentResources } from "./componentResources"
|
||||||
export { CNAME } from "./cname"
|
|
||||||
export { CustomOgImages } from "./ogImage"
|
|
||||||
|
|||||||
@ -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<Readable> {
|
|
||||||
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<Partial<SocialImageOptions>> = (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 && (
|
|
||||||
<>
|
|
||||||
<meta property="og:image:width" content={fullOptions.width.toString()} />
|
|
||||||
<meta property="og:image:height" content={fullOptions.height.toString()} />
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<meta property="og:image" content={ogImagePath} />
|
|
||||||
<meta property="og:image:url" content={ogImagePath} />
|
|
||||||
<meta name="twitter:image" content={ogImagePath} />
|
|
||||||
<meta property="og:image:type" content={ogImageMimeType} />
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,4 +1,4 @@
|
|||||||
import { ContentDetails } from "../plugins/emitters/contentIndex"
|
import { ContentDetails } from "../../.quartz/plugins"
|
||||||
import { FullSlug, joinSegments } from "./path"
|
import { FullSlug, joinSegments } from "./path"
|
||||||
|
|
||||||
interface FileTrieData {
|
interface FileTrieData {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user