I've had a personal site for a while, but it was sitting on a drag-and-drop builder that I had zero control over. No versioning, no local dev environment, no way to test changes before they went live. When I decided to relaunch it, I wanted to actually build it myself and use it as a way to learn CI/CD properly along the way.
Here's what I set up and why.
Starting point
The site started as plain HTML and CSS. No framework, no build step, just an index.html and a netlify.toml pointing at it. That's a legitimate way to ship a personal site, and it kept the focus on the pipeline rather than the stack.
Once I decided to add a blog, plain HTML stopped making sense. Writing posts by hand in HTML and maintaining a listing page manually would get tedious fast. That's when I migrated to Eleventy.
Why Eleventy
Eleventy is a static site generator: you write content in Markdown, it compiles everything down to HTML, and Netlify serves it. No framework overhead, no client-side JavaScript unless you add it yourself.
The appeal for this project specifically was that the CI/CD concepts stay identical whether you use a framework or not. A push triggers a build, the build produces static files, Netlify deploys them. Eleventy just makes the authoring side cleaner.
The folder structure
After migrating, the repo looks like this:
src/
_includes/
layouts/
base.njk # shared HTML shell, nav, styles
post.njk # wraps individual blog posts
blog/
index.njk # blog listing page
my-post.md # posts live here as Markdown files
index.njk # homepage content
_site/ # build output, never edited directly
.eleventy.js # Eleventy config
netlify.toml # build config for Netlify
The src/ directory is where all the work happens. _site/ is generated on every build and ignored by Git.
The Eleventy config
The .eleventy.js file at the root tells Eleventy where to find things and where to put the output:
module.exports = function(eleventyConfig) {
eleventyConfig.addPassthroughCopy("src/assets");
eleventyConfig.addFilter("readableDate", (dateObj) => {
return new Date(dateObj).toLocaleDateString("en-US", {
year: "numeric", month: "long", day: "numeric"
});
});
return {
dir: {
input: "src",
output: "_site",
includes: "_includes",
layouts: "_includes/layouts"
}
};
};
The readableDate filter is a small custom function that formats post dates into something human-readable. Eleventy doesn't include one out of the box.
The branch strategy
I wanted a staging environment to preview changes before they went live. The setup is two branches:
maindeploys toramirez.site(production)stagingdeploys to a separate Netlify preview URL
In Netlify under Site configuration → Branches and deploy contexts, you add staging as a branch deploy. From that point on, every push to either branch triggers its own build and deploy automatically.
Any work goes to staging first. Once it looks right, it gets merged to main.
The netlify.toml
[build]
command = "npx @11ty/eleventy"
publish = "_site"
[context.staging]
command = "npx @11ty/eleventy"
publish = "_site"
Netlify reads this file on every build. Before adding Eleventy, publish was just "." and there was no build command. Now it runs the build first and serves the output directory.
Merging and rollbacks
Once staging looks good, changes go to main via a pull request on GitHub. The PR gives you a diff of everything that changed before anything touches prod. Netlify also generates a deploy preview for every PR automatically, so you get one final look before merging.
If something breaks after a merge, GitHub puts a Revert button on every merged PR. Click it, merge the revert PR, and Netlify redeploys the previous state. No command line needed.
For more surgical rollbacks you can also revert via terminal:
git checkout main
git revert HEAD
git push origin main
This adds a new commit that undoes the last change rather than rewriting history, which keeps the record clean.
What I'd do differently
Starting with plain HTML was the right call for learning the pipeline, but I'd probably start with Eleventy from day one next time. The migration wasn't complicated, but it was an extra step that could have been avoided.
The staging setup is simple but effective. For a personal site it might feel like overkill, but having a place to push and preview without touching prod is a good habit regardless of project size.
I'll keep posting here as the site evolves.