Skip to content

kevinryan.io

The portfolio site at kevinryan.io is the primary web property for Kevin Ryan. It is a statically exported Next.js application served by nginx.

TechnologyVersionRole
Next.js16.1.4React framework (App Router, static export)
React19.2.3UI library
TypeScript^5Type safety (strict mode)
Tailwind CSS^4Utility-first styling (@theme token source)
PostCSS^8.5.6CSS processing via @tailwindcss/postcss
Fitty^2.4.2Responsive text fitting
ESLint^9Linting (with eslint-config-next)
graph TD
    subgraph app["Next.js App Router"]
        layout["app/layout.tsx<br/>(root layout)"]
        home["app/page.tsx<br/>(home)"]
    end

    subgraph components["Components"]
        header["SiteHeader"]
        footer["SiteFooter"]
        sections["Section components ×20"]
    end

    subgraph styling["Styling"]
        tw["Tailwind CSS 4 (@theme tokens)"]
        globals["globals.css<br/>(tokens + component classes)"]
    end

    layout --> header & footer
    home --> sections
    sections --> tw & globals
SettingValuePurpose
output'export'Static HTML export (no Node.js runtime)
images.unoptimizedtrueRequired for static export
trailingSlashtrueConsistent URL paths
reactStrictModetrueReact development checks
NEXT_PUBLIC_COMMIT_SHAFrom env or 'dev'Version display in footer
  • Strict mode enabled
  • Target: ES2017
  • Module resolution: bundler
  • Path alias: @/* maps to project root
sites/kevinryan-io/
├── app/
│ ├── layout.tsx # Root layout (fonts, analytics, header, footer)
│ ├── page.tsx # Home page (section composition)
│ ├── ai-native-readiness-assessment/
│ │ └── page.tsx # Assessment page (9 section components)
│ └── globals.css # @theme tokens + component classes (.section, .case, #assessment-root scoping)
├── components/
│ ├── SiteHeader.tsx # Fixed navigation (next/link) with mobile menu
│ ├── SiteFooter.tsx # Footer with commit SHA (rendered by root layout)
│ ├── Container.tsx # Max-width 1400px wrapper (replaces old CONTAINER constant)
│ ├── SectionHeader.tsx # Reusable section number/subtitle/title header
│ ├── Reveal.tsx # Scroll-reveal wrapper (.reveal class)
│ ├── AssessmentContactForm.tsx
│ └── sections/
│ ├── HeroSection.tsx
│ ├── TickerBar.tsx
│ ├── DocsBanner.tsx
│ ├── AboutSection.tsx
│ ├── CapabilitiesSection.tsx
│ ├── DeliverySection.tsx
│ ├── ClientsSection.tsx
│ ├── TimelineSection.tsx
│ ├── WritingSection.tsx
│ ├── CertificationsSection.tsx
│ ├── ContactSection.tsx
│ └── assessment/ # Assessment page sections
│ ├── AssessmentHero.tsx
│ ├── AssessmentProblem.tsx
│ ├── AssessmentCapabilities.tsx
│ ├── AssessmentEngagement.tsx
│ ├── AssessmentDeliverables.tsx
│ ├── AssessmentAudience.tsx
│ ├── AssessmentEvidence.tsx
│ ├── AssessmentCredibility.tsx
│ └── AssessmentCta.tsx
├── hooks/
│ └── useRevealOnScroll.ts # IntersectionObserver for scroll animations
├── public/ # Static assets (images, favicons)
├── Dockerfile
├── nginx.conf
├── next.config.ts
├── tsconfig.json
├── postcss.config.mjs
├── eslint.config.mjs
└── package.json

The home page is a composition of section components rendered in sequence. Each section is a standalone component in components/sections/:

  1. HeroSection — full-viewport hero with name, title, and animated text (Fitty)
  2. TickerBar — scrolling ticker with key phrases
  3. DocsBanner — link to documentation site
  4. AboutSection — professional summary
  5. CapabilitiesSection — skill areas
  6. DeliverySection — delivery methodology
  7. ClientsSection — client logos and names
  8. TimelineSection — career timeline
  9. WritingSection — published works
  10. CertificationsSection — professional certifications
  11. ContactSection — contact details

The root layout (app/layout.tsx) renders SiteHeader and SiteFooter once for every route, so individual pages only compose their own section content.

/ai-native-readiness-assessment composes 9 section components from components/sections/assessment/ (Hero, Problem, Capabilities, Engagement, Deliverables, Audience, Evidence, Credibility, Cta) inside a single #assessment-root wrapper. The page file is ~30 lines; all content lives in the components.

Tailwind v4 is configured CSS-first. The entire brand palette and typography scale live in a single @theme block in globals.css, which generates real Tailwind utilities (bg-accent, text-grey-400, font-display, …):

@import "tailwindcss";
@theme {
--color-accent: #A8E10C;
--color-accent-dim: #92C40A;
--color-black: #0A0A0A;
/* …grey/dark/white/pure-white… */
--font-sans: 'Archivo', …;
--font-display: 'Bebas Neue', …;
}

Components use Tailwind utility classes for layout and one-off styling. A small set of reusable composite classes (.section, .section--dark, .case, .timeline__*, .reveal, .nav-link) live in globals.css for repeated patterns that would otherwise blow up component files or require ::after/@keyframes (not expressible as utilities). Assessment-page-specific classes are scoped under #assessment-root so they never leak to the home page. No inline style={{}} objects are used (one justified exception: the HeroSection dual-gradient grid texture). DaisyUI was removed (it was a phantom dependency, never activated).

Five Google Fonts are loaded in the root layout:

FontWeight(s)Usage
Archivo400–900Primary body text
Bebas Neue400Display headings
Work Sans300, 900Accent text
DM Sans300–700Secondary body text
JetBrains Mono400–700Code and monospace

The useRevealOnScroll hook uses the IntersectionObserver API to add a .visible class to elements with the .reveal class when they enter the viewport. This drives CSS-based entrance animations without a third-party animation library.

The fitty library dynamically scales text to fill its container width. It is used in the hero section for responsive headline sizing that adapts to any viewport width.

The site uses output: 'export' in Next.js, which generates a fully static site at build time. There are no API routes, no server components with runtime data fetching, and no middleware. The output directory (out/) contains only HTML, CSS, JavaScript, and static assets.

The site uses a multi-stage Docker build:

  1. Build stage — Node.js 22 Alpine with pnpm runs next build, producing static files in out/
  2. Serve stage — nginx 1.28.2 Alpine serves the static output on port 8080

The COMMIT_SHA build argument is exposed as NEXT_PUBLIC_COMMIT_SHA, making the git commit hash available in the client bundle for version identification in the footer.

  • JSON structured access logs (consumed by Promtail/Loki)
  • Gzip compression for HTML, CSS, JS, JSON, and SVG
  • Long-lived cache headers for /_next/static/ (immutable hashed assets)
  • No-cache headers for HTML files (ensures fresh content)
  • /healthz endpoint for Kubernetes health probes
  • Security headers: X-Content-Type-Options, X-Frame-Options, Referrer-Policy
  • try_files fallback: $uri → $uri.html → $uri/index.html → /404.html