HTML + Jinja2 Templates — Your First Page
Return HTML instead of JSON. Jinja2 templates let you compose pages with {% extends %} (shared shell) and {{ var }} (inject data).
# The basic concept: a template is an HTML file with holes
# base.html defines the shell:
# <title>{% block title %}{% endblock %}</title>
# <body>{% block content %}{% endblock %}</body>
# landing.html fills the holes:
# {% extends 'base.html' %}
# {% block title %}Home{% endblock %}
# {% block content %}<h1>{{ app_name }}</h1>{% endblock %}
# {{ app_name }} is replaced by the value you pass from Python.
# {% %} is for logic (extends, block, for, if). {{ }} is for values.
# Step 2: Add HTML rendering on top of step 1
from fastapi import FastAPI, Request
from fastapi.responses import HTMLResponse
from fastapi.templating import Jinja2Templates
import uvicorn
app = FastAPI(title='OpenBenchML')
templates = Jinja2Templates(directory='templates')
@app.get('/health')
async def health():
return {'status': 'ok'}
@app.get('/', response_class=HTMLResponse)
async def landing(request: Request):
# Pass variables to the template
return templates.TemplateResponse('landing.html', {
'request': request, # required by Jinja2Templates
'app_name': 'OpenBenchML',
'tagline': 'Benchmark ML models in the browser',
})
if __name__ == '__main__':
uvicorn.run(app, host='0.0.0.0', port=8000)
<!DOCTYPE html>
<html>
<head>
<title>{% block title %}OpenBenchML{% endblock %}</title>
<style>
body { font-family: sans-serif; margin: 2rem; background: #fafafa; }
.nav { display: flex; gap: 1rem; margin-bottom: 2rem; }
.nav a { color: #a0c000; text-decoration: none; }
</style>
</head>
<body>
<nav class='nav'>
<a href='/'>Home</a>
<a href='/health'>Health</a>
</nav>
<main>
{% block content %}{% endblock %}
</main>
</body>
</html>
{% extends 'base.html' %}
{% block title %}{{ app_name }} — Home{% endblock %}
{% block content %}
<h1>{{ app_name }}</h1>
<p>{{ tagline }}</p>
<p>This is step 2 of the build course. You now have a styled HTML page.</p>
{% endblock %}
How it works
Templates separate structure (HTML) from data (Python). The base.html file is the shell — every page shares the same <head>, nav, and footer. Each child template (landing.html) extends base.html and fills in named blocks. This is how OpenBenchML has 22 templates that all look like one app: they all extend base.html. The {{ app_name }} syntax is Jinja2's way of saying 'insert the value of app_name here'. When the route calls TemplateResponse, Jinja2 reads landing.html, follows the extends to base.html, fills the blocks, replaces the {{ }} variables, and returns a complete HTML string. FastAPI sends that string to the browser with Content-Type: text/html.
- Forgetting `{% endblock %}` — Jinja2 will throw a TemplateSyntaxError.
- Not passing `request` to TemplateResponse — Jinja2Templates requires it since FastAPI 0.85.
- Putting templates in the wrong folder — `Jinja2Templates(directory='templates')` expects a `templates/` folder next to main.py.
- Using `{{ app_name }}` in base.html when app_name is only passed to landing.html — variables don't flow up to the parent template unless you pass them explicitly.