Skip to content

Develop - #34

Open
danielheene wants to merge 210 commits into
mainfrom
develop
Open

Develop#34
danielheene wants to merge 210 commits into
mainfrom
develop

Conversation

@danielheene

Copy link
Copy Markdown
Owner

No description provided.

danielheene and others added 30 commits May 5, 2025 10:16
Add detailed documentation for Claude Code to understand the project
architecture, development workflow, and key implementation details.

Includes:
- Development commands and environment setup
- Dual layout system (frontend/admin) architecture
- Payload CMS structure (collections, globals, blocks, fields)
- Image processing pipeline with hooks
- Path aliases and custom admin components
- Type generation workflow
- Special features (nested docs, SEO, blur hash, SVG adaptation)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Implement JSON-LD schemas across the site using schema-dts for type safety:
- Add Person and WebSite schemas to global layout
- Add BlogPosting schema with breadcrumbs to blog posts
- Add CollectionPage schemas to category and tag pages
- Create reusable utility functions for schema generation

This enhances SEO by providing rich snippets for search engines.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
# Conflicts:
#	app/(frontend)/posts/[slug]/page.tsx
#	app/(frontend)/tags/[slug]/page.tsx
#	app/(payload)/admin/importMap.js
#	package.json
#	pnpm-lock.yaml
#	src/blocks/ResumeExperienceBlock/Renderer/index.tsx
#	src/components/Badge/Badge.tsx
#	src/components/Header/Header.tsx
#	src/contexts/UmamiCharts/UmamiCharts.context.ts
#	src/contexts/UmamiCharts/UmamiCharts.types.ts
#	src/contexts/UmamiCharts/UmamiChartsProvider.tsx
#	src/styles/frontend.css
# Conflicts:
#	.github/dependabot.yml
#	package.json
#	pnpm-lock.yaml
Signed-off-by: Daniel Heene <daniel@heene.io>
…nt generation

Signed-off-by: Daniel Heene <daniel@heene.io>
…ume document handling

Signed-off-by: Daniel Heene <daniel@heene.io>
…ganization

Signed-off-by: Daniel Heene <daniel@heene.io>
…types for improved type safety

Signed-off-by: Daniel Heene <daniel@heene.io>
…atic params

Signed-off-by: Daniel Heene <daniel@heene.io>
… assets

- Replace bun with pnpm (add pnpm-lock.yaml, pnpm-workspace.yaml; remove bun.lock)
- Add resume document page and API route under app/(frontend)/resume/document/
- Add new favicon assets (apple-touch-icon, 96/192/512px PNGs)
- Update next.config, payload.config, biome.json, and various app layouts/pages

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…filters, and improved error handling

- Refactor `renderTemplate` to async with structured error handling and return types
- Add proxy-based data injection for user settings, server URLs, and dynamic values
- Implement `nanoid` dynamic generation with configurable length
- Simplify type guards with `isMediaObject` base function
- Switch AI models from Claude Sonnet 4.6 to Claude Haiku 4.5
- Remove cached versions of data fetchers in favor of direct calls
- Update imports to use centralized fetcher functions
- Fix `generateContentURL` to use `SERVER_URL` environment variable
- Add Radix UI components for enhanced UI elements
- Update dependencies including Payload CMS, Next.js, AI SDK, and GraphQL

Signed-off-by: Daniel Heene <daniel@heene.io>
Captures the plan to extract the Next.js + Payload app from the workspace
root into its own web/ package, keep env loading centralized at the repo
root via @next/env, and route dev/build through the already-present
Turborepo config.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
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>
danielheene and others added 5 commits August 14, 2026 08:22
Biome's useLiteralKeys flagged the bracket-notation access this task's
own Task 10 commit introduced. No behavior change.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0117L74bir5CML47UEocBY6U
…build

renderLinkLabel's afterRead hook renders {title} via renderTemplateCore,
which fetches global settings for {siteName} etc. SiteSettings itself
holds LinkField footer entries, so reading it re-triggers
renderLinkLabel on each one — and fetchSiteSettings/fetchGlobalUserSettings
take no req, so the request-scoped in-flight cache in loadTemplateGlobals
never sees these nested, req-less calls and can't short-circuit them.
The result was unbounded recursion: every next build prerender pass hit
Next's own USE_CACHE_TIMEOUT on whichever fetcher's chain happened to
touch SiteSettings first (fetchSiteSettingsCached, fetchResumeProjectsCached,
the root layout, ...), rotating across pages between runs.

Root-caused via payload run probes with a full backup/patch/restore
cycle against the real SiteSettings document (verified byte-identical
afterward) and a --prof CPU profile that ruled out I/O wait (0 active
requests, CPU pinned) before finding the actual recursive call chain.

Fixed with a process-wide in-flight guard in loadTemplateGlobals: a
nested attempt to fetch globals while one is already in flight anywhere
in the process degrades to an empty TemplateGlobals object instead of
recursing. A blank {siteName} substitution nested this deeply is an
acceptable trade for a build that terminates.

Also makes the '@payload-config' import inside renderTemplateCore lazy
(dynamic import inside the fallback branch, not a top-of-module import):
this module is reached via LinkField's lazily-imported afterRead hook,
which is itself embedded in payload.config.ts's own module graph
(RichText -> LinkFeature -> LinkField), so a static import here would
resolve the config from inside its own still-evaluating module graph
on every link read.

Verified: pnpm run build no longer produces any USE_CACHE_TIMEOUT
errors (previously failed on every run); the build now progresses past
the point where LinkField-bearing pages generate and fails later on an
unrelated, pre-existing access-control bug in the auto-translate
branch's in-flight work (app/(frontend)/resume/[slug]/page.tsx, not
part of this change — confirmed via git diff that file predates this
commit and is untouched by it).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0117L74bir5CML47UEocBY6U
… a link

TARGET_COLLECTIONS only listed pages/posts/topics/globals. Any
RichTextField whose editorVariant builds on captionFeatures or
markdownFeatures also spreads LinkFeature (src/fields/RichText/index.ts)
and can hold a lexical link node — that's ResumeProjects (markdown) and
the four Media collections (caption), none of which were covered.
ResumeJobs/ResumeSkills use the inline variant, which stops short of
LinkFeature, so they're correctly still absent.

Found via a dry-run against the dev database (backed up each newly-added
collection first, diffed after): exactly one legacy link node, inside
ResumeProjects' description, needed rewriting — type dropped, url/label
preserved. Everything else across all five newly-covered collections was
already clean. Diffed every collection against its pre-migration backup
to confirm only that one document changed; backups dropped after
verification. 511/511 tests still pass, tsc clean.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0117L74bir5CML47UEocBY6U
- Fix COLLECTION_PREFIX_MAP for BlogPosts: 'posts' -> 'blog/post' to match
  the actual live route app/(frontend)/blog/post/[slug]/page.tsx. The old
  prefix produced /posts/<slug> URLs that don't exist, which would have
  broken the sitemap, generateContentURL previews, and live-preview links
  for every blog post.
- Uncomment the BlogPosts and BlogTopics blocks in app/sitemap.ts so /blog,
  /blog/post/<slug>, and /blog/<topic> are included alongside Pages.
- Skip the draft: false query option for BlogTopics in getCollectionData,
  since that collection has no drafts/versions system and querying with
  draft: false is unsupported there (per the existing warning comment in
  src/collections/BlogTopics/index.ts).
- Update generateContentPath/generateContentURL/generatePreviewPath test
  fixtures to expect the corrected /blog/post/... paths.

ResumeDocuments (/resume/[slug]) intentionally stays out of the sitemap:
that collection requires authenticated read access and holds
per-job-application generated resume variants, not general public content.
Making it crawlable would require a separate access-control decision.
Cherry-picked from develop-merge (97450c6) with the old-generation
payload.config.ts / src/types/payload.ts hunks dropped — that branch
predates the current admin config and those files no longer apply.
Dashboard registration for the widget still needs to be re-wired into
the current src/payload.config.ts.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BicpURmrvbTiCAU9pK7Wfr
… header

GenerateLocalizedResumeDocument was crashing with 'Cannot read
properties of null (reading '\''url'\'')' whenever GlobalUserSettings.
portrait.regular.value resolved to null — e.g. the portrait upload
isn'\''t set in the admin, or points at a deleted MediaImages doc.

portraitUrl is required by the PDF schema (z.url(), non-optional), so
this isn'\''t safe to silently default — surface it as a clear,
actionable error instead of an opaque property-access crash deep in
the Payload jobs queue.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BicpURmrvbTiCAU9pK7Wfr
poster={item.poster ?? undefined}
preload={priority ? 'auto' : 'metadata'}
ref={videoRef}
src={item.url}
y="0"
preserveAspectRatio="none"
filter="url(#blur)"
href={blurDataURL}
poster={item.poster ?? undefined}
preload={priority ? 'auto' : 'metadata'}
ref={videoRef}
src={item.url}
y="0"
preserveAspectRatio="none"
filter="url(#blur)"
href={blurDataURL}
danielheene and others added 22 commits August 14, 2026 20:08
…ashes

Payload's TaskError wrapper only preserves err.message when re-throwing
a task handler's error, discarding the original stack. That's why the
'Cannot read properties of null (reading url)' crash keeps showing the
same unhelpful top frame (getRunTaskFunction.js:83) no matter what —
none of the application code paths this task touches have an unguarded
.url read left (checked buildDocumentHeader, buildResumeDocumentData,
buildSkillSections, buildWorkExperienceSection, resolveRelation, and
@react-pdf/image's resolver).

Wraps the handler body in try/catch and logs err.stack via
payload.logger.error before rethrowing, so the real crash site survives
into the job's logger output on the next run. Temporary — remove once
the actual crash site is found.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BicpURmrvbTiCAU9pK7Wfr
generateLocalizedResumeDocument's four inline steps (filename, resume
data, render+upload, thumbnails) become their own top-level Payload
tasks — GenerateResumeFilename, BuildLocalizedResumeData,
GenerateResumeFile (render+upload combined, so the PDF buffer never
leaves that task's own scope — no task passes a blob through its
input/output, which Payload persists as JSON) and the existing
GenerateDocumentThumbnails.

generateLocalizedResumeDocument is now a thin orchestrator calling
tasks.X() for each step, mirroring the outer generateResumeDocument
workflow's own pattern. Each step is wrapped so a failure logs which
step failed plus the raw stack (payload.logger.error) before
rethrowing — Payload's TaskError wrapper only preserves err.message
even for tasks called via tasks.X(), so this is still needed to
actually find the 'Cannot read properties of null (reading url)' crash.

Hand-extended src/types/payload.ts with the three new task interfaces
since generate:types needs a live DB connection this worktree doesn't
have — verified the shape against the existing generated entries.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BicpURmrvbTiCAU9pK7Wfr
Replaces the earlier ad-hoc payload.logger.error(err.stack) diagnostic
with a structural fix: withTaskObservability(taskConfig) wraps a task's
handler and is applied once, to every entry in TASKS (src/jobs-queue/
tasks/index.ts) via .map() — a task cannot be registered without it,
unlike a per-task try/catch that's easy to forget on a new file.

Each task execution runs inside Sentry.startSpan (op: 'queue.task',
named after the task slug, tagged with job id / workflow slug), and a
thrown error is sent via Sentry.captureException — with the real stack,
task slug, job id and input — before Payload's TaskError wrapper gets a
chance to swallow the stack down to just err.message.

Verified this covers both execution paths: a task run as a top-level
job (cron/queue-triggered) and a task invoked via tasks.X() from
another task's handler both resolve through the same wrapped entries
in payload.config.ts's jobs.tasks (traced through Payload's own
getRunTaskFunction.js — both paths look up the task by slug from
req.payload.config.jobs.tasks, which is TASKS).

generateLocalizedResumeDocument.tsx drops its per-step runStep
diagnostic now that the wrapper covers every tasks.X() call
automatically.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BicpURmrvbTiCAU9pK7Wfr
'queue.process' is Sentry's convention for message-queue consumers,
which Payload's jobs queue isn't — 'task' is the documented op for
background/scheduled work (per skills.sentry.dev/instrument's Next.js
tracing reference). No behavior change, just a semantic-convention fix
so the span shows up correctly categorized in Sentry's UI.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BicpURmrvbTiCAU9pK7Wfr
… file

Fixes WEBSITE-3M — 'A "use server" file can only export async
functions, found object.'

src/lib/fetchAnthropicTranslation.ts has 'use server' at the top, but
was also exporting BilingualLanguage (a type — fine, stripped at
compile time) and BILINGUAL_LANGUAGE_LABEL (a const object — not fine,
Next.js requires every runtime export from a 'use server' file to be an
async function). TranslateControls.tsx, a client component, imported
both directly from that file, which is exactly the pattern that trips
this build-time check on the admin's POST /admin/[[...segments]] path.

Moves the type + label map into a plain src/lib/bilingualLanguage.ts
module with no 'use server' directive. fetchAnthropicTranslation.ts now
imports the label map from there instead of defining it; the server
action itself (the one thing that should stay in the 'use server' file)
is unchanged.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BicpURmrvbTiCAU9pK7Wfr
Applies the same pattern used for generateLocalizedResumeDocument to the
outer workflow: GenerateDocumentTitle, GenerateDocumentSlug and
CreateResumeDocument become their own top-level tasks
(generateResumeDocumentTitle.ts, generateResumeDocumentSlug.ts,
createResumeDocument.ts) instead of inlineTask blocks — each gets its
own Sentry span/error context via withJobObservability, matching every
other step in the pipeline.

Renames withTaskObservability -> withJobObservability and generalizes
it to wrap WorkflowConfig as well as TaskConfig — WORKFLOWS
(src/jobs-queue/workflows/index.ts) is now wrapped the same way TASKS
is, so the workflow's own top-level span/error is captured directly,
not just its nested tasks.X() calls. Span naming distinguishes the two
(job.task/<slug> vs job.workflow/<slug>) via a job.kind attribute.

Behavior note: CreateResumeDocument's error handling changed from
catch-and-continue (silently returning "success: false" on a failed
payload.create, hiding the failure from Sentry entirely) to
throw-and-retry — consistent with every other step, and the whole
point of this observability work is that failures should be visible
rather than swallowed.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BicpURmrvbTiCAU9pK7Wfr
Two stray imports — storybook/manager-api's controlOrMetaKey (unused)
and tailwind-variants' cn (wrong package, silently type-compatible with
the call site so tsc never caught it) — replaced with @/lib/cn, the
twMerge-backed helper already used correctly everywhere else in this
feature (TranslateControls.tsx).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BicpURmrvbTiCAU9pK7Wfr
… shape

The field's fields array has been wrapped in a single row (for the
responsive column/row CSS toggle added after the original plan) since
this test was last touched, and TranslateControls' component
registration changed from a bare path string to { path, clientProps:
{ layout } } so the layout prop reaches the client component. Also
adds coverage for the enqueueAutoTranslate afterChange hook wiring,
which had no test at all.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BicpURmrvbTiCAU9pK7Wfr
…nguageLabel

Shared en/de language enum + label map for the bilingual-translation
feature, kept separate from Payload's own Locale (also 'en'|'de') per
the naming discussion: Locale is Payload's own vocabulary for its
localization config and shouldn't be repurposed for a two-language
feature that predates and is unrelated to any future locale expansion.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BicpURmrvbTiCAU9pK7Wfr
bilingualTranslateChannel(jobId) / isBilingualTranslateChannel are a
second channel-naming scheme alongside the existing fixed-slug
SSE_CHANNELS allowlist — one channel per queued translation job rather
than one fixed public channel, so route.ts additionally requires a
signed-in session before subscribing to one (translated CV content
isn't public, unlike the existing service-status channel).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BicpURmrvbTiCAU9pK7Wfr
fetchAnthropicTranslation previously defined BilingualLanguage/
LanguageLabel inline; now imports BilingualLanguageValue/
BilingualLanguageLabel from the shared module (previous commit) so
every other consumer (the task, the enqueue action, the enqueue hook)
uses the same names instead of each redefining its own copy.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BicpURmrvbTiCAU9pK7Wfr
Two modes selected by input.mode: 'auto' (queued by the save-time
enqueueAutoTranslate hook, patches the document directly via
payload.update once translation completes) and 'manual' (queued by a
translate-button click, publishes progress/result over the job's SSE
channel instead of touching the document — the browser tab is open and
applies the result client-side). Registered in TASKS so it's covered
by withTaskObservability's Sentry span/exception wrapper like every
other task.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BicpURmrvbTiCAU9pK7Wfr
Queues an AutoTranslateBilingualField job in 'manual' mode and
triggers it immediately via next/server's after() rather than waiting
for the next autoRun poll, so a translate-button click doesn't sit
idle for up to the poll interval. Returns just the job id; the caller
opens an SSE subscription on bilingualTranslateChannel(jobId) to watch
it run.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BicpURmrvbTiCAU9pK7Wfr
Group-field afterChange hook: after a save that leaves one language
side populated and the other newly empty (both now and on the
previous save — the non-destructive guard from the design spec),
queues AutoTranslateBilingualField in 'auto' mode for that direction.
context.skipAutoTranslate is the escape hatch the task's own writeback
sets, matching this repo's skipGenerateResumeDocumentHook /
skipRevalidate pattern so the writeback doesn't re-trigger itself.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BicpURmrvbTiCAU9pK7Wfr
The manual translate buttons now call enqueueBilingualTranslation and
track the returned job id through a bilingualTranslateChannel SSE
subscription instead of calling fetchAnthropicTranslation directly —
progress and the final result both arrive as toast updates, and a
confirmation modal (replacing window.confirm) still gates overwriting
existing target-language content. ResumeJobs' Tasks field already used
BilingualRichTextField before this session; no change needed there
beyond confirming it still resolves correctly against the rewritten
component.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BicpURmrvbTiCAU9pK7Wfr
…ncurrency risks

Final whole-branch review found two Important issues in the task's
'auto' mode writeback:

- Both findByID calls now request depth: 0. The task re-submits its own
  read through payload.update's data — without depth: 0, a
  depth-populated relationship comes back as an object, and feeding that
  back through update is a known Payload footgun (validation failure, or
  silently writing a nested object where an ID belongs). ResumeJobs has
  relationship fields, so this was reachable. Added a test assertion
  that both findByID calls request depth: 0.

- concurrency now sets exclusive: true, matching every other task in
  this repo (generateDocumentThumbnails, generateVideoThumbnails,
  generateLocalizedResumeDocument). Without it, two jobs sharing the
  same concurrency key could run concurrently instead of one waiting for
  the other — the exact window that would let a superseded job's stale
  translation land after a newer one's.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BicpURmrvbTiCAU9pK7Wfr
…BilingualField's task slug

src/types/payload.ts had drifted ahead of what was committed for this
worktree (includes ScheduledResumeGenerationWidgetWidget and the
ResumeSkills name->content/title rename from other in-progress work
this session, plus the AutoTranslateBilingualField task's generated
type entries). Registers TaskSlug.AutoTranslateBilingualField
(src/types/jobs-queue.ts) to match, and fixes the one stale
'queue.task' -> 'task' op assertion left over from the Sentry
semantic-convention fix earlier this session.

Committing this before merging origin/develop's task-splitting work
(withTaskObservability -> withJobObservability rename) so both sides
are real commits git can 3-way merge, rather than leaving this as
uncommitted state that would conflict.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BicpURmrvbTiCAU9pK7Wfr
# Conflicts:
#	src/jobs-queue/lib/withTaskObservability.test.ts
#	src/jobs-queue/tasks/index.ts
#	src/types/payload.ts
- Fix COLLECTION_PREFIX_MAP for BlogPosts: 'posts' -> 'blog/post' to match
  the actual live route app/(frontend)/blog/post/[slug]/page.tsx. The old
  prefix produced /posts/<slug> URLs that don't exist, which would have
  broken the sitemap, generateContentURL previews, and live-preview links
  for every blog post.
- Uncomment the BlogPosts and BlogTopics blocks in app/sitemap.ts so /blog,
  /blog/post/<slug>, and /blog/<topic> are included alongside Pages.
- Skip the draft: false query option for BlogTopics in getCollectionData,
  since that collection has no drafts/versions system and querying with
  draft: false is unsupported there (per the existing warning comment in
  src/collections/BlogTopics/index.ts).
- Update generateContentPath/generateContentURL/generatePreviewPath test
  fixtures to expect the corrected /blog/post/... paths.

ResumeDocuments (/resume/[slug]) intentionally stays out of the sitemap:
that collection requires authenticated read access and holds
per-job-application generated resume variants, not general public content.
Making it crawlable would require a separate access-control decision.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants