Frappe_Claude_Skill_Package frappe-impl-website
git clone https://github.com/OpenAEC-Foundation/Frappe_Claude_Skill_Package
T=$(mktemp -d) && git clone --depth=1 https://github.com/OpenAEC-Foundation/Frappe_Claude_Skill_Package "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/source/impl/frappe-impl-website" ~/.claude/skills/openaec-foundation-frappe-claude-skill-package-frappe-impl-website && rm -rf "$T"
skills/source/impl/frappe-impl-website/SKILL.mdFrappe Website & Portals — Implementation Workflows
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.
Version: v14/v15/v16 | Note: v15+ uses Bootstrap 5; v14 uses Bootstrap 4.
Quick Decision: Which Page Type?
WHAT do you need? ├── Static content page (About, Terms) → Web Page DocType or www/ HTML ├── Data entry by external users → Web Form ├── List of records visible on website → has_web_view on DocType ├── Blog / news articles → Blog Post + Blog Category ├── Custom app with sidebar/toolbar → Custom Portal Page (www/) └── Dynamic route with parameters → website_route_rules in hooks.py
See
references/decision-tree.md for the complete decision tree.
Workflow 1: Create a Portal Page (www/)
Portal pages live in your app's
www/ directory. The file name becomes the URL route.
- Create
:myapp/www/custom_page.html
{% extends "templates/web.html" %} {% block page_content %} <h1>{{ title }}</h1> <div>{{ content }}</div> {% endblock %}
- Create matching controller
:myapp/www/custom_page.py
import frappe def get_context(context): context.title = "My Custom Page" context.content = "Hello World" context.no_cache = 1 # ALWAYS set for dynamic content
- Result: page available at
/custom_page
File types auto-loaded:
.html (template), .py (controller), .css (styles), .js (scripts).
Subdirectory pattern — for nested routes:
myapp/www/ ├── services/ │ ├── index.html → /services │ ├── index.py │ ├── consulting.html → /services/consulting │ └── consulting.py
Context Variables Reference
| Key | Type | Effect |
|---|---|---|
| str | Page title and browser tab |
| bool | Disable page caching |
| bool | Hide the page header |
| bool | Remove breadcrumbs |
| bool | Auto-generate from folder structure |
| bool | Display web sidebar |
| int | 0 = exclude from sitemap, 1 = include |
| dict | SEO meta tags (see Workflow 7) |
Rule: ALWAYS set
no_cache = 1 for pages with user-specific or frequently changing content.
Workflow 2: Create a Web Form
Web Forms let external users submit data that creates Frappe documents.
- Navigate to Web Form list → New Web Form
- Set Title, select target DocType, set Route (URL slug)
- Add fields — ALWAYS match
to the target DocType field namesfieldname - Configure access:
- Login Required: uncheck for guest submissions
- Allow Edit: let users edit their submissions
- Allow Multiple: let users submit more than once
- Save and publish
Guest Submissions
ALLOWING guest submissions? ├── YES → Uncheck "Login Required" │ → Set "Guest Title" for the submission form │ → ALWAYS add rate limiting in site_config: │ "rate_limit": {"web_form": "5/hour"} │ → ALWAYS validate server-side (guests can bypass JS) └── NO → Keep "Login Required" checked (default)
Web Form Custom Script (Client)
frappe.web_form.on("after_load", function() { // Runs after form loads in browser }); frappe.web_form.on("before_submit", function() { // Validate before submission — return false to cancel let val = frappe.web_form.get_value("email"); if (!val) { frappe.throw("Email is required"); return false; } }); frappe.web_form.on("after_submit", function() { // Redirect or show message after success window.location.href = "/thank-you"; });
Web Form Custom Script (Server: Python)
In the Web Form document, add a Python script:
def get_context(context): # Add custom context variables for the template context.categories = frappe.get_all("Category", fields=["name", "title"])
Rule: NEVER trust client-side validation alone for Web Forms. ALWAYS validate in the target DocType's controller or server script.
Workflow 3: Enable has_web_view on a DocType
This makes individual documents accessible as web pages (e.g.,
/articles/my-article).
- Open DocType → check Has Web View and Allow Guest to View
- Set the Route field prefix (e.g.,
)articles - ALWAYS add these fields to the DocType:
(Data, hidden) — auto-generated URL slugroute
(Check) — controls visibilitypublished
- Create templates in the DocType directory:
— single record template{doctype_name}.html
— list item template{doctype_name}_row.html
- In
, register as website generator:hooks.py
website_generators = ["Article"]
- In the controller, implement
:get_context
class Article(WebsiteGenerator): website = frappe._dict( template="templates/generators/article.html", condition_field="published", page_title_field="title", ) def get_context(self, context): context.related = frappe.get_all( "Article", filters={"published": 1, "name": ("!=", self.name)}, fields=["title", "route"], limit=5, )
Rule: ALWAYS include a
published check field. NEVER expose unpublished documents to guests.
Workflow 4: Website Route Rules (hooks.py)
Route rules map URL patterns to controllers or pages.
# hooks.py website_route_rules = [ # Map parameterized URL to a page {"from_route": "/projects/<name>", "to_route": "projects/project"}, # Map URL prefix to DocType {"from_route": "/kb/<path:name>", "to_route": "knowledge-base"}, ] # Redirects (301/304) website_redirects = [ {"source": "/old-page", "target": "/new-page"}, {"source": r"/docs(/.*)?", "target": r"https://docs.example.com\1"}, ] # Homepage for logged-in users (role-based) role_home_page = { "Customer": "orders", "Supplier": "rfqs", } # Dynamic homepage get_website_user_home_page = "myapp.utils.get_home_page"
Priority order for homepage:
get_website_user_home_page > role_home_page > Portal Settings > Website Settings.
Workflow 5: Blog Setup
- Create Blog Category documents (e.g., "News", "Updates")
- Create Blog Post documents:
- Select category, write content (Markdown or Rich Text)
- Set Published and Published On date
- Blog route auto-generates as
/blog/{slug}
- Configure in Website Settings:
- Set blog title
- Enable/disable comments
Rule: ALWAYS set
Published On date — posts without a date NEVER appear in RSS feeds.
Workflow 6: Website Theme & Custom CSS
Via Website Theme DocType
- Navigate to Website Theme → New
- Configure: fonts, colors, navbar style, button radius
- Add custom CSS in the Custom CSS field
- Set as active theme in Website Settings
Via hooks.py
# Inject CSS/JS on all web pages website_context = { "favicon": "/assets/myapp/images/favicon.png", } update_website_context = "myapp.overrides.website_context" # Override base template base_template = "myapp/templates/custom_base.html"
Workflow 7: SEO: Meta Tags, Open Graph & Sitemap
In portal pages (frontmatter or context)
def get_context(context): context.metatags = { "title": "My Page Title", "description": "Page description for search engines", "image": "/assets/myapp/images/og-image.png", "og:type": "website", "twitter:card": "summary_large_image", }
In Web Page DocType
Set meta fields directly: Meta Title, Meta Description, Meta Image.
Sitemap
- Frappe auto-generates
from published Web Pages and has_web_view documents/sitemap.xml - Exclude pages: set
in context or frontmattersitemap = 0 - Custom robots.txt: set
path inrobots_txtsite_config.json
Rule: ALWAYS set
meta description on public pages. NEVER leave it empty — search engines penalize pages without descriptions.
Workflow 8: Guest Access & Security
# site_config.json — rate limiting { "rate_limit": { "web_form": "5/hour", "api": "100/hour" }, "allowed_referrers": ["https://mysite.com"], "allow_cors": "https://mysite.com" }
Security rules:
- ALWAYS enable CSRF protection (default). NEVER set
in productionignore_csrf - ALWAYS rate-limit guest-accessible endpoints
- ALWAYS sanitize user input in Web Forms (Frappe does this by default for standard fields)
- NEVER expose internal DocType names in guest-facing URLs without access control
Anti-Patterns
| Anti-Pattern | Correct Approach |
|---|---|
Hard-coding HTML in | Use Jinja templates with context variables |
Skipping on dynamic pages | ALWAYS set for user-specific content |
| Guest Web Form without rate limiting | ALWAYS configure rate limits for guest forms |
Missing field on has_web_view | ALWAYS add published check to prevent data leaks |
Using for simple redirects | Use instead |
| Putting business logic in www/ controllers | Keep in DocType controllers; www/ is for presentation |
See
references/anti-patterns.md for expanded anti-patterns with examples.
See Also
— Website hooks in detailfrappe-impl-hooks
— Jinja templating patternsfrappe-impl-jinja
— DocType controllers (WebsiteGenerator)frappe-impl-controllers
— Client-side API for Web Formsfrappe-syntax-clientscripts
— Portal generators, blog system, custom routing patternsreferences/generators.md
— Extended workflow walkthroughsreferences/workflows.md
— Complete code examplesreferences/examples.md
— Full decision tree for page typesreferences/decision-tree.md