I've made a few scrubbed attempts at getting a personal website up and running. It's usually run aground on the rocky shoals of choosing a hosting platform, deciding on a web framework, or figuring out how to host the thing.
This time, I was lucky enough to remind myself of the simple phrase: > "Make it work. You can make it good later."
Well, it works. And hopefully I can learn a little something on the way to making it good.
Starting Swiftly
When I started planning the site, I knew that I wanted to use Swift. I'm (primarily) an iOS Developer, and I've been curious about how Swift on the Server would work.
Vapor is my framework of choice. There are other options, such as Hummingbird or Perfect, but I haven't really evaluated them.
To be honest, my friend Romain knows about Vapor, so that's what I picked! As always, going back to "Make it work".
Setting up a basic Vapor site is relatively straightforward. The documentation is solid, and vapor is installable via Homebrew.
Aside: LLM Training
One thing I've noticed about Large Language Model usage is that some people seem to get much more out of them than others. People like Joe Fabisevich have made it clear how proper use of an LLM can be a game changer.
I don't have access to a massive number of tokens on a solid Agentic LLM like Claude Code. I do have a Plus subscription to OpenAI, so I wanted to try and see how far that could get me. My intent was to get the LLM to fill in anything I didn't know for myself, and to suggest next steps when I was stuck.
The first thing I did was to have the LLM create a base set of instructions to use, by asking me a set of questions, which would inform how we'd proceed. Once the basic instructions were set, I had it provide me a simple 4 step plan for how to get the website up and running. From there, I had it plan out each step itself, correcting and guiding where necessary.
I won't go into the gory details, but suffice it to say, using an LLM helps me overcome lots of problems. I rarely need it to write the code for me, but it frequently unblocks problems that would otherwise have been a go-for-a-walk issue.
Leaf, Not Fluent
Vapor's templating language is called Leaf, and works quite well. In keeping with the 'Make it work' ethos, I'm eschewing any database driven content for now, so I haven't adopted Vapor's ORM (called Fluent). There's plenty of time to think about that later.
I have a Resources/Views/ folder in the repo root, where my templates live. There aren't many yet, so they're in a flat structure for now. If I want to add separate components, I will probably put them in appropriate folders, but this is good for now.
My base.leaf template looks like this:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>#import("title")</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
[A bunch of css here that will eventually go into its own file]
</style>
</head>
<body>
<header>
<nav>
<a href="/">Home</a>
<a href="/portfolio">Portfolio</a>
<a href="/blog">Blog</a>
<a href="/contact">Contact</a>
</nav>
</header>
<main>
#import("body")
</main>
<footer>
© #(year) Francis Chary. All rights reserved.
</footer>
</body>
</html>
This provides the main structure for every page on the site. The most important bits are behind the hashes:
#import("title")#import("body")#(year)
How do these work? I assume this is roughly similar to other templating languages (which to be honest, I'm not terribly familiar with, being primarily an iOS dev).
For the title & body tags, it's pretty straight-forward. Since we have an #import statement in the base template, we need to have an #export statement somewhere else!
So, let's have a look at the contact.leaf template:
#extend("base"):
#export("title"):
Talk To Me
#endexport
#export("body"):
<h1>Contact</h1>
<p>Have a question, a project, or just want to say hello? I’d love to hear from you.</p>
... Implement the actual form here ...
#endexport
#endextend
Above, we see the export of the title & body tags for the page. The content of the tags is wrapped in #export and #endexport calls, if we are also pulling in the base.leaf template with the #extend("base") call.
This is where the LLM let me down a little bit. It initially added Vapor 4.0.0 to the project. However, it didn't really know how to use the Leaf templating system, and led me down a couple of wrong roads. I had to step in at this point and review the documentation. It turned out that since LLMs generally have a knowledge cut-off date, it wasn't aware of the templating changes that have happened. Human Brain 1, LLM 0.
Deploy, Deploy, Deploy!
Having a working website doesn't do much good if nobody else can see it. The LLM was useful in identifying a few pathways, and I eventually settled on using Digital Ocean's App Platform, with a Docker container to deploy the app.
You might say that this is going a bit overboard for a simple website. I'm here to tell you that yes, it's definitely overboard. But for me, it's worth doing for a number of reasons, not least of which that it's simply good practice for me.
Vapor provides a decent guide for geting up-and-running with Docker, although their guide presupposes that you haven't yet started development. Vapor Toolbox can also generate a Dockerfile for you, which is what I used to get myself up and running.
I had a little bit of trouble lining up the Swift versions; I ended up using Swiftly to manage my local Swift versions, and used the swift:6.1-noble Docker image as my base.
Sweet Sweet Automation
So, I had an image, and DigitalOcean's App Platform makes it relatively easy to deploy. But I didn't want to always be building an image from my local machine, and uploading it to Docker's repositories manually. I wanted a solution that would simply auto-deploy from a commit to my repository.
Luckily, Github has Actions and an Image Repository, which Digital Ocean can be configured to pull from!
I created a Github Action that runs on commits to the main branch, and does the following:
- Set up Docker buildx (which allows us to compile to linux)
- Logs in to ghcr.io (Github's Container Repository)
- Builds the Docker image, and pushes it to ghcr.io
- Triggers a DigitalOcean deployment that pulls the image that was just pushed to Github's Container Registry
Now The Important Part
A website is ultimately an information delivery system. That is to say, the only thing that really matters, is.. Content. I've got the website, it's updating easily, and now I have to keep it updated!