mirror of
https://github.com/jackyzha0/quartz.git
synced 2025-12-24 05:14:06 -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
|
- `tertiary`: hover states and visited [[graph view|graph]] nodes
|
||||||
- `highlight`: internal link background, highlighted text, [[syntax highlighting|highlighted lines of code]]
|
- `highlight`: internal link background, highlighted text, [[syntax highlighting|highlighted lines of code]]
|
||||||
- `textHighlight`: markdown highlighted text background
|
- `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
|
## Plugins
|
||||||
|
|
||||||
|
|||||||
@ -4,7 +4,7 @@ tags:
|
|||||||
- plugin/emitter
|
- 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`
|
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: {
|
plugins: {
|
||||||
transformers: [
|
transformers: [
|
||||||
|
|||||||
@ -70,6 +70,13 @@ export interface GlobalConfiguration {
|
|||||||
* Region Codes: https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2
|
* Region Codes: https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2
|
||||||
*/
|
*/
|
||||||
locale: ValidLocale
|
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 {
|
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 { QuartzEmitterPlugin } from "../types"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import fs from "fs"
|
import fs from "fs"
|
||||||
import { glob } from "../../util/glob"
|
import { glob } from "../../util/glob"
|
||||||
import { Argv } from "../../util/ctx"
|
import { Argv } from "../../util/ctx"
|
||||||
import { QuartzConfig } from "../../cfg"
|
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) => {
|
const filesToCopy = async (argv: Argv, cfg: QuartzConfig) => {
|
||||||
// glob all non MD files in content folder and copy it over
|
// glob all non MD files in content folder and copy it over
|
||||||
return await glob("**", argv.directory, ["**/*.md", ...cfg.configuration.ignorePatterns])
|
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 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
|
const dest = joinSegments(argv.output, name) as FilePath
|
||||||
|
|
||||||
// ensure dir exists
|
// ensure dir exists
|
||||||
const dir = path.dirname(dest) as FilePath
|
const dir = path.dirname(dest) as FilePath
|
||||||
await fs.promises.mkdir(dir, { recursive: true })
|
await fs.promises.mkdir(dir, { recursive: true })
|
||||||
|
|
||||||
|
if (doOptimizeImage) {
|
||||||
|
await processImage(src, dest)
|
||||||
|
} else {
|
||||||
await fs.promises.copyFile(src, dest)
|
await fs.promises.copyFile(src, dest)
|
||||||
|
}
|
||||||
return 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 = () => {
|
export const Assets: QuartzEmitterPlugin = () => {
|
||||||
return {
|
return {
|
||||||
name: "Assets",
|
name: "Assets",
|
||||||
async *emit({ argv, cfg }) {
|
async *emit({ argv, cfg }) {
|
||||||
const fps = await filesToCopy(argv, cfg)
|
const fps = await filesToCopy(argv, cfg)
|
||||||
for (const fp of fps) {
|
for (const fp of fps) {
|
||||||
yield copyFile(argv, fp)
|
yield copyFile(argv, cfg, fp)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async *partialEmit(ctx, _content, _resources, changeEvents) {
|
async *partialEmit(ctx, _content, _resources, changeEvents) {
|
||||||
for (const changeEvent of changeEvents) {
|
for (const changeEvent of changeEvents) {
|
||||||
const ext = path.extname(changeEvent.path)
|
const ext = path.extname(changeEvent.path).toLowerCase()
|
||||||
if (ext === ".md") continue
|
if (ext === ".md") continue
|
||||||
|
|
||||||
if (changeEvent.type === "add" || changeEvent.type === "change") {
|
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") {
|
} 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
|
const dest = joinSegments(ctx.argv.output, name) as FilePath
|
||||||
await fs.promises.unlink(dest)
|
await fs.promises.unlink(dest)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -13,7 +13,7 @@ import { ReplaceFunction, findAndReplace as mdastFindReplace } from "mdast-util-
|
|||||||
import rehypeRaw from "rehype-raw"
|
import rehypeRaw from "rehype-raw"
|
||||||
import { SKIP, visit } from "unist-util-visit"
|
import { SKIP, visit } from "unist-util-visit"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import { splitAnchor } from "../../util/path"
|
import { FullSlug, splitAnchor } from "../../util/path"
|
||||||
import { JSResource, CSSResource } from "../../util/resources"
|
import { JSResource, CSSResource } from "../../util/resources"
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import calloutScript from "../../components/scripts/callout.inline"
|
import calloutScript from "../../components/scripts/callout.inline"
|
||||||
@ -206,7 +206,7 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>>
|
|||||||
|
|
||||||
return src
|
return src
|
||||||
},
|
},
|
||||||
markdownPlugins(_ctx) {
|
markdownPlugins(ctx) {
|
||||||
const plugins: PluggableList = []
|
const plugins: PluggableList = []
|
||||||
|
|
||||||
// regex replacements
|
// regex replacements
|
||||||
@ -227,8 +227,18 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>>
|
|||||||
// embed cases
|
// embed cases
|
||||||
if (value.startsWith("!")) {
|
if (value.startsWith("!")) {
|
||||||
const ext: string = path.extname(fp).toLowerCase()
|
const ext: string = path.extname(fp).toLowerCase()
|
||||||
const url = slugifyFilePath(fp as FilePath)
|
let url = slugifyFilePath(fp as FilePath)
|
||||||
if ([".png", ".jpg", ".jpeg", ".gif", ".bmp", ".svg", ".webp"].includes(ext)) {
|
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 match = wikilinkImageEmbedRegex.exec(alias ?? "")
|
||||||
const alt = match?.groups?.alt ?? ""
|
const alt = match?.groups?.alt ?? ""
|
||||||
const width = match?.groups?.width ?? "auto"
|
const width = match?.groups?.width ?? "auto"
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user