berrypod/docs/plans/legal-page-generator.md
jamey 0f1135256d add canonical URLs, robots.txt, and sitemap.xml
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>
2026-02-23 21:47:35 +00:00

10 KiB
Raw Blame History

Legal page generator

Status: Planned Tasks: #8385 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 27 business days
Printful 25 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

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