Concepts 🚀 Project 🧮 Labs
📚 Learn / Build OpenBenchML — Step by Step / HTML + Jinja2 Templates — Your First Page
🚀 Step 2 of 10
← Previous: Step 1 — Your First FastAPI Route

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).

🎯 Goal — what you'll achieve in this step Visit http://localhost:8000/ and see a styled HTML landing page with your app's name.
📖 The basic concept Learn this first. It's the pure Python/HTML idea, stripped of all app complexity.
# 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.
🧮 Apply it to OpenBenchML
📄 main.py python
# 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)
📄 templates/base.html html
<!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>
📄 templates/landing.html 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.

✏️ Try yourself Add a `features` list to the TemplateResponse call: `'features': ['Upload models', 'Benchmark', 'Leaderboard']`. In landing.html, render it with `{% for f in features %}<li>{{ f }}</li>{% endfor %}`. Wrap it in a <ul>.
⚠️ Common mistakes to avoid
  • 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.
🔍 Where this is used in OpenBenchML templates/base.html is the shell every page extends. app/main.py uses Jinja2Templates to render 22 different templates.
Next — Step 3
SQLite + SQLAlchemy — Store Users
→