// @ts-check
import { fileURLToPath } from "node:url";
import { defineConfig } from "astro/config";
import starlight from "@astrojs/starlight";
import starlightLlmActions from "starlight-llm-actions";
import starlightLinksValidator from "starlight-links-validator";
import sitemap from "@astrojs/sitemap";
import svelte from "@astrojs/svelte";
import tailwindcss from "@tailwindcss/vite";
import Icons from "unplugin-icons/vite";
import { fontsBase } from "../web/src/fontsBase.mjs";
// the ui components are shared with the web app, imported straight from its
// source rather than published as a package
const webLib = fileURLToPath(new URL("../web/src/lib", import.meta.url));
const base = "/docs";
const svelteStyleFallback = {
name: "svelte-style-fallback",
enforce: "pre",
transform(code, id) {
if (!id.includes("svelte&type=style&lang.css")) return;
if (!code.includes("<style")) return;
const match = code.match(/<style[^>]*>([\s\S]*?)<\/style>/i);
return { code: match ? match[1] : "", map: null };
},
};
export default defineConfig({
site: "https://tangled.org",
// served at tangled.org/docs by the docs worker. the build lands in dist/docs
// because static assets on workers are matched against the full request path
base,
outDir: "./dist/docs",
integrations: [
starlight({
title: "Tangled Docs",
titleDelimiter: "·",
disable404Route: true,
routeMiddleware: ["./src/starlightRouteData.ts"],
tableOfContents: { minHeadingLevel: 2, maxHeadingLevel: 2 },
plugins: [
starlightLlmActions({
renderMarkdown: "simple",
linkAlternate: true,
llmsTxt: { exclude: ["index"] },
}),
starlightLinksValidator(),
],
description:
"Documentation for Tangled — decentralized code collaboration built on the AT Protocol.",
social: [
{ icon: "x.com", label: "X", href: "https://x.com/tangled_org" },
{
icon: "blueSky",
label: "Bluesky",
href: "https://bsky.app/profile/tangled.org",
},
],
favicon: "/favicon.svg",
head: [
{
tag: "link",
attrs: {
rel: "preload",
href: `${base}/fonts/TimelessSansVF.woff2`,
as: "font",
type: "font/woff2",
crossorigin: "anonymous",
},
},
{
tag: "link",
// head entries are emitted verbatim; starlight only rebases `favicon`
attrs: { rel: "apple-touch-icon", href: "/docs/apple-touch-icon.png" },
},
],
editLink: {
baseUrl: "https://tangled.org/@tangled.org/core/blob/master/docs/",
},
customCss: [
"./src/styles/global.css",
"./src/styles/tangled.css",
"./src/styles/custom.css",
"./src/styles/diagrams.css",
],
components: {
Header: "./src/components/Header.astro",
Hero: "./src/components/Hero.astro",
MarkdownContent: "./src/components/MarkdownContent.astro",
MobileMenuFooter: "./src/components/MobileMenuFooter.astro",
PageTitle: "./src/components/PageTitle.astro",
Sidebar: "./src/components/Sidebar.astro",
SiteTitle: "./src/components/SiteTitle.astro",
ThemeProvider: "./src/components/ThemeProvider.astro",
},
expressiveCode: {
// same palettes as the web app's syntax tokens
themes: ["catppuccin-macchiato", "catppuccin-latte"],
// a plain bordered block like tangled.org's, no editor or terminal chrome
defaultProps: { frame: "none" },
styleOverrides: {
// expressive-code adds borderWidth to this for the outer
// radius, so subtract it to land on --radius-md exactly
borderRadius: "calc(var(--radius-md) - 1px)",
borderColor: "var(--color-border-default)",
borderWidth: "1px",
codeBackground: "var(--color-background-canvas)",
codeFontFamily: "var(--font-mono)",
codeFontSize: "0.8125rem",
codeLineHeight: "1.5385",
codePaddingBlock: "0.75rem",
codePaddingInline: "0.75rem",
frames: {
shadowColor: "transparent",
frameBoxShadowCssValue: "none",
},
},
},
sidebar: [
{
label: "Getting started",
items: [
"introduction",
"quick-start",
"comparison-with-github",
"migrating-from-github",
"why-atproto",
],
},
{
label: "Using Tangled",
items: [
"guides/profile",
"guides/repositories",
"guides/pull-requests",
"guides/access",
"guides/vouching",
"guides/notifications",
"guides/organizations",
"guides/signing-commits",
"guides/importing",
"sites",
"troubleshooting",
],
},
{
label: "Repositories",
items: ["knots/repositories", "knots/self-hosting"],
},
{
label: "Running CI",
items: [
"spindles",
"spindles/writing-workflows",
"spindles/workflows",
"spindles/secrets",
"spindles/webhooks",
"spindles/self-hosting",
],
},
{
label: "Self-hosting",
items: [
"self-hosting",
"self-hosting/knot",
"self-hosting/spindle",
"self-hosting/frontend",
"self-hosting/indexer",
],
},
{
label: "Integrations",
items: ["integrations/discord", "integrations/contributing"],
},
{
label: "Upgrading",
items: ["upgrading/knots-and-spindles", "upgrading/knot-2"],
},
{
label: "API",
items: ["api/knot", "api/spindle", "api/bobbin"],
},
{
label: "Reference",
items: [
"reference/records",
"reference/identifiers",
"reference/knot-config",
"reference/spindle-config",
],
},
{
label: "Contributing",
items: ["contributing/hacking", "contributing/guide"],
},
],
}),
sitemap(),
svelte(),
],
vite: {
plugins: [svelteStyleFallback, tailwindcss(), Icons({ compiler: "svelte" }), fontsBase(base)],
resolve: {
// same aliases the web app's components import through
alias: {
$lib: webLib,
// the web app's own imports (sveltekit 3 subpath imports)
"#lib": webLib,
"$icon/": "~icons/lucide/",
// routing and environment are sveltekit-only; components that build hrefs or
// guard browser apis import them
"$app/paths": fileURLToPath(new URL("./src/stubs/app-paths.ts", import.meta.url)),
"$app/env": fileURLToPath(new URL("./src/stubs/app-env.ts", import.meta.url)),
},
// web/ has no node_modules in the nix build: resolve the components'
// runtime deps (and svelte itself, which must be a single copy) from here
dedupe: ["svelte", "tailwind-variants", "tailwind-merge"],
},
server: {
fs: {
allow: [
fileURLToPath(new URL(".", import.meta.url)),
fileURLToPath(new URL("../web/src", import.meta.url)),
],
},
},
},
});