Back to projects
Software Engineering··

Lightweight Personal Website Template

A lightweight, content-first personal website template for publishing blogs and projects, with a Markdown workflow and a straightforward path to deployment.

Where the project came from

One Friday evening after work, I decided I wanted a personal website. I already used Notion as a second brain for notes and ideas, and it had become useful in both work and everyday life. A public site felt like a natural place to share the parts that might be useful to someone else.

I spent the night vibe coding the first version. I built the Next.js pages and the Blog and Project content models, connected Obsidian notes to an MDX publishing workflow, and set up GitHub Actions and Vercel. Then I bought a domain, configured DNS and HTTPS, and added search, RSS, a sitemap, and light and dark themes. By the time I stopped, the site was online. I liked the result enough to start extracting the reusable parts into a GitHub template.

What the project is

Lightweight Personal Website Template is the reusable foundation behind this site. It is for developers and creators who want an independent place to publish writing and present their work. A user can replace the profile, branding, and sample content, then write new pages in Markdown. There is no database or admin application to set up first.

The full MyWebsite repository also contains a Spring Boot backend, MySQL, and Redis for engineering practice. The public site does not depend on them. The template keeps the frontend and the content workflow, while the experimental backend and local infrastructure stay outside the core.

Design goals

I started with three constraints:

  1. Keep the site light and use as few dependencies as practical.
  2. Make publishing and maintenance simple enough for one person.
  3. Use a minimal visual style that leaves room for the content instead of decorative animation.

These constraints shaped both the interface and the project structure.

Design details

Keep content in the repository

Blogs and Projects are Markdown files. Each file has frontmatter for its title, summary, dates, tags, cover image, and related content. The body remains ordinary Markdown.

This is less convenient than editing in a hosted CMS, but I prefer owning the files and keeping their history beside the code. Content is also separate from the React components. I can revise the article layout without touching an article, and I can publish an article without editing a page component.

Put reading before interface

The home page introduces the site and shows recent work. Blogs, Projects, About Me, and Search have clear top-level navigation. Article pages use a narrow reading column, and the same design rules work in light mode, dark mode, and on small screens.

The template avoids elaborate animation and large interactive sections. Images, code blocks, and related links support the article. The page itself stays quiet.

Keep the public site small

Published content is read from local MDX during the build. Blogs and Projects do not need a database, and a page request does not call a content API. The deployed site is one Next.js application, so each release and rollback maps to a Git commit.

Comments, authentication, or other dynamic features can use a backend later. The template does not add services before the site needs them.

Technology choices

LayerTechnologyWhy I chose it
Web frameworkNext.js 16The App Router covers routing, static generation, metadata, RSS, and the sitemap in one application. It also deploys directly to Vercel.
UIReact 19Components keep the article layout, navigation, search, and theme controls reusable without turning every page into a client application.
LanguageTypeScriptContent metadata, component props, and route data have explicit types, which catches missing fields during refactors.
StylingTailwind CSSMost styles stay near their components, while shared colors and typography remain in global variables.
ContentMarkdown and MDXMarkdown works well for writing and version control. MDX adds syntax highlighting and a small set of custom content components.
Content pipelinegray-matter, unified, and remarkFrontmatter parsing, Markdown conversion, internal links, and asset validation all run locally.
Code highlightingShikiHighlighting happens during rendering, so readers do not download a browser-based editor.
HostingVercelGit integration produces a Preview for each pull request and deploys production after a merge.
Continuous integrationGitHub ActionsEvery change checks content synchronization, linting, TypeScript, and the production build.

Obsidian is an optional writing tool, not a runtime dependency. The source files are plain Markdown and work just as well in VS Code or another editor.

Project structure

The parts that matter to the template live in frontend and obsidian:

Plain text
MyWebsite/
  frontend/
    content/             # Generated MDX read by the site
    public/              # Cover images and article assets
    scripts/content/     # Content synchronization and validation
    src/app/             # Next.js routes and pages
    src/components/      # Navigation, articles, search, and UI
    src/data/            # Profile and site configuration
  obsidian/
    blog/                # Blog Markdown sources
    projects/            # Project Markdown sources
    project-posts/       # Child articles for collection projects
    assets/              # Source images used by content
    templates/           # Starting points for new notes

Markdown under obsidian is the source of truth. The files in frontend/content are generated output and should not be edited by hand.

Deploying a complete site

This workflow keeps the source on GitHub and deploys the public site with Vercel. It requires Node.js 22, Git, a GitHub account, and a Vercel account. A custom domain is optional.

1. Run the template locally

After creating a repository from the template, clone it and install the locked dependencies:

Bash
git clone <your-repository-url>
cd <your-repository-name>/frontend
npm ci
npm run dev

Open http://localhost:3000. Check the home page, Projects, Blogs, and About Me before making changes.

2. Replace the site information

Start with the name, role, and contact email in frontend/src/data/site.ts. Then update the home page, About Me page, metadata, and site branding. In the current project, the relevant files are:

Plain text
frontend/src/data/site.ts
frontend/src/app/layout.tsx
frontend/src/app/page.tsx
frontend/src/app/about/page.tsx
frontend/src/components/Header.tsx

Profile photos and shared images live in frontend/public/images. Update the import or public path whenever a filename changes.

Run the full local checks after customization:

Bash
cd frontend
npm run content:test
npm run content:check
npm run lint
npm run typecheck
npm run build

3. Push the customized site

Bash
git add .
git commit -m "Customize personal website"
git push origin main

A pull request works better when more than one person maintains the site. GitHub Actions checks the branch, and Vercel can attach a Preview URL before the change reaches main.

4. Create the Vercel project

Import the GitHub repository into Vercel and use these settings:

Plain text
Framework Preset: Next.js
Root Directory: frontend
Node.js Version: 22.x
Install Command: npm ci
Build Command: npm run build

After the first deployment, check the main routes and generated files:

Plain text
/
/blogs
/projects
/about
/search
/robots.txt
/sitemap.xml

5. Set the canonical URL

Once the public domain is known, add this environment variable in Vercel:

Bash
NEXT_PUBLIC_SITE_URL=https://example.com

The site uses it for canonical URLs, Open Graph metadata, RSS, and the sitemap. Redeploy after saving the value.

For a custom domain, add both the apex and www hostnames to Vercel, then copy the DNS records Vercel provides to the DNS provider. Choose one hostname as canonical and redirect the other. Finally, verify that HTTPS, redirects, and sitemap.xml all use the same hostname.

Publishing new content

Source content lives under obsidian. The folder can be opened as an Obsidian vault or edited with any text editor.

A Blog starts with frontmatter like this:

Yaml
---
status: "draft"
title: "My first post"
slug: "my-first-post"
summary: "A short description of the article."
coverImage: ""
publishedAt:
updatedAt: 2026-08-25
category: "Software Engineering"
tags: ["Next.js"]
pinned: false
relatedBlogs: []
relatedProjects: []
---

Keep status as draft while writing. When the article is ready, change it to published, add publishedAt, and run:

Bash
cd frontend
npm run content:sync
npm run content:check

The synchronization script reads the Markdown, validates frontmatter, internal links, and images, then generates the MDX used by the website. Commit the source Markdown, generated MDX, and any generated assets together.

Projects use the same workflow and can add techStack, demoUrl, and repositoryUrl. A standard Project renders as one article. A collection Project can hold a set of smaller articles under one project page.

Add an image to an article

Place the source image in obsidian/assets, then reference it with Obsidian image syntax:

Markdown
![[architecture.png|Architecture overview]]

content:sync copies the image into the article's public asset directory and rewrites its path in the generated MDX. The pipeline accepts PNG, JPEG, WebP, GIF, and AVIF files.

Publish to production

After the checks pass, push a branch and inspect the article in the Vercel Preview. Check its images, code blocks, search result, RSS entry, and sitemap entry. Merging the branch into main triggers the production deployment.

Maintenance

The template has no database migrations or separately hosted content to back up. Code, articles, and images are all stored in Git. Most maintenance work is dependency updates and release checks.

I run the following checks after a framework upgrade and use the same list for a periodic dependency review:

Bash
cd frontend
npm audit
npm outdated
npm run content:test
npm run content:check
npm run lint
npm run typecheck
npm run build

Do not store API keys, private notes, or unpublished personal attachments under obsidian, because the folder is committed to the public repository. Features that need credentials should read them from Vercel environment variables.

If a release is broken, revert its Git commit or pull request, run content:check, and let the next main deployment restore the previous version. To remove an article without deleting its history, change its status to archived and synchronize the content again.

If a future version adds a dynamic backend, its database and Redis instance should stay on a private network. The local Docker Compose file is not intended for a public server. For a personal site that only publishes Blogs and Projects, the frontend-only deployment is much easier to maintain.

Extracting the template

The template is built from the shared parts of MyWebsite. Personal content and the experimental backend are removed, and site branding will move into one configuration file. Sample Blogs and Projects, content validation, and Vercel deployment settings will remain so a new user can replace the examples and publish a site without rebuilding the system first.