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>
10 KiB
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_ratestable) - 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>:
[
%{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:
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 pagelib/berrypod_web/live/shop/content.ex— replace threePreviewData.*_content()calls withLegalPages.*_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) |