Ultimate Developer Guide

Frappe Web Development

The comprehensive handbook for building Web Pages, Templates, and Custom Blocks in the Frappe Framework.

Frappe Framework Jinja Templates Bootstrap 5 JavaScript Python MIT License
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 id attributes
  • 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>
Style
.portal-wrapper { padding: 2rem; }
.portal-wrapper h1 { color: var(--primary-color); }
Script
frappe.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
Mohamed Ayman
Frappe Framework Developer — Available for consulting & custom development

MIT License — Free to use and modify  ·  Built with for the Frappe Community

frappe.io  ·