This is the second entry on a series where I go in depth as to how I built some of the web applications I’ve created and maintained lately. In this post I will be discussing how I built this very website and how you, dear reader, can create one of your own! (of course you could simply clone the repo but that would be cheating)
I anticipate this will be a shorter post than the previous one, as this is the much simpler project. So let’s get started.
Why I started Django Horizons
I started Django Horizons for two main reasons.
First, when I was just beginning to seek employment in web development, I emailed Will Vincent—whose Django books I had studied—for guidance. He gave me the usual pointers, but in addition to suggesting a portfolio of production-level projects, he also recommended starting a blog to document my journey. (By the way, Mr. Vincent has been incredibly kind and helpful, even though we've never met and I just cold-emailed him one day asking for advice. I'm very grateful to him for that. Also, in my opinion, his books are the best Django resource out there.)
The second reason is that I had started learning Wagtail CMS. I completed a few simple tutorial projects, but I wanted to build something more personal—something I would continue to maintain over time. Django Horizons would serve both as a blog to document my journey and as a long-term Wagtail CMS project.
I also wanted to challeng myself to try some new technologies I hadn’t used extensively before. I usually work with Bootstrap, so I chose to use Tailwind CSS instead. I typically deploy to Heroku, so I decided to host this site on Fly.io. And since I normally try to keep my life simple, I thought I’d complicate things a bit and use AWS too.
I wasn’t in much of a rush, so I took my time to make sure the website worked well and looked decent. I spent about a month on the project, working a few hours each week, from the initial setup to deployment and publishing the first post.
How this website is structured
Django Horizons is built with Wagtail CMS, and it follows the fundamental structure of a typical Wagtail project. Wagtail sites are organized around a tree-like hierarchy of nodes and leaves. You can read more about that here and here.
Each node and its corresponding leaves are instances of one of several models I created, all of which inherit from the wagtail.models.Page class. Here’s where Wagtail introduces a bit of a twist: in Wagtail, you don’t use Views at all (arguably a defining feature of traditional Django development, along with the ORM), and you also don’t maintain a urls.py file within each app. Instead, the view and URL layers are abstracted into the Page model, which handles everything behind the scenes. The overall site structure is determined by the hierarchical relationship between instances of the Page class (i.e., pages and their child pages), and Wagtail takes care of the URL routing automatically.
Here’s the page tree structure of Django Horizons:
Home is the root page and has two child pages: the About Page (which has no child pages of its own), and the Blog Listing Page, which serves as the parent page for all blog entries. Whenever I want to add a new blog post, I navigate to the Blog Listing Page panel within the CMS and create a new child page. As you can see, it’s a very simple website.
There are four page models: HomePage, AboutPage, BlogListingPage, and BlogPage. As mentioned, they all inherit from Wagtail’s Page model. Of these, only BlogPage has any real complexity.
I won’t do a deep dive into Wagtail-specific fields, but to give a general idea: the body of a blog page uses what’s called a StreamField. This allows for inserting various block types and rearranging them flexibly. A RichTextBlock, as the name implies, represents a block of rich text within the StreamField, while an ImageBlock allows for including an image, optionally accompanied by some text.
Django Horizons uses a PostgreSQL database, which primarily stores the page objects themselves and BlogComment objects—these represent the comments users can leave on individual blog entries. The site uses Django’s built-in User model, with a single instance: the superuser account I use to manage the CMS.
Additional Tools
Over the years, whenever I’ve built web apps, I’ve almost exclusively used Bootstrap for CSS. But since Tailwind CSS is much more in demand in the job market, I decided to use Tailwind for this project. I’ve written about CSS in a previous post, so I won’t go too deep into Tailwind here, but I’ll just say that I’ve come to really enjoy working with it—especially if you can find a good component library. For this site, I used DaisyUI.
During development, I used the Tailwind and DaisyUI CDNs. Before deployment, I had Tailwind compile all the utility classes into a static CSS file and removed the CDNs. It’s a very clean and efficient setup that way.
For storing and serving media files—like the images I upload for each blog post—I used an AWS S3 bucket. There’s definitely a learning curve with AWS, and I had to spend a bit of time tweaking the Django settings to get media files working properly. That said, I found some great YouTube tutorials on integrating S3 into Django projects. And honestly, for all its intimidating UI, AWS does try to hold your hand through the setup process.
Lastly, I deployed the site on Fly.io instead of Heroku, which has usually been my go-to. I had tried Fly.io before and found the deployment process a bit clunky, and the service itself somewhat unreliable (they used to have frequent downtime). This time around, though, it’s been much smoother. Their free tier is generous, and for projects that draw more traffic, Fly.io seems to be more cost-effective than Heroku. That said, if I were maintaining a production-level project, I’d still be a bit cautious about potential service downtime.
Final Words
I guess I don’t have too much more to add. I plan to keep writing about my projects, and the next post will probably be about a project I’m currently working on (though it’s not live yet). I’ve got a lot going on in life my right now, so it might be a few weeks before I’m able to post again.
Until then, thanks for dropping by—and I’ll see you on the next one.
© 2025 Nicolás Ferreira. All rights reserved. Landscape Vectors by Vecteezy.