Skip to main content

Frappe v15 Request Lifecycle, Routing, and Page Rendering

What is the Frappe request lifecycle?

In Frappe v15, every incoming HTTP request goes through a well-defined lifecycle. The framework identifies the request type, resolves the path, applies redirect and routing rules, and finally selects a page renderer to generate the response. Frappe Documentation

At a high level, Frappe handles three main types of requests: Frappe Documentation

  1. API requests – URLs starting with /api are handled by the REST API handler.
  2. File downloads – Paths like /backups, /files, and /private/files are served as file responses.
  3. Web page requests – Other routes like /about or /blog are handled by the website router and page renderers.

Understanding this lifecycle is crucial when you build custom websites, portals, or extend routing in ERPNext apps.

How does Frappe pre-process a request?

Before routing logic runs, Frappe performs a few pre-processing steps: Frappe Documentation

  • Initializes request-related utilities (for example, request recorder).
  • Applies rate limiting, based on configuration.
  • Normalizes and prepares the request context for subsequent routing.

You usually do not need to customize this phase, but it explains why some features like rate limiting or profiling can affect every route consistently.

How does Frappe resolve request paths?

Once a request reaches the website router (from app.py internally), it is passed to the Path Resolver. The Path Resolver performs three main operations in order: redirect resolution, route resolution, and renderer selection. Frappe Documentation

What is redirect resolution?

Redirect Resolution checks whether the incoming URL should be redirected to another path.

Redirects are pulled from:

  • The website_redirects hook in your app’s hooks.py.
  • Route redirects configured in Website Settings.

Example website_redirects configuration in hooks.py:

website_redirects = [
{"source": "/from", "target": "https://mysite/from"},
{"source": "/from", "target": "/main"},
{"source": "/from/(.*)", "target": "/main/\\1"},
]

You can optionally use fields such as redirect_http_status or match_with_query_string in v15 to control status codes and query-string-aware redirects.

If a redirect rule matches, Frappe sends an HTTP redirect and does not continue with route resolution.

What is route resolution?

If no redirect applies, the Route Resolution phase tries to map the incoming path to a final endpoint. Frappe builds this mapping from:

  • The website_routing_rules hook (previously known as website route rules).
  • Dynamic routes generated from DocTypes that have has_web_view enabled (for example, Blog Posts or Web Pages).

Typical website_routing_rules in hooks.py look like:

website_route_rules = [
{"from_route": "/about-us", "to_route": "about"},
{"from_route": "/contact-us", "to_route": "contact"},
]

If the path matches a rule or a dynamic web view route, the Path Resolver identifies the final endpoint, which may reference:

  • A template from the www folder,
  • A Web Form route,
  • A document-based template, or
  • A custom page renderer.

How does Frappe select a page renderer?

After Frappe resolves the final endpoint, it passes the path through all available Page Renderers. Each renderer has a can_render method; the first renderer that returns True will be used to render the page.

This design lets you add custom renderers for special routes while keeping standard behavior for common pages.

What is a page renderer in Frappe?

A Page Renderer is a Python class responsible for generating the response for a given endpoint. It must implement two methods:

  • can_render(self) – returns True if this renderer can handle the current request.
  • render(self) – returns an HTTP response (typically using build_response).

Basic structure:

from frappe.website.page_renderers.base_renderer import BaseRenderer
class PageRenderer(BaseRenderer):
def can_render(self):
# Add logic to decide if this renderer can handle the path
return True
def render(self):
response_html = "Response"
return self.build_response(response_html)

The build_response method (inherited from BaseRenderer) wraps your HTML into a proper HTTP response object with status and headers.

What are the standard page renderers in Frappe v15?

Frappe ships with several built-in renderers that cover most common use cases. Frappe Documentation

StaticPage

StaticPage serves static files (PDFs, images, etc.) from the www folder of installed apps. Any file under www that is not one of the following types is treated as a static file:

  • html, md, js, xml, css, txt, py

Tip: For production setups, the recommended way is to put static assets under the public folder so NGINX can serve them directly, improving caching and latency.

TemplatePage

TemplatePage looks up matching files in the www folder across apps.

  • If the path matches an HTML (.html) or Markdown (.md) file, that template is rendered.
  • If the path matches a folder, index.html or index.md inside that folder is rendered.

This is the standard mechanism for building static or semi-static web pages in Frappe.

WebformPage

WebformPage renders Web Forms defined in the Web Form DocType.

  • Frappe checks whether the requested path matches a route configured in any Web Form.
  • If it matches, the WebformPage renderer builds the Web Form view with server-side meta and client-side scripts.

This renderer is used for data collection forms, public sign-up forms, and similar flows.

DocumentPage

DocumentPage renders a document-specific template for a given DocType. It looks up templates in the DocType’s templates folder.

Expected folder structure for a DocType User:

doctype/
user/
templates/
user.html

If you navigate to a route that resolves to a specific User record with has_web_view enabled and a template present, DocumentPage will render user.html.

ListPage

ListPage renders list views defined as templates under a DocType’s templates folder.
If a DocType has a list template, ListPage renders it instead of the standard Frappe table listing. The official docs reference the Blog Post templates folder as an example implementation.

PrintPage

PrintPage is responsible for the print view of a document.

  • By default, it uses the standard print format for the DocType. Frappe Documentation
  • If a custom print format is set via default_print_format on the DocType, that format is used instead.

This renderer is used when users open /print views or when print formats are accessed via URLs.

NotFoundPage

NotFoundPage renders the standard 404 Not Found page.

  • Triggered when no renderer can handle the route or no matching content is found.
  • Returns HTTP status code 404. Frappe Documentation

Use this renderer behavior as a guide when customizing 404 experiences.

NotPermittedPage

NotPermittedPage renders a 403 Permission Denied page.

  • Triggered when the route exists but the user lacks the required permissions.
  • Returns HTTP status code 403. Frappe Documentation

This makes permission failures explicit and separate from missing routes.

How do you add a custom page renderer in Frappe v15?

You can plug in a custom page renderer using the page_renderer hook in your app’s hooks.py. Custom page renderers get priority over standard ones; their can_render is evaluated before standard renderers.

Step 1: Configure the page_renderer hook

In your app’s hooks.py:

page_renderer = "path.to.your.custom_page_renderer.CustomPage"

You can also provide a list of renderers if necessary:

page_renderer = [
"my_app.renderers.custom_page.CustomPage",
"my_app.renderers.marketing_page.MarketingPage",
]

The order in the list influences which custom renderer’s can_render is called first.

Step 2: Implement the custom renderer class

Create the file referenced in the hook, for example my_app/renderers/custom_page.py:

from frappe.website.page_renderers.base_renderer import BaseRenderer
class CustomPage(BaseRenderer):
def can_render(self):
# Example: only handle a specific route
return self.path == "/custom-page"
def render(self):
response_html = "<h1>Custom Response</h1>"
return self.build_response(response_html)

Or using the example pattern from the official docs: Frappe Documentation

from frappe.website.utils import build_response
from frappe.website.page_renderers.base_renderer import BaseRenderer
class CustomPage(BaseRenderer):
def can_render(self):
return True
def render(self):
response_html = "Custom Response"
return self.build_response(response_html)

Note: Custom renderers can also inherit and extend standard renderers like TemplatePage or DocumentPage to reuse existing logic.

When should you use a custom page renderer?

Use a custom page renderer in Frappe v15 when:

  • You need advanced routing logic not covered by website_routing_rules alone.
  • You want to return non-HTML responses from website routes (for example, custom JSON or streaming responses) without going through /api.
  • You need complex content assembly that does not fit into standard www templates, Web Forms, or DocType templates.

For most apps, TemplatePage, DocumentPage, and Web Forms are enough. A custom renderer is ideal for highly specialized pages like dashboards, embeddable widgets, or multi-step flows.

Best practices for routing and rendering in Frappe v15

Prefer standard mechanisms first
Use www/ templates, Web Forms, and DocType templates before jumping to custom renderers. This keeps your app aligned with Frappe defaults and easier to maintain.

Use website_redirects for SEO-friendly URLs
Manage legacy URLs or marketing-friendly routes using the website_redirects hook or Website Settings instead of hardcoding redirects in Python. Frappe Documentation

Centralize complex routing in website_route_rules
For multi-lingual or multi-brand websites, aggregate route mapping in website_route_rules to keep path resolution clear and version-controlled. agiliq.com

Keep can_render fast and deterministic
Avoid heavy database calls or complex logic in can_render. Keep it lightweight so Frappe can quickly decide which renderer should serve a route.

Leverage BaseRenderer.build_response
Always return responses via build_response to ensure consistent headers, status codes, and middleware behavior across renderers. Frappe Documentation

Common issues and troubleshooting tips

Why am I always seeing the NotFoundPage?

If your route always returns the default 404 page:

  • Confirm there is no redirect that misroutes the request (website_redirects).
  • Check whether website_route_rules or has_web_view routes are correct.
  • Ensure your custom renderer’s can_render is not always returning False.

If none of the renderers can handle the path, Frappe falls back to NotFoundPage. Frappe Documentation

Why is my custom page renderer not being used?

If your renderer never triggers:

  1. Verify the page_renderer hook path is correct and importable. Frappe Forum
  2. Ensure your class inherits from BaseRenderer.
  3. Confirm can_render returns True for the route you are testing.

Restart bench and clear caches:

bench restart
bench --site your.site clear-cache
bench --site your.site clear-website-cache

Remember that custom renderers run before standard renderers; if your can_render never returns True, the framework falls back to the built-in ones.

Integration patterns with other Frappe features

REST API: Use /api routes for programmatic JSON access and keep website rendering logic separate from API handlers.

Hooks & Events: Combine page_renderer with other hooks like app_include_js, app_include_css, or DocType events to create rich portal experiences.

Portal Development: Use custom renderers for advanced portal layouts that go beyond what Web Forms and standard pages offer.

Click to rate this post!
[Total: 0 Average: 0]