Files
Julien Donck ab5aaa0382 Remove the FAQ from the marketing site
The FAQ shipped here was a verbatim copy of the one on radicle.dev,
carrying a canonical tag that already pointed search engines at that
original. Drop the duplicate and redirect /faq to radicle.dev/faq so
existing links still reach the content.
2026-08-25 11:36:01 +02:00

134 lines
3.8 KiB
JavaScript

import rehypeSlug from "rehype-slug";
import remarkUnwrapImages from "remark-unwrap-images";
import { getSingletonHighlighter } from "shiki";
import { mdsvex, escapeSvelte } from "mdsvex";
import { vitePreprocess } from "@sveltejs/vite-plugin-svelte";
// Add an id and a leading anchor link to linkable doc entries so they can be
// deep-linked and copied: glossary term paragraphs (a bold term followed by an
// em-dash, e.g. `**Term** — definition`). Each linkable element also gets a
// `term` class that drives the shared styling and copy interaction in
// Docs.svelte.
function slugify(text) {
return text
.toLowerCase()
.replace(/[^a-z0-9]+/g, "-")
.replace(/^-+|-+$/g, "");
}
function textOf(node) {
if (node.type === "text") {
return node.value;
}
if (Array.isArray(node.children)) {
return node.children.map(textOf).join("");
}
return "";
}
function addTermAnchor(node, id, label) {
node.properties = node.properties || {};
node.properties.className = [...(node.properties.className || []), "term"];
node.children.unshift({
type: "element",
tagName: "a",
properties: {
className: ["term-link"],
href: `#${id}`,
ariaLabel: `Link to ${label}`,
},
children: [],
});
}
function rehypeTermAnchors() {
return tree => {
const seen = new Map();
const walk = nodes => {
for (const node of nodes) {
// Glossary: a bold term immediately followed by an em-dash. This is
// pattern-based, so it never matches guide paragraphs.
if (
node.type === "element" &&
node.tagName === "p" &&
Array.isArray(node.children)
) {
const firstIndex = node.children.findIndex(
child => !(child.type === "text" && !child.value.trim()),
);
const first = node.children[firstIndex];
const after = node.children[firstIndex + 1];
const isTerm =
first &&
first.type === "element" &&
first.tagName === "strong" &&
after &&
after.type === "text" &&
/^\s*[—–-]/.test(after.value);
if (isTerm) {
const text = textOf(first).trim();
let slug = slugify(text);
const count = seen.get(slug) || 0;
seen.set(slug, count + 1);
if (count) {
slug = `${slug}-${count}`;
}
node.properties = node.properties || {};
node.properties.id = slug;
addTermAnchor(node, slug, text);
// Drop the em-dash separator from the rendered output, keeping just
// the bold term followed by its definition.
after.value = after.value.replace(/^\s*[—–-]\s*/, " ");
}
}
if (Array.isArray(node.children)) {
walk(node.children);
}
}
};
walk(tree.children);
};
}
const mdsvexOptions = {
extensions: [".md"],
highlight: {
highlighter: async (code, lang = "text") => {
const highlighter = await getSingletonHighlighter({
themes: ["github-light", "github-dark"],
langs: [
"bash",
"sh",
"toml",
"ini",
"rust",
"javascript",
"typescript",
"json",
"yaml",
"markdown",
"html",
"css",
],
});
const html = escapeSvelte(
highlighter.codeToHtml(code, {
lang,
themes: { light: "github-light", dark: "github-dark" },
defaultColor: false,
}),
);
return `{@html \`${html}\` }`;
},
},
remarkPlugins: [remarkUnwrapImages],
rehypePlugins: [rehypeSlug, rehypeTermAnchors],
};
export default {
extensions: [".svelte", ".md"],
preprocess: [vitePreprocess(), mdsvex(mdsvexOptions)],
};