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.

Minimaxing template preview — homepage with banner, 3-column layout, and teal navigation 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:

  1. Extracting the shared parts (header, nav, footer) into app/templates/base.html
  2. Creating page-specific templates that {% extend "base.html" %} and fill in {% block content %}
  3. Replacing hardcoded asset paths (assets/css/main.css) with dynamic ones ({{ url_for('static', path='css/main.css') }})
  4. 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:

  1. Check frontmatter first — if the .md file has slug: my-custom-slug in its frontmatter, use that
  2. Fall back to filename — if no slug: in frontmatter, strip the date prefix from the filename using a regex: 2026-04-17-fastapi-tipsfastapi-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

tronghien.com homepage Screenshot: The live site at tronghien.com — add by taking a screenshot of the homepage.

Write a new blog post

  1. Create a new file in content/posts/ named YYYY-MM-DD-your-slug.md
  2. 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
---
  1. Write your content below the --- in Markdown
  2. 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 in app/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 ✓

GitHub Actions workflow run 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