This portfolio is easiest to understand as a static core with small dynamic edges. The repository uses the Next.js Pages Router, React, Tailwind CSS, and SWR, but those names are less important than the boundaries between them. Local content is built with the site. External data goes through server routes. Shared components make both sides feel like one terminal-inspired interface.

That architecture lets the blog, projects, and dashboard have different data needs without becoming separate applications.

The Pages Router is the map

The route structure is explicit. pages/blog.js, pages/projects.js, and pages/dashboard.js map directly to public URLs. pages/posts/[id].js represents individual Markdown posts. Server endpoints sit under pages/api, including /api/dashboard, /api/metrics, /api/project-repos, /api/activity, and the Spotify and Steam routes.

The application shell in pages/_app.js wraps every page with Layout and a global SWRConfig. It also loads Space Mono through next/font and mounts Vercel Analytics. There is no Framer Motion dependency in the current package or component tree. UI reveals and transitions come from local components and CSS.

The shared visual vocabulary is visible throughout the code: TermBar labels cards like terminal windows, Reveal handles entry presentation, and CSS variables such as --accent, --text-dim, and --border keep surfaces consistent. The design is not a skin added to one page; it is a small component language reused across navigation, projects, articles, metrics, and error states.

Static content stays local

The blog does not need a content service. lib/posts.js reads Markdown, uses gray-matter for frontmatter, converts bodies with remark and remark-html, and calculates reading time. The blog index uses getStaticProps; each post uses getStaticPaths and getStaticProps.

That build-time pipeline also supports features beyond the article body. Metadata drives blog search, tags, SEO descriptions, RSS entries, sitemap dates, and previous/next post links. After a post mounts, a focused client enhancement adds heading anchors and copy controls to code blocks.

The complete path is documented in How This Site Pre-renders Its Blog. The important architectural choice is that writing owned by the repository does not depend on a live provider.

Dynamic data crosses server boundaries

The live sections use a different route. Browser components call this site’s API endpoints with SWR, while helpers in lib talk to external services. For GitHub, that helper is lib/github.js; there is no /api/github-profile route. /api/project-repos returns repository metadata and a profile summary, while /api/metrics and /api/dashboard include aggregated GitHub counts.

The dashboard endpoint demonstrates the server-side composition pattern:

const [githubResult, steam, topTracks, nowPlaying] = await Promise.all([
  aggregateUserStats('mofdabo'),
  aggregateSteam().catch(() => null),
  safeTopTracksLong(),
  safeNowPlaying(),
])

The route creates one payload containing metrics, Steam data, top tracks, now-playing data, and status metadata for each source. Its safe Spotify wrappers return usable fallback shapes when the provider is unavailable. GitHub errors similarly become zero-value metrics rather than breaking the whole response.

On the client, the dashboard requests that aggregate every 60 seconds and deduplicates requests for 30 seconds. Its activity timeline polls every 120 seconds with a 60-second deduplication window. The projects page does not poll GitHub continuously; it uses a five-minute deduplication interval for /api/project-repos. Server Cache-Control headers add another reuse layer, so browser polling cadence and upstream request cadence are not assumed to be identical.

The details of the GitHub branch are covered in Integrating GitHub Metrics Without a Client SDK.

Loading, failure, and freshness are UI states

External integrations are not treated as guaranteed. Components render terminal-style skeletons before data arrives and DataError controls with retry actions when requests fail. FreshnessBadge displays source timestamps and status values. The activity timeline can identify a partial feed and keep cached data visible when one provider is degraded.

This is more honest than presenting every number as equally current. It also matches the server payload, which records generatedAt, source fetchedAt values, and provider status.

Some client-only areas are isolated deliberately. The dashboard loads TopTracks and RecentTracks with next/dynamic and ssr: false. Local achievements wait until mount before reading localStorage, ensuring the server pass and first client render agree.

Metadata follows the same design

components/Seo.js centralizes canonical URLs, Open Graph tags, Twitter cards, and JSON-LD. When a post has no socialImage, the component builds a URL for /api/og. That edge route uses ImageResponse to generate a 1200-by-630 terminal-style image from the page title and subtitle. The old placeholder image fields in these posts were removed specifically to use that fallback.

The result is a portfolio whose parts have clear jobs: Markdown and route metadata provide durable content; API routes protect credentials and normalize changing sources; SWR manages browser revalidation; and shared components keep the experience coherent. Static Generation and Dynamic Data in This Portfolio explains why those timing choices matter in the Pages Router.