Pages, routing and assets
#Pages and routes
from pyweb import App, NotFound
app = App(title="Library", stylesheets=["/static/app.css"], lang="en")
BOOKS = {1: "Dune", 2: "Emma"}
@app.page("/")
def Home():
<ul>
for book_id, title in BOOKS.items():
<li><a href={"/books/" + str(book_id)}>{title}</a></li>
</ul>
@app.page("/books/{book_id}", title="Book")
def Book(book_id: int):
if book_id not in BOOKS:
raise NotFound()
title = BOOKS[book_id]
<h1>{title}</h1>
<a href="/">Back</a>What this compiles to
No JavaScript: nothing on this page changes after it loads.
page Home route=/
page Book route=/books/{book_id}
browser title constant<ul>
<li><a href="/books/1">Dune</a></li>
<li><a href="/books/2">Emma</a></li>
</ul>{name}segments become parameters of the page function. Annotate them asintorfloatto convert; a value that doesn't convert is a 404.- Other parameters come from the query string; see below.
title=on@app.pagesets the<title>; otherwise the app title is used.- Links between pages are ordinary
<a href>links. In apps with interactive pages they load without a full page reload, and shared layouts stay in place; see Layouts & navigation. Each page ships only its own small module plus the shared, cached runtime. - Routes are matched in definition order; unmatched paths get a 404 page.
pyweb devshows Python tracebacks for errors in pages;serveshows a generic 500 page with a request id and logs the traceback. Both can be replaced with your own pages (below).
#Query parameters
Page parameters that aren't in the route are read from the query string and converted to their annotated type:
from pyweb import App
app = App()
BOOKS = ["Dune", "Emma", "Ulysses"]
@app.page("/search")
def Search(q: str = "", page: int = 1, tags: list[str] = [], exact: bool = False):
found = [b for b in BOOKS if (b == q if exact else q.lower() in b.lower())]
<h1>Results for {q} (page {page})</h1>
<ul>
for title in found:
<li>{title}</li>
</ul>What this compiles to
No JavaScript: nothing on this page changes after it loads.
page Search route=/search
browser found constant<h1>Results for (page )</h1>
<ul>
</ul>/search?q=em&page=2&tags=a&tags=b&exact=no calls Search(q="em", page=2, tags=["a", "b"], exact=False).
int,floatandboolvalues are converted (boolaccepts1/0,true/false,yes/no,on/off).list[...]collects every value of a repeated key.- Parameters with a default are optional. A missing required one, or a value that doesn't convert, answers 400 with a message saying which. (Route segments that don't convert answer 404 instead.)
pyweb.request.querystill gives the raw values as a dict.
#Titles, descriptions and social cards
from pyweb import App, head
app = App(title="Library", base_url="https://books.example",
description="A small library", image="/static/cover.png")
BOOKS = {1: {"title": "Dune", "blurb": "Spice and sand."}}
@app.page("/about", title="About us", description="Who runs the library")
def About():
<p>About</p>
@app.page("/books/{book_id}")
def Book(book_id: int):
book = BOOKS[book_id]
head(title=book["title"] + " - Library", description=book["blurb"])
<h1>{book["title"]}</h1>What this compiles to
No JavaScript: nothing on this page changes after it loads.
page About route=/about
page Book route=/books/{book_id}
browser book constant<p>About</p>description=,image=andnoindex=Trueon@app.pageadd<meta name="description">, Open Graph and Twitter card tags, and arobotstag. The app'sdescriptionandimageare the defaults.head(title=..., description=..., image=..., canonical=..., noindex=...)sets them from page code, once the data is loaded. It runs on the server; layouts can call it too, and the page's call wins.- With
App(base_url=...)every page gets<link rel="canonical">andog:urlfor its path (without the query string), and relative image paths become absolute, which social sites need.canonical=Falseon a page turns it off;canonical="/other"points elsewhere.
#Error pages
Write the 404 and 500 pages (or any status) as pages:
from pyweb import App
app = App()
@app.error(404)
def Missing(path):
<h1>Nothing at {path}</h1>
<a href="/">Home</a>
@app.error(500)
def Broken(request_id):
<h1>Something went wrong</h1>
<p>Quote this id if you contact us: {request_id}</p>What this compiles to
No JavaScript: nothing on this page changes after it loads.
page Missing route=None
page Broken route=None<h1>Nothing at </h1>
<a href="/">Home</a>- Error pages can take any of
path,status,messageandrequest_id. They use root layouts (prefix/) like other pages. - A 500 page also covers other 5xx errors.
pyweb devstill shows the traceback for 500s;pyweb serveshows your page. - If an error page itself fails, the built-in page is used.
#Responses other than HTML
Return redirect(url) from a page to send 303 See Other. Raise NotFound() for 404. Bad query parameters answer 400. Raise RPCError("forbidden") or RPCError("unauthenticated") for 403/401. Anything else that escapes a page is a 500.
#Page-level Python
A page function body runs on the server for every request, top to bottom, until the markup. You can use any Python there: queries, if statements, loops, early returns. Values assigned that way are computed on the server; see State and reactivity.
#Static files and stylesheets
Put files in a static/ folder next to app.pyweb. They are served at /static/... by pyweb dev, copied into dist/static/ by pyweb build, and served with long-lived caching by pyweb serve.
myapp/
app.pyweb
static/
app.css
logo.svgAdd stylesheets for every page with App(stylesheets=[...]). Any CSS approach works: hand-written CSS, a CSS framework's built file, or Tailwind's CLI output written into static/.
#The HTML document
Every page response is:
<!doctype html>
<html lang="en"><head>…title, stylesheets…</head>
<body>
<div data-pw-root="Home"> …server-rendered markup… </div>
<script id="pw-state" type="application/json">{…state the browser reads…}</script>
<script type="module" src="/static/Home.<hash>.js"></script>
</body></html>Pages with no interactivity have neither script tag. Layouts add their own root element around the page's, with their own state script and module.