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 | Astro favicon https://astro.build/blog/astro-640/
Astro 6.4 | Astro

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 favicon 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,    },  },// mdxプラグインの方でも同様に記述する