Files
blog/astro.config.mjs
Gabriel Vidal e18bf4c0dd feat(design): ship the blueprint redesign as the site's only look
Picks "blueprint" out of the five beta propositions and makes it the site:
the design switcher, the other four stylesheets and the fonts only they used
are gone, and the token block moved to `html:root` (a bare `html` loses to
base.css's `:root` fallbacks no matter the order, which rendered everything in
the wrong palette).

Chrome changes:
- the header avatar is gone;
- the homepage drops "Recent Projects" and leads with posts;
- a post/project card is now a single anchor, so the whole block is the click
  target instead of just the title;
- nav marks the current section, sub-paths included (`/blog/<slug>` lights up
  "blog"), with three cues — amber, a leader tick and an underline — so it
  still reads where there is no hover;
- every off-site link opens in a new tab and carries a `↗` glyph. Components
  decide via `isExternal()`; posts and pages via a rehype pass that covers all
  three shapes this repo actually uses — markdown syntax, raw `<a>` in .mdx,
  and raw `<a>` in .md.

Motion, from @gabvdl/ui: headings and the hero tagline type in with
ProgressiveText, card lists and archive rows stagger in with ProgressiveList,
and any post with more than one figure gets a "play as story" control that
opens the image viewer in story mode. The islands render finished content on
the server and only animate after hydration (in a layout effect, so nothing
flashes), and TypedText reserves the final text box with a hidden ghost so a
wrapping tagline can't shove the page around while it types.

Mobile: header stacks, nav and archive rows wrap, covers crop to a band, the
title block folds to one column, prose tables and code scroll inside
themselves, and the graph paper tightens. Verified at 390px and 1280px.
2026-08-09 18:13:16 +02:00

152 lines
5.7 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import path, { dirname } from "path";
import { fileURLToPath } from "url";
import svelte from "@astrojs/svelte";
import tailwind from "@astrojs/tailwind";
import sitemap from "@astrojs/sitemap";
import mdx from "@astrojs/mdx";
import { defineConfig } from "astro/config";
import react from "@astrojs/react";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const SITE_URL = "https://blog.dev.gabvdl.xyz";
/** An absolute http(s) link pointing somewhere other than this site. */
function isExternalHref(href) {
if (typeof href !== "string" || !/^https?:\/\//i.test(href)) return false;
try {
return new URL(href).origin !== new URL(SITE_URL).origin;
} catch {
return false;
}
}
/**
* Rehype pass over every markdown/MDX page and post: any anchor that leaves the
* site opens in a new tab and is tagged `.external-link`, which paints the
* new-tab glyph (see `.external-link::after` in src/styles/base.css).
*
* Done at build time rather than with a client script so the behaviour is in
* the emitted HTML — it works with JS off and can't flash in after paint.
*
* Posts write links three different ways, and all three have to be covered:
* - markdown syntax, which reaches us as a hast `element`;
* - raw `<a>` HTML in a `.md` file, which is still an unparsed `raw` string at
* this point (Astro parses it later);
* - raw `<a>` HTML in a `.mdx` file, which MDX has already turned into a JSX
* element node rather than a hast one.
*/
function rehypeExternalLinks() {
const REL = "noopener noreferrer";
/** hast element — markdown-syntax links. */
const markElement = (node) => {
if (!isExternalHref(node.properties?.href)) return;
node.properties.target = "_blank";
node.properties.rel = ["noopener", "noreferrer"];
const existing = node.properties.className ?? [];
node.properties.className = [
...(Array.isArray(existing) ? existing : [existing]),
"external-link",
];
};
/** MDX JSX element — raw `<a>` written inside a .mdx file. */
const markMdxJsx = (node) => {
const attrs = (node.attributes ??= []);
const attr = (name) => attrs.find((a) => a.type === "mdxJsxAttribute" && a.name === name);
if (!isExternalHref(attr("href")?.value) || attr("target")) return;
attrs.push({ type: "mdxJsxAttribute", name: "target", value: "_blank" });
attrs.push({ type: "mdxJsxAttribute", name: "rel", value: REL });
const cls = attr("className") ?? attr("class");
if (cls && typeof cls.value === "string") cls.value = `${cls.value} external-link`;
else attrs.push({ type: "mdxJsxAttribute", name: "className", value: "external-link" });
};
/** Raw HTML string — `<a>` written inside a .md file. */
const markRaw = (node) => {
node.value = node.value.replace(/<a\b([^>]*)>/gi, (tag, attrs) => {
const href = /\bhref\s*=\s*["']([^"']*)["']/i.exec(attrs)?.[1];
if (!isExternalHref(href) || /\bclass\s*=\s*["'][^"']*external-link/i.test(attrs)) return tag;
const withClass = /\bclass\s*=\s*["']([^"']*)["']/i.test(attrs)
? attrs.replace(/\bclass\s*=\s*["']([^"']*)["']/i, (_m, c) => `class="${c} external-link"`)
: `${attrs} class="external-link"`;
// A post that already wrote `target` keeps it; only the missing bits are added.
const withTarget = /\btarget\s*=/i.test(withClass) ? withClass : `${withClass} target="_blank"`;
const withRel = /\brel\s*=/i.test(withTarget) ? withTarget : `${withTarget} rel="${REL}"`;
return `<a${withRel}>`;
});
};
const walk = (node) => {
if (node.type === "element" && node.tagName === "a") markElement(node);
else if (
(node.type === "mdxJsxTextElement" || node.type === "mdxJsxFlowElement") &&
node.name === "a"
)
markMdxJsx(node);
else if (node.type === "raw" && typeof node.value === "string") markRaw(node);
(node.children ?? []).forEach(walk);
};
return (tree) => walk(tree);
}
// Full Astro Configuration API Documentation:
// https://docs.astro.build/reference/configuration-reference
// @type-check enabled!
// VSCode and other TypeScript-enabled text editors will provide auto-completion,
// helpful tooltips, and warnings if your exported object is invalid.
// You can disable this by removing "@ts-check" and `@type` comments below.
// @ts-check
// https://astro.build/config
// https://astro.build/config
// https://astro.build/config
export default defineConfig(
/** @type {import('astro').AstroUserConfig} */ {
// root: '.', // Where to resolve all URLs relative to. Useful if you have a monorepo project.
// outDir: './dist', // When running `astro build`, path to final static output
// publicDir: './public', // A folder of static files Astro will copy to the root. Useful for favicons, images, and other files that dont need processing.
output: "static",
site: SITE_URL,
markdown: {
rehypePlugins: [rehypeExternalLinks],
},
// Your public domain, e.g.: https://my-site.dev/. Used to generate sitemaps and canonical URLs.
server: {
// port: 3000, // The port to run the dev server on.
},
integrations: [
mdx(),
svelte(),
tailwind({
config: {
applyBaseStyles: false,
},
}),
sitemap(),
react(),
],
vite: {
plugins: [],
resolve: {
alias: {
$: path.resolve(__dirname, "./src"),
},
},
optimizeDeps: {
allowNodeBuiltins: true,
},
ssr: {
// @gabvdl/ui (and its prismjs subpath imports) must be bundled by
// Vite: left external, Node's ESM resolver chokes on the package's
// extensionless `prismjs/components/*` imports during static builds.
noExternal: ["@gabvdl/ui", "prismjs"],
},
},
}
);