Skip to main content
app/controllers/posts.py
That controller pairs with a template at app/templates/posts/show.svelte. Here’s the flow: the router resolves a request path to a controller and an action, then imports the matching file from app/controllers/. It calls getContext() with whatever route params that function accepts, and the return value becomes the props for the matching .svelte file. getDoc() runs alongside it, supplying the page title and <head> content.

Route matching

Routes come from fymo.yml. A root entry and a list of resources cover the common case:
fymo.yml
Router._expand_resources() turns each resource name into four RESTful routes, each with a {controller}/{action}.svelte template: An app doesn’t have to define every one of these templates, only the ones it actually renders. The blog example only ships posts/show.svelte, so /posts and the others simply aren’t reachable in that app. Router.match() checks routes in order: an exact path match first, then a pattern with a :param segment, and finally a convention-based fallback for anything not declared explicitly. The fallback covers two shapes: That same fallback is how a bare slash route resolves to home.index, or whatever the config’s root: entry points at, with no separate resource entry needed.
Router.add_route() lets an app register a path directly, without going through resources, for routes that don’t fit the RESTful shape.

Protecting a route

A route entry can carry require_auth: true, or a dotted path to a guard function for a more specific check. Either one requires the dict form of that route entry, with to: naming the controller action:
fymo.yml
A resource entry works the same way, as a dict with name: required:
fymo.yml
The route named signin is the redirect target by convention, and it’s always public: require_auth on it is stripped at boot, with a warning printed if it’s set. Any protected route with no signin route registered fails at boot with a ConfigurationError naming the fix. An anonymous request to a protected route gets a 302 to signin?next=<original path>, before any SSR work starts. An authenticated request proceeds as normal. When require_auth is a dotted path instead of true, the signed-in check runs first, then the guard is imported and called with no arguments inside the request scope; any exception it raises redirects the same way as an anonymous visitor.
A convention-alias route (hitting /home when only root is declared protected) inherits the most restrictive require_auth declared anywhere for that controller. An alias can’t be used to bypass protection on the routes that share its controller.

Route params

A segment like :id in a route pattern gets captured by name. Fymo passes it to getContext() as a keyword argument, filtered down to whatever the function actually declares in its signature:
app/controllers/posts.py
Extra params in the match that getContext() doesn’t accept are simply dropped, instead of raising a TypeError.
This filtering happens in fymo/core/ssr_controller.py, in a helper called load_controller_context(). It uses inspect.signature under the hood, and both the full page render and the soft nav data endpoint call the same helper. That’s why params behave identically whether it’s the first load or a client-side navigation.

getContext and getDoc

A controller module can export two functions, both optional. getContext(**params) returns a dict that becomes the template’s props, available as $props() in the Svelte component. It can return more than plain JSON. Values can include remote functions imported from app/remote/*, which get serialized as callable markers that the client resolves through $remote. getDoc() returns a dict of page metadata:
  • title sets the <title> tag.
  • An optional head dict can include meta and link lists, which become <meta> and <link> tags.
app/controllers/index.py
Neither function is required. A controller with no getContext() just renders its template with no props. A controller with no getDoc() falls back to the app’s configured name as the title. When identity resolvers are registered, both functions run inside a read-only request scope. This lets current_uid() resolve the session during server-side rendering itself, not just after the client hydrates. Fymo opens this same scope for both a full page render and a soft nav data fetch. That’s what keeps a logged-in user from flashing as logged out during client-side navigation.

Redirecting from a controller

getContext() can raise Redirect to send the visitor elsewhere instead of returning props. During a full page render this produces a real 30x response with a Location header, no template gets rendered at all:
app/controllers/posts.py
Redirect is the same primitive a remote function raises to redirect the client after an action. From a controller it produces the 30x response above; from a remote function it produces a {"type": "redirect", ...} envelope that the client runtime turns into a navigation. Either call site takes a location and an optional status, defaulting to 303.

Layouts

An optional app/templates/_layout.svelte wraps every route in the app, and an optional app/templates/<resource>/_layout.svelte wraps every route under that resource. A layout receives its page content through Svelte’s children snippet:
app/templates/posts/_layout.svelte
Each _layout.svelte can be paired with a matching _layout.py controller. The path depends on the level:
  • Root layout controller: app/controllers/_layout.py
  • Resource layout controller: app/controllers/<resource>/_layout.py
That controller exports its own getContext() and getDoc(), called the same way as a page controller. A route with no layout files just gets flat props, whatever its own getContext() returned. A route with a layout chain gets nested props instead, combining the layout’s data with the page’s.
When a route has a layout chain, its props come nested rather than flat: { leafProps, layoutProps: { root, resource } }.Head metadata merges too, starting from the root layout, then the resource layout, then the leaf page. Scalar keys like title get overridden by the more specific level, so the leaf’s title wins.head.meta and head.link arrays work differently: they concatenate instead of replacing. A leaf’s page-specific <meta> tags add to the root layout’s defaults, rather than replacing them.

Soft navigation

By default, clicking between routes in a Fymo app skips the full page reload. The client fetches GET /_fymo/data/<path> instead, which resolves the same route and runs the same controller and layout chain as a normal page load. That request returns the next page’s props, doc metadata, and bundle URLs, packed into a devalue-encoded payload. The client swaps in the new leaf, and the layout too if it changed, without tearing down the rest of the page.
A resource can opt out of soft navigation per route, forcing full page reloads for it instead:
fymo.yml
Router.soft_nav_enabled(controller) defaults to true for any controller not listed with soft_nav: false.If a browser tab was already open before soft nav got disabled, it might still request /_fymo/data/admin. Fymo answers with a 409 soft_nav_disabled error instead of serving data, since that resource always wants a full reload.

The route store

Soft navigation swaps in a new leaf without a full reload, which means a component can’t always rely on a fresh mount to know it’s on a new page. $route gives any component the current route reactively, without passing it down through props:
It resolves to { pathname, search, params }. params holds the same route segments a controller’s getContext() receives, like :id from /posts/:id. The store is seeded before hydration runs, so its value is correct from first paint, and it updates on every soft nav afterward.

Head markup with svelte:head

getDoc() still owns the data-driven parts of <head>, the title and meta tags that change with the page. For markup that’s the same on every render of a template, a favicon link or a font preconnect, svelte:head inside a component is the simpler tool:
app/templates/_layout.svelte
The same tag works for things like Google Fonts preconnects in a shared Nav.svelte, or any other static tag a component wants to contribute to <head> regardless of which controller rendered the page. The rule of thumb: if a controller needs to decide the content, it belongs in getDoc(). If it’s fixed markup tied to a template, svelte:head is more direct.