How This Site Works — A Beginner's Guide
#web-development, #fastapi, #jinja2, #markdown, #beginners
How This Site Works — A Beginner's Guide
This document explains how tronghien.com is built, from the ground up. It assumes you have never built a website before. Every term is explained as it appears.
1. How a Website Works (The Basics)
When you type tronghien.com into a browser, this is what happens:
┌─────────────┐ "tronghien.com?" ┌────────────┐
│ Browser │ ───────────────────────► │ DNS Server │
│ (Chrome) │ ◄─────────────────────── │ │
└─────────────┘ "IP: 51.254.204.33" └────────────┘
│
│ GET https://tronghien.com/
▼
┌─────────────┐ builds HTML page ┌────────────┐
│ VPS Server │ ◄─────────────────────── │ Browser │
│ (Ubuntu) │ ───────────────────────► │ │
└─────────────┘ sends HTML back └────────────┘
Key terms
- Browser — Chrome, Firefox, Safari. It displays web pages.
- IP address — A number that identifies a computer on the internet, like a postal address. Ours is
51.254.204.33. - DNS — Domain Name System. It's the internet's phone book — translates human-readable names (
tronghien.com) into IP addresses. - VPS — Virtual Private Server. A computer rented from a hosting provider that runs 24/7 and serves our website. Ours runs Ubuntu Linux.
- Request / Response — A browser requests a page; the server responds with HTML. This back-and-forth is called HTTP.
- HTTP / HTTPS — The protocol (language) browsers and servers use to talk. HTTPS is the secure (encrypted) version.
2. What the Server Sends Back
A web page is made of three things:
| File type | Purpose | Example |
|---|---|---|
| HTML | Structure and content | Headings, paragraphs, links |
| CSS | Visual styling | Colors, fonts, layout |
| JavaScript | Interactivity | Dropdown menus, animations |
The browser receives these files and renders them into what you see on screen.
3. Our Tech Stack
Here is how all the pieces connect:
┌─────────────────────────────────────────────────────┐
│ VPS (Ubuntu) │
│ │
│ ┌──────────────────────────────────────────────┐ │
│ │ Nginx (system service, ports 80 & 443) │ │
│ │ - Handles HTTPS/SSL │ │
│ │ - Serves /static/ files directly │ │
│ │ - Forwards all other requests ──────────┐ │ │
│ └──────────────────────────────────────────│───┘ │
│ │ │
│ ┌──────────────────────────────────────────▼───┐ │
│ │ Docker Container (port 8000) │ │
│ │ FastAPI app │ │
│ │ - Reads Markdown files from content/ │ │
│ │ - Renders Jinja2 templates │ │
│ │ - Returns HTML to nginx │ │
│ └──────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────┘
A "tech stack" is the set of tools used to build a website. Ours is:
Python + FastAPI (the backend)
Backend means the code that runs on the server — the user never sees it directly.
FastAPI is a Python framework for building web servers. When a browser requests /blog, FastAPI:
1. Receives the request
2. Loads the relevant blog posts from Markdown files
3. Builds an HTML page using a template
4. Sends the HTML back to the browser
We chose FastAPI because: - It's written in Python, which is readable and beginner-friendly - It's fast and modern - It has excellent documentation
Jinja2 (templates)
Imagine writing the same navigation bar on every page of a 50-page website. If you want to add one link, you'd have to edit 50 files.
Templates solve this. A template is an HTML file with placeholders:
<h1>{{ post.title }}</h1>
<p>{{ post.summary }}</p>
Jinja2 is the templating engine — it fills in the placeholders with real data before sending the page to the browser.
We have a base.html template that contains the navigation and footer. Every other page extends it, meaning it inherits the shared parts and only defines its own unique content.
Markdown (content)
Markdown is a simple way to write formatted text using plain characters:
# This becomes a heading
**This becomes bold**
- This becomes a bullet point
Blog posts and documentation are written as .md files. FastAPI reads them, converts them to HTML, and injects them into templates. This means:
- No database needed
- Content is version-controlled in Git alongside the code
- Writing a new post is as simple as creating a new .md file
Docker (deployment)
Docker packages the application and all its dependencies into a container — a self-contained unit that runs identically everywhere.
Think of it like a shipping container: the contents are sealed and standardised, so it doesn't matter whether it's on a ship, a truck, or a train.
We use docker compose up to start the app. One command brings everything up.
Nginx (reverse proxy)
Nginx (pronounced "engine-x") is a web server that sits in front of our FastAPI app.
Internet → Nginx (port 443, handles HTTPS) → FastAPI app (port 8000)
Nginx handles: - SSL/HTTPS — encrypts traffic so data can't be intercepted - Static files — serves CSS, JS, and images directly without bothering FastAPI - Routing — passes all other requests to FastAPI
4. The HTML5 UP Template
What is HTML5 UP?
HTML5 UP is a website that offers free, professionally designed HTML templates. The template used here is called Minimaxing.
Screenshot: The Minimaxing template as seen on html5up.net. Add this screenshot by saving the template demo page.
Why Minimaxing?
- Clean, minimal design that doesn't distract from content
- Responsive — looks good on mobile, tablet, and desktop
- Uses a simple 12-column CSS grid (explained below)
- Free to use for personal and commercial projects
What is a CSS Grid?
A grid divides the page into columns. Minimaxing uses 12 columns. You assign widths to elements using classes:
<div class="col-8">Main content (8/12 = 67% wide)</div>
<div class="col-4">Sidebar (4/12 = 33% wide)</div>
Desktop (12 columns total):
┌──────────────────────────────┬───────────────┐
│ col-8 (main content) │ col-4 (sidebar)│
│ 67% wide │ 33% wide │
└──────────────────────────────┴───────────────┘
Mobile (col-12-medium kicks in — stacks vertically):
┌──────────────────────────────────────────────┐
│ col-12 (full width) │
└──────────────────────────────────────────────┘
┌──────────────────────────────────────────────┐
│ col-12 (full width) │
└──────────────────────────────────────────────┘
On small screens, these automatically stack vertically thanks to responsive classes like col-12-medium.
How We Adapted It
The original template was a set of static .html files. We converted it to Jinja2 by:
- Extracting the shared parts (header, nav, footer) into
app/templates/base.html - Creating page-specific templates that
{% extend "base.html" %}and fill in{% block content %} - Replacing hardcoded asset paths (
assets/css/main.css) with dynamic ones ({{ url_for('static', path='css/main.css') }}) - Adding Jinja2 loops and variables where real data is displayed
The CSS and JavaScript files from the original template are copied unchanged to app/static/ and served as-is.
5. Project Structure Explained
tronghien_site/
│
├── app/ ← Python application code
│ ├── main.py ← Starts the FastAPI server
│ ├── routes/ ← One file per section of the site
│ │ ├── home.py ← Handles /, /about, /portfolio
│ │ ├── blog.py ← Handles /blog and /blog/{slug}
│ │ └── docs.py ← Handles /docs and /docs/{slug}
│ ├── templates/ ← Jinja2 HTML templates
│ │ ├── base.html ← Shared layout (nav, footer)
│ │ ├── index.html ← Homepage
│ │ ├── blog/
│ │ │ ├── list.html ← Blog listing page
│ │ │ └── post.html ← Single blog post page
│ │ └── docs/
│ │ ├── list.html ← Docs listing page
│ │ └── doc.html ← Single doc page
│ ├── static/ ← CSS, JS, images (served directly by nginx)
│ └── utils/
│ ├── markdown.py ← Reads .md files, parses frontmatter
│ └── github.py ← Calls GitHub API for portfolio page
│
├── content/ ← All written content (blog posts, docs)
│ ├── posts/ ← Blog posts as Markdown files
│ └── docs/ ← Documentation as Markdown files
│
├── nginx/
│ └── default.conf ← Reference config for the VPS nginx setup
│
├── Dockerfile ← Instructions to build the Docker image
├── docker-compose.yml ← Runs the app container
└── .github/
└── workflows/
└── deploy.yml ← Auto-deploys on push to main branch
6. How Python Code Connects to Page Content
This section traces the full journey from a .md file on disk to a rendered page in the browser.
Step 1 — The file naming convention
Blog posts are named YYYY-MM-DD-your-slug.md. Example:
content/posts/2026-04-17-fastapi-tips.md
The date prefix (2026-04-17-) serves one purpose: sorting. In app/utils/markdown.py line 66:
for path in sorted((CONTENT_DIR / "posts").glob("*.md"), reverse=True):
Python sorts the filenames alphabetically in reverse order. Since filenames start with the date, 2026-04-17 automatically comes before 2026-04-10 — newest posts first. No database, no manual ordering needed.
Step 2 — How the slug is derived from the filename
The slug is the URL-friendly identifier for a post. For /blog/fastapi-tips, the slug is fastapi-tips.
In app/utils/markdown.py line 70:
slug = meta.get("slug") or re.sub(r"^\d{4}-\d{2}-\d{2}-", "", path.stem)
This line does two things in priority order:
- Check frontmatter first — if the
.mdfile hasslug: my-custom-slugin its frontmatter, use that - Fall back to filename — if no
slug:in frontmatter, strip the date prefix from the filename using a regex:2026-04-17-fastapi-tips→fastapi-tips
So the slug fastapi-tips becomes the URL /blog/fastapi-tips.
Step 3 — How frontmatter is parsed
Every .md file starts with a YAML block between --- markers:
---
title: "FastAPI Tips"
date: 2026-04-17
slug: fastapi-tips
lang: en
tags: [python, fastapi]
summary: "Useful tips for FastAPI development."
draft: false
---
# FastAPI Tips
Post content starts here...
In app/utils/markdown.py the _parse_frontmatter() function splits this into two parts:
def _parse_frontmatter(text: str) -> tuple[dict, str]:
if not text.startswith("---"):
return {}, text
end = text.index("---", 3) # find closing ---
meta = yaml.safe_load(text[3:end]) # parse YAML → Python dict
body = text[end + 3:].lstrip("\n") # everything after = post body
return meta, body
meta becomes a Python dictionary: {"title": "FastAPI Tips", "date": ..., "lang": "en", ...}
body is the raw Markdown text of the post.
Step 4 — How a route serves a post
When a browser requests /blog/fastapi-tips, FastAPI calls the route handler in app/routes/blog.py:
@router.get("/blog/{slug}")
async def blog_post(request: Request, slug: str):
post = load_post(slug) # find post where post.slug == "fastapi-tips"
if not post:
raise HTTPException(status_code=404, detail="Post not found")
return templates.TemplateResponse("blog/post.html", {
"request": request,
"post": post, # the Post object (title, date, tags, etc.)
"content": post.html, # Markdown converted to HTML
})
load_post(slug) scans all .md files, finds the one whose slug matches, and returns a Post object.
post.html converts the raw Markdown body to HTML using the python-markdown library.
Step 5 — How the template renders it
The template app/templates/blog/post.html receives the post object and content string:
<h2>{{ post.title }}</h2> <!-- "FastAPI Tips" -->
<small>{{ post.date }}</small> <!-- "2026-04-17" -->
<div class="post-content">
{{ content | safe }} <!-- rendered HTML from Markdown -->
</div>
{{ content | safe }} tells Jinja2 to insert the HTML without escaping it (the | safe filter is needed because the content contains HTML tags like <p>, <h2>, <code>).
Full journey summary
content/posts/2026-04-17-fastapi-tips.md ← you write this
│
▼
markdown.py: _parse_frontmatter() ← splits YAML meta + body
│
▼
markdown.py: load_posts() ← builds Post objects, sorted by filename
│
▼
blog.py: blog_post(slug="fastapi-tips") ← route handler finds matching post
│
▼
post.html template ← Jinja2 fills in title, date, content
│
▼
Browser renders the page ← user sees the post
Multiple posts on the same day
If you write two posts on the same day, both get the same date in their frontmatter, which is what the reader sees. Their order on the listing page is determined by filename sort — alphabetical within the same date:
2026-04-17-fastapi-tips.md → appears first (f < p alphabetically)
2026-04-17-python-basics.md → appears second
If you want to control the order explicitly, set an explicit slug: — the filename can be anything, and the URL will use the slug you specified.
7. How to Update the Site Content
Screenshot: The live site at tronghien.com — add by taking a screenshot of the homepage.
Write a new blog post
- Create a new file in
content/posts/namedYYYY-MM-DD-your-slug.md - Add frontmatter at the top:
---
title: "Your Post Title"
date: 2026-05-01
slug: your-post-slug
lang: en
tags: [python, data-engineering]
summary: "One sentence describing the post."
draft: false
---
- Write your content below the
---in Markdown - Commit and push to
main— GitHub Actions deploys it automatically
For a Vietnamese post, set lang: vi. It will appear in the Vietnamese filter on the blog page.
Write a new documentation page
Same process, but save the file in content/docs/:
---
title: "Doc Title"
slug: doc-slug
tags: [guide]
category: guides
summary: "One sentence description."
---
Update the About page
Edit app/templates/about.html directly — it's static HTML/Jinja2 (no Markdown file backing it).
Add a GitHub project to the portfolio
Nothing to do — the portfolio page automatically fetches your latest public repos from the GitHub API. Push a new repo and it appears on the site within minutes.
Change the site's look and feel
- Colors and fonts — edit
app/static/css/main.css - Layout of a page — edit the relevant template in
app/templates/ - Navigation links — edit the
<nav>section inapp/templates/base.html
8. The Deployment Pipeline
Your laptop GitHub VPS
───────────── ────────────────── ──────────────────
Write a post ──► Receives push to main ──► git pull
git commit Triggers Actions docker compose build
git push ◄── Workflow: success ◄── docker compose up -d
Site updated ✓
Screenshot: A successful deployment workflow in the GitHub Actions tab. Add by taking a screenshot of the Actions tab after a push.
The whole process takes about 15–30 seconds from push to live.
9. Glossary
| Term | Meaning |
|---|---|
| Backend | Server-side code; runs on the VPS, not in the browser |
| Frontend | Client-side code; runs in the browser (HTML, CSS, JS) |
| Container | A packaged, isolated environment for running an app (Docker) |
| DNS | Translates domain names to IP addresses |
| Framework | A set of tools/conventions for building apps (FastAPI, Jinja2) |
| Frontmatter | YAML metadata block at the top of a Markdown file |
| Git | Version control system — tracks changes to files over time |
| HTTP/HTTPS | Protocol for browser-server communication; S = secure |
| Markdown | Lightweight text format that converts to HTML |
| Nginx | Web server and reverse proxy |
| Port | A numbered channel on a server; nginx uses 443, app uses 8000 |
| Reverse proxy | A server that forwards requests to another server (nginx → FastAPI) |
| Route | A URL pattern mapped to a handler function (e.g. /blog/{slug}) |
| Slug | URL-friendly version of a title: my-post-title |
| SSL/TLS | Encryption for HTTPS; the padlock in your browser |
| Template | HTML with placeholders filled in at request time |
| VPS | A rented virtual server running 24/7 |