I created this portfolio to have one durable place for my software engineering experience, projects, and technical writing. The main constraint was clarity: a hiring manager should understand the work quickly, while another engineer can inspect the implementation details.

I kept the content in the repository instead of adding a CMS. Four project case studies and a small set of typed blog records did not justify another service, authentication flow, or deployment dependency.

Next.js handles the shared layout, file-based routes, per-page metadata, and static generation. TypeScript keeps the post records and route contracts explicit, while one global stylesheet carries the visual system across every page.

Article map
Typed contentApp RouterStatic paramsProduction buildReader

A small content model was enough

Each post is a typed object with a slug, title, description, publication date, article sections, diagrams, code samples, and sources. That keeps authoring close to the code and makes missing fields a compile-time problem.

The tradeoff is deliberate: editing requires a code change and a deployment. If the writing library grows enough to need drafts, multiple authors, or non-technical editors, that is the point to introduce a content layer.

The route structure mirrors the reader journey

The root layout owns the navigation rail, mobile header, footer, metadata defaults, and global styles. The blog index sorts typed post records, while the dynamic [slug] route resolves one article.

generateStaticParams lists known slugs during the production build. generateMetadata uses the same post record for the page title and description, and notFound handles an unknown slug instead of rendering a half-empty template.

The design system stays in one place

CSS custom properties define the paper, ink, muted text, rules, teal tide, signal colour, and shared type scale. Components then use semantic class names instead of repeating values.

The desktop rail becomes a sticky mobile header below the layout breakpoint. Dark mode follows the operating-system preference, visible focus styles remain intact, and motion is disabled when the reader requests reduced motion.

Step by step

From content to a verified page

  1. Define the page hierarchy and the one job each route needs to do.
  2. Store the small amount of content in typed records and avoid a CMS until editing needs justify it.
  3. Build the shared shell in the root layout and use file-system routes for pages and posts.
  4. Generate static article paths and metadata from the same source of truth.
  5. Create shared colour, typography, spacing, focus, dark-mode, and reduced-motion rules.
  6. Check desktop and mobile rendering, then require lint and a production build to pass.

Code & commands

Working notes

Static article routes

export function generateStaticParams() {
  return posts.map((post) => ({
    slug: post.slug,
  }));
}

Release check

npm run lint
npm run build

Typed content contract

type BlogPost = {
  slug: string;
  title: string;
  publishedAt: string;
  sections: ArticleSection[];
  samples: CodeSample[];
};

Per-post metadata

export async function generateMetadata({ params }) {
  const post = getPostBySlug((await params).slug);

  return post
    ? { title: post.title, description: post.description }
    : { title: "Post not found" };
}

AI-assisted workflow

Use AI to narrow the work, not outsource judgment.

I used AI as an editor and implementation partner: it could organize verified facts, compare repeated UI patterns, and suggest a small patch. I still owned the content, inspected every diff, and required the normal checks to pass.

Draft from verified facts

Example prompt

Turn the facts below into a concise case-study section.

Rules:
- Use only the supplied facts.
- Do not invent metrics, incidents, or user feedback.
- Separate implementation facts from recommendations.
- Return a heading and two short paragraphs.

Facts:
{{paste reviewed notes here}}

Human checks

  • Compare every claim with the resume, repository, or cited source.
  • Remove generic praise and anything that cannot be demonstrated.
  • Read the result in the voice of the rest of the site.

Diagnose a responsive layout

Example prompt

Review this CSS and the mobile measurements.

Viewport: 390px
document.scrollWidth: 627px
document.clientWidth: 390px

Find the narrowest selector that causes horizontal overflow.
Prefer a CSS fix over JavaScript. Explain how to verify it.

CSS:
{{paste the relevant grid and pre rules}}

Human checks

  • Confirm the suspected element in the browser instead of trusting the guess.
  • Verify that the page width matches the viewport after the change.
  • Run lint and a production build before accepting the patch.

Failure modes & troubleshooting

When the first path fails

The development server panics inside the Turbopack persistence cache.

Stop the server, move the generated .next directory aside, restart, and confirm the clean production build still passes. Source files do not belong in that cache.

A dynamic post renders but its title or description is missing from metadata.

Resolve both the page and generateMetadata from the same slug lookup, and return notFound for a missing record.

The desktop layout looks balanced but mobile content overflows.

Test the actual breakpoint, compare document scroll width with client width, and make diagrams and code blocks collapse or scroll intentionally.

Animation makes the page appear empty during capture or distracts from reading.

Keep the entrance short, verify the settled state, and disable non-essential motion under prefers-reduced-motion.

Keep

Practical takeaways

  • Start with the smallest content system that fits the real editing workflow.
  • Use one source of truth for routes, metadata, and rendered content.
  • Treat responsive and production checks as part of the feature, not cleanup.