Flaxon 0.3.0 and Teloce-Py 0.3.0 are available on PyPI. Release instructions are maintained on this site; source links identify the framework snapshot. Check version and upgrade guidance.
08 | Teloce HTML shell and scoped CSS
Goal
Serve a browser shell and placeholder pages from the existing backend.
What you are building
app.use_teloce registers the compiler, runtime and browser routes. request.compile returns the compiled app.html shell for known page URLs. Each .html file has a template, script lang=ts and style scoped. The shell provides data-teloce-router-view. Module ui_routes maps compiled page names to browser paths. The first account/project pages are labelled placeholders; we replace them in the next two chapters. Shared types.ts describes JSON contracts and api.ts includes cookies, CSRF and error handling. Scoped CSS belongs to each component, so there is no separate app.css file.
Build this chapter
Stop the server. Work in project_manager. Follow each CREATE, whole-file replacement, or marked-block edit below in order.
Step 1: CREATE - make parent folders, then create this file - ui/types.ts
Match the Python response fields exactly. These types document the browser contract; runtime validation stays on the server.
Create the parent directories, then save this complete file.
export interface User {
id: number;
name: string;
email: string;
}
export interface Project {
id: number;
owner_id: number;
name: string;
description: string;
}
export type TaskStatus = "todo" | "doing" | "done";
export interface Task {
id: number;
project_id: number;
title: string;
status: TaskStatus;
due_date: string | null;
}
export interface Session {
user: User | null;
csrf: string;
}
export interface Article {
id: string;
title: string;
slug: string;
summary: string;
body?: string;
}
Say: "Match the Python response fields exactly. These types document the browser contract; runtime validation stays on the server."
Type: Follow the action above in ui/types.ts; explain each new function or binding as you add it.
Viewers should see: The saved file matches this step. After all steps, run the chapter checks below and show the expected result.
Step 2: CREATE - make parent folders, then create this file - ui/api.ts
Centralize fetch, cookies, CSRF rotation and error messages here so pages share one contract.
Create the parent directories, then save this complete file.
import type { Session } from "./types";
let session: Session | null = null;
export async function loadSession(): Promise<Session> {
const response = await fetch("/api/auth/session", { credentials: "same-origin" });
const payload = await response.json();
if (!response.ok)
throw new Error(payload.error?.message || "Could not load your session.");
session = payload.data;
return session!;
}
export async function api<T>(path: string, method = "GET", body?: unknown): Promise<T> {
if (!session) await loadSession();
const response = await fetch(path, {
method,
credentials: "same-origin",
headers: { "Content-Type": "application/json", "X-CSRF-Token": session!.csrf },
body: body === undefined ? undefined : JSON.stringify(body),
});
const payload = await response.json();
if (!response.ok) {
if (response.status === 401 || response.status === 403) session = null;
throw new Error(payload.error?.message || "The request failed. Please try again.");
}
if (path.startsWith("/api/auth/")) session = payload.data;
return payload.data as T;
}
Say: "Centralize fetch, cookies, CSRF rotation and error messages here so pages share one contract."
Type: Follow the action above in ui/api.ts; explain each new function or binding as you add it.
Viewers should see: The saved file matches this step. After all steps, run the chapter checks below and show the expected result.
Step 3: EDIT - replace the entire file - ui/app.html
This is the SPA shell. Its internal anchors and router view cooperate with each module's page mappings.
Select all existing contents of this file and replace them with the complete code below.
<template>
<div class="app-shell">
<header>
<a href="/" data-teloce-link class="brand">Project Manager</a>
<nav aria-label="Main navigation">
<a href="/projects" data-teloce-link>Projects</a>
<a href="/login" data-teloce-link>Account</a>
</nav>
</header>
<main id="router-view" data-teloce-router-view></main>
<footer>Built with Flaxon and Teloce. <a href="/admin/">Staff Admin</a></footer>
</div>
</template>
<script lang="ts">
export default {};
</script>
<style scoped>
/* Only the document reset is global: body is outside this component. */
:global(body) {
margin: 0;
}
.app-shell {
min-height: 100vh;
font-family: system-ui, sans-serif;
color: #17243a;
background: #f3f6fc;
}
header {
display: flex;
justify-content: space-between;
gap: 1rem;
align-items: center;
padding: 1.2rem max(1rem, calc((100vw - 1080px) / 2));
background: #17243a;
color: white;
}
header a {
color: white;
text-decoration: none;
}
.brand {
font-weight: 800;
font-size: 1.3rem;
}
nav {
display: flex;
gap: 1rem;
flex-wrap: wrap;
}
main {
max-width: 1080px;
margin: auto;
min-height: 75vh;
padding: 2rem 1rem;
}
footer {
padding: 1.5rem;
text-align: center;
color: #526079;
}
footer a {
color: #1d4ed8;
}
:focus-visible {
outline: 3px solid #f59e0b;
outline-offset: 3px;
}
@media (max-width: 520px) {
header {
flex-direction: column;
align-items: flex-start;
}
main {
padding: 1rem;
}
}
</style>
Say: "This is the SPA shell. Its internal anchors and router view cooperate with each module's page mappings."
Type: Follow the action above in ui/app.html; explain each new function or binding as you add it.
Viewers should see: The saved file matches this step. After all steps, run the chapter checks below and show the expected result.
Step 4: CREATE - make parent folders, then create this file - ui/pages/Home.html
This file owns the feature named by its module or component. Read the route/form flow before continuing.
Create the parent directories, then save this complete file.
<template>
<section class="hero">
<p class="eyebrow">A practical Python full-stack course</p>
<h1>Make room for your next idea.</h1>
<p>Create projects, break them into tasks, and watch your progress.</p>
<a class="button" href="/projects" data-teloce-link>Open projects</a>
<a href="/login" data-teloce-link>Create an account</a>
</section>
</template>
<script lang="ts">
export default {};
</script>
<style scoped>
* {
box-sizing: border-box;
}
a {
color: #1d4ed8;
}
h1 {
font-size: clamp(1.8rem, 5vw, 3rem);
line-height: 1.15;
}
h2 {
font-size: 1.2rem;
}
p {
line-height: 1.6;
}
[role="alert"] {
color: #a21c2b;
padding: 0.8rem;
border-left: 4px solid;
}
:focus-visible {
outline: 3px solid #f59e0b;
outline-offset: 3px;
}
.hero {
max-width: 700px;
padding: 3rem 0;
}
.eyebrow {
text-transform: uppercase;
font-size: 0.8rem;
letter-spacing: 0.1em;
}
.button {
display: inline-block;
border-radius: 7px;
min-height: 44px;
background: #2563eb;
color: white;
padding: 0.7rem 1rem;
text-decoration: none;
margin-right: 0.5rem;
}
</style>
Say: "This file owns the feature named by its module or component. Read the route/form flow before continuing."
Type: Follow the action above in ui/pages/Home.html; explain each new function or binding as you add it.
Viewers should see: The saved file matches this step. After all steps, run the chapter checks below and show the expected result.
Step 5: CREATE - make parent folders, then create this file - modules/auth/ui/pages/Login.html
Replace the placeholder with account forms. Use the session API and rotated CSRF token; do not read the HttpOnly cookie from JavaScript.
Create the parent directories, then save this complete file.
<template><section><h1>Account</h1><p>This screen is built in the next UI lesson.</p></section></template>
<script lang="ts">export default {};</script>
<style scoped>
h1 { font-size: 2rem; }
p { line-height: 1.6; }
</style>
Say: "Replace the placeholder with account forms. Use the session API and rotated CSRF token; do not read the HttpOnly cookie from JavaScript."
Type: Follow the action above in modules/auth/ui/pages/Login.html; explain each new function or binding as you add it.
Viewers should see: The saved file matches this step. After all steps, run the chapter checks below and show the expected result.
Step 6: CREATE - make parent folders, then create this file - modules/projects/ui/pages/ProjectList.html
Replace the placeholder with the complete projects screen. Connect its form to the typed API helper; show loading, error and empty states. Keep all CSS inside style scoped.
Create the parent directories, then save this complete file.
<template><section><h1>Projects</h1><p>This screen is built in the next UI lesson.</p></section></template>
<script lang="ts">export default {};</script>
<style scoped>
h1 { font-size: 2rem; }
p { line-height: 1.6; }
</style>
Say: "Replace the placeholder with the complete projects screen. Connect its form to the typed API helper; show loading, error and empty states. Keep all CSS inside style scoped."
Type: Follow the action above in modules/projects/ui/pages/ProjectList.html; explain each new function or binding as you add it.
Viewers should see: The saved file matches this step. After all steps, run the chapter checks below and show the expected result.
Step 7: CREATE - make parent folders, then create this file - modules/projects/ui/pages/ProjectDetails/[id].html
Replace the placeholder with the project detail page. Load the owned project and its tasks using the router ID, then compose the task form, list and progress display.
Create the parent directories, then save this complete file.
<template><section><h1>Project details</h1><p>This screen is built in the next UI lesson.</p></section></template>
<script lang="ts">export default {props: ['id']};</script>
<style scoped>
h1 { font-size: 2rem; }
p { line-height: 1.6; }
</style>
Say: "Replace the placeholder with the project detail page. Load the owned project and its tasks using the router ID, then compose the task form, list and progress display."
Type: Follow the action above in modules/projects/ui/pages/ProjectDetails/[id].html; explain each new function or binding as you add it.
Viewers should see: The saved file matches this step. After all steps, run the chapter checks below and show the expected result.
Step 8: EDIT - replace the marked blocks - app.py
The application factory mounts only the features already introduced. Follow the action label: a marked-block edit keeps the surrounding factory intact.
Edit 1: find this exact block
Open app.py. Locate this block once. The unchanged surrounding lines identify its position; do not use line numbers from another revision.
if app.is_management:
return app
@app.get("/health/course")
async def health():
Replace that block with
app.use_teloce(
project_root=ROOT,
ui_dir="ui",
title="Project Manager",
options={"minifier": "minifyjs"},
)
if app.is_management:
return app
@app.get("/health/course")
async def health():
Leave the rest of the file unchanged.
Edit 2: find this exact block
Open app.py. Locate this block once. The unchanged surrounding lines identify its position; do not use line numbers from another revision.
await User.all().limit(1)
return {"data": {"status": "ok"}}
@app.get("/")
async def home():
return {"data": {"message": "Backend lesson ready. The SPA starts in chapter 8."}}
return app
Replace that block with
await User.all().limit(1)
return {"data": {"status": "ok"}}
# Explicit shell routes preserve real 404 responses for unknown API paths.
@app.get("/")
@app.get("/login")
@app.get("/projects")
async def spa(request: Request, project_id=None, slug=None):
return await request.compile("app.html", {})
return app
Leave the rest of the file unchanged.
Say: "The application factory mounts only the features already introduced. Follow the action label: a marked-block edit keeps the surrounding factory intact."
Type: Follow the action above in app.py; explain each new function or binding as you add it.
Viewers should see: The saved file matches this step. After all steps, run the chapter checks below and show the expected result.
Remove public/app.css
The replacement factory and components no longer load this generated starter stylesheet. Remove the file after replacing the shell.
Run and check
python management.py runserver
# Open http://127.0.0.1:8000/
# Account/project screens are placeholders until chapter 9.
Expected result
The shell and navigation appear at /. The account and project placeholder pages mount. /api/welcome/status still responds as JSON.
Common errors
A blank page can be a compiler/import error: inspect terminal and browser console. Delete public/app.css after replacing the factory; the completed app does not mount it. The shell's explicit global body reset is intentional; child components own their other rules.
Short exercise
Change the shell's scoped header rules without modifying a child component's styles.
What to say and show
Say: "Serve a browser shell and placeholder pages from the existing backend. The server remains responsible for persistence and authorization; the browser presents the result."
Show the expected result above, then point to the file responsible for it. Run the chapter check before the next lesson.
Save your checkpoint
git add .
git commit -m "Complete chapter 08: Teloce HTML shell and scoped CSS"