Welcome to xyzleo!
This blog is a space to record my programming journey and everything related to it.
The idea is to use the blog not just to publish what I've learned, but also as a way to consolidate knowledge, document projects, talk about programming, and track my evolution as a software developer.
Why start over from scratch?
LearningSea was the first project I deployed on a real server. Over time using it, I naturally started identifying things I wanted to improve and features I'd like to implement.
Okay, but why start over from scratch instead of just fixing and implementing those things directly in LearningSea?
The answer is below, where I explain a bit about why I chose Rails for this new project.
Why switch from Python/Django to Ruby/Rails?
I developed a strong interest in Rails after hearing about the framework and its philosophy straight from its creator, DHH. I had also already come across the famous idea that Ruby was created with the philosophy of making programmers happy. I got curious about what that actually meant in practice and wanted to experience it for myself.
Beyond that initial curiosity, I figured it would be worthwhile to build the same kind of software — a web blog, using a similar architecture — and then be able to see firsthand the differences between a language and framework and the ones I already knew.
It's not a port, it's a deliberate fresh start
Before diving into the technical content, it's worth making one thing clear: the xyzleo blog is not a migration of LearningSea. I didn't port models, didn't copy logic, and didn't use the old architecture as a template.
Even though the end goal of both blogs is the same, as mentioned above, this is a new project, built from scratch.
Stack
Side by side comparison:
| LearningSea | xyzleo | |
|---|---|---|
| Language | Python | Ruby 3.4 |
| Framework | Django | Rails 8.1 |
| Database | PostgreSQL | PostgreSQL |
| Application server | Gunicorn | Puma |
| Reverse proxy | Caddy | Caddy |
| Frontend | Tailwind CSS | Vanilla CSS |
| Cache / background jobs | — | Solid Cache / Solid Queue (no Redis) |
| Content editor | Summernote (WYSIWYG) + Markdown | Pure Markdown |
| Static assets | WhiteNoise | Propshaft |
| JS | jQuery / Summernote plugin | Vanilla JS + import maps (no bundler, no Node) |
| Markdown → HTML | python-markdown | Redcarpet + Rouge (syntax highlighting) |
| Content sanitization | Bleach | rails-html-sanitizer |
| Infra | Docker | Docker |
The numbers
Running cloc on the project gives a real sense of its size today:
~5,200 lines of code written across 136 files — already excluding all the standard Rails/Docker boilerplate (untouched scaffolding, generated schema, standard binstubs) and any .md documentation files.
| Category | Lines | Files | What |
|---|---|---|---|
App code (app/, lib/) | 3,301 | 86 | the blog itself |
| — CSS | 1,169 | 3 | application.css, admin.css, fonts.css |
| — Ruby | 987 | 31 | controllers, models, helpers, services |
| — ERB | 786 | 38 | views/partials |
| — JavaScript | 359 | 14 | light interactivity (app/javascript/*) |
| Tests | 1,403 | 29 | everything in test/ |
| Config/DB | 228 | 15 | routes, importmap, CSP, migrations, seeds |
| Infra | 261 | 6 | Dockerfiles, compose, CI, entrypoint |
What actually changed
What was improved in the new blog and what it has that LearningSea didn't (or had in a much simpler way).
100% Markdown writing, no WYSIWYG editor. LearningSea lived in a hybrid model: Summernote for images and fine adjustments, Markdown for writing quickly — and the two flows weren't symmetric (the HTML generated by Summernote never converted back into Markdown). The new blog no longer has Summernote at all: image upload via drag-and-drop straight into the textarea, automatically inserting
at the cursor. A single flow, no switching between editors.Native marginalia in Markdown. The footnotes (that clickable span with a popup) are now written directly in the markdown text. Before, they depended on a Summernote plugin I had custom-built, which injected the marginalia tag into the HTML. On top of that, there's now a reusable glossary — I define a term once and it can be referenced in any future post, without rewriting the description or inserting everything manually.
Redesigned backup system. LearningSea's version versioned on every save (including drafts), triggered by Django signals. The new one is deliberately simpler: just two levels — the "original" (first publication) and the "current" (overwritten on each new publication) — triggered only when a post is published, not on every save. Less history, but exactly what I actually use.
Themes, not just light/dark. LearningSea had two modes (light/dark, saved in localStorage). Now there are 20 combinations: 8 full themes (Dark, Light, Cream, Brown, Blue, Ruby, Purple, Pink), plus a CRT-terminal-style "Contrast" family (7 variations) and an "Editor" family with real code-editor palettes (Dracula, Nord, Gruvbox, Monokai, Solarized) — each one swapping only CSS variables, never the structure.
Selectable typography. 4 fonts (Source Serif 4 as default, Inter, IBM Plex Sans, Source Sans 3) and 3 reading sizes, all self-hosted.
Fixed three-column layout. Marginalia panel (or featured posts) on the left, content in the center, table of contents (TOC) on the right — reserved on every page, even when empty, so the layout stays correct through any interaction. The table of contents has scroll-spy: it highlights the current section as you scroll the page.
Featured posts. One or more posts can be pinned to the home page. This didn't exist in LearningSea and adds a nice touch, besides filling the empty space that used to be on the left side of the page.
Fully custom-built admin. Django Admin came ready-made, for free, bundled with the framework. Here there's no admin gem at all — every screen (posts, tags, marginalia, images) was built to do exactly what I need, without inheriting the conventions of a generic admin.
Error pages in the site's theme. 400/404/422/500 now have a new look — the 404 even echoes back the path you tried to access, in a terminal style.
PT/EN stays the same. The idea of duplicated columns (
title/title_en, etc.) instead of a generic translations table was already the right call in LearningSea, and I simply brought it back: the number of languages will never go beyond two, so there's no point paying the complexity cost of a system built for N languages.
Conclusion
After using LearningSea for a while, I realized I needed a bunch of quality-of-life improvements:
- a standardized, automatic way to insert marginalia
- granular or bulk post backups, directly from the admin panel
- easier CRUD management for posts, tags, marginalia, and images
- more themes and typography options for reader comfort
- a standard writing format, which in this case was Markdown — with no need for extra conversion or additional work.
Beyond the quality-of-life improvements, as discussed throughout this post, the project's organization and robustness improved, the number of dependencies decreased, the code became simpler to maintain, and some responsibilities are now better separated. Rails' own structure and conventions also helped make the project more predictable and easier to evolve.