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.
This commit is contained in:
Julien Donck
2026-08-25 11:36:01 +02:00
committed by Rūdolfs Ošiņš
parent fc31f33fad
commit ab5aaa0382
14 changed files with 66 additions and 487 deletions
+4 -4
View File
@@ -112,10 +112,10 @@ The `nodes.homepage` key controls what is rendered at the root URL (`/`):
This is the default so that a self-hosted deployment acts as a dedicated
front-end for its own seed.
* `"landing"` — serve the full Radicle marketing site: the landing page and
its `/learn`, `/install` and documentation (`/faq`, `/glossary`,
`/guides/*`) sub-routes. This is opt-in and intended for the canonical
[radicle.dev][app] deployment; the marketing sub-routes are only reachable
in this mode.
its `/learn`, `/install` and documentation (`/glossary`, `/guides/*`)
sub-routes. This is opt-in and intended for the canonical
[radicle.network][app] deployment; the marketing sub-routes are only
reachable in this mode.
The cross-seed Explore page is always available at `/explore`, regardless of
the `homepage` setting.
+1
View File
@@ -0,0 +1 @@
/faq https://radicle.dev/faq 301
+2 -2
View File
@@ -205,7 +205,7 @@
}
/* On section-based pages, each top-level block rises and fades in, staggered,
when the page mounts. Markdown docs (FAQ, glossary, guides) are a flat list
when the page mounts. Markdown docs (glossary, guides) are a flat list
of paragraphs, so they rise in as a single block instead. */
@keyframes section-rise {
from {
@@ -233,7 +233,7 @@
animation-delay: 350ms;
}
/* Markdown docs (FAQ, glossary, guides) are a flat element list. Animate the
/* Markdown docs (glossary, guides) are a flat element list. Animate the
elements with the same timing (no stagger) so the doc rises in as one block.
This lives on the children, not the reused .markdown-content container, so it
replays on client-side navigation and not only on hard reload. */
+4 -4
View File
@@ -147,9 +147,10 @@ const loadExecutor = mutexExecutor.create();
async function navigate(
action: "push" | "replace",
newRoute: Route,
// Optional URL fragment to preserve, so links like `/faq#general` land on the
// right section. `routeToPath` builds only the path, so the hash is appended
// here and picked up by `restoreScroll` below once the view has rendered.
// Optional URL fragment to preserve, so links like `/glossary#delegate`
// land on the right section. `routeToPath` builds only the path, so the
// hash is appended here and picked up by `restoreScroll` below once the
// view has rendered.
hash: string = "",
): Promise<void> {
isLoading.set(true);
@@ -480,7 +481,6 @@ function urlToRoute(url: URL): Route | null {
}
case "learn":
case "install":
case "faq":
case "glossary":
case "download":
case "guides":
+17 -106
View File
@@ -29,7 +29,7 @@
$: isGuide = page.startsWith("guides/");
$: hero = guideHeroes[page];
$: isInlineGap = page === "faq" || page === "glossary";
$: isGlossary = page === "glossary";
// Copy a term's permalink to the clipboard instead of navigating to it, and
// briefly show a checkmark as confirmation. Navigating to the link from
@@ -90,107 +90,33 @@
},
};
}
let toc: { id: string; text: string }[] = [];
// Build a sidebar index from the rendered section headings, rebuilding when
// the page changes.
function collectToc(node: HTMLElement, _page: DocsPage) {
function build() {
const headings = node.querySelectorAll<HTMLElement>(
".markdown-content h2[id]",
);
toc = Array.from(headings).map(heading => ({
id: heading.id,
text: heading.textContent?.trim() ?? "",
}));
}
build();
return {
update() {
build();
},
destroy() {
toc = [];
},
};
}
</script>
<style>
.doc-inline {
.doc-glossary {
--content-inline-gap: 4rem;
}
.doc-inline :global(.page-container) {
.doc-glossary :global(.page-container) {
padding-left: var(--content-inline-gap);
padding-right: var(--content-inline-gap);
}
.doc-inline :global(.markdown-content) {
.doc-glossary :global(.markdown-content) {
max-width: none;
}
/* Match the page-title size used on the other marketing pages, rather than
the larger default markdown h1. */
.doc-inline :global(.markdown-content h1) {
.doc-glossary :global(.markdown-content h1) {
font: var(--txt-bold-32);
letter-spacing: normal;
}
.doc-two-col {
display: grid;
grid-template-columns: minmax(0, 1fr) clamp(12rem, 18vw, 15rem);
gap: 3rem;
padding: 0 var(--content-inline-gap);
}
.doc-two-col :global(.page-container) {
padding-left: 0;
padding-right: 0;
}
.doc-inline :global(.markdown-content h2) {
.doc-glossary :global(.markdown-content h2) {
scroll-margin-top: 6rem;
}
.doc-toc {
position: sticky;
top: calc(var(--global-header-height) + 6rem);
align-self: start;
margin-top: 6rem;
font: var(--txt-medium-14);
line-height: 1.6;
}
.doc-toc ul {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.doc-toc-link {
color: var(--color-text-tertiary);
text-decoration: none;
}
.doc-toc-link:hover {
color: var(--color-text-primary);
}
@media (max-width: 64rem) {
.doc-two-col {
display: block;
}
.doc-toc {
display: none;
}
}
.doc-glossary :global(.markdown-content p) {
color: var(--color-text-tertiary);
}
@@ -199,16 +125,16 @@
color: var(--color-text-primary);
}
/* Linkable entries: glossary term paragraphs and FAQ question headings.
Both are tagged with the `term` class by the rehypeTermAnchors plugin. */
.doc-inline :global(.markdown-content .term) {
/* Linkable entries: glossary term paragraphs, tagged with the `term` class
by the rehypeTermAnchors plugin. */
.doc-glossary :global(.markdown-content .term) {
position: relative;
padding-left: 1.5rem;
margin-left: -1.5rem;
scroll-margin-top: 6rem;
}
.doc-inline :global(.term-link) {
.doc-glossary :global(.term-link) {
position: absolute;
left: 0;
top: 0.4em;
@@ -221,12 +147,12 @@
mask: url("/icons/link.svg") center / 1rem 1rem no-repeat;
}
.doc-inline :global(.markdown-content .term:hover .term-link) {
.doc-glossary :global(.markdown-content .term:hover .term-link) {
opacity: 1;
transition: opacity 0.15s ease;
}
.doc-inline :global(.term-link.copied) {
.doc-glossary :global(.term-link.copied) {
background-color: var(--color-text-primary);
-webkit-mask-image: url("/icons/check.svg");
mask-image: url("/icons/check.svg");
@@ -252,18 +178,18 @@
}
}
.doc-inline :global(.markdown-content .term.arrived) {
.doc-glossary :global(.markdown-content .term.arrived) {
animation: term-arrive 2s ease-out;
}
@media (max-width: 90rem) {
.doc-inline {
.doc-glossary {
--content-inline-gap: 2rem;
}
}
@media (max-width: 64rem) {
.doc-inline {
.doc-glossary {
--content-inline-gap: 1.5rem;
}
}
@@ -278,26 +204,11 @@
<svelte:component this={component} />
</MarkdownLayout>
</div>
{:else if isInlineGap}
<div
class="doc-inline doc-two-col"
class:doc-glossary={page === "glossary"}
use:copyTermLinks
use:collectToc={page}>
{:else if isGlossary}
<div class="doc-glossary" use:copyTermLinks>
<MarkdownLayout>
<svelte:component this={component} />
</MarkdownLayout>
{#if page === "faq" && toc.length}
<aside class="doc-toc" aria-label="On this page">
<ul>
{#each toc as item}
<li>
<a class="doc-toc-link" href={`#${item.id}`}>{item.text}</a>
</li>
{/each}
</ul>
</aside>
{/if}
</div>
{:else}
<MarkdownLayout>
-1
View File
@@ -77,7 +77,6 @@
Get started
</a>
<a href="/learn#guides" class="txt-medium-14" use:link>Guides</a>
<a href="/faq" class="txt-medium-14" use:link>FAQ</a>
<a href="/glossary" class="txt-medium-14" use:link>Glossary</a>
</div>
-86
View File
@@ -97,44 +97,6 @@
margin: 0;
}
.faq-row {
--product-card-gap: 1.5rem;
align-items: stretch;
}
.faq-row .product-card {
position: relative;
display: flex;
padding: 1.5rem;
border-radius: var(--border-radius-small);
background: var(--color-surface-subtle);
}
.faq-row .product-card-text {
flex: 1;
padding-right: 0;
}
.faq-row .product-title {
font: var(--txt-bold-16);
}
.faq-row .product-description {
margin-bottom: 0;
}
.faq-row .product-link {
margin-top: auto;
padding-top: 1.25rem;
}
/* Stretch the read-more link across the card so the whole box is clickable. */
.faq-row .product-link::after {
content: "";
position: absolute;
inset: 0;
}
.product-link {
font: var(--txt-bold-14);
}
@@ -412,54 +374,6 @@
</a>
</section>
<section class="section">
<div class="section-header">
<h2 class="txt-bold-24">FAQ</h2>
<p class="txt-medium-16 txt-color-tertiary">
Answers to the most common questions about Radicle. What it is, how it
compares to GitHub, and the technical details behind the protocol.
</p>
</div>
<div class="card-row faq-row">
<article class="product-card">
<div class="product-card-text">
<strong class="product-title">General questions</strong>
<span class="product-description">
What is Radicle and how is it different from GitHub? Who’s building
it, how many people use it, and what’s the project’s history.
</span>
<a
href="/faq#general-questions"
class="product-link arrow-link"
use:link>
Read general FAQ <span class="link-arrow link-arrow-right">→</span>
</a>
</div>
</article>
<article class="product-card">
<div class="product-card-text">
<strong class="product-title">Technical questions</strong>
<span class="product-description">
What does “peer-to-peer” mean in practice? Why not just use Git
directly? How does identity work without accounts? The technical
reasoning explained.
</span>
<a
href="/faq#technical-questions"
class="product-link arrow-link"
use:link>
Read technical FAQ <span class="link-arrow link-arrow-right">
→
</span>
</a>
</div>
</article>
</div>
<a href="/faq" class="section-all product-link arrow-link" use:link>
All FAQ <span class="link-arrow link-arrow-right">→</span>
</a>
</section>
<section class="garden-promo">
<div class="garden-promo-text">
<h3 class="txt-bold-22">
+1 -12
View File
@@ -20,7 +20,6 @@
const principlesRoute: Route = { resource: "principles", params: undefined };
const guidesRoute: Route = { resource: "guides", params: undefined };
const exploreRoute: Route = { resource: "explore", params: undefined };
const faqRoute: Route = { resource: "docs", params: { page: "faq" } };
const glossaryRoute: Route = {
resource: "docs",
params: { page: "glossary" },
@@ -31,7 +30,7 @@
// Only show a section's sub-nav while the viewer is within that section.
$: installOpen = ["/install", "/cli", "/desktop"].includes(pathname);
$: learnOpen =
["/learn", "/principles", "/faq", "/glossary"].includes(pathname) ||
["/learn", "/principles", "/glossary"].includes(pathname) ||
pathname.startsWith("/guides");
// Flag the document element only when a section actually opens or closes, so
@@ -255,16 +254,6 @@
{/if}
Guides
</a>
<a
href={routeToPath(faqRoute)}
class="nav-link"
class:active={pathname === "/faq"}
on:click={e => onNav(e, faqRoute)}>
{#if pathname === "/faq"}
<span class="dot"></span>
{/if}
FAQ
</a>
<a
href={routeToPath(glossaryRoute)}
class="nav-link"
-219
View File
@@ -1,219 +0,0 @@
---
title: FAQ
---
<script>
import Meta from '@app/marketing/Meta.svelte';
</script>
<svelte:head>
<title>FAQ - Radicle</title>
</svelte:head>
<Meta title="FAQ | Radicle" description="Answers to the most common questions about Radicle." canonical="https://radicle.dev/faq" />
<h1 class="txt-bold-32">FAQ. <span class="txt-color-tertiary">Answers to common questions.</span></h1>
<style>
:global(.markdown-content p),
:global(.markdown-content li) {
font: var(--txt-medium-16);
line-height: 1.6;
color: var(--color-text-tertiary);
}
</style>
If you can’t find an answer here, drop by our [support channel][support] and ask it there.
[support]: https://radicle.zulipchat.com/#narrow/stream/369873-support
## General questions
### What is Radicle? How is it different from Git/GitHub?
Radicle is a peer-to-peer code collaboration platform ([“forge”][forge]) built on Git. Unlike centralized platforms like GitHub, there is no single entity controlling the network or user data. Repositories are replicated across peers in a decentralized manner. Radicle is an alternative for people and organizations who want full control of their data and user experience, without compromising on the social aspects of collaboration platforms.
[forge]: https://en.wikipedia.org/wiki/Forge_(software)
### Who is using Radicle currently? How many users does it have?
As of September 2024, there are around 2000 repositories hosted on the network, and a little over 200 nodes online on a weekly basis.
### How many people are working on it?
We currently have around 12 people working on Radicle, spread across the protocol, CLI, TUI, web, operations and content.
### How long have you been working on Radicle?
The current codebase is a little under 2 years old, but we’ve been working on the problem for over 4 years in total. Previous iterations of Radicle can be found online, but are considered deprecated in favor of [Heartwood][heartwood], the latest generation of the protocol.
[heartwood]: https://radicle.network/nodes/seed.radicle.xyz/rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5
## Technical questions
### What do you mean by “peer-to-peer”?
We define it the same way [Wikipedia][p2p] defines it; specifically this part: “Peers are equally privileged, equipotent participants in the network”.
So a peer-to-peer system is one where all participants are “equally privileged in the network”. This usually means they all run the same software as well.
[p2p]: https://en.wikipedia.org/wiki/Peer-to-peer
### Isn’t Git already peer-to-peer? Why do I need Radicle?
While Git is designed in some way for peer-to-peer interactions, there is no deployment of it that works that way. All deployments use the client-server model because Git lacks functionality to be deployed as-is in a peer-to-peer network.
For one, it has no way of verifying that the repository you downloaded after a `git clone` is the one you asked for, which means you need to clone from a trusted source (ie. a known server). This isn’t compatible with peer-to-peer in any useful way.
Radicle solves this by assigning stable identities to repositories that can be verified locally, allowing repositories to be served by untrusted parties.
### What does Radicle solve that isn’t already solved by Git commit signatures?
Verifying Git commit signatures, eg. via `git verify-commit` is a good security practice, however Git doesn’t solve the problem of knowing whether a signature was made by an active maintainer of the repository or not. It can only indicate whether the signature is valid or not.
Therefore, to ensure that the repository head was signed by one of the repository’s _current maintainers_, users would have to keep track of what the current maintainer set is at all times, getting that information from a trusted source.
Radicle checks this automatically, by verifying changes to the maintainer set through the repository identity document. All you need to know is that you have the correct Repository ID (RID). See [RIP-2][rip-2] for more information.
[rip-2]: https://radicle.network/nodes/seed.radicle.xyz/rad:z3trNYnLWS11cJWC6BbxDs5niGo82/tree/0002-identity.md
### Isn’t SHA-1 broken? How do you handle hash collisions?
SHA-1 is indeed broken, which is why Git uses a “hardened” SHA-1 hash function that can detect collisions, and generate a new “safe” hash. This process is called [counter-cryptanalysis][cc]. Radicle uses Git’s hardened SHA-1 function, since all replicated objects are stored in Git, and is therefore safe from collision attacks.
[cc]: https://marc-stevens.nl/research/papers/C13-S.pdf
### How does Radicle handle code discoverability and search, compared to centralized platforms?
Basic search functionality exists, but this area is still a work-in-progress. We expect more improvements in search/discovery in the future.
### Can Radicle handle large binary files like datasets or machine learning models?
Radicle uses Git internally, which can handle moderately-sized binary data relatively well. For large data, we built [Radicle Artifact](https://radicle.network/nodes/daniel.radicle.garden/rad:z4VYyJ9KuwMNkXGQnmKuGPGKw3inv) which complements Radicle with peer-to-peer artifact distribution and provenance. It can be used to distribute any file that would typically be too large for a repository like open source AI models, and large data sets.
### How does Radicle ensure persistence and availability of repositories when nodes go offline?
Repositories are replicated across multiple peers on the network, so they remain available as long as at least one seeding node is online. Users have fine-grained control over which repositories they personally host and seed.
Additionally, certain nodes on the network act as _seed nodes_ - always-on public nodes - that ensure a certain level of data availability.
### How does Radicle handle issues, pull requests, code reviews etc. compared to GitHub?
Radicle has its own model for issues and pull requests (called “patches”), implemented via its own technology called Collaborative Objects (COBs). It supports non-destructive updates to patches and code reviews tied to patch revisions.
Patches and issues in Radicle are stored within Git’s object database and are therefore also available offline.
### Is there a way to run Radicle over Tor, I2P or other censorship-resistant networks?
Yes, Radicle is designed with Tor, Nym and I2P support in mind, though it’s still very early and these integrations haven’t been thoroughly tested.
### How does Radicle deal with NATs?
Radicle currently relies on seed nodes to serve data to peers behind [NATs][nat]. However, a [hole-punching][hp] protocol is currently under development, which would allow peers behind NATs to exchange data directly.
[hp]: https://en.wikipedia.org/wiki/Hole_punching_(networking)
[nat]: https://en.wikipedia.org/wiki/Network_address_translation
### How does Radicle deal with potential abuse, illegal content sharing etc. on the network?
Each node is free to choose which repositories to host (_seed_) using configured policies. Nodes can block specific repositories or peers exhibiting abusive behavior.
### Is there a way to host private repositories on Radicle?
Yes, Radicle supports private repositories that are only shared among a trusted set of peers, not the entire network. These are not encrypted at rest but rely on selective replication and are thus completely invisible to the rest of the network.
### What is the process for migrating existing GitHub projects to Radicle?
There are community tools under development for migrating issues and pull-requests. We are planning full migration tooling for the future.
### Is there a way to easily mirror GitHub repositories on Radicle?
There is no built-in mirroring yet, but you can easily set up a `cron` job to pull from GitHub and push to Radicle periodically.
### Does Radicle have plans to support Windows?
Currently Radicle only supports Unix-based systems like Linux and macOS. We plan to add Windows support if there is enough demand.
### How does Radicle ensure consistency and prevent multiple versions of the “same” repository?
Radicle assigns stable identities to repositories that can be cryptographically verified. Changes in repository ownership are signed by previous owners forming a verifiable chain of provenance.
### Does Radicle use existing peer-to-peer technologies such as IPFS?
No. The first generation of Radicle protocols was built on top of IPFS, but it was too slow and impractical to use. One of the key issues Radicle needs to solve is handling mutable objects: code repositories change all the time, so a content-addressable store like IPFS is not really a solution.
### Does Radicle use a DHT?
Radicle does not use DHT technology because DHTs don’t allow nodes on the network to have much of a say in terms of what content they participate in serving.
Additionally, DHTs can introduce high network latency, especially in the case of permissionless networks, due to requests beyond routed across the globe.
## User experience
### Are there plans to add multilingual/localization support to the UI?
We will add multilingual support if there is enough demand from the community.
### How does the user experience of using Radicle command line tools compare to Git?
Radicle is built on top of Git, so the workflow is quite similar. The `rad` CLI is provided for Radicle-specific functionality, and a Git [remote-helper][grh] is used for seamless integration with Git `push` and `pull` workflows.
Additionally, the `rad` CLI was designed to be as familiar as possible to Git users.
[grh]: https://git-scm.com/docs/gitremote-helpers
### What is the suggested development workflow when using Radicle?
The workflow is similar to using Git with a platform like GitHub. You clone a repository, make changes locally, then push to the network. Collaboration happens via issues, patches (pull requests), and code review. The difference is that Radicle doesn’t have a shared write access model like GitHub. Each user is required to push to their own namespace.
### How do I use Radicle with multiple devices?
At the moment, each device is required to be identified by a unique Radicle identity. We are currently working on proper multi-device support in Radicle, which will enable sharing a single identity across devices.
### Do I need to run a node to use Radicle?
Yes. The node is an essential part of what makes Radicle sovereign. However, you don’t have to keep your node running at all times, if you are trying to conserve bandwidth; Radicle works just as well while offline.
### How do you fork an abandoned repository?
With `rad init`.
When you fork an abandoned repo, you are essentially giving it a new repository identity, which is a new root of trust, with a new maintainer set. You’ll then have to communicate the new repository identifier and explain that this is a fork of the old repo.
### Are there plugins for code editors to work with Radicle?
There is a Visual Studio Code [integration][vscode] extension built by the community.
[vscode]: https://marketplace.visualstudio.com/items?itemName=radicle-ide-plugins-team.radicle
## Monetization & funding
### What is the monetization strategy for Radicle? Is Radicle free software?
The Radicle protocol and clients are free and open source software licensed under the MIT and Apache 2.0 licenses, and will remain free software for ever.
Radicle is currently funded by Radworks. Radworks intends to offer hosting and retrieval services on top of the Radicle protocol. All of Radworks financials are publicly accessible [here][tally].
### What is the relationship between Radicle and the Radworks (`$RAD`) token?
Radicle is a true peer-to-peer protocol. It doesn’t use nor depend on any blockchain or cryptocurrency.
[Radworks][radworks], the organization that has been financing Radicle is organized around the `RAD` token which is a governance token on Ethereum.
### How much funding has Radicle raised?
The Radicle team is funded by [Radworks][radworks]. To date, around $7m has been granted towards the development of Radicle, by Radworks. Grant proposals for the last two years can be found [here][tally].
## Legal
### Am I liable as a node operator?
Node operators should consult applicable local regulations. Radicle is free and open-source software; the project does not provide legal advice.
[tally]: https://www.tally.xyz/gov/radworks
[radworks]: https://radworks.org
---
If you have any other questions, feel free to drop into our [support channel](https://radicle.zulipchat.com/#narrow/stream/369873-support) on Zulip.
-3
View File
@@ -22,7 +22,6 @@ const docsLoaders = new Map<
DocsPage,
() => Promise<{ default: ComponentType }>
>([
["faq", () => import("./docs/faq.md")],
["glossary", () => import("./docs/glossary.md")],
["download", () => import("./docs/download.md")],
["guides/getting-started", () => import("./docs/guides/getting-started.md")],
@@ -32,7 +31,6 @@ const docsLoaders = new Map<
]);
const docsTitles = new Map<DocsPage, string>([
["faq", "FAQ"],
["glossary", "Glossary"],
["download", "Download"],
["guides/getting-started", "Getting started"],
@@ -81,7 +79,6 @@ export function marketingRoute(
return segments.length === 0
? { resource: "principles", params: undefined }
: null;
case "faq":
case "glossary":
case "download":
return segments.length === 0
-2
View File
@@ -36,7 +36,6 @@ export interface PrinciplesRoute {
}
export type DocsPage =
| "faq"
| "glossary"
| "download"
| "guides/getting-started"
@@ -45,7 +44,6 @@ export type DocsPage =
| "guides/user";
const docsPages: DocsPage[] = [
"faq",
"glossary",
"download",
"guides/getting-started",
+36 -44
View File
@@ -6,9 +6,9 @@ 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`) and FAQ question headings. Each
// linkable element also gets a `term` class that drives the shared styling and
// copy interaction in Docs.svelte.
// 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()
@@ -42,53 +42,45 @@ function addTermAnchor(node, id, label) {
}
function rehypeTermAnchors() {
return (tree, file) => {
const name = String((file && (file.filename || file.path)) || "");
const isFaq = name.endsWith("faq.md");
return tree => {
const seen = new Map();
const walk = nodes => {
for (const node of nodes) {
if (node.type === "element" && Array.isArray(node.children)) {
// Glossary: a bold term immediately followed by an em-dash. This is
// pattern-based, so it never matches FAQ or guide paragraphs.
if (node.tagName === "p") {
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);
// 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 (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}`;
}
} else if (
isFaq &&
node.tagName === "h3" &&
node.properties &&
node.properties.id
) {
// FAQ: each question heading. rehype-slug has already set the id.
addTermAnchor(node, node.properties.id, textOf(node).trim());
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)) {
+1 -1
View File
@@ -31,7 +31,7 @@ test("marketing sub-routes resolve in landing mode", async ({ page }) => {
).toBeVisible();
// A markdown doc route renders its compiled content.
await page.goto("/faq", { waitUntil: "networkidle" });
await page.goto("/glossary", { waitUntil: "networkidle" });
await expect(page.locator(".markdown-content")).toBeVisible();
});
-3
View File
@@ -1,3 +0,0 @@
{
"rewrites": [{ "source": "/(.*)", "destination": "/index.html" }]
}