Develop - #34
Open
danielheene wants to merge 279 commits into
Open
Conversation
Seven-task plan: relocate app code, split package.json between root tooling and the new web package, register web in the pnpm workspace, centralize env loading via @next/env, update biome includes, and run a full install/dev/build/migrate verification pass. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…g code from next.config.ts
@next/env is CJS-only with no exports field, and its interop shape differs between Next.js's config loader (named exports on the namespace, default undefined) and plain Node ESM used by Payload's CLI (whole exports object as default, named import throws). Use a namespace import with a default fallback so loadEnvConfig resolves under both. Also pass forceReload: true so the repo-root .env* files are actually read instead of returning @next/env's primed empty cache. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
pnpm-lock.yaml was previously regenerated while the root package.json had uncommitted, unrelated WIP changes (@changesets/* additions) in the working tree, so the committed lockfile's root importer no longer matched the committed root package.json. Regenerate against a clean checkout of package.json so the lockfile is internally consistent with this commit again; the storybook importer's Tailwind additions are unaffected. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Also fixes Icons.mdx's broken '@sb//IconGallery.tsx' import (an unconfigured alias introduced when files moved into src/ earlier this session) to a correct relative import, since it directly blocked verifying this task's own build.
Generated via the acquire-codebase-knowledge workflow to give future sessions (human or agent) a verified map of the stack, structure, architecture, conventions, integrations, testing setup, and known concerns without re-deriving them from scratch each time. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ix import paths - Convert `web/env.ts` to async `loadEnv()` that returns validated `Env` or exits on schema failure - Add `storybook/global.d.ts` for CSS module declarations - Update `storybook/package.json` with import mappings for `#styles/*` and `#fonts/*` - Configure `storybook/.storybook/main.ts` with `TsconfigPathsPlugin` for path resolution - Integrate Tailwind decorators and theme support in `storybook/.storybook/preview.tsx` - Change `web/tsconfig.json` jsx to `react-jsx` and clean up excludes - Format `web/payload.config.ts` and `web/next.config.ts` with prettier-style spacing - Update `web/next-env.d.ts` to new Next.js routes import style - Install `tsconfig-paths-webpack-plugin` and remove `polished` from storybook dependencies Signed-off-by: Daniel Heene <daniel@heene.io>
Adds web/Dockerfile (base/deps/builder/runner stages, full node_modules retained per CON-002 so `payload jobs:run` resolves as a command override), web/docker-entrypoint.sh (optional userspace-networking Tailscale bring-up then `exec "$@"`, no mode-branching per REQ-003/ REQ-011), and web/.dockerignore. Deviates from the plan's draft Dockerfile in two build-tested ways: CI=true in the builder stage (pnpm's non-interactive deps-status check otherwise aborts with no TTY during `docker build`), and copying web/app into the runner stage (Payload's admin panel dynamically imports app/(payload)/admin/importMap.js by path at runtime). Build currently fails at the `next build` step due to an upstream, unpatched Turbopack production-build bug in next@16.2.10 affecting next/font/local resolution — reproduces identically outside Docker, documented in .superpowers/sdd/task-1-report.md.
Docker only auto-discovers a plain .dockerignore at the build context root, not inside a subdirectory. Since web/Dockerfile is built with the repo root as context (`-f web/Dockerfile .`), the correctly-resolved per-Dockerfile ignore file is web/Dockerfile.dockerignore. Verified empirically: build context transfer drops from 3.82GB (unfiltered) to 507MB (filtered) with the file correctly named and placed. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…cker image Adds storybook/Dockerfile (deps/build/nginx-runtime stages) and storybook/Dockerfile.dockerignore for the repo-root build context, mirroring the pattern from web/Dockerfile (Task 1). Named the ignore file Dockerfile.dockerignore rather than .dockerignore, since Docker's per-Dockerfile ignore-file resolution requires that exact naming when the build context is the repo root. The deps stage installs both `storybook` and `web` (`--filter storybook... --filter web...`), not just storybook: Storybook's build compiles web/src components directly (staticDirs, tsconfig path aliases, story sources) rather than importing `web` as a declared workspace dependency, so `--filter storybook...` alone doesn't pull in web's own dependencies (e.g. @iconify-icon/react, used by web/src/components/Icon). Also fixes a pre-existing bug (reproducible outside Docker on a plain `pnpm --filter storybook run build`) that blocked any build: package.json's `imports` field used `../web/src/styles/*` as a subpath target, which Node's imports spec rejects (targets must start with `./`, no parent-directory traversal). Repoints preview.tsx's CSS import at the already-working `@/*` tsconfig path alias instead, and drops the broken/dead `imports` field (#fonts/* was already unused - the only font import uses the working @fonts/* tsconfig alias). Verified: docker buildx build succeeds, context transfer is ~150-240kB (vs 3.8GB+ with a deliberately misnamed ignore file, confirmed as a negative control), the image runs on nginx:1-alpine at 88.3MB, and curl against the running container returns HTTP 200 with the Storybook manager HTML. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Bump storybook to 1.0.0 to match web's baseline, then group both packages under Changesets' `fixed` mode so every release bumps them to the identical version number in lockstep (Babel/Payload model). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds .github/workflows/docker-images.yml with build-web, build-storybook, and deploy jobs. Tags are computed via docker/metadata-action per branch/PR context, web builds select the correct Tailscale-routed database secret (prod/develop/preview) via BuildKit build secrets, and successful main/develop pushes trigger the corresponding Dokploy webhook. PR builds never trigger deploy. Implements TASK-013 through TASK-017 (GOAL-004).
Dependabot's docker ecosystem only scans the exact directory given (no recursion), so the existing root entry doesn't cover the new web/Dockerfile and storybook/Dockerfile added for the image pipeline.
…ages Captures the design for splitting the four PP font families into self-contained @danielheene/font-* packages usable by both Next.js and react-pdf, including a verified feasibility spike proving next/font/local works when called from a separate workspace package. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
next/font/local requires a literal src array at the call site, so next.ts can never consume a shared manifest. Since one consumer would end up hardcoding literals anyway, keeping a manifest only for pdf.ts added indirection without payoff — both files now hardcode their own literal file list. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds the local pdf.ts react-pdf config (reading bundled ttf files directly instead of over HTTP), removes the unused manifest-style index.ts, and narrows package.json to the two subpath exports. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The index.ts deletion from the prior commit was made in the working tree but never staged, so it wasn't actually part of that commit. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
… ignore Task 2's build accidentally committed tsup's dist/ output. Widens the existing dist/media ignore rule to dist generally, since no other tracked dist/ directory exists in the repo, and untracks the one that slipped in. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Preserves the existing behavior of registering each weight twice in the react-pdf config (normal and italic both pointing at the same file), since no separate italic files exist for this family. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EQE1CxwMDmNUcYJQJBGjFz
deletedPages only made sense for Pages. Renamed to deleted and added skipped, so the same channel type and admin-panel toast summary work for cleanPosts/cleanTopics (added in later tasks) without inventing a new field name per collection. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GjuWYPbCQ16C3Fkc1k8grT
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EQE1CxwMDmNUcYJQJBGjFz
Ported verbatim (same node shapes, same deterministic-random helpers) into src/lib/seed/lexical.ts so Pages, BlogPosts, and BlogTopics' seed generators can share one implementation instead of each carrying its own copy. Added test coverage the original script never had. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GjuWYPbCQ16C3Fkc1k8grT
Flagged by Task 3's review — imported but never referenced. No italic-specific test coverage was planned; IS_BOLD already exercises the format bitmask structurally.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EQE1CxwMDmNUcYJQJBGjFz
- payload.config.ts reads MIGRATION_DIR, defaulting to migrations/required — the set safe to run unattended in every environment, including production. - migrations/dev (opt-in via MIGRATION_DIR=migrations/dev, wired through the new migrate:dev script) will hold local-only sample data that must never reach production. - migrations/required/20260818_090411_seed_site_settings.ts seeds the SiteSettings header nav with a single "Home" entry when empty, so a brand-new database doesn't render with an empty header. Both up and down are idempotent and scoped to only the entry this migration owns.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EQE1CxwMDmNUcYJQJBGjFz
lexical.ts's link() was ported from scripts/seed-blog.ts, whose claimed field shape (nested under a 'link' key with a 'type' discriminator) no longer matches this project's actual LinkField (flat fields, no 'type' field at all — removed in an earlier refactor). Every link insertion into a rich-text field threw a deterministic ValidationError. Verified the fix against the real Payload validator, not just the unit test. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GjuWYPbCQ16C3Fkc1k8grT
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EQE1CxwMDmNUcYJQJBGjFz
Replaced the generic 'Seeded Dummy Page N' title and single boilerplate paragraph with a realistic title pool and 2-4 blocks per page (One-Column, Two-Column, Code, Link Group), each with real prose generated via the shared src/lib/seed/lexical builders. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GjuWYPbCQ16C3Fkc1k8grT
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EQE1CxwMDmNUcYJQJBGjFz
Follows the seedPages/cleanPages shape exactly. cleanTopics' one new behavior: it never deletes a seeded topic a non-seeded (real) post still references, relying on the corrected topics.contains query from the BlogTopics access-check fix. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GjuWYPbCQ16C3Fkc1k8grT
Ports scripts/seed-blog.ts's lexicalArticle generator onto the shared src/lib/seed/lexical builders. seedPosts resolves 1-2 topic ids per post, creating its own seeded topics via seedTopics when too few exist rather than requiring seedTopics to run first. cleanPosts cascade-deletes hero images with the same live-flag-recheck safety property cleanPages already has, and does not touch BlogTopics. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GjuWYPbCQ16C3Fkc1k8grT
…ribution
The `caption` editor variant passes an array to `LinkFeature({ fields })`, so
lexical's `transformExtraFields` replaces the default `linkType`/`url`/`newTab`
base fields with `LinkField()`'s own. `buildCreditsValue` was emitting the
default shape, which has no `label` — and `label` is what `CMSLink` actually
renders. Because `label` carries `defaultValue: '{title}'`, this passed
validation and then rendered the literal template in the credits line.
Emit `{ reference: null, url, newTab, label }` with empty children (CMSLink
renders label *then* children, so text children would duplicate it), matching
the canonical shape `migrateLinkFields` produces.
Adds a validation-boundary test that sanitizes `LinkField().fields` through
Payload's own `sanitizeFields` and reproduces `linkValidation`'s
`fieldSchemasToFormState` call, asserting on resolved values as well as
`errorPaths` — an errors-only assertion would not have caught this.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EQE1CxwMDmNUcYJQJBGjFz
seedTopics is idempotent-by-slug, not "create N more" — it skips pool slots that already exist. A naive shortfall calculation could silently under-fulfill the requested count. resolveTopicIds now loops until it actually has `count` ids (bounded, since topicFor's cycling suffixes make running out of the pool practically impossible for realistic seed sizes). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GjuWYPbCQ16C3Fkc1k8grT
Extends the single 'pages'-only branch to also handle 'posts' and 'topics', each calling the matching seedX/cleanX pair from Tasks 5-6. Same task, same channel, same mode-parameterized shape. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GjuWYPbCQ16C3Fkc1k8grT
Link nodes emitted by buildCreditsValue set `children: []`, on the theory
that CMSLink prints `resolvedLabel || label` followed by the children and
would otherwise duplicate the text. CMSLink is not on this render path:
MediaImages.credits is rendered via `<RichText data={credit.credits} />`,
which emits a bare `<a>{children}</a>`.
Every Lexical -> JSX link converter — Payload's own and this repo's
override — builds the anchor body from `nodesToJSX({ nodes: node.children })`
and never from `fields.label`, so empty children rendered as an empty
`<a href="…"></a>`. Each link node now carries a real text child.
The flat `fields` shape is confirmed correct and unchanged: LinkFeature is
configured with `[...LinkField().fields]`, which spreads the group's inner
fields, and lexical's transformExtraFields replaces the base fields when
given an array — the same flat shape the admin link editor drawer produces.
Adds creditsRendering.test.tsx, which drives Payload's converter stack over
the real output and asserts on the HTML, with a negative control.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EQE1CxwMDmNUcYJQJBGjFz
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GjuWYPbCQ16C3Fkc1k8grT
Registers the existing, unmodified SeedActions component on both collections, matching Pages' registration exactly — no component changes needed. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GjuWYPbCQ16C3Fkc1k8grT
Superseded by seed:posts and seed:topics (src/lib/seed/posts.ts, src/lib/seed/topics.ts), which cover the same content generation via the shared src/lib/seed/lexical module, plus the seeded-dummy tagging and admin-panel action seed-blog.ts never had. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GjuWYPbCQ16C3Fkc1k8grT
…uired migration
migrations/dev/20260820_120000_seed_dummy_pages.ts seeds 5 sample Pages
via the existing seedPages/cleanPages fixtures (src/lib/seed/pages.ts —
same ones pnpm run seed:pages uses) so a fresh clone gets usable content
automatically on `pnpm run migrate:dev`, without duplicating that fixture
logic. Idempotent and reversible, matching the seed script's own
matched-by-slug skip behavior.
migrations/required/20260818_090411_seed_site_settings.ts: its
updateGlobal call triggers an afterChange hook that calls Next.js's
revalidateTag, which throws outside of a running Next.js request
("Invariant: static generation store missing") — i.e. every time this
runs via the standalone `payload migrate` CLI, including in production.
Sets req.context.skipUpdateCachedData (the escape hatch the hook already
checks for, used elsewhere in the repo) before both updateGlobal calls.
Verified end-to-end against local Mongo: migrate:dev now completes
(previously crashed the process), both migrations are idempotent on
re-run, and migrate:down cleanly reverts both.
…-returns resolveTopicIds was being called fresh for every seeded post, causing redundant queries and giving every post the same leading 1-2 topics instead of a spread across the pool. Hoisted resolution to run once per seedPosts call, rotating which topics each post gets by index. Also logs when the bounded retry loop can't reach the requested count, since a silent short-return now affects every post in a run instead of just one. Also: dropped the vestigial type:'custom' field from pages.ts's linkGroupBlock (inconsistent with lexical.ts's corrected link() shape), replaced two inline text-node literals with the shared text() helper, and restored the spec's videoIds parity branch in lexicalArticle (dead but present, matching the design doc's explicit instruction). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GjuWYPbCQ16C3Fkc1k8grT
The final-review fix hoisted resolveTopicIds out of seedPosts' loop but requested only MAX_TOPICS_PER_POST (2), so the whole corpus rotated between just 2 topics regardless of how many were seeded — the review's re-verification flagged this as a residual realism gap versus the retired script's full-pool rotation. Pool size is now min(count, MAX_TOPIC_POOL_SIZE=6), floored at 2, so a larger seed run spreads posts across more topics. Updated two tests whose mocks assumed a fixed 2-topic pool request to match the wider, count-scaled request.
…d admin UI
Mirrors the seedPages/cleanPages pattern (src/lib/seed/pages.ts): the
813-line monolithic scripts/seed-blog.ts becomes a thin argv-parsing
wrapper around new seedBlog/cleanBlog functions in src/lib/seed/blog.ts,
making the fixture logic reusable outside the CLI script — specifically
by the job-queue seed/clean task, matching Pages' admin-panel integration.
Two real bugs surfaced while verifying the extraction end-to-end against
local Mongo (not just typechecked):
- The seeded article's link() node nested its fields under a `link` key
(`fields: { link: { type, newTab, label, url, reference } }`). Payload's
Lexical link validator runs `node.fields` directly against the custom
LinkField's inner field schema, which expects those fields flat — so
every post containing a link node failed to validate and the whole
create() aborted. This bug was already latent in the original
scripts/seed-blog.ts; it likely never fired because 0.45 odds across
many section loops made it easy to not hit in a spot-check.
- cleanBlog's topic-deletion loop threw and aborted the rest of cleanup
the moment a seeded topic was still referenced by an unrelated,
non-seeded post (a real reference-integrity guard elsewhere in the
app). Now caught per-topic and skipped with a warning, so one
still-referenced topic no longer blocks removing everything else.
Also, since Pages and Blog now return different result shapes (one
document type vs. two, plus media), generalized `SeedTaskProgress`'s
success payload from fixed optional fields (created/deletedPages/
deletedMedia) to a `counts: Record<string, number>` label→count bag, and
updated SeedActions' toast to render it generically — avoids each newly
seedable collection needing bespoke fields wired through the SSE type,
the task, and the toast.
BlogPosts admin now carries the same Seed…/Clean list-menu action as
Pages (mounted once, since seeding always creates/removes topics and
posts together — see src/lib/seed/blog.ts's docstring).
Verified end-to-end against local Mongo: seed creates topics + posts +
downloads placeholder images correctly (including the fixed link nodes),
reruns are idempotent, and clean removes everything it created while
correctly skipping a topic still referenced elsewhere.
…pages-seed-realism # Conflicts: # scripts/seed-blog.ts # src/collections/BlogPosts/index.ts # src/components/AdminPanel/SeedActions/SeedActions.tsx # src/jobs-queue/tasks/seedCollection.ts # src/lib/sse/channels.ts
Comment on lines
+31
to
+33
| const response = await fetch(`${UNSPLASH_API_BASE}/photos/${photoId}`, { | ||
| headers: unsplashHeaders(accessKey), | ||
| }) |
| }), | ||
| } | ||
| } | ||
| if (String(url).includes('images.unsplash.com')) { |
| if (node.type === 'link') node.children = [] | ||
| } | ||
|
|
||
| expect(renderCredits(value).replace(/<[^>]+>/g, '')).toBe('Photo by on ') |
|
|
||
| // No empty anchors: the exact failure mode `children: []` produced. | ||
| expect(html).not.toMatch(/<a\b[^>]*><\/a>/) | ||
| expect(html.replace(/<[^>]+>/g, '')).toBe('Photo by Jane Doe on Unsplash') |
* feat(hero): add HeroBackgroundField and shader preset registry Shared field group (media vs. shader discriminator) for Pages/BlogPosts' hero backgrounds. shaderPresets.ts stays import-light (no ogl, no shader components) so it's safe to import from the field config module, which payload generate:types runs under plain Node. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GjuWYPbCQ16C3Fkc1k8grT * feat(hero): add the shader picker drawer admin field component Drawer-based picker with 4 live-rendering shader preset cards, modeled on src/fields/Icon/Field.tsx's structure (useField/useModal/useDrawerSlug + Drawer, pick-and-close). Dynamically imported with ssr:false throughout since the shader components touch window outside any effect. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GjuWYPbCQ16C3Fkc1k8grT * fix(hero): disable the shader picker when the field is read-only FieldComponent.tsx never checked readOnly, unlike its structural precedent (src/fields/Icon/Field.tsx), which disables the trigger button and omits the drawer entirely in read-only contexts (versions/ diff views, access-controlled fields, locked documents). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GjuWYPbCQ16C3Fkc1k8grT * feat(hero): add shader-aware rendering to HeroMedia HeroMedia now takes the whole hero.background group value instead of a pre-normalized media array, so it can branch between media (image/video/ carousel) and a live shader preset. Callers still passing the old media prop will fail typecheck until Tasks 5-6 update them - expected, not a regression. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GjuWYPbCQ16C3Fkc1k8grT * fix(hero): update not-found.tsx's HeroMedia call to the new background prop A plan gap found during Task 4's review — this standalone caller (driven by SiteSettings' errorHero global field, not Pages/BlogPosts) was missed by every task's file list. No shader option added here, per the spec's non-goal restricting shader support to Pages/BlogPosts — just updates the call site to HeroMedia's new background prop shape. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GjuWYPbCQ16C3Fkc1k8grT * feat(hero): migrate Pages' hero.media to hero.background Pages now uses the shared HeroBackgroundField, enabling shader backgrounds. hero.media -> hero.background.media; listViewThumbnailPath and the frontend page renderer updated to match. seedPages writes the new shape (media-only for now — Task 13 adds shader variety). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GjuWYPbCQ16C3Fkc1k8grT * feat(hero): migrate BlogPosts' heroImage to hero.background BlogPosts now uses the shared HeroBackgroundField (single-image, not a carousel — hasManyMedia: false), enabling shader backgrounds. Updates the post detail page's HeroMedia call, PostCard's list-view thumbnail (shader posts get a static gradient fallback instead of an empty card), and seedPosts' write shape. Also updates TrendingBlogPostsBlock's Renderer, which read the old heroImage field directly, and regenerates src/types/payload.ts. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GjuWYPbCQ16C3Fkc1k8grT * fix(hero): correct BlogPosts' listViewThumbnailPath for its single-object media hero.background.media is hasMany:false for BlogPosts (unlike Pages, which is hasMany:true and correctly indexes .media.0) — the '.0' suffix meant CellWithThumbnail's lodash get() always resolved to undefined, leaving every BlogPost row's admin thumbnail blank regardless of whether the post had a hero image. BlogPosts' media field is also a polymorphic upload relation (relationTo: [MediaImages, MediaVideos]), so Payload/resolveRelations represent it as a { relationTo, value } wrapper even when hasMany is false — the actual media document (with .url/.thumbnailURL) lives at .value, not on the wrapper itself. Verified empirically against a seeded post's resolved document: hero.background.media resolves to undefined via CellWithThumbnail's get() logic, while hero.background.media.value resolves to the real thumbnail URL. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GjuWYPbCQ16C3Fkc1k8grT * fix(hero): correct Pages' listViewThumbnailPath for its polymorphic media relation Same latent bug as BlogPosts' (fixed in 0ba8a9f), found while re-reviewing that fix: Pages' hero.background.media is hasMany:true, so the array's first entry (.media.0) is itself a {relationTo, value} polymorphic upload wrapper, not the resolved media document. The .0 index alone never reached .url/.thumbnailURL, leaving every Page row's admin thumbnail blank. Fixed to .media.0.value, verified against a real seeded page: the old path resolves to undefined, the new path resolves to the real thumbnail URL. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GjuWYPbCQ16C3Fkc1k8grT * fix(og): render the root OG image with real site data via Takumi Replaces the unfinished placeholder (hardcoded fake copy, stock photo, next/og) with real site name/description via the same cached fetchers generateMeta already uses, rendered through takumi-js/response — a drop-in ImageResponse replacement with native Tailwind v4 support. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GjuWYPbCQ16C3Fkc1k8grT * feat(og): add a per-page OG image for Pages Media-backed heroes render the real image; shader-backed heroes render a static CSS gradient approximation (Takumi/Satori cannot execute WebGL). Reuses [slug]/page.tsx's own cached query rather than a second fetch path. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GjuWYPbCQ16C3Fkc1k8grT * feat(og): add a per-post OG image for BlogPosts Same media/shader-aware rendering as Pages' OG image. JSON-LD's image field now points at this route uniformly, working for both media- and shader-backed posts instead of trying to resolve a raw asset URL. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GjuWYPbCQ16C3Fkc1k8grT * feat(og): add an OG image for BlogTopics Simple title-only design — BlogTopics has no hero.background field. Guards RESERVED_TOPIC_SLUGS the same way the sibling page.tsx does. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GjuWYPbCQ16C3Fkc1k8grT * feat(og): add OG images for the /blog index and its pagination Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GjuWYPbCQ16C3Fkc1k8grT * feat(og): add generateMetadata and an OG image for ResumeDocuments This route had no metadata at all before — no title, no OG image. Simple, non-hero design matching BlogTopics' approach. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GjuWYPbCQ16C3Fkc1k8grT * feat(seed): occasionally give seeded Pages/BlogPosts a shader hero ~1 in 4 seeded documents gets a shader background instead of media, deterministic per document (seeded by slug), so the new hero.background shader path is exercised in fixture data without dominating it. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GjuWYPbCQ16C3Fkc1k8grT * chore: fix verification findings Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GjuWYPbCQ16C3Fkc1k8grT * fix: complete generateMeta's openGraph block, guard the shader-preset lookup in BlogListPage Two findings from the final whole-branch review, both in seams no per-task review could see: - generateMeta.ts's openGraph metadata block was left commented out despite the spec explicitly requiring it be completed (relying on Next's auto-discovery of each route's opengraph-image.tsx, per the spec's own guidance, rather than resolving images explicitly). Removed the dead getImageURL reference alongside it. - BlogListPage.tsx's PostCard read SHADER_PRESET_MAP[shaderKey].gradient with no guard, the only one of four SHADER_PRESET_MAP consumers without one -- and the one with the widest blast radius, since an unrecognized key would throw during a server-rendered /blog list page render rather than degrading one card or one admin panel. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GjuWYPbCQ16C3Fkc1k8grT --------- Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.