From 77038c7609a4f3c4af9e0e7d427d68b33a4162df Mon Sep 17 00:00:00 2001 From: Dinesh <13635627+HumbleBee14@users.noreply.github.com> Date: Tue, 14 Jul 2026 05:00:05 -0700 Subject: [PATCH 1/8] Optimize OG: raw cover / shared brand card, drop per-post generation Keeps build time flat regardless of post count (no render or file per post, so PR preview links aren't delayed and we stay well under Cloudflare's 20k-file deploy cap): - Cover posts share the raw cover as og:image; others share the single prebuilt /og-default.png - Per-post composite OG endpoint preserved but disabled (getStaticPaths returns []); documented how to re-enable, ideally with edge caching - Add article:author meta (fixes 'No author found' in link inspectors) --- src/layouts/BaseLayout.astro | 4 +++ src/lib/og.tsx | 48 ++++++++++++++++++++++----------- src/pages/blog/[slug].astro | 9 ++++--- src/pages/og/post/[slug].png.ts | 17 +++++++----- 4 files changed, 53 insertions(+), 25 deletions(-) diff --git a/src/layouts/BaseLayout.astro b/src/layouts/BaseLayout.astro index 03dfaa6..58df2c1 100644 --- a/src/layouts/BaseLayout.astro +++ b/src/layouts/BaseLayout.astro @@ -41,6 +41,8 @@ interface Props { modifiedTime?: string; /** Article section (topic) — OG article metadata */ section?: string; + /** Article author name(s) — emitted as article:author OG metadata */ + author?: string; /** Article tags — emitted as article:tag OG metadata */ tags?: string[]; /** Alt text for the OG image — defaults to the page title */ @@ -60,6 +62,7 @@ const { publishedTime, modifiedTime, section, + author, tags, imageAlt, noindex = false, @@ -167,6 +170,7 @@ const siteJsonLd = [ {publishedTime && } {modifiedTime && } {section && } + {author && } {tags?.map((tag) => )} diff --git a/src/lib/og.tsx b/src/lib/og.tsx index 4c7c0c0..8dd8af3 100644 --- a/src/lib/og.tsx +++ b/src/lib/og.tsx @@ -168,7 +168,7 @@ function coverTemplate(d: OgArticle, coverUrl: string): React.ReactElement { height: 630, display: 'flex', background: - 'linear-gradient(180deg, rgba(0,0,0,0.15) 0%, rgba(0,0,0,0.35) 42%, rgba(0,0,0,0.84) 100%)', + 'linear-gradient(180deg, rgba(0,0,0,0.42) 0%, rgba(0,0,0,0.28) 34%, rgba(0,0,0,0.84) 100%)', }} />
- - - MLSYSTEMS.DEV - + +
+ + MLSYSTEMS.DEV + + + Machine learning, from{' '} + + kernels + {' '} + to clusters. + +
diff --git a/src/pages/og/post/[slug].png.ts b/src/pages/og/post/[slug].png.ts index 18cf8e9..c70c35c 100644 --- a/src/pages/og/post/[slug].png.ts +++ b/src/pages/og/post/[slug].png.ts @@ -1,5 +1,5 @@ import type { APIRoute } from 'astro'; -import { getCollection, getEntries } from 'astro:content'; +import { getEntries } from 'astro:content'; import type { CollectionEntry } from 'astro:content'; import { readFileSync } from 'fs'; import { join } from 'path'; @@ -7,12 +7,17 @@ import { generateOgPng } from '@/lib/og'; import { pngResponseWithFallback } from '@/lib/og-response'; import { topicName } from '@/lib/data'; +// Per-post OG cards (title/cover composited into a 1200×630 card) are preserved +// here as an opt-in feature but NOT generated — one render + one file per post +// doesn't scale (Cloudflare Pages caps a deploy at 20k files) and adds build +// time that delays the PR preview link. Posts share the raw cover or the single +// prebuilt /og-default.png instead (see src/pages/blog/[slug].astro). +// +// To re-enable: restore the getCollection import and return the mapped posts, +// then repoint ogImage in blog/[slug].astro back to `/og/post/${post.id}.png`. +// Best paired with edge/on-demand caching so cards render once, not per build. export async function getStaticPaths() { - const posts = await getCollection('posts', ({ data }) => !data.draft); - return posts.map((post) => ({ - params: { slug: post.id }, - props: { post }, - })); + return [] as { params: { slug: string }; props: { post: CollectionEntry<'posts'> } }[]; } const MIME: Record = { From 340b8c09bd5e53a393bb60d77dfcdd1be2fbc758 Mon Sep 17 00:00:00 2001 From: Dinesh <13635627+HumbleBee14@users.noreply.github.com> Date: Tue, 14 Jul 2026 05:06:17 -0700 Subject: [PATCH 2/8] Add opt-in 'Designed share card' checkbox (env-gated) - New ogCard frontmatter flag: when set (with a cover), the post renders a generated 1200x630 card (title over cover); otherwise raw cover / shared card - Per-post OG generation now runs ONLY for opted-in posts, so build stays flat - Checkbox shows under a cover in /write, gated by PUBLIC_OG_CARD_OPTIN (off by default) so it can be toggled centrally; short label + hover info tooltip - Round-trips via the .write-source.json sidecar like other meta --- src/content/config.ts | 4 ++++ src/lib/site.ts | 4 ++++ src/pages/blog/[slug].astro | 15 +++++++++++---- src/pages/og/post/[slug].png.ts | 24 +++++++++++++----------- src/write/WritePortal.tsx | 1 + src/write/editor/editor-theme.css | 20 +++++++++++++++++++- src/write/meta/MetaForm.tsx | 18 ++++++++++++++++++ src/write/serialize/toMdx.ts | 4 ++++ 8 files changed, 74 insertions(+), 16 deletions(-) diff --git a/src/content/config.ts b/src/content/config.ts index 49d9e86..7f148e9 100644 --- a/src/content/config.ts +++ b/src/content/config.ts @@ -38,6 +38,10 @@ const posts = defineCollection({ tags: z.array(z.string()).optional(), updated: z.coerce.date().optional(), cover: z.union([image(), z.string().url()]).optional(), + // Opt in to a generated 1200×630 share card (post title over the cover). + // Only these posts render an OG card at build; everyone else uses the raw + // cover or the shared default, keeping build time flat. + ogCard: z.boolean().optional().default(false), featured: z.boolean().optional().default(false), draft: z.boolean().optional().default(false), }), diff --git a/src/lib/site.ts b/src/lib/site.ts index b97f12d..529a408 100644 --- a/src/lib/site.ts +++ b/src/lib/site.ts @@ -36,6 +36,10 @@ export const SITE = { // Shows the "Post to GitHub" button in /write. Flip to true once the GitHub App // credentials are set in the Cloudflare Function env (see /api/create-pr). githubPostEnabled: true, + // Shows the "Designed share card" opt-in under a cover in /write. Off by default; + // set PUBLIC_OG_CARD_OPTIN=true to expose it. Opted-in posts render a per-post OG + // card at build (a small build-time cost), so keep it off unless you want it. + ogCardOptIn: import.meta.env.PUBLIC_OG_CARD_OPTIN === 'true', }; export const APPEARANCE = { diff --git a/src/pages/blog/[slug].astro b/src/pages/blog/[slug].astro index 045cb45..2dc1771 100644 --- a/src/pages/blog/[slug].astro +++ b/src/pages/blog/[slug].astro @@ -45,10 +45,17 @@ const dateISO = post.data.date.toISOString(); const modifiedISO = (post.data.updated ?? post.data.date).toISOString(); const topicLabel = topicName(post.data.topicId); const cover = post.data.cover; -// Cover posts share the raw cover; others share one prebuilt brand card. Per-post -// OG generation is disabled (see src/pages/og/post/[slug].png.ts) to keep build -// time flat regardless of post count — no render or file per post. -const ogImage = cover ? (typeof cover === 'string' ? cover : cover.src) : '/og-default.png'; +// Opted-in posts (ogCard + cover) get a generated card; cover posts share the raw +// cover; the rest share one prebuilt brand card. Per-post generation runs only for +// the opted-in handful, keeping build time flat (see og/post/[slug].png.ts). +const ogImage = + cover && post.data.ogCard + ? `/og/post/${post.id}.png` + : cover + ? typeof cover === 'string' + ? cover + : cover.src + : '/og-default.png'; const articleJsonLd = { '@context': 'https://schema.org', diff --git a/src/pages/og/post/[slug].png.ts b/src/pages/og/post/[slug].png.ts index c70c35c..a866a2b 100644 --- a/src/pages/og/post/[slug].png.ts +++ b/src/pages/og/post/[slug].png.ts @@ -1,5 +1,5 @@ import type { APIRoute } from 'astro'; -import { getEntries } from 'astro:content'; +import { getCollection, getEntries } from 'astro:content'; import type { CollectionEntry } from 'astro:content'; import { readFileSync } from 'fs'; import { join } from 'path'; @@ -7,17 +7,19 @@ import { generateOgPng } from '@/lib/og'; import { pngResponseWithFallback } from '@/lib/og-response'; import { topicName } from '@/lib/data'; -// Per-post OG cards (title/cover composited into a 1200×630 card) are preserved -// here as an opt-in feature but NOT generated — one render + one file per post -// doesn't scale (Cloudflare Pages caps a deploy at 20k files) and adds build -// time that delays the PR preview link. Posts share the raw cover or the single -// prebuilt /og-default.png instead (see src/pages/blog/[slug].astro). -// -// To re-enable: restore the getCollection import and return the mapped posts, -// then repoint ogImage in blog/[slug].astro back to `/og/post/${post.id}.png`. -// Best paired with edge/on-demand caching so cards render once, not per build. +// Per-post OG cards (title composited over the cover) render ONLY for posts that +// opted in via `ogCard: true` + a cover. Everyone else uses the raw cover or the +// shared /og-default.png (see blog/[slug].astro), so build time stays flat: one +// render/file only for the handful that ask for it. export async function getStaticPaths() { - return [] as { params: { slug: string }; props: { post: CollectionEntry<'posts'> } }[]; + const posts = await getCollection( + 'posts', + ({ data }) => !data.draft && data.ogCard && !!data.cover, + ); + return posts.map((post) => ({ + params: { slug: post.id }, + props: { post }, + })); } const MIME: Record = { diff --git a/src/write/WritePortal.tsx b/src/write/WritePortal.tsx index bc522a3..20cb4fd 100644 --- a/src/write/WritePortal.tsx +++ b/src/write/WritePortal.tsx @@ -111,6 +111,7 @@ function emptyMeta(): PostMeta { tags: [], slug: '', coverFileName: '', + ogCard: false, proposedTopic: '', newAuthor: null, }; diff --git a/src/write/editor/editor-theme.css b/src/write/editor/editor-theme.css index d47f974..8586778 100644 --- a/src/write/editor/editor-theme.css +++ b/src/write/editor/editor-theme.css @@ -360,7 +360,25 @@ } .write-cover-set { display: flex; - justify-content: center; + flex-direction: column; + align-items: center; + gap: 10px; +} +.write-ogcard-opt { + display: inline-flex; + align-items: center; + gap: 8px; + font-family: var(--font-mono); + font-size: 12px; + color: var(--ink-2); + cursor: pointer; +} +.write-ogcard-opt input { + cursor: pointer; +} +.write-ogcard-info { + color: var(--ink-4); + cursor: help; } .write-cover-frame { position: relative; diff --git a/src/write/meta/MetaForm.tsx b/src/write/meta/MetaForm.tsx index 35f46e3..50dac07 100644 --- a/src/write/meta/MetaForm.tsx +++ b/src/write/meta/MetaForm.tsx @@ -1,4 +1,5 @@ import { useState } from 'react'; +import { SITE } from '@/lib/site'; import type { NewAuthor, PostMeta } from '../serialize/toMdx'; import { slugify } from '../serialize/validate'; import { addAsset, getAssetUrl } from '../storage/assets'; @@ -242,6 +243,23 @@ export function MetaForm({ authors, topics, meta, images, onChange }: Props) { ✕
+ {SITE.ogCardOptIn && ( + + )} ) : (
diff --git a/src/write/serialize/toMdx.ts b/src/write/serialize/toMdx.ts index 815890e..46f1845 100644 --- a/src/write/serialize/toMdx.ts +++ b/src/write/serialize/toMdx.ts @@ -35,6 +35,9 @@ export type PostMeta = { tags: string[]; slug: string; coverFileName: string; + // Opt in to a generated share card (title over the cover) instead of the raw + // cover. Only meaningful when a cover is set. + ogCard?: boolean; // Set only when editing an existing post; preserves its original publish date. date?: string; // A topic the writer proposes that isn't in the list yet — a maintainer (or, later, @@ -404,6 +407,7 @@ function buildFrontmatter(meta: PostMeta, blocks: SBlock[], opts: SerializeOptio if (meta.tags.length > 0) lines.push(`tags: [${meta.tags.map(yaml).join(', ')}]`); if (meta.proposedTopic?.trim()) lines.push(`proposedTopic: ${yaml(meta.proposedTopic.trim())}`); if (meta.coverFileName) lines.push(`cover: ${yaml(`./${meta.coverFileName}`)}`); + if (meta.coverFileName && meta.ogCard) lines.push('ogCard: true'); return `---\n${lines.join('\n')}\n---`; } From 101b2a17c8e3c384ae38f445151c9beb1b2daa25 Mon Sep 17 00:00:00 2001 From: Dinesh <13635627+HumbleBee14@users.noreply.github.com> Date: Tue, 14 Jul 2026 05:09:00 -0700 Subject: [PATCH 3/8] Document PUBLIC_OG_CARD_OPTIN in .env.example --- .env.example | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.env.example b/.env.example index 737761a..966b013 100644 --- a/.env.example +++ b/.env.example @@ -9,3 +9,8 @@ PUBLIC_GA_MEASUREMENT_ID= # Leaving these blank shows the "comments not configured" fallback. PUBLIC_GISCUS_REPO_ID= PUBLIC_GISCUS_CATEGORY_ID= + +# Show the "Designed share card" opt-in under a cover in /write. Only "true" +# enables it; anything else (or unset) is treated as off. Opted-in posts render +# a per-post OG card at build, so leave off unless you want that cost. +PUBLIC_OG_CARD_OPTIN=true From faafeea3d75629ec6758a9cb7164d5ad1778c390 Mon Sep 17 00:00:00 2001 From: Dinesh <13635627+HumbleBee14@users.noreply.github.com> Date: Tue, 14 Jul 2026 05:14:26 -0700 Subject: [PATCH 4/8] Show cover at true 1.91:1 crop in editor + hero; hint on the label MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Editor cover preview is now a 1200x630 (1.91:1) frame with object-fit:cover, so authors see the exact crop link previews will show; tip nudges 1200x630 - Article hero uses the same 1.91:1 ratio, so editor = hero = social all match - Non-ideal covers are center-cropped for display only (file unchanged, no build cost) — authors see it and can swap - Replace the broken info glyph with a native title tooltip on the whole label --- src/styles/global.css | 2 +- src/write/editor/editor-theme.css | 28 +++++++++++++++++----------- src/write/meta/MetaForm.tsx | 15 +++++++-------- 3 files changed, 25 insertions(+), 20 deletions(-) diff --git a/src/styles/global.css b/src/styles/global.css index fb12db8..23f5f05 100644 --- a/src/styles/global.css +++ b/src/styles/global.css @@ -1346,7 +1346,7 @@ a.hashtag:hover { } .article-cover img { width: 100%; - max-height: 440px; + aspect-ratio: 1200 / 630; object-fit: cover; height: auto; border-radius: var(--radius-lg, 10px); diff --git a/src/write/editor/editor-theme.css b/src/write/editor/editor-theme.css index 8586778..db653ea 100644 --- a/src/write/editor/editor-theme.css +++ b/src/write/editor/editor-theme.css @@ -376,24 +376,30 @@ .write-ogcard-opt input { cursor: pointer; } -.write-ogcard-info { - color: var(--ink-4); - cursor: help; -} .write-cover-frame { position: relative; - display: inline-block; + display: block; line-height: 0; -} -.write-cover-frame img { - max-width: 220px; - max-height: 150px; - width: auto; - height: auto; + width: 340px; + max-width: 100%; + aspect-ratio: 1200 / 630; border-radius: 8px; border: 1px solid var(--line-2); + overflow: hidden; +} +.write-cover-frame img { + width: 100%; + height: 100%; + object-fit: cover; display: block; } +.write-cover-tip { + font-family: var(--font-mono); + font-size: 11px; + color: var(--ink-3); + text-align: center; + max-width: 340px; +} .write-cover-remove { position: absolute; top: -8px; diff --git a/src/write/meta/MetaForm.tsx b/src/write/meta/MetaForm.tsx index 50dac07..730c7b8 100644 --- a/src/write/meta/MetaForm.tsx +++ b/src/write/meta/MetaForm.tsx @@ -243,21 +243,20 @@ export function MetaForm({ authors, topics, meta, images, onChange }: Props) { ✕
+ + This is the crop shown in link previews. Best at 1200×630 (landscape). + {SITE.ogCardOptIn && ( -