diff --git a/package-lock.json b/package-lock.json index b887f5bdc..fff318046 100644 --- a/package-lock.json +++ b/package-lock.json @@ -75,7 +75,6 @@ "quartz": "quartz/bootstrap-cli.mjs" }, "devDependencies": { - "@types/cli-spinner": "^0.2.3", "@types/d3": "^7.4.3", "@types/hast": "^3.0.4", "@types/js-yaml": "^4.0.9", @@ -1585,15 +1584,6 @@ "integrity": "sha512-XKLA6syeBUaPzx4j3qwMqzzq+V4uo72BnlbOjmuljLrRqdsd3qnzvZZoxvMHZ23ndsRS4aufU6JOZYpCbU6T1A==", "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": { "version": "0.0.12", "resolved": "https://registry.npmjs.org/@types/css-font-loading-module/-/css-font-loading-module-0.0.12.tgz", diff --git a/package.json b/package.json index 4ee8cfc72..2e1587487 100644 --- a/package.json +++ b/package.json @@ -98,7 +98,6 @@ "yargs": "^17.7.2" }, "devDependencies": { - "@types/cli-spinner": "^0.2.3", "@types/d3": "^7.4.3", "@types/hast": "^3.0.4", "@types/js-yaml": "^4.0.9", diff --git a/quartz.config.ts b/quartz.config.ts index 4b51e16cc..5264b64b7 100644 --- a/quartz.config.ts +++ b/quartz.config.ts @@ -87,6 +87,7 @@ const config: QuartzConfig = { Plugin.Assets(), Plugin.Static(), Plugin.NotFoundPage(), + Plugin.CustomOgImages(), ], }, } diff --git a/quartz/plugins/emitters/ogImage.tsx b/quartz/plugins/emitters/ogImage.tsx index f2a2bc431..056976a77 100644 --- a/quartz/plugins/emitters/ogImage.tsx +++ b/quartz/plugins/emitters/ogImage.tsx @@ -3,7 +3,6 @@ import { i18n } from "../../i18n" import { unescapeHTML } from "../../util/escape" import { FullSlug, getFileExtension } from "../../util/path" import { ImageOptions, SocialImageOptions, defaultImage, getSatoriFonts } from "../../util/og" -import { getFontSpecificationName } from "../../util/theme" import sharp from "sharp" import satori from "satori" import { loadEmoji, getIconCode } from "../../util/emoji" diff --git a/quartz/processors/emit.ts b/quartz/processors/emit.ts index b4d4aecd7..0d3f8d764 100644 --- a/quartz/processors/emit.ts +++ b/quartz/processors/emit.ts @@ -4,6 +4,7 @@ import { ProcessedContent } from "../plugins/vfile" import { QuartzLogger } from "../util/log" import { trace } from "../util/trace" import { BuildCtx } from "../util/ctx" +import chalk from "chalk" export async function emitContent(ctx: BuildCtx, content: ProcessedContent[]) { const { argv, cfg } = ctx @@ -24,14 +25,18 @@ export async function emitContent(ctx: BuildCtx, content: ProcessedContent[]) { emittedFiles++ if (ctx.argv.verbose) { console.log(`[emit:${emitter.name}] ${file}`) + } else { + log.updateText(`Emitting output files: ${chalk.gray(file)}`) } } } else { // Array case 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}`) + } else { + log.updateText(`Emitting output files: ${chalk.gray(file)}`) } } } diff --git a/quartz/util/log.ts b/quartz/util/log.ts index 773945c97..ea27fb848 100644 --- a/quartz/util/log.ts +++ b/quartz/util/log.ts @@ -1,26 +1,41 @@ -import { Spinner } from "cli-spinner" - export class QuartzLogger { verbose: boolean - spinner: Spinner | undefined + private spinnerInterval: NodeJS.Timeout | undefined + private spinnerText: string = "" + private spinnerIndex: number = 0 + private readonly spinnerChars = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"] + constructor(verbose: boolean) { this.verbose = verbose } start(text: string) { + this.spinnerText = text if (this.verbose) { console.log(text) } else { - this.spinner = new Spinner(`%s ${text}`) - this.spinner.setSpinnerString(18) - this.spinner.start() + this.spinnerIndex = 0 + this.spinnerInterval = setInterval(() => { + 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) } } + updateText(text: string) { + this.spinnerText = text + } + end(text?: string) { - if (!this.verbose) { - this.spinner!.stop(true) + if (!this.verbose && this.spinnerInterval) { + clearInterval(this.spinnerInterval) + this.spinnerInterval = undefined + process.stdout.clearLine(0) + process.stdout.cursorTo(0) } + if (text) { console.log(text) } diff --git a/quartz/util/og.tsx b/quartz/util/og.tsx index 1ef7e4927..683f71bd1 100644 --- a/quartz/util/og.tsx +++ b/quartz/util/og.tsx @@ -1,8 +1,11 @@ +import { promises as fs } from "fs" import { FontWeight, SatoriOptions } from "satori/wasm" import { GlobalConfiguration } from "../cfg" import { QuartzPluginData } from "../plugins/vfile" import { JSXInternal } from "preact/src/jsx" import { FontSpecification, ThemeKey } from "./theme" +import path from "path" +import { QUARTZ } from "./path" const defaultHeaderWeight = [700] const defaultBodyWeight = [400] @@ -48,48 +51,55 @@ export async function getSatoriFonts(headerFont: FontSpecification, bodyFont: Fo return fonts } -// Cache for memoizing font data -const fontCache = new Map>() - /** * Get the `.ttf` file of a google font * @param fontName name of google font * @param weight what font weight to fetch font * @returns `.ttf` file of google font */ -export async function fetchTtf(fontName: string, weight: FontWeight): Promise { - const cacheKey = `${fontName}-${weight}` - if (fontCache.has(cacheKey)) { - return fontCache.get(cacheKey)! +export async function fetchTtf( + fontName: string, + weight: FontWeight, +): Promise> { + 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 - const cssResponse = await fetch( - `https://fonts.googleapis.com/css2?family=${fontName}:wght@${weight}`, - ) - const css = await cssResponse.text() + // Get css file from google fonts + const cssResponse = await fetch( + `https://fonts.googleapis.com/css2?family=${fontName}:wght@${weight}`, + ) + const css = await cssResponse.text() - // Extract .ttf url from css file - const urlRegex = /url\((https:\/\/fonts.gstatic.com\/s\/.*?.ttf)\)/g - const match = urlRegex.exec(css) + // Extract .ttf url from css file + const urlRegex = /url\((https:\/\/fonts.gstatic.com\/s\/.*?.ttf)\)/g + const match = urlRegex.exec(css) - if (!match) { - throw new Error("Could not fetch font") - } + if (!match) { + 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) - const fontResponse = await fetch(match[1]) - return await fontResponse.arrayBuffer() - } catch (error) { - throw new Error(`Error fetching font: ${error}`) - } - })() + // fontData is an ArrayBuffer containing the .ttf file data + const fontResponse = await fetch(match[1]) + const fontData = Buffer.from(await fontResponse.arrayBuffer()) - fontCache.set(cacheKey, fontPromise) - return fontPromise + try { + 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 = { @@ -161,7 +171,7 @@ export const defaultImage: SocialImageOptions["imageStructure"] = ( title: string, description: string, fonts: SatoriOptions["fonts"], - _fileData: QuartzPluginData, + fileData: QuartzPluginData, ) => { const fontBreakPoint = 22 const useSmallerFont = title.length > fontBreakPoint @@ -177,8 +187,8 @@ export const defaultImage: SocialImageOptions["imageStructure"] = ( height: "100%", width: "100%", backgroundColor: cfg.theme.colors[colorScheme].light, - gap: "2rem", - padding: "1.5rem 5rem", + gap: "1rem", + padding: "3rem 3rem", }} >
- +
+ +
-

{title} -

+
@@ -230,7 +245,7 @@ export const defaultImage: SocialImageOptions["imageStructure"] = ( margin: 0, display: "-webkit-box", WebkitBoxOrient: "vertical", - WebkitLineClamp: 3, + WebkitLineClamp: 5, overflow: "hidden", textOverflow: "ellipsis", }}