- v6.8.0 Sites & Forms publics (migrations 24)
- v6.9.0 Recherche sémantique hybride + Ask AI (migration 25)
- v7.0.0 Automations v2 multi-étapes + Workers sandboxés (migration 26)
- v7.1.0 Calendar sync Google/CalDAV + Meeting Notes (migration 27)
- v7.2.0 Enterprise : SCIM 2.0, 2FA TOTP/passkeys, audit UI, agent approvals (migration 28)
- v7.3.0 Wiki/Teamspaces, verified pages, collab polish, charts, unfurl (migration 29)
- docs V68→V73, ROADMAP/CHANGELOG/WORKLOAD à jour, VERSION 7.3.0
- A9 : flowdeck.db, flowdeck_dev.db, test-commit.md, upload_test.txt et e2e/{node_modules,shots,test-results} désindexés + ignorés (.gitignore/.dockerignore)
122 lines
5.2 KiB
Markdown
122 lines
5.2 KiB
Markdown
# V6.8.0 — Sites & Forms publics
|
|
|
|
> **Statut** : ✅ Livré en v6.8.0 (2026-09-28) — `app/routers/sites.py`, migration 24, 20 tests verts · **Roadmap** : `ROADMAP.md § v6.8.0`
|
|
> **Référence Notion** : Sites (multi-pages, domaine custom, SEO, analytics, password/expiry) + Forms (soumission anonyme → DB, embed, notifs).
|
|
> **Existant réutilisé** : page publique `/p/<slug>` (`dashboard.py`), `_render_blocks_public`, `export.py` (récursif + images), `validate_property_rule` (`property_types.py`), `notifications` + `mailer.py`, vue Form (`_database_table_scripts.html`).
|
|
|
|
---
|
|
|
|
## 1. Vision
|
|
|
|
Passer de « une page publiée isolée » à « publier un mini-site » et « collecter des réponses » :
|
|
`page privée → site (arbre + thème + gating) → URL publique /s/<slug>` et
|
|
`collection → form public /f/<token> → ligne collection_pages`.
|
|
|
|
Hors scope : builder drag&drop marketing, paiement, commentaires publics.
|
|
|
|
## 2. Sites multi-pages
|
|
|
|
### 2.1 Schéma (migration 24)
|
|
|
|
```sql
|
|
CREATE TABLE sites (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
slug TEXT NOT NULL UNIQUE, -- /s/<slug>
|
|
root_page_id INTEGER NOT NULL REFERENCES pages(id) ON DELETE CASCADE,
|
|
title TEXT NOT NULL DEFAULT '',
|
|
theme TEXT NOT NULL DEFAULT 'dark', -- light|dark
|
|
custom_domain TEXT UNIQUE, -- Host header match
|
|
password_hash TEXT, -- NULL = public
|
|
expires_at TIMESTAMP, -- NULL = jamais
|
|
noindex BOOLEAN NOT NULL DEFAULT 0,
|
|
analytics_id TEXT DEFAULT '', -- Plausible/GA, pas d'IP brute
|
|
created_by INTEGER REFERENCES users(id),
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
);
|
|
CREATE TABLE site_pages (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
site_id INTEGER NOT NULL REFERENCES sites(id) ON DELETE CASCADE,
|
|
page_id INTEGER NOT NULL REFERENCES pages(id) ON DELETE CASCADE,
|
|
position INTEGER NOT NULL DEFAULT 0,
|
|
UNIQUE(site_id, page_id)
|
|
);
|
|
CREATE TABLE site_views (
|
|
site_id INTEGER NOT NULL REFERENCES sites(id) ON DELETE CASCADE,
|
|
day TEXT NOT NULL, -- YYYY-MM-DD UTC
|
|
views INTEGER NOT NULL DEFAULT 0,
|
|
PRIMARY KEY (site_id, day)
|
|
);
|
|
```
|
|
|
|
Règles : arbre construit depuis `site_pages` ordonné ; page hors site → 404 ; page supprimée → exclue + état « Deleted » ; `resolve_content_json()` (synced) appliqué comme `/p/<slug>`.
|
|
|
|
### 2.2 Gating & SEO
|
|
|
|
- Mot de passe : bcrypt, cookie signé `site_auth_<id>` 24h, `GET/POST /s/<slug>/auth`.
|
|
- Expiry : `expires_at < now` → 410 « Site expiré ».
|
|
- SEO : `<title>`, meta description (1er paragraphe), OG/Twitter (cover+icône), `sitemap.xml`, `robots.txt` (`noindex` → `noindex,nofollow`).
|
|
- Stats : `+1/jour/site` sur chaque vue (upsert), `GET /api/v2/sites/{id}/stats?days=30`.
|
|
|
|
### 2.3 Routes
|
|
|
|
```
|
|
GET /s/<slug> → home (root_page + nav)
|
|
GET /s/<slug>/<page-slug> → page du site (nav active)
|
|
GET /s/<slug>/sitemap.xml
|
|
GET|POST /s/<slug>/auth → gate password
|
|
GET|POST|PATCH|DELETE /api/v2/sites
|
|
GET|POST|DELETE /api/v2/sites/{id}/pages
|
|
GET /api/v2/sites/{id}/stats
|
|
```
|
|
|
|
Domaine custom : si `Host == custom_domain` → monte le site sans `/s/<slug>`.
|
|
|
|
## 3. Forms publics
|
|
|
|
### 3.1 Config (colonne `collections.form_config_json`)
|
|
|
|
```json
|
|
{
|
|
"enabled": true, "public_token": "f_abc123",
|
|
"title": "Contact", "success_message": "Merci !",
|
|
"fields": ["Name", "Email", "Message"],
|
|
"required": ["Name", "Email"],
|
|
"notify_user_ids": [1, 2]
|
|
}
|
|
```
|
|
|
|
Table `form_responses (id, collection_id, row_id, ip_hash, created_at)` — `ip_hash` = sha256(IP+jour), jamais d'IP brute.
|
|
|
|
### 3.2 Soumission anonyme
|
|
|
|
```
|
|
GET /f/<token> → formulaire thèmable (no-auth, `?embed=1` sans chrome)
|
|
POST /f/<token> → valide via validate_property_rule → 400 + messages
|
|
rate-limit 20/h/IP → 429, honeypot __hp + Turnstile optionnel
|
|
→ collection_pages + form_responses + notif in-app/email
|
|
```
|
|
|
|
Trigger automation `form.submitted` (v7.0.0 s'y branchera).
|
|
|
|
Embed : `<iframe src="https://host/f/<token>?embed=1" width="100%" height="600">`.
|
|
|
|
## 4. UI
|
|
|
|
- Éditeur « … » → `Share → Publish → Site` : créer site, choisir pages, thème, password/expiry/noindex, copier URL + snippet.
|
|
- Collection → `Views → Form → Share form` : toggle public, champs affichés, message succès, liste réponses (compteur + lien lignes filtrées `form:true`).
|
|
- Settings → Sites (liste, stats sparkline, domaine custom, revoke).
|
|
|
|
## 5. Sécurité / perfs
|
|
|
|
- Password bcrypt cost 12, cookie `itsdangerous` signé, CSRF exempt uniquement `POST /f/<token>` (rate-limit + honeypot compensent).
|
|
- `custom_domain` validé (hostname, pas d'IP privée) ; `slug` `^[a-z0-9-]{3,50}$`.
|
|
- Cache rendu public 60s (`Cache-Control: public, max-age=60`), stats en upsert (pas de ligne/vue).
|
|
|
|
## 6. Tests (`tests/test_v68_sites_forms.py`, ~25)
|
|
|
|
CRUD site, nav ordonnée, 404 hors-site, password OK/KO + cookie, expiry 410, noindex meta, sitemap, stats +1/jour, domaine custom (Host override), form GET anonyme, POST valide → ligne, POST invalide → 400, rate-limit 429, honeypot 400, embed `?embed=1`, notif créée, ACL (non-owner 403/404).
|
|
|
|
## 7. Rollout
|
|
|
|
1. Migration 24 + CRUD API + tests. 2. Rendu `/s/` + gating + SEO. 3. Forms + embed + notifs. 4. UI Share/Site + docs `/help` + OpenAPI.
|