Skip to content

Develop - #34

Open
danielheene wants to merge 249 commits into
mainfrom
develop
Open

Develop#34
danielheene wants to merge 249 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 26 commits August 18, 2026 05:18
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
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
danielheene and others added 3 commits August 20, 2026 10:57
- 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
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.

3 participants