mirror of
https://github.com/jackyzha0/quartz.git
synced 2025-12-01 18:27:57 +01:00
Compare commits
5 Commits
c5a8b199ae
...
jackyzha0/
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3173d185ed | ||
|
|
de727b4686 | ||
|
|
07ffc8681e | ||
|
|
f301eca9a7 | ||
|
|
1fb7756c49 |
@@ -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
|
||||||
|
|
||||||
|
|||||||
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(),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,8 +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 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"
|
||||||
@@ -54,9 +53,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
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -20,20 +21,22 @@ 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}`)
|
||||||
|
} 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,43 @@
|
|||||||
import { Spinner } from "cli-spinner"
|
import readline from "readline"
|
||||||
|
|
||||||
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()
|
readline.clearLine(process.stdout, 0)
|
||||||
|
readline.cursorTo(process.stdout, 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
|
||||||
|
readline.clearLine(process.stdout, 0)
|
||||||
|
readline.cursorTo(process.stdout, 0)
|
||||||
|
}
|
||||||
|
|
||||||
if (text) {
|
if (text) {
|
||||||
console.log(text)
|
console.log(text)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,51 +1,80 @@
|
|||||||
|
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 { ThemeKey } from "./theme"
|
import { FontSpecification, ThemeKey } from "./theme"
|
||||||
|
import path from "path"
|
||||||
|
import { QUARTZ } from "./path"
|
||||||
|
import { formatDate } from "../components/Date"
|
||||||
|
import { getDate } from "../components/Date"
|
||||||
|
|
||||||
/**
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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}`,
|
||||||
@@ -60,16 +89,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 = {
|
||||||
@@ -141,68 +173,94 @@ 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 = 32
|
||||||
const useSmallerFont = title.length > fontBreakPoint
|
const useSmallerFont = title.length > fontBreakPoint
|
||||||
const iconPath = `https://${cfg.baseUrl}/static/icon.png`
|
const iconPath = `https://${cfg.baseUrl}/static/icon.png`
|
||||||
|
|
||||||
|
// Format date if available
|
||||||
|
const rawDate = getDate(cfg, fileData)
|
||||||
|
const date = rawDate ? formatDate(rawDate, cfg.locale) : null
|
||||||
|
|
||||||
|
// Get tags if available
|
||||||
|
const tags = fileData.frontmatter?.tags ?? []
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
justifyContent: "center",
|
|
||||||
alignItems: "center",
|
|
||||||
height: "100%",
|
height: "100%",
|
||||||
width: "100%",
|
width: "100%",
|
||||||
backgroundColor: cfg.theme.colors[colorScheme].light,
|
backgroundColor: cfg.theme.colors[colorScheme].light,
|
||||||
gap: "2rem",
|
padding: "2.5rem",
|
||||||
padding: "1.5rem 5rem",
|
fontFamily: fonts[1].name,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
{/* Header Section */}
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
width: "100%",
|
gap: "1rem",
|
||||||
flexDirection: "row",
|
marginBottom: "0.5rem",
|
||||||
gap: "2.5rem",
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<img src={iconPath} width={135} height={135} />
|
<img
|
||||||
|
src={iconPath}
|
||||||
|
width={56}
|
||||||
|
height={56}
|
||||||
|
style={{
|
||||||
|
borderRadius: "50%",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
color: cfg.theme.colors[colorScheme].dark,
|
fontSize: 32,
|
||||||
fontSize: useSmallerFont ? 70 : 82,
|
color: cfg.theme.colors[colorScheme].gray,
|
||||||
fontFamily: fonts[0].name,
|
fontFamily: fonts[1].name,
|
||||||
maxWidth: "70%",
|
|
||||||
overflow: "hidden",
|
|
||||||
textOverflow: "ellipsis",
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<p
|
{cfg.baseUrl}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Title Section */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
marginTop: "1rem",
|
||||||
|
marginBottom: "1.5rem",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<h1
|
||||||
style={{
|
style={{
|
||||||
margin: 0,
|
margin: 0,
|
||||||
|
fontSize: useSmallerFont ? 64 : 72,
|
||||||
|
fontFamily: fonts[0].name,
|
||||||
|
fontWeight: 700,
|
||||||
|
color: cfg.theme.colors[colorScheme].dark,
|
||||||
|
lineHeight: 1.2,
|
||||||
|
display: "-webkit-box",
|
||||||
|
WebkitBoxOrient: "vertical",
|
||||||
|
WebkitLineClamp: 2,
|
||||||
overflow: "hidden",
|
overflow: "hidden",
|
||||||
textOverflow: "ellipsis",
|
|
||||||
whiteSpace: "nowrap",
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{title}
|
{title}
|
||||||
</p>
|
</h1>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Description Section */}
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
color: cfg.theme.colors[colorScheme].dark,
|
flex: 1,
|
||||||
fontSize: 44,
|
fontSize: 36,
|
||||||
fontFamily: fonts[1].name,
|
color: cfg.theme.colors[colorScheme].darkgray,
|
||||||
maxWidth: "100%",
|
lineHeight: 1.4,
|
||||||
maxHeight: "40%",
|
|
||||||
overflow: "hidden",
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<p
|
<p
|
||||||
@@ -210,14 +268,80 @@ export const defaultImage: SocialImageOptions["imageStructure"] = (
|
|||||||
margin: 0,
|
margin: 0,
|
||||||
display: "-webkit-box",
|
display: "-webkit-box",
|
||||||
WebkitBoxOrient: "vertical",
|
WebkitBoxOrient: "vertical",
|
||||||
WebkitLineClamp: 3,
|
WebkitLineClamp: 4,
|
||||||
overflow: "hidden",
|
overflow: "hidden",
|
||||||
textOverflow: "ellipsis",
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{description}
|
{description}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Footer with Metadata */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
marginTop: "2rem",
|
||||||
|
paddingTop: "2rem",
|
||||||
|
borderTop: `1px solid ${cfg.theme.colors[colorScheme].lightgray}`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Left side - Date */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
color: cfg.theme.colors[colorScheme].gray,
|
||||||
|
fontSize: 28,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{date && (
|
||||||
|
<div style={{ display: "flex", alignItems: "center" }}>
|
||||||
|
<svg
|
||||||
|
style={{ marginRight: "0.5rem" }}
|
||||||
|
width="28"
|
||||||
|
height="28"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
>
|
||||||
|
<rect x="3" y="4" width="18" height="18" rx="2" ry="2"></rect>
|
||||||
|
<line x1="16" y1="2" x2="16" y2="6"></line>
|
||||||
|
<line x1="8" y1="2" x2="8" y2="6"></line>
|
||||||
|
<line x1="3" y1="10" x2="21" y2="10"></line>
|
||||||
|
</svg>
|
||||||
|
{date}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Right side - Tags */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
gap: "0.5rem",
|
||||||
|
flexWrap: "wrap",
|
||||||
|
justifyContent: "flex-end",
|
||||||
|
maxWidth: "60%",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{tags.slice(0, 3).map((tag: string) => (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
padding: "0.5rem 1rem",
|
||||||
|
backgroundColor: cfg.theme.colors[colorScheme].highlight,
|
||||||
|
color: cfg.theme.colors[colorScheme].secondary,
|
||||||
|
borderRadius: "10px",
|
||||||
|
fontSize: 24,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
#{tag}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ interface Colors {
|
|||||||
darkMode: ColorScheme
|
darkMode: ColorScheme
|
||||||
}
|
}
|
||||||
|
|
||||||
type FontSpecification =
|
export type FontSpecification =
|
||||||
| string
|
| string
|
||||||
| {
|
| {
|
||||||
name: string
|
name: string
|
||||||
|
|||||||
Reference in New Issue
Block a user