Commit 9f4f2a2

HPCesia <me@hpcesia.com>
2026-09-09 04:57:39
Astro image optimization support
Assisted-by: pi-coding-agent:deepseek-v4.1-flash-expires-on-0910
1 parent 6b35804
src/contents/_lib/components/image.typ
@@ -0,0 +1,135 @@
+#import "@preview/bullseye:0.1.0": match-target
+
+#let jsx(content) = html.elem("script", attrs: ("data-jsx": content))
+
+#let import-counter = counter("kp-image-import")
+
+#let image-import() = jsx("import { Image } from 'astro:assets'")
+
+#let supported-exts = regex("(?i)\\.(png|jpg|jpeg|webp|gif|svg|avif|tiff)$")
+
+// `path("...")` -> `"..."`
+#let resolve-path(source) = eval(repr(source).slice(5, -1))
+
+#let import-specifier(source) = {
+  if type(source) == path {
+    resolve-path(source)
+  } else if type(source) == str and source.starts-with("/") {
+    source
+  } else {
+    none
+  }
+}
+
+#let css-length(value) = {
+  if value == auto { none } else if repr(value).ends-with(" + 0pt") {
+    repr(value).slice(0, -6)
+  } else if repr(value).starts-with("0% + ") {
+    repr(value).slice(5)
+  } else if repr(value).contains(" + ") { "calc(" + repr(value) + ")" } else {
+    repr(value)
+  }
+}
+
+#let fit-map = ("cover": "cover", "contain": "contain", "stretch": "fill")
+
+#let js-value(value) = {
+  if value == none { "null" } else if type(value) == str {
+    repr(value)
+  } else if type(value) == bool {
+    if value { "true" } else { "false" }
+  } else if type(value) in (int, float) { str(value) } else if (
+    type(value) == array
+  ) { "[" + value.map(js-value).join(", ") + "]" } else if (
+    type(value) == dictionary
+  ) {
+    (
+      "{" + value.pairs().map(pair => repr(pair.first()) + ": " + js-value(pair.last())).join(", ") + "}"
+    )
+  } else {
+    panic("kp.img: unsupported astro option " + repr(value))
+  }
+}
+
+#let jsx-attributes(options) = (
+  options
+    .pairs()
+    .map(pair => {
+      let value = pair.last()
+      if value == none { none } else {
+        pair.first() + "={" + js-value(value) + "}"
+      }
+    })
+    .filter(attr => attr != none)
+)
+
+/// An image optimized by Astro's `<Image />`.
+///
+/// `src` must be a `path` or root-absolute string; a relative string is rejected
+/// because it would resolve against this file, not the caller. `..args` go to
+/// `image()`, `astro` to `<Image />`, and unsupported sources fall back to Typst.
+///
+/// - src (path, str):
+/// - alt (str, none):
+/// - width (auto, relative, length):
+/// - height (auto, relative, length):
+/// - fit (str):
+/// - scaling (auto, str):
+/// - astro (dictionary):
+/// -> content
+#let img(src, astro: (:), ..args) = {
+  if type(src) == str and not src.starts-with("/") {
+    panic("kp.img: wrap " + repr(src) + " in path() or make it root-absolute")
+  }
+
+  let element = image(src, ..args)
+  let specifier = import-specifier(element.at("source", default: none))
+
+  if specifier == none or specifier.matches(supported-exts).len() == 0 {
+    return element
+  }
+
+  let width = css-length(element.at("width", default: auto))
+  let height = css-length(element.at("height", default: auto))
+  let fit = element.at("fit", default: "cover")
+  let scaling = element.at("scaling", default: auto)
+  let alt = element.at("alt", default: none)
+
+  let options = (layout: "constrained") + astro
+  let styles = ("display: block",)
+  if width != none { styles.push("width: " + width) }
+  if height != none { styles.push("height: " + height) }
+  if width != none and height != none {
+    styles.push("object-fit: " + fit-map.at(fit))
+  }
+  if scaling == "pixelated" { styles.push("image-rendering: pixelated") }
+  if "style" in options {
+    styles.push(options.at("style"))
+    let _ = options.remove("style")
+  }
+  if "alt" not in options {
+    options.insert("alt", if alt == none { "" } else { alt })
+  }
+
+  let attributes = jsx-attributes(options)
+  let tag = (
+    "<Image src={import(" + repr(specifier) + ").then(m => m.default)} style={",
+    repr(styles.join("; ")),
+    "} ",
+    attributes.join(" "),
+    " />",
+  ).join("")
+
+  import-counter.step()
+
+  context {
+    let render = match-target(
+      html: () => {
+        if import-counter.get().first() == 1 { image-import() }
+        jsx(tag)
+      },
+      paged: () => element,
+    )
+    render()
+  }
+}
src/contents/_lib/lib.typ
@@ -1,5 +1,6 @@
 #import "@preview/bullseye:0.1.0": match-target
 
+#import "./components/image.typ": img
 #import "./components/ruby.typ": ruby
 #import "./components/sidenote.typ": sidenote
 
@@ -24,9 +25,7 @@
   show: raw-template
 
   context {
-    let frontmatter = query(<frontmatter>)
-      .first(default: (value: (title: "", date: auto)))
-      .value
+    let frontmatter = query(<frontmatter>).first(default: (value: (title: "", date: auto))).value
     set document(
       title: frontmatter.title,
       date: frontmatter.date,
src/contents/posts/typst-syntax.typ
@@ -152,6 +152,19 @@ can help you with that.
 / URL: #[*U*]niform #[*R*]esource #[*L*]ocator, a text string that specifies where
   a resource can be found on the Internet.
 
+= Images
+
+Pass a `path` value or a root-absolute string to ```typ #kp.img()``` and Astro's
+```jsx <Image />``` component optimizes the image.
+
+#figure(
+  kp.img(path("../avatar.png"), alt: "HPCesia, author of Kerplunk's avatar", width: 30%),
+  caption: [An image optimized by Astro.],
+)
+
+Or just use Typst's original ```typ #image()``` function to avoid Astro's optimization.
+For web image, use ```typ #html.img()``` instead.
+
 = Extra Components
 
 These components are added by Kerplunk.
src/contents/avatar.png
Binary file
astro.config.ts
@@ -14,6 +14,10 @@ const base = SITE_CONFIG.website.split(site).slice(1).join("/") || undefined;
 export default defineConfig({
   site: `${protocol}://${site}`,
   base: base,
+  image: {
+    // Required by the `constrained` layout that `kp.img` sets by default.
+    responsiveStyles: true,
+  },
   integrations: [
     typst({
       options: {
deno.json
@@ -1,3 +1,4 @@
 {
+  "nodeModulesDir": "auto",
   "unstable": ["fmt-component"]
 }
deno.lock
@@ -6,9 +6,11 @@
     "npm:@myriaddreamin/typst-ts-renderer@~0.8.0-rc3": "0.8.0-rc3",
     "npm:@myriaddreamin/typst.ts@~0.8.0-rc3": "0.8.0-rc3_@myriaddreamin+typst-ts-renderer@0.8.0-rc3",
     "npm:astro-typst@~0.12.4": "0.12.4_@myriaddreamin+typst-ts-node-compiler@0.8.0-rc3_@myriaddreamin+typst-ts-renderer@0.8.0-rc3_@myriaddreamin+typst.ts@0.8.0-rc3__@myriaddreamin+typst-ts-renderer@0.8.0-rc3_astro@7.2.0__@astrojs+markdown-remark@7.2.2_@astrojs+markdown-remark@7.2.2",
+    "npm:astro@*": "7.2.0_@astrojs+markdown-remark@7.2.2",
     "npm:astro@^7.2.0": "7.2.0_@astrojs+markdown-remark@7.2.2",
     "npm:sanitize.css@13": "13.0.0",
     "npm:sass@^1.102.0": "1.102.0",
+    "npm:sharp@~0.35.4": "0.35.4",
     "npm:typescript@^6.0.3": "6.0.3",
     "npm:unist-util-visit@^5.1.0": "5.1.0"
   },
@@ -251,6 +253,12 @@
         "tslib"
       ]
     },
+    "@emnapi/runtime@1.11.3": {
+      "integrity": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==",
+      "dependencies": [
+        "tslib"
+      ]
+    },
     "@emnapi/runtime@2.0.0-alpha.3": {
       "integrity": "sha512-hFPAhMUjJD9BSyCANEISPOogeXC9Zo9ZQl7L6vKnaVsMkCtzznaW/naYypeyl0Gv5rYfWYsZbpixTMpjDJzQeA==",
       "dependencies": [
@@ -402,168 +410,168 @@
     "@img/colour@1.1.0": {
       "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ=="
     },
-    "@img/sharp-darwin-arm64@0.35.3": {
-      "integrity": "sha512-RMnFX7YQsMoh7lWfcM4NEHHymBX/rLuKNPVM84XE9ONPcaSCDgE7CHIHpSgPcO2xcRthgBy1HfNO319mwhIAkg==",
+    "@img/sharp-darwin-arm64@0.35.4": {
+      "integrity": "sha512-Uhfl4V4lhP2nbUVF9+hyH1+luj86f1gUFeo8ALYxFoULoU+G87D43BfeMP8XHsk9boxAnCY/bf2EHwhA7MuGsA==",
       "optionalDependencies": [
         "@img/sharp-libvips-darwin-arm64"
       ],
       "os": ["darwin"],
       "cpu": ["arm64"]
     },
-    "@img/sharp-darwin-x64@0.35.3": {
-      "integrity": "sha512-Xo+5uFBtLN0BKqieTxiFzFPQAUlBbbH5iBKyRX/z1JrbnYsHTfKJnUfL8+p2TPXr1pXqao4eeL4Rl144uDpK9w==",
+    "@img/sharp-darwin-x64@0.35.4": {
+      "integrity": "sha512-hWniXY3bG5qKpkKrAwPe4y+VTPmf086YQAnkxWh7uA1YrlRouWGa0M0Mxj3ZjnXFkv7/TD1bTy9lGUK26vRvWw==",
       "optionalDependencies": [
         "@img/sharp-libvips-darwin-x64"
       ],
       "os": ["darwin"],
       "cpu": ["x64"]
     },
-    "@img/sharp-freebsd-wasm32@0.35.3": {
-      "integrity": "sha512-lUxcqWIj2wMQ9BrwNjngcr1gWUr5xgaGThBRqPPalIC2n67Cqj1uPh8NnA/ZhAg8hUbKl+kVHKwgUIwe6ZYPrg==",
+    "@img/sharp-freebsd-wasm32@0.35.4": {
+      "integrity": "sha512-lIsKw/BU+kjB4eZjxrYrZmwOJYi3Ajrv66iAlBmUPyKc3HpnloevB1g3wxGD9P/5BbQ1brBGl65VRRrCvQDEqA==",
       "dependencies": [
         "@img/sharp-wasm32"
       ],
       "os": ["freebsd"]
     },
-    "@img/sharp-libvips-darwin-arm64@1.3.2": {
-      "integrity": "sha512-9J6ypZFpQBj4YnePGoq/S38w6nz+vqg5WZLrLGY4YuSemdMq47GMLBPO42MzwdGwpg/agZ7xzZcFHa48xlywfg==",
+    "@img/sharp-libvips-darwin-arm64@1.3.3": {
+      "integrity": "sha512-suTBPTDGrI9WodccaDdwZItTSaBYASlBk1NSfElSHrUfzu3szG6lvIF58+WiFvnfzuK8ZBFS5zE00PxqxnRiPg==",
       "os": ["darwin"],
       "cpu": ["arm64"]
     },
-    "@img/sharp-libvips-darwin-x64@1.3.2": {
-      "integrity": "sha512-m2pW1n6cns9VaubNwsZ+c3CRYjxNQWgJ5gPlnL1nbBcpkBvFm6SCFN5o0psFHI8w9n11NKhFkeEDns98tiqbEw==",
+    "@img/sharp-libvips-darwin-x64@1.3.3": {
+      "integrity": "sha512-FVJZ5mITMobmXIz/hPDTw0EintTW5H3WfrxwLqEqjiIihlu+hVRyGrFQ60xl0Lxn7Bt3zdpevPaQi0HEzqz9fw==",
       "os": ["darwin"],
       "cpu": ["x64"]
     },
-    "@img/sharp-libvips-linux-arm64@1.3.2": {
-      "integrity": "sha512-dqVSFynCox4C/J8kT16V7SIFAns0IjgLwkvYT7p8LQVmJ5OS5b6tI9IGflxTeuBS//zXeFIUbwt5dwxyZ17cnA==",
+    "@img/sharp-libvips-linux-arm64@1.3.3": {
+      "integrity": "sha512-0DaL0A6Xu6sQSQFwe4iVCrKWU2cCTItnRsYsCdxAMm9NF6twAA9BKnoqy4hqz4+azQ0JHuA26qiUKsf1XJ/v5A==",
       "os": ["linux"],
       "cpu": ["arm64"]
     },
-    "@img/sharp-libvips-linux-arm@1.3.2": {
-      "integrity": "sha512-1eMLzy92I4J6rmi4mAT8yC3HxOtniyGELlzGbNMLLeqe052ahFQ0h6LFq+lh5DsDIdYViIDst08abvSbcEdLXQ==",
+    "@img/sharp-libvips-linux-arm@1.3.3": {
+      "integrity": "sha512-3rbU4vqXXc3hY/OiXdl52xZvT0F1yEngWfvqudtPJg/KkyiaQw2DRsFrNzpmLvfavbwOq3qXn36GP8obHRULQA==",
       "os": ["linux"],
       "cpu": ["arm"]
     },
-    "@img/sharp-libvips-linux-ppc64@1.3.2": {
-      "integrity": "sha512-3z0NHDxD6n5I9gc05U1eW1AyRm+Gznzq3naMrthPNqE6oYykcogW0l/jfpJdjYnuNl8R7yI9pNbE1XiUeyq0Aw==",
+    "@img/sharp-libvips-linux-ppc64@1.3.3": {
+      "integrity": "sha512-cdn1OvUBwsXhbC0zSzJnNzf5MZ/mTrobawDvNXBTxe8VtqKAm0sRuEY2Evzovb/w9JMk4TvRxqt1mekSuJz64w==",
       "os": ["linux"],
       "cpu": ["ppc64"]
     },
-    "@img/sharp-libvips-linux-riscv64@1.3.2": {
-      "integrity": "sha512-bsb4rI+NldGOsXuej2r8OdSS8+zXDVaCWxyWrcv6kneTOlgAHtZABRzBBCwdsPiD90J4myNJuHpg6kA20ImW/w==",
+    "@img/sharp-libvips-linux-riscv64@1.3.3": {
+      "integrity": "sha512-HjPVx7yKz+0lqdhDlTw1tt90wamBoxhiXpvl1XZpJLiHH4RCJ5yDTqH+VlYPv2fwFs89JFw4c1IexYOcQUi4IQ==",
       "os": ["linux"],
       "cpu": ["riscv64"]
     },
-    "@img/sharp-libvips-linux-s390x@1.3.2": {
-      "integrity": "sha512-/ABshyj8gCpyIrNXnHn4LorDJ0HHm1VhXPBlxZ8zAtfVPAaSafXPGn+sUSIRiwaSBy0mmFjSjiXI5mkcwdChKQ==",
+    "@img/sharp-libvips-linux-s390x@1.3.3": {
+      "integrity": "sha512-neWLh+3yCNThxnfy3c4BbVBeGgt9aftno+XbT56iK28RgeDs3UOFWviLWlUu0bArYVYJaFDK+RRohbicUNCm8Q==",
       "os": ["linux"],
       "cpu": ["s390x"]
     },
-    "@img/sharp-libvips-linux-x64@1.3.2": {
-      "integrity": "sha512-ITPEtgffGJ0S6G9dRyw/366tJQqFRcHWPHhC+Stpg3Z8AEMrDrTr2lhdz4f/Y/HMbRh//7Z5mBzEpVdi62Oc3w==",
+    "@img/sharp-libvips-linux-x64@1.3.3": {
+      "integrity": "sha512-4vKmvAst9nrowcqquKFAyZJUDolUaIp8uRiN0mWFguJ1IplC9/pitXtlnnlU4aa/eJw3J7i67V+pwUL+wZGdsA==",
       "os": ["linux"],
       "cpu": ["x64"]
     },
-    "@img/sharp-libvips-linuxmusl-arm64@1.3.2": {
-      "integrity": "sha512-zE9EdiUzUmg5mDT5a1rk5fYJ6GWPloTwWBYDS14naqHsL+EaMpDj1AWnpLgh3u0YCORv2Tt50wrcrpYqkP97Kw==",
+    "@img/sharp-libvips-linuxmusl-arm64@1.3.3": {
+      "integrity": "sha512-Y9kQaLMuNoB0bPYOOdcZMaseNrFpPodIWWMrx+CZyydf2xn68j9WYc6sWWRrDwNkzCQjKYfc68L7jKjGlHMibw==",
       "os": ["linux"],
       "cpu": ["arm64"]
     },
-    "@img/sharp-libvips-linuxmusl-x64@1.3.2": {
-      "integrity": "sha512-m0lrLiUt+lBYnCFr8qV/65yMR4E/c7/wf78I5eKTdkEakFAlZ9QlzEM3QIhhAwVeUhLAHLcCq7a7Vszq/oFNZQ==",
+    "@img/sharp-libvips-linuxmusl-x64@1.3.3": {
+      "integrity": "sha512-fj8Mv0HHfD1Rr+4I68+3agJynxDWtBFgicTbSOb9Bke6pIwzGcJ+RX/yHjmiEGFMCavY/dxvem7MyNaJF+wDiw==",
       "os": ["linux"],
       "cpu": ["x64"]
     },
-    "@img/sharp-linux-arm64@0.35.3": {
-      "integrity": "sha512-QgKDspHPnrU+GQ55XPhGwyhC8acLVOOSyAvo1oVfFmrIXLkDNmGWzAfDZ4xK8oSA1qBQrALcHX0G5UZni/SuFQ==",
+    "@img/sharp-linux-arm64@0.35.4": {
+      "integrity": "sha512-De4jpEnAU8Hd5oT0j1G3uL4ZvTuipVMn7YC6vPaJhy6/7EwEae0SVAoBrUMYQbkLGDm85taVWwuPc1a44LTzCQ==",
       "optionalDependencies": [
         "@img/sharp-libvips-linux-arm64"
       ],
       "os": ["linux"],
       "cpu": ["arm64"]
     },
-    "@img/sharp-linux-arm@0.35.3": {
-      "integrity": "sha512-affVWCTLooy8TSxbDx2qkzuDeaWLNVBA+P//FNBirHsXpP2fuBhk5AuboYUnrDnzoXes8GFjpTx0SBFOCRg+FA==",
+    "@img/sharp-linux-arm@0.35.4": {
+      "integrity": "sha512-7OAS8gI0EReKGVN2HssHlM6umJgxF5VI3xN0p9FA91p/YO+ou5hiNghLdZ5BEHztwaaK5+bLKRf8x/o2L2nk9A==",
       "optionalDependencies": [
         "@img/sharp-libvips-linux-arm"
       ],
       "os": ["linux"],
       "cpu": ["arm"]
     },
-    "@img/sharp-linux-ppc64@0.35.3": {
-      "integrity": "sha512-sMd8rDxmpLOwv/7N44klFjOD5DUO7FLdjiXDI0hoxYaf7Ar262dQIEkosE98bps+5HPLtp/EvNqeqQtOycP/IA==",
+    "@img/sharp-linux-ppc64@0.35.4": {
+      "integrity": "sha512-2oYZJeIl4kCcMGk4ouZVjnkCtFrpQFlNEtJ6GbxzhHQchwH0NH/qEb9ykmOl29dqwMq+JhFdZn+1ak2FKhI9fQ==",
       "optionalDependencies": [
         "@img/sharp-libvips-linux-ppc64"
       ],
       "os": ["linux"],
       "cpu": ["ppc64"]
     },
-    "@img/sharp-linux-riscv64@0.35.3": {
-      "integrity": "sha512-0Eob78yjlYPfL5vMNWAW55l3R9Y6BQS/gOfe0ZcP9mEz9ohhKSt4im1hayiknXgf8AWrFqMvJcKIdmLmEe7yeQ==",
+    "@img/sharp-linux-riscv64@0.35.4": {
+      "integrity": "sha512-cPbNChoRURAWdebDIHSenxRpgEdy7JkPydSnUxRm9VvKD7m0/xVaR/8Fzlu81pk5nHEvHH87UZUA7cTtwnbJSA==",
       "optionalDependencies": [
         "@img/sharp-libvips-linux-riscv64"
       ],
       "os": ["linux"],
       "cpu": ["riscv64"]
     },
-    "@img/sharp-linux-s390x@0.35.3": {
-      "integrity": "sha512-KgAxQ0DxpNOq1rG2t5cgTgShJFGSuU7XO45cqC+1NVOuZnP6tlgZRuSYOfNupGkHID0o3cJOsw4DVeJpMovcGw==",
+    "@img/sharp-linux-s390x@0.35.4": {
+      "integrity": "sha512-RY0JFY8Fd6RonCBtHz+DvadaPkXDSI1AUn6yWL9TipqkZ1vY8w8evqdgyDFnkm4/K1ve1TvZiaePP5oSd4+WVQ==",
       "optionalDependencies": [
         "@img/sharp-libvips-linux-s390x"
       ],
       "os": ["linux"],
       "cpu": ["s390x"]
     },
-    "@img/sharp-linux-x64@0.35.3": {
-      "integrity": "sha512-8pqvxubL2PGdhlPy6GLqzDYMUjyRmKAwKHYKixpdJYBUK7PJ0C029XdsnpFIdgRZG68fZiGdHVWcKPvtiPB4cA==",
+    "@img/sharp-linux-x64@0.35.4": {
+      "integrity": "sha512-9qvvEAuk8k89TfWUoX2htWjbAMX8p+NxCppjpcg5k6xMsjhBQPTsoIh36h9Qde4WRuGpJeYnOjdosDn/cnv+OA==",
       "optionalDependencies": [
         "@img/sharp-libvips-linux-x64"
       ],
       "os": ["linux"],
       "cpu": ["x64"]
     },
-    "@img/sharp-linuxmusl-arm64@0.35.3": {
-      "integrity": "sha512-Vz0iQjzzcSX3HCbfwFfCSG/9SCIqyO0mH2sXyiHaAYfBk0cRsCWXRyQYX0ovCK/PAQBbTzQ0dsPQHh5MAFL59w==",
+    "@img/sharp-linuxmusl-arm64@0.35.4": {
+      "integrity": "sha512-KB5jxpfWQTr0nc3xdHtWChdbifHrBGsd2SM62Eyxrl8afikm+f5qGBU75SJIZBT/S1MC8XyacdlXBMSWq6OURA==",
       "optionalDependencies": [
         "@img/sharp-libvips-linuxmusl-arm64"
       ],
       "os": ["linux"],
       "cpu": ["arm64"]
     },
-    "@img/sharp-linuxmusl-x64@0.35.3": {
-      "integrity": "sha512-6O1NPKcDVj9QEdg7Hx549EX8U0rp6yXQERqru6yRN7fGBn32UvIRJUlWnk+8xDCiG76hXVBbX82NZ/ZKr0euIg==",
+    "@img/sharp-linuxmusl-x64@0.35.4": {
+      "integrity": "sha512-f+eZJZIQNEEd26RPSW+76chwOf1XtA2Y/O+5ocVyLliHkeih3e+jhLVBdNTd2rS3IbNXK8+ug93Vf5ZXtF5Lxg==",
       "optionalDependencies": [
         "@img/sharp-libvips-linuxmusl-x64"
       ],
       "os": ["linux"],
       "cpu": ["x64"]
     },
-    "@img/sharp-wasm32@0.35.3": {
-      "integrity": "sha512-cZ0XkcYGpHZkqW6iCkqTcmUC0CD9DhD5d/qeZlZkfRBn6GnHniZXLUo5+9xw8Iv76YE6LQFN9YNBlKREcCG76w==",
+    "@img/sharp-wasm32@0.35.4": {
+      "integrity": "sha512-zQnl4Kwp7Q6NHsENtU2T/00Zi+w3AQNwz3+UaTyVBy2FpXrzXzGjndpK61onhZjRtRpQXxCTeqw19bVyXOh7jA==",
       "dependencies": [
-        "@emnapi/runtime@1.11.1"
+        "@emnapi/runtime@1.11.3"
       ]
     },
-    "@img/sharp-webcontainers-wasm32@0.35.3": {
-      "integrity": "sha512-2rnq7bX3NzeR2T4YWgz8qiG4h3TSdMe+vN1iQXpJleSJ3SM5zQ8Fy2SyyXAWlbxpEZ2Y+Z4u1BePgJEYbSy80Q==",
+    "@img/sharp-webcontainers-wasm32@0.35.4": {
+      "integrity": "sha512-ESfNkywmCfPNyaZjxooddJQiQ+l/nTpGEOGthxiLnIHXC/CmcBixnfwUleX9mCz9ovrUUvKMap/pm8RYbzfwaA==",
       "dependencies": [
         "@img/sharp-wasm32"
       ],
       "cpu": ["wasm32"]
     },
-    "@img/sharp-win32-arm64@0.35.3": {
-      "integrity": "sha512-4bPwFdMbeC4JQ8L8LOyWp6nsHcboP5fxkp6iPOXz2Vg49R42TuMs2whkJ5OAP4/Ul035qOzy0AecOF9VOscn4w==",
+    "@img/sharp-win32-arm64@0.35.4": {
+      "integrity": "sha512-iNdlBX9gLVvqe2I3uIJSIKTq6wckP/DYxZtcqxm09x5Gi24DnFBmPAWZmr60ZyYMG0xlzo6goG3670ar+RXvRw==",
       "os": ["win32"],
       "cpu": ["arm64"]
     },
-    "@img/sharp-win32-ia32@0.35.3": {
-      "integrity": "sha512-r53mXsBN6lFUDiST764SvgwUdHAqM4rPAiDzAmf4fLoB6X/rkfyTrLCg6+g17wJJiCmB3JYgHuUldCWUIRFSXw==",
+    "@img/sharp-win32-ia32@0.35.4": {
+      "integrity": "sha512-kqRsbaa5CS6KHlpxnN7WhE6vAAugXyZButpRdvDWetlv6Qv4N9WTcrWzF7tXfB9T7MsoadqdI8hmwLq6UlLvtw==",
       "os": ["win32"],
       "cpu": ["ia32"]
     },
-    "@img/sharp-win32-x64@0.35.3": {
-      "integrity": "sha512-D4y1vNeZrIIJCN+uHaWVtH86B+aCrdMYYjicy9pXHvbGZeGYLLSd3wdVuC37FxVXlU1ARsk84eKWfWMXGYEqvA==",
+    "@img/sharp-win32-x64@0.35.4": {
+      "integrity": "sha512-XtmnYhBcrORsJ4XJngyzr/EWP0hRZLAZRFaApdKuviyqF78+ylxh2y06ZmtULAMOnObJ3ucpN0AcwSWnMowTRg==",
       "os": ["win32"],
       "cpu": ["x64"]
     },
@@ -2751,8 +2759,8 @@
       "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
       "bin": true
     },
-    "sharp@0.35.3": {
-      "integrity": "sha512-ej0zVHuZGHCiABXcNxeYhpRnPNPAcvbG8RMdBAhDAxLKkCRVSpK3Iyu7qbqw3JMzoj0REeM6f3tJLtVwl0023Q==",
+    "sharp@0.35.4": {
+      "integrity": "sha512-n++8XWcj+jCOr2IOl7h8LbKnGBDY4aPbmprMONBNFdn0ImXqpGVv5zliDs0V9HbmbCQLpbuo2ej9rAoOQTvMDA==",
       "dependencies": [
         "@img/colour",
         "detect-libc",
@@ -3110,6 +3118,7 @@
         "npm:astro@^7.2.0",
         "npm:sanitize.css@13",
         "npm:sass@^1.102.0",
+        "npm:sharp@~0.35.4",
         "npm:typescript@^6.0.3",
         "npm:unist-util-visit@^5.1.0"
       ],
flake.nix
@@ -43,6 +43,14 @@
             vscode-json-languageserver
             tinymist # Typst LSP
           ];
+
+          env = [
+            {
+              # For Sharp in Deno
+              name = "LD_LIBRARY_PATH";
+              prefix = "${pkgs.stdenv.cc.cc.lib}/lib";
+            }
+          ];
         };
 
         treefmt = {
package.json
@@ -20,6 +20,7 @@
     "astro-typst": "^0.12.4",
     "sanitize.css": "^13.0.0",
     "sass": "^1.102.0",
+    "sharp": "^0.35.4",
     "unist-util-visit": "^5.1.0"
   },
   "devDependencies": {