Skip to content

Port hybrid design to root site (replaces /refresh-preview/) - #4

Merged
medoix merged 13 commits into
mainfrom
agent/frontend-designer/c6c92c2a
May 16, 2026
Merged

Port hybrid design to root site (replaces /refresh-preview/)#4
medoix merged 13 commits into
mainfrom
agent/frontend-designer/c6c92c2a

Conversation

@PrisStratton

@PrisStratton PrisStratton commented May 16, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Replaces the existing root site with the hybrid dark-hero → cream-body design — no longer preview-only HTML under /refresh-preview/
  • Contacts the root page.tsx, layout.tsx, globals.css, Header.tsx, Footer.tsx with full hybrid content and design system
  • Adds ContactForm.tsx (Formspree-ready) and HybridAnimations.tsx (scroll progress + reveal + past-hero transitions)
  • Updates Insights pages to match cream hybrid theme

What changed

File Change
src/app/page.tsx Full hybrid home page (hero, manifesto, capabilities, work, process, testimonial, insights preview, CTA)
src/app/layout.tsx Fontshare General Sans + Switzer + JetBrains Mono; updated metadata; no dark bg
src/app/globals.css Complete NITSOF design system (cream/cobalt/signal palette + all section CSS)
src/components/Header.tsx Adaptive sticky nav — dark on hero, cream on scroll; ticker strip; mobile hamburger
src/components/Footer.tsx Hybrid footer — Made in Australia + LinkedIn/GitHub + privacy/email
src/components/ContactForm.tsx NEW — 3-field form; Formspree when NEXT_PUBLIC_FORMSPREE_ID is set, mailto fallback otherwise
src/components/HybridAnimations.tsx NEW — client effects: scroll progress bar, reveal animations, past-hero class
src/app/insights/page.tsx Restyled to cream hybrid theme
src/app/insights/[slug]/page.tsx Restyled to cream hybrid theme

Contact 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:

  1. Sign up at formspree.io (free: 50 submissions/month; paid for more)
  2. Create a new form → copy the Form ID (looks like xpzgkqab)
  3. Add to your environment: NEXT_PUBLIC_FORMSPREE_ID=xpzgkqab
  4. In Vercel: Settings → Environment Variables → add it → redeploy

The 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.com link 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 → open http://localhost:3000/ — should show full hybrid home page
  • Scroll past hero → nav transitions from dark to cream
  • Contact form submits (mailto fallback if no Formspree ID)
  • /insights and /insights/[slug] — cream theme, correct content
  • Mobile: hamburger menu opens/closes

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added an insights/blog section with individual article pages
    • Integrated a contact form with email fallback
    • New client-side animation system: scroll progress and reveal effects
  • Design Updates

    • Complete homepage and marketing layout redesign
    • New site design system: palette, typography and comprehensive UI styles
    • Header now includes a scrolling ticker; footer simplified and updated
  • Chores

    • Added markdown processing and typography-related dependencies

Review Change Stack

medoix and others added 10 commits May 15, 2026 16:12
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>
@coderabbitai

coderabbitai Bot commented May 16, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

@PrisStratton has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 43 minutes and 23 seconds before requesting another review.

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 @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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 configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a452cbba-c3d2-477e-b8fd-47ee7f089c90

📥 Commits

Reviewing files that changed from the base of the PR and between f142a09 and 4e57fca.

⛔ Files ignored due to path filters (2)
  • src/app/favicon.ico is excluded by !**/*.ico
  • src/app/icon.svg is excluded by !**/*.svg
📒 Files selected for processing (3)
  • src/app/carrier-rates/faq/page.tsx
  • src/app/layout.tsx
  • src/app/privacy/page.tsx
📝 Walkthrough

Walkthrough

This 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.

Changes

Brand Refresh, Insights System, and Homepage Redesign

Layer / File(s) Summary
Dependencies and Root Layout
package.json, src/app/layout.tsx
Three new dependencies added; root layout switches to JetBrains Mono and two local fonts, enriches metadata/OpenGraph/Twitter and themeColor, and injects scroll-progress, grain-overlay, and HybridAnimations while simplifying main markup.
Design System and Global Styling
src/app/globals.css
Replaces prior globals with a NITSOF design system: CSS variables (cream/ink/dark), Tailwind typography directive, grain overlay, ticker, sticky nav/CTA, hero/bridge styles, manifesto, capabilities tiles, selected-work cards, timeline, pull-quote, insights listing/post styles, CTA form states, footer/client strip, fixed scroll progress, mobile menu UI, reveal utilities, and prefers-reduced-motion override.
Insights Data Layer and Utilities
src/lib/insights.ts
New module with InsightMeta/InsightPost interfaces; getAllInsights() enumerates and sorts markdown posts; getInsightBySlug() loads and parses a post returning HTML content via marked; formatDate() formats dates to en-AU locale.
Insights Content Articles and Pages
src/content/insights/*.md, src/app/insights/page.tsx, src/app/insights/[slug]/page.tsx
Three new insight articles added; insights index page with metadata and preview grid; dynamic per-slug page with generateStaticParams, per-post metadata, notFound handling, and HTML body injection.
Component Library Updates
src/components/Header.tsx, src/components/Footer.tsx, src/components/HybridAnimations.tsx, src/components/ContactForm.tsx
Header now has permanent ticker and simplified menu toggle; Footer simplified with flag and social links; new HybridAnimations manages reveal and scroll-progress behavior; ContactForm handles Formspree submission with mailto fallback and success/error states.
Homepage Complete Redesign
src/app/page.tsx
Full homepage rewrite into AI-native studio layout: hero with CTAs, bridge visual, manifesto, four-disciplines capabilities, selected work case studies, how-we-work timeline, pull-quote, conditional insights preview (first three posts), and Contact section embedding ContactForm.
Marketing Page Updates
src/app/carrier-rates/page.tsx, src/app/carrier-rates/faq/page.tsx, src/app/privacy/page.tsx
Carrier-rates page expanded with metadata, carriers/features arrays, and structured sections; FAQ converted to data-driven array with metadata; privacy page refactored into a section-array renderer with breadcrumb and updated last-updated date.

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
Loading
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
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • nitsof/nitsof.com#2: Overlapping changes to layout metadata, homepage content, and header navigation that form part of the same brand/homepage evolution.

Suggested reviewers

  • medoix

Poem

🐰 A rabbit hops through redesigned halls,
Insights bloom and ticker softly calls,
Fonts and gradients, pages fresh and bright,
Forms that send and animations light,
A fluffy cheer for code that feels just right.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Port hybrid design to root site (replaces /refresh-preview/)' directly describes the main change: moving the hybrid design system from a preview endpoint to the root site.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch agent/frontend-designer/c6c92c2a

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 9

🧹 Nitpick comments (2)
src/components/Header.tsx (1)

96-120: ⚡ Quick win

Avoid 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 win

Make filename the source of truth for slug.

Relying on frontmatter for slug can silently drift from the actual file path and break links/static params. Derive slug from 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

📥 Commits

Reviewing files that changed from the base of the PR and between f6ddaa2 and f819ec3.

⛔ Files ignored due to path filters (2)
  • package-lock.json is excluded by !**/package-lock.json
  • public/favicon.svg is excluded by !**/*.svg
📒 Files selected for processing (19)
  • package.json
  • public/refresh-preview/README.md
  • public/refresh-preview/hybrid.html
  • public/refresh-preview/index.html
  • public/refresh-preview/second.html
  • public/refresh-preview/swiss.html
  • src/app/globals.css
  • src/app/insights/[slug]/page.tsx
  • src/app/insights/page.tsx
  • src/app/layout.tsx
  • src/app/page.tsx
  • src/components/ContactForm.tsx
  • src/components/Footer.tsx
  • src/components/Header.tsx
  • src/components/HybridAnimations.tsx
  • src/content/insights/ai-native-delivery.md
  • src/content/insights/from-prototype-to-production.md
  • src/content/insights/shopify-shipping-complexity.md
  • src/lib/insights.ts

Comment thread package.json Outdated
Comment thread public/refresh-preview/hybrid.html Outdated
Comment thread public/refresh-preview/hybrid.html Outdated
Comment thread src/app/globals.css
Comment thread src/app/layout.tsx
Comment thread src/app/layout.tsx Outdated
Comment thread src/components/ContactForm.tsx
Comment thread src/components/HybridAnimations.tsx
Comment thread src/lib/insights.ts
const { data, content } = matter(raw);
return {
...(data as InsightMeta),
content: marked(content) as string,

@coderabbitai coderabbitai Bot May 16, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

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.

Suggested change
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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@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>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
src/app/privacy/page.tsx (1)

10-10: ⚡ Quick win

Remove unused helper function.

The section function 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 value

Consider 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

📥 Commits

Reviewing files that changed from the base of the PR and between f819ec3 and b868e71.

📒 Files selected for processing (5)
  • src/app/carrier-rates/faq/page.tsx
  • src/app/carrier-rates/page.tsx
  • src/app/layout.tsx
  • src/app/page.tsx
  • src/app/privacy/page.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/app/layout.tsx

PrisStratton and others added 2 commits May 16, 2026 22:43
- 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>
@medoix
medoix merged commit bd495d4 into main May 16, 2026
1 check passed
@medoix
medoix deleted the agent/frontend-designer/c6c92c2a branch May 16, 2026 23:18
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.

2 participants