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.
152 lines
5.7 KiB
JavaScript
152 lines
5.7 KiB
JavaScript
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 don’t 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"],
|
||
},
|
||
},
|
||
}
|
||
);
|