01
HTML Rules — Jinja Templates
Frappe uses Jinja2 to
render dynamic HTML. Your code is injected into a global layout — no
need for <html> or <body>.
Do's
- Write content only — Frappe handles layout
-
Use Bootstrap:
container,row,col-md-6 -
Semantic tags:
<section>,<article> - Escape output:
{{ value | e }} - Absolute URLs:
{{ "/path" | abs_url }}
Don'ts
-
No
<html>,<head>,<body> - No duplicate
idattributes - No inline styles
- No hardcoded URLs
Always escape user input with
| e filter to prevent
XSS attacks.
Jinja Template
{# Dynamic content #} <div class="my-page"> <h1>{{ doc.title or "Welcome" }}</h1> {% for item in doc.items %} <p>{{ item.name | e }}</p> {% endfor %} <a href="{{ '/about' | abs_url }}">About</a> </div>
02
CSS Rules & Scoping
Critical: Unscoped CSS leaks break the entire
ERPNext Desk UI!
Forbidden
/* Breaks system UI */
* { margin: 0; padding: 0; }
body { font-size: 16px; }
h1 { color: red; }
Always Scope
/* Safe — scoped wrapper */ .my-page h1 { color: var(--primary-color); }
Namespacing Pattern
<div class="my-unique-page"> <h1>Title</h1> </div> .my-unique-page { padding: 2rem; background: var(--bg-color); } .my-unique-page h1 { color: var(--text-color); }
CSS Variables Available
| Variable | Purpose | Value |
|---|---|---|
--primary-color |
Brand color | #2490E3 |
--text-color |
Default text | #2d3748 |
--bg-color |
Background | #ffffff |
--card-bg |
Card background | #f8f9fa |
--border-color |
Borders | #e2e8f0 |
03
JavaScript — Client Side
Frappe provides jQuery and
Vanilla JS. Always wrap
code in frappe.ready().
Initialization
// ✅ Always use frappe.ready frappe.ready(() => { console.log("Page loaded!"); initMyComponent(); });
API Call
frappe.call({ method: "my_app.api.get_data", args: { doctype: "Lead", status: "Open" }, freeze: true, callback: function(r) { if (!r.exc) { console.log("Data:", r.message); } }, error: () => frappe.throw(__("Failed!")) });
Utility Methods
frappe.msgprint(__('msg'))Show modal dialog
frappe.show_alert({message, indicator})Toast notification
frappe.throw(__('Error'))Error alert
frappe.confirm(__('Sure?'), fn)Confirmation dialog
04
Web Page Doctype Structure
| Field | Content | Notes |
|---|---|---|
| HTML Template | HTML + Jinja | Structure, loops, conditionals |
| Style | CSS only | No <style> tags needed |
| Script | JavaScript only | No <script> tags needed |
HTML Template
<div class="portal-wrapper">
<h1>{{ doc.page_title }}</h1>
{% for item in doc.items %}
<p>{{ item.name }}</p>
{% endfor %}
</div>
<h1>{{ doc.page_title }}</h1>
{% for item in doc.items %}
<p>{{ item.name }}</p>
{% endfor %}
</div>
Style
.portal-wrapper { padding: 2rem; }
.portal-wrapper h1 { color: var(--primary-color); }
.portal-wrapper h1 { color: var(--primary-color); }
Script
frappe.ready(() => {
console.log("Ready!");
});
console.log("Ready!");
});
05
Common Mistakes
Style Bleeding
.btn { background: red; }
/* Kills ALL system buttons */
Fix
.my-page .btn { background: var(--primary-color); }
Hardcoded URL
<a href="http://localhost:8000/about"> About </a>
Fix
<a href="{{ '/about' | abs_url }}"> About </a>
| ❌ Mistake | ✅ Solution |
|---|---|
| System styles broken | Always use wrapper classes |
| Links break on production | Use | abs_url filter |
| Modals are covered | Keep z-index below 1000 |
| Page freezes | Never use synchronous AJAX |
06
Best Practice Structure
HTML
<div class="portal-container"> <section class="hero"> <h1>{{ doc.title or "Welcome" }}</h1> <p>{{ doc.subtitle or _("Welcome!") }}</p> <button id="submit-btn" class="btn btn-primary"> {{ _("Get Started") }} </button> </section> </div>
CSS
.portal-container { max-width: 1200px; margin: 0 auto; padding: var(--padding, 2rem); } .portal-container .hero { padding: 4rem 2rem; background: var(--card-bg); border-radius: 12px; border: 1px solid var(--border-color); }
JavaScript
frappe.ready(() => { const btn = document.getElementById('submit-btn'); btn?.addEventListener('click', () => { frappe.confirm(__('Proceed?'), () => { frappe.call({ method: 'my_app.api.process', callback: (r) => { if (!r.exc) frappe.show_alert({ message: __('Done!'), indicator: 'green' }); } }); }); }); });
07
Advanced Pro Tips
Localization & i18n
JS + Jinja
// JavaScript const msg = __("Hello!"); const n = n_("item", "items", count); {# Jinja #} <h1>{{ _("Welcome") }}</h1>
Security Checklist
| Check | Implementation |
|---|---|
| ✅ CSRF | frappe.call handles automatically |
| ✅ XSS | {{ input | e }} in Jinja |
| ✅ Permissions | frappe.has_permission() |
| ✅ Guest Access | @frappe.whitelist(allow_guest=True) |
Python
@frappe.whitelist() def secure_method(data): if not frappe.has_permission("DocType", "create"): frappe.throw(_("No permission")) safe = frappe.sanitize_html(data) return { "status": "success" }
Performance & Caching
Python
@frappe.whitelist() def get_cached_data(): key = "my_app:data" data = frappe.cache().get_value(key) if not data: data = frappe.get_all("DocType", fields=["name", "title"], limit=10) frappe.cache().set_value(key, data, expires_in_sec=3600) return data
Real-time Updates
JavaScript
frappe.realtime.on('task_progress', (data) => { frappe.show_progress(__('Loading'), data.percent, 100); }); frappe.realtime.on('task_complete', () => { frappe.msgprint(__('Done!')); });
DB Operations
JavaScript
// Insert frappe.db.insert({ doctype: "Lead", first_name: "John" }) .then(doc => console.log(doc.name)); // Update frappe.db.set_value("Lead", "DOC-001", "status", "Converted"); // Delete frappe.db.delete("Lead", "DOC-001");
hooks.py Reference
| Hook | Purpose |
|---|---|
app_include_js |
JS for Desk + Website |
web_include_js |
JS for Website only |
app_include_css |
CSS for Desk + Website |
website_route_rules |
Custom URL routing |
Debugging Tips
JS +
Python
// Browser console frappe.ready(() => { console.log("User:", frappe.session.user); console.log("Boot:", frappe.boot); }); # Python server log frappe.errprint(f"Debug: {my_variable}")
Mohamed Ayman
Frappe Framework Developer — Available for consulting & custom
development
MIT License — Free to use and modify · Built with for the Frappe Community