import { defineConfig } from "blume"; interface ThemeMetadata { value: string; color_scheme: "dark" | "light"; preview: { darker: string; dark: string; primary: string; content: string; }; docs: { muted: string; muted_foreground: string; border: string; accent_foreground: string; code_background: string; }; } interface BunRuntime { file(path: URL): { text(): Promise }; write(path: URL, content: string): Promise; YAML: { parse(source: string): unknown }; } const bun = (globalThis as typeof globalThis & { Bun: BunRuntime }).Bun; const themes = bun.YAML.parse( await bun.file(new URL("./config/themes.yml", import.meta.url)).text(), ) as ThemeMetadata[]; const defaultTheme = themes.find((theme) => theme.value === "neon"); if (!defaultTheme) throw new Error("The default Neon theme is missing"); const docsThemes = Object.fromEntries( themes.map((theme) => [ theme.value, { colorScheme: theme.color_scheme, tokens: { "--blume-background": theme.preview.darker, "--blume-foreground": theme.preview.content, "--blume-muted": theme.docs.muted, "--blume-muted-foreground": theme.docs.muted_foreground, "--blume-border": theme.docs.border, "--blume-accent": theme.preview.primary, "--blume-accent-foreground": theme.docs.accent_foreground, "--blume-code-background": theme.docs.code_background, }, }, ]), ); // The colours are plain CSS keyed off data-hackatime-theme so the cascade, // not a script, paints them. :root[data-theme][data-hackatime-theme="x"] is // one attribute more specific than Blume's :root[data-theme="dark"] token // block, so these rules win in both colour schemes regardless of sheet order. const themeCss = themes .map((theme) => { const declarations = Object.entries(docsThemes[theme.value].tokens) .map(([property, value]) => ` ${property}: ${value};`) .join("\n"); return [ `:root[data-hackatime-theme="${theme.value}"],`, `:root[data-theme][data-hackatime-theme="${theme.value}"] {`, declarations, "}", ].join("\n"); }) .join("\n\n"); // Pages can ship a screenshot per colour scheme by marking each with // data-docs-theme="light" or "dark"; only the one matching data-theme shows. const themedImageCss = [ `:root[data-theme="dark"] [data-docs-theme="light"],`, `:root:not([data-theme="dark"]) [data-docs-theme="dark"] {`, " display: none;", "}", ].join("\n"); const themeCssUrl = new URL("./tmp/hackatime-docs-themes.css", import.meta.url); await bun.write( themeCssUrl, `/* Generated by blume.config.ts from config/themes.yml. Do not edit. */\n\n${themeCss}\n\n${themedImageCss}\n`, ); // The script only picks the theme from the cookie and stamps attributes; the // same pattern as Blume's own dark-mode init, re-applied on astro:after-swap // because the client router swaps in a fresh before the new page paints. const themeScript = `(() => { const schemes = ${JSON.stringify( Object.fromEntries( themes.map((theme) => [theme.value, theme.color_scheme]), ), )}; const apply = () => { const cookie = document.cookie .split("; ") .find((entry) => entry.startsWith("hackatime_theme=")); const selected = cookie ? cookie.slice("hackatime_theme=".length) : "neon"; const theme = Object.hasOwn(schemes, selected) ? selected : "neon"; document.documentElement.dataset.hackatimeTheme = theme; document.documentElement.dataset.theme = schemes[theme]; try { localStorage.setItem("blume-theme", schemes[theme]); } catch {} }; apply(); document.addEventListener("astro:after-swap", apply); })();`; export default defineConfig({ title: "Hackatime Docs", description: "Learn how to set up Hackatime, track your coding time, and build integrations.", basePath: "/docs", logo: { image: "/images/new-icon-rounded.png", text: "Hackatime", href: "/", }, github: { owner: "hackclub", repo: "hackatime", }, navigation: { sidebar: { display: "page", }, }, integrations: [ { name: "hackatime-docs-theme", hooks: { "astro:config:setup": ({ injectScript }) => { injectScript("head-inline", themeScript); injectScript("page-ssr", `import ${JSON.stringify(themeCssUrl.pathname)};`); }, }, }, ], theme: { accent: defaultTheme.preview.primary, background: defaultTheme.preview.dark, mode: defaultTheme.color_scheme, radius: "lg", }, deployment: { output: "static", site: "https://hackatime.hackclub.com", }, redirects: [ { from: "/getting-started/configuration", to: "/configuration", status: 301, }, ], analytics: { scripts: [ { content: `window.addEventListener("blume:track", (event) => { if (event.detail?.event !== "feedback") return; let visitorToken = localStorage.getItem("hackatime-docs-visitor"); if (!visitorToken) { visitorToken = crypto.randomUUID(); localStorage.setItem("hackatime-docs-visitor", visitorToken); } fetch("/docs/feedback", { method: "POST", credentials: "same-origin", keepalive: true, headers: { "Content-Type": "application/json" }, body: JSON.stringify({ helpful: event.detail.props.helpful === "yes", path: event.detail.props.path, title: event.detail.props.title, visitor_token: visitorToken, }), }).catch(() => {}); });`, }, ], }, seo: { og: { enabled: true, logo: "/images/hackatime-icon.svg", palette: { accent: defaultTheme.preview.primary, background: defaultTheme.preview.darker, foreground: defaultTheme.preview.content, muted: defaultTheme.docs.muted_foreground, border: defaultTheme.docs.border, }, }, rss: { enabled: false }, sitemap: true, robots: false, structuredData: true, x: { handle: "@hackclub" }, }, });