Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
4af9644
Add the collection route filters spec
hta218 Sep 21, 2026
d93f0af
Resolve catalog facets, filter and sort in one shared module
hta218 Sep 21, 2026
42f0f5e
Carry filter and sort through getCollectionProducts
hta218 Sep 21, 2026
6a81c48
Hand the resolved collection browse state to composed sections
hta218 Sep 21, 2026
bba8f98
Add the collection toolbar child element
hta218 Sep 21, 2026
c92a70c
Add the collection content child element
hta218 Sep 21, 2026
f792c19
Add the collection filters child element
hta218 Sep 21, 2026
c64d5cf
Add the collection product grid child element
hta218 Sep 21, 2026
847b58b
Compose the collection from a main collection section tree
hta218 Sep 21, 2026
300af7f
Resolve collection filter and sort in the route
hta218 Sep 21, 2026
7ec695c
Mark the collection route filters spec completed
hta218 Sep 21, 2026
878048a
Apply collection filter settings on mobile
hta218 Sep 21, 2026
b4fe12b
Trim collection browse state and cover composed filters
hta218 Sep 21, 2026
ed4b3e2
Render only authored collection elements
hta218 Sep 21, 2026
68f9957
Build both facet dimensions from one table
hta218 Sep 22, 2026
2b4343b
Let the view decide whether facet counts render
hta218 Sep 22, 2026
15a9f68
Collapse the product grid to a single component
hta218 Sep 22, 2026
ca12ab9
Drop the sort form's Suspense boundary
hta218 Sep 22, 2026
bb5c1cd
Follow the facet builder's narrowed options
hta218 Sep 22, 2026
1255773
Add the store-driven catalog spec
hta218 Sep 22, 2026
f498d55
Source every product field from the store
hta218 Sep 22, 2026
56fdf8b
Rebuild the deterministic catalog without the profile table
hta218 Sep 22, 2026
0b8fe28
Take collections as the store publishes them
hta218 Sep 22, 2026
c1159b8
Restate the catalog contract as rules over live data
hta218 Sep 22, 2026
e9fa550
Describe facets, sort keys and pages in store terms
hta218 Sep 22, 2026
2c15d03
Read a collection as its own query
hta218 Sep 22, 2026
2c96984
Add getCollectionPage to the storefront seam
hta218 Sep 22, 2026
03fa22c
Carry the store's own facets through the URL
hta218 Sep 22, 2026
74d8546
Render facets, order and paging from what the store returned
hta218 Sep 22, 2026
6a686e4
Put the collection route on native facets and cursor paging
hta218 Sep 22, 2026
f062020
Read the whole catalog a page at a time
hta218 Sep 22, 2026
017dbe2
Compose /shop as an ALL_PRODUCTS page
hta218 Sep 22, 2026
02c3a91
Cover the browse contract instead of a fixed facet list
hta218 Sep 22, 2026
8ad9196
Seed the collection and all-products templates
hta218 Sep 22, 2026
3f8ffad
Record the store-driven catalog rules and close the spec
hta218 Sep 22, 2026
c90e682
Allow collection images from the owned Shopify CDN tenant
hta218 Sep 27, 2026
816eb3c
Drop route-level section fallbacks on Shop and product pages
hta218 Sep 27, 2026
b799618
Render an empty Weaverse page as authored instead of as missing
hta218 Sep 27, 2026
25b5387
Drop the main-product default composition for childless sections
hta218 Sep 27, 2026
498d196
Render mp-- children only inside main-product and require a revalidat…
hta218 Sep 27, 2026
f9802f3
Map menus and the Shop panel from the store without static fallbacks
hta218 Sep 27, 2026
dd215fb
Require every Shopify executor so live mode never reads fixtures
hta218 Sep 27, 2026
ae20d9d
Carry only sort through header links into the shop
hta218 Sep 27, 2026
a2af484
Share Shopify path routing between menus and content
hta218 Sep 27, 2026
5120734
Read pages, articles and policies from the store without a profile table
hta218 Sep 27, 2026
d79e072
Drop the unused product filter and sort parameters
hta218 Sep 28, 2026
981752e
Share one sort table and one paged executor
hta218 Sep 28, 2026
b5dc74e
Clear filters through browseHref
hta218 Sep 28, 2026
ae8d51b
Share the catalog toolbar and grid between both browse trees
hta218 Sep 28, 2026
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
10 changes: 5 additions & 5 deletions .weaverse/specs/2026-09-18--main-product-children/plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,11 @@ main-product section shell: grid, gallery side, panel width;

## Safeguards

- A `main-product` with no children (the route fallback, or a template seeded
before this change) renders the default composition, so a product URL is
never without gallery, selection and add to cart.
- Every setting has a code default equal to its schema default, because the
route fallback renders without Weaverse.
- Every setting has a code default equal to its schema default.
- (2026-09-27) No default composition: the route no longer renders
`main-product` when the page omits it, and a childless `main-product`
renders only its shell. A Weaverse project always ships its default
templates, and an empty one renders empty.

## Files

Expand Down
37 changes: 37 additions & 0 deletions .weaverse/specs/2026-09-21--collection-route-filters/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Feature: Collection route filters

| Field | Value |
| ---------------- | --------------------------------------- |
| **Status** | completed |
| **Owner** | @hta218 |
| **Issue** | [#78](https://github.com/Weaverse/forward/issues/78) |
| **Branch** | `feat/collection-route-filters` |
| **Created** | 2026-09-21 |
| **Last Updated** | 2026-09-21 |

## Original Prompt

> Check the collection filters for me. Right now only `/shop` has filters; routes of the form `/shop/<collection-handle>` do not. Why?
>
> Compare it with Pilot's collection page.
>
> Go with option A, the Pilot model. I need an issue on Forward first — create it with `/create-task`.
>
> Good, now `/work` it. The goal is to finish; when done, commit in multiple commits, one commit per child element. Then push and open a PR.

## Follow-up

Superseded in the same branch by
[2026-09-22 store-driven catalog](../2026-09-22--store-driven-catalog/README.md):
the Activity and Category facets this spec introduced were theme constants that
existed in no Shopify store, and were replaced by the Storefront API's own
filter connection.

## Summary

`/shop/<collectionHandle>` handed all rendering to Weaverse and parsed no query
state, so nothing on a collection page held filter or sort. This replaces the
opaque `collection-grid` with a `main-collection` tree — toolbar, content,
filters, product grid — driven by URL query state the route resolves through
the storefront seam. Only Pilot's file organization is borrowed; the facet
model, components and styling are Forward's own.
92 changes: 92 additions & 0 deletions .weaverse/specs/2026-09-21--collection-route-filters/plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# Plan — Collection route filters

## Problem

`/shop` (`src/app/shop/page.tsx`) is a hand-written theme-owned page that parses
`category`, `activity` and `sort`, derives filter groups from the catalog, and
renders `FilterSidebar` + `ProductResults`.

`/shop/[collectionHandle]/page.tsx` renders nothing of its own: it forwards
`searchParams` to `loadWeaversePage()` only and returns `<WeaversePage>`. The
composed page is `collection-hero` + `system-manifest` + `collection-grid` +
`field-practice`, and `collection-grid` maps the whole product array. No query
state is parsed anywhere on the route, and `getCollectionProducts(handle)` has
no parameter to carry a filter through.

## Approach

Adopt Pilot's *organization* — a composable section tree whose filter state
lives in the URL and is resolved by the route — implemented against Forward's
normalized `Product` model. No Pilot source is translated.

```
collection-hero (unchanged — already the page header)
main-collection shell: layout
├─ mc--toolbar count, sort control
└─ mc--content two-column wrapper
├─ mc--filters desktop sidebar, mobile disclosure
└─ mc--product-grid grid, pagination, empty state
```

`collection-grid` is retired; `mc--product-grid` supersedes it and nothing in
the repository references it outside the two registries.
The Studio preset supplies the complete child tree. `main-collection` renders
only its configured children, with no implicit composition for an empty shell.

### Query contract

The collection route reuses `/shop`'s param names rather than inventing a
second convention: `?category=&activity=&sort=`, plus `?page=` for the grid.
One module owns parsing, facet derivation and href building for both routes.

### Data boundary

`getCollectionProducts(handle, filter?, sort?)` gains the same optional
parameters `listProducts` already has, and both adapters run the existing
`filterAndSortProducts` over normalized records, so live mode cannot drift from
static mode. Facets are derived from the collection's *unfiltered* products so
options never disappear mid-filter; counts are computed against the filter.
The `mc--filters` count setting applies to both desktop and mobile facets.

### Pagination

Page-number pagination held in the URL and applied by `mc--product-grid`, whose
`pageSize` is a merchant setting. Slicing is client-side over the collection's
product list, which is already fully loaded in memory by both adapters. Marked
with a `ponytail:` comment naming the ceiling.

## Files and folders touched

**New**

- `src/lib/storefront/catalog-facets.ts` — filter/sort parsing, facet
derivation, href building; shared by `/shop` and the collection route
- `src/sections/main-collection/index.tsx`, `schema.ts`
- `src/sections/main-collection/toolbar/index.tsx`, `schema.ts`
- `src/sections/main-collection/content/index.tsx`, `schema.ts`
- `src/sections/main-collection/filters/index.tsx`, `schema.ts`
- `src/sections/main-collection/product-grid/index.tsx`, `schema.ts`
- `.weaverse/specs/2026-09-21--collection-route-filters/`

**Changed**

- `src/lib/storefront/data-source.ts` — `getCollectionProducts` signature,
static implementation
- `src/lib/storefront/shopify/data-source.ts` — same signature, live path
- `src/lib/weaverse/data-context.tsx` — collection facets, filter, sort
- `src/lib/weaverse/components.ts`, `src/lib/weaverse/section-schemas.ts`
- `src/app/shop/[collectionHandle]/page.tsx` — parse query, resolve products
- `src/app/shop/page.tsx` — consume the shared facet module
- `src/components/filter-sidebar.tsx` — types move to the facet module
- `src/sections/product-results.tsx` — follow the type move
- `tests/storefront-data-source.test.ts`, `tests/dom/composed-sections.test.tsx`
- `AGENTS.md` — amend the theme-owned-grid clause

**Removed**

- `src/sections/collection-grid/`

## Verification

`bun run check` (typecheck, lint, format:check, test, check:graphql, build,
check:theme, check:routes), then `bun run smoke:routes`.
16 changes: 16 additions & 0 deletions .weaverse/specs/2026-09-21--collection-route-filters/work-logs.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Work logs

## 2026-09-21 — PR #79 review fixes

- Synced `origin/feat/collection-route-filters` before editing.
- Moved the mobile facet disclosure into `mc--filters` so its `showCounts`
setting controls both mobile and desktop. Updated the section settings copy
and DOM coverage.
- Removed the unused `collectionBrowse.total` field from the route, context,
and test fixtures.
- Verified with Bun 1.3.14: frozen install, typecheck, lint, format check,
tests, GraphQL check, build, theme and route checks, route smoke test, and
the aggregate `check` command.
- Removed the empty `main-collection` shell's implicit child composition after
review. Studio's preset supplies the toolbar, filters, and grid; the DOM
composition test now renders that authored tree explicitly.
30 changes: 30 additions & 0 deletions .weaverse/specs/2026-09-22--store-driven-catalog/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Feature: Store-driven catalog

| Field | Value |
| ---------------- | --------------------------------------- |
| **Status** | completed |
| **Owner** | @hta218 |
| **Issue** | [#80](https://github.com/Weaverse/forward/issues/80) |
| **Branch** | `feat/collection-route-filters` |
| **Created** | 2026-09-22 |
| **Last Updated** | 2026-09-27 |

## Original Prompt

> Next, the filters — where are the facets coming from right now?
>
> I connected Pilot to this store and it only has these two filters
> [Availability, Price]. So Forward's filters are completely hardcoded, right?
>
> Do it now, it belongs in this PR #79. Remove all the hardcoding — how else
> could it run on a different real store? It has to work the way Pilot does.

## Summary

Forward's catalog was a fixed set of nine theme-declared products: Shopify
supplied copy, price and images, while the ownership tag, the handle allowlist
and the per-handle presentation profiles decided everything else — including
the Activity and Category facets, which exist in no Shopify store. This makes
the catalog store-driven: facets come from the Storefront API's own filter
connection, and every field previously read from a profile is sourced from the
store or retired.
95 changes: 95 additions & 0 deletions .weaverse/specs/2026-09-22--store-driven-catalog/plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
# Plan — Store-driven catalog

## Evidence

A read-only probe of the connected store (Storefront API 2026-07) decided every
mapping below. Structure only was printed; no prices, tokens or bodies.

| Probe | Result |
| --- | --- |
| `collection.products.filters` | `filter.v.availability` (LIST), `filter.v.price` (PRICE_RANGE) — on every collection |
| `QueryRoot.products.filters` | empty — all-products has no facets, which is why Pilot's `/products` is sort-only |
| `productType` | `Outerwear`, `Packs`, `Footwear` |
| `tags` | 48 values; `alpine`/`trail`/`camp`/`travel` present, mixed with material and feature tags |
| option value swatches | none — 0 colors, 0 images |
| `forward.*` metafields | `highlights`, `materials`, `field_specs`, `care`, `colorway_media_map` only |
| `productRecommendations` | returns 8 handles |

## Field sourcing

| `Product` field | Was | Becomes |
| --- | --- | --- |
| facets | `catalog-presentation.ts` profiles | `collection.products.filters` |
| `category` | `profile.category` (3-literal union) | `productType`, typed `string` |
| `activities` | `profile.activities` | `tags` minus infrastructure tags; display only, never a filter |
| `relatedHandles` | `profile.relatedHandles` | `productRecommendations` |
| `subtitle` | `profile.subtitle` | first `forward.highlights` entry |
| `repair` | `profile.repair` | theme setting — brand policy, not per-product data |
| colorway swatch | `profile.colorways` hex | the colorway's own image; the store has no native swatches |

## Approach

Filter state keeps Pilot's contract: `?filter.<key>=<json>` collected into a
`ProductFilter[]` and passed to Shopify as a query variable, so narrowing is
server-side and the facet list is whatever the store exposes. Sort moves to
`ProductCollectionSortKeys`/`ProductSortKeys` with `reverse`. Pagination
becomes cursor-based.

The whole-catalog read is retired: a collection is its own query, so the cache
key includes handle, filters, sort and cursor rather than one blob.

`/shop` becomes sort-only and composed through the `ALL_PRODUCTS` page type,
matching Pilot. `mc--filters` renders whatever facets the response carried, so
a merchant enabling a filter in Search & Discovery gets it with no code change.

## Ordered slices

1. Break the profile dependency in `mapProduct`; widen `ProductCategory`. Done.
2. Re-author `fixtures/products.ts` as literal data; re-source
`collection-presentation.ts`. Done.
3. Per-collection query with `filters`, `sortKey`, `reverse`, cursors; new
mapper for the filter connection. Done.
4. Route + `mc--filters`/`mc--toolbar`/`mc--product-grid` onto native facets;
retire `catalog-facets.ts`'s invented dimensions. Done.
5. `/shop` to `ALL_PRODUCTS` composition, sort-only. Done.
6. Suites, scripts, seeds and `AGENTS.md`. Done.

## What the API decided

Two findings changed the shape of the work after the plan was written.

`QueryRoot.products` accepts no `filters` argument — only `first`, `after`,
`last`, `before`, `reverse`, `sortKey` and `query`. Faceted browsing is a
collection feature, so `/shop` is sort and paging only. This is why Pilot's
`/products` is sort-only too, and the theme does not offer controls that could
not be applied.

`TITLE` exists in both `ProductCollectionSortKeys` and `ProductSortKeys`, so
every sort option maps to a real key and none is applied after the fact.

## Field sourcing, as shipped

Colorway ids derive from the published Color values, so `?colorway=` changed
(`charcoal` became `charcoal-moss`). Old links still resolve to the product and
fall back to its first colorway. Swatches use Shopify's native option swatch
when the merchant set one, and the colorway's own image otherwise — this store
sets none. `repair` is empty until it becomes a theme setting; the store has no
source for it.

## Files and folders touched

**Removed**: `src/lib/storefront/catalog-presentation.ts`

**Changed**: `src/lib/storefront/shopify/{queries,mapper,client,cache-policy,data-source}.ts`,
`src/lib/storefront/{types,catalog-query,catalog-facets,collection-presentation,data-source}.ts`,
`src/lib/storefront/fixtures/products.ts`, `src/app/shop/page.tsx`,
`src/app/shop/[collectionHandle]/page.tsx`, `src/sections/main-collection/**`,
`src/lib/weaverse/{components,section-schemas,data-context}.tsx?`,
`src/lib/weaverse/settings/`, `AGENTS.md`

**New**: `src/sections/all-products/**`

**Suites**: `tests/shopify-catalog-adapter.test.ts`,
`tests/storefront-data-source.test.ts`, `tests/production-polish-home.test.ts`,
`tests/catalog-facets.test.ts`, `tests/dom/collection-browse.test.tsx`,
`tests/browser/home.pw.ts`, `scripts/verify-shopify.mts`
45 changes: 45 additions & 0 deletions .weaverse/specs/2026-09-22--store-driven-catalog/work-logs.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Work Logs

## 2026-09-27 — @hta218

- Removed every fallback that substituted content for what the store or the
Weaverse page did not provide:
- Routes no longer render their own copy of a section a template omits
(`/shop`, PDP); a missing Weaverse page is `notFound()`, an empty one
renders empty; a childless `main-product` renders only its shell, and an
`mp--*` child outside it renders nothing.
- Studio revalidation without a route context is an error, not a bare client.
- Shopify mode no longer falls back to static navigation, footer or
collection structure. Menus map whatever the merchant arranged
(`navigation-mapper.ts`): Shopify paths route onto theme routes, a missing
menu is empty, and off-store or unroutable links are left out.
- The header Shop panel is built from the merchant's Shop links, dressed with
each collection's own description, image and field code. The
`FIELD_INDEX_PRESENTATION` and `COLLECTION_PRESENTATION_PROFILES` tables
are gone; the static profiles now live only in `fixtures/collections.ts`.
- Collection images: the CDN allowlist is scoped to the store tenant, so
`collections/` images pass alongside `files/`.
- Files: `src/lib/weaverse/server.ts`, `src/app/{shop,products}/**`,
`src/sections/main-product/{index,context}.tsx`,
`src/app/api/weaverse/revalidate/route.ts`,
`src/lib/storefront/shopify/{navigation-mapper,data-source}.ts`,
`src/lib/storefront/{data-source,image-source}.ts`,
`src/lib/storefront/fixtures/collections.ts`,
`src/components/site-header/**`, `scripts/verify-shopify.mts`, and their tests.

## 2026-09-27 — @hta218 (content)

- Content is store-driven: the query reads every page (`pages`) and every
article across blogs (`articles`, newest first) instead of seven aliased
pages and one approved blog, and `content-presentation.ts` is gone.
- Article plate counts from the oldest, reading time comes from word count,
the image is the article's own, and location/coordinates are the optional
`forward.location` / `forward.coordinates` metafields.
- Pages carry no eyebrow or image (the page hero uses its settings);
unheaded paragraphs form one untitled section. Policies have no summary.
- An entry whose body the parser refuses is left out (its route 404s)
instead of failing the whole content read; the live store's
`data-sharing-opt-out` page is the case that proved it.
- Shopify path → theme route mapping is shared by menus and content links
(`shopify/theme-routes.ts`); `/collections/<any>` maps to `/shop/<any>`.
- Header links carry only `sort` into `/shop/**`: facets are per collection.
Loading