$npx -y skills add Impertio-Studio/Frappe_Claude_Skill_Package --skill frappe-impl-websiteUse when building portal pages, Web Forms, website routes, or configuring themes and SEO in Frappe. Prevents 404 errors from wrong route resolution, broken Web Form submissions, and missing meta tags for SEO. Covers Web Page, Web Form, Portal Settings, Website Settings, website r
| 1 | # Frappe Website & Portals — Implementation Workflows |
| 2 | |
| 3 | Step-by-step workflows for building websites, portals, and public-facing pages. For hooks syntax see `frappe-impl-hooks`. For Jinja templating see `frappe-impl-jinja`. |
| 4 | |
| 5 | **Version**: v14/v15/v16 | **Note**: v15+ uses Bootstrap 5; v14 uses Bootstrap 4. |
| 6 | |
| 7 | ## Quick Decision: Which Page Type? |
| 8 | |
| 9 | ``` |
| 10 | WHAT do you need? |
| 11 | ├── Static content page (About, Terms) → Web Page DocType or www/ HTML |
| 12 | ├── Data entry by external users → Web Form |
| 13 | ├── List of records visible on website → has_web_view on DocType |
| 14 | ├── Blog / news articles → Blog Post + Blog Category |
| 15 | ├── Custom app with sidebar/toolbar → Custom Portal Page (www/) |
| 16 | └── Dynamic route with parameters → website_route_rules in hooks.py |
| 17 | ``` |
| 18 | |
| 19 | See `references/decision-tree.md` for the complete decision tree. |
| 20 | |
| 21 | ## Workflow 1: Create a Portal Page (www/) |
| 22 | |
| 23 | Portal pages live in your app's `www/` directory. The file name becomes the URL route. |
| 24 | |
| 25 | 1. Create `myapp/www/custom_page.html`: |
| 26 | |
| 27 | ```html |
| 28 | {% extends "templates/web.html" %} |
| 29 | {% block page_content %} |
| 30 | <h1>{{ title }}</h1> |
| 31 | <div>{{ content }}</div> |
| 32 | {% endblock %} |
| 33 | ``` |
| 34 | |
| 35 | 2. Create matching controller `myapp/www/custom_page.py`: |
| 36 | |
| 37 | ```python |
| 38 | import frappe |
| 39 | |
| 40 | def get_context(context): |
| 41 | context.title = "My Custom Page" |
| 42 | context.content = "Hello World" |
| 43 | context.no_cache = 1 # ALWAYS set for dynamic content |
| 44 | ``` |
| 45 | |
| 46 | 3. Result: page available at `/custom_page` |
| 47 | |
| 48 | **File types auto-loaded**: `.html` (template), `.py` (controller), `.css` (styles), `.js` (scripts). |
| 49 | |
| 50 | **Subdirectory pattern** — for nested routes: |
| 51 | ``` |
| 52 | myapp/www/ |
| 53 | ├── services/ |
| 54 | │ ├── index.html → /services |
| 55 | │ ├── index.py |
| 56 | │ ├── consulting.html → /services/consulting |
| 57 | │ └── consulting.py |
| 58 | ``` |
| 59 | |
| 60 | ### Context Variables Reference |
| 61 | |
| 62 | | Key | Type | Effect | |
| 63 | |-----|------|--------| |
| 64 | | `title` | str | Page title and browser tab | |
| 65 | | `no_cache` | bool | Disable page caching | |
| 66 | | `no_header` | bool | Hide the page header | |
| 67 | | `no_breadcrumbs` | bool | Remove breadcrumbs | |
| 68 | | `add_breadcrumbs` | bool | Auto-generate from folder structure | |
| 69 | | `show_sidebar` | bool | Display web sidebar | |
| 70 | | `sitemap` | int | 0 = exclude from sitemap, 1 = include | |
| 71 | | `metatags` | dict | SEO meta tags (see Workflow 7) | |
| 72 | |
| 73 | **Rule**: ALWAYS set `no_cache = 1` for pages with user-specific or frequently changing content. |
| 74 | |
| 75 | ## Workflow 2: Create a Web Form |
| 76 | |
| 77 | Web Forms let external users submit data that creates Frappe documents. |
| 78 | |
| 79 | 1. Navigate to **Web Form** list → **New Web Form** |
| 80 | 2. Set **Title**, select target **DocType**, set **Route** (URL slug) |
| 81 | 3. Add fields — ALWAYS match `fieldname` to the target DocType field names |
| 82 | 4. Configure access: |
| 83 | - **Login Required**: uncheck for guest submissions |
| 84 | - **Allow Edit**: let users edit their submissions |
| 85 | - **Allow Multiple**: let users submit more than once |
| 86 | 5. Save and publish |
| 87 | |
| 88 | ### Guest Submissions |
| 89 | |
| 90 | ``` |
| 91 | ALLOWING guest submissions? |
| 92 | ├── YES → Uncheck "Login Required" |
| 93 | │ → Set "Guest Title" for the submission form |
| 94 | │ → ALWAYS add rate limiting in site_config: |
| 95 | │ "rate_limit": {"web_form": "5/hour"} |
| 96 | │ → ALWAYS validate server-side (guests can bypass JS) |
| 97 | └── NO → Keep "Login Required" checked (default) |
| 98 | ``` |
| 99 | |
| 100 | ### Web Form Custom Script (Client) |
| 101 | |
| 102 | ```javascript |
| 103 | frappe.web_form.on("after_load", function() { |
| 104 | // Runs after form loads in browser |
| 105 | }); |
| 106 | |
| 107 | frappe.web_form.on("before_submit", function() { |
| 108 | // Validate before submission — return false to cancel |
| 109 | let val = frappe.web_form.get_value("email"); |
| 110 | if (!val) { |
| 111 | frappe.throw("Email is required"); |
| 112 | return false; |
| 113 | } |
| 114 | }); |
| 115 | |
| 116 | frappe.web_form.on("after_submit", function() { |
| 117 | // Redirect or show message after success |
| 118 | window.location.href = "/thank-you"; |
| 119 | }); |
| 120 | ``` |
| 121 | |
| 122 | ### Web Form Custom Script (Server: Python) |
| 123 | |
| 124 | In the Web Form document, add a Python script: |
| 125 | |
| 126 | ```python |
| 127 | def get_context(context): |
| 128 | # Add custom context variables for the template |
| 129 | context.categories = frappe.get_all("Category", fields=["name", "title"]) |
| 130 | ``` |
| 131 | |
| 132 | **Rule**: NEVER trust client-side validation alone for Web Forms. ALWAYS validate in the target DocType's controller or server script. |
| 133 | |
| 134 | ## Workflow 3: Enable has_web_view on a DocType |
| 135 | |
| 136 | This mak |