This is a simple guide how to build one’s own webblog. This guide is split in tow parts. This part walks through building personal blog using Silex for frontend design and Django for backend development.
After completing my engineering degree, I found it exciting to finally use my free time for all the projects that had been on my mind for years but kept getting postponed during my studies. Still, I asked myself: what’s the point of sitting alone in a small room, working on my ideas, if I never share them with anyone? That’s why I decided to make this blog my very first post-graduation engineering project—one that I can actually share with others.
In this part of the tutorial, I’ll walk you through how to set up a website, covering all the key steps for designing a basic blog with content management using the Django framework.
In my opinion, online portfolios in the form of blogs are a great way to showcase projects. Of course, one could simply publish a project on GitHub with a markdown file, but I feel that a self-designed website adds a much more personal touch.
Here are some other webblogs that inspired me to make my own:
Of course, not all online portfolios or blogs are completely self-made. Platforms like Medium have become very popular for publishing technical tutorials because they require little management from the user. However, the goal of my project is to capture the look and feel of the examples above, while also giving me a more personal way to publish my technical documentation.
In front-end development, or client-side development, we use technologies such as HTML, CSS, and JavaScript—along with additional libraries and frameworks like React, Vue.js, or Angular. The client-side refers to everything that happens in the user’s browser: the part of a website or web application that visitors interact with directly. Tools like Silex, a free website builder, allow us to create websites following the what you see is what you get (WYSIWYG) principle. This means you don’t have to write HTML and CSS files manually.
There are also other options for prototyping websites, such as using FIGMA in combination with builder.io or GrapesJS. Established WYSIWYG website builders like WordPress, Wix, Squerspace, and others are often easy to use, but they don’t always provide the option to download the client-side files and usually require subscription fees for hosting. Since our goal is to self-host and maintain full control over every building block of the website, Silex is a fitting choice.
To start prototyping one has to create or log in with a gitlab account and click the create website button.
Now we’re on the editor page. To create an appealing website, you can follow the tutorials provided in the Silex documentation. In the next sections, I’ll show you how to create a very basic webpage outline.
The landing page is the first thing a visitor sees. As a baseline, it should include a header, a main content section, and a footer. In the Silex editor, the left-hand panel is mainly used to set the page layout (corresponding to HTML elements), while the right-hand panel is used to define the style of those elements (corresponding to CSS properties).
In the editor click on the + icon. Add 4 containers to the empty page make sure the layout matches the Layer structure below
Change the background color of the Header- and FooterContainer in the Decorations tab of the Style Manager on the right hand side.
Next, set a border width of 1px for the BlogContainer. To make the page fill the entire screen regardless of the user’s device height, set the Min height of both the Content-Container and the Blog-Container to 100vh (vh = “viewport height”) in the Dimensions tab. Also, set the Max width property of the Blog-Container to 1200px.
The simplest way to center an element on the page is by setting Margin left and Margin right to auto in the Dimensions tab.
By clicking the eye icon in the top menu bar, you can view the preview version of your webpage, which should now look like this:
When scrolling down, you should not be able to go past the FooterContainer. Adding a text block for the title and the footer completes the creation of this basic landing page.
Hint: Next to the Style Manager is a gear icon for the Component Settings. Here, I changed the default DIV type of the newly added title text to H1, giving it the properties of a header. It’s also a good idea to replace the default random ID with something meaningful yet unique. This will make it easier to locate and reference the component later.
Now that we have the landing page, we need to add links to each individual blog entry. A common pattern is to place these links inside boxes that include a picture and some additional information about the content. These boxes, called content cards, are widely used by major websites like YouTube, Netflix, and others.
To create our own content card, start by making a new page by clicking the + icon in the Pages section. The design of the card is completely up to you, but most commonly it includes the following elements: an image, a title, a description, metadata, and icons or badges.
Here is my example of a simplyfied content card:
The image and the
Hint: By setting the Width and Height properties of the ContentCardContainer to fit-content, the container will automatically shrink to fit its content. This ensures that your card doesn’t have unnecessary extra space and adjusts dynamically to the elements inside.
Before we copy this content card to our landing page, we need to make a few adjustments to the BlogContainer on the landing page. First, set the Display property to flex. This will add a new Flex tab in the editor, allowing us to control the arrangement of all content inside the container. I set the following properties:
Now, when you copy and paste the content card into the BlogContainer, it will initially be placed at the top center. When you duplicate the content card multiple times, each new instance will automatically fill and extend the BlogContainer, maintaining the layout defined by the Flex settings.
That concludes the frontend portion of the project. First, delete all the content cards from the BlogContainer so that it is empty again.
Finally, click the publish icon in the top-right menu bar and download the HTML and CSS files of your created pages as a .zip. These files will serve as the foundation for the next steps of your project.
Django is a high-level, open-source web framework written in Python. It follows the Model–View–Template (MVT) architectural pattern and is designed to help developers build robust, scalable, and secure web applications quickly. Since we don’t want to rewrite the website every time we publish a new article, we need a content management system (CMS). Django comes with a built-in admin interface that allows us to update the database with new content easily. For these reasons, Django is an excellent choice for starting our full-stack web blog journey.
Django provides an excellent tutorial for getting started with web development, and we’ll follow it for the first few steps.
First, create a new Django project in your project folder by running the following command in the terminal:
This automatically generates a folder structure with various files to get you ready for development. One of these files is manage.py. In a Django project, manage.py is a command-line utility that lets you interact with your project in multiple ways. For example, you can start the built-in development server by running:
This built-in server is intended for development only and should be replaced with a more robust server for production, which we’ll cover in the second part of this tutorial. For now, we’ll focus on creating an app inside our project, which will serve as our web blog with the following command:
At this point, your project folder should now have the following structure:
webblog_project/
manage.py
blog/
__init__.py
admin.py
apps.py
migrations/
__init__.py
models.py
tests.py
views.py
mysite/
__init__.py
settings.py
urls.py
asgi.py
wsgi.pyBefore continuing, make sure to register the newly created app in your settings.py file. This tells Django to include your app when running the project.
[...]
# Application definition
INSTALLED_APPS = [
'blog.apps.BlogConfig',
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
]
[...]Next, let’s create our first view. In Django, a view is a Python function or class that receives a http request and returns a http response.
To start, we’ll create a simple function-based view that returns a text response. Later, we can replace this with the HTML content of our landing page.
from django.http import HttpResponse
def index(request):
return HttpResponse("Hello, world. You're at the blogs index.")Next, create a new urls.py file inside your blog/ directory and register the view you just created. This allows Django to route requests to your view when users visit the corresponding URL.
from django.urls import path
from . import views
urlpatterns = [
path("", views.index, name="index"),
]In your project’s main urls.py file (mysite/urls.py), include the blog app so Django recognizes its URLs. This ensures that requests to your blog pages are properly routed to the views you defined.
from django.contrib import admin
from django.urls import include, path
urlpatterns = [
path("", include("blog.urls")),
path("admin/", admin.site.urls),
]If you now run the development server again and open http://localhost:8000/ in your browser, you should see the HTTP test response. Once this works, you can continue to the next section.
Instead of serving a text we can now implement our landing page that we created in section 2.2 to be served instead.
To serve your landing page, we first need to create two folders inside the blog app: static and templates.
The static folder holds all static resources, such as .css files, images, or JavaScript files.
The templates folder contains HTML files that serve as building blocks for the pages rendered to the user.
Place your .css files in the static folder and your .html files in the templates folder.
webblog_project/
manage.py
blog/
__init__.py
static/
blog/
content-card.css
index.css
templates/
blog/
content-card.html
index.html
admin.py
apps.py
migrations/
__init__.py
models.py
tests.py
views.py
mysite/
__init__.py
settings.py
urls.py
asgi.py
wsgi.pyThe additional static/blog/ and templates/blog/ folders are necessary because Django uses these folder structures as namespaces. To avoid naming conflicts, it’s best practice to place the static and template files in a unique folder for each app. This ensures that files from different apps won’t overwrite or collide with each other.
the templates serve the HTML components, but the static files are not yet being loaded. At this point there’s no way avoiding it, we need to make some adjustments in the index.html file to properly link the static resources.
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="UTF-8">
{% load static %}
<link rel="stylesheet" href="{% static 'blog/index.css' %}" />
<link href="https://fonts.googleapis.com" rel="preconnect" >
<link href="https://fonts.gstatic.com" rel="preconnect" crossorigin ></head>
<body id="il6a"><div id="HeaderContainer">
<H1 id="TitleText">Webblog</H1>
</div>
<div id="ContentContainer">
<div id="BlogContainer"></div>
</div><div id="FooterContainer">
<div id="ip46k">
<span id="i0fui">© Benedikt Görgei</span>
</div>
</div>
</body>
</html>Here, we use template tags {%
The next step is to add content cards to the empty BlogContainer on the landing page. Since each content card and blog entry should be unique, we need a way to create blog posts in a database and then dynamically load them into the landing page.
Django comes with SQLite as its default database. SQLite is a lightweight, self-contained, serverless SQL database engine. Since our blog is for personal use without the need to scale, we won’t switch to other SQL databases like PostgreSQL or MariaDB.
Django allows us to interact directly with the database through its framework. To do this, we need to create a new database model class in blog/models.py.
from django.db import models
from django.utils import timezone
from django.utils.text import slugify
class BlogPost(models.Model):
title = models.CharField(max_length=200)
slug = models.SlugField(unique=True, blank=True)
author = models.CharField(max_length=100)
html_file = models.FileField(upload_to='documents/', blank=True, null=True)
category = models.CharField(max_length=100, default="")
content = models.TextField(blank=True)
featured_image = models.ImageField(upload_to='blog/images/', blank=True, null=True)
created_at = models.DateTimeField(default=timezone.now)
is_published = models.BooleanField(default=False)
def save(self, *args, **kwargs):
if not self.slug: # Auto-create slug from title
self.slug = slugify(self.title)
super().save(*args, **kwargs)
class Meta:
ordering = ['-created_at']
verbose_name = "Blog Post"
verbose_name_plural = "Blog Posts"
def __str__(self):
return self.titleThere are a few important concepts to understand here. In Django, models are Python classes that define the structure and behavior of your database tables. They are a central part of Django’s Object-Relational Mapping (ORM) system, which allows you to interact with the database using Python code instead of writing raw SQL.
Every attribute in a model class represents a field in the corresponding database table. For example:
title, featured_image, and created_at correspond to the placeholders we used earlier in the content-card section.
The save method is overridden to customize behavior; here, slugify(self.title) generates a unique string that can be used as the URL endpoint.
author and content store additional information in the database but are not directly visible to users.
The core content of each blog post is the html_file, which is stored as a separate HTML file. We will return to this later.
is_published allows you to save a blog entry in the database without displaying it on the website immediately.
This structure ensures that each blog post has all the necessary metadata, content, and visibility settings.
Now run the following command
and next run
This step will update the database according to the models defined in blog/models.py. Any files uploaded through the model will be stored in a designated media folder.
To properly serve static and media files, we need to make the following changes in the settings.py file.
import os
[...]
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/5.2/howto/static-files/
STATIC_URL = "static/"
STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles')
MEDIA_URL = 'media/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
STATICFILES_DIRS = [
os.path.join(BASE_DIR, 'media/')
]
[...]Now create a superuser for the admin page
change the blog/admin.py file to the following
Run the server again and navigate to http://localhost:8000/admin . After login with the superusers credentials, we see the new created BlogPost model is registered as a model on the administration page.
Clicking on the + add icon allows for creating a new Blog entry, by filling all necessary fields. For testing any html file will do.
After successful creation, the new blog entry shows up in the model list.
In the last section, we created a new blog entry. However, its information is currently only stored in the database and isn’t being displayed on the website.
To fix this, we’ll use Django template tags to add logic to the index.html file. These template tags will dynamically populate the BlogContainer with blog posts retrieved from the database, so each entry appears as a unique content card on the landing page.
<div id="ContentContainer">
<div id="BlogContainer">
{% if blog_posts %}
{% for post in blog_posts %}
{% if post.is_published %}
{% block content %}{% endblock %}
{% endif %}
{% endfor %}
{% else %}
<p>No Blogs are available.</p>
{% endif %}
</div>
</div>The {% block card %}{% endblock %} tags act as placeholders for the content cards, which will hold the specific information for each blog entry stored in the database.
To make this work, we use template inheritance. We extend the base template (index.html) with a child template. In this case, the child template is content-card.html, which we’ll update to define how each blog post is displayed inside the block.
The {% block card %}{% endblock %} tags are the place holders for the content cards holding the specific information for each blog entry in the database. To do so we need to extend the base.html template file (in this case index.html) by a child template. The child template is the contented-card.html that need to be changed.
{% extends "blog/index.html" %}
{% load static %}
{% block card %}
<link rel="stylesheet" href="{% static 'blog/content-card.css' %}" />
<link href="https://fonts.googleapis.com" rel="preconnect" >
<link href="https://fonts.gstatic.com" rel="preconnect" crossorigin ></head>
<body id="ixa77"><div id="ContentCardContainer">
<img id="i0o92" src="{% static post.featured_image %}"/>
<div id="BlogInfo">
<div id="BlogInfoTitle">
{{ post.title }}
</div>
<div id="BlogInfoDate">
{{ post.created_at }}
</div>
</div>
</div>
{% endblock card %}In the Django template language variables are inside double curved brackets {{ }} . You can find out more about the template language following this link https://docs.djangoproject.com/en/5.2/ref/templates/language/ .
Finally, Let’s change blog/views.py so we are able to pass the BlogPost option to the render function.
from django.shortcuts import render
from .models import BlogPost
def index(request):
blog_posts = BlogPost.objects.order_by("created_at")
content = {"blog_posts": blog_posts}
return render(request, "blog/content-card.html", content)Once you restart the development server and refresh the page, your new blog entry should now appear on the landing page as a content card. This confirms that your database, view, and template are connected correctly.
In the last section, we finally got the content card to display on the landing page using the data from the database. What’s still missing is the ability to switch to the full blog article by clicking on the picture.
To achieve this, we can wrap the <img> tag in an <a> (hyperlink) tag inside the content-card.html template. This makes the image clickable and directs the user to the blog post’s detail page.
<body id="ixa77"><div id="ContentCardContainer">
<a href="{% url 'blog_article' post.slug %}">
<img id="i0o92" src="{% static post.featured_image %}"/>
</a>
<div id="BlogInfo">
<div id="BlogInfoTitle">
{{ post.title }}
</div>
<div id="BlogInfoDate">
{{ post.created_at }}
</div>
</div>
</div>We set the link’s href to the specific URL name ‘blog_article’ and pass the unique slug of the clicked blog post along with it. This slug acts as an identifier, allowing Django to know which article to display.
In blog/urls.py, we define a URL pattern that dynamically matches the slug. This tells Django to route requests like:
urlpatterns = [
path("", views.index, name="index"),
path("blog/article/<slug:slug>", views.blog_article, name='blog_article'),
]Add the the following function in blog/views.py
def blog_article(request, slug):
blog = BlogPost.objects.get(slug=slug)
content = {"blog_content": blog}
return render(request, f"blog/documents/{blog.slug}.html", content)Now we add some more logic to the index.html
[...]
<div id="ContentContainer">
{% if blog_content %}
{% block article %}{% endblock %}
{% else %}
<div id="BlogContainer">
{% if blog_posts %}
{% for post in blog_posts %}
{% if post.is_published %}
{% block card %}{% endblock %}
{% endif %}
{% endfor %}
{% else %}
<p>No Blogs are available.</p>
{% endif %}
</div>
{% endif %}
</div>
[...]We don’t need a new base template for displaying blog articles. Instead, we reuse the existing base.html (via the ContentContainer block) and simply insert the uploaded blog content into it.
However, the HTML file uploaded to the database is currently a raw snippet — it doesn’t yet know that it should extend the base template. To fix this, we can update our BlogPost model in blog/models.py so that when saving a post, the uploaded HTML is wrapped with the correct template tags.
def prepare_html(self):
# Path where uploaded file was stored
media_file_path = self.html_file.path
templates_dir = f"./blog/templates/blog/documents/"
# Adjust default file
begin_text = "{% extends 'blog/index.html' %}\n{% block article %}\n"
end_text = "\n{% endblock article %}"
remove_text = ["<!DOCTYPE html>", "<html>", "</html>","<body>", "</body>"]
with open(media_file_path, 'r', encoding='utf-8') as f:
file = f.read()
for text in remove_text:
file = file.replace(text, "")
with open(media_file_path, 'w', encoding='utf-8') as f:
f.write(begin_text + file + end_text)
shutil.copy(media_file_path, templates_dir)
def save(self, *args, **kwargs):
if not self.slug: # Auto-create slug from title
self.slug = slugify(self.title)
super().save(*args, **kwargs)
if self.html_file:
self.prepare_html()The prepare_html(self) function inserts the necessary template tags to extend the base HTML file. This function is called after _super().save(*args, **kwargs)_ inside the model’s save() method, which means the changes are applied only after the file has been uploaded to the media folder.
However, HTML files stored in the media directory are not treated as Django templates by default. To make them usable by Django’s template system, the easiest solution is to copy the uploaded HTML file to the templates folder.
Now, when we click on the image of a content card on the landing page, Django loads the corresponding blog post template and displays its content inside the site’s layout.
That’s great, now we can finally upload and read our blog posts. The last step is to provide a way for users to return to the landing page. To achieve this, simply wrap the title in the index.html template with an <a> hyperlink tag.
Many tools are available for creating documentation and exporting it as HTML. For Django projects Sphinx and MkDocs are commonly used options, but you have a wide variety to choose from depending on your needs:
Sphinx – Python-focused, great for API docs (make html).
MkDocs – Markdown-based, simple and modern (mkdocs build).
pdoc – Auto-generates Python docs as static HTML.
Swagger/OpenAPI – For REST API documentation.
Docusaurus – React-based static site generator for docs.
Docsify – Dynamic, single-page Markdown-to-HTML docs.
GitBook – Markdown docs with clean HTML export.
Jekyll or Hugo – Static site generators for general docs.
Google Docs / Word / LibreOffice – Export written docs directly as HTML.
Pandoc – Convert between formats like .docx, .md, and .html.
Jupiter Notebook - Executable Python notebooks
In my case, I did not use any of the tools above. Instead, I wrote this document in RStudio using an RMarkdown file . RMarkdown is widely used in data analysis, research, and reproducible reporting. Its main advantage is flexibility: you can mix narrative text with executable code chunks, making it easy to create dynamic and highly customizable documentation. On the downside, it has a relatively steep learning curve, especially for users unfamiliar with Markdown, R, or LaTeX. Styling and customization options are somewhat limited and often require additional work with CSS or LaTeX. For large reports with heavy computations, rendering can become slow and collaboration can be difficult for non-technical teams, as RMarkdown does not offer real-time editing features like Google Docs.
Building a personal blog from scratch is not only a rewarding technical exercise but also a way to give your projects a personal voice and presentation beyond platforms like GitHub or Medium. In this guide, we started with frontend prototyping using tools like Silex to design a simple landing page with a header, content section, and footer. We then enhanced the layout with content cards, providing a clean and reusable way to present blog entries.
On the backend, we introduced Django, leveraging its built-in admin interface to create, store, and manage blog posts in a database. Through templates, static files, and views, we connected frontend and backend logic, dynamically rendering blog content while keeping the site structure scalable and maintainable. We further improved navigation by linking content cards to individual articles and ensuring users could always return to the landing page.
In the second part of this tutorial we will explore how we can self-host and customizing every part of the stack, you gain not only technical control but also a platform that truly reflects your style and ideas.
Thank you for reading.