Welcome to Mima's Room

Many developers face the question of how to present their work to the world. One of the first options that comes to mind is GitHub, which has become a de facto public library filled with blueprints of ideas. However, thoughts expressed only in code inevitably limit themselves – not just to those who can read it, but to those willing to do so. This can lead to a vicious cycle where ideas struggle to find an audience, motivation decreases, and thoughts fade into obscurity.
To avoid losing heart, it seems that ideas should be presented in a more engaging form – through practical application rather than dry theory. This approach demands a place to showcase a project and perhaps share some relevant insight. To fill that missing piece comes the website.
Initial Mindset and Project Goals
There’s always the option to go full cypherpunk, like running a .onion server somewhere in South Asia, paid for in Monero. I genuinely respect this approach and think more people should consider this option, but for my use case it felt more like an overkill. There are also plenty of ready-made solutions out there that offer hosting for both frontend and backend, sometimes bundled with a personal blog. But using such services comes with at least two critical downsides, in my view.
First, there’s the ever-growing trend toward custodial platforms and algorithmic censorship on the modern web. This is blatantly obvious to anyone who experienced the pre-social media Internet of the early 2000s. As web technologies become more deeply integrated into everyday life, this drift toward centralization feels not only predictable but inevitable. The “I have nothing to hide” argument has been beaten to death over the years. Both sides have laid out their cases, and at this point, they’re mostly just repeating themselves. I have no interest in joining that debate. I’d rather act on my own conclusions: my work should preemptively avoid the possibility of censorship, rather than rely on appealing to some support moderator who removed it based on some brilliant regulation – or the color of my passport. And even Zuck laying off his “fact-checkers” at the time of writing should be seen as a temporary fluctuation, not a meaningful shift in direction. So when faced with a choice between the convenience of services that offer social media–style custody and ownership of my work, I lean toward the tin foil hat.
Second, there’s the matter of technical constraints and learning depth. Pre-built templates and drag-and-drop site builders often offer limited customization and do little to encourage a deeper understanding of how things actually work. In contrast, building things by hand is inherently more satisfying – and educational. I don’t just want something that works; I want to understand why it works, why it doesn’t, and how I can fix it myself. I treat everything I build as part of a practical learning process, and my personal website should be no different. Of course, there’s a limit to this approach – it can become an endless construction site and a bottomless rabbit hole of research – so I’ve cut a few corners on a question of hosting, which I’ll mention later.
In short, these are the core reasons I decided to hand-code my personal website as much as possible. I had a few small but strong opinions about how it should be built, and I tried to stay true to them throughout the process.
Choosing the Stack and Building the Site
Python was my pick for programming language. It is the first language that I learned, and that is definitely part of the reason why I love it and feel right at home when writing, debugging and try new libraries. For framework I thought about Django or Flask. While the latter is considered to be easier and maybe more appropriate for couple of simple HTML pages that I needed, I chose Django because it felt like right path. I already had some experience with it and wanted to learn more, it was unlikely that I would stuck on some problem due to it's big community, and Django clearly has more room for scaling in the future.
In design process for my Projects list I wanted to create little tags to quickly show used technologies. I did so by implementing separate model for tags and linking it with main Projects model. Tags embedding done with default models.ManyToManyField
interface:
python
from django.db import models
class Tag(models.Model):
LANGUAGE = "Language"
FRAMEWORK = 'Framework'
LIBRARY = "Library"
TOOL = 'Tool'
TAG_TYPES = [
(LANGUAGE, "Language"),
(FRAMEWORK, 'Framework'),
(LIBRARY, "Library"),
(TOOL, "Tool"),
]
name = models.CharField(max_length=50, unique=True) # Tag name
type = models.CharField(max_length=10, choices=TAG_TYPES) # Type of tag
def __str__(self):
return self.name
class Project(models.Model):
# ...
stack_list = models.ManyToManyField(Tag, related_name="projects") # Tags linked to projects
Being a fan of Markdown markup language I wanted to find a way to use it for text formatting, ideally to just transfer text from Obsidian note. It has all features that I could need – code highlighting, tables, lists, etc. After search for ways to implement it in the end I decided to use MarkdownX library, as it provided the way convert Markdown to html and vice versa basically with one wrapper:
python
import markdown
from django.utils.safestring import mark_safe
from markdown.extensions.codehilite import CodeHiliteExtension
def markdownify(text):
"""
Converts Markdown text to HTML with code highlighting, adds ids to headings,
and wraps consecutive images in a div with the class "image-gallery".
"""
# Convert the Markdown to HTML
html = markdown.markdown(
text,
extensions=['fenced_code', 'codehilite', 'tables', 'sane_lists', CodeHiliteExtension(pygments_formatter=CustomHtmlFormatter)],
extension_configs={'codehilite': {'linenums': False, 'guess_lang': True, 'lang_prefix': ''}},
)
# Add IDs to headings
html = add_ids_to_headings(html)
# Wrap consecutive images in a div with class "image-gallery"
html = wrap_consecutive_images(html)
With except of couple helper functions to format table of contents and image gallery this wrapper is the only thing needed to make the magic work. Also the ability to install extensions comes in handy, especially CodeHilite with Pygments. I wanted code blocks to show language they are written in, but it seems there are no recommendations on proper way to do that. CodeHilite embeds language in separate html class name, and while it has dedicated lang_prefix
config, for some reason I could not change it. This resulted in somewhat messy, but working solution:
python
from markdown.extensions.codehilite import CodeHiliteExtension
class CustomHtmlFormatter(HtmlFormatter):
def __init__(self, lang_str='', **options):
super().__init__(**options)
# lang_str has the value {lang_prefix}{lang} specified by the CodeHilite's options
# default {lang_prefix} is 'language-'
self.lang_str = lang_str
def _wrap_code(self, source):
language = f"{self.lang_str.removeprefix('language-')}"
yield 0, f'<code class="{self.lang_str}"><span class="c1"><i>{language}</i><br><br>''</span>'
yield from source
yield 0, '</code>'
For some reason I convinced myself, that posts should have table of contents. I placed corresponding section after first paragraph, and it looked nicely if Markdown text indeed had any # headings
inside. But if it did not, empty section showed up nonetheless. I thought about modifying function that parses headings, but in the end decided that work around condition inside Jinja does the job:
html
<!-- Table of Contents -->
{% with post.content|generate_toc as toc %}
{% if toc|length > 9 %} <!-- Empty Table of Contents consist of 9 symbols: <ul></ul> -->
<div class="toc-section">{{ post.content | generate_toc }}</div>
{% endif %}
{% endwith %}
There was no real need for this websites to have frightening amounts of JavaScript. Reasons for it's extensive use on the web are obvious and there is no point to yell at clouds like Abe Simpson. But if something is excepted, it doesn't automatically have to be this way, so I kept design and internal logic as lightweight as possible. I believe right now the only time client receives any JavaScript is image expansion script, which is reasonable compromise between simplicity and usability. However, my dislike of JavaScript doesn't prevent me from using it in scenarios where it is actually needed. Post submitting form in Admin panel was one of those cases. After applying markdownify
wrapper for post preview I made image upload section, which demanded some interactivity. My guess is that there are a ton of ready solutions for this task, but I was interested in trying to do my own. Final code supports multiple image upload, assigning captions and button inside text editor to insert Markdown link to file:
javascript
// Add a confirm button to insert the selected image
var confirmButton = document.createElement("button");
confirmButton.textContent = "Insert";
confirmButton.classList.add("confirm-image-button");
// Insert the selected image into the editor when confirmed
confirmButton.addEventListener("click", function () {
var selectedOption =
dropdown.options[dropdown.selectedIndex];
var imageUrl = selectedOption.value;
var caption = selectedOption.textContent;
// Get the current content and append the image Markdown syntax
var contentField = document.querySelector(
"textarea[name='content']",
);
var imageMarkdown = ``;
contentField.value += imageMarkdown;
contentField.dispatchEvent(new Event("input")); // Trigger input event to update the field
});
After thoroughly examining my project on localhost and celebrating victory over CSS demons I turned on Responsive Device Mode to get an idea of how my masterpiece would look on mobile device. The famous Groove Street poet quote came to mind right away – ah shit, here we go again. I didn't want to increase Universe entropy with another Corporate Memphis 'hamburger' menu and got by manual @media (orientation: landscape)
and @media (orientation: portrait)
CSS parameters setup.
Hosting Setup and Final Thoughts
I chose not to open-source my website for security reasons, since I consider it critical infrastructure for all my future projects. As for the “cutting corners” I mentioned earlier – well, I decided not to lose my mind dealing with a bare VPS and manual DNS configuration. Instead, I embraced the second downside from my earlier rant and went with a ready-made solution using the default cPanel setup.
Even that route turned out to be a learning experience. I picked up new knowledge about SSL certificates and ended up stuck for an entire day debugging my WSGI configuration. And my hopes for a smooth, painless email setup were quickly shattered when I discovered that Gmail flat-out refused to accept my messages—until I completed the full SPF and DKIM quest.
On surprising side I discovered a couple of bugs not authored by me. Looks like cPanel tracks integrity of client-side rendered pages – 'Dark Reader' browser extension when turned on somehow messes up cPanel in such a way, that it constantly demands login. Also it refused to change my git repository branch in internal version control tool, so the only way to fix this I could find was switching default branch on GitHub and adding it again in cPanel interface. Weird, but whatever.
Anyway, I am satisfied with result. Welcome to Mima's Room and thanks for coming to my TED talk.