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
DaisyUI^5.5.14Tailwind component library
PostCSS^8.5.6CSS processing
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)"]
        speaking["app/speaking/<br/>(talk pages + decks)"]
    end

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

    subgraph styling["Styling"]
        tw["Tailwind CSS 4"]
        daisy["DaisyUI"]
        globals["globals.css<br/>(CSS variables)"]
    end

    layout --> header & footer
    home --> sections
    sections --> tw & daisy
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)
│ ├── page.tsx # Home page (section composition)
│ ├── globals.css # Tailwind imports + CSS variables
│ └── speaking/
│ ├── layout.tsx # Speaking section metadata
│ ├── page.tsx # Speaking index
│ ├── spec-driven-development/
│ │ ├── page.tsx # Talk detail page
│ │ └── deck/page.tsx # Slide deck
│ └── dark-factory/
│ ├── page.tsx # Talk detail page
│ └── deck/page.tsx # Slide deck
├── components/
│ ├── SiteHeader.tsx # Fixed navigation with mobile menu
│ ├── SiteFooter.tsx # Footer with commit SHA
│ ├── BookCover.tsx # Book cover component
│ └── sections/
│ ├── HeroSection.tsx
│ ├── TickerBar.tsx
│ ├── DocsBanner.tsx
│ ├── AboutSection.tsx
│ ├── CapabilitiesSection.tsx
│ ├── DeliverySection.tsx
│ ├── ClientsSection.tsx
│ ├── TimelineSection.tsx
│ ├── SpecMcpSection.tsx
│ ├── WritingSection.tsx
│ ├── CertificationsSection.tsx
│ └── ContactSection.tsx
├── hooks/
│ └── useRevealOnScroll.ts # IntersectionObserver for scroll animations
├── lib/
│ └── constants.ts # Layout constants (container config)
├── 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. SpecMcpSection — SpecMCP project highlight
  10. WritingSection — published works
  11. CertificationsSection — professional certifications
  12. ContactSection — contact details

The /speaking route contains talk detail pages and slide decks. Each talk has its own directory with a detail page and a /deck sub-route for the presentation slides.

Tailwind is imported via the new v4 CSS-first configuration:

@import "tailwindcss";

PostCSS processes Tailwind via @tailwindcss/postcss. Custom CSS variables define the brand colour palette and typography scale in globals.css.

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

DaisyUI provides pre-built Tailwind components (buttons, cards, navigation). It is installed as a dev dependency and integrated via the Tailwind plugin system.

The useRevealOnScroll hook uses the IntersectionObserver API to add a .revealed 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