Canonical: all shop pages now assign og_url (reusing the existing og:url assign), which the layout renders as <link rel="canonical">. Collection pages strip the sort param so ?sort=price_asc doesn't create a duplicate canonical. robots.txt: dynamic controller disallows /admin/, /api/, /users/, /webhooks/, /checkout/. Removed robots.txt from static_paths so it goes through the router instead of Plug.Static. sitemap.xml: auto-generated from all visible products + categories + static pages, served as application/xml. 8 tests. Also updates PROGRESS.md: marks tasks 55, 58, 59, 61, 62 as done. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
238 lines
10 KiB
Markdown
238 lines
10 KiB
Markdown
# Legal page generator
|
||
|
||
> Status: Planned
|
||
> Tasks: #83–85 in PROGRESS.md
|
||
> Tier: 4 (Growth & content), Phase 1 can ship earlier
|
||
|
||
## Goal
|
||
|
||
Replace the static `PreviewData` placeholder content on the four policy/legal pages with generated content that's factually accurate for each shop — because Berrypod knows exactly what it does, who processes what data, and how fulfilment works.
|
||
|
||
Not a generic "fill in the blanks" template. Not LLM-generated waffle. A set of conditional paragraph functions that produce correct, shop-specific content from actual settings and provider data.
|
||
|
||
## The problem with current approach
|
||
|
||
All four content pages (`/privacy`, `/terms`, `/delivery`, `/about`) call `PreviewData.*_content()` which returns hardcoded placeholder text. Shop owners are expected to replace it manually — but most won't, or will copy-paste something generic that doesn't match how Berrypod actually works.
|
||
|
||
Berrypod already knows:
|
||
- Which providers are connected (Printify, Printful — each with different lead times)
|
||
- Which countries it ships to (from the `shipping_rates` table)
|
||
- Whether VAT is enabled and the shop country
|
||
- Whether abandoned cart recovery is enabled
|
||
- Whether a newsletter is enabled
|
||
- The shop name and contact email
|
||
|
||
This is enough to produce accurate, legally grounded content automatically.
|
||
|
||
## Pages covered
|
||
|
||
### 1. Privacy policy (`/privacy`)
|
||
|
||
**Always included:**
|
||
|
||
- What's collected: name, email, shipping address from orders (legal basis: contract performance, Article 6(1)(b) UK/EU GDPR)
|
||
- Payment: processed by Stripe, card data never touches the shop
|
||
- Analytics: privacy-first, no cookies, no personal data stored — server-side only, includes device type, country (derived from IP, not stored), referrer
|
||
- Cookies: session cookie for cart and auth (strictly necessary, no consent required); country preference cookie for shipping rates. No tracking cookies, no third-party analytics cookies.
|
||
- Sharing: shipping details shared with the connected provider(s) — names dynamically inserted
|
||
- Retention: order data kept for 7 years (UK statutory accounting requirement); analytics data kept for 2 years
|
||
- Contact: shop contact email from settings
|
||
- Rights: right of access, rectification, deletion (with caveat: statutory retention periods apply), right to object to marketing
|
||
|
||
**Conditional sections:**
|
||
|
||
- Abandoned cart recovery enabled → "If you enter your email on our checkout page but don't complete payment, we may send you a single follow-up email. This is the only email you'll receive. You can unsubscribe at any time using the link in the email. We delete this data after 30 days." (UK PECR soft opt-in / EU legitimate interests — depending on shop country)
|
||
- Newsletter enabled → email marketing section: subscription basis, how to unsubscribe, no third-party sharing
|
||
- Stripe Tax enabled → "Tax calculation is handled by Stripe, which processes transaction and location data to determine applicable rates."
|
||
|
||
**Shop country drives jurisdiction language:**
|
||
- UK → "under UK GDPR and PECR"
|
||
- EU country → "under the EU General Data Protection Regulation (GDPR)"
|
||
- US, AU, other → generic "applicable data protection laws"
|
||
|
||
---
|
||
|
||
### 2. Delivery & returns (`/delivery`)
|
||
|
||
This is the most data-rich page — Berrypod has real numbers from the DB.
|
||
|
||
**Production lead times** — driven by connected provider(s):
|
||
|
||
| Provider | Typical production |
|
||
|----------|--------------------|
|
||
| Printify | 2–7 business days |
|
||
| Printful | 2–5 business days |
|
||
|
||
If both providers are connected, show combined note ("production times vary by product").
|
||
|
||
**Shipping destinations** — derived from `shipping_rates` table:
|
||
- Query distinct countries with rates → group into regions → list with approximate delivery windows
|
||
- If no shipping data: generic placeholder (fallback)
|
||
|
||
**Returns — POD-specific and legally accurate:**
|
||
|
||
This is where generic templates get it wrong. The correct position for print-on-demand:
|
||
|
||
- Consumer Contracts Regulations Regulation 28(1)(b) — "goods made to the consumer's specifications or clearly personalised" are **exempt** from the 14-day statutory right to cancel. Every POD product qualifies. This is the legal exemption that applies, and most shops don't cite it correctly.
|
||
- Consumer Rights Act 2015 still applies to defective goods — if the item arrives damaged or with a printing defect, the customer is entitled to a repair, replacement, or refund.
|
||
- The generated policy states this clearly: no change-of-mind returns (citing the exemption), but reprints/refunds for defects (citing CRA).
|
||
|
||
**Contact and cancellation window:**
|
||
- Contact email from settings
|
||
- Cancellation window: ~2 hours after ordering (before production begins)
|
||
|
||
---
|
||
|
||
### 3. Terms of service (`/terms`)
|
||
|
||
**Always included:**
|
||
|
||
- Governing law: driven by shop country setting
|
||
- UK → "English law"
|
||
- Ireland → "Irish law and EU regulations"
|
||
- etc.
|
||
- Products: made to order, colour variance disclaimer, all sales final (with returns caveat)
|
||
- Payment: via Stripe, orders only confirmed on successful payment
|
||
- Intellectual property: designs are the property of the shop owner; customers receive a licence for personal use
|
||
- Limitations: we're not liable for delays caused by the print provider or postal service
|
||
- Changes: terms may be updated, current version always at this URL
|
||
|
||
**Conditional:**
|
||
- VAT enabled + registered → "prices include VAT where applicable"
|
||
- Newsletter → marketing communications clause
|
||
|
||
---
|
||
|
||
### 4. Cookie policy
|
||
|
||
Currently a section within the privacy policy. Can be a standalone page if desired, or remain embedded.
|
||
|
||
**Berrypod's actual cookies (exhaustive):**
|
||
|
||
| Cookie | Purpose | Duration | Consent required? |
|
||
|--------|---------|----------|-------------------|
|
||
| `_berrypod_session` | Session state: cart contents, auth | Session | No — strictly necessary |
|
||
| `country_code` | Remember shipping country preference | 1 year | No — strictly necessary for service |
|
||
|
||
That's it. No analytics cookies. No tracking. No third-party embeds. The generated cookie policy is short and accurate.
|
||
|
||
---
|
||
|
||
### 5. About page
|
||
|
||
Not generated — it's the shop owner's own story, Berrypod can't write that for them. But the existing placeholder template should be clearly labelled as placeholder and easy to replace. The page editor (task #19) handles this properly. No changes needed here for Phase 1.
|
||
|
||
---
|
||
|
||
## Content block format
|
||
|
||
The generator produces lists of `content_blocks` in the format already used by `<.rich_text>`:
|
||
|
||
```elixir
|
||
[
|
||
%{type: :lead, text: "..."},
|
||
%{type: :heading, text: "..."},
|
||
%{type: :paragraph, text: "..."},
|
||
%{type: :list, items: ["...", "..."]},
|
||
%{type: :closing, text: "..."}
|
||
]
|
||
```
|
||
|
||
No changes to the template layer — it already knows how to render these. The generator just produces better data.
|
||
|
||
---
|
||
|
||
## Generator module
|
||
|
||
`lib/berrypod/legal_pages.ex` — one public function per page:
|
||
|
||
```elixir
|
||
defmodule Berrypod.LegalPages do
|
||
alias Berrypod.{Settings, Shipping, Providers}
|
||
|
||
def privacy_content do
|
||
shop_name = Settings.get(:shop_name) || "this shop"
|
||
contact_email = Settings.get(:contact_email)
|
||
shop_country = Settings.get(:shop_country, "GB")
|
||
abandoned_cart_enabled = Settings.get(:abandoned_cart_enabled, false)
|
||
newsletter_enabled = Settings.get(:newsletter_enabled, false)
|
||
|
||
base_sections()
|
||
|> maybe_add_abandoned_cart(abandoned_cart_enabled)
|
||
|> maybe_add_newsletter(newsletter_enabled)
|
||
|> add_jurisdiction(shop_country)
|
||
|> add_contact(shop_name, contact_email)
|
||
end
|
||
|
||
def delivery_content do
|
||
providers = Providers.connected_providers()
|
||
shipping_countries = Shipping.list_countries_with_rates()
|
||
|
||
production_section(providers)
|
||
++ shipping_section(shipping_countries)
|
||
++ returns_section()
|
||
++ cancellation_section()
|
||
end
|
||
|
||
def terms_content do
|
||
shop_name = Settings.get(:shop_name) || "this shop"
|
||
shop_country = Settings.get(:shop_country, "GB")
|
||
vat_enabled = Settings.get(:vat_enabled, false)
|
||
|
||
base_terms(shop_name)
|
||
|> add_governing_law(shop_country)
|
||
|> maybe_add_vat_clause(vat_enabled)
|
||
end
|
||
end
|
||
```
|
||
|
||
---
|
||
|
||
## Two phases
|
||
|
||
### Phase 1 — replace PreviewData (no page editor needed)
|
||
|
||
Wire `LegalPages.*_content()` into the existing `Content` LiveView, replacing the three `PreviewData.*_content()` calls for privacy, delivery, and terms. The about page stays as-is (it's the shop owner's story).
|
||
|
||
The generated content shows in the live shop immediately. No admin UI needed yet — the content is always accurate because it reflects real settings.
|
||
|
||
### Phase 2 — page editor integration
|
||
|
||
When the page editor (task #19) ships, add:
|
||
|
||
- "Regenerate from settings" button per page — reruns the generator and replaces stored content
|
||
- Content marked as "auto-generated" vs "customised" — so the admin can tell what's been manually edited
|
||
- Generator runs automatically when relevant settings change (provider connected, VAT toggled, abandoned cart enabled) — a PubSub broadcast triggers regeneration
|
||
|
||
---
|
||
|
||
## What the generator is and isn't
|
||
|
||
**Is:**
|
||
- Factually accurate based on real Berrypod behaviour
|
||
- Legally grounded (cites correct UK statutes: PECR, Consumer Contracts Regulations, Consumer Rights Act)
|
||
- Useful as a starting point that's better than any generic template
|
||
|
||
**Isn't:**
|
||
- Legal advice. The generated pages include a brief footer note: "This policy was auto-generated based on how this shop is configured. You should review it and seek independent legal advice if you're unsure."
|
||
- Comprehensive for edge cases (international VAT registration, non-UK statutory frameworks beyond GDPR)
|
||
- A substitute for a solicitor if the shop does complex things
|
||
|
||
---
|
||
|
||
## Files to create/modify
|
||
|
||
- `lib/berrypod/legal_pages.ex` — new, generator functions for each page
|
||
- `lib/berrypod_web/live/shop/content.ex` — replace three `PreviewData.*_content()` calls with `LegalPages.*_content()`
|
||
- Phase 2: page editor admin UI for saved/regenerated page content
|
||
|
||
---
|
||
|
||
## Tasks
|
||
|
||
| # | Task | Est |
|
||
|---|------|-----|
|
||
| 83 | `LegalPages` module — generate accurate privacy, delivery, and terms content from settings + provider + shipping data | 2.5h |
|
||
| 84 | Wire `LegalPages` into `Content` LiveView — replace `PreviewData` calls, add tests | 45m |
|
||
| 85 | Page editor integration — "Regenerate" button, auto-regenerate on settings change, customised vs auto label | 1.5h (depends on task #19) |
|