Documentation Index Fetch the complete documentation index at: https://mintlify.com/frappe/frappe/llms.txt
Use this file to discover all available pages before exploring further.
Frappe provides a flexible routing system for web pages, web forms, dynamic routes, and portal pages. It automatically handles URL mapping, template rendering, and context building.
Overview
Frappe’s routing system resolves URLs to templates, controllers, and data in this priority order:
Desk routes : /desk/* routes to the Frappe desk interface
Web Forms : Published web forms with custom routes
Web Pages : Dynamic web pages with custom routes
DocType web views : DocTypes with has_web_view enabled
Standard pages : Static HTML/MD files in www/ folders
Website route redirects : Configured redirects
Generators : Blog posts, products, etc.
# Example: How Frappe resolves a URL
# /products/laptop-15inch
# 1. Check if it's a web form route
# 2. Check if it's a web page with dynamic route
# 3. Check DocType generators (e.g., Item with route 'products/laptop-15inch')
# 4. Check static pages in www/products/laptop-15inch.html
# 5. Return 404 if not found
Static pages
Create static pages by adding HTML or Markdown files in the www/ directory:
my_app/
└── www/
├── about.html # /about
├── about.py # Controller (optional)
├── about.js # JavaScript (optional)
├── about.css # Styles (optional)
├── contact/
│ └── index.html # /contact
└── products/
├── index.html # /products
└── features.md # /products/features
HTML pages
<!-- www/about.html -->
{% extends "templates/web.html" %}
{% block title %}About Us{% endblock %}
{% block page_content %}
< div class = "container" >
< h1 > About Our Company </ h1 >
< p > We are a leading provider of... </ p >
<!-- Access Jinja context -->
< p > Current user: {{ frappe.session.user }} </ p >
</ div >
{% endblock %}
Markdown pages
<!-- www/blog/my-post.md -->
---
title: My Blog Post
description: A great blog post about Frappe
---
# Welcome to My Blog
This is **markdown** content that will be automatically converted to HTML.
## Features
- Automatic HTML conversion
- Frontmatter support
- Code highlighting
Page controllers
Add Python controllers for dynamic content:
# www/products/index.py
import frappe
def get_context ( context ):
"""Build page context"""
context.products = frappe.get_all(
'Item' ,
filters = { 'show_in_website' : 1 },
fields = [ 'name' , 'item_name' , 'image' , 'description' ],
limit = 20
)
context.featured_product = get_featured_product()
context.categories = get_categories()
return context
def get_featured_product ():
"""Get featured product"""
return frappe.get_doc( 'Item' , { 'featured' : 1 })
Use context in template:
<!-- www/products/index.html -->
{% extends "templates/web.html" %}
{% block page_content %}
< div class = "products" >
< h1 > Our Products </ h1 >
<!-- Featured product -->
< div class = "featured" >
< h2 > {{ featured_product.item_name }} </ h2 >
< img src = "{{ featured_product.image }}" />
</ div >
<!-- Product list -->
< div class = "product-grid" >
{% for product in products %}
< div class = "product-card" >
< h3 > {{ product.item_name }} </ h3 >
< p > {{ product.description }} </ p >
</ div >
{% endfor %}
</ div >
</ div >
{% endblock %}
Dynamic routes
Web pages can have dynamic URL patterns:
Web Page with dynamic route
# Create a Web Page with dynamic_route = 1
doc = frappe.get_doc({
'doctype' : 'Web Page' ,
'title' : 'Product Details' ,
'route' : 'products/<product_slug>' , # Dynamic parameter
'dynamic_route' : 1 ,
'published' : 1
})
doc.insert()
Create controller for the web page:
# www/products.py (controller for dynamic route)
import frappe
from frappe.website.website_generator import WebsiteGenerator
def get_context ( context ):
"""Get context for product/<product_slug>"""
# Get dynamic parameter from frappe.form_dict
product_slug = frappe.form_dict.get( 'product_slug' )
if not product_slug:
frappe.throw( 'Product not found' , frappe.DoesNotExistError)
# Load product data
product = frappe.get_doc( 'Item' , { 'route' : f 'products/ { product_slug } ' })
context.product = product
context.related_products = get_related_products(product)
context.reviews = get_product_reviews(product.name)
return context
The route /products/laptop-15inch will call get_context() with product_slug='laptop-15inch'.
Werkzeug routing
Frappe uses Werkzeug for advanced routing:
from werkzeug.routing import Rule
# Define routes in web page
rules = [
Rule( '/project/<project_id>' , endpoint = 'view_project' ),
Rule( '/project/<project_id>/edit' , endpoint = 'edit_project' ),
Rule( '/project/<int:project_id>/tasks' , endpoint = 'project_tasks' )
]
# Access parameters in controller
def get_context ( context ):
project_id = frappe.form_dict.get( 'project_id' )
context.project = frappe.get_doc( 'Project' , project_id)
Web forms provide auto-generated forms for DocTypes:
# Create a web form
web_form = frappe.get_doc({
'doctype' : 'Web Form' ,
'title' : 'Job Application' ,
'route' : 'careers/apply' ,
'doc_type' : 'Job Applicant' ,
'is_standard' : 0 ,
'published' : 1 ,
'allow_edit' : 1 ,
'allow_multiple' : 1 ,
'show_sidebar' : 1 ,
'allow_print' : 0 ,
'allow_delete' : 0
})
# Add fields
web_form.append( 'web_form_fields' , {
'fieldname' : 'applicant_name' ,
'label' : 'Full Name' ,
'fieldtype' : 'Data' ,
'reqd' : 1
})
web_form.append( 'web_form_fields' , {
'fieldname' : 'email' ,
'label' : 'Email' ,
'fieldtype' : 'Data' ,
'reqd' : 1
})
web_form.insert()
Web form URLs:
/careers/apply - New form
/careers/apply/new - Explicit new form
/careers/apply/JA-0001 - View existing
/careers/apply/JA-0001/edit - Edit existing
/careers/apply/list - List view
# Custom web form controller
# my_app/www/careers/apply.py
def get_context ( context ):
"""Customize web form context"""
context.show_sidebar = True
context.positions = frappe.get_all( 'Job Opening' ,
filters = { 'status' : 'Open' },
fields = [ 'name' , 'job_title' ]
)
DocType web views
Enable web views for DocTypes:
# DocType with web view
# has_web_view = 1
# route field
class Item ( WebsiteGenerator ):
def get_context ( self , context ):
"""Build context for item web page"""
context.related_items = self .get_related_items()
context.specifications = self .get_specifications()
context.reviews = get_reviews( self .name)
return context
def get_related_items ( self ):
return frappe.get_all( 'Item' ,
filters = {
'item_group' : self .item_group,
'name' : [ '!=' , self .name],
'show_in_website' : 1
},
limit = 5
)
Template for DocType web view:
<!-- templates/generators/item.html -->
{% extends "templates/web.html" %}
{% block title %}{{ doc.item_name }}{% endblock %}
{% block page_content %}
< div class = "item-details" >
< h1 > {{ doc.item_name }} </ h1 >
< div class = "item-image" >
< img src = "{{ doc.website_image or doc.image }}" />
</ div >
< div class = "item-description" >
{{ doc.web_long_description or doc.description }}
</ div >
< div class = "specifications" >
< h2 > Specifications </ h2 >
{% for spec in specifications %}
< div class = "spec" >
< strong > {{ spec.label }}: </ strong > {{ spec.value }}
</ div >
{% endfor %}
</ div >
< div class = "related-items" >
< h2 > Related Products </ h2 >
{% for item in related_items %}
< a href = "/{{ item.route }}" > {{ item.item_name }} </ a >
{% endfor %}
</ div >
</ div >
{% endblock %}
Route configuration
Website route rules
Define route rules in hooks.py:
# hooks.py
website_route_rules = [
# Map /shop/<category> to DocType
{ "from_route" : "/shop/<category>" , "to_route" : "Item" },
# Profile redirects
{ "from_route" : "/profile" , "to_route" : "me" },
# Desk routes
{ "from_route" : "/desk/<path:app_path>" , "to_route" : "desk" }
]
Website redirects
Configure redirects:
# hooks.py
website_redirects = [
# Simple redirect
{ "source" : "/old-page" , "target" : "/new-page" },
# Regex redirect with query params
{
"source" : r "/app/ ( . * ) " ,
"target" : r "/desk/ \\ 1" ,
"forward_query_parameters" : True
},
# Multiple sources
{ "source" : [ "/apps" , "/app" ], "target" : "/desk" }
]
Custom route resolution
# In page controller
def get_context ( context ):
# Get current route
route = frappe.local.request.path
# Parse route segments
parts = route.strip( '/' ).split( '/' )
# Custom logic based on route
if len (parts) > 1 and parts[ 0 ] == 'category' :
context.category = parts[ 1 ]
context.items = get_items_by_category(parts[ 1 ])
Portal pages
Portal pages are for authenticated users:
# www/portal/my-orders.py
import frappe
def get_context ( context ):
# Check if user is logged in
if frappe.session.user == 'Guest' :
frappe.throw( 'Please login to view orders' , frappe.PermissionError)
# Get user's orders
context.orders = frappe.get_all(
'Sales Order' ,
filters = { 'customer' : get_customer()},
fields = [ 'name' , 'transaction_date' , 'grand_total' , 'status' ],
order_by = 'creation desc'
)
context.no_cache = 1 # Don't cache user-specific content
return context
def get_customer ():
"""Get customer linked to current user"""
return frappe.db.get_value( 'Customer' , { 'email_id' : frappe.session.user})
Portal permissions
# Check portal permissions
def has_website_permission ( doc , ptype = 'read' , user = None ):
"""Check if user can access document on portal"""
if not user:
user = frappe.session.user
# Guest users have no access
if user == 'Guest' :
return False
# Check if user is linked to document
customer = frappe.db.get_value( 'Customer' , { 'email_id' : user})
return doc.customer == customer
Use in hooks.py:
has_website_permission = {
'Sales Order' : 'my_app.permissions.sales_order_website_permission'
}
Context building
Default context
Every web page gets default context:
context = {
'frappe' : frappe, # Frappe module
'frappe.form_dict' : frappe.form_dict, # URL parameters
'frappe.session' : frappe.session, # Session data
'_' : frappe._, # Translation function
'get_url' : get_url, # URL builder
'cint' : cint, # Integer conversion
'flt' : flt, # Float conversion
}
Custom context
Add custom context in controller:
def get_context ( context ):
# Add custom data
context.custom_field = 'value'
context.items = get_items()
# Modify page properties
context.no_cache = 1 # Disable caching
context.show_sidebar = True
context.title = 'Custom Title'
context.description = 'Page description for SEO'
# Add meta tags
context.meta_tags = {
'og:title' : 'Social media title' ,
'og:description' : 'Social media description' ,
'og:image' : '/assets/image.jpg'
}
return context
URL parameters
Query parameters
# URL: /products?category=electronics&sort=price
def get_context ( context ):
# Access query parameters
category = frappe.form_dict.get( 'category' )
sort_by = frappe.form_dict.get( 'sort' , 'name' ) # Default value
context.products = frappe.get_all( 'Item' ,
filters = { 'item_group' : category} if category else {},
order_by = sort_by
)
Path parameters
# URL: /products/laptop-15inch
def get_context ( context ):
# Get path segments
path = frappe.local.request.path
segments = path.strip( '/' ).split( '/' )
# Extract parameters
if len (segments) >= 2 :
category = segments[ 0 ] # 'products'
slug = segments[ 1 ] # 'laptop-15inch'
context.product = frappe.get_doc( 'Item' , { 'route' : path})
Caching
Page caching
def get_context ( context ):
# Disable caching for dynamic content
context.no_cache = 1
# Or enable caching
context.cache = 1
Cache invalidation
# Clear website cache
frappe.clear_website_cache()
# Clear cache for specific page
frappe.clear_website_cache( 'products/laptop' )
# Clear cache for specific DocType
frappe.clear_website_cache( doctype = 'Item' )
Best practices
Use appropriate route types
Static pages: Simple content pages
Dynamic routes: Pattern-based URLs
Web forms: User data collection
DocType web views: Database-driven content
def get_context ( context ):
try :
context.product = frappe.get_doc( 'Item' , slug)
except frappe.DoesNotExistError:
frappe.throw( 'Product not found' , frappe.PageDoesNotExistError)
def get_context ( context ):
# Limit database queries
# Use caching where appropriate
# Lazy load heavy content
context.featured_items = frappe.cache.get_value(
'featured_items' ,
generator = lambda : get_featured_items()
)
def get_context ( context ):
# Always validate user input
slug = frappe.form_dict.get( 'slug' )
if not slug or not slug.isalnum():
frappe.throw( 'Invalid request' )
# Check permissions
if frappe.session.user == 'Guest' :
frappe.throw( 'Login required' , frappe.PermissionError)
Troubleshooting routes
Debug routing
# Enable website route debugging
frappe.conf.developer_mode = 1
# Check route resolution
from frappe.website.router import resolve_route
page_info = resolve_route( '/products/laptop' )
print (page_info)
Common issues
404 errors
Template errors
Permission errors
Check if page file exists in www/ folder
Verify route in Web Page
Check DocType has_web_view and route field
Clear cache: bench clear-cache
Check template syntax
Verify template extends correct base
Check context variables are defined
Look at error logs: bench watch
Check has_website_permission
Verify user authentication
Check DocType permissions
Review portal settings
Hooks system Configure route rules and redirects
Forms Understand web forms
Permissions Control page access
DocType system Enable web views for DocTypes