Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion apps/docs/.oxlintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@
"correctness": "warn",
"suspicious": "warn"
},
"ignorePatterns": [".next/**", "dist/**", "build/**", ".claude/**"],
"ignorePatterns": [".next/**", "dist/**", "build/**", ".claude/**", "components/ai-elements/**"],
"plugins": ["typescript", "react", "nextjs"],
"rules": {
"no-underscore-dangle": ["warn", { "allow": ["__dirname", "__registerFileInput"] }],
"no-unused-vars": "off",
"react/react-in-jsx-scope": "off"
},
Expand Down
1 change: 1 addition & 0 deletions apps/docs/content/docs/anatomy/_meta.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"navigation",
"footer",
"aeo-geo",
"sitemap",
"webhooks"
]
}
14 changes: 7 additions & 7 deletions apps/docs/content/docs/anatomy/aeo-geo.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,13 @@ The template ships with several built-in surfaces that contribute to AEO/GEO. Th

## Surfaces in the template

| Surface | What it does | Key files |
| -------------------------------- | ---------------------------------------------------------------------- | -------------------------------------------------------------- |
| **Content negotiation** | Serves clean markdown to AI clients via `Accept: text/markdown` | `next.config.ts`, `app/md/**`, `lib/markdown/**` |
| **Schema.org JSON-LD** | Embeds structured `Product`, `BreadcrumbList`, and `Organization` data | `components/product-detail/schema.tsx`, `components/schema/**` |
| **Sitemap** | Enumerates products, collections, and pages for crawlers | `app/sitemap.ts`, `lib/shopify/operations/sitemap.ts` |
| **Robots** | Declares crawl policy, including allowances for known AI user agents | `app/robots.ts` |
| **OpenGraph & Twitter metadata** | Provides title, description, and image previews per route | `lib/seo.ts`, route-level `generateMetadata` |
| Surface | What it does | Key files |
| -------------------------------- | ------------------------------------------------------------------------------------ | -------------------------------------------------------------- |
| **Content negotiation** | Serves clean markdown to AI clients via `Accept: text/markdown` | `next.config.ts`, `app/md/**`, `lib/markdown/**` |
| **Schema.org JSON-LD** | Embeds structured `Product`, `BreadcrumbList`, and `Organization` data | `components/product-detail/schema.tsx`, `components/schema/**` |
| **Sitemap** | [Sitemap index + paged children](/docs/anatomy/sitemap) for products and collections | `app/sitemap.xml/route.ts`, `app/sitemap/[shard]/route.ts` |
| **Robots** | Declares crawl policy and blocks faceted (sort/filter) collection URLs | `app/robots.ts` |
| **OpenGraph & Twitter metadata** | Provides title, description, and image previews per route | `lib/seo.ts`, route-level `generateMetadata` |

The rest of this page goes deep on content negotiation. The other surfaces are documented inline in the linked files.

Expand Down
60 changes: 43 additions & 17 deletions apps/docs/content/docs/anatomy/authentication.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,13 @@ Generate the auth secret with `openssl rand -base64 32`. The client ID and secre

### Shopify Admin setup

1. Go to **Shopify Admin → Settings → Customer accounts**
2. Enable **Customer Account API**
3. Create a **Customer Account API client** (under "API clients")
4. Set the redirect URI to `{YOUR_DOMAIN}/api/auth/callback/shopify`
5. Copy the client ID and client secret to your environment variables
6. Ensure the store domain matches `SHOPIFY_STORE_DOMAIN`
1. Enable customer accounts: **Shopify Admin → Settings → Customer accounts → Edit**, choose **Customer accounts**, and Save
2. Install the **Headless** sales channel from the Shopify App Store — Customer Account API credentials live there, not under Settings
3. Go to **Sales channels → Headless → (your storefront) → Customer Account API**
4. Set the client type to **Confidential** — the template authenticates with a client secret, so a public (PKCE-only) client will not work. The toggle is at the top of the Customer Account API settings
5. Set the redirect URI to `{YOUR_DOMAIN}/api/auth/oauth2/callback/shopify`
6. Copy the client ID and client secret to `SHOPIFY_CUSTOMER_CLIENT_ID` and `SHOPIFY_CUSTOMER_CLIENT_SECRET`
7. Ensure the store domain matches `SHOPIFY_STORE_DOMAIN`

See [Environment Variables](/docs/reference/env-vars) for the full variable reference.

Expand Down Expand Up @@ -59,14 +60,14 @@ This means auth infrastructure has zero runtime cost when disabled.

Authentication uses Shopify-native URL paths:

| Route | Description |
| ---------------------- | --------------------------------------------------------------------- |
| `/account/login` | Auto-redirects to Shopify OIDC. Not indexed by search engines. |
| `/account` | Redirects to `/account/profile`. |
| `/account/profile` | Displays customer name and email. |
| `/account/orders` | Order history (scaffold — wire with Customer Account API operations). |
| `/account/orders/[id]` | Order detail (scaffold). |
| `/account/addresses` | Address book (scaffold). |
| Route | Description |
| ---------------------- | --------------------------------------------------------------------------- |
| `/account/login` | Auto-redirects to Shopify OIDC. Not indexed by search engines. |
| `/account` | Redirects to `/account/profile`. |
| `/account/profile` | View and edit the customer's name; email shown read-only. |
| `/account/orders` | Paginated order history (cursor-based, newest first). |
| `/account/orders/[id]` | Order detail: line items, totals, shipping address, and a status-page link. |
| `/account/addresses` | Address book with create, edit, delete, and default selection. |

The account pages use a `(authenticated)` route group so the auth-gated layout applies to protected pages without blocking `/account/login`.

Expand All @@ -75,10 +76,18 @@ The account pages use a `(authenticated)` route group so the auth-gated layout a
### Session flow

1. Customer visits `/account/login` → auto-redirected to Shopify OIDC
2. After Shopify consent → redirected to `/api/auth/callback/shopify`
2. After Shopify consent → redirected to `/api/auth/oauth2/callback/shopify`
3. better-auth exchanges the code for tokens, decodes the ID token, and creates a session
4. Session stored in an `httpOnly` cookie with PKCE verification

### Logout

Logout is **local-only**. `signOut()` (`lib/auth/client.ts`) calls better-auth's sign-out endpoint to clear the session cookie, then redirects to `/`. The template does **not** call Shopify's OIDC `end_session_endpoint`, so the customer's session at Shopify's identity provider is not terminated — better-auth's `genericOAuth` plugin has no RP-initiated logout support to do this automatically.

The practical consequence: the storefront session ends, but a later **Sign in** can silently re-authenticate via SSO with no credential prompt. This is fine for most storefronts but surprising on a shared device. It also means the **Logout URI** configured in the Customer Account API settings is unused.

To fully end the Shopify session, implement RP-initiated logout: redirect the customer to the provider's `end_session_endpoint` (from the OIDC discovery document) with `id_token_hint` and a `post_logout_redirect_uri`. That redirect URI must be registered exactly in the Customer Account API **Logout URI** setting — Shopify does not support wildcard logout URIs, so each one (including any preview domains) must be added explicitly.

### Nav account icon

The nav uses a fixed-size container (`size-5`) with the fallback icon rendered inline and the async `NavAccount` component positioned absolutely on top via Suspense. This ensures the icon space is always reserved and there is no layout shift when the Suspense boundary resolves.
Expand Down Expand Up @@ -111,6 +120,23 @@ function AccountMenu() {
}
```

### Account data

The account pages read and write customer data through the Shopify Customer Account API — a separate GraphQL endpoint and schema from the Storefront API.

| Module | Purpose |
| ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `lib/shopify/fetch.ts` | `customerAccountFetch()` — POSTs to `https://shopify.com/{shopId}/account/customer/api/{version}/graphql` (the `shopId` is derived from the OIDC discovery document's `issuer` and cached) with the customer's access token as the raw `Authorization` header (no `Bearer` prefix). The endpoint is **not** on the storefront domain. |
| `lib/shopify/operations/customer.ts` | Queries (profile, orders, order detail, addresses) and mutations (address create/update/delete, profile update). Each call resolves the token via `requireSession()`. |
| `lib/shopify/transforms/customer.ts` | Maps Customer Account API responses to the provider-agnostic domain types in `lib/types.ts`. |
| `lib/customer/action.ts` | `"use server"` actions for the address and profile forms. They validate input, surface Shopify `userErrors`, and `revalidatePath` on success. |

Order and profile pages are read-only server components wrapped in Suspense. Addresses and profile editing use client forms that call the server actions. Order requests are per-customer `POST`s, so responses are never shared across customers.

Status values (`fulfillmentStatus`, `financialStatus`) are raw Shopify enums (e.g. `FULFILLED`, `PAID`); they're humanized at the display layer rather than stored in locale catalogs.

> The address form collects ISO codes for country (`territoryCode`) and region (`zoneCode`) as plain text inputs. Storefronts that want country/region dropdowns can layer a picker on top — the underlying `CustomerAddressInput` is unchanged.

## Guardrails

- Never expose access tokens to the client — `getSession()` and `requireSession()` are server-only
Expand All @@ -120,6 +146,6 @@ function AccountMenu() {
- PKCE is enabled for the OAuth flow — never disable it
- `isAuthEnabled` must read a `NEXT_PUBLIC_` variable — server-only env vars cause hydration mismatches with cache components. Don't replace it with an inline `process.env.BETTER_AUTH_SECRET` check

## Next steps
## Extending

The account pages are scaffolds. To populate them with real data, create Customer Account API operations in `lib/shopify/operations/customer.ts` for fetching profile, orders, and addresses using the access token from `requireSession()`.
To add more account data — store credit, subscriptions, draft orders, or richer order fields — add an operation in `lib/shopify/operations/customer.ts`, a transform in `lib/shopify/transforms/customer.ts`, and a matching domain type in `lib/types.ts`. Validate any new fields against the live Customer Account API schema before adding them. Mutations should run through a `"use server"` action in `lib/customer/action.ts` so input validation and `revalidatePath` stay in one place.
41 changes: 41 additions & 0 deletions apps/docs/content/docs/anatomy/sitemap.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
---
title: Sitemap
description: A sitemap index plus paged child sitemaps backed by Shopify's Storefront sitemap query — the same model Hydrogen uses.
type: guide
---

The storefront exposes a sitemap index at `/sitemap.xml` and paged children at `/sitemap/{shard}.xml`. Each child holds up to 250 entries — Shopify's Storefront `sitemap(type:)` query computes the pagination, so the storefront just iterates.

`robots.ts` points crawlers at `/sitemap.xml`; that URL alone is enough to discover every product and collection.

## URLs

| URL | Contents |
| ------------------------------ | --------------------------------------- |
| `/sitemap.xml` | Sitemap index listing every child shard |
| `/sitemap/static.xml` | Home page |
| `/sitemap/products-{n}.xml` | Up to 250 products per shard |
| `/sitemap/collections-{n}.xml` | Up to 250 collections per shard |

`{n}` is 1-indexed and runs to the `pagesCount` Shopify returns for each type.

## Cache behavior

Both the page count and each page of resources are cached with `cacheLife("max")` and tagged `products` or `collections`. The webhook handler at [`/api/webhooks/shopify`](/docs/anatomy/webhooks) invalidates those tags on product/collection mutation, so the sitemap stays fresh without scheduled regeneration.

## Adding more resource types

The template scopes sitemaps to products and collections. Shopify's `SitemapType` enum also covers `PAGE`, `BLOG`, `ARTICLE`, and `METAOBJECT`. Both routes name those two types explicitly, so adding one touches three places:

1. **`lib/shopify/operations/sitemap.ts`** — extend the `ShopifySitemapType` union.
2. **`app/sitemap.xml/route.ts`** — fetch the new type's `getShopifySitemapPagesCount(...)` and spread its `{type}-{n}` ids into the index alongside the product and collection shards. The index lists only what you add here — it does not discover new types on its own.
3. **`app/sitemap/[shard]/route.ts`** — widen the shard regex and the type mapping so the new `{type}-{n}` segments resolve, then map each item to its URL path.

## Key files

| File | Purpose |
| ----------------------------------- | ------------------------------------------------------------ |
| `app/sitemap.xml/route.ts` | Sitemap index — lists every shard |
| `app/sitemap/[shard]/route.ts` | Child sitemaps — `static`, `products-{n}`, `collections-{n}` |
| `lib/shopify/operations/sitemap.ts` | `getShopifySitemapPagesCount`, `getShopifySitemapPage` |
| `app/robots.ts` | Declares the sitemap index as the crawler entry point |
16 changes: 8 additions & 8 deletions apps/docs/content/docs/anatomy/webhooks.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,11 @@ Product topics also try to derive a numeric product tag from `admin_graphql_api_

Metaobject topics inspect the payload's `type` field to invalidate narrower CMS tags. The exact mapping follows the conventions used by [Shopify CMS](/docs/skills/enable-shopify-cms):

| Metaobject `type` | Additional tags |
| ---------------------------- | -------------------------------- |
| `cms_page` | `cms:pages`, `cms:page:{slug}` |
| `cms_homepage` | `cms:homepage` |
| `cms_section`, `cms_hero` | `cms:pages`, `cms:homepage` |
| Metaobject `type` | Additional tags |
| ------------------------- | ------------------------------ |
| `cms_page` | `cms:pages`, `cms:page:{slug}` |
| `cms_homepage` | `cms:homepage` |
| `cms_section`, `cms_hero` | `cms:pages`, `cms:homepage` |

All metaobject topics also invalidate the broad `cms:all` tag as a safety net for unrecognized types.

Expand Down Expand Up @@ -70,9 +70,9 @@ You can register only the topics you care about. Skipping `inventory_levels/*`,

## Environment variables

| Variable | When to set |
| ------------------------- | ---------------------------------------------------------------------------------- |
| `SHOPIFY_WEBHOOK_SECRET` | Required in any environment where Shopify posts to `/api/webhooks/shopify`. Without it, signature verification is skipped — fine for local testing, unsafe for production. |
| Variable | When to set |
| ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `SHOPIFY_WEBHOOK_SECRET` | Required in any environment where Shopify posts to `/api/webhooks/shopify`. Without it, signature verification is skipped — fine for local testing, unsafe for production. |

See [Environment Variables](/docs/reference/env-vars) for the full reference.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,10 +57,10 @@ The owning tool can overwrite the content between `BEGIN` and `END` on upgrade w

The template ships with two blocks:

| Block | Owner | What it contains |
| -------------------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `nextjs-agent-rules` | Next.js | Reminds agents this Next.js version has breaking changes and to read bundled docs before writing code. Refreshed on Next.js upgrade. |
| `vercel-shop-style` | Vercel Shop | Code style conventions: alphabetized exports and keys, no barrel files, push `"use client"` to leaves, naming rules, Tailwind patterns. Remove the block if your team prefers different conventions. |
| Block | Owner | What it contains |
| -------------------- | ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `nextjs-agent-rules` | Next.js | Reminds agents this Next.js version has breaking changes and to read bundled docs before writing code. Refreshed on Next.js upgrade. |
| `vercel-shop-style` | Vercel Shop | Code style conventions: alphabetized exports and keys, no barrel files, push `"use client"` to leaves, naming rules, Tailwind patterns. Remove the block if your team prefers different conventions. |

Everything outside these markers is yours. Add team conventions or domain constraints anywhere else and no upgrade will overwrite them.

Expand Down
Loading