diff --git a/apps/site/package.json b/apps/site/package.json index 443fd2c..b27fb8c 100644 --- a/apps/site/package.json +++ b/apps/site/package.json @@ -16,16 +16,21 @@ "dependencies": { "@astrojs/rss": "^4.0.19", "@astrojs/sitemap": "^3.7.3", + "@fontsource-variable/fraunces": "^5.3.0", + "@fontsource-variable/newsreader": "^5.3.0", + "@fontsource/jetbrains-mono": "^5.3.0", "astro": "^7.2.9", "marked": "^18.0.11", "pagefind": "^1.5.2", - "smol-toml": "^1.8.0" + "smol-toml": "^1.8.0", + "three": "^0.185.1" }, "allowScripts": { "esbuild": true }, "devDependencies": { "@astrojs/check": "^0.9.10", + "@types/three": "^0.185.4", "typescript": "^5.9.3" } } diff --git a/apps/site/src/content.config.ts b/apps/site/src/content.config.ts index d85d392..581a56a 100644 --- a/apps/site/src/content.config.ts +++ b/apps/site/src/content.config.ts @@ -18,6 +18,8 @@ const writings = defineCollection({ updated: z.coerce.date().optional(), draft: z.boolean().default(false), canonical_url: z.string().optional(), + wordCount: z.number().optional(), + readingMinutes: z.number().optional(), taxonomies, extra: z .object({ diff --git a/apps/site/src/layouts/Base.astro b/apps/site/src/layouts/Base.astro index 86fba5e..3ce2f34 100644 --- a/apps/site/src/layouts/Base.astro +++ b/apps/site/src/layouts/Base.astro @@ -1,4 +1,8 @@ --- +import "@fontsource-variable/fraunces"; +import "@fontsource-variable/newsreader"; +import "@fontsource/jetbrains-mono"; +import "../styles/global.css"; import { siteConfig } from "../site.config"; interface Props { @@ -7,66 +11,145 @@ interface Props { } const { title, description } = Astro.props; const metaDescription = description ?? siteConfig.description; +const currentPath = Astro.url.pathname; --- - {title} — andwati.com + {title} — {siteConfig.title} + - -
- -
+
+
+ {siteConfig.author.toUpperCase()} + +
+
+ +
+ +
+ diff --git a/apps/site/src/lib/toml-content-loader.ts b/apps/site/src/lib/toml-content-loader.ts index 66c4f71..39966a7 100644 --- a/apps/site/src/lib/toml-content-loader.ts +++ b/apps/site/src/lib/toml-content-loader.ts @@ -19,6 +19,15 @@ function slugify(text: string): string { .replace(/^-+|-+$/g, ""); } +/** Strips inline markdown syntax for use as plain-text TOC labels/slugs. */ +function stripInlineMarkdown(text: string): string { + return text + .replace(/\*\*(.*?)\*\*/g, "$1") + .replace(/\*(.*?)\*/g, "$1") + .replace(/`(.*?)`/g, "$1") + .replace(/\[(.*?)\]\([^)]*\)/g, "$1"); +} + // Collects headings for the file currently being parsed (reset before each // `marked.parse()` call below) so Astro's `render(entry)` can expose them // as `headings`, matching the shape a built-in markdown loader would give. @@ -27,9 +36,11 @@ let currentHeadings: Heading[] = []; const marked = new Marked({ renderer: { heading(token) { - const slug = slugify(token.text); - currentHeadings.push({ depth: token.depth, slug, text: token.text }); - return `${token.text}`; + const plainText = stripInlineMarkdown(token.text); + const slug = slugify(plainText); + const html = this.parser.parseInline(token.tokens); + currentHeadings.push({ depth: token.depth, slug, text: plainText }); + return `${html}`; }, }, }); @@ -80,10 +91,13 @@ export function tomlContentLoader(contentDir: string): Loader { const relPath = relative(dirUrl.pathname, file); const id = relPath.replace(/\/index\.md$/, "").replace(/\.md$/, ""); currentHeadings = []; - const html = await marked.parse(body.trim()); + const trimmedBody = body.trim(); + const html = await marked.parse(trimmedBody); + const wordCount = trimmedBody.split(/\s+/).filter(Boolean).length; + const readingMinutes = Math.max(1, Math.round(wordCount / 200)); store.set({ id, - data, + data: { ...data, wordCount, readingMinutes }, rendered: { html, metadata: { headings: currentHeadings } }, }); } diff --git a/apps/site/src/pages/writings/[...slug].astro b/apps/site/src/pages/writings/[...slug].astro index f0d1d42..bd3c205 100644 --- a/apps/site/src/pages/writings/[...slug].astro +++ b/apps/site/src/pages/writings/[...slug].astro @@ -55,6 +55,7 @@ const tocHeadings = headings.filter((h) => h.depth <= 3);

{entry.data.title}

{entry.data.date.toDateString()} + {entry.data.readingMinutes && <> · {entry.data.readingMinutes} min read} {entry.data.updated && <> · updated {entry.data.updated.toDateString()}}

{tags.map((tag) => #{tag})} diff --git a/apps/site/src/pages/writings/index.astro b/apps/site/src/pages/writings/index.astro index 426e9d5..f68f3ad 100644 --- a/apps/site/src/pages/writings/index.astro +++ b/apps/site/src/pages/writings/index.astro @@ -13,7 +13,10 @@ const writings = ( writings.map((entry) => (

{entry.data.title}

-

{entry.data.date.toDateString()}

+

+ {entry.data.date.toDateString()} + {entry.data.readingMinutes && <> · {entry.data.readingMinutes} min read} +

{entry.data.description &&

{entry.data.description}

} {entry.data.taxonomies?.tags.map((tag) => #{tag})}
diff --git a/apps/site/src/site.config.ts b/apps/site/src/site.config.ts index d594e91..93e06f1 100644 --- a/apps/site/src/site.config.ts +++ b/apps/site/src/site.config.ts @@ -16,6 +16,12 @@ export const siteConfig = { { name: "Bookshelf", url: "/bookshelf/" }, { name: "Blogs", url: "/blogs/" }, ], + social: { + github: "https://github.com/andwati", + twitter: "https://twitter.com/andwati_", + youtube: "https://www.youtube.com/@pwnforfunandprofit", + email: "andwatiian@gmail.com", + }, tagDescriptions: { linux: "Practical notes on running Arch-based Linux distributions, from WiFi hotspot quirks to database setup.", diff --git a/apps/site/src/styles/global.css b/apps/site/src/styles/global.css new file mode 100644 index 0000000..bba2b27 --- /dev/null +++ b/apps/site/src/styles/global.css @@ -0,0 +1,177 @@ +@import "./tokens.css"; + +*, +*::before, +*::after { + box-sizing: border-box; +} + +html { + -webkit-text-size-adjust: 100%; +} + +body { + margin: 0; + background: var(--bg); + color: var(--text); + font-family: var(--font-body); + font-size: var(--font-size-base); + line-height: 1.65; + transition: + background-color 0.3s var(--ease-out), + color 0.3s var(--ease-out); +} + +::selection { + background: var(--accent-muted); + color: var(--text); +} + +a { + color: inherit; +} + +img, +svg { + max-width: 100%; + display: block; +} + +h1, +h2, +h3 { + font-family: var(--font-display); + font-weight: 600; + font-style: italic; + line-height: 1.15; + letter-spacing: -0.01em; + margin: 0 0 var(--space-4); +} + +h1 { + font-size: var(--font-size-3xl); + display: inline-block; + position: relative; +} + +h1::after { + content: ""; + position: absolute; + left: 0.05em; + right: 0.4em; + bottom: 0.08em; + height: 0.09em; + background: var(--accent); + opacity: 0.55; + z-index: -1; + border-radius: 1px; +} + +h2 { + font-size: var(--font-size-2xl); +} + +h3 { + font-size: var(--font-size-xl); +} + +p { + margin: 0 0 var(--space-5); +} + +code, +pre { + font-family: var(--font-mono); + font-size: 0.9em; +} + +pre { + padding: var(--space-4); + overflow-x: auto; + background: var(--bg-elevated); + border-radius: var(--radius-md); + border: 1px solid var(--border); +} + +.container { + max-width: 42rem; + margin-inline: auto; + padding-inline: var(--space-5); +} + +.eyebrow { + font-family: var(--font-mono); + font-size: var(--font-size-sm); + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--text-muted); +} + +.meta { + font-family: var(--font-mono); + font-size: var(--font-size-sm); + color: var(--text-muted); +} + +.tag { + display: inline-block; + font-family: var(--font-mono); + font-size: var(--font-size-sm); + color: var(--text-muted); + margin-right: var(--space-3); + text-decoration: none; +} + +.tag:hover { + color: var(--accent); +} + +.card { + padding-block: var(--space-5); + border-bottom: 1px solid var(--border); +} + +.card h3 { + margin-bottom: var(--space-1); +} + +.card h3 a { + text-decoration: none; +} + +.card h3 a:hover { + color: var(--accent); +} + +.button { + display: inline-flex; + align-items: center; + gap: var(--space-2); + font-family: var(--font-mono); + font-size: var(--font-size-sm); + letter-spacing: 0.04em; + text-decoration: none; + color: var(--text); + border: 1px solid var(--border); + border-radius: var(--radius-sm); + padding: var(--space-2) var(--space-4); + transition: + border-color 0.2s var(--ease-out), + color 0.2s var(--ease-out); +} + +.button:hover { + border-color: var(--accent); + color: var(--accent); +} + +@media (prefers-reduced-motion: reduce) { + *, + *::before, + *::after { + animation-duration: 0.001ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.001ms !important; + scroll-behavior: auto !important; + } +} diff --git a/apps/site/src/styles/tokens.css b/apps/site/src/styles/tokens.css new file mode 100644 index 0000000..dfa1b67 --- /dev/null +++ b/apps/site/src/styles/tokens.css @@ -0,0 +1,66 @@ +/** + * Design tokens. Dark-first (matches the reflective, slightly technical + * tone of the content), with a warm paper-toned light mode — not pure + * black/white either way, so the palette reads considered rather than + * default browser colors. + */ +:root { + --bg: #faf7f1; + --bg-elevated: #f1ece0; + --text: #1c1a15; + --text-muted: #6f6a5c; + --border: rgba(28, 26, 21, 0.12); + --accent: #d1502f; + --accent-muted: rgba(209, 80, 47, 0.12); + + --font-display: "Fraunces Variable", ui-serif, Georgia, serif; + --font-body: "Newsreader Variable", ui-serif, Georgia, serif; + --font-mono: "JetBrains Mono", ui-monospace, "SF Mono", monospace; + + --font-size-sm: clamp(0.8rem, 0.77rem + 0.15vw, 0.875rem); + --font-size-base: clamp(1rem, 0.96rem + 0.2vw, 1.0625rem); + --font-size-lg: clamp(1.125rem, 1.05rem + 0.35vw, 1.25rem); + --font-size-xl: clamp(1.375rem, 1.2rem + 0.8vw, 1.75rem); + --font-size-2xl: clamp(1.75rem, 1.4rem + 1.6vw, 2.5rem); + --font-size-3xl: clamp(2.25rem, 1.7rem + 2.6vw, 3.5rem); + + --space-1: 0.25rem; + --space-2: 0.5rem; + --space-3: 0.75rem; + --space-4: 1rem; + --space-5: 1.5rem; + --space-6: 2rem; + --space-7: 3rem; + --space-8: 4.5rem; + + --radius-sm: 4px; + --radius-md: 10px; + + --ease-out: cubic-bezier(0.16, 1, 0.3, 1); + + color-scheme: light; +} + +@media (prefers-color-scheme: dark) { + :root:not([data-theme="light"]) { + --bg: #0d0d0b; + --bg-elevated: #171613; + --text: #ece7db; + --text-muted: #8e897b; + --border: rgba(236, 231, 219, 0.12); + --accent: #ff6b45; + --accent-muted: rgba(255, 107, 69, 0.15); + color-scheme: dark; + } +} + +:root[data-theme="dark"] { + --bg: #0d0d0b; + --bg-elevated: #171613; + --text: #ece7db; + --text-muted: #8e897b; + --border: rgba(236, 231, 219, 0.12); + --accent: #ff6b45; + --accent-muted: rgba(255, 107, 69, 0.15); + color-scheme: dark; +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4df8fa4..31910ab 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -78,6 +78,15 @@ importers: '@astrojs/sitemap': specifier: ^3.7.3 version: 3.7.3 + '@fontsource-variable/fraunces': + specifier: ^5.3.0 + version: 5.3.0 + '@fontsource-variable/newsreader': + specifier: ^5.3.0 + version: 5.3.0 + '@fontsource/jetbrains-mono': + specifier: ^5.3.0 + version: 5.3.0 astro: specifier: ^7.2.9 version: 7.2.9(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.3)(@types/node@20.19.43)(terser@5.51.2)(yaml@2.9.0) @@ -90,10 +99,16 @@ importers: smol-toml: specifier: ^1.8.0 version: 1.8.0 + three: + specifier: ^0.185.1 + version: 0.185.1 devDependencies: '@astrojs/check': specifier: ^0.9.10 version: 0.9.10(prettier@3.9.6)(typescript@5.9.3) + '@types/three': + specifier: ^0.185.4 + version: 0.185.4 typescript: specifier: ^5.9.3 version: 5.9.3 @@ -581,6 +596,9 @@ packages: '@dabh/diagnostics@2.0.8': resolution: {integrity: sha512-R4MSXTVnuMzGD7bzHdW2ZhhdPC/igELENcq5IjEverBvq5hn1SXCWcsi6eSsdWP0/Ur+SItRRjAktmdoX/8R/Q==} + '@dimforge/rapier3d-compat@0.12.0': + resolution: {integrity: sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow==} + '@discoveryjs/json-ext@0.6.3': resolution: {integrity: sha512-4B4OijXeVNOPZlYA2oEwWOTkzyltLao+xbotHQeqN++Rv27Y6s818+n2Qkp8q+Fxhn0t/5lA5X1Mxktud8eayQ==} engines: {node: '>=14.17.0'} @@ -999,6 +1017,15 @@ packages: '@floating-ui/utils@0.2.12': resolution: {integrity: sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==} + '@fontsource-variable/fraunces@5.3.0': + resolution: {integrity: sha512-9BYGySn4AHEJdgp9Z28tQ3X+laJMEOITXkQarZXeloWQZDq5oOvXJ3kDA8c7MGIfpogIaZfjrQBqmda8POOCKA==} + + '@fontsource-variable/newsreader@5.3.0': + resolution: {integrity: sha512-rrzYi43qMpbzwuFtf9OkWH8sxAPVPcQQQEwXpPtwaKYeJ8yVg5aLs5kawmo1f2Q1t1M38TLmEKCkGVDsYwgdFw==} + + '@fontsource/jetbrains-mono@5.3.0': + resolution: {integrity: sha512-fqDfB5I9f1p1TV486aUgB9t8zP84P0O1FtQR5Ol9vjwPy+S+EIGlVYm1cvj2W5shcZMTg2nZFdVMoH5wFu8a1A==} + '@formatjs/ecma402-abstract@1.18.2': resolution: {integrity: sha512-+QoPW4csYALsQIl8GbN14igZzDbuwzcpWrku9nyMXlaqAlwRBgl5V+p0vWMGFqHOw37czNXaP/lEk4wbLgcmtA==} @@ -2940,6 +2967,9 @@ packages: '@tokenizer/token@0.3.0': resolution: {integrity: sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==} + '@tweenjs/tween.js@23.1.3': + resolution: {integrity: sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==} + '@tybys/wasm-util@0.10.3': resolution: {integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==} @@ -3102,6 +3132,12 @@ packages: '@types/serve-static@2.2.0': resolution: {integrity: sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==} + '@types/stats.js@0.17.4': + resolution: {integrity: sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA==} + + '@types/three@0.185.4': + resolution: {integrity: sha512-gAsBIC07NIFrxjbf7tH2t71c38uulFfk/RFoC7FNBSjMRAQ8J1x/RBvusX0N5PJouaYFJawXQqfCQ0RKUx/1nA==} + '@types/through@0.0.33': resolution: {integrity: sha512-HsJ+z3QuETzP3cswwtzt2vEIiHBk/dCcHGhbmG5X3ecnwFD/lPrMpliGXxSCg03L9AhrdwA4Oz/qfspkDW+xGQ==} @@ -3120,6 +3156,9 @@ packages: '@types/use-sync-external-store@0.0.3': resolution: {integrity: sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==} + '@types/webxr@0.5.24': + resolution: {integrity: sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg==} + '@ucast/core@1.10.2': resolution: {integrity: sha512-ons5CwXZ/51wrUPfoduC+cO7AS1/wRb0ybpQJ9RrssossDxVy4t49QxWoWgfBDvVKsz9VXzBk9z0wqTdZ+Cq8g==} @@ -4377,6 +4416,9 @@ packages: fflate@0.7.5: resolution: {integrity: sha512-QieYf//cis6ywHNi5qW1+PXPQ4bC+XVJAtS4AXIML8P76GroEiOxm/oQtn1f02UkJY1+KsXMJcC+R2v/Eg4G3g==} + fflate@0.8.3: + resolution: {integrity: sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==} + file-selector@2.1.2: resolution: {integrity: sha512-QgXo+mXTe8ljeqUFaX3QVHc5osSItJ/Km+xpocx0aSqWGMSCf6qYs/VnzZgS864Pjn5iceMRFigeAV7AfTlaig==} engines: {node: '>= 12'} @@ -5550,6 +5592,9 @@ packages: merge-stream@2.0.0: resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} + meshoptimizer@1.1.1: + resolution: {integrity: sha512-oRFNWJRDA/WTrVj7NWvqa5HqE1t9MYDj2VaWirQCzCCrAd2GHrqR/sQezCxiWATPNlKTcRaPRHPJwIRoPBAp5g==} + methods@1.1.2: resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} engines: {node: '>= 0.6'} @@ -7050,6 +7095,9 @@ packages: thenify@3.3.1: resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + three@0.185.1: + resolution: {integrity: sha512-5aojFCXKwnjBRZvUnt3WFfEcvUJgkN5LlijRFN95hMy8WVkG4I0QNcJE+OuWvuJ0bOdStrbfXn0pkd6/QyiAlg==} + throttleit@2.1.0: resolution: {integrity: sha512-nt6AMGKW1p/70DF/hGBdJB57B8Tspmbp5gfJ8ilhLnt7kkr2ye7hzD6NVG8GGErk2HWF34igrL2CXmNIkzKqKw==} engines: {node: '>=18'} @@ -8425,6 +8473,8 @@ snapshots: enabled: 2.0.0 kuler: 2.0.0 + '@dimforge/rapier3d-compat@0.12.0': {} + '@discoveryjs/json-ext@0.6.3': {} '@dnd-kit/accessibility@3.1.1(react@18.3.1)': @@ -8735,6 +8785,12 @@ snapshots: '@floating-ui/utils@0.2.12': {} + '@fontsource-variable/fraunces@5.3.0': {} + + '@fontsource-variable/newsreader@5.3.0': {} + + '@fontsource/jetbrains-mono@5.3.0': {} + '@formatjs/ecma402-abstract@1.18.2': dependencies: '@formatjs/intl-localematcher': 0.5.4 @@ -11424,6 +11480,8 @@ snapshots: '@tokenizer/token@0.3.0': {} + '@tweenjs/tween.js@23.1.3': {} + '@tybys/wasm-util@0.10.3': dependencies: tslib: 2.8.1 @@ -11627,6 +11685,17 @@ snapshots: '@types/http-errors': 2.0.5 '@types/node': 20.19.43 + '@types/stats.js@0.17.4': {} + + '@types/three@0.185.4': + dependencies: + '@dimforge/rapier3d-compat': 0.12.0 + '@tweenjs/tween.js': 23.1.3 + '@types/stats.js': 0.17.4 + '@types/webxr': 0.5.24 + fflate: 0.8.3 + meshoptimizer: 1.1.1 + '@types/through@0.0.33': dependencies: '@types/node': 20.19.43 @@ -11642,6 +11711,8 @@ snapshots: '@types/use-sync-external-store@0.0.3': {} + '@types/webxr@0.5.24': {} + '@ucast/core@1.10.2': {} '@ucast/js@3.1.0': @@ -13057,6 +13128,8 @@ snapshots: fflate@0.7.5: {} + fflate@0.8.3: {} + file-selector@2.1.2: dependencies: tslib: 2.8.1 @@ -14351,6 +14424,8 @@ snapshots: merge-stream@2.0.0: {} + meshoptimizer@1.1.1: {} + methods@1.1.2: {} micromark-core-commonmark@2.0.3: @@ -16070,6 +16145,8 @@ snapshots: dependencies: any-promise: 1.3.0 + three@0.185.1: {} + throttleit@2.1.0: {} through2@4.0.2: