Sätteri ってなに?
Astro 6.4 | Astro
Astro 6.4 introduces a new pluggable Markdown processor API, a Rust-based Markdown processor for faster builds, and helpers to wire up experimental advanced routing with Cloudflare.
https://astro.build/blog/astro-640/Astroのバージョンが6.4に更新された。
Markdown処理の記述方法が変わるとともに将来的にはSätteriというRust製markdownパーサーに移行するのだという。
Sätteri
Sätteri puts flexible JavaScript plugins on top of a fast Rust Markdown / MDX engine. Best of both worlds.
https://satteri.bruits.org/Sätteriでは既存のremark/rehypeプラグインは動作しないので、今までのプラグインをそのまま使う場合はunifiedを使う。
astro.config.mjs
import { defineConfig } from 'astro/config';import { unified } from '@astrojs/markdown-remark';import remarkToc from 'remark-toc';export default defineConfig({ markdown: { processor: unified({ remarkPlugins: [remarkToc], }), },});
Sätteri に移行する
将来的に廃止ならば書き換えようということで BridgyFedのために作った u-photoクラス付与と 外部から追加していた remark-link-cardを Sätteri用に作成した。
u-photo付与
import { defineHastPlugin } from "satteri";const addUPhoto = defineHastPlugin({ name: "add-u-photo", element: { filter: ["img"], visit(node, ctx) { const existing = node.properties?.class ?? ""; const classList = (Array.isArray(existing) ? existing : String(existing).split(" ")) .map(c => c.trim()) .filter(Boolean); if (!classList.includes("u-photo")) { classList.push("u-photo"); } ctx.setProperty(node, "class", classList.join(" ")); }, },});export default addUPhoto;
import { defineMdastPlugin, defineHastPlugin } from "satteri";import ogs from "open-graph-scraper";const ogCache = new Map();async function isImageValid(url) { if (!url) return false; try { const res = await fetch(url, { method: "HEAD", signal: AbortSignal.timeout(3000) }); return res.ok; } catch { return false; }}async function fetchOgData(url) { try { const { result } = await ogs({ url, timeout: 8000 }); const domain = new URL(url).hostname; const imageUrl = result.ogImage?.[0]?.url ?? ""; const image = await isImageValid(imageUrl) ? imageUrl : ""; return { title: result.ogTitle ?? result.dcTitle ?? domain, description: result.ogDescription ?? "", image, favicon: "https://www.google.com/s2/favicons?domain=" + domain, }; } catch (e) { console.error("[link-card] OGS error:", e?.result?.error ?? String(e), "url:", url); const domain = new URL(url).hostname; return { title: domain, description: "", image: "", favicon: "https://www.google.com/s2/favicons?domain=" + domain, }; }}function makeText(value) { return { type: "text", value };}function makeEl(tagName, props, children) { return { type: "element", tagName, properties: props ?? {}, children: children ?? [] };}function buildCardNode(url, og) { const hasImage = og.image != null && og.image !== ""; return makeEl("a", { class: "rlc-container", href: url }, [ makeEl("div", { class: "rlc-info" }, [ makeEl("div", { class: "rlc-title" }, [makeText(og.title)]), ...(og.description ? [makeEl("div", { class: "rlc-description" }, [makeText(og.description)])] : []), makeEl("div", { class: "rlc-url-container" }, [ makeEl("img", { class: "rlc-favicon", src: og.favicon, alt: og.title + " favicon", width: "16", height: "16" }), makeEl("span", { class: "rlc-url" }, [makeText(url)]), ]), ]), ...(hasImage ? [makeEl("div", { class: "rlc-image-container" }, [ makeEl("img", { class: "rlc-image", src: og.image, alt: og.title }), ])] : []), ]);}export const linkCardMdast = defineMdastPlugin({ name: "link-card-collect", async paragraph(node) { if (node.children.length !== 1) return; const child = node.children[0]; if (child.type !== "link") return; if (child.children[0]?.value !== child.url) return; console.log("[link-card] fetching OGP for:", child.url); const og = await fetchOgData(child.url); ogCache.set(child.url, og); },});export const linkCardHast = defineHastPlugin({ name: "link-card-replace", element: { filter: ["p"], visit(node, ctx) { if (node.children.length !== 1) return; const a = node.children[0]; if (a.type !== "element" || a.tagName !== "a") return; const href = a.properties?.href; if (!ogCache.has(href)) return; ctx.replaceNode(node, buildCardNode(href, ogCache.get(href))); }, },});
これらを astro.config.mjs で読み込む。
astro.config.mjs
markdown: { processor: satteri({ mdastPlugins: [linkCardMdast], hastPlugins: [addUPhoto, linkCardHast], gfm: true, }), shikiConfig: { theme: "catppuccin-frappe", langs: [], wrap: false, }, },