My first version of this post reduced pre-rendering to two definitions: Static Generation happens at build time, while Server-side Rendering happens for each request. That distinction is correct, but it does not explain the more useful question: what does this portfolio actually pre-render, and where does the content come from?
The clearest answer is the blog. Its source is a directory of Markdown files, and the Pages Router turns those files into a blog index and individual post pages during the build.
Markdown is the source of truth
The pipeline begins in lib/posts.js. It reads the posts directory with Node’s filesystem APIs, parses each file’s frontmatter with gray-matter, normalizes tags, and calculates reading time. For a full post, it passes the Markdown body through remark and remark-html:
const matterResult = matter(fileContents)
const processedContent = await remark()
.use(html)
.process(matterResult.content)
const contentHtml = processedContent.toString()
That work is appropriate at build time because the input is local and versioned with the site. A visitor does not need to trigger filesystem reads or Markdown conversion. The deployed page receives HTML that has already been produced from the checked-in post.
The helper also keeps process-level caches for the post list and rendered post content. Those caches are useful while a build asks for the same information more than once; they are not presented as a cross-deployment data store.
Building the blog index
pages/blog.js exports getStaticProps, which calls getSortedPostsData() and passes the resulting metadata into the page:
export async function getStaticProps() {
const allPostsData = getSortedPostsData()
return {
props: { allPostsData },
}
}
The generated page already knows each post’s slug, date, title, description, tags, and reading time. Search is then performed in the browser against that supplied list. Typing into the search field does not call an API or rebuild the page; React filters data that was included in the static payload.
This is a useful split. The post catalog changes only when Markdown changes, so it is generated with the deployment. The query changes constantly while a reader types, so it stays as local UI state.
Building every post route
The dynamic filename pages/posts/[id].js may look request-driven, but the route is statically generated. getStaticPaths returns one path for each Markdown filename, and fallback: false limits the valid routes to that build-time set:
export async function getStaticPaths() {
return {
paths: getAllPostIds(),
fallback: false,
}
}
For each path, getStaticProps calls getPostData(params.id). It also reads the sorted post list to determine previous and next links. By deployment time, /posts/pre-rendering, /posts/ssg-ssr, and the other post URLs have complete HTML and navigation data.
Some progressive enhancement still happens after the page loads. A useEffect adds stable anchors to second- and third-level headings and copy buttons to code blocks. That client-side behavior improves an already rendered article; it is not responsible for fetching or creating the article itself.
Static content beside live data
Pre-rendering the blog does not force the entire portfolio to become a static export. The dashboard and projects page call Pages Router API routes for data that can change independently of a deployment. /api/dashboard, /api/metrics, and /api/project-repos execute server-side, set cache headers, and return JSON. SWR consumes those responses in the browser.
That boundary is the practical architecture: build local writing ahead of time, but request external activity through server routes when the interface needs it. The API routes also keep provider credentials and normalization logic out of the client bundle.
For a closer comparison of those timing choices, continue with Static Generation and Dynamic Data in This Portfolio. The broader structure is covered in Designing the Current Portfolio Architecture.