<- Back to blog

How I Built My Personal Website with Next.js

How I built and iterated on my portfolio, from its Next.js architecture and motion design to SEO and Markdown access for AI agents.

By Han Jo Xi

My personal website brings together my interests in software engineering, artificial intelligence, data analytics and web development. I wanted it to be more than a static page with my name on it, so I built it myself and used it to learn how the pieces of a modern website fit together.

The site has changed considerably since I started it. My commit history includes repeated design iterations, animation adjustments, image optimisation, search-engine tooling and, more recently, work on making the site's content easier for AI agents to discover and read. Not every experiment stayed: I tried replacing the skills marquee with an interactive metro-style map, then reverted that change. That is part of building a real project too. An idea can be worth testing without being right for the final interface.

You can explore the current result on my homepage, browse my projects, or read the other articles on my blog.

How I Built My Personal Website with Next.js

Choosing the stack

The site is built with Next.js, React and TypeScript. Next.js provides routing, page rendering, metadata and API routes in one application. TypeScript helps keep the interfaces between components and the structured portfolio data consistent as the project changes.

The project uses the Next.js App Router. The homepage is assembled from separate components for the About section, Skills section, Projects section and Certifications section, along with navigation, links and contact details. Larger homepage sections are dynamically imported where appropriate, rather than keeping every section in one large page component.

I keep much of the site's content in data/portfolio.ts. That gives the homepage a central source for information such as my skills, projects, certifications and links, instead of scattering those details through presentation code. The result is easier to update because content and layout have clearer responsibilities.

Building the visual design

Tailwind CSS handles much of the layout, while app/globals.css contains global styles, theme variables and reusable visual rules. The design uses a dark background, purple accents, clear section labels and a mix of display typography and monospace details. The aim is to make the site feel technical without making it difficult to read.

I use Syne for the main typography and JetBrains Mono for smaller labels and technical details. The interface also includes a responsive navigation menu, a custom hero section, background effects and a consistent style across the homepage and blog.

The visual design was not settled in one pass. The commit history shows many small changes to the hero, navigation, global styles, projects and skills components. I also tried different visual treatments and reverted some of them. Iterating on the actual interface helped me judge changes in context instead of assuming the first implementation was the best one.

Motion and interaction

I use a mix of small reusable motion components and more involved scroll effects. The hero includes animated shimmer text, and the skills section uses an infinite slider to move technology icons across the page. The blog has a scroll-progress indicator so readers can see how far they have progressed through an article.

For larger homepage transitions, HomeMotion dynamically loads GSAP, ScrollTrigger and Lenis on desktop. GSAP handles scroll-triggered reveals and project-section effects, while Lenis provides smooth scrolling. The code avoids enabling these effects when the user prefers reduced motion, and it does not load the same animation setup on smaller screens. Keeping the heavier motion code behind a dynamic import also avoids loading it unconditionally.

I use effects selectively. Animation should help establish hierarchy or give feedback, not compete with the content. The skills section is a useful example of experimentation: I tested an interactive metro-style map with categories and descriptions, but later reverted it and returned to the marquee. The current interface is the result of those decisions, not a record of every prototype I tried.

Writing the blog with MDX

Blog posts live in content/blog as MDX files. Each file exports metadata such as its title, description, publication date, author and tags, followed by content written in Markdown with the option to use React components.

The posts are registered in lib/blog.ts. That registry supplies the blog index, static route generation and page metadata. The individual article pages are generated for known slugs, so adding a post does not require building a separate page component from scratch.

The blog also has reusable presentation components. For example, BlogImage wraps Next.js image handling and requires explicit alternative text and dimensions, while BlogScrollProgress provides the reading-progress indicator. These components keep repeated behaviour consistent and make articles easier to maintain.

Search engines and structured metadata

Search visibility was part of the implementation rather than an afterthought. The site defines page titles, descriptions, canonical URLs, Open Graph metadata and Twitter card metadata. Blog articles also include BlogPosting JSON-LD, while the homepage provides structured data describing the site and its person profile.

The sitemap is generated from the same blog registry used to list and route articles. This reduces the chance of adding a post to the site but forgetting to include it in the sitemap. The project also has a robots file and an IndexNow workflow for submitting URLs for discovery. A build script can submit sitemap URLs to IndexNow after the Next.js build.

I also pay attention to technical details beyond metadata. The Next.js configuration defines security-related response headers, including a Content Security Policy, HSTS, frame restrictions, a referrer policy and a permissions policy. These settings are part of the site's delivery configuration, not just its visual layer.

Making the site easier for AI agents to read

More recently, I added a machine-readable path through the site's content. The root llms.txt file summarises the portfolio and links to the important sections, projects and blog posts. It gives AI tools a compact starting point instead of requiring them to infer the site's structure from navigation alone.

I also added a Markdown API at /api/markdown. It can return Markdown representations of supported pages, including the homepage, blog index and individual posts. The route responds with text/markdown, and the project includes API documentation and an API catalogue describing the available endpoints. This is a dedicated endpoint, rather than relying on every normal HTML page to change format based on the request.

The motivation is straightforward: a page can be visually polished and still be awkward for a machine to process. Providing structured links and a clean text representation makes the content easier to retrieve and parse. It does not guarantee that an AI system will use or cite the site, but it removes some unnecessary friction.

Images and performance

Images are part of the design, but they need to be delivered carefully. The hero uses responsive image sources, and the repository includes image assets for projects, backgrounds and skill icons. I also merged an image-optimisation change through ImgBot, which reduced the weight of image assets without requiring a redesign of the page.

The site uses caching headers for selected static asset paths. On the application side, dynamically importing heavier sections and loading the desktop motion libraries only when needed helps avoid treating every feature as essential on every page.

Performance is not just about one optimisation. It comes from a collection of decisions about image formats, loading behaviour, JavaScript and how much motion to run. I continue to adjust those trade-offs as the design evolves.

Deployment and domain management

The application is deployed with Vercel, which builds and serves the Next.js project from the repository. I use Cloudflare for domain and DNS management, keeping the domain layer separate from the application code.

This setup gives me a straightforward publishing workflow: make a change, check the result, and deploy it. It also means the website's content, application logic and deployment configuration can evolve together in version control.

What I learned from the process

The most useful lesson has been that a personal website is not just a design exercise. It brings together content modelling, component structure, rendering, accessibility, motion, performance, deployment, search metadata and machine-readable interfaces.

Git history made that process visible. Some commits introduced features; others refined them, fixed implementation details or reverted experiments that did not work as well as expected. That is why I do not treat the current site as the result of one perfect plan. It is the version that emerged from testing ideas and deciding which ones were worth keeping.

The stack gives me room to continue improving the website without making every change a rewrite. I can add content, refine the interface, or build better ways for people and tools to discover the information while keeping the core structure understandable.