Port hybrid design to root site (replaces /refresh-preview/) - #4
Conversation
A single-file directional concept exploring a refreshed NITSOF identity around the three values from the brief: simplicity, confidence, future. Decoupled from the React app on purpose — opens as plain HTML so the direction can be reviewed before any production code is changed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
So a static screenshot capture (Firefox headless) shows the laid-out page instead of waiting on IntersectionObserver to fire. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Type switcher in bottom-right of the preview lets reviewers A/B four schemes without rebuilds: Editorial Bold (Fraunces+Manrope), Editorial Calm (Newsreader+Inter Tight), Newsstand (Instrument Serif+Inter), Swiss Modern (General Sans+Switzer). Schemes are also reachable via ?type=. - All four font sets are loaded up front so there's no FOUT when switching. Active scheme is selected by setting --font-display and --font-body as inline CSS variables on <html>, which outrank the :root rule. - Carrier Rates case study now links to the actual Shopify App Store listing and uses the correct product name (Live Carrier Rates).
- Hero "From" meta: Remote · UK / EU → Australia · Worldwide - Carrier checkout mock: swap £ for $ and replace UK-only carriers (Royal Mail / DPD / Evri) with a global mix (USPS / FedEx / Australia Post / DHL) so the brand reads as globally agnostic while keeping the Australian identity visible.
- New swiss.html: same content as index.html, locked to General Sans + Switzer (D · Swiss Modern) with no type-switcher — clean standalone alternative for review. Variant badge in the top bar identifies it. - Fixed skumanager.com link in both files: was href="#", now points to https://skumanager.com (matching the Shopify App Store fix for carrier rates). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… review Dark-theme concept: near-black #07080B bg, electric orange #FF4500 accent, Space Grotesk + DM Sans. Structurally different from swiss.html: full-height hero with stat bar, manifesto as horizontal rows, capabilities as numbered list rows, work cases in full-width alternating split-panel layout, process steps with oversized faded numbers, and all-orange CTA section. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…m body Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ial links - favicon.svg: N-mark with cobalt dot, dark bg, for use as site favicon - hybrid.html: inline SVG favicon data URI + OG/twitter meta + theme-color - hybrid.html: cobalt scroll progress bar (fixed, 2px, top of page) - hybrid.html: mobile hamburger menu with full-screen dark overlay - hybrid.html: "Trusted by" client strip between hero and manifesto - hybrid.html: footer social links (LinkedIn, GitHub) + Made in Australia marker
…form - hybrid.html: replace mailto-only CTA button with a Name/Company/Message form that pre-fills a mailto: on submit (no backend required for preview) - hybrid.html: add Insights section with 3 article cards and nav link - Next.js: add /insights list page and /insights/[slug] post pages - Next.js: create src/content/insights/ with 3 seed markdown posts - Next.js: add src/lib/insights.ts helper (gray-matter + marked) - Next.js: add Insights link to Header nav - Install gray-matter, marked, @tailwindcss/typography - Enable @tailwindcss/typography plugin for prose styling on post pages Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- page.tsx: full hybrid home page (hero, manifesto, capabilities, work, process, quote, insights, CTA) - layout.tsx: Fontshare fonts (General Sans + Switzer) + JetBrains Mono, updated metadata - globals.css: complete NITSOF design system CSS (cream/cobalt/signal palette) - Header.tsx: adaptive sticky nav — dark on hero, cream on scroll; includes ticker + hamburger - Footer.tsx: hybrid footer with Made in Australia + social links - ContactForm.tsx: Formspree-ready contact form (falls back to mailto when NEXT_PUBLIC_FORMSPREE_ID is unset) - HybridAnimations.tsx: client-side scroll progress, reveal animations, past-hero body class - insights/page.tsx + [slug]/page.tsx: restyled to match cream hybrid theme - Build: ✓ compiled, ✓ 12/12 static pages, ✓ no type errors Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
Warning Rate limit exceeded
You’ve run out of usage credits. Purchase more in the billing tab. ⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: ⛔ Files ignored due to path filters (2)
📒 Files selected for processing (3)
📝 WalkthroughWalkthroughThis PR delivers a comprehensive brand refresh by adding a markdown-based insights/blog system, completely redesigning the global design system with extensive CSS variables and animations, updating core components with new interactions, redesigning the homepage to emphasize AI-native positioning, and refreshing supporting marketing pages. ChangesBrand Refresh, Insights System, and Homepage Redesign
Sequence Diagram(s)sequenceDiagram
participant InsightsPages as Insights Pages
participant InsightsLib as src/lib/insights.ts
participant FileSystem as src/content/insights/*.md
participant Marked as marked
InsightsPages->>InsightsLib: getAllInsights() / getInsightBySlug()
InsightsLib->>FileSystem: readFileSync(.md)
InsightsLib->>Marked: marked(post body)
Marked-->>InsightsLib: HTML content
InsightsLib-->>InsightsPages: InsightMeta[] / InsightPost
sequenceDiagram
participant Layout as RootLayout
participant HybridAnimations as HybridAnimations
participant Header as Header
participant ContactForm as ContactForm
participant Footer as Footer
Layout->>HybridAnimations: Mount component
HybridAnimations->>Document: Toggle past-hero class
HybridAnimations->>Document: Update scroll-progress width
HybridAnimations->>Document: Add in class to reveals
Layout->>Header: Render with ticker
Header->>Document: Toggle menuOpen state
Layout->>ContactForm: Embed in page
ContactForm->>Formspree: POST form data
Layout->>Footer: Render with social links
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 9
🧹 Nitpick comments (2)
src/components/Header.tsx (1)
96-120: ⚡ Quick winAvoid keeping the closed mobile menu interactive in the DOM.
Line 96 keeps the mobile nav mounted even when closed, and Line 98 only sets
aria-hidden. That can still leave hidden links focusable. Prefer conditional rendering when closed.♻️ Proposed refactor
- <nav - className={`mobile-menu${menuOpen ? " open" : ""}`} - aria-hidden={!menuOpen} - > - <Link href="/#capabilities" onClick={closeMenu}>Capabilities</Link> - <Link href="/#work" onClick={closeMenu}>Work</Link> - <Link href="/#process" onClick={closeMenu}>Process</Link> - <Link href="/#about" onClick={closeMenu}>Studio</Link> - <Link href="/insights" onClick={closeMenu}>Insights</Link> - <Link className="menu-cta" href="/#contact" onClick={closeMenu}> - Start a project - <svg - width="14" - height="14" - viewBox="0 0 24 24" - fill="none" - stroke="currentColor" - strokeWidth="2.2" - strokeLinecap="round" - strokeLinejoin="round" - > - <path d="M5 12h14M13 5l7 7-7 7" /> - </svg> - </Link> - </nav> + {menuOpen && ( + <nav className="mobile-menu open"> + <Link href="/#capabilities" onClick={closeMenu}>Capabilities</Link> + <Link href="/#work" onClick={closeMenu}>Work</Link> + <Link href="/#process" onClick={closeMenu}>Process</Link> + <Link href="/#about" onClick={closeMenu}>Studio</Link> + <Link href="/insights" onClick={closeMenu}>Insights</Link> + <Link className="menu-cta" href="/#contact" onClick={closeMenu}> + Start a project + <svg + width="14" + height="14" + viewBox="0 0 24 24" + fill="none" + stroke="currentColor" + strokeWidth="2.2" + strokeLinecap="round" + strokeLinejoin="round" + > + <path d="M5 12h14M13 5l7 7-7 7" /> + </svg> + </Link> + </nav> + )}🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/components/Header.tsx` around lines 96 - 120, The mobile nav is kept mounted when closed which leaves its links focusable; change it to conditionally render instead of always mounting: replace the always-rendered <nav className={`mobile-menu${menuOpen ? " open" : ""}`} aria-hidden={!menuOpen}> block with a conditional render that only outputs the nav and its Link children when menuOpen is true (or alternatively unmount when closing), keeping the same attributes/children (including the "menu-cta" Link and SVG) and keeping the existing closeMenu handlers so the open/close behaviour remains intact.src/lib/insights.ts (1)
24-28: ⚡ Quick winMake
filenamethe source of truth forslug.Relying on frontmatter for
slugcan silently drift from the actual file path and break links/static params. Deriveslugfrom the filename and merge metadata on top.Suggested diff
return files .map((filename) => { + const slug = filename.replace(/\.md$/, ""); const raw = fs.readFileSync(path.join(CONTENT_DIR, filename), "utf-8"); const { data } = matter(raw); - return data as InsightMeta; + return { + slug, + ...(data as Omit<InsightMeta, "slug">), + } as InsightMeta; })🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/lib/insights.ts` around lines 24 - 28, The mapping that reads frontmatter should derive the canonical slug from the filename and merge frontmatter on top: inside the .map over filenames in src/lib/insights.ts (the block that reads files with fs.readFileSync and parses with matter), compute slug from the filename (strip the extension, e.g. remove .md/.mdx) and then return an object like { ...data, slug } cast to InsightMeta so the filename is the source of truth and any frontmatter slug does not override it.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@package.json`:
- Around line 15-17: The package.json currently lists the redundant, deprecated
type-only package "`@types/marked`" alongside "marked"; remove the "`@types/marked`"
entry from package.json, ensure only "marked" (^18.0.3) remains, then
reinstall/update the lockfile (npm install / yarn install) so the lockfile
reflects the removal; check for any stray imports referencing "`@types/marked`"
(none expected) and run the TypeScript build to verify types resolve from
"marked" directly.
In `@public/refresh-preview/hybrid.html`:
- Line 1357: The hamburger button with id "hamburger-btn" (class "hamburger")
always has aria-label="Open menu" even when opened; update the toggle logic that
handles this button so it sets aria-expanded correctly and swaps the aria-label
between "Open menu" and "Close menu" based on state (e.g., when the click/toggle
handler for `#hamburger-btn` opens the menu set aria-expanded="true" and
aria-label="Close menu", and when closing set aria-expanded="false" and
aria-label="Open menu"); apply the same fix to the other instance around lines
1777-1786 so both buttons keep label and expanded state in sync.
- Around line 1624-1627: The current markup applies the "section-head" class to
the h2 and uses a "section-label" class that isn't matching the established
pattern; change this to the canonical section-head container pattern by wrapping
the label and title in a container div with class "section-head reveal" (move
the "reveal" modifier there), keep the label as its own element (use the same
label class used elsewhere, e.g. "section-label" if that exists in the project’s
pattern) and remove "section-head" from the h2 so the h2 is a plain heading
inside the container (preserve the inline margin-top if needed); update the
elements around "§ Insights" and the h2 to follow this container pattern so
styling and layout match other sections.
In `@src/app/globals.css`:
- Line 38: The CSS currently sets html { scroll-behavior: smooth; } which still
animates anchor/hash jumps for users who prefer reduced motion; inside the
existing `@media` (prefers-reduced-motion: reduce) block add a rule html {
scroll-behavior: auto; } so anchor navigation respects the reduced-motion
preference (apply the same fix for the other occurrence around lines 1187-1194
where html scroll-behavior is defined).
In `@src/app/layout.tsx`:
- Around line 17-29: Update the static metadata in src/app/layout.tsx to remove
the quarter-specific phrase "Q3 engagements" from the top-level description and
the openGraph.twitter.description entries and replace it with evergreen copy or
reference a dynamic value (e.g., an availability/config variable) so
social/search previews don't become stale; locate the description,
openGraph.title/description and twitter.title/description fields in that file
and either substitute general language (e.g., "Currently accepting engagements"
or similar) or read from a shared config/prop (e.g., availabilityMessage) so the
copy can be updated centrally.
- Around line 45-50: Replace the external Fontshare <link rel="stylesheet"> and
preconnect tags with self-hosted fonts: download the General Sans and Switzer
.woff2 files into src/app/fonts/, create next/font/local font objects (e.g.,
generalSans = localFont({ src: [{ path: './fonts/general-sans-400.woff2',
weight: '400' }, ...], display: 'swap' }) and switzer = localFont({...})), then
remove the two <link rel="preconnect"> and the <link rel="stylesheet"> lines and
apply the new font variables (generalSans.className, switzer.className) to the
document root or relevant components the same way JetBrains Mono is already
applied via next/font/google in layout.tsx so the site no longer depends on
Fontshare CDN.
In `@src/components/ContactForm.tsx`:
- Around line 44-48: The fallback branch currently sets setStatus("success")
immediately after invoking window.location.href = `mailto:...`, which
incorrectly reports success before the user actually sends an email; remove or
change that immediate setStatus call in the mailto fallback inside the
submit/handleSubmit flow so the UI does not claim "sent" prematurely—either
leave status unchanged or set it to a pending/awaiting-user action state and
show a notice asking the user to complete sending in their email client, keeping
references to window.location.href and setStatus in your fix.
In `@src/components/HybridAnimations.tsx`:
- Around line 45-46: The scroll progress handler is only registered but not
invoked initially, so call handleScrollProgress once on mount to initialize the
progress bar; inside the component's mount effect (the same place where
window.addEventListener("scroll", handleScrollProgress, { passive: true }) is
added) invoke handleScrollProgress() immediately after registering (or just
before) and keep the existing cleanup that removes the listener; reference the
handleScrollProgress function and the mount effect where the listener is
attached.
In `@src/lib/insights.ts`:
- Line 39: The markdown is being converted to HTML with "marked(content) as
string" and then later injected via dangerouslySetInnerHTML—sanitize the
generated HTML before returning it by piping the marked output through a
sanitizer (e.g., DOMPurify): replace "content: marked(content) as string" with
something like "content: DOMPurify.sanitize(marked(content))" (or use
isomorphic-dompurify for SSR), import and initialize the sanitizer at the top,
and adjust typings/imports accordingly so the returned content is safe for
client rendering.
---
Nitpick comments:
In `@src/components/Header.tsx`:
- Around line 96-120: The mobile nav is kept mounted when closed which leaves
its links focusable; change it to conditionally render instead of always
mounting: replace the always-rendered <nav className={`mobile-menu${menuOpen ? "
open" : ""}`} aria-hidden={!menuOpen}> block with a conditional render that only
outputs the nav and its Link children when menuOpen is true (or alternatively
unmount when closing), keeping the same attributes/children (including the
"menu-cta" Link and SVG) and keeping the existing closeMenu handlers so the
open/close behaviour remains intact.
In `@src/lib/insights.ts`:
- Around line 24-28: The mapping that reads frontmatter should derive the
canonical slug from the filename and merge frontmatter on top: inside the .map
over filenames in src/lib/insights.ts (the block that reads files with
fs.readFileSync and parses with matter), compute slug from the filename (strip
the extension, e.g. remove .md/.mdx) and then return an object like { ...data,
slug } cast to InsightMeta so the filename is the source of truth and any
frontmatter slug does not override it.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: db0f9bd6-1bc0-4c9f-9a14-2d51fe47eea9
⛔ Files ignored due to path filters (2)
package-lock.jsonis excluded by!**/package-lock.jsonpublic/favicon.svgis excluded by!**/*.svg
📒 Files selected for processing (19)
package.jsonpublic/refresh-preview/README.mdpublic/refresh-preview/hybrid.htmlpublic/refresh-preview/index.htmlpublic/refresh-preview/second.htmlpublic/refresh-preview/swiss.htmlsrc/app/globals.csssrc/app/insights/[slug]/page.tsxsrc/app/insights/page.tsxsrc/app/layout.tsxsrc/app/page.tsxsrc/components/ContactForm.tsxsrc/components/Footer.tsxsrc/components/Header.tsxsrc/components/HybridAnimations.tsxsrc/content/insights/ai-native-delivery.mdsrc/content/insights/from-prototype-to-production.mdsrc/content/insights/shopify-shipping-complexity.mdsrc/lib/insights.ts
| const { data, content } = matter(raw); | ||
| return { | ||
| ...(data as InsightMeta), | ||
| content: marked(content) as string, |
There was a problem hiding this comment.
Sanitize markdown HTML before returning it.
Line 39 produces HTML from markdown that is later injected with dangerouslySetInnerHTML. Without sanitization, embedded HTML/event payloads can execute in the browser.
Suggested diff
import matter from "gray-matter";
import { marked } from "marked";
+import sanitizeHtml from "sanitize-html";
@@
return {
...(data as InsightMeta),
- content: marked(content) as string,
+ content: sanitizeHtml(marked(content) as string),
};
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| content: marked(content) as string, | |
| import matter from "gray-matter"; | |
| import { marked } from "marked"; | |
| import sanitizeHtml from "sanitize-html"; | |
| return { | |
| ...(data as InsightMeta), | |
| content: sanitizeHtml(marked(content) as string), | |
| }; |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/lib/insights.ts` at line 39, The markdown is being converted to HTML with
"marked(content) as string" and then later injected via
dangerouslySetInnerHTML—sanitize the generated HTML before returning it by
piping the marked output through a sanitizer (e.g., DOMPurify): replace
"content: marked(content) as string" with something like "content:
DOMPurify.sanitize(marked(content))" (or use isomorphic-dompurify for SSR),
import and initialize the sanitizer at the top, and adjust typings/imports
accordingly so the returned content is safe for client rendering.
There was a problem hiding this comment.
Intentionally skipped — the markdown content in src/content/insights/ is authored locally and committed to the repo, not user-generated input. Injecting malicious HTML would require repository write access. Adding a sanitizer dependency for internal, trusted content adds unnecessary overhead. If the content source ever becomes external (CMS, user-submitted), this should be revisited.
There was a problem hiding this comment.
@PrisStratton, that's a fair and well-reasoned call. For repo-committed, internally authored content the XSS risk surface is gated behind repository write access, so sanitisation overhead isn't justified here.
Would you like me to open a GitHub issue as a future reminder to add sanitisation if the content source ever moves to an external CMS or accepts user-submitted content? That way it won't get lost.
✏️ Learnings added
Learnt from: PrisStratton
Repo: nitsof/nitsof.com PR: 4
File: src/lib/insights.ts:39-39
Timestamp: 2026-05-16T23:00:55.479Z
Learning: In `src/lib/insights.ts` (nitsof/nitsof.com), the `getInsightBySlug()` function intentionally omits HTML sanitisation on the `marked()` output because all markdown content in `src/content/insights/` is locally authored and committed to the repository — not user-generated. The maintainer (PrisStratton) considers exploiting it equivalent to having repository write access. Sanitisation should only be added if the content source moves to an external CMS or becomes user-submitted.
Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.
…older content - Wire public/favicon.svg in layout.tsx as the site favicon - Remove fake "Trusted by" client strip from homepage - Replace fake testimonial attribution with brand statement copy - Restyle /carrier-rates with hybrid cream theme: adds breadcrumb, How It Works, Supported Carriers, Key Features sections, SEO metadata - Restyle /carrier-rates/faq with hybrid cream theme: complete answers for all 6 FAQs, proper H2 hierarchy, breadcrumb + back-link, SEO metadata - Restyle /privacy with hybrid cream theme: removes rainbow gradient, cream background, cobalt accent links, SEO metadata - Delete public/refresh-preview/ prototype files (superseded by root site) Build: 12/12 static pages, no errors. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
🧹 Nitpick comments (2)
src/app/privacy/page.tsx (1)
10-10: ⚡ Quick winRemove unused helper function.
The
sectionfunction is defined but never used. The sections array at lines 68-190 is constructed inline without calling this helper.🧹 Remove the unused function
-const section = (title: string, children: React.ReactNode) => ({ title, children }); - export default function PrivacyPolicy() {🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/app/privacy/page.tsx` at line 10, The helper function section(title: string, children: React.ReactNode) is unused—remove its declaration to eliminate dead code; locate the function named section in the file (it returns { title, children }) and delete that whole function definition, ensuring no other code references section (the sections array is constructed inline), then run TypeScript/ESLint to confirm no unused-symbol errors remain.src/app/carrier-rates/faq/page.tsx (1)
89-142: 💤 Low valueConsider using a stable key instead of array index.
Using the array index as the key (Line 100:
key={i}) works for this static FAQ list, but using a more stable identifier would be more resilient to future edits.♻️ Refactor to use question text as key
- key={i} + key={faq.q}🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/app/carrier-rates/faq/page.tsx` around lines 89 - 142, The list currently uses the array index as the React key in the faqs.map callback (key={i}); replace this with a stable identifier such as faq.q or a dedicated faq.id to avoid reordering bugs: update the data source to include a unique id for each FAQ if not present, then use that id (or the question text faq.q if guaranteed unique) as the key on the outer div rendered in the map; ensure the chosen key is unique and stable across renders (optionally derive a slug from faq.q if you cannot add ids).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@src/app/carrier-rates/faq/page.tsx`:
- Around line 89-142: The list currently uses the array index as the React key
in the faqs.map callback (key={i}); replace this with a stable identifier such
as faq.q or a dedicated faq.id to avoid reordering bugs: update the data source
to include a unique id for each FAQ if not present, then use that id (or the
question text faq.q if guaranteed unique) as the key on the outer div rendered
in the map; ensure the chosen key is unique and stable across renders
(optionally derive a slug from faq.q if you cannot add ids).
In `@src/app/privacy/page.tsx`:
- Line 10: The helper function section(title: string, children: React.ReactNode)
is unused—remove its declaration to eliminate dead code; locate the function
named section in the file (it returns { title, children }) and delete that whole
function definition, ensuring no other code references section (the sections
array is constructed inline), then run TypeScript/ESLint to confirm no
unused-symbol errors remain.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 9dde4bbc-2760-4e22-a1d7-ee3d3494fc36
📒 Files selected for processing (5)
src/app/carrier-rates/faq/page.tsxsrc/app/carrier-rates/page.tsxsrc/app/layout.tsxsrc/app/page.tsxsrc/app/privacy/page.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
- src/app/layout.tsx
- Remove deprecated @types/marked (marked v18 ships built-in types)
- Self-host General Sans and Switzer via next/font/local, removing Fontshare CDN dependency and render-blocking external requests
- Add html { scroll-behavior: auto } inside prefers-reduced-motion block so anchor jumps respect user preference
- Replace Q3-specific copy in metadata with evergreen "Currently accepting new engagements"
- Fix ContactForm mailto fallback: set status back to idle (not success) since delivery isn't confirmed
- Initialize scroll progress bar on mount, not only on first scroll event
Build: ✓ 12/12 static pages, no type errors
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Replace src/app/favicon.ico with src/app/icon.svg so Next.js App Router serves the N· cobalt dot mark correctly (browsers default to /favicon.ico, which was the old placeholder — icon.svg is auto-linked in <head>) - Remove manual <link rel="icon"> from layout.tsx (Next.js handles it via icon.svg) - Remove unused section() helper from privacy/page.tsx - Use faq.q as stable React key in carrier-rates/faq/page.tsx (was array index) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Summary
/refresh-preview/page.tsx,layout.tsx,globals.css,Header.tsx,Footer.tsxwith full hybrid content and design systemContactForm.tsx(Formspree-ready) andHybridAnimations.tsx(scroll progress + reveal + past-hero transitions)What changed
src/app/page.tsxsrc/app/layout.tsxsrc/app/globals.csssrc/components/Header.tsxsrc/components/Footer.tsxsrc/components/ContactForm.tsxNEXT_PUBLIC_FORMSPREE_IDis set, mailto fallback otherwisesrc/components/HybridAnimations.tsxsrc/app/insights/page.tsxsrc/app/insights/[slug]/page.tsxContact form — how to connect it (like skumanager.com)
The form is built Formspree-ready, which is the same pattern used by most Next.js studio/agency sites including skumanager.com:
xpzgkqab)NEXT_PUBLIC_FORMSPREE_ID=xpzgkqabThe form will POST JSON to
https://formspree.io/f/{ID}, Formspree emails you the submission, and the site shows a success message. No backend code required.Until the env var is set, the form pre-fills and opens a
mailto:hello@nitsof.comlink on submit — so it works immediately.What's still in
/refresh-preview/The old HTML preview files remain untouched (no breaking changes). They can be removed in a follow-up cleanup PR once this is confirmed in production.
Test plan
npm run dev→ openhttp://localhost:3000/— should show full hybrid home page/insightsand/insights/[slug]— cream theme, correct content🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Design Updates
Chores