blume.config.ts
Rama main · réplica de lectura del repositorio original.
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<string> };
write(path: URL, content: string): Promise<number>;
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 <img> 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 <html> 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" },
},
});