# Linked Products — Shopify app
Connect separate products that belong together (e.g. the same item sold as
"T-Shirt - Red" and "T-Shirt - Blue") so each product page shows links to the
others. **The sibling links render server-side as real `<a href>` links**, so
search engines crawl them — improving internal linking and SEO.
## How it works (SEO core)
```
Admin: create/auto-detect a group ──► DB (product_groups + group_members)
│ on PUBLISH
▼
write metafield linked_products.group (type: json)
to every member product (Admin API metafieldsSet)
│
▼
Storefront product page: the "Linked Products" theme block reads
product.metafields.linked_products.group in LIQUID and renders
<a href="/products/sibling-handle">…</a> ← crawlable, no JS needed
```
Draft groups write **no** metafield, so they never appear on the storefront
until published. Unpublishing or deleting clears the metafield from members.
## Files
### Backend (`web/`)
- **`db.js`** — `product_groups` (`status` = draft|published, `source` =
manual|auto) + `group_members`, with a small ALTER-TABLE migration.
- **`auto-group.js`** — detect groups by shared title prefix (`"Base - Value"`),
derive each member's label from the suffix, auto-colour known colour names.
- **`color-utils.js`** — colour name → hex (EN + DA), used by auto-grouping.
- **`metafields.js`** — create the metafield definition + write/clear the
`linked_products.group` metafield on member products (the SEO bridge).
- **`routes/admin.js`** — groups CRUD (syncs metafields on publish/edit/delete),
`/groups/auto/preview` + `/groups/auto` (detect → create as drafts),
`/groups/:id/resync`, `/groups/cleanup-orphans`, product search.
- **`index.js`** — auth, mandatory privacy webhooks, admin API, static serving.
### Admin UI (`web/frontend/`)
- **`pages/index.jsx`** — overview + **Auto-detect groups** (preview → create all
as drafts) + published/draft counts.
- **`pages/groups/`** — list (status badge, publish/unpublish, re-sync, delete,
clean-up), create, edit. Searchable multi-select product picker.
- **`components/`** — `GroupEditor`, `ProductMultiSelect`.
- **`utils/autoColor.js`** — client-side colour guess for the editor.
### Theme app extension (`extensions/variant-swatches/`)
- **`blocks/linked_products.liquid`** — the SEO block: renders sibling
`<a href>` links server-side from the metafield, and collapses its own empty
wrapper when a product has no group. **Add this block to product templates.**
- **`assets/linked.css`** — styling for the block.
## Setup
1. `npm install`
2. `shopify app dev` — re-grants scopes (`read_products,write_products`; the
write scope is needed to set metafields).
3. In the app: **Auto-detect groups** → review → **Create all as drafts**.
4. **Linked groups** → review a draft → **Publish** (writes the metafields).
5. Theme editor → add the **Linked Products** block to your product template.
## Verify the SEO links are real
On a published product page, **View Source** (not Inspect): you should see
literal `<a href="/products/…">` tags inside `.lpx__values` — present in the raw
HTML before any JavaScript runs.
## Notes
- Auto-detect separators: ` - `, ` – `, ` / `, ` | `, `: `, `, `. Single-product
bases are ignored; re-running skips already-grouped products.
- Style is chosen automatically: colour swatches when every value is a known
colour, otherwise buttons.
- Deleting a group clears its metafields; a `cleanup-orphans` safety net also
runs after deletes and is available as a manual "Clean up storefront" button.