diff --git a/.github/workflows/blog-deploy.yml b/.github/workflows/blog-deploy.yml new file mode 100644 index 00000000..5fd8e258 --- /dev/null +++ b/.github/workflows/blog-deploy.yml @@ -0,0 +1,62 @@ +name: Deploy blog.elimelt.com + +on: + push: + branches: ["main"] + paths: + - "blog/**" + - ".github/workflows/blog-deploy.yml" + - ".github/workflows/publish-site.yml" + pull_request: + branches: ["main"] + paths: + - "blog/**" + - ".github/workflows/blog-deploy.yml" + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: blog-deploy-${{ github.ref }} + cancel-in-progress: false + +jobs: + build: + name: Build blog + runs-on: ubuntu-latest + defaults: + run: + working-directory: blog + steps: + - name: Checkout + uses: actions/checkout@v5 + - name: Setup Node.js + uses: actions/setup-node@v5 + with: + node-version: "22" + cache: npm + cache-dependency-path: blog/package-lock.json + - name: Install dependencies + run: npm ci + - name: Check and build + run: npm run build + - name: Upload build + if: github.event_name != 'pull_request' + uses: actions/upload-artifact@v4 + with: + name: blog-site + path: blog/dist/ + if-no-files-found: error + retention-days: 7 + + publish: + if: github.event_name != 'pull_request' + needs: build + uses: ./.github/workflows/publish-site.yml + with: + site: blog + project: elimelt-blog + host: blog.elimelt.com + secrets: + CLOUDFLARE_PAGES_TOKEN: ${{ secrets.CLOUDFLARE_PAGES_TOKEN }} diff --git a/.github/workflows/publish-site.yml b/.github/workflows/publish-site.yml index 205a81e4..f9963f8b 100644 --- a/.github/workflows/publish-site.yml +++ b/.github/workflows/publish-site.yml @@ -1,13 +1,13 @@ name: Publish static site -# Called by web-deploy.yml and notes-deploy.yml after they upload a build -# artifact named "-site". Cloudflare Pages serves the result; nothing -# runs on the homelab. +# Called by web-deploy.yml, notes-deploy.yml and blog-deploy.yml after they +# upload a build artifact named "-site". Cloudflare Pages serves the +# result; nothing runs on the homelab. on: workflow_call: inputs: site: - description: "Build artifact prefix (web or notes)" + description: "Build artifact prefix (web, notes or blog)" required: true type: string project: diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9037de47..3e112c99 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -3,9 +3,10 @@ name: Run Tests on: pull_request: branches: ["main"] - # Notes PRs are validated by notes-deploy.yml. + # Notes and blog PRs are validated by their deploy workflows. paths-ignore: - "notes/**" + - "blog/**" schedule: # Run daily at 8:00 AM UTC diff --git a/.github/workflows/web-deploy.yml b/.github/workflows/web-deploy.yml index 05e77d5e..c2c1cc42 100644 --- a/.github/workflows/web-deploy.yml +++ b/.github/workflows/web-deploy.yml @@ -6,6 +6,7 @@ on: paths: - "**" - "!notes/**" + - "!blog/**" - "!docs/**" - "!infra/**" workflow_dispatch: diff --git a/README.md b/README.md index 42b8c5eb..ef395420 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Personal Site -This repository holds the public site (elimelt.com), the notes site in [`notes/`](notes/README.md) (notes.elimelt.com), and the homelab in [`infra/`](infra/README.md). The two sites build and deploy independently. +This repository holds the public site (elimelt.com), the notes site in [`notes/`](notes/README.md) (notes.elimelt.com), the blog in [`blog/`](blog/README.md) (blog.elimelt.com), and the homelab in [`infra/`](infra/README.md). The three sites build and deploy independently. The public frontend uses [Astro](https://astro.build) with TypeScript. Astro renders the portfolio and music pages to static HTML and bundles their interactive modules. The homelab homepage remains a separate TypeScript application. diff --git a/blog/.gitignore b/blog/.gitignore new file mode 100644 index 00000000..ddce69b6 --- /dev/null +++ b/blog/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +dist/ +.astro/ diff --git a/blog/README.md b/blog/README.md new file mode 100644 index 00000000..1cec4589 --- /dev/null +++ b/blog/README.md @@ -0,0 +1,56 @@ +# Blog + +blog.elimelt.com: Elijah's own writing. (notes.elimelt.com is reference +material, often generated or expanded with an LLM.) It's an +[Astro](https://astro.build) site with +[MDX](https://docs.astro.build/en/guides/integrations-guide/mdx/), built to +static files and served by Cloudflare Pages. It builds and deploys on its own +(`.github/workflows/blog-deploy.yml`). + +```bash +cd blog +npm ci +npm run dev # http://localhost:3001, drafts included +npm run build # type checks, then builds dist/ +``` + +## Writing a post + +Add `src/content/posts/.md` or `.mdx`; the file name becomes +`/posts//`. Frontmatter is checked against `src/content.config.ts`: + +```yaml +--- +title: Post title +description: One sentence for the index, feed and link previews. +date: 2026-10-25 +updated: 2026-11-02 # optional +draft: true # optional; shown in dev, left out of the build +--- +``` + +## Interactive elements + +Use `.mdx` and import components: + +```mdx +import Figure from '../../components/Figure.astro'; +import RandomWalk from '../../components/RandomWalk.astro'; + +
+ +
+``` + +`src/components/RandomWalk.astro` shows the pattern: markup plus a ` diff --git a/blog/src/content.config.ts b/blog/src/content.config.ts new file mode 100644 index 00000000..4059ea8a --- /dev/null +++ b/blog/src/content.config.ts @@ -0,0 +1,18 @@ +import { defineCollection } from 'astro:content'; +import { glob } from 'astro/loaders'; +import { z } from 'astro/zod'; + +const posts = defineCollection({ + // Each post is src/content/posts/.md or .mdx; the file name is the URL. + loader: glob({ base: './src/content/posts', pattern: '**/*.{md,mdx}' }), + schema: z.object({ + title: z.string(), + description: z.string(), + date: z.coerce.date(), + updated: z.coerce.date().optional(), + // Drafts render in `npm run dev` but are left out of the build. + draft: z.boolean().default(false), + }), +}); + +export const collections = { posts }; diff --git a/blog/src/content/posts/hello-world.mdx b/blog/src/content/posts/hello-world.mdx new file mode 100644 index 00000000..46fd7e9a --- /dev/null +++ b/blog/src/content/posts/hello-world.mdx @@ -0,0 +1,24 @@ +--- +title: Hello, world +description: What goes here versus in my notes, and a quick tour of what a post can do. +date: 2026-10-25 +--- + +import Figure from '../../components/Figure.astro'; +import RandomWalk from '../../components/RandomWalk.astro'; + +Everything on this blog is my own writing. My [notes](https://notes.elimelt.com) are different: they're reference material, and a lot of them are generated, rewritten, or expanded with an LLM. If you want to know what I actually think, read this. + +Posts are Markdown, so most of the time it's just text, `inline code`, and the occasional code block: + +```ts +const walk = (x: number, step: number) => x + (Math.random() - 0.5) * 2 * step; +``` + +When a post needs something you can poke at, it imports a component and drops it in: + +
+ +
+ +That one is a plain custom element with no framework, and its script only loads on pages that use it. diff --git a/blog/src/layouts/Base.astro b/blog/src/layouts/Base.astro new file mode 100644 index 00000000..d833f288 --- /dev/null +++ b/blog/src/layouts/Base.astro @@ -0,0 +1,53 @@ +--- +import '../styles/global.css'; + +interface Props { + title: string; + description: string; +} + +const { title, description } = Astro.props; +const canonicalUrl = new URL(Astro.url.pathname, Astro.site); +const socialImageUrl = new URL('/assets/apple-touch.png', Astro.site); +--- + + + + + + + {title} + + + + + + + + + + + + + + + + +
+ +
+ +
+ +
+ + diff --git a/blog/src/layouts/Post.astro b/blog/src/layouts/Post.astro new file mode 100644 index 00000000..1f19df1b --- /dev/null +++ b/blog/src/layouts/Post.astro @@ -0,0 +1,27 @@ +--- +import Base from './Base.astro'; +import { formatDate, type Post } from '../posts'; + +interface Props { + post: Post; +} + +const { post } = Astro.props; +const { title, description, date, updated } = post.data; +--- + + + +
+
+

{title}

+

+ + {updated && <> · updated } +

+
+
+ +
+
+ diff --git a/blog/src/pages/404.astro b/blog/src/pages/404.astro new file mode 100644 index 00000000..d4e69b3c --- /dev/null +++ b/blog/src/pages/404.astro @@ -0,0 +1,8 @@ +--- +import Base from '../layouts/Base.astro'; +--- + + +

Not found

+

There's nothing here. Back to all posts.

+ diff --git a/blog/src/pages/index.astro b/blog/src/pages/index.astro new file mode 100644 index 00000000..bf7a90b2 --- /dev/null +++ b/blog/src/pages/index.astro @@ -0,0 +1,51 @@ +--- +import Base from '../layouts/Base.astro'; +import { formatDate, getPosts } from '../posts'; + +const posts = await getPosts(); +--- + + +

Posts

+
    + {posts.map((post) => ( +
  • + {post.data.title} + {post.data.draft && (draft)} +

    + · {post.data.description} +

    +
  • + ))} +
+ + + diff --git a/blog/src/pages/posts/[...slug].astro b/blog/src/pages/posts/[...slug].astro new file mode 100644 index 00000000..f59d0b22 --- /dev/null +++ b/blog/src/pages/posts/[...slug].astro @@ -0,0 +1,17 @@ +--- +import { render } from 'astro:content'; +import Post from '../../layouts/Post.astro'; +import { getPosts } from '../../posts'; + +export async function getStaticPaths() { + const posts = await getPosts(); + return posts.map((post) => ({ params: { slug: post.id }, props: { post } })); +} + +const { post } = Astro.props; +const { Content } = await render(post); +--- + + + + diff --git a/blog/src/pages/rss.xml.ts b/blog/src/pages/rss.xml.ts new file mode 100644 index 00000000..39e0e2a2 --- /dev/null +++ b/blog/src/pages/rss.xml.ts @@ -0,0 +1,19 @@ +import rss from '@astrojs/rss'; +import type { APIContext } from 'astro'; +import { getPosts } from '../posts'; + +export async function GET(context: APIContext) { + const posts = await getPosts(); + return rss({ + title: "Elijah Melton's blog", + description: "Elijah Melton's own writing, on distributed systems, infrastructure, and whatever else.", + site: context.site!, + trailingSlash: true, + items: posts.map((post) => ({ + title: post.data.title, + description: post.data.description, + pubDate: post.data.date, + link: `/posts/${post.id}/`, + })), + }); +} diff --git a/blog/src/posts.ts b/blog/src/posts.ts new file mode 100644 index 00000000..5364fb28 --- /dev/null +++ b/blog/src/posts.ts @@ -0,0 +1,13 @@ +import { getCollection, type CollectionEntry } from 'astro:content'; + +export type Post = CollectionEntry<'posts'>; + +/** Published posts, newest first. Drafts are included only in dev. */ +export async function getPosts(): Promise { + const posts = await getCollection('posts', ({ data }) => import.meta.env.DEV || !data.draft); + return posts.sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf()); +} + +export function formatDate(date: Date): string { + return date.toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric', timeZone: 'UTC' }); +} diff --git a/blog/src/styles/global.css b/blog/src/styles/global.css new file mode 100644 index 00000000..9a29e068 --- /dev/null +++ b/blog/src/styles/global.css @@ -0,0 +1,200 @@ +@font-face { + font-family: 'Crimson Pro'; + font-style: normal; + font-weight: 400 600; + font-display: swap; + src: url('/assets/fonts/crimson-pro-latin.woff2') format('woff2'); +} + +:root { + --text: #1a1a1a; + --text-secondary: #666; + --bg: #ffffff; + --bg-secondary: #f5f5f5; + --accent: #0062ff; + --accent-hover: #5493fe; + --border: #d8d8d8; + + --font: -apple-system, system-ui, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; + --font-prose: 'Crimson Pro', Georgia, serif; + --font-mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; + + --measure: 42rem; + color-scheme: light dark; +} + +@media (prefers-color-scheme: dark) { + :root { + --text: #f1f1f1; + --text-secondary: #b3b3b3; + --bg: #000000; + --bg-secondary: #1a1a1a; + --accent: #00ff08; + --accent-hover: #00ff0886; + --border: #333; + } + + .astro-code, + .astro-code span { + color: var(--shiki-dark) !important; + background-color: var(--shiki-dark-bg) !important; + } +} + +*, +*::before, +*::after { + box-sizing: border-box; +} + +body { + margin: 0; + background: var(--bg); + color: var(--text); + font-family: var(--font); + line-height: 1.6; + -webkit-font-smoothing: antialiased; +} + +a { + color: var(--accent); + text-underline-offset: 0.15em; +} + +a:hover { + color: var(--accent-hover); +} + +.site { + max-width: var(--measure); + margin: 0 auto; + padding: clamp(1.5rem, 5vw, 4rem) 1rem 4rem; +} + +.site-header { + display: flex; + justify-content: space-between; + align-items: baseline; + gap: 1rem; + margin-bottom: 3rem; + font-size: 0.9375rem; +} + +.site-header a { + color: var(--text); + text-decoration: none; +} + +.site-title { + font-weight: 600; +} + +.site-nav { + display: flex; + gap: 1rem; +} + +.site-nav a { + color: var(--text-secondary); +} + +.site-footer { + margin-top: 4rem; + padding-top: 1.5rem; + border-top: 1px solid var(--border); + color: var(--text-secondary); + font-size: 0.875rem; +} + +.meta { + color: var(--text-secondary); + font-size: 0.875rem; +} + +/* Post body */ +.prose { + font-family: var(--font-prose); + font-size: 1.25rem; + line-height: 1.65; +} + +.prose h1, +.prose h2, +.prose h3 { + font-family: var(--font); + line-height: 1.25; + margin: 2.5rem 0 1rem; +} + +.prose h2 { + font-size: 1.375rem; +} + +.prose h3 { + font-size: 1.125rem; +} + +.prose img, +.prose video { + max-width: 100%; + height: auto; +} + +.prose blockquote { + margin: 1.5rem 0; + padding-left: 1rem; + border-left: 3px solid var(--border); + color: var(--text-secondary); +} + +.prose code { + font-family: var(--font-mono); + font-size: 0.8em; +} + +.prose :not(pre) > code { + padding: 0.1em 0.3em; + border-radius: 4px; + background: var(--bg-secondary); +} + +.prose pre { + padding: 1rem; + border-radius: 6px; + overflow-x: auto; + font-size: 1rem; + line-height: 1.5; +} + +.prose table { + width: 100%; + border-collapse: collapse; + font-family: var(--font); + font-size: 0.9375rem; +} + +.prose th, +.prose td { + padding: 0.4rem 0.6rem; + border-bottom: 1px solid var(--border); + text-align: left; +} + +/* Interactive components get a plain frame and may break out of the text measure. */ +.figure { + margin: 2rem 0; + font-family: var(--font); + font-size: 1rem; +} + +.figure-wide { + width: min(100vw - 2rem, 64rem); + margin-left: 50%; + transform: translateX(-50%); +} + +.figure figcaption { + margin-top: 0.5rem; + color: var(--text-secondary); + font-size: 0.875rem; +} diff --git a/blog/tsconfig.json b/blog/tsconfig.json new file mode 100644 index 00000000..8bf91d3b --- /dev/null +++ b/blog/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "astro/tsconfigs/strict", + "include": [".astro/types.d.ts", "**/*"], + "exclude": ["dist"] +} diff --git a/infra/sites/README.md b/infra/sites/README.md index d0ef2e82..fbde7972 100644 --- a/infra/sites/README.md +++ b/infra/sites/README.md @@ -1,12 +1,14 @@ # Static sites -elimelt.com and notes.elimelt.com are plain static files on Cloudflare Pages. +elimelt.com, notes.elimelt.com and blog.elimelt.com are plain static files on +Cloudflare Pages. Nothing they serve runs on the homelab. | Host | Source | Workflow | Pages project | | --- | --- | --- | --- | | elimelt.com (www redirects) | repository root (Astro) | `.github/workflows/web-deploy.yml` | `elimelt` | | notes.elimelt.com | `notes/` (Quartz) | `.github/workflows/notes-deploy.yml` | `elimelt-notes` | +| blog.elimelt.com | `blog/` (Astro + MDX) | `.github/workflows/blog-deploy.yml` | `elimelt-blog` | Each workflow builds on a GitHub-hosted runner and uploads the output as an artifact. `publish-site.yml` downloads it and runs `wrangler pages deploy` @@ -14,8 +16,8 @@ against the project, then checks that the host answers. Pages keeps every deployment and purges its own cache, so changes show up immediately. The projects, custom domains, DNS records, and the www redirect are in -`infra/terraform/main.tf`. Response headers live in `frontend/public/_headers` -and `notes/quartz-site/routes/_headers`. +`infra/terraform/main.tf`. Response headers live in `frontend/public/_headers`, +`notes/quartz-site/routes/_headers` and `blog/public/_headers`. ## Setup @@ -29,7 +31,7 @@ and `notes/quartz-site/routes/_headers`. ## Deploy and roll back Redeploy the current `main` with `gh workflow run web-deploy.yml` or -`gh workflow run notes-deploy.yml`. Roll back from the project's Deployments +`gh workflow run notes-deploy.yml` (or `blog-deploy.yml`). Roll back from the project's Deployments tab in the Cloudflare dashboard (Rollback to this deployment). ## Homelab (offline) diff --git a/infra/terraform/main.tf b/infra/terraform/main.tf index 53910b6c..e82fdf0f 100644 --- a/infra/terraform/main.tf +++ b/infra/terraform/main.tf @@ -141,7 +141,7 @@ resource "cloudflare_record" "transcribe" { # ============================================================================= # Static sites on Cloudflare Pages # ============================================================================= -# .github/workflows/{web,notes}-deploy.yml upload builds to these projects with +# .github/workflows/{web,notes,blog}-deploy.yml upload builds to these projects with # wrangler (direct upload, no Git integration). resource "cloudflare_pages_project" "web" { @@ -156,6 +156,12 @@ resource "cloudflare_pages_project" "notes" { production_branch = "main" } +resource "cloudflare_pages_project" "blog" { + account_id = var.cloudflare_account_id + name = "elimelt-blog" + production_branch = "main" +} + resource "cloudflare_pages_domain" "web" { account_id = var.cloudflare_account_id project_name = cloudflare_pages_project.web.name @@ -168,6 +174,12 @@ resource "cloudflare_pages_domain" "notes" { domain = "notes.${var.domain}" } +resource "cloudflare_pages_domain" "blog" { + account_id = var.cloudflare_account_id + project_name = cloudflare_pages_project.blog.name + domain = "blog.${var.domain}" +} + # Apex domain (elimelt.com); Cloudflare flattens the CNAME at the apex. resource "cloudflare_record" "apex" { zone_id = data.cloudflare_zone.main.id @@ -197,6 +209,15 @@ resource "cloudflare_record" "notes" { comment = "Quartz notes (Cloudflare Pages)" } +resource "cloudflare_record" "blog" { + zone_id = data.cloudflare_zone.main.id + name = "blog" + type = "CNAME" + content = cloudflare_pages_project.blog.subdomain + proxied = true + comment = "Astro blog (Cloudflare Pages)" +} + resource "cloudflare_ruleset" "www_redirect" { zone_id = data.cloudflare_zone.main.id name = "Redirects" diff --git a/notes/content/index.md b/notes/content/index.md index 2669497c..4b97b5ac 100644 --- a/notes/content/index.md +++ b/notes/content/index.md @@ -1,16 +1,18 @@ --- title: Elijah Melton's Technical Notes -description: Elijah Melton's notes on algorithms, machine learning, recommender systems, LLM serving, software, hardware, and mathematics. -updated: 2026-10-09 +description: Reference notes on algorithms, machine learning, systems, software, hardware, and mathematics, written mostly by language models and curated by Elijah Melton. +updated: 2026-10-25 authors: - elimelt - gpt-5.6-sol - gpt-6 + - claude-opus-5-5 --- -Technical notes by Elijah Melton on algorithms, machine learning, systems, -software, hardware, and mathematics. I use this site to work through proofs, -papers, and implementation details that I want to be able to find again. +Reference notes on algorithms, machine learning, systems, software, hardware, +and mathematics. Language models write most of the text from Elijah Melton's +notes, papers, and code. He picks what goes in and reviews some of the drafts. +Elijah's own writing is on his [blog](https://blog.elimelt.com). ## Selected notes @@ -31,35 +33,20 @@ papers, and implementation details that I want to be able to find again. - [[systems/index|Systems]] - [[reference/index|Reference material]] -## About these notes +## How these notes are written -While in most cases I'd prefer reading the prose/thoughts of a fellow human, -these notes aren't for my own or anyone else's enjoyment, nor am I trying to -connect with anyone through my writing here. To be honest, I'm not even a great -writer, and have limited time to refine my thoughts and produce well-organized -and thoroughly cited works of literature. +Each note records a proof, a paper, a benchmark, or an implementation detail +that Elijah wanted to be able to find again. A model drafts the note and does +most of the revising. Elijah chooses the topics and sources and reviews some of +the drafts. The rest go up as the model wrote them. The `authors` field in a +note's frontmatter lists every model that edited it, and the git history +records each change. -At this point, I'd give ~90% of the credit to whatever frontier model polished -and organized my thoughts for presentation here ("co-authorship" according to -the git history...), and would describe myself as more of a reviewer/curator of -these notes than the actual author. +Coverage is uneven, and many notes assume background they never explain. For a +first introduction to a subject, a textbook or a course is a better place to +start. -If you haven't built up a tolerance for AI slop, you might want to close this -tab now. If you're a student learning something for the first time and want a -rigorous and vetted introduction to a topic, I wouldn't look here. If your name -is Elijah Melton and you want a refresher on something you learned a half-decade -ago, then maybe these notes will be useful to you. +## Corrections -## What if it's wrong?!? - -LLMs are wrong all the time, but the amount they're wrong has quickly trended -downward, and even seems to approach never when frontier models are anchored to -the truth by documents without errors. In 2026, I wholeheartedly expect my own -writing to contain more mistakes than most frontier models. - -All that being said, yeah, there are probably a few errors here and there. I'd -expect the majority of them are caused either directly by my prompt, or by -errors in source documents I/the model cite. Still, any time I notice one, I -try to fix it. If you're the type of person that must right the wrong you -notice in the world, feel free to open a [PR](https://github.com/elimelt/web) -to correct it. +Notes can contain errors. They get fixed when someone finds them, and +corrections are welcome as a [pull request](https://github.com/elimelt/web). diff --git a/notes/quartz-site/custom.scss b/notes/quartz-site/custom.scss index 269503d5..9184fc31 100644 --- a/notes/quartz-site/custom.scss +++ b/notes/quartz-site/custom.scss @@ -7,18 +7,47 @@ --site-page-width: 1440px; --site-article-width: 48rem; --site-column-gap: 2rem; - --site-body-size: 20.5px; - --site-body-leading: 1.35; - --site-paragraph-gap: 2rem; - --site-title-size: 1.6rem; - --site-heading-size: 1.15rem; - --site-code-radius: 0.75rem; - --site-media-radius: 0.4rem; + --site-body-size: 17px; + --site-body-leading: 1.65; + --site-paragraph-gap: 1.1em; + --site-title-size: 2rem; + --site-heading-size: 1.3rem; + --site-code-radius: 6px; + --site-media-radius: 6px; +} + +// Links read as text with an underline instead of Quartz's highlighted chips. +a, +a.internal { + font-weight: inherit; + background-color: transparent; + padding: 0; + border-radius: 0; + text-decoration: underline; + text-decoration-color: color-mix(in srgb, currentColor 35%, transparent); + text-decoration-thickness: 1px; + text-underline-offset: 0.18em; +} + +a:hover, +a.internal:hover { + text-decoration-color: currentColor; +} + +// Navigation, tags and titles are links too, but underlines there are noise. +.page-title a, +.breadcrumb-element a, +.explorer a, +.toc a, +a.tag-link, +.search a, +.popover a.internal { + text-decoration: none; } .page-title a { - font-weight: 700; - letter-spacing: -0.02em; + font-weight: 600; + letter-spacing: -0.01em; } .page { @@ -40,9 +69,11 @@ .article-title { margin-top: 0; - margin-bottom: 0.35rem; - line-height: 1.1; + margin-bottom: 0.5rem; + line-height: 1.2; font-size: var(--site-title-size); + font-weight: 600; + letter-spacing: -0.015em; } .content-meta { @@ -84,14 +115,46 @@ article p { margin-block: var(--site-paragraph-gap); } +article :is(h2, h3, h4) { + font-weight: 600; + letter-spacing: -0.01em; + line-height: 1.3; +} + article > h2 { font-size: var(--site-heading-size); + margin-top: 2.5rem; + padding-top: 1.25rem; + border-top: 1px solid var(--lightgray); +} + +article > h3 { + font-size: 1.1rem; + margin-top: 2rem; +} + +article li + li { + margin-top: 0.3em; } article pre { border: 1px solid var(--lightgray); border-radius: var(--site-code-radius); padding: 1rem; + font-size: 0.85em; + line-height: 1.55; +} + +article :not(pre) > code { + font-size: 0.88em; + padding: 0.1em 0.3em; + border-radius: 4px; + background: color-mix(in srgb, var(--lightgray) 55%, transparent); +} + +article blockquote { + border-left: 3px solid var(--lightgray); + color: var(--darkgray); } article img, @@ -106,10 +169,21 @@ article table { display: block; max-width: 100%; overflow-x: auto; - border: 1px solid var(--lightgray); - border-radius: var(--site-code-radius); - padding: 0.25rem; - background: color-mix(in srgb, var(--lightgray) 20%, transparent); + border-collapse: collapse; + font-size: 0.92em; + line-height: 1.45; +} + +article :is(th, td) { + padding: 0.5rem 0.75rem; + border-bottom: 1px solid var(--lightgray); + text-align: left; + vertical-align: top; +} + +article th { + font-weight: 600; + border-bottom: 2px solid var(--darkgray); } .sidebar.right { diff --git a/notes/quartz-site/overrides/Head.tsx b/notes/quartz-site/overrides/Head.tsx index 1b13d170..e02db1f2 100644 --- a/notes/quartz-site/overrides/Head.tsx +++ b/notes/quartz-site/overrides/Head.tsx @@ -8,10 +8,10 @@ import { googleFontHref, googleFontSubsetHref } from "../util/theme" import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types" import { unescapeHTML } from "../util/escape" -// Google Fonts' Latin subset for the normal Noto Serif variable face used by +// Google Fonts' Latin subset for the normal IBM Plex Sans variable face used by // body copy and headings. Keep this synchronized with the build-time font // validation in scripts/patch_google_font_fetch.py. -const primaryFontPreload = "/static/fonts/ga6daw1J5X9T9RW6j9bNVls-hfgvz8JcMofYTYf6D30.woff2" +const primaryFontPreload = "/static/fonts/zYXzKVElMYYaJe8bpLHnCwDKr932-G7dytD-Dmu1syxeKYY.woff2" export function canonicalUrl(baseUrl: string, slug: FullSlug): string { const path = simplifySlug(slug) diff --git a/notes/quartz-site/routes/graph/graph.css b/notes/quartz-site/routes/graph/graph.css index 3c4d359e..1234d6b7 100644 --- a/notes/quartz-site/routes/graph/graph.css +++ b/notes/quartz-site/routes/graph/graph.css @@ -1,23 +1,23 @@ :root { color-scheme: light dark; - --paper: #f7f3eb; - --ink: #292823; - --muted: #6e6a61; - --line: rgb(73 83 76 / 18%); - --panel: rgb(247 243 235 / 86%); - --accent: #276755; - --border: #d8d1c5; + --paper: #ffffff; + --ink: #141413; + --muted: #6f6f6b; + --line: rgb(20 20 19 / 14%); + --panel: rgb(255 255 255 / 88%); + --accent: #1f5fbf; + --border: #e4e4e1; } @media (prefers-color-scheme: dark) { :root { - --paper: #191d1c; - --ink: #eff0e9; - --muted: #aeb4ae; - --line: rgb(198 214 205 / 16%); - --panel: rgb(25 29 28 / 88%); - --accent: #84cbb5; - --border: #39423e; + --paper: #121212; + --ink: #f3f3f3; + --muted: #9a9a9a; + --line: rgb(243 243 243 / 14%); + --panel: rgb(18 18 18 / 88%); + --accent: #8ab4ff; + --border: #2b2b2b; } } @@ -39,9 +39,7 @@ body { radial-gradient(circle at 15% 10%, color-mix(in srgb, var(--accent) 9%, transparent), transparent 35%), var(--paper); color: var(--ink); - font-family: "Noto Serif", serif; - font-optical-sizing: auto; - font-variation-settings: "wdth" 100; + font-family: "IBM Plex Sans", system-ui, sans-serif; } /* Let an embedding page supply the background in either color scheme. */ @@ -80,9 +78,9 @@ html.embedded body { h1 { margin: 0; - font-family: "Noto Serif", serif; + font-family: "IBM Plex Sans", system-ui, sans-serif; font-size: clamp(1.8rem, 5vw, 3.4rem); - font-weight: 500; + font-weight: 600; letter-spacing: -0.045em; line-height: 0.95; } diff --git a/notes/quartz-site/routes/graph/graph.js b/notes/quartz-site/routes/graph/graph.js index 8f91604b..562739e0 100644 --- a/notes/quartz-site/routes/graph/graph.js +++ b/notes/quartz-site/routes/graph/graph.js @@ -77,7 +77,7 @@ function fitGraph(animated = true) { function drawLabel(node, emphasized = false) { const fontSize = emphasized ? 13 : 10 - context.font = `${emphasized ? 600 : 500} ${fontSize}px "Noto Serif", serif` + context.font = `${emphasized ? 600 : 500} ${fontSize}px "IBM Plex Sans", system-ui, sans-serif` context.textAlign = "center" context.textBaseline = "bottom" context.fillStyle = getComputedStyle(document.documentElement).getPropertyValue("--ink") diff --git a/notes/quartz-site/routes/graph/index.html b/notes/quartz-site/routes/graph/index.html index 2d8178fd..83bcc3f7 100644 --- a/notes/quartz-site/routes/graph/index.html +++ b/notes/quartz-site/routes/graph/index.html @@ -18,7 +18,7 @@ @@ -27,7 +27,7 @@ - +
@@ -45,6 +45,6 @@

Graph

Drag to pan, scroll to zoom, select a node to open its note.

- + diff --git a/notes/quartz.config.yaml b/notes/quartz.config.yaml index 2848d9ad..89096942 100644 --- a/notes/quartz.config.yaml +++ b/notes/quartz.config.yaml @@ -17,30 +17,33 @@ configuration: # is disabled, so visitors load the font files from this site. cdnCaching: false typography: - header: Noto Serif - body: Noto Serif + # Headings use 600, so request it instead of Quartz's default 700. + header: + name: IBM Plex Sans + weights: [400, 600] + body: IBM Plex Sans code: IBM Plex Mono colors: lightMode: - light: "#fffbf5" - lightgray: "#e5e2dc" - gray: "#8b8680" - darkgray: "#595959" - dark: "#252320" - secondary: "#07507d" - tertiary: "#8a5a44" - highlight: rgba(53, 104, 89, 0.15) - textHighlight: "#f2df9b88" + light: "#ffffff" + lightgray: "#e4e4e1" + gray: "#6f6f6b" + darkgray: "#33332f" + dark: "#141413" + secondary: "#1f5fbf" + tertiary: "#0f3f86" + highlight: rgba(31, 95, 191, 0.08) + textHighlight: "#fff1a3" darkMode: - light: "#1d1f21" - lightgray: "#35393d" - gray: "#8d969e" - darkgray: "#d2d6da" - dark: "#f3f4f5" - secondary: "#86c7b5" - tertiary: "#d6a184" - highlight: rgba(134, 199, 181, 0.15) - textHighlight: "#8a6f2b88" + light: "#121212" + lightgray: "#2b2b2b" + gray: "#9a9a9a" + darkgray: "#d6d6d6" + dark: "#f3f3f3" + secondary: "#8ab4ff" + tertiary: "#b9d1ff" + highlight: rgba(138, 180, 255, 0.1) + textHighlight: "#6b5a1f" plugins: - source: "@quartz-community/note-properties" enabled: true diff --git a/notes/scripts/patch_google_font_fetch.py b/notes/scripts/patch_google_font_fetch.py index 16a8c07e..c8c2e056 100644 --- a/notes/scripts/patch_google_font_fetch.py +++ b/notes/scripts/patch_google_font_fetch.py @@ -8,7 +8,7 @@ MARKER = "// Notes site: request unicode-subsetted WOFF2 font CSS." -PRIMARY_FONT_FILE = "ga6daw1J5X9T9RW6j9bNVls-hfgvz8JcMofYTYf6D30.woff2" +PRIMARY_FONT_FILE = "zYXzKVElMYYaJe8bpLHnCwDKr932-G7dytD-Dmu1syxeKYY.woff2" OLD = """ const theme = ctx.cfg.configuration.theme const response = await fetch(googleFontHref(theme))