mirror of
https://github.com/jackyzha0/quartz.git
synced 2025-07-09 05:51:00 +02:00
replace spinner, use disk cache for fonts
This commit is contained in:
parent
f301eca9a7
commit
07ffc8681e
10
package-lock.json
generated
10
package-lock.json
generated
@ -75,7 +75,6 @@
|
|||||||
"quartz": "quartz/bootstrap-cli.mjs"
|
"quartz": "quartz/bootstrap-cli.mjs"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/cli-spinner": "^0.2.3",
|
|
||||||
"@types/d3": "^7.4.3",
|
"@types/d3": "^7.4.3",
|
||||||
"@types/hast": "^3.0.4",
|
"@types/hast": "^3.0.4",
|
||||||
"@types/js-yaml": "^4.0.9",
|
"@types/js-yaml": "^4.0.9",
|
||||||
@ -1585,15 +1584,6 @@
|
|||||||
"integrity": "sha512-XKLA6syeBUaPzx4j3qwMqzzq+V4uo72BnlbOjmuljLrRqdsd3qnzvZZoxvMHZ23ndsRS4aufU6JOZYpCbU6T1A==",
|
"integrity": "sha512-XKLA6syeBUaPzx4j3qwMqzzq+V4uo72BnlbOjmuljLrRqdsd3qnzvZZoxvMHZ23ndsRS4aufU6JOZYpCbU6T1A==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@types/cli-spinner": {
|
|
||||||
"version": "0.2.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/cli-spinner/-/cli-spinner-0.2.3.tgz",
|
|
||||||
"integrity": "sha512-TMO6mWltW0lCu1de8DMRq9+59OP/tEjghS+rs8ZEQ2EgYP5yV3bGw0tS14TMyJGqFaoVChNvhkVzv9RC1UgX+w==",
|
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
|
||||||
"@types/node": "*"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@types/css-font-loading-module": {
|
"node_modules/@types/css-font-loading-module": {
|
||||||
"version": "0.0.12",
|
"version": "0.0.12",
|
||||||
"resolved": "https://registry.npmjs.org/@types/css-font-loading-module/-/css-font-loading-module-0.0.12.tgz",
|
"resolved": "https://registry.npmjs.org/@types/css-font-loading-module/-/css-font-loading-module-0.0.12.tgz",
|
||||||
|
@ -98,7 +98,6 @@
|
|||||||
"yargs": "^17.7.2"
|
"yargs": "^17.7.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/cli-spinner": "^0.2.3",
|
|
||||||
"@types/d3": "^7.4.3",
|
"@types/d3": "^7.4.3",
|
||||||
"@types/hast": "^3.0.4",
|
"@types/hast": "^3.0.4",
|
||||||
"@types/js-yaml": "^4.0.9",
|
"@types/js-yaml": "^4.0.9",
|
||||||
|
@ -87,6 +87,7 @@ const config: QuartzConfig = {
|
|||||||
Plugin.Assets(),
|
Plugin.Assets(),
|
||||||
Plugin.Static(),
|
Plugin.Static(),
|
||||||
Plugin.NotFoundPage(),
|
Plugin.NotFoundPage(),
|
||||||
|
Plugin.CustomOgImages(),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,6 @@ 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, getSatoriFonts } from "../../util/og"
|
import { ImageOptions, SocialImageOptions, defaultImage, getSatoriFonts } from "../../util/og"
|
||||||
import { getFontSpecificationName } from "../../util/theme"
|
|
||||||
import sharp from "sharp"
|
import sharp from "sharp"
|
||||||
import satori from "satori"
|
import satori from "satori"
|
||||||
import { loadEmoji, getIconCode } from "../../util/emoji"
|
import { loadEmoji, getIconCode } from "../../util/emoji"
|
||||||
|
@ -4,6 +4,7 @@ import { ProcessedContent } from "../plugins/vfile"
|
|||||||
import { QuartzLogger } from "../util/log"
|
import { QuartzLogger } from "../util/log"
|
||||||
import { trace } from "../util/trace"
|
import { trace } from "../util/trace"
|
||||||
import { BuildCtx } from "../util/ctx"
|
import { BuildCtx } from "../util/ctx"
|
||||||
|
import chalk from "chalk"
|
||||||
|
|
||||||
export async function emitContent(ctx: BuildCtx, content: ProcessedContent[]) {
|
export async function emitContent(ctx: BuildCtx, content: ProcessedContent[]) {
|
||||||
const { argv, cfg } = ctx
|
const { argv, cfg } = ctx
|
||||||
@ -24,14 +25,18 @@ export async function emitContent(ctx: BuildCtx, content: ProcessedContent[]) {
|
|||||||
emittedFiles++
|
emittedFiles++
|
||||||
if (ctx.argv.verbose) {
|
if (ctx.argv.verbose) {
|
||||||
console.log(`[emit:${emitter.name}] ${file}`)
|
console.log(`[emit:${emitter.name}] ${file}`)
|
||||||
|
} else {
|
||||||
|
log.updateText(`Emitting output files: ${chalk.gray(file)}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Array case
|
// Array case
|
||||||
emittedFiles += emitted.length
|
emittedFiles += emitted.length
|
||||||
if (ctx.argv.verbose) {
|
|
||||||
for (const file of emitted) {
|
for (const file of emitted) {
|
||||||
|
if (ctx.argv.verbose) {
|
||||||
console.log(`[emit:${emitter.name}] ${file}`)
|
console.log(`[emit:${emitter.name}] ${file}`)
|
||||||
|
} else {
|
||||||
|
log.updateText(`Emitting output files: ${chalk.gray(file)}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,26 +1,41 @@
|
|||||||
import { Spinner } from "cli-spinner"
|
|
||||||
|
|
||||||
export class QuartzLogger {
|
export class QuartzLogger {
|
||||||
verbose: boolean
|
verbose: boolean
|
||||||
spinner: Spinner | undefined
|
private spinnerInterval: NodeJS.Timeout | undefined
|
||||||
|
private spinnerText: string = ""
|
||||||
|
private spinnerIndex: number = 0
|
||||||
|
private readonly spinnerChars = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]
|
||||||
|
|
||||||
constructor(verbose: boolean) {
|
constructor(verbose: boolean) {
|
||||||
this.verbose = verbose
|
this.verbose = verbose
|
||||||
}
|
}
|
||||||
|
|
||||||
start(text: string) {
|
start(text: string) {
|
||||||
|
this.spinnerText = text
|
||||||
if (this.verbose) {
|
if (this.verbose) {
|
||||||
console.log(text)
|
console.log(text)
|
||||||
} else {
|
} else {
|
||||||
this.spinner = new Spinner(`%s ${text}`)
|
this.spinnerIndex = 0
|
||||||
this.spinner.setSpinnerString(18)
|
this.spinnerInterval = setInterval(() => {
|
||||||
this.spinner.start()
|
process.stdout.clearLine(0)
|
||||||
|
process.stdout.cursorTo(0)
|
||||||
|
process.stdout.write(`${this.spinnerChars[this.spinnerIndex]} ${this.spinnerText}`)
|
||||||
|
this.spinnerIndex = (this.spinnerIndex + 1) % this.spinnerChars.length
|
||||||
|
}, 100)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
end(text?: string) {
|
updateText(text: string) {
|
||||||
if (!this.verbose) {
|
this.spinnerText = text
|
||||||
this.spinner!.stop(true)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
end(text?: string) {
|
||||||
|
if (!this.verbose && this.spinnerInterval) {
|
||||||
|
clearInterval(this.spinnerInterval)
|
||||||
|
this.spinnerInterval = undefined
|
||||||
|
process.stdout.clearLine(0)
|
||||||
|
process.stdout.cursorTo(0)
|
||||||
|
}
|
||||||
|
|
||||||
if (text) {
|
if (text) {
|
||||||
console.log(text)
|
console.log(text)
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,11 @@
|
|||||||
|
import { promises as fs } from "fs"
|
||||||
import { FontWeight, SatoriOptions } from "satori/wasm"
|
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 { FontSpecification, ThemeKey } from "./theme"
|
import { FontSpecification, ThemeKey } from "./theme"
|
||||||
|
import path from "path"
|
||||||
|
import { QUARTZ } from "./path"
|
||||||
|
|
||||||
const defaultHeaderWeight = [700]
|
const defaultHeaderWeight = [700]
|
||||||
const defaultBodyWeight = [400]
|
const defaultBodyWeight = [400]
|
||||||
@ -48,24 +51,28 @@ export async function getSatoriFonts(headerFont: FontSpecification, bodyFont: Fo
|
|||||||
return fonts
|
return fonts
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cache for memoizing font data
|
|
||||||
const fontCache = new Map<string, Promise<ArrayBuffer>>()
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the `.ttf` file of a google font
|
* Get the `.ttf` file of a google font
|
||||||
* @param fontName name of google font
|
* @param fontName name of google font
|
||||||
* @param weight what font weight to fetch font
|
* @param weight what font weight to fetch font
|
||||||
* @returns `.ttf` file of google font
|
* @returns `.ttf` file of google font
|
||||||
*/
|
*/
|
||||||
export async function fetchTtf(fontName: string, weight: FontWeight): Promise<ArrayBuffer> {
|
export async function fetchTtf(
|
||||||
const cacheKey = `${fontName}-${weight}`
|
fontName: string,
|
||||||
if (fontCache.has(cacheKey)) {
|
weight: FontWeight,
|
||||||
return fontCache.get(cacheKey)!
|
): Promise<Buffer<ArrayBufferLike>> {
|
||||||
|
const cacheKey = `${fontName.replaceAll(" ", "-")}-${weight}`
|
||||||
|
const cacheDir = path.join(QUARTZ, ".quartz-cache", "fonts")
|
||||||
|
const cachePath = path.join(cacheDir, cacheKey)
|
||||||
|
|
||||||
|
// Check if font exists in cache
|
||||||
|
try {
|
||||||
|
await fs.access(cachePath)
|
||||||
|
return fs.readFile(cachePath)
|
||||||
|
} catch (error) {
|
||||||
|
// ignore errors and fetch font
|
||||||
}
|
}
|
||||||
|
|
||||||
// If not in cache, fetch and store the promise
|
|
||||||
const fontPromise = (async () => {
|
|
||||||
try {
|
|
||||||
// Get css file from google fonts
|
// Get css file from google fonts
|
||||||
const cssResponse = await fetch(
|
const cssResponse = await fetch(
|
||||||
`https://fonts.googleapis.com/css2?family=${fontName}:wght@${weight}`,
|
`https://fonts.googleapis.com/css2?family=${fontName}:wght@${weight}`,
|
||||||
@ -80,16 +87,19 @@ export async function fetchTtf(fontName: string, weight: FontWeight): Promise<Ar
|
|||||||
throw new Error("Could not fetch font")
|
throw new Error("Could not fetch font")
|
||||||
}
|
}
|
||||||
|
|
||||||
// fontData is an ArrayBuffer containing the .ttf file data (get match[1] due to google fonts response format, always contains link twice, but second entry is the "raw" link)
|
// fontData is an ArrayBuffer containing the .ttf file data
|
||||||
const fontResponse = await fetch(match[1])
|
const fontResponse = await fetch(match[1])
|
||||||
return await fontResponse.arrayBuffer()
|
const fontData = Buffer.from(await fontResponse.arrayBuffer())
|
||||||
} catch (error) {
|
|
||||||
throw new Error(`Error fetching font: ${error}`)
|
|
||||||
}
|
|
||||||
})()
|
|
||||||
|
|
||||||
fontCache.set(cacheKey, fontPromise)
|
try {
|
||||||
return fontPromise
|
await fs.mkdir(cacheDir, { recursive: true })
|
||||||
|
await fs.writeFile(cachePath, fontData)
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(`Failed to cache font: ${error}`)
|
||||||
|
// Continue even if caching fails
|
||||||
|
}
|
||||||
|
|
||||||
|
return fontData
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SocialImageOptions = {
|
export type SocialImageOptions = {
|
||||||
@ -161,7 +171,7 @@ export const defaultImage: SocialImageOptions["imageStructure"] = (
|
|||||||
title: string,
|
title: string,
|
||||||
description: string,
|
description: string,
|
||||||
fonts: SatoriOptions["fonts"],
|
fonts: SatoriOptions["fonts"],
|
||||||
_fileData: QuartzPluginData,
|
fileData: QuartzPluginData,
|
||||||
) => {
|
) => {
|
||||||
const fontBreakPoint = 22
|
const fontBreakPoint = 22
|
||||||
const useSmallerFont = title.length > fontBreakPoint
|
const useSmallerFont = title.length > fontBreakPoint
|
||||||
@ -177,8 +187,8 @@ export const defaultImage: SocialImageOptions["imageStructure"] = (
|
|||||||
height: "100%",
|
height: "100%",
|
||||||
width: "100%",
|
width: "100%",
|
||||||
backgroundColor: cfg.theme.colors[colorScheme].light,
|
backgroundColor: cfg.theme.colors[colorScheme].light,
|
||||||
gap: "2rem",
|
gap: "1rem",
|
||||||
padding: "1.5rem 5rem",
|
padding: "3rem 3rem",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@ -187,31 +197,36 @@ export const defaultImage: SocialImageOptions["imageStructure"] = (
|
|||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
width: "100%",
|
width: "100%",
|
||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
gap: "2.5rem",
|
gap: "2rem",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
border: "1px solid red",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<img src={iconPath} width={135} height={135} />
|
<img src={iconPath} width={135} height={135} />
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
color: cfg.theme.colors[colorScheme].dark,
|
color: cfg.theme.colors[colorScheme].dark,
|
||||||
fontSize: useSmallerFont ? 70 : 82,
|
maxWidth: "80%",
|
||||||
fontFamily: fonts[0].name,
|
|
||||||
maxWidth: "70%",
|
|
||||||
overflow: "hidden",
|
|
||||||
textOverflow: "ellipsis",
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<p
|
<h1
|
||||||
style={{
|
style={{
|
||||||
margin: 0,
|
margin: 0,
|
||||||
overflow: "hidden",
|
overflow: "hidden",
|
||||||
textOverflow: "ellipsis",
|
textOverflow: "ellipsis",
|
||||||
whiteSpace: "nowrap",
|
whiteSpace: "nowrap",
|
||||||
|
fontSize: useSmallerFont ? 64 : 72,
|
||||||
|
fontFamily: fonts[0].name,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{title}
|
{title}
|
||||||
</p>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
@ -221,7 +236,7 @@ export const defaultImage: SocialImageOptions["imageStructure"] = (
|
|||||||
fontSize: 44,
|
fontSize: 44,
|
||||||
fontFamily: fonts[1].name,
|
fontFamily: fonts[1].name,
|
||||||
maxWidth: "100%",
|
maxWidth: "100%",
|
||||||
maxHeight: "40%",
|
maxHeight: "60%",
|
||||||
overflow: "hidden",
|
overflow: "hidden",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@ -230,7 +245,7 @@ export const defaultImage: SocialImageOptions["imageStructure"] = (
|
|||||||
margin: 0,
|
margin: 0,
|
||||||
display: "-webkit-box",
|
display: "-webkit-box",
|
||||||
WebkitBoxOrient: "vertical",
|
WebkitBoxOrient: "vertical",
|
||||||
WebkitLineClamp: 3,
|
WebkitLineClamp: 5,
|
||||||
overflow: "hidden",
|
overflow: "hidden",
|
||||||
textOverflow: "ellipsis",
|
textOverflow: "ellipsis",
|
||||||
}}
|
}}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user