Skip to content

Portfolio page

A personal site where availability and work come before biography.

Contentstarterportfoliopersonalfreelanceavailability

Live preview

Open the full page
full widthLive preview — open it in a new tab for the full-height version.
Open the preview in a new tab

Source

This exact file renders the preview above.

import EditorialNavbar from '@/components/blocks/navigation/editorial'
import PortfolioHeader from '@/components/blocks/headers/portfolio'
import CaseStudyIndexList from '@/components/blocks/sections/case-studies/index-list'
import QuoteOutcomeCaseStudy from '@/components/blocks/sections/case-studies/quote-outcome'
import EditorialIndexBlog from '@/components/blocks/sections/blog/editorial-index'
import CompanyHistoryTimeline from '@/components/blocks/sections/timeline/company-history'
import BookingCTAContact from '@/components/blocks/sections/contact/booking-cta'
import MinimalFooter from '@/components/blocks/footers/minimal'

/**
 * Portfolio page
 *
 * A personal site, which means availability and work come before biography.
 * The writing index sits below the work because it is what keeps people
 * returning, not what gets them to hire you.
 */
export default function PortfolioPattern() {
  return (
    <>
      <EditorialNavbar />
      <main>
        <PortfolioHeader />
        <CaseStudyIndexList />
        <QuoteOutcomeCaseStudy />
        <EditorialIndexBlog />
        <CompanyHistoryTimeline />
        <BookingCTAContact />
      </main>
      <MinimalFooter />
    </>
  )
}

components/patterns/portfolio.tsx

import EditorialNavbar from '@/components/blocks/navigation/editorial'
import PortfolioHeader from '@/components/blocks/headers/portfolio'
import CaseStudyIndexList from '@/components/blocks/sections/case-studies/index-list'
import QuoteOutcomeCaseStudy from '@/components/blocks/sections/case-studies/quote-outcome'
import EditorialIndexBlog from '@/components/blocks/sections/blog/editorial-index'
import CompanyHistoryTimeline from '@/components/blocks/sections/timeline/company-history'
import BookingCTAContact from '@/components/blocks/sections/contact/booking-cta'
import MinimalFooter from '@/components/blocks/footers/minimal'

/**
 * Portfolio page
 *
 * A personal site, which means availability and work come before biography.
 * The writing index sits below the work because it is what keeps people
 * returning, not what gets them to hire you.
 */
export default function PortfolioPattern() {
  return (
    <>
      <EditorialNavbar />
      <main>
        <PortfolioHeader />
        <CaseStudyIndexList />
        <QuoteOutcomeCaseStudy />
        <EditorialIndexBlog />
        <CompanyHistoryTimeline />
        <BookingCTAContact />
      </main>
      <MinimalFooter />
    </>
  )
}

components/blocks/headers/portfolio.tsx

import Link from 'next/link'
import { ArrowUpRight, MapPin } from 'lucide-react'
import { Container } from '@/components/ui/layout'
import { Status } from '@/components/ui/status'

/**
 * Portfolio header
 *
 * A person, not a product: name, what they do, where they are, and whether
 * they are available. Availability is a Status rather than a badge, so it
 * carries an icon and a word — the one piece of information a visitor is
 * actually here to check.
 */
const links = [
  { label: 'Selected work', href: '/starters/portfolio/preview/projects' },
  { label: 'Writing', href: '/starters/portfolio/preview/writing' },
  { label: 'Contact', href: '/starters/portfolio/preview/contact' },
]

export default function PortfolioHeader() {
  return (
    <header className="border-b border-line bg-canvas py-section">
      <Container size="narrow">
        <div className="flex flex-wrap items-center gap-x-4 gap-y-2">
          <h1 className="display-type text-3xl font-semibold text-ink-strong sm:text-4xl">
            Amara Osei
          </h1>
          <Status appearance="pill" kind="operational" label="Available from June" />
        </div>

        <p className="mt-5 max-w-xl text-md leading-relaxed text-ink-muted sm:text-lg">
          Frontend architect working on design systems, accessibility and the parts of performance
          that turn out to be architecture problems.
        </p>

        <div className="mt-6 flex items-center gap-1.5 text-sm text-ink-subtle">
          <MapPin className="size-4" aria-hidden="true" />
          Accra, Ghana — remote across GMT ± 3
        </div>

        <nav aria-label="Portfolio sections" className="mt-8 border-t border-line pt-6">
          <ul className="flex flex-wrap gap-x-6 gap-y-2">
            {links.map((link) => (
              <li key={link.href}>
                <Link
                  href={link.href}
                  className="inline-flex items-center gap-1.5 text-sm font-medium text-ink underline underline-offset-4 hover:text-accent"
                >
                  {link.label}
                  <ArrowUpRight className="size-3.5" aria-hidden="true" />
                </Link>
              </li>
            ))}
          </ul>
        </nav>
      </Container>
    </header>
  )
}

components/blocks/sections/case-studies/index-list.tsx

import Link from 'next/link'
import { ArrowRight } from 'lucide-react'
import { Container } from '@/components/ui/layout'
import { caseStudies } from '@/content/demo'

/**
 * Case study index list
 *
 * A dense table-like index for a work archive. Hairline rows scan faster than
 * cards once a portfolio passes about eight entries, and the sector and year
 * columns give a reader something to filter by mentally.
 */
export default function CaseStudyIndexList() {
  return (
    <section className="border-b border-line bg-canvas py-section">
      <Container>
        <h2 className="display-type text-2xl font-semibold text-ink-strong sm:text-3xl">
          Work archive
        </h2>

        <ul className="mt-8 divide-y divide-[var(--color-border-subtle)] border-y border-line">
          {caseStudies.map((study) => (
            <li key={study.slug}>
              <Link
                href={`/starters/agency/preview/work/${study.slug}`}
                className="group grid gap-x-6 gap-y-1 py-5 sm:grid-cols-[auto_1fr_auto_auto] sm:items-baseline"
              >
                <span className="label-caps text-ink-subtle sm:w-24">{study.year}</span>
                <span className="min-w-0">
                  <span className="block text-md font-semibold text-ink-strong group-hover:text-accent">
                    {study.title}
                  </span>
                  <span className="mt-0.5 block text-sm text-ink-muted">{study.client}</span>
                </span>
                <span className="text-xs text-ink-subtle sm:w-32 sm:text-right">
                  {study.sector}
                </span>
                <ArrowRight
                  className="hidden size-4 text-ink-subtle transition-transform group-hover:translate-x-0.5 group-hover:text-accent sm:block"
                  aria-hidden="true"
                />
              </Link>
            </li>
          ))}
        </ul>
      </Container>
    </section>
  )
}

Demo source — adapt to your project. Foundry is not published as a package.

Usage

The writing index sits below the work because it is what keeps people returning, not what gets them to hire you.

  • Availability belongs in the header. It is the one thing most visitors came to check.
  • One deep case study beats five shallow ones on a personal site.

Variants and states

Every entry below is a genuine difference in behaviour or layout, and every one of them is visible in the preview above.

  • Editorial masthead
  • Availability status in the header
  • Work index and one deep study
  • Booking-led contact

Accessibility

Availability
Status carries an icon and a word, so it survives greyscale.
Index links
Every archive row is a single anchor with a descriptive name.

Foundry implements published ARIA patterns and is tested against them. No WCAG certification is claimed — see the accessibility documentation for what is and is not covered.