Frappe_Claude_Skill_Package frappe-impl-website

install
source · Clone the upstream repo
git clone https://github.com/OpenAEC-Foundation/Frappe_Claude_Skill_Package
Claude Code · Install into ~/.claude/skills/
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"
manifest: skills/source/impl/frappe-impl-website/SKILL.md
source content

Frappe 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.

  1. Create
    myapp/www/custom_page.html
    :
{% extends "templates/web.html" %}
{% block page_content %}
<h1>{{ title }}</h1>
<div>{{ content }}</div>
{% endblock %}
  1. 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
  1. 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

KeyTypeEffect
title
strPage title and browser tab
no_cache
boolDisable page caching
no_header
boolHide the page header
no_breadcrumbs
boolRemove breadcrumbs
add_breadcrumbs
boolAuto-generate from folder structure
show_sidebar
boolDisplay web sidebar
sitemap
int0 = exclude from sitemap, 1 = include
metatags
dictSEO 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.

  1. Navigate to Web Form list → New Web Form
  2. Set Title, select target DocType, set Route (URL slug)
  3. Add fields — ALWAYS match
    fieldname
    to the target DocType field names
  4. Configure access:
    • Login Required: uncheck for guest submissions
    • Allow Edit: let users edit their submissions
    • Allow Multiple: let users submit more than once
  5. 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
).

  1. Open DocType → check Has Web View and Allow Guest to View
  2. Set the Route field prefix (e.g.,
    articles
    )
  3. ALWAYS add these fields to the DocType:
    • route
      (Data, hidden) — auto-generated URL slug
    • published
      (Check) — controls visibility
  4. Create templates in the DocType directory:
    • {doctype_name}.html
      — single record template
    • {doctype_name}_row.html
      — list item template
  5. In
    hooks.py
    , register as website generator:
website_generators = ["Article"]
  1. 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

  1. Create Blog Category documents (e.g., "News", "Updates")
  2. 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}
  3. 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

  1. Navigate to Website Theme → New
  2. Configure: fonts, colors, navbar style, button radius
  3. Add custom CSS in the Custom CSS field
  4. 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
    /sitemap.xml
    from published Web Pages and has_web_view documents
  • Exclude pages: set
    sitemap = 0
    in context or frontmatter
  • Custom robots.txt: set
    robots_txt
    path in
    site_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
    ignore_csrf
    in production
  • 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-PatternCorrect Approach
Hard-coding HTML in
get_context
Use Jinja templates with context variables
Skipping
no_cache
on dynamic pages
ALWAYS set
no_cache = 1
for user-specific content
Guest Web Form without rate limitingALWAYS configure rate limits for guest forms
Missing
published
field on has_web_view
ALWAYS add published check to prevent data leaks
Using
website_route_rules
for simple redirects
Use
website_redirects
instead
Putting business logic in www/ controllersKeep in DocType controllers; www/ is for presentation

See

references/anti-patterns.md
for expanded anti-patterns with examples.

See Also

  • frappe-impl-hooks
    — Website hooks in detail
  • frappe-impl-jinja
    — Jinja templating patterns
  • frappe-impl-controllers
    — DocType controllers (WebsiteGenerator)
  • frappe-syntax-clientscripts
    — Client-side API for Web Forms
  • references/generators.md
    — Portal generators, blog system, custom routing patterns
  • references/workflows.md
    — Extended workflow walkthroughs
  • references/examples.md
    — Complete code examples
  • references/decision-tree.md
    — Full decision tree for page types