Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 31 additions & 15 deletions .github/workflows/pages.yml
Original file line number Diff line number Diff line change
@@ -1,45 +1,61 @@
name: Deploy GitHub Pages
name: Validate and deploy GitHub Pages

on:
pull_request:
branches: [main]
push:
branches: [main]
workflow_dispatch:

permissions:
contents: read
pages: write
id-token: write

concurrency:
group: pages
group: pages-${{ github.ref }}
cancel-in-progress: false

jobs:
build-and-deploy:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
build:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
persist-credentials: false
- name: Set up Node
uses: actions/setup-node@v4
with:
node-version: 24
cache: npm
- name: Guard immutable and append-only preservation fixtures
env:
BASE_SHA: ${{ github.event.pull_request.base.sha || github.event.before }}
run: npm run check:published-copy
- name: Install dependencies
run: npm ci --ignore-scripts
- name: Validate and build
run: npm run check
- name: Export static pages
run: npm run export:static
- name: Configure Pages
uses: actions/configure-pages@v5
- name: Upload Pages artifact
uses: actions/upload-pages-artifact@v3
if: github.ref == 'refs/heads/main' && github.event_name != 'pull_request'
uses: actions/upload-pages-artifact@v4
with:
path: github-pages
path: out

deploy:
if: github.ref == 'refs/heads/main' && github.event_name != 'pull_request'
needs: build
runs-on: ubuntu-latest
permissions:
pages: write
id-token: write
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- name: Configure Pages
uses: actions/configure-pages@v5
- name: Deploy
id: deployment
uses: actions/deploy-pages@v4
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ yarn-error.log*

# typescript
next-env.d.ts
*.tsbuildinfo

# legacy and local build artifacts
/dist/
/.wrangler/
/outputs/
Expand Down
31 changes: 17 additions & 14 deletions app/README.md
Original file line number Diff line number Diff line change
@@ -1,25 +1,28 @@
# Application Routes

This directory contains every public page and the shared site interface.

The router requires the filename `page.tsx`; the directory determines the URL.
For a complete page-to-file index, see:

```text
website-maintenance/01-page-file-map.md
```
This directory uses the official Next.js App Router. Static pages keep their
bespoke copy beside their markup; repeatable collections are driven by typed,
validated registries.

Key routes:

- `page.tsx` — Home.
- `education/page.tsx` — Education.
- `past-experience/` — Past Experience directory and five domain pages.
- `past-experience/page.tsx` — Past Experience directory.
- `past-experience/[slug]/page.tsx` — all five experience domain pages.
- `now/page.tsx` — Current Chapter at `/now/`.
- `personal-posts/` — Personal Posts index and article pages.
- `personal-posts/page.tsx` — Personal Posts index.
- `personal-posts/[slug]/page.tsx` — all article routes.

Shared files:
Shared responsibilities:

- `components/SiteShell.tsx` — navigation, profile sidebar, and footer.
- `components/SiteShell.tsx` — navigation, profile sidebar, footer, and skip link.
- `globals.css` — approved visual system and responsive behavior.
- `layout.tsx` — global metadata, fonts, and browser icons.
- `lib/` — source parsing and route-independent logic.
- `layout.tsx` — global metadata, icons, and Person structured data.
- `lib/content/site.ts` — site constants, navigation, and metadata helper.
- `lib/content/experience.ts` — strict archive parser and experience registry.
- `lib/content/posts.ts` — article metadata registry.
- `lib/content/routes.ts` — canonical public routes used by the sitemap.
- `robots.ts`, `sitemap.ts`, and `not-found.tsx` — static discovery and failure pages.

See `website-maintenance/01-page-file-map.md` for the complete source map.
48 changes: 28 additions & 20 deletions app/components/SiteShell.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,19 @@
import Link from "next/link";
import type { ReactNode } from "react";

type ActivePage = "home" | "now" | "education" | "experience" | "posts";

const navigation: Array<{ key: ActivePage; label: string; href: string }> = [
{ key: "home", label: "Home", href: "/" },
{ key: "education", label: "Education", href: "/education/" },
{ key: "experience", label: "Past Experience", href: "/past-experience/" },
{ key: "now", label: "Current Chapter", href: "/now/" },
{ key: "posts", label: "Personal Posts", href: "/personal-posts/" },
];
import { navigation, type ActivePage } from "../lib/content/site";

export function SiteShell({
active,
children,
}: {
active: ActivePage;
active?: ActivePage;
children: ReactNode;
}) {
return (
<div className="site-shell">
<a className="skip-link" href="#main-content">
Skip to main content
</a>
<header className="topbar">
<div className="topbar-inner">
<Link className="wordmark" href="/" aria-label="Theodore Ouyang home">
Expand All @@ -44,15 +38,27 @@ export function SiteShell({
<aside className="profile-sidebar" aria-label="Profile">
<div className="avatar-frame">
{/* Static GitHub Pages deployment serves this pre-sized portrait directly. */}
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src="/assets/profile/theodore-avatar-warm.png"
alt="Illustrated portrait of Theodore Ouyang"
width="192"
height="192"
/>
<picture>
<source
type="image/avif"
srcSet="/assets/profile/theodore-avatar-warm-384.avif 384w, /assets/profile/theodore-avatar-warm-768.avif 768w"
sizes="(max-width: 470px) 86px, (max-width: 720px) 104px, (max-width: 930px) 148px, 164px"
/>
<source
type="image/webp"
srcSet="/assets/profile/theodore-avatar-warm-384.webp 384w, /assets/profile/theodore-avatar-warm-768.webp 768w"
sizes="(max-width: 470px) 86px, (max-width: 720px) 104px, (max-width: 930px) 148px, 164px"
/>
<img
src="/assets/profile/theodore-avatar-warm.png"
alt="Illustrated portrait of Theodore Ouyang"
width="1024"
height="1536"
sizes="(max-width: 470px) 86px, (max-width: 720px) 104px, (max-width: 930px) 148px, 164px"
/>
</picture>
</div>
<h2>Theodore Ouyang</h2>
<p className="profile-name">Theodore Ouyang</p>
<p className="sidebar-bio">
<span>Strategy at a World Model Unicorn</span>
<span>Duke B.S. &amp; M.Eng.</span>
Expand All @@ -72,7 +78,9 @@ export function SiteShell({
</ul>
</aside>

<main className="content-column">{children}</main>
<main className="content-column" id="main-content">
{children}
</main>
</div>

<footer className="site-footer">
Expand Down
6 changes: 4 additions & 2 deletions app/education/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { Metadata } from "next";
import { SiteShell } from "../components/SiteShell";
import { createPageMetadata } from "../lib/content/site";

const courseworkGroups = [
{
Expand Down Expand Up @@ -55,10 +56,11 @@ const courseworkGroups = [
},
] as const;

export const metadata: Metadata = {
export const metadata: Metadata = createPageMetadata({
title: "Education",
description: "Theodore Ouyang’s education at Duke University.",
};
path: "/education/",
});

export default function EducationPage() {
return (
Expand Down
Loading
Loading