Generating HTML

Building web pages in Python

Concepts

htpy basics

Building a page layout

Serving htpy output from Litestar

Reviewing the generated code

Check for Understanding

Why is f"<p>{user_input}</p>" dangerous if user_input comes from a form field?

If user_input is <script>alert('xss')</script>, the resulting HTML contains an executable script tag. Anyone who views the page will have that script run in their browser, potentially stealing session cookies or redirecting them to a malicious site. htpy escapes < and > to &lt; and &gt; so that the text is displayed literally rather than interpreted as markup.

How does htpy represent an HTML attribute like class="nav-bar"?

As a Python keyword argument to the element function. Since class is a reserved word in Python, htpy uses class_ instead. Hyphens in attribute names become underscores (e.g., data-value is written data_value=…).

What is the difference between returning HTMLResponse and returning a plain string from a Litestar handler?

Returning a plain string sends a response with Content-Type: text/plain, so the browser displays raw text without rendering markup. Returning HTMLResponse sets Content-Type: text/html, which tells the browser to parse and render the content as a web page. The body bytes may be identical; the header controls interpretation.

The lesson mentions missing lang, meta charset, and viewport attributes. What does each of these do?

lang on the <html> element tells screen readers and search engines the primary language of the page (e.g., lang="en"). meta charset declares the character encoding (almost always utf-8) so the browser decodes bytes correctly. The viewport meta tag (content="width=device-width, initial-scale=1") prevents mobile browsers from shrinking the page to fit a desktop-sized layout.

Exercises

Add an about page

Prompt the LLM to add a /about route that returns an HTML page describing the snail survey project in two or three sentences. Use the shared layout function so the page has the same navigation and footer as the main page. Visit the page in your browser and verify the navigation link to it works.

Trigger an XSS attempt

Write a small test where user_input is the string <script>alert('xss')</script>. Render it using htpy and print the resulting HTML. Verify that the output contains &lt;script&gt; rather than an executable tag. Then try the same experiment with an f-string and compare the results.

Inspect the response headers

Use curl -i http://localhost:8000/ to fetch the home page and display the response headers alongside the body. Find the Content-Type header and confirm it says text/html. Then modify the handler temporarily to return a plain string instead of an HTMLResponse and run curl -i again. Note which header changes and what the browser does differently when you visit the page.

Render a list of species

Prompt the LLM to add a /snails route that retrieves a hardcoded Python list of snails and renders it as an HTML unordered list. Each species name should be a <li> element inside a <ul>. Look at the code the LLM generates for the list and note how it turns a Python list into a sequence of htpy elements. What would you need to change to switch from <ul> to <ol>?

Conditional page elements

Modify the code created above so that the Python list has snails and sizes, and that snails above a certain size are marked in bold using <strong>…</strong>.