Compare commits

...

2 Commits

Author SHA1 Message Date
Jacky Zhao
f301eca9a7 custom font spec 2025-03-12 22:56:59 -07:00
Jacky Zhao
1fb7756c49 fix 2025-03-12 22:26:39 -07:00
6 changed files with 98 additions and 53 deletions

View File

@ -107,27 +107,35 @@ export const myImage: SocialImageOptions["imageStructure"] = (...) => {
> import fs from "fs" > import fs from "fs"
> import path from "path" > import path from "path"
> >
> const headerFont = joinSegments(QUARTZ, "static", "Newsreader.woff2") > const newsreaderFontPath = joinSegments(QUARTZ, "static", "Newsreader.woff2")
> const bodyFont = joinSegments(QUARTZ, "static", "Newsreader.woff2") > export async function getSatoriFonts(headerFont: FontSpecification, bodyFont: FontSpecification) {
> > // ... rest of implementation remains same
> export async function getSatoriFont(cfg: GlobalConfiguration): Promise<SatoriOptions["fonts"]> { > const fonts: SatoriOptions["fonts"] = [
> const headerWeight: FontWeight = 700 > ...headerFontData.map((data, idx) => ({
> const bodyWeight: FontWeight = 400 > name: headerFontName,
> > data,
> const [header, body] = await Promise.all( > weight: headerWeights[idx],
> [headerFont, bodyFont].map((font) => > style: "normal" as const,
> fs.promises.readFile(path.resolve(font)) > })),
> ), > ...bodyFontData.map((data, idx) => ({
> ) > name: bodyFontName,
> > data,
> return [ > weight: bodyWeights[idx],
> { name: cfg.theme.typography.header, data: header, weight: headerWeight, style: "normal" }, > style: "normal" as const,
> { name: cfg.theme.typography.body, data: body, weight: bodyWeight, style: "normal" }, > })),
> {
> name: "Newsreader",
> data: await fs.promises.readFile(path.resolve(newsreaderFontPath)),
> weight: 400,
> style: "normal" as const,
> },
> ] > ]
>
> return fonts
> } > }
> ``` > ```
> >
> This font then can be used with your custom structure > This font then can be used with your custom structure.
## Examples ## Examples

View File

@ -250,15 +250,25 @@ async function partialRebuildFromEntrypoint(
([_node, vfile]) => !toRemove.has(vfile.data.filePath!), ([_node, vfile]) => !toRemove.has(vfile.data.filePath!),
) )
const emittedFps = await emitter.emit(ctx, files, staticResources) const emitted = await emitter.emit(ctx, files, staticResources)
if (Symbol.asyncIterator in emitted) {
// Async generator case
for await (const file of emitted) {
emittedFiles++
if (ctx.argv.verbose) { if (ctx.argv.verbose) {
for (const file of emittedFps) {
console.log(`[emit:${emitter.name}] ${file}`) console.log(`[emit:${emitter.name}] ${file}`)
} }
} }
} else {
// Array case
emittedFiles += emitted.length
if (ctx.argv.verbose) {
for (const file of emitted) {
console.log(`[emit:${emitter.name}] ${file}`)
}
}
}
emittedFiles += emittedFps.length
continue continue
} }
@ -280,15 +290,24 @@ async function partialRebuildFromEntrypoint(
.filter((file) => !toRemove.has(file)) .filter((file) => !toRemove.has(file))
.map((file) => contentMap.get(file)!) .map((file) => contentMap.get(file)!)
const emittedFps = await emitter.emit(ctx, upstreamContent, staticResources) const emitted = await emitter.emit(ctx, upstreamContent, staticResources)
if (Symbol.asyncIterator in emitted) {
// Async generator case
for await (const file of emitted) {
emittedFiles++
if (ctx.argv.verbose) { if (ctx.argv.verbose) {
for (const file of emittedFps) {
console.log(`[emit:${emitter.name}] ${file}`) console.log(`[emit:${emitter.name}] ${file}`)
} }
} }
} else {
emittedFiles += emittedFps.length // Array case
emittedFiles += emitted.length
if (ctx.argv.verbose) {
for (const file of emitted) {
console.log(`[emit:${emitter.name}] ${file}`)
}
}
}
} }
} }

View File

@ -2,7 +2,7 @@ import { QuartzEmitterPlugin } from "../types"
import { i18n } from "../../i18n" import { i18n } from "../../i18n"
import { unescapeHTML } from "../../util/escape" import { unescapeHTML } from "../../util/escape"
import { FullSlug, getFileExtension } from "../../util/path" import { FullSlug, getFileExtension } from "../../util/path"
import { ImageOptions, SocialImageOptions, defaultImage, getSatoriFont } from "../../util/og" import { ImageOptions, SocialImageOptions, defaultImage, getSatoriFonts } from "../../util/og"
import { getFontSpecificationName } from "../../util/theme" import { getFontSpecificationName } from "../../util/theme"
import sharp from "sharp" import sharp from "sharp"
import satori from "satori" import satori from "satori"
@ -54,9 +54,9 @@ export const CustomOgImages: QuartzEmitterPlugin<Partial<SocialImageOptions>> =
}, },
async *emit(ctx, content, _resources) { async *emit(ctx, content, _resources) {
const cfg = ctx.cfg.configuration const cfg = ctx.cfg.configuration
const headerFont = getFontSpecificationName(cfg.theme.typography.header) const headerFont = cfg.theme.typography.header
const bodyFont = getFontSpecificationName(cfg.theme.typography.body) const bodyFont = cfg.theme.typography.body
const fonts = await getSatoriFont(headerFont, bodyFont) const fonts = await getSatoriFonts(headerFont, bodyFont)
for (const [_tree, vfile] of content) { for (const [_tree, vfile] of content) {
// if this file defines socialImage, we can skip // if this file defines socialImage, we can skip

View File

@ -20,9 +20,7 @@ export async function emitContent(ctx: BuildCtx, content: ProcessedContent[]) {
const emitted = await emitter.emit(ctx, content, staticResources) const emitted = await emitter.emit(ctx, content, staticResources)
if (Symbol.asyncIterator in emitted) { if (Symbol.asyncIterator in emitted) {
// Async generator case // Async generator case
const files: string[] = []
for await (const file of emitted) { for await (const file of emitted) {
files.push(file)
emittedFiles++ emittedFiles++
if (ctx.argv.verbose) { if (ctx.argv.verbose) {
console.log(`[emit:${emitter.name}] ${file}`) console.log(`[emit:${emitter.name}] ${file}`)

View File

@ -2,29 +2,49 @@ import { FontWeight, SatoriOptions } from "satori/wasm"
import { GlobalConfiguration } from "../cfg" import { GlobalConfiguration } from "../cfg"
import { QuartzPluginData } from "../plugins/vfile" import { QuartzPluginData } from "../plugins/vfile"
import { JSXInternal } from "preact/src/jsx" import { JSXInternal } from "preact/src/jsx"
import { ThemeKey } from "./theme" import { FontSpecification, ThemeKey } from "./theme"
/** const defaultHeaderWeight = [700]
* Get an array of `FontOptions` (for satori) given google font names const defaultBodyWeight = [400]
* @param headerFontName name of google font used for header export async function getSatoriFonts(headerFont: FontSpecification, bodyFont: FontSpecification) {
* @param bodyFontName name of google font used for body // Get all weights for header and body fonts
* @returns FontOptions for header and body const headerWeights: FontWeight[] = (
*/ typeof headerFont === "string"
export async function getSatoriFont(headerFontName: string, bodyFontName: string) { ? defaultHeaderWeight
const headerWeight = 700 as FontWeight : (headerFont.weights ?? defaultHeaderWeight)
const bodyWeight = 400 as FontWeight ) as FontWeight[]
const bodyWeights: FontWeight[] = (
typeof bodyFont === "string" ? defaultBodyWeight : (bodyFont.weights ?? defaultBodyWeight)
) as FontWeight[]
// Fetch fonts const headerFontName = typeof headerFont === "string" ? headerFont : headerFont.name
const [headerFont, bodyFont] = await Promise.all([ const bodyFontName = typeof bodyFont === "string" ? bodyFont : bodyFont.name
fetchTtf(headerFontName, headerWeight),
fetchTtf(bodyFontName, bodyWeight), // Fetch fonts for all weights
const headerFontPromises = headerWeights.map((weight) => fetchTtf(headerFontName, weight))
const bodyFontPromises = bodyWeights.map((weight) => fetchTtf(bodyFontName, weight))
const [headerFontData, bodyFontData] = await Promise.all([
Promise.all(headerFontPromises),
Promise.all(bodyFontPromises),
]) ])
// Convert fonts to satori font format and return // Convert fonts to satori font format and return
const fonts: SatoriOptions["fonts"] = [ const fonts: SatoriOptions["fonts"] = [
{ name: headerFontName, data: headerFont, weight: headerWeight, style: "normal" }, ...headerFontData.map((data, idx) => ({
{ name: bodyFontName, data: bodyFont, weight: bodyWeight, style: "normal" }, name: headerFontName,
data,
weight: headerWeights[idx],
style: "normal" as const,
})),
...bodyFontData.map((data, idx) => ({
name: bodyFontName,
data,
weight: bodyWeights[idx],
style: "normal" as const,
})),
] ]
return fonts return fonts
} }

View File

@ -15,7 +15,7 @@ interface Colors {
darkMode: ColorScheme darkMode: ColorScheme
} }
type FontSpecification = export type FontSpecification =
| string | string
| { | {
name: string name: string