Develop - #34
Open
danielheene wants to merge 249 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>
Add root-level *.test.{ts,tsx} to test.include so instrumentation-client.test.ts
(a Next.js convention file that must live at repo root) is picked up by
pnpm test / vitest run. Previously test.include only matched src/**, so this
test silently never ran.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H7VPuMQy7gfpFq7xWKWuqh
…void ad-blockers Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H7VPuMQy7gfpFq7xWKWuqh
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H7VPuMQy7gfpFq7xWKWuqh
…sessions Add isTrackingSuppressed() which verifies the payload-token cookie's JWT (HS256, PAYLOAD_SECRET) via jose and checks the enableOwnTracking claim, without a payload.auth() DB roundtrip. Suppresses tracking for the site owner's own session by default; anonymous visitors and invalid/expired tokens are never suppressed. Wires the guard into trackServerEvent. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H7VPuMQy7gfpFq7xWKWuqh
…Payload sessions Adds UmamiSuppressionFlag, an isolated async server component that calls isTrackingSuppressed() and renders an inline script setting window.__UMAMI_SUPPRESSED__ = true when the current authenticated Payload session should be excluded from analytics. Wrapped in its own Suspense boundary in RootLayout, mirroring DraftModeListener, so the cookies() read inside isTrackingSuppressed() doesn't block static prerendering under cacheComponents. track() now checks window.__UMAMI_SUPPRESSED__ before isTrackingAllowed() and no-ops without building/sending a payload when set.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H7VPuMQy7gfpFq7xWKWuqh
Trending blog post metrics were cached with the same 2h TTL as the admin Umami widget. Per REQ-007, trending needs to reflect more recent popularity shifts, so use a 30-60 minute window instead — 45 minutes via date-fns minutesToSeconds, matching the hoursToSeconds convention already used in UmamiWidget.data.ts. Cache key format (raw metrics URL, limit-exclusive) is unchanged from Task 14 — that's intentional since limit is applied post-cache-read. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H7VPuMQy7gfpFq7xWKWuqh
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H7VPuMQy7gfpFq7xWKWuqh
Add the Payload block schema for the trending blog posts feature: an optional bilingual heading (en/de) and a postCount number field (default 4, min 1, max 10) controlling how many trending posts the renderer will display. Registers BlockSlug.TrendingBlogPosts in the shared block slug enum. Not yet wired into blockComponentMap, RenderBlocks, or any collection's allowed-blocks array — no renderer exists yet. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H7VPuMQy7gfpFq7xWKWuqh
- Async server component fetches trending posts via fetchTrendingBlogPosts (7-day window), renders a teaser card grid linking to /blog/post/[slug] - Renders null when the fetcher returns null or an empty array (TASK-020) - Optional bilingual heading resolved with reduceDataToLocale, defaulting to 'en'; omitted when empty on both locales - TrendingBlogPostsBlock payload-types generation hasn't run yet, so props are typed locally against the block config's fields Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H7VPuMQy7gfpFq7xWKWuqh
…derer map Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H7VPuMQy7gfpFq7xWKWuqh
Fixes two bugs found in a final whole-branch review before merge: 1. instrumentation-client.ts's onRouterTransitionStart fired track() bare on client-side route transitions, which derives its URL from location.pathname/search internally. Since onRouterTransitionStart fires BEFORE location updates to the destination, this recorded the page being left, not the destination href. It also never fired on initial page load, missing REQ-004's parity with Umami's data-auto-track behavior. Added a new trackPageview(url?) export in src/lib/umami/track.ts that accepts an explicit destination URL, falling back to location.pathname + location.search when omitted (preserving prior bare-track() pageview behavior for the initial-load case). Shared suppression/allow-list guard and browser-field population factored into small private helpers used by both track() and trackPageview(). instrumentation-client.ts now calls trackPageview(href) on route transitions and fires a bare trackPageview() as a module-level side effect for the initial page load. 2. TrendingBlogPostsBlock's Renderer rendered <ImageMedia> whenever heroImage.value resolved to an object, without checking heroImage.url was truthy. ImageMedia throws on a relative URL string passed to `new URL()`, and this block renders inside a Suspense boundary with no error boundary in RenderBlocks.tsx, so a relative/missing URL could crash the whole page render. Guarded on heroImage?.url, matching the precedent in BlogListPage.tsx, and added the same blurDataURL/sizes props that precedent uses. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H7VPuMQy7gfpFq7xWKWuqh
- Replace `Locale` with `BilingualLanguage` across all related functions, types, and components to support dual-language workflows. - Implement `reduceDataToBilingualLanguage` method for resolving relations and processing localized data. - Update CSS class naming conventions for consistency (e.g., `.Container` -> `.duo-tone`). - Add `ChecksumValidator` for validating document checksums, including drag-and-drop and file upload support. - Introduce Scheduled Resume Generation Widget and enqueue hooks for automated translation tasks. - Remove unused `authenticated` and `forbidden` access utilities. Signed-off-by: Daniel Heene <daniel@heene.io>
…rrorBoundary - Remove NEXT_PUBLIC_UMAMI_DO_NOT_TRACK/NEXT_PUBLIC_UMAMI_DOMAINS env vars and their config-reading logic from track()/trackServerEvent() — not currently needed, and their presence implied a capability nothing wired up to configure. - Add a general-purpose ErrorBoundary component (src/components/ErrorBoundary) and wrap each rendered block in RenderBlocks.tsx with it: CMS-authored blocks fetch live external data (Umami, Payload), so one failing block must not take down the whole page. Reports to Sentry, renders nothing on catch. - Extend src/lib/typeGuards.ts with isRenderableImage/isRenderableImageRelation, narrowing a MediaImage (or its upload-relation wrapper) to one with a populated, non-empty url — replacing an ad-hoc inline guard in TrendingBlogPostsBlock's Renderer with a typed, reusable check. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H7VPuMQy7gfpFq7xWKWuqh
…gualLanguage develop's i18n refactor (8c60f03) renamed reduceDataToLocale to reduceDataToBilingualLanguage while this branch was in flight; this was the only remaining call site using the old name post-merge. Also commits the regenerated payload-types.ts reflecting TrendingBlogPostsBlock/ enableOwnTracking, produced by the merge's dependency install. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H7VPuMQy7gfpFq7xWKWuqh
Remove the MarkdownDescription server component and its react-markdown dependency. All group fields that used it now use SectionGroupField, which already wraps a local copy of the description component. That local DescriptionComponent no longer renders markdown via ReactMarkdown; instead it splits the description on newlines and joins them with <br /> tags, making it compatible with dedent-authored strings. Non-group fields that referenced MarkdownDescription (Template field, the sameAs array field) now point directly at the local DescriptionComponent instead.
Adds React Email templates for the contact form, themed with a Tailwind config derived from the site's design tokens instead of the site's default Tailwind setup (which is CSS-first and unreadable by @react-email/tailwind's inline-style compiler). - src/emails/theme.ts: brand colors converted from oklch() to hex (email clients, notably Outlook desktop, don't render oklch()), the neutral scale, the font's real fallback stack, and border-radius scale, kept in sync with src/styles/theme/*.css. - src/emails/components/EmailLayout.tsx: shared header/footer chrome. - ContactFormNotificationEmail: sent to the site owner, reply-to set to the submitter. - ContactFormConfirmationEmail: copy back to the form author, reply-to set to the owner so a reply reaches them directly. - render.ts: renders a template to the html/text pair payload.sendEmail expects. submitContactForm now renders and sends both emails; a failure sending the (non-critical) confirmation copy is logged but doesn't turn a successful submission into an error. Kept the individual @react-email/* packages (components/render/tailwind) for the runtime import path rather than the unified `react-email` package, since the latter pulls prismjs/marked/esbuild/tailwindcss into the server bundle via top-level imports (resend/react-email#3556); `react-email` stays a devDependency for the `email:dev` preview CLI only, with @react-email/ui for its preview UI. Also added SERVER_HOST to vitest.setup.ts's deterministic test env, alongside the existing SERVER_URL default.
Component stories (with Controls generated from each component's props): - Separator, Skeleton, Tooltip, ButtonGroup, Headline, ThemeToggleIconButton - DarkVeil, FaultyTerminal, GradientBlinds, Grainient — args and control ranges matched against reactbits.dev's own published control-panel data for each component; previewed at 16:9 via a shared aspect-ratio decorator New Foundation pages, auditing every custom @theme block in src/styles/theme: - Radii, Effects (shadows/text-shadows/aspect ratios), Motion (animations) - Retitled Typography.mdx and Icons.mdx to Foundation/* so all foundation pages group together in the sidebar Shaders now has its own top-level sidebar section (Overview + the four component stories) instead of nesting under Components. Fixes found and applied along the way: - Shaders/index.ts re-exported DarkVeil/FaultyTerminal/GradientBlinds/ Grainient via `export * from` although all four are default exports, so nothing was actually re-exported; switched to named re-exports and exported DarkVeilProps/GrainientProps for consistency - Grainient.tsx and GradientBlinds.tsx imported sibling .css files that don't exist in the repo, hard-crashing any build that touched them; removed the dead imports - GradientBlinds' mixBlendMode:'lighten' default washes out against a light background — its story/overview panels get a dark backdrop New shared story helper: src/stories/components/Specimen.tsx (SpecimenItem/SpecimenGallery), mirroring the existing ColorItem/IconItem pattern, used across Radii/Effects/Motion. FaultyTerminal renders blank in this Storybook environment even with reactbits.dev's own wrapper markup and demo props reproduced exactly; confirmed the component source is unmodified from upstream, so this is noted as a known environment gap in Shaders.mdx rather than worked around.
docs/ is already gitignored; remove the previously-tracked files from the index so it matches plan/ and stays local-only. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H7VPuMQy7gfpFq7xWKWuqh
Repository-orientation file for AI coding agents per the llms.txt spec (llmstxt.org) — points to AGENTS.md, README.md, key config, source layout, and in-progress plans. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H7VPuMQy7gfpFq7xWKWuqh
- SectionGroupField defaulted name to undefined, but Payload's fieldAffectsData checks `'name' in field` rather than truthiness — a present-but-undefined key still marked every nameless group as data-affecting, so multiple nameless SectionGroupField() usages at the same level collided as duplicate fields named "undefined" and crashed Payload's config sanitizer on init. name is now omitted entirely when not provided. - fetchLatestResumeDocumentCore returns null instead of crashing via JSON.parse(undefined) when no résumé document exists yet. - proxy.ts and ResumeDownloadsBlockRenderer handle that null case (404 / render nothing) instead of destructuring blindly. - proxy.ts: removed the legacy ?page=<n> -> /page/<n> redirect block; that logic was never deployed.
Merges origin/claude/react-email-tailwind-setup-r2p4af into develop. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GjuWYPbCQ16C3Fkc1k8grT
@react-email/components, @react-email/render, and @react-email/tailwind were all deprecated upstream (Resend collapsed the scoped-package split into the single react-email package, which now re-exports render, components, and Tailwind together). Every individual component package (@react-email/heading, @react-email/text, etc.) was deprecated too, so there's no narrower successor to move to. Point every import in src/emails/ at 'react-email' instead. Move react-email from devDependencies to dependencies since it's now on the submitContactForm request path, not just used by the email:dev preview CLI. Note: this reintroduces the bundle-size tradeoff the original implementation deliberately avoided (resend/react-email#3556) — the unified package transitively pulls in prismjs/marked/tailwindcss, which now ride along in the server bundle for submitContactForm. No smaller alternative exists upstream anymore. Verified: tsc --noEmit shows zero errors in src/emails/ (11 remaining errors elsewhere are pre-existing payload.ts generated-type drift, present before this change); submitContactForm.test.ts 7/7 passing; manually rendered ContactFormNotificationEmail end-to-end to confirm HTML/text output and Tailwind color inlining still work. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GjuWYPbCQ16C3Fkc1k8grT
The production build's TypeScript check failed: CMSLink's prop type
(and every LinkFieldData consumer — Footer, Header, LinkGroupBlock,
RichText's link converter) is structurally self-referential. Payload's
generated LinkFieldData.reference.value types the populated relation
as the *full* Page/BlogPostData/Topic document, and Page.content
recursively contains blocks that embed another LinkFieldData. TS
produces two expansions of that cycle at different nesting depths and
rejects them as incompatible — this is a real limitation of generating
flat interfaces for a self-referential relationship graph, not stale
generated types (confirmed: payload generate:types recompiles to
byte-identical output and no-ops, per its own diff-and-skip logic).
Nothing downstream actually reads past id/title/slug on a populated
reference (CMSLink only needs slug for the href and title for
{title} substitution). Added LinkReferenceDocument/LinkFieldDataLean/
LinkGroupEntry in resolveLinkTarget.ts — the field's own lib, so the
lean shape lives next to the code that already narrows this exact
value — and swapped every consumer from the raw generated LinkFieldData
to the lean equivalent. The Payload field config and stored data are
unchanged; only how the codebase types reads of them.
Also regenerated app/(payload)/admin/importMap.js, stale since
7a9a653 removed MarkdownDescription — an unrelated but real build
blocker (Turbopack couldn't resolve the removed export) surfaced by
running a clean build here.
Verified: tsc --noEmit is clean (zero errors, down from 4/11 depending
on Payload types cache state); pnpm build succeeds end-to-end (47/47
pages, exit 0); vitest 605/605 passing across 71 files.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GjuWYPbCQ16C3Fkc1k8grT
Exact-pinned since b691d69 with no stated reason — unlike the deliberate exact pins syncpack.config.mjs documents for pdfjs-dist (patch-keyed) and redis (v4 API pin), nothing about jose's usage (verifying a JWT cookie) needs version-exactness. Brings it in line with the config's default 'everything else uses caret ranges' rule. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GjuWYPbCQ16C3Fkc1k8grT
- Remove the Plans and Design Docs section: plan/ no longer exists (docs/ untracking earlier this session moved planning docs to a gitignored docs/superpowers/ directory). - Fix the AGENTS.md summary line: the SSE-channel guardrail is now partial (an allowlist plus auth on job-progress channels exist; only the public channels stay intentionally unauthenticated), and the queryPresets guardrail describes a feature no longer present anywhere in the codebase. - Add src/lib/seed/ and src/emails/, both real additions since this file was last written. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GjuWYPbCQ16C3Fkc1k8grT
Re-verified each guardrail against current code: - SSE channels: the guardrail described unlimited unauthenticated subscriptions with no allowlist. That's no longer true — an allowlist (src/lib/sse/channels.ts) plus payload.auth() for per-job channels already exist. Rewrote to describe the actual split and what a new non-public channel needs. - queryPresets: removed entirely. grep confirms the feature no longer exists anywhere in the codebase. - Raw SVG rendering: the guardrail said only client-side svgo ran, with no real security boundary. A server-side beforeChange hook (src/lib/sanitizeSvg.ts, DOMPurify with an explicit deny-list) was already wired into ResumeCustomers/index.ts:87 — the guardrail's own ask was already done. Rewrote to describe the existing boundary and point future raw-HTML/SVG fields at it. - CRON_SECRET and the Doppler/preview-route notes: re-verified, still accurate, left unchanged. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GjuWYPbCQ16C3Fkc1k8grT
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.