Site maintenance sweep: security headers, tests, and refactors - #16
Merged
Conversation
- Add Spartan Development d.o.o. (Full Stack Developer, Sep 2026) as the current entry at the top of the career carousel - Set the Optiweb tenure end date to Aug 2026 - Update the llms.txt intro to name Spartan as the current employer; the experience listing is generated from the same constant Reflects the employer change effective 1 Sep 2026. Spartan bullets are written in present tense as the active role and intentionally omit the CTO track, which is planned for a Q1 2027 update to this same card.
Closes 22 of the 23 children of thecodedestroyer_github_io-35m. Only the test-harness issue (.23) is left open. Security (P0) - next + eslint-config-next 16.2.9 -> 16.3.3, clearing 4 high and 5 moderate advisories (middleware/proxy bypass, SSRF in Server Actions and rewrites, DoS in Server Actions and image optimization, cache confusion, unbounded Edge Server Action payload, Server Function endpoint disclosure). - postcss 8.5.16 -> 8.5.26 as a direct bump rather than an override, which also lifts nanoid transitively. - pnpm.overrides collapsed from 17 entries to 4. Every deleted entry was proven dead with `pnpm why`; the two security entries that remain are load-bearing (brace-expansion via rimraf>glob, minimatch via eslint-plugin-import-x). pnpm audit is now clean for both --prod and dev, down from 23 advisories. Bugs (P1) - scroll-smooth moved from <body> to the actual scroll container in page.tsx, so navbar anchors ease instead of hard-jumping. - Dropped react-device-detect. It read navigator.userAgent at module-eval time, so isMobile was always false during SSR and every section hydrated with a mismatch. The mobile opt-out is now a CSS media query, which the server and client agree on by construction. - useLayoutEffect -> useEffect in SectionWrapper (SSR'd client component). - "Contact me" was a <button> assigning window.location.href. It is now a real anchor at all three call sites, so it is crawlable, announced as a link, and supports middle-click/cmd-click/copy-link. contact.util.ts deleted. CI (P1) - checkout@v4->v7, setup-node@v4->v7, pnpm/action-setup@v2->v6; the hand-rolled store cache replaced with setup-node's native cache: 'pnpm'. - Added --frozen-lockfile, a build step, a concurrency group, and a pull_request trigger; push now scoped to main. Node 22 -> 24. Deprecations and a11y (P2) - Disclosure.Button/.Panel -> DisclosureButton/DisclosurePanel. - framer-motion 12 -> motion 13 (the package was renamed). - prefers-reduced-motion is now respected across the section entrance animation, the border-spin ring, the halo, and anchor scrolling. - Security response headers added. The CSP ships report-only on purpose; see -35m.26 for what must be verified on a preview before enforcing it. Cleanup and additions (P3) - Custom 404 page, web manifest, icon set, explicit robots, sitemap lastmod. - Static imports replace five no-op next/dynamic wrappers in a Server Component, removing a request round-trip. - Removed zod, stale tsconfig path aliases, dead CSS tokens, a no-op clsx, a redundant zustand spread, and an unused image quality. Notes - pnpm 10.25 defaults minimumReleaseAge to 1209600, which it reads as MINUTES (~840 days), blocking all resolution. Worked around per-invocation with --config.minimumReleaseAge=0; no config file was changed. See -35m.25. - pnpm-lock.yaml is Prettier-formatted and lint-checked. Both conventions are now documented in the README. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Closes thecodedestroyer_github_io-35m.23, the last child of the maintenance sweep epic. The repo had zero tests; CI ran eslint, prettier and tsc only. Five tests, ~0.8s, always against the production build rather than `next dev` — the section ids, metadata routes and prerendered markup are all build-time output, so the dev server would be testing a different artifact. - smoke: `/` returns 200, the h1 renders, and every value of the `Sections` enum is attached as an element id. Sourced from the enum, not a literal list, so a renamed section fails here instead of silently breaking deep links. - metadata routes: /llms.txt, /robots.txt and /sitemap.xml return 200 with the expected content type AND a per-entry body marker. Status and content type alone are not enough — an empty sitemap generator still emits a well-formed `<urlset></urlset>`, which is the regression most worth catching. - navbar highlight: asserts `aria-current` before and after scrolling to #technologies. Deliberately not asserting the `text-accent` class: both come from the same `isCurrent` ternary, so the class adds no signal and would go red on any Tailwind token rename. Playwright over Cypress, despite Cypress being on the site's own skill list: the ESLint preset sets `no-unused-expressions: error`, which bans the Chai property assertions Cypress leans on, and Cypress's injected globals need a second tsconfig. Playwright needed no eslint.config.mjs override at all, has `webServer` built in (Cypress would need start-server-and-test as a second dependency), and exposes `reducedMotion` as a first-class context option. Each test was verified to fail when its subject breaks, not just to pass: pinning the navbar highlight to the first section fails the post-scroll assertion while the load-state one still passes, which is what proves the scroll half is not a no-op. CI: the tests run in the existing job after the build, since `next start` needs that artifact. Splitting would mean uploading .next plus a second checkout and install for no parallelism. Job renamed lint -> ci to match what it now does. The report artifact upload is scoped to the E2E step so it does not warn on lint or build failures. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…rasableSyntaxOnly
'export enum Sections' was the only thing tripping TS1294 under the base config's 'erasableSyntaxOnly: true', so tsconfig.json had been overriding it to false. Replace the enum with an 'as const' object plus a derived 'Section' union type, switch the four type-position consumers (SectionWrapper, navbar.types, common.store, the navbar-highlight e2e spec) over to it, and drop the override. Value consumers ('Sections.X', 'Object.values(Sections)') are unchanged.
Closes thecodedestroyer_github_io-35m.24
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
public/profile.png was 2850x3491 and 4.0 MB for an image next/image renders at 531x650 CSS px. With a fixed height and no `sizes`, next/image only ever requests the 640w (1x) and 1080w (2x) variants, so the source needed ~1100px of width, not 2850. - Resize with sharp (Lanczos3, premultiplied alpha, sRGB) to 1143x1400 and re-encode as RGBA PNG at zlib level 9 with adaptive filtering - 4,166,539 -> 774,142 bytes (-81%) in every clone, build and deploy, and ~6x fewer pixels to decode on the first optimization request per variant - Same aspect ratio, so the prerendered <img> width/height are unchanged Verified against `next build` + `next start`: the 640w/1080w AVIF variants serve at 640x784 / 1080x1323 with alpha, a DPR-2 browser picks the 1080w variant at native scale, and the Playwright suite passes. Closes thecodedestroyer_github_io-35m.27 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Rename Content-Security-Policy-Report-Only to Content-Security-Policy now that the policy has been verified. Verified against the production build (next start) in headless Chrome, desktop and mobile viewports: zero violations on first paint, while scrolling every section, during the motion entrance animations, hovering the halo cards (--mouse-x/--mouse-y inline writes) and toggling the mobile nav; JSON-LD parses as a Person; /_next/image negotiates avif and webp; manifest.webmanifest and all icon routes load. Playwright suite passes against the enforcing build. Also add upgrade-insecure-requests, which browsers ignore in report-only mode, and keep X-Frame-Options: DENY deliberately as zero-cost belt-and-braces now that frame-ancestors 'none' is enforced. next dev under the enforcing header logs one React-dev-only eval() console.error but hydrates, connects HMR and blocks nothing, so no dev gating and no 'unsafe-eval'. No report-to sink, on purpose: re-verify by hand on a Vercel preview after any policy change. Closes thecodedestroyer_github_io-35m.26 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Button styling: move `inline-flex items-center justify-center` and `border-none` into the cva base, so the two consumers stop hand-repeating the layout classes and a bare `buttonVariants()` really is styled like `<ButtonLink />`, as its comment already claimed. Drop the `disabled:*` classes left over from the deleted Button component — both surviving consumers render anchors, where `:disabled` never matches. Metadata: apply AUTHOR_HEADLINE to the on-page profile alt, interpolate META_DESCRIPTION from AUTHOR_NAME/SITE_NAME/JOB_TITLE, and use JOB_TITLE and SITE_NAME in llms.txt. Each was a literal copy sitting outside the constant that exists to prevent it drifting. The metadata-routes spec now asserts AUTHOR_NAME instead of the literal, matching the smoke spec. Entrance animation: delete ENTRANCE_ANIMATION_QUERY and its useCallback. The media query was a hand-maintained exact complement of the `static-entrance` CSS variant, in a second language, with nothing enforcing the invariant; the `!important` utilities are what pin the resting state, so globals.css is now the single definition. Likewise drop the useReducedMotion guard in WorkCard — `motion-reduce:hover:before: opacity-0!` already hides the halo, so the hook only bought a post-hydration re-render to skip an invisible layout read. Also flatten the robots metadata (max-image-preview, max-snippet and max-video-preview are valid top-level RobotsInfo keys, so the googleBot nesting only duplicated index/follow into a subset meta tag) and point --background-image-contact at the theme colour variables rather than re-typing both brand hexes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Every run was a cold `next build`: a full bundler compile, a fresh next/font fetch of both families, and a full sharp re-optimisation of the profile image. Cache `.next/cache`, keyed on the lockfile plus the source hash with prefix fallbacks, so an unchanged dependency set reuses it. Give the Playwright cache `restore-keys` as well — the key is the whole lockfile hash, so any unrelated dependency bump was re-downloading the Chromium bundle for a browser that had not changed. On a cache hit the browser binaries are already present but the runner's system libraries are not, so `--with-deps` is replaced by a plain `install-deps` there instead of running unconditionally. Finally, read the Node version from .nvmrc rather than hardcoding 24, which was a third copy alongside .nvmrc and the README — the pnpm version was already de-duplicated this way via packageManager. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- Split `test` into `test:unit` (node --test over src/**/*.test.ts) and `test:e2e` (playwright), with `test` running both in that order - Allow `allowImportingTsExtensions` so the explicit-extension imports that `node --test` needs typecheck; nothing is emitted, so it costs nothing - Turn off `no-floating-promises` for test files, where the runner awaits the `node:test` suites itself - Run the unit tests in CI ahead of the build: no bundler and no browser, so a broken rule fails in seconds rather than after a build and a boot - Ignore `.env*` Pure logic (the navbar highlight rule, next) had nowhere to be tested except through a browser, which made the fast feedback loop as slow as the slow one.
Six sections each wrote the highlight from their own `useInView` effect, so
whenever two were in view the winner was whichever effect happened to run last,
and `SectionInterests` — the one section taller than the viewport — needed an
`amount={0.007}` fudge to reach the threshold at all.
- Add `@client/currentSection`: one module-scope IntersectionObserver over a
thin probe band a quarter down the viewport, with `pickCurrent` (topmost
section covering the band wins) extracted as a pure, unit-tested rule
- Give sections `useSectionSpy` and the navbar `useCurrentSection`; drop the
zustand `common.store` and `CurrentSectionState`
- Extract `NavLink` and `DisclosureAnchor`, so a link reads the highlight
itself and scrolling re-renders three links instead of the whole navbar
- Move the nav's section list and labels into `section.constants`, replacing
the `NabBarNavigation` array duplicated across the desktop and mobile
renderers; the e2e spec now looks links up by that list
- Skip the observer and the animation loop in `SectionWrapper` below 48rem and
under reduced motion, where `static-entrance` discards every frame anyway
- Restore `SectionInterests` to a real `amount`, now that it is only an
entrance-animation knob
Adds `link.constants` as the home for `links`; the old `link.types` name goes
once its remaining importers move.
The three views over what Nace works with disagreed: JSON-LD `knowsAbout` named 5 technologies, the llms.txt skills section about 40, and the icon grid 12 — Convex, Supabase, Claude Code and Docker were missing from the structured data entirely. Each view held its own hand-written copy. - Add `technology.constants` as the single list, with a `Technology` type carrying a canonical name, a prose-only detail and a category; the featured set is type-level, so a featured name with no icon (or an icon with no name) is a compile error - Move the JSON-LD document and the llms.txt body out of `layout.tsx` and the route handler into `@shared/profile`, where both read the list - Render the icon grid from `featuredTechnologyOrder` instead of twelve literal elements - Pull `Slovenia` and the remaining literal name and job title in the sections up into `meta.constants` as `AUTHOR_COUNTRY` and friends - Finish the `link.types` -> `link.constants` move and drop the old file - Add an e2e spec asserting the three renderers agree with the list Also drops three now-needless `use client` directives from sections that only render constants.
Names the vocabulary the code now uses for itself — Section, Nav section, Current section, Probe band, Entrance animation, Profile, Renderer, Technology, Featured technology — and says where each one lives. Written after the navbar-highlight and profile refactors, which introduced most of these terms; it exists so the distinctions they turn on (Current section vs. Entrance animation, list vs. renderer) stay legible.
- Add `Ellipsis`, holding the contract every blob shares: out of the flow, behind the content - Replace the eight hand-rolled `absolute -z-10 block ellipsis` divs across the sections and the 404 page with it - Split the responsibility explicitly — the wrapper owns stacking, the `ellipsis` utility owns blur, radius and opacity, and the call site keeps position, size, rotation and colour - Record the term and that split in CONTEXT.md Changing how the blobs stack was an eight-file edit with no single place to make it; it is now one.
- Expand the palette comment in `globals.css` to name both copies CSS cannot reach: THEME_COLOR in `@shared/constants/meta.constants.ts`, and the fill and stroke in `icon.svg` - Head `icon.svg` with what it is — the source the apple-icon and manifest PNGs are exported from, by hand — and which tokens its hexes mirror Nothing enforces these copies, so a palette change silently left the theme colour and the favicon on the old brand. The comments make the blast radius visible from either end.
- Hold a single lazy `MediaQueryList` for the entrance-animation query instead of minting one per subscribe and per `getSnapshot` - Keep it lazy so the module still imports on the server, where `window` does not exist `getSnapshot` runs on every render of every section, so each one was allocating a fresh `MediaQueryList` per render for an answer that is global to the page. This mirrors how `@client/currentSection` keeps one observer.
- Export `ONGOING` from `work.constants` and use it for the current entry's `to` value - Match against it in `llmsDocument` instead of re-typing the `'Present'` literal in a second file The string is both prose the work card prints and the flag llms.txt finds the current job by. Sharing it means a reworded marker cannot quietly leave llms.txt naming the previous employer.
- Derive the e2e port from a hash of the checkout path, so `reuseExistingServer` reuses *this* checkout's server rather than whichever one started first; `E2E_PORT` still pins a port, and the base stays off 3000 - Ignore `.claude/` in ESLint and Prettier — it holds agent worktrees, whose files are full checkouts of other branches and were reported as this branch's errors, and none of it is in tsconfig anyway - Update the README's test section to describe the derived port A second `pnpm test` in a parallel worktree found the first one's `next start` on the fixed 3111, reused it, and passed green against somebody else's build.
- Make the screen-reader-only label track the open state, so the button reads as "Close main menu" once the menu is expanded The icon already swapped between the hamburger and the X, but the visually hidden label stayed fixed at "Open main menu". Screen reader users were told to open a menu that was already open.
The enforcing Content-Security-Policy sent two directives that only make sense against a deployed HTTPS origin. Both are now gated on a production build; the shipped policy is byte-for-byte unchanged. - `upgrade-insecure-requests` rewrote every `http://` subresource URL to `https://`. `next dev` speaks plain HTTP, so every chunk, stylesheet and font was requested on a port with no TLS listener and failed. Chrome exempts `localhost` and hid this locally, but it does not exempt the LAN "Network:" URL and WebKit exempts nothing — Safari rendered a bare, scriptless page, and Chrome on the LAN URL dropped 12 requests. - `script-src` now carries `'unsafe-eval'` in development. React's dev bundle calls `eval()` to reconstruct call stacks across the server/client boundary; blocking it logged a console.error on every load and stripped those frames from the error overlay. The production bundle never calls `eval()`, so this is a dev-tooling allowance, not a looser shipped policy. Verified in Chromium, Firefox and WebKit: zero failed requests and a clean console on `next dev`, and `next build` + `next start` still emits `upgrade-insecure-requests` with no `'unsafe-eval'`. Closes thecodedestroyer_github_io-blt
The contact section was the only one carrying Tailwind's `transition` class, which puts a 150ms CSS transition on `transform` and `opacity` — the two properties motion writes inline on every frame. The browser then interpolated toward each frame instead of painting it, so the rendered scale chased the real one: 0.66 painted where motion had already reached 0.91, and the entrance dragged to ~470ms against the 350ms every other section takes. The result read as a rubbery zoom-out into zoom-in. The class was vestigial — the section element has no hover or state change to transition — so removing it is the whole fix. Re-measured: rendered scale now tracks the inline value to three decimals and lands on time.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
…be band The README's test guide still said end-to-end only, but `pnpm test` runs the Node unit suite first. Name both layers and both scripts. The probe band's percentage `rootMargin` was flagged as aspect-ratio dependent. It is not: Chromium, Gecko and WebKit all resolve the vertical percentages against viewport height, whatever the spec's prose says. An e2e case on a wide, short viewport now holds that down, and the comment says why the reading is safe. Addresses: ANALYSIS_pr-review-job-a8753ad504e044f890892602a45609df_0001 Declines: BUG_pr-review-job-a8753ad504e044f890892602a45609df_0001 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.
Summary
minimumReleaseAge, dependency sweep) and fixes a handful of UI, a11y and navigation bugs.Changes
Security & config
minimumReleaseAgeand document the minutes trap..nvmrcand cache the Next build in CI.Testing
--testunit runner forsrc/**/*.test.ts, starting withpickCurrent.Navigation & a11y
src/lib/client/currentSection).NavLinkandDisclosureAnchorout ofNavBar.UI
Ellipsiscomponent.buttonVariants.Data & metadata
llmsDocument,personJsonLd,technology.constants,section.constants.not-foundpage.profile.pngto 1400px tall (4.2 MB to 774 KB).Docs
CONTEXT.md; note where the brand hexes are re-typed.Test plan
pnpm lintandpnpm buildpnpm test(runstest:unitthentest:e2e)pnpm devandpnpm build && pnpm start.