update
+960 / −395 files
Files changed
- M.claude/launch.json+2 / −1
- Ascripts/seed/refusals.ts+680 / −0
- Asrc/app/(marketing)/no/page.tsx+151 / −0
- Msrc/app/share/feature/[slug]/opengraph-image.tsx+83 / −26
- Msrc/marketing/components/named-betrayal-beat.tsx+44 / −12
Diff
Lines reveal in sequence as you scroll. First 20 lines per file shown — expand for the rest.
.claude/launch.json+2 / −1json| @@ -5,7 +5,8 @@ | |
| "name": "dev", | |
| "runtimeExecutable": "npm", | |
| "runtimeArgs": ["run", "dev"], | |
| − | "port": 3000 |
| + | "port": 3000, |
| + | "autoPort": true |
| } | |
| ] | |
| } |
scripts/seed/refusals.ts+680 / −0typescript| @@ -0,0 +1,680 @@ | |
| + | /** |
| + | * Seed the first wave of constitutional refusals (the NO. catalog). |
| + | * |
| + | * Inserts 5 Features (kind=refusal, importance=core, productScope=null) + |
| + | * 5 Comparisons (one per Feature, status=draft) + 7 commitment ties + |
| + | * 5 feature-comparison junction rows. |
| + | * |
| + | * The Comparisons are inserted as `draft` so the operator visits each in |
| + | * /hall/admin/comparisons, glances at citations one final time, and clicks |
| + | * Publish — the publish action runs the validator (>=2 citations + >=1 |
| + | * would-violate tie for refusals), fires the pulse event, and revalidates |
| + | * the public surfaces. Five small operator clicks; one for each refusal. |
| + | * |
| + | * Idempotent: re-running updates body/citation content but preserves |
| + | * status (so a published Comparison is not demoted back to draft on re-seed). |
| + | * |
| + | * Run: dotenv -e .env.local -- npx tsx scripts/seed/refusals.ts |
| + | */ |
| + |
View truncated for page performance. Full diff on GitHub ↗
src/app/(marketing)/no/page.tsx+151 / −0tsx| @@ -0,0 +1,151 @@ | |
| + | import Link from "next/link"; |
| + | import type { Metadata } from "next"; |
| + | import { listFeatures } from "@/shared/features/queries"; |
| + | import type { Feature } from "@/shared/db/schema/features"; |
| + | |
| + | export const dynamic = "force-dynamic"; |
| + | |
| + | export const metadata: Metadata = { |
| + | title: "The NO. catalog — Our.one", |
| + | description: |
| + | "Every constitutional refusal Our.one has named. What we won't build, and which incumbent does. Each refusal cited from the incumbent's own engineering blog or public statements.", |
| + | }; |
| + | |
| + | function splitNoTitle(title: string): { rest: string } | null { |
| + | const match = title.match(/^No\.?\s+(.+)$/i); |
| + | if (!match) return null; |
| + | return { rest: match[1] }; |
| + | } |
| + |
src/marketing/components/named-betrayal-beat.tsx+44 / −12tsx| @@ -34,19 +34,31 @@ async function pickElephant(): Promise<Feature | null> { | |
| return pickFeatureForRotation(elephants, weekOfYear(new Date())); | |
| } | |
| + | function splitNoTitle(title: string): { rest: string } | null { |
| + | const match = title.match(/^No\.?\s+(.+)$/i); |
| + | if (!match) return null; |
| + | return { rest: match[1] }; |
| + | } |
| + | |
| export async function NamedBetrayalBeat() { | |
| const feature = await pickElephant(); | |
| if (!feature) { | |
| return ( | |
| − | <BeatShell label="Beat 1 · The named betrayal"> |
| + | <BeatShell label="Beat 1 · What we refuse"> |
| <div className="flex flex-col gap-6 rounded-2xl border-2 border-dashed border-stone-300 bg-stone-50 px-6 py-10 md:px-8 md:py-12"> | |
| <p className="font-sans text-[11px] font-medium uppercase tracking-[0.25em] text-stone-500"> | |
| Refusals being drafted now |