Develop - #34
Open
danielheene wants to merge 210 commits into
Open
Conversation
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>
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} |
…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
… server' file" This reverts commit 26b37dc.
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.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y7yfWAjDPqBMpCECZPYiLv
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.