Skip to main content
ClaudeWave
Instalar en Claude Code
Copiar
git clone --depth 1 https://github.com/Impertio-Studio/Frappe_Claude_Skill_Package /tmp/frappe-impl-website && cp -r /tmp/frappe-impl-website/skills/source/impl/frappe-impl-website ~/.claude/skills/frappe-impl-website
Después abre una sesión nueva de Claude Code; el skill carga automáticamente.

SKILL.md

# 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`:

```html
{% extends "templates/web.html" %}
{% block page_content %}
<h1>{{ title }}</h1>
<div>{{ content }}</div>
{% endblock %}
```

2. Create matching controller `myapp/www/custom_page.py`:

```python
import frappe

def get_context(context):
    context.title = "My Custom Page"
    context.content = "Hello World"
    context.no_cache = 1  # ALWAYS set for dynamic content
```

3. 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 |
|-----|------|--------|
| `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.

## 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)

```javascript
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:

```python
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:

```python
website_generators = ["Article"]
```

6. In the controller, implement `get_context`:

```python
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.

```python
# 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_