mirror of
https://github.com/jackyzha0/quartz.git
synced 2025-12-31 00:34:05 -06:00
feat: use sharp to convert to webp, add content headers
This commit is contained in:
parent
ee15e27265
commit
3887de1eca
@ -36,7 +36,6 @@
|
|||||||
"@clack/prompts": "^0.6.3",
|
"@clack/prompts": "^0.6.3",
|
||||||
"@floating-ui/dom": "^1.4.0",
|
"@floating-ui/dom": "^1.4.0",
|
||||||
"@napi-rs/simple-git": "0.1.9",
|
"@napi-rs/simple-git": "0.1.9",
|
||||||
"@resvg/resvg-js": "^2.4.1",
|
|
||||||
"async-mutex": "^0.4.0",
|
"async-mutex": "^0.4.0",
|
||||||
"chalk": "^4.1.2",
|
"chalk": "^4.1.2",
|
||||||
"chokidar": "^3.5.3",
|
"chokidar": "^3.5.3",
|
||||||
@ -80,6 +79,7 @@
|
|||||||
"rimraf": "^5.0.1",
|
"rimraf": "^5.0.1",
|
||||||
"satori": "^0.10.6",
|
"satori": "^0.10.6",
|
||||||
"serve-handler": "^6.1.5",
|
"serve-handler": "^6.1.5",
|
||||||
|
"sharp": "^0.32.6",
|
||||||
"source-map-support": "^0.5.21",
|
"source-map-support": "^0.5.21",
|
||||||
"to-vfile": "^7.2.4",
|
"to-vfile": "^7.2.4",
|
||||||
"toml": "^3.0.0",
|
"toml": "^3.0.0",
|
||||||
|
|||||||
@ -342,6 +342,15 @@ export async function handleBuild(argv) {
|
|||||||
source: "**/*.html",
|
source: "**/*.html",
|
||||||
headers: [{ key: "Content-Disposition", value: "inline" }],
|
headers: [{ key: "Content-Disposition", value: "inline" }],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
source: "**/*.webp",
|
||||||
|
headers: [{ key: "Content-Type", value: "image/webp" }],
|
||||||
|
},
|
||||||
|
// fixes bug where avif images are displayed as text instead of images (future proof)
|
||||||
|
{
|
||||||
|
source: "**/*.avif",
|
||||||
|
headers: [{ key: "Content-Type", value: "image/avif" }],
|
||||||
|
},
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
const status = res.statusCode
|
const status = res.statusCode
|
||||||
|
|||||||
@ -5,16 +5,20 @@ import satori from "satori"
|
|||||||
import * as fs from "fs"
|
import * as fs from "fs"
|
||||||
import { getTtfFromGfont } from "../util/fonts"
|
import { getTtfFromGfont } from "../util/fonts"
|
||||||
import { GlobalConfiguration } from "../cfg"
|
import { GlobalConfiguration } from "../cfg"
|
||||||
import { Resvg } from "@resvg/resvg-js"
|
import sharp from "sharp"
|
||||||
|
|
||||||
// const robotoData = await (
|
/**
|
||||||
// await fetch("https://fonts.gstatic.com/s/roboto/v30/KFOmCnqEu92Fr1Me5Q.ttf")
|
* Generates social image (OG/twitter standard) and saves it as `.web` inside the public folder
|
||||||
// ).arrayBuffer()
|
* @param title what title to use
|
||||||
|
* @param description what description to use
|
||||||
async function generateSvg(
|
* @param fileName what fileName to use when writing to disk
|
||||||
|
* @param fontName name of font to use (must be google font)
|
||||||
|
* @param cfg `GlobalConfiguration` of quartz
|
||||||
|
*/
|
||||||
|
async function generateSocialImage(
|
||||||
title: string,
|
title: string,
|
||||||
description: string,
|
description: string,
|
||||||
filePath: string,
|
fileName: string,
|
||||||
fontName: string,
|
fontName: string,
|
||||||
cfg: GlobalConfiguration,
|
cfg: GlobalConfiguration,
|
||||||
) {
|
) {
|
||||||
@ -68,7 +72,6 @@ async function generateSvg(
|
|||||||
width: "2vw",
|
width: "2vw",
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
backgroundColor: cfg.theme.colors.lightMode.tertiary,
|
backgroundColor: cfg.theme.colors.lightMode.tertiary,
|
||||||
opacity: 0.8,
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>,
|
</div>,
|
||||||
@ -91,33 +94,34 @@ async function generateSvg(
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
const resvg = new Resvg(svg)
|
|
||||||
const pngData = resvg.render()
|
// Convert svg directly to webp (with additional compression)
|
||||||
const pngBuffer = pngData.asPng()
|
const compressed = await sharp(Buffer.from(svg)).webp({ quality: 40 }).toBuffer()
|
||||||
fs.writeFileSync(`public/static/${filePath}.png`, pngBuffer)
|
|
||||||
|
// Write to file system
|
||||||
|
fs.writeFileSync(`${imageDir}/${fileName}.${extension}`, compressed)
|
||||||
}
|
}
|
||||||
|
|
||||||
const ogHeight = 1200
|
const ogHeight = 1200
|
||||||
const ogWidth = 676
|
const ogWidth = 676
|
||||||
|
const extension = "webp"
|
||||||
|
|
||||||
|
const imageDir = "public/static/social-images"
|
||||||
export default (() => {
|
export default (() => {
|
||||||
let font: Promise<ArrayBuffer | undefined>
|
let font: Promise<ArrayBuffer | undefined>
|
||||||
function Head({ cfg, fileData, externalResources }: QuartzComponentProps) {
|
function Head({ cfg, fileData, externalResources }: QuartzComponentProps) {
|
||||||
if (!font) {
|
if (!font) {
|
||||||
font = getTtfFromGfont(cfg.theme.typography.header)
|
font = getTtfFromGfont(cfg.theme.typography.header)
|
||||||
}
|
}
|
||||||
const dir = "public/static"
|
|
||||||
const slug = fileData.filePath
|
const slug = fileData.filePath
|
||||||
const filePath = slug?.replaceAll("/", "-")
|
const filePath = slug?.replaceAll("/", "-")
|
||||||
const ogArr = slug?.split("/")
|
|
||||||
const title = fileData.frontmatter?.title ?? "Untitled"
|
const title = fileData.frontmatter?.title ?? "Untitled"
|
||||||
const description = fileData.description?.trim() ?? "No description provided"
|
const description = fileData.description?.trim() ?? "No description provided"
|
||||||
|
|
||||||
generateSvg(title, description, filePath as string, cfg.theme.typography.header, cfg)
|
generateSocialImage(title, description, filePath as string, cfg.theme.typography.header, cfg)
|
||||||
// }
|
|
||||||
|
|
||||||
if (!fs.existsSync(dir)) {
|
if (!fs.existsSync(imageDir)) {
|
||||||
fs.mkdirSync(dir)
|
fs.mkdirSync(imageDir, { recursive: true })
|
||||||
}
|
}
|
||||||
const { css, js } = externalResources
|
const { css, js } = externalResources
|
||||||
|
|
||||||
@ -126,8 +130,9 @@ export default (() => {
|
|||||||
const baseDir = fileData.slug === "404" ? path : pathToRoot(fileData.slug!)
|
const baseDir = fileData.slug === "404" ? path : pathToRoot(fileData.slug!)
|
||||||
|
|
||||||
const iconPath = joinSegments(baseDir, "static/icon.png")
|
const iconPath = joinSegments(baseDir, "static/icon.png")
|
||||||
const ogImagePath = `https://${cfg.baseUrl}/static/og-image.png`
|
// TODO: use default image if undefined
|
||||||
const ogImagePathNew = `https://${cfg.baseUrl}/static/${filePath}.png`
|
const ogImageDefaultPath = `https://${cfg.baseUrl}/static/og-image.png`
|
||||||
|
const ogImagePath = `https://${cfg.baseUrl}/static/${filePath}.${extension}`
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<head>
|
<head>
|
||||||
@ -139,18 +144,18 @@ export default (() => {
|
|||||||
<meta property="og:title" content={title} />
|
<meta property="og:title" content={title} />
|
||||||
<meta property="og:type" content="website" />
|
<meta property="og:type" content="website" />
|
||||||
<meta property="og:description" content={description} />
|
<meta property="og:description" content={description} />
|
||||||
<meta property="og:image:type" content="image/png" />
|
<meta property="og:image:type" content={`image/${extension}`} />
|
||||||
<meta property="og:image:alt" content={fileData.description} />
|
<meta property="og:image:alt" content={fileData.description} />
|
||||||
<meta property="og:image:width" content={"" + ogWidth} />
|
<meta property="og:image:width" content={"" + ogWidth} />
|
||||||
<meta property="og:image:height" content={"" + ogHeight} />
|
<meta property="og:image:height" content={"" + ogHeight} />
|
||||||
<meta property="og:image:url" content={ogImagePathNew} />
|
<meta property="og:image:url" content={ogImagePath} />
|
||||||
<meta name="twitter:card" content="summary_large_image" />
|
<meta name="twitter:card" content="summary_large_image" />
|
||||||
<meta name="twitter:title" content={title} />
|
<meta name="twitter:title" content={title} />
|
||||||
<meta name="twitter:description" content={description} />
|
<meta name="twitter:description" content={description} />
|
||||||
<meta property="twitter:url" content={`https://${cfg.baseUrl}/${fileData.slug}`}></meta>
|
<meta property="twitter:url" content={`https://${cfg.baseUrl}/${fileData.slug}`}></meta>
|
||||||
<meta property="twitter:domain" content={cfg.baseUrl}></meta>
|
<meta property="twitter:domain" content={cfg.baseUrl}></meta>
|
||||||
{cfg.baseUrl && <meta name="twitter:image" content={ogImagePathNew} />}
|
{cfg.baseUrl && <meta name="twitter:image" content={ogImagePath} />}
|
||||||
{cfg.baseUrl && <meta property="og:image" content={ogImagePathNew} />}
|
{cfg.baseUrl && <meta property="og:image" content={ogImagePath} />}
|
||||||
<meta property="og:width" content="1200" />
|
<meta property="og:width" content="1200" />
|
||||||
<meta property="og:height" content="675" />
|
<meta property="og:height" content="675" />
|
||||||
<link rel="icon" href={iconPath} />
|
<link rel="icon" href={iconPath} />
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user