feat: use sharp to convert to webp, add content headers

This commit is contained in:
Ben Schlegel 2023-09-22 15:15:40 +02:00
parent ee15e27265
commit 3887de1eca
No known key found for this signature in database
GPG Key ID: 8BDB8891C1575E22
3 changed files with 39 additions and 25 deletions

View File

@ -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",

View File

@ -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

View File

@ -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} />