Develop - #34
Open
danielheene wants to merge 296 commits into
Open
Conversation
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>
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>
react-pdf font loading no longer depends on process.env.SERVER_URL / HTTP fetch at render time -- fonts are read from local files bundled in each @danielheene/font-pp-* package. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Accidentally committed as an empty file in the previous commit while staging an unrelated font-package migration; restoring the pre-existing config content here. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Deletes web/src/fonts/*.ts and the four duplicated font asset directories under web/public/fonts/ now that every consumer (web Next.js layouts, react-pdf, Storybook) imports fonts from the @danielheene/font-pp-* workspace packages instead. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Task 7's commit accidentally renamed storybook/package.json's name field to @danielheene/storybook, which wasn't requested and breaks .changeset/config.json's fixed version group (which matches packages by their literal "storybook" name). Reverting the name only; the font-pp-frama workspace dependency addition from Task 7 is kept. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Final review flagged these 8 font files were tracked with the executable bit set (100755), inconsistent with the identical-nature files in the other 3 sibling packages (100644) — cosmetic artifact of the original source files' permissions on disk. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…stem - Remove unused ProjectList field components and styles - Remove unused collection/global data fetchers - Remove unused resume CSS customizations - Replace generateDocumentThumbnail task with generateDocumentThumbnails (supports multiple thumbnails) - Update Icon component ref type from IconifyIcon to SVGSVGElement - Update middleware to proxy latest resume document requests - Migrate PDF builders to new fetcher functions - Clean up package.json dependencies and resolutions - Fix formatting in Link component Signed-off-by: Daniel Heene <daniel@heene.io>
- Remove UmamiControlBar widget and date picker modal - Remove all Umami widget components (Stats, PageViews, Events, Paths) - Remove UmamiCharts context provider and related hooks - Clean up widget Signed-off-by: Daniel Heene <daniel@heene.io>
Add a complete testing environment to a previously untested repo: - Vitest with native tsconfig-paths resolution, v8 coverage, and shared mocks (payload via importOriginal spread, next/cache, redis) plus a forced TZ=UTC in vitest.setup.ts - 141 unit tests across 21 colocated *.test.ts files covering lib utilities, date classes, i18n, JSON-LD generators, access control and Payload field hooks (latent quirks pinned with comments, not fixed) - Playwright chromium smoke suite in e2e/ with a committed, format-valid .env.test so the app boots against the zod env gate; runnable locally (auto dev server) and via the version-matched Playwright Docker image with host networking (scripts/e2e-docker.sh) - test/test:watch/test:coverage/test:e2e/test:e2e:ui/test:e2e:docker scripts, a unit-test CI workflow on Node 26, and rewritten testing docs in AGENTS.md/README.md replacing the never-implemented node --test convention Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adopt Next.js Cache Components (cacheComponents: true) manually on
16.2.12:
- migrate the eight unstable_cache fetchers to 'use cache' functions
with cacheTag/cacheLife('max'); revalidate helpers stay on
revalidateTag so existing Payload hooks keep working
- switch the root layout and generateMeta to the cached fetcher
variants
- split page queries into a cached published variant and a dynamic
draft variant branched on draftMode(), replacing the await headers()
force-dynamic hack ([slug], blog/post/[slug], FeaturedTopics)
- guard every generateStaticParams with a '__placeholder__' fallback so
builds against an empty database still validate
- defer /api/service-status to request time via connection() and drop
the runtime segment config from the SSE route (incompatible with
cacheComponents)
Also fixes two pre-existing production build blockers: mediabunny is
now in serverExternalPackages (Turbopack inlined its WASM as a template
string with invalid octal escapes), and the unused WIP
StaticMeshGradient shader is excluded from type checking. All 23 pages
prerender; CMS routes land as Partial Prerender.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…blog topic/post infrastructure - Move video thumbnail extraction from inline afterChange hook to queued GenerateVideoThumbnails task for faster uploads and retry support - Fix ImageMedia aspect-ratio calculation to output ratio (width/height) instead of template string - Add ArticleSidebar component with scroll-spy TableOfContents for blog post navigation - Export VideoMedia component with thumbnail poster and blurred placeholder support - Add blog topic access fix (remove authenticatedOrPublished check that crashed on missing _status field) - Add seed:blog scripts for content generation - Update blog list page with PostCard grid, pagination, and topic filtering - Clean up Link component unused imports and variable names Signed-off-by: Daniel Heene <daniel@heene.io>
Adds a searchable icon picker backed by the project's self-hosted Iconify API, in two forms sharing one panel component: - IconPickerField: a text field storing `prefix:name`, edited through a preview swatch, manual input and a browsable grid - IconPickerFeature: a Lexical toolbar button and /icon slash command inserting an inline icon node The client node is standalone rather than extending the server node: Lexical matches a serialized node's type against the registered class by identity, so two classes sharing the `icon` type break loading saved documents. The insert command lives in its own module for the same reason — importing it from the server node would pull that class into the browser bundle. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The default converters only handle Payload's built-in `media` collection, so this project's `images`/`videos` uploads rendered as nothing. Populated upload values omit `mimeType`, so the node's target collection is used as the discriminator instead. - images render through ImageMedia, videos through VideoMedia with their generated poster frame and blur-up placeholder - links resolve via the custom LinkField shape (nested under `fields.link`, keyed by `type`), which LinkJSXConverter cannot read — hrefs were empty - CodeBlock renders through the existing CodeBlockRenderer - headings emit slugified ids so a table of contents can anchor to them - inline icon nodes from the IconPicker feature Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Resume PDFs and their page thumbnails now live in the existing media collections, leaving this one unused. Its 138 documents were deleted through the Local API so the S3 storage adapter removed the underlying files rather than orphaning them in the bucket. Also drops the commented-out thumbnail block in generateLocalizedResumeDocument, superseded by the GenerateDocumentThumbnails task already used above it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Adds a hidden `document-references` table recording which documents use which, so "is this still referenced?" is one indexed query instead of a per-collection scan whose field paths differ everywhere. Capture happens at the collection level rather than in field hooks: field hooks miss references inside Lexical rich text and never fire on delete. Walking the saved document also means blocks, arrays, tabs and rich text are covered without enumerating paths per entity, and it works before every schema migrates to MediaField. Three reference shapes are handled — polymorphic values, Lexical upload/relationship nodes, and monomorphic relationships, whose bare id carries no type and is resolved through the field config. Tracking covers every collection; delete-guarding defaults to media only, where it replaces a hand-rolled check that scanned one hardcoded field path inside a try/catch that swallowed errors into "no references". Blocked deletes now name the referencing documents and paths. Backfill with `pnpm refs:backfill`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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>
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_01GjuWYPbCQ16C3Fkc1k8grT
…eld tests Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GjuWYPbCQ16C3Fkc1k8grT
…hinery 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_01GjuWYPbCQ16C3Fkc1k8grT
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GjuWYPbCQ16C3Fkc1k8grT
`payload generate:types` adds the new required `linkType` field to `LinkFieldData` and drops `resolvedLabel`/`appearance`, matching the LinkField rewrite from earlier commits. Making `linkType` required in the generated types surfaced two more call sites the plan's file list didn't cover, both writing hand-built LinkFieldData literals that never pass through the CMS: - `src/lib/seed/lexical.ts`'s `link()` seed helper (used by `pages.ts`/`posts.ts`) was missing `linkType` entirely, same gap Task 6 fixed in `pages.ts`'s inline literal. Added `linkType: 'url'` (this helper only ever builds custom-URL links) and updated its doc comment, which described the field shape before this refactor. - `src/components/Footer/Footer.tsx`'s three social-link literals (email/telephone/sameAs) all set `url`, never `reference` — added `linkType: 'url'` to each. - `src/components/Link/index.test.tsx` (added in an earlier commit) needed `linkType="url"` on its two CMSLink usages. Updated `lexical.test.ts`'s shape assertion to include `linkType`. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GjuWYPbCQ16C3Fkc1k8grT
…restore iconOnly description Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GjuWYPbCQ16C3Fkc1k8grT
…ch resolveLinkTarget The final-review fix pass added a reference-before-url precedence check using bare truthiness (`if (siblingData?.reference)`), but resolveLinkTarget (the function that actually decides the rendered href) gates on a fully populated reference: `reference?.relationTo && reference.value`. These diverged on a half-populated reference alongside a populated url — the doc comment's claim that the inferred mode "always agrees with which target the frontend actually renders" wasn't literally true for that case. Narrow the reference check to the same relationTo-and-value gate, and add a test proving a half-populated reference correctly falls through to 'url' inference rather than getting stuck in an empty reference-mode UI. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GjuWYPbCQ16C3Fkc1k8grT
…t nested `fields.link`
`RichText/index.tsx`'s `link` converter override read `node.fields.link` —
a nested sub-key that has never existed for this project's link nodes.
`LinkFeature({ fields: [...LinkField().fields] })` spreads `LinkField`'s
*inner* fields directly onto the node's `fields` object (lexical's
`transformExtraFields` replaces its base fields with these rather than
merging them under a key), so `fields` was always the flat
`{ linkType, reference, url, newTab, iconBefore, label, iconAfter,
iconOnly }` shape — never `{ link: {...} }`.
The bug predates the LinkField simplification: `creditsRendering.test.tsx`
already documented it as "a pre-existing bug in shared rendering
infrastructure" it deliberately didn't assert against. Every link in every
rich text field — human-authored ones included — silently rendered as
plain text instead of an anchor.
Extracted the converter into its own module, `linkConverter.tsx`, reading
`node.fields` directly through the same `resolveLinkTarget` helper CMSLink
uses. Splitting it out (rather than testing through `RichText/index.tsx`)
sidesteps that module's `blocks.CodeBlock` converter, which transitively
imports `@payloadcms/ui`'s `CopyIcon` — a `.scss` import Vitest's Node
transform can't load — the same class of bundler-only-import problem
`src/fields/Icon/index.ts`'s module comment describes.
Added `linkConverter.test.tsx`, driving Payload's real
`convertLexicalNodesToJSX` + `defaultJSXConverters` over hand-built link
nodes in the actual flat shape, proving a real `<a>` renders (with and
without `newTab`) and that an unresolvable target still falls back to bare
children. Updated `creditsRendering.test.tsx`'s scope-note comment, which
had documented the bug as deliberately unfixed.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GjuWYPbCQ16C3Fkc1k8grT
Picks up the JSDoc comments payload generate:types derives from admin.description on iconOnly and label (restored in caa0fcd) — these weren't present in the types committed alongside that fix. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GjuWYPbCQ16C3Fkc1k8grT
Removed outdated `service-status` API route, unused migrations, and redundant `ScheduledResumeGenerationWidget` alongside its helper modules and tests. Deleted unused `ChecksumValidator` export. Cleaned up `package.json` scripts and updated dependencies. Simplified codebase by consolidating link field migration naming tests. Signed-off-by: Daniel Heene <daniel@heene.io>
…ils and config Each shader component is now in its own self-contained folder: src/components/Shaders/ ├── DarkVeil/ │ ├── DarkVeil.tsx (component — moved) │ ├── DarkVeil.stories.tsx (moved) │ ├── config.ts (new: curated hero defaultProps) │ ├── thumbnail.png (new: static placeholder — replace with screenshot) │ └── index.ts (re-exports component, types, defaultProps) ├── FaultyTerminal/ (same structure) ├── GradientBlinds/ (same structure) └── Grainient/ (same structure) shaderPresets.ts: replace gradient string with statically imported StaticImageData thumbnail — enables pixel-accurate previews in OG images and blog list cards without any WebGL context. ShaderHeroBackground.tsx: import defaultProps from each shader's config.ts instead of inline bundles. shaderComponents.ts (admin picker): use defaultProps from config.ts for DarkVeil/GradientBlinds/Grainient; keep FaultyTerminal's admin preview props (green tint, low intensity) inline with a comment. opengraph-image.tsx (pages + posts): render thumbnail as <img> instead of CSS background gradient. BlogListPage.tsx: render thumbnail via next/image <Image> instead of a styled div with a CSS gradient background. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019J1SZPc1FVHu11Gsypd3SL
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.