mirror of
https://github.com/jackyzha0/quartz.git
synced 2025-12-23 21:04:07 -06:00
feat(assets): optimize images for web serving on build
This commit is contained in:
parent
2acdec323f
commit
99add6f782
@ -57,6 +57,7 @@ This part of the configuration concerns anything that can affect the whole site.
|
||||
- `tertiary`: hover states and visited [[graph view|graph]] nodes
|
||||
- `highlight`: internal link background, highlighted text, [[syntax highlighting|highlighted lines of code]]
|
||||
- `textHighlight`: markdown highlighted text background
|
||||
- `optimizeImages`: whether to optimize images for web serving when building Quartz. If `true`, JPEG and PNG images will be stripped all metadata and converted to WebP format, and associated image links in [[wikilinks]] will be updated with the new file extension.
|
||||
|
||||
## Plugins
|
||||
|
||||
|
||||
@ -4,7 +4,7 @@ tags:
|
||||
- plugin/emitter
|
||||
---
|
||||
|
||||
This plugin emits all non-Markdown static assets in your content folder (like images, videos, HTML, etc). The plugin respects the `ignorePatterns` in the global [[configuration]].
|
||||
This plugin emits all non-Markdown static assets in your content folder (like images, videos, HTML, etc). The plugin respects the `ignorePatterns` and `optimizeImages` in the global [[configuration]].
|
||||
|
||||
Note that all static assets will then be accessible through its path on your generated site, i.e: `host.me/path/to/static.pdf`
|
||||
|
||||
|
||||
@ -52,6 +52,8 @@ const config: QuartzConfig = {
|
||||
},
|
||||
},
|
||||
},
|
||||
// Disable `optimizeImages` to speed up build time
|
||||
optimizeImages: true,
|
||||
},
|
||||
plugins: {
|
||||
transformers: [
|
||||
|
||||
@ -70,6 +70,13 @@ export interface GlobalConfiguration {
|
||||
* Region Codes: https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2
|
||||
*/
|
||||
locale: ValidLocale
|
||||
/**
|
||||
* Whether to optimize images for web serving when building Quartz.
|
||||
*
|
||||
* If true, eligible images will be stripped all metadata and converted to WebP format,
|
||||
* and associated image links in wikilinks will be updated with the new file extension.
|
||||
*/
|
||||
optimizeImages: boolean
|
||||
}
|
||||
|
||||
export interface QuartzConfig {
|
||||
|
||||
@ -1,48 +1,77 @@
|
||||
import { FilePath, joinSegments, slugifyFilePath } from "../../util/path"
|
||||
import { FilePath, FullSlug, joinSegments, slugifyFilePath } from "../../util/path"
|
||||
import { QuartzEmitterPlugin } from "../types"
|
||||
import path from "path"
|
||||
import fs from "fs"
|
||||
import { glob } from "../../util/glob"
|
||||
import { Argv } from "../../util/ctx"
|
||||
import { QuartzConfig } from "../../cfg"
|
||||
import sharp from "sharp"
|
||||
|
||||
// Sharp doesn't support BMP input out of the box:
|
||||
// https://github.com/lovell/sharp/issues/543
|
||||
// GIF processing can be very slow or ineffective (larger file size when CPU effort is
|
||||
// set to a low number); only enable after testing:
|
||||
// https://github.com/lovell/sharp/issues/3176
|
||||
const imageExtsToOptimize: Set<string> = new Set([".png", ".jpg", ".jpeg"])
|
||||
|
||||
const filesToCopy = async (argv: Argv, cfg: QuartzConfig) => {
|
||||
// glob all non MD files in content folder and copy it over
|
||||
return await glob("**", argv.directory, ["**/*.md", ...cfg.configuration.ignorePatterns])
|
||||
}
|
||||
|
||||
const copyFile = async (argv: Argv, fp: FilePath) => {
|
||||
const copyFile = async (argv: Argv, cfg: QuartzConfig, fp: FilePath) => {
|
||||
const src = joinSegments(argv.directory, fp) as FilePath
|
||||
|
||||
const name = slugifyFilePath(fp)
|
||||
const srcExt = path.extname(fp).toLowerCase()
|
||||
const doOptimizeImage = cfg.configuration.optimizeImages && imageExtsToOptimize.has(srcExt)
|
||||
|
||||
const name = doOptimizeImage
|
||||
? ((slugifyFilePath(fp, true) + ".webp") as FullSlug)
|
||||
: slugifyFilePath(fp)
|
||||
const dest = joinSegments(argv.output, name) as FilePath
|
||||
|
||||
// ensure dir exists
|
||||
const dir = path.dirname(dest) as FilePath
|
||||
await fs.promises.mkdir(dir, { recursive: true })
|
||||
|
||||
if (doOptimizeImage) {
|
||||
await processImage(src, dest)
|
||||
} else {
|
||||
await fs.promises.copyFile(src, dest)
|
||||
}
|
||||
return dest
|
||||
}
|
||||
|
||||
async function processImage(src: FilePath, dest: FilePath) {
|
||||
const originalFile = await fs.promises.readFile(src)
|
||||
const convertedFile = await sharp(originalFile)
|
||||
.webp({ quality: 90, smartSubsample: true, effort: 6 })
|
||||
.toBuffer()
|
||||
await fs.promises.writeFile(dest, convertedFile)
|
||||
}
|
||||
|
||||
export const Assets: QuartzEmitterPlugin = () => {
|
||||
return {
|
||||
name: "Assets",
|
||||
async *emit({ argv, cfg }) {
|
||||
const fps = await filesToCopy(argv, cfg)
|
||||
for (const fp of fps) {
|
||||
yield copyFile(argv, fp)
|
||||
yield copyFile(argv, cfg, fp)
|
||||
}
|
||||
},
|
||||
async *partialEmit(ctx, _content, _resources, changeEvents) {
|
||||
for (const changeEvent of changeEvents) {
|
||||
const ext = path.extname(changeEvent.path)
|
||||
const ext = path.extname(changeEvent.path).toLowerCase()
|
||||
if (ext === ".md") continue
|
||||
|
||||
if (changeEvent.type === "add" || changeEvent.type === "change") {
|
||||
yield copyFile(ctx.argv, changeEvent.path)
|
||||
yield copyFile(ctx.argv, ctx.cfg, changeEvent.path)
|
||||
} else if (changeEvent.type === "delete") {
|
||||
const name = slugifyFilePath(changeEvent.path)
|
||||
const doOptimizeImage =
|
||||
ctx.cfg.configuration.optimizeImages && imageExtsToOptimize.has(ext)
|
||||
const name = doOptimizeImage
|
||||
? ((slugifyFilePath(changeEvent.path, true) + ".webp") as FullSlug)
|
||||
: slugifyFilePath(changeEvent.path)
|
||||
const dest = joinSegments(ctx.argv.output, name) as FilePath
|
||||
await fs.promises.unlink(dest)
|
||||
}
|
||||
|
||||
@ -13,7 +13,7 @@ import { ReplaceFunction, findAndReplace as mdastFindReplace } from "mdast-util-
|
||||
import rehypeRaw from "rehype-raw"
|
||||
import { SKIP, visit } from "unist-util-visit"
|
||||
import path from "path"
|
||||
import { splitAnchor } from "../../util/path"
|
||||
import { FullSlug, splitAnchor } from "../../util/path"
|
||||
import { JSResource, CSSResource } from "../../util/resources"
|
||||
// @ts-ignore
|
||||
import calloutScript from "../../components/scripts/callout.inline"
|
||||
@ -206,7 +206,7 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>>
|
||||
|
||||
return src
|
||||
},
|
||||
markdownPlugins(_ctx) {
|
||||
markdownPlugins(ctx) {
|
||||
const plugins: PluggableList = []
|
||||
|
||||
// regex replacements
|
||||
@ -227,8 +227,18 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>>
|
||||
// embed cases
|
||||
if (value.startsWith("!")) {
|
||||
const ext: string = path.extname(fp).toLowerCase()
|
||||
const url = slugifyFilePath(fp as FilePath)
|
||||
if ([".png", ".jpg", ".jpeg", ".gif", ".bmp", ".svg", ".webp"].includes(ext)) {
|
||||
let url = slugifyFilePath(fp as FilePath)
|
||||
if (
|
||||
[".png", ".jpg", ".jpeg", ".gif", ".gifv", ".bmp", ".svg", ".webp"].includes(
|
||||
ext,
|
||||
)
|
||||
) {
|
||||
// Replace extension of eligible image files with ".webp" if image optimization is enabled.
|
||||
url =
|
||||
ctx.cfg.configuration.optimizeImages &&
|
||||
[".png", ".jpg", ".jpeg"].includes(ext)
|
||||
? ((slugifyFilePath(fp as FilePath, true) + ".webp") as FullSlug)
|
||||
: url
|
||||
const match = wikilinkImageEmbedRegex.exec(alias ?? "")
|
||||
const alt = match?.groups?.alt ?? ""
|
||||
const width = match?.groups?.width ?? "auto"
|
||||
|
||||
Loading…
Reference in New Issue
Block a user