Introduction
The website you are looking at now is completely built with [FastHTML](https://fastht.ml). FastHTML is a Python framework that greatly simplifies building web apps. Many web frameworks have become bloated and notoriously complicated. Building websites also generally requires mastery of multiple languages like Javascript, HTML and CSS. Although Python frameworks like [Streamlit](https://streamlit.io), [Gradio](https://www.gradio.app) and [Dash](https://dash.plotly.com) have been great for Python developers to easily set up applications and dashboards, they often come with restrictions and knowledge does not transfer well from one framework to the other.
FastHTML offers a way to write web apps in pure Python. It is built on lightweight frameworks such as [Starlette](https://www.starlette.io) and [HTMX](https://htmx.org). Its syntax is inspired by [FastAPI](https://fastapi.tiangolo.com). As a data scientist who mostly writes Python all day, I'm a big fan of FastHTML and think it strikes a solid balance between simplicity, development speed and flexibility. When I started to build this website I had some experience with FastHTML through contributing code snippets for the [FastHTML Gallery](https://gallery.fastht.ml), which provides example components for FastHTML. I've contributed a [3D Plotly visualization](https://gallery.fastht.ml/vizualizations/bloch_sphere/display), [PDF Display](https://gallery.fastht.ml/widgets/pdf/display), [Audio Player](https://gallery.fastht.ml/widgets/audio/display) and made a [FastHTML Plugin for Plotly](https://github.com/CarloLepelaars/fh-plotly). However, creating a full website is a more intricate challenge. In this post I will explain how the main components of this FastHTML website are set up and lessons learned along the way. Let's dive in!
Project Setup
A minimal FastHTML app (`main.py`) can be defined as follows:
from fasthtml.common import *
app, rt = fast_app(hdrs=None)
@rt("/")
def get():
return Title("Home"), Div(H1("Welcome to FastHTML"))
serve()
Those with experience in FastAPI or [Flask](https://flask.palletsprojects.com/en/3.0.x) will recognize this structure. `app, rt = fast_app()` initializes a global object and routing system. The `@rt("/")` decorator defines a route for the root URL (i.e. homepage). `get` will be the request that is called when the user navigates to the homepage and loads the content. In this case the title of our page is `"Home"` and the page will just say "Welcome to FastHTML" in a large (`H1`) font. Like in HTML, `Div` is the way to separate components and can be extended in various ways with CSS by setting the `cls` argument. Lastly, `serve()` is called to run the webapp. For more elaborate examples check out the [FastHTML Gallery](https://gallery.fastht.ml) or the [fasthtml-example repo](https://github.com/AnswerDotAI/fasthtml-example).
To get started I've created an environment in [uv](https://docs.astral.sh/uv), which is a very fast and convenient Python dependency manager.
pip install uv
uv init --app website
uv add python-fasthtml
uv run python main.py
To keep the codebase nice and tidy I'm using another tool by [Astral](https://astral.sh) called [ruff](https://docs.astral.sh/ruff), which is a Python linter and formatter.
uv pip install ruff
ruff check
ruff format
The editor I'm using is [Cursor](https://www.cursor.com). The great benefit of using Cursor here is that you can pass the [FastHTML Docs](https://docs.fastht.ml) as context for an LLM to generate code, so it is aware of FastHTML syntax. As of the time of writing, LLMs like ChatGPT and Claude are not familiar with FastHTML, because it is a fairly new library. As someone who has not mastered Javascript and CSS, LLMs also make it easier to implement custom features like dark mode for this website.
Headers
Headers are global context and scripts to configure your webapp. They are defined as `hdrs` in the global `fast_app` function. Here are some of the headers I'm using for this website:
from fasthtml.common import Script, Link, HighlightJS, MarkdownJS
hdrs = (
# Code highlighting and general markdown formatting.
HighlightJS(langs=['python', 'bash']),
MarkdownJS(),
# Font Awesome for Icons
Link(rel="stylesheet", href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.1/css/all.min.css"),
# MathJax for Latex equations
Script(src="https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.7/MathJax.js?config=TeX-AMS-MML_HTMLorMML"),
Script(src="MathJax.Hub.Config({tex2jax: {inlineMath: [['$', '$']],displayMath: [['$$', '$$']],processEscapes: true}});"),
# Headers for rendering Altair charts.
Script(src="https://cdn.jsdelivr.net/npm/vega@5"),
Script(src="https://cdn.jsdelivr.net/npm/vega-lite@5"),
Script(src="https://cdn.jsdelivr.net/npm/vega-embed@6")
)
The code blocks you are looking at are automatically generated whenever you write a `Div` with `cls="marked"`.
[Font Awesome](https://fontawesome.com) is a convenient way to add icons, as you can see in the navigation bar and footer of this website. By adding headers I can add icons with FastHTML's `I` component, like `I(cls="fas fa-home")` for the home icon.
Some of the blog posts on this website have LaTeX equations, so I add [MathJax](https://www.mathjax.org) headers to enable rendering of LaTeX.
The `vega` headers are used to generate data visualization with [Altair](https://altair-viz.github.io). An example of the beautiful plots this enables can be found in the [Poisson post](/posts/poisson) on this website. [Vincent Warmerdam](https://github.com/koaning) made a convenient [FastHTML component for Altair](https://github.com/koaning/fh-altair) that I'm using.
Lastly, you might have some custom CSS you would like to add the context. This can be done with the `Style` header. Load in your CSS and add `Style("my_css_string")` to `hdrs`.
Images
Images can be shown with `Img`. The `src` of `Img` can be either a local path or URL. For example,
Img(src="https://images.pexels.com/photos/45201/kitty-cat-kitten-pet-45201.jpeg",
alt="Image of a cat",
style="width: 300px; height: auto;"),
style="display: flex; justify-content: center; align-items: center;"
),
Shows this cute cat! I hope he/she doesn't mind being on this website. Of course you also still have options to render images within [Markdown](https://www.markdownguide.org).
Div Posts
A fun thing I've been experimenting with is treating each blog post as a `Div`. Writing blog posts as Markdown takes care of most of the rendering of text, headers, links, images, LaTeX, etc., but treating a post as a Div gives you total freedom to add custom components in Python. This is something I take advantage of with custom Altair plots in the [Poisson post](/posts/poisson) and the [Ebbinghaus TIL](/til/ebbinghaus). Writing these plots and interactions in Python provides for a freeing and very fun experience. The downside is that everything is running [server-side](https://skillcrush.com/blog/client-side-vs-server-side), which can get clunky for large applications and/or large traffic. Some applications are better suited to develop with client-side Javascript.
The post you are reading now is defined as a dataclass:
from fasthtml.common import Div
from dataclasses import dataclass, field
@dataclass
class Post:
title: str # Title as displayed in the overview and as a header here.
teaser: str # Small intro displayed in the overview.
slug: str # "fasthtml" in this case
date: str # Date as displayed in the overview.
img_path: str # Image as displayed in the overview.
content: Div # Blog post you are reading now.
routes: list = field(default_factory=list) # List of web routes to add.
# For example to update a visualization when using a slider or clicking a button.
A route is then created for each post:
@rt("/posts/{slug}")
async def get(slug: str):
return Title(f"{slug} - Carlo Lepelaars"), post_detail(slug)
`post_detail` handles the display of each post with some custom CSS. It also includes a simple back button, which you can see on the bottom of this page.
post = Post(...) # Post dataclass object
Div(
Style("..."), # Custom css for post
Div(
H1(post.title),
Div(post.content, cls="post-content"),
A("Back to Blog Post Overview", href="/posts", cls="back-button"),
cls="post-detail"
)
)
Deployment
For deploying this website I'm using [Railway](https://railway.app?referralCode=mTpdVW), which simplifies deployment. Railway is also used for the [official FastHTML website](https://fastht.ml).
To deploy we need a `Dockerfile` for our website. Below is an example inspired by [an existing template](https://github.com/bambrose24/fasthtml-template/blob/main/Dockerfile), adapted so it works with uv:
FROM python:3.12-slim
# Set environment variables
ENV PORT=8080
ENV HOST=0.0.0.0
ENV PATH="/root/.local/bin:$PATH"
ENV UV_SYSTEM_PYTHON=1 # Recommended when using uv
# Install curl and uv
RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/* && curl -LsSf https://astral.sh/uv/install.sh | sh
# Set the working directory in the container
WORKDIR /app
# Copy the entire application code first
COPY . .
# Sync dependencies
RUN uv sync --frozen
# Expose the port your app runs on
EXPOSE 8080
# Run your application
CMD ["uv", "run", "python", "main.py"]
The deployment in [Railway](https://railway.app?referralCode=mTpdVW) is straightforward. Connect your GitHub account and point to the repository you want to deploy. Railway will automatically detect the `Dockerfile` and try to deploy your website. If the build fails you can check logs to debug. Once the deployment is successful you can connect your own domain following instructions at `Settings->Public Networking`. The benefit of Railway is that it updates every time you push to Github, which allows for fast iteration. It is also simple to setup and monitor your apps. Railway is collaborating with FastHTML and many FastHTML apps are deployed on Railway. Check out the [answer.ai guide on deploying with Railway through a CLI](https://github.com/AnswerDotAI/fh-deploy/tree/main/railway). There are also [guides for other deployment platforms](https://github.com/AnswerDotAI/fh-deploy?tab=readme-ov-file). To connect the domain [https://carlo.ai](https://carlo.ai) to Railway I use [Cloudflare](https://www.cloudflare.com). Cloudflare offers a free tier. They also optimize your website for performance and security.
Learn More
Want to learn more about FastHTML? Check out:
Acknowledgments
Special thanks to [Mickey Beurskens](https://mickey.coffee/about) for providing valuable feedback and guiding me in web development. Also thanks to [Vincent Warmerdam](https://koaning.io/til) for the inspiration to start a [Today I Learned (TIL) section](/til) and to [Patrick Collison](https://patrickcollison.com/bookshelf) for the inspiration to start a [reading list](/books). Thanks to [Answer.ai](https://www.answer.ai) for creating FastHTML and to [Isaac Flath](https://x.com/isaac_flath) for providing a platform ([FastHTML Gallery](https://gallery.fastht.ml)) which helped me gain to experience with FastHTML. If you have any questions feel free to reach out on [Linkedin](https://www.linkedin.com/in/carlolepelaars), [X](https://x.com/carlolepelaars) or [Bluesky](https://bsky.app/profile/carlolepelaars.bsky.social).