# StoreSites

Store-owned public websites — the landing page a shop points its own domain at.

## Shape

```
stores.slug                 the public address:  {STOREFRONT_BASE_URL}/s/{slug}
store_sites                 one per store: theme, hours, contact, SEO, publish flag
store_site_pages            ordered pages; the landing page is is_home = true
store_domains               custom hostnames, TXT-verified
```

A page's content is an ordered JSON array of **blocks**:

```json
{ "id": "…", "type": "hero", "data": { "headline": "…", "cta_href": "#products" } }
```

Array order is render order. `type` is `App\Support\Enums\StoreSiteBlockType`;
each type has its own `data` rule set in `Services\BlockValidator`, which is
why blocks are not validated by a FormRequest. A block whose type the renderer
doesn't know is skipped client-side rather than breaking the page.

`products` blocks store only a query (`source`, `category_slug`, `item_ids`,
`limit`) — the web tier resolves it against the existing public item search, so
listings are never duplicated into site content.

## Endpoints

Public (unauthenticated, `throttle:storefront-public`):

| Method | Path | Purpose |
| --- | --- | --- |
| GET | `/v1/storefronts/resolve?hostname=` | Host header → store slug, for web middleware |
| GET | `/v1/storefronts/{slug}` | Theme, nav, hours, contact, SEO |
| GET | `/v1/storefronts/{slug}/pages/{pageSlug}` | One page's blocks (+ shell in `meta`) |

Public reads 404 unless the site is published **and** the store is verified and
not suspended. The store owner (and admins) see drafts, so the editor's preview
is the same code path as production.

Owner (`auth:sanctum` + `store.owner`):

| Method | Path |
| --- | --- |
| GET / PUT | `/v1/stores/{store}/site` |
| PUT | `/v1/stores/{store}/site/slug` |
| GET / POST | `/v1/stores/{store}/site/pages` |
| PUT | `/v1/stores/{store}/site/pages/reorder` |
| PUT / DELETE | `/v1/stores/{store}/site/pages/{page}` |
| GET / POST | `/v1/stores/{store}/domains` |
| POST | `/v1/stores/{store}/domains/{domain}/verify` |
| PUT | `/v1/stores/{store}/domains/{domain}/primary` |
| DELETE | `/v1/stores/{store}/domains/{domain}` |

`GET /site` provisions on first call (`StoreSiteProvisioner`) — the seller opens
the editor onto a working landing page plus About Us / Hours / Directions rather
than a blank canvas.

## Custom domains

1. Seller adds `shop.example.com`; the row is unverified and inert.
2. They create TXT `_alqove.shop.example.com` = `alqove-site-verification=<token>`.
3. `POST …/verify` re-reads DNS through `Support\TxtRecordLookup` (bound to
   `DnsTxtRecordLookup`; feature tests swap in a fake) and stamps `verified_at`.
4. They point the hostname at `STOREFRONT_CNAME_TARGET`.
5. `GET /v1/storefronts/resolve` now answers for that host, and the web
   middleware rewrites `/*` → `/s/{slug}/*`.

Only verified rows resolve, so a claim alone never lets one store answer for a
hostname it doesn't control. **TLS is not issued here** — the edge platform
(Vercel/Cloudflare) owns certificate provisioning for the CNAME target.

`config/storefront.php` holds the base URL, reserved platform hostnames, the
CNAME target and the per-store domain cap.
