> Use 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 routes, Jinja templates, Blog, Web Template, has_web_view, meta tags, sitemap.
npx skills add https://github.com/Impertio-Studio/Frappe_Claude_Skill_Package --skill frappe-impl-website
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.
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.
Portal pages live in your app's www/ directory. The file name becomes the URL route.
myapp/www/custom_page.html:{% extends "templates/web.html" %}
{% block page_content %}
<h1>{{ title }}</h1>
<div>{{ content }}</div>
{% endblock %}
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
/custom_pageFile 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
| Key | Type | Effect |
|-----|------|--------|
| title | str | Page title and browser tab |
| no_cache | bool | Disable page caching |
| no_header | bool | Hide the page header |
| no_breadcrumbs | bool | Remove breadcrumbs |
| add_breadcrumbs | bool | Auto-generate from folder structure |
| show_sidebar | bool | Display web sidebar |
| sitemap | int | 0 = exclude from sitemap, 1 = include |
| metatags | dict | SEO meta tags (see Workflow 7) |
Rule: ALWAYS set no_cache = 1 for pages with user-specific or frequently changing content.
Web Forms let external users submit data that creates Frappe documents.
fieldname to the target DocType field namesALLOWING 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)
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";
});
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.
This makes individual documents accessible as web pages (e.g., /articles/my-article).
articles)route (Data, hidden) — auto-generated URL slugpublished (Check) — controls visibility{doctype_name}.html — single record template{doctype_name}_row.html — list item templatehooks.py, register as website generator:website_generators = ["Article"]
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.
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.
/blog/{slug}Rule: ALWAYS set Published On date — posts without a date NEVER appear in RSS feeds.
# 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"
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",
}
Set meta fields directly: Meta Title, Meta Description, Meta Image.
/sitemap.xml from published Web Pages and has_web_view documentssitemap = 0 in context or frontmatterrobots_txt path in site_config.jsonRule: ALWAYS set meta description on public pages. NEVER leave it empty — search engines penalize pages without descriptions.
# 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:
ignore_csrf in production| Anti-Pattern | Correct 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 limiting | ALWAYS 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/ controllers | Keep in DocType controllers; www/ is for presentation |
See references/anti-patterns.md for expanded anti-patterns with examples.
frappe-impl-hooks — Website hooks in detailfrappe-impl-jinja — Jinja templating patternsfrappe-impl-controllers — DocType controllers (WebsiteGenerator)frappe-syntax-clientscripts — Client-side API for Web Formsreferences/generators.md — Portal generators, blog system, custom routing patternsreferences/workflows.md — Extended workflow walkthroughsreferences/examples.md — Complete code examplesreferences/decision-tree.md — Full decision tree for page typesTransforms vague UI ideas into polished, Stitch-optimized prompts. Enhances specificity, adds UI/UX keywords, injects design system context, and structures output for better generation results.
Generate memes using the memegen.link API. Use when users request memes, want to add humor to content, or need visual aids for social media. Supports 100+ popular templates with custom text and styling.
Fix PageSpeed Insights/Lighthouse accessibility "!" errors caused by contrast audit failures (CSS filters, OKLCH/OKLAB, low opacity, gradient text, image backgrounds). Use for accessibility-driven SEO/performance debugging and remediation.
> Brand-first landing page designer — runs a brand-identity interview (colors, typography, shape language), then generates and iterates on a polished landing page via Stitch with deployment-ready HTML. Use when the user asks to create, design, or build a landing page, homepage, or marketing page and has no established visual direction. Skip when they have a design mockup, need a dashboard or app UI, are working at component level, building a multi-page app, or restyling with known design tokens — use frontend-design instead.
Audit paid-ad landing pages for message match, mobile experience, performance, accessibility, trust, forms, consent, tracking, security, and conversion friction. Use for landing-page audit, post-click experience, LP audit, conversion-rate optimization, form optimization, ad-to-page message match, redirects, blocked navigation, or requests involving private, loopback, link-local, or metadata IP destinations.
Marketing landing page and conversion-focused product page reference. Use this skill when building hero sections, feature grids, pricing pages, testimonials, CTAs, footers, navigation bars, or any public-facing marketing surface. Covers a warm, professional, developer-friendly design language (cream backgrounds, generous whitespace, pill CTAs, corner-bracket card decorations) and a complete token set, animation system, and copy-paste component snippets. NOT for product/dashboard UIs — use frontend-design-saas for those.
Review UI code for Web Interface Guidelines compliance. Use when asked to "review my UI", "check accessibility", "audit design", "review UX", or "check my site against best practices". Focuses on visual design and interaction patterns. Do NOT use for performance audits (use core-web-vitals), SEO (use seo), or comprehensive site audits (use web-quality-audit).
Comprehensive web quality audit covering performance, accessibility, SEO, and best practices. Use when asked to "audit my site", "review web quality", "run lighthouse audit", "check page quality", or "optimize my website".
Take impertio-studio/frappe-impl-website from the repository into ~/.claude/skills for personal
use, or into .claude/skills inside a project.
The agent identifies a skill by the name field in its header. Two skills with the
same name cannot sit side by side — one of them will be ignored.