import shopify from "./shopify.js";
/**
* SEO core: we mirror each published group into a metafield on every member
* product. The theme block then renders crawlable <a href> sibling links in
* Liquid (server-side), so search engines see the cross-links between products.
*
* Namespace/key: linked_products.group (type: json)
* Value shape: { label, style, members: [{ handle, title, value, color, image, current }] }
*
* Note: `current` can't be known at write time (it depends on which product page
* is viewed), so the Liquid template determines "current" itself by comparing
* handles. We store the full sibling list on every member.
*/
const NAMESPACE = "linked_products";
const KEY = "group";
function clientFor(session) {
return new shopify.api.clients.Graphql({ session });
}
const DEFINITION_MUTATION = `
mutation createDef($definition: MetafieldDefinitionInput!) {
metafieldDefinitionCreate(definition: $definition) {
createdDefinition { id }
userErrors { field message code }
}
}`;
let definitionEnsured = false;
/**
* Ensure a metafield definition exists for linked_products.group with
* storefront/theme access. Without a definition, the value is NOT exposed to
* Liquid (product.metafields.linked_products.group is empty), which is why the
* theme block renders nothing. Idempotent: a "taken" error means it exists.
*/
export async function ensureDefinition(session) {
if (definitionEnsured) return;
const client = clientFor(session);
try {
const resp = await client.request(DEFINITION_MUTATION, {
variables: {
definition: {
name: "Linked products group",
namespace: NAMESPACE,
key: KEY,
description: "Sibling products linked for cross-linking and SEO.",
type: "json",
ownerType: "PRODUCT",
access: { storefront: "PUBLIC_READ" },
},
},
});
const errs = resp.data?.metafieldDefinitionCreate?.userErrors || [];
const onlyTaken = errs.every((e) => e.code === "TAKEN");
if (errs.length && !onlyTaken) {
console.error("metafieldDefinitionCreate errors:", JSON.stringify(errs));
}
} catch (e) {
console.error("ensureDefinition error:", e.message);
}
definitionEnsured = true;
}
function buildValue(group) {
return JSON.stringify({
label: group.label || "Color",
style: group.style || "color",
members: (group.members || []).map((m) => ({
productId: m.product_id,
handle: m.handle,
title: m.product_title,
value: m.swatch_value,
color: m.swatch_color,
image: m.image,
})),
});
}
const SET_MUTATION = `
mutation metafieldsSet($metafields: [MetafieldsSetInput!]!) {
metafieldsSet(metafields: $metafields) {
userErrors { field message }
}
}`;
// Plural metafieldsDelete is stable and takes identifiers in one call.
const DELETE_MUTATION = `
mutation metafieldsDelete($metafields: [MetafieldIdentifierInput!]!) {
metafieldsDelete(metafields: $metafields) {
deletedMetafields { ownerId namespace key }
userErrors { field message }
}
}`;
/** Write the group metafield to every member product. */
export async function syncGroupMetafields(session, group) {
await ensureDefinition(session);
const client = clientFor(session);
const value = buildValue(group);
// metafieldsSet accepts up to 25 per call; chunk to be safe.
const inputs = (group.members || []).map((m) => ({
ownerId: m.product_id,
namespace: NAMESPACE,
key: KEY,
type: "json",
value,
}));
for (let i = 0; i < inputs.length; i += 25) {
const chunk = inputs.slice(i, i + 25);
const resp = await client.request(SET_MUTATION, {
variables: { metafields: chunk },
});
const errs = resp.data?.metafieldsSet?.userErrors || [];
if (errs.length) {
console.error("metafieldsSet errors:", JSON.stringify(errs));
}
}
}
/** Remove the group metafield from a set of product GIDs. */
export async function clearGroupMetafields(session, productIds) {
const ids = productIds || [];
if (!ids.length) return;
const client = clientFor(session);
const identifiers = ids.map((ownerId) => ({
ownerId,
namespace: NAMESPACE,
key: KEY,
}));
// Delete in chunks of 25 (mutation limit).
for (let i = 0; i < identifiers.length; i += 25) {
const chunk = identifiers.slice(i, i + 25);
const resp = await client.request(DELETE_MUTATION, {
variables: { metafields: chunk },
});
const errs = resp.data?.metafieldsDelete?.userErrors || [];
if (errs.length) {
console.error("metafieldsDelete errors:", JSON.stringify(errs));
}
}
}