Setting up a personal website
published on 2025-06-28I've wanted to set up a blog/personal website for a while, but kept putting it off. I finally took a couple hours on a weekend to figure it out, so let's kick my blog off with an overview of the setup and my thought process while making it.
Goals
I had a couple requirements in mind that shaped my decisions.
- I want it to be relatively minimal and low-tech
- Avoid front-end frameworks and SPA
- Avoid client-side JavaScript as much as possible
- I want it to be convenient
- Pages have a shared layout (e.g. a navigation bar) that is automatically generated
- The content of the blog should be mostly markdown, but with the option to include raw html and javascript when needed
- A central table of contents is generated and sorted by date
I knew about the existence of Static Website Generators, so I at least had a search term to start my research, which quickly led me to awesome-static-generators on GitHub, and from there - to 11ty. The setup looked simple enough so I went ahead and tried it out.
Setting up 11ty
Most of the documentation sounded relevant to me so I began by simply reading it front-to-back, only skipping the Using Data in Templates
section as it seemed too advanced for my use-case.
Then I set up an npm project and a git repo for it and installed @11ty/eleventy
.
After playing around with it, I settled on the following directory structure in my repo:
package.json
eleventy.config.mjs
Makefile
src/
index.md
style.css
_includes/
main_layout.html
post.html
blog/
index.html
setting-up-a-personal-website.md
Let's explore it.
eleventy.config.mjs
This file tells 11ty about the structure of our project, imports a server-side syntax highlighting plugin, and defines a helper function - all fairly self-explanatory.
import syntaxHighlight from "@11ty/eleventy-plugin-syntaxhighlight";
export default function(eleventyConfig) {
eleventyConfig.setInputDirectory("src");
eleventyConfig.addPassthroughCopy("src/style.css");
eleventyConfig.addPlugin(syntaxHighlight);
eleventyConfig.addFilter("date", (dateObj) => dateObj.toISOString().split('T')[0]);
};
Makefile
Also fairly simple, my main reason for even having a Makefile over package.json
scripts is that my upload script has more than one command.
.PHONY: all clean build upload
clean:
rm -rf _site/
dev: clean
npx @11ty/eleventy --serve
build: clean
npx @11ty/eleventy
upload: build
ssh my_vps "rm -rf personal-website/"
scp -r _site/* my_vps:personal-website/
My VPS that hosts this website also hosts several other projects, and I added a configuration for nginx pointed at ~/personal-website/
index.md
---
layout: main_layout
title: Home
---
Hey, I'm Amit. WIP, check out my [blog](/blog)!
Here's where this gets interesting.
Firstly, this is a markdown document that gets rendered as html and served as the root of the website - 11ty will also happily accept index.html
or any other supported extension and render it.
This is an example of a page with a front matter - metadata that 11ty or my code can use to modify how the page is rendered.
The layout
property instructs 11ty to look at _includes/main_layout.html
, find a liquid.js template there, and wrap its contents around the current document. The navigation bar is defined there.
style.css
I blended together some "CSS Reset" snippets, then wrote some styles for the navigation bar and some other components. It's pretty short, feel free to explore it.
main_layout.html
This file defines the skeleton of the website that wraps every other page.
<html lang="en">
<head>
<title>{{ title }}</title>
<link rel="stylesheet" href="/style.css">
{%- if code -%}
<link href="https://unpkg.com/prismjs@1.20.0/themes/prism-tomorrow.css" rel="stylesheet" />
{%- endif -%}
</head>
<body>
<header>
<!-- ... -->
</header>
<main>
{{ content }}
</main>
</body>
</html>
{% comment %} vim: set ft=liquid {% endcomment %}
A couple things to note here:
- Files with
.html
extension are rendered usingliquid.js
, but Vim does not recognize it from the.html
extension and complains about the weird{%syntax%}
. The last line is a modeline that informs it about the filetype. - Variables from the specific page (like
title
andcode
) are used to render parts of the template - The content of the specific page is inserted inside of a
main
tag
post.html
This file is the template that is used for every blog post.
---
layout: main_layout
tags: post
permalink: "/blog/{{ date | date }}/{{ title | slugify }}/"
---
<h1>{{ title }}</h1>
{{ content }}
layout: main_layout
defines a chain of templates:main_layout > post > specific_post
, each wrapped by the previoustags: post
will cause the blog post to be attached into thepost
collectionpermalink
: defines the url of the post based on the date and title
The template itself is merely appending a <h1>
above the content with the title of the post, but I'm sure I'll evolve it over time.
blog/index.html
---
layout: main_layout
title: Blog
---
<h1>Blog</h1>
<ul class="posts-list">
{%- for post in collections.post -%}
<li class="post-link">
<a href="{{ post.url }}">
<span class="post-date">{{ post.data.date | date }}</span>
<span class="post-title">{{ post.data.title }}</span>
</a>
</li>
{%- endfor -%}
</ul>
Here we use the post
collection to render a sitemap for the blog. By default, collections are sorted by date which is very convenient.
blog/setting-up-a-personal-website.md
And now, the culmination - I can conveniently open a new markdown file:
---
layout: post
title: Setting up a personal website
date: 2025-06-28
code: true
---
Content goes *here* :)
All I need is to select the post
layout, give it a title and date, and optionally add the code
variable that includes the CSS for highlighting code blocks, and the system will:
- Render the markdown file as HTML
- Wrap it with the correct template, and
- Add it to the table of contents
- Give me a live preview with hot reloading by running
make dev
If I ever need to include raw HTML or javascript, I just use a .html
file and it simply transparently works with the same front matter.
To publish my cool new blog, all I need is to commit, push, and make upload
it away!