Astro6.4
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.

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.
Sätteriでは既存のremark/rehypeプラグインは動作しないので、今までのプラグインをそのまま使う場合はunifiedを使う。
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;
remark-link-cardの代わり
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 で読み込む。
// 省略
markdown: {
processor: satteri({
mdastPlugins: [linkCardMdast],
hastPlugins: [addUPhoto, linkCardHast],
gfm: true,
}),
shikiConfig: {
theme: "catppuccin-frappe",
langs: [],
wrap: false,
},
},
// mdxプラグインの方でも同様に記述する
