- gifting tab CTAs now point at our /collections pages (seed defaults + one-off PB migration rewriting shop.mozimo.in cta_urls, idempotent) - Hero/Footer/our-world/journal 'shop' links -> /chocolate; bespoke bulk-ordering -> /gifting#corporate-bulk - still on Shopify (deliberate, until Phase 2): PDP buy button, Header bag+account, footer policies
503 lines
15 KiB
JavaScript
503 lines
15 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* PocketBase bootstrap — idempotent.
|
|
*
|
|
* 1. ensures the editorial collections exist (gifting_tabs, collection_rows,
|
|
* gifting_page) with public read rules;
|
|
* 2. seeds each empty collection with the content currently shipped in the
|
|
* site bundle, so a fresh PocketBase looks exactly like the static site.
|
|
*
|
|
* Runs at container start (entrypoint) and can be re-run any time:
|
|
* node --env-file=.env scripts/pb-seed.mjs
|
|
*
|
|
* Env:
|
|
* PB_URL default http://127.0.0.1:8090
|
|
* PB_SUPERUSER_EMAIL required to seed
|
|
* PB_SUPERUSER_PASSWORD required to seed
|
|
*
|
|
* Never fatal: any failure logs a warning and exits 0 — the site falls back
|
|
* to its bundled defaults until PocketBase is ready.
|
|
*/
|
|
|
|
const PB_URL = (process.env.PB_URL ?? "http://127.0.0.1:8090").replace(
|
|
/\/$/,
|
|
"",
|
|
);
|
|
const EMAIL = process.env.PB_SUPERUSER_EMAIL;
|
|
const PASSWORD = process.env.PB_SUPERUSER_PASSWORD;
|
|
|
|
const IMAGE_MIME = [
|
|
"image/jpeg",
|
|
"image/png",
|
|
"image/webp",
|
|
"image/avif",
|
|
"image/gif",
|
|
];
|
|
|
|
/* ── defaults: mirror of the shipped site content ──────────── */
|
|
|
|
const GIFTING_TABS = [
|
|
{
|
|
name: "Signature",
|
|
note: "From ₹265",
|
|
title: "Small gestures, remembered.",
|
|
blurb:
|
|
"Thoughtful everyday gifting — a bar, a little box, a note of chocolate that says enough.",
|
|
panel: "products",
|
|
tile_url: "/products/mozi14.jpg",
|
|
product_handles: "mozimo-dark-chocolate-bar-85\n9-pralines-box\nbars-box",
|
|
collection_handle: "gift-collection",
|
|
cta_label: "View all Signature gifts",
|
|
cta_url: "/collections/gift-collection",
|
|
sort: 1,
|
|
visible: true,
|
|
},
|
|
{
|
|
name: "Best Sellers",
|
|
note: "From ₹825",
|
|
title: "The boxes they ask for by name.",
|
|
blurb:
|
|
"The spreads, dragees and bars our regulars return for — gifting that never misses.",
|
|
panel: "products",
|
|
tile_url: "/products/mozi16.jpg",
|
|
product_handles:
|
|
"pistachio-spread-60\nmozimo-almond-dragees-60\nkunafa-chocolate-bar\nspreads-box",
|
|
collection_handle: "best-sellers",
|
|
cta_label: "Shop Best Sellers",
|
|
cta_url: "/collections/best-sellers",
|
|
sort: 2,
|
|
visible: true,
|
|
},
|
|
{
|
|
name: "Exquisite",
|
|
note: "From ₹2,400",
|
|
title: "Celebrations, elevated.",
|
|
blurb:
|
|
"The Carnival boxes — layered, abundant and composed for the moments in between.",
|
|
panel: "products",
|
|
tile_url: "/products/mozi20.jpg",
|
|
product_handles:
|
|
"mozimo-carnival-celebration-s\nmozimo-carnival-celebration-m",
|
|
collection_handle: "gift-collection",
|
|
cta_label: "View the full collection",
|
|
cta_url: "/collections/gift-collection",
|
|
sort: 3,
|
|
visible: true,
|
|
},
|
|
{
|
|
name: "Premium",
|
|
note: "From ₹3,900",
|
|
title: "Generosity, composed.",
|
|
blurb:
|
|
"Our grandest hampers — the ones that arrive before you speak and say everything after.",
|
|
panel: "products",
|
|
tile_url: "/products/mozi-33.jpg",
|
|
product_handles: "mozimo-carnival-celebration-l\nmozimo-celestial-s",
|
|
collection_handle: "gift-collection",
|
|
cta_label: "View the full collection",
|
|
cta_url: "/collections/gift-collection",
|
|
sort: 4,
|
|
visible: true,
|
|
},
|
|
{
|
|
name: "Seasonal",
|
|
note: "Limited editions",
|
|
title: "Chocolate for the calendar.",
|
|
blurb:
|
|
"Diwali to Valentine's — editions composed for the season's light, here only while it lasts.",
|
|
panel: "seasonal",
|
|
tile_url: "/products/mozi-30.jpg",
|
|
collection_handle: "gift-collection",
|
|
cta_label: "Shop the Seasonal Edit",
|
|
cta_url: "/collections/gift-collection",
|
|
sort: 5,
|
|
visible: true,
|
|
},
|
|
{
|
|
name: "Corporate & Bulk",
|
|
note: "At scale",
|
|
title: "Gratitude at scale.",
|
|
blurb:
|
|
"Branded hampers, volume pricing and door-step delivery for teams, clients and occasions of every size.",
|
|
panel: "enquiry",
|
|
tile_url: "/products/mozi21.jpg",
|
|
cta_label: "Start a corporate enquiry",
|
|
cta_url: "/bespoke#enquiry",
|
|
sort: 6,
|
|
visible: true,
|
|
},
|
|
{
|
|
name: "Bespoke Gifting",
|
|
note: "Made to order",
|
|
title: "Composed around your moment.",
|
|
blurb:
|
|
"Custom selections, packaging and personalisation — designed with our chocolatiers from the first conversation.",
|
|
panel: "enquiry",
|
|
tile_url: "/products/mozi-17.jpg",
|
|
cta_label: "Begin a bespoke commission",
|
|
cta_url: "/bespoke#enquiry",
|
|
sort: 7,
|
|
visible: true,
|
|
},
|
|
];
|
|
|
|
const COLLECTION_ROWS = [
|
|
{
|
|
shopify_handle: "bars",
|
|
name_override: "Bars",
|
|
blurb: "Single-origin tablets, 45% to 85%",
|
|
carousel_urls:
|
|
"/products/01-dark85-60gm.jpg\n/products/02-dark60-60gm.jpg\n/products/img-5469.jpg",
|
|
sort: 1,
|
|
visible: true,
|
|
},
|
|
{
|
|
shopify_handle: "bark",
|
|
name_override: "Barks",
|
|
blurb: "Crackled slabs of nuts, fruit & crunch",
|
|
carousel_urls:
|
|
"/products/01-almondbark60.jpg\n/products/02-almondbark45.jpg\n/products/20-almondpralinebark.jpg",
|
|
sort: 2,
|
|
visible: true,
|
|
},
|
|
{
|
|
shopify_handle: "pralines",
|
|
name_override: "Pralines",
|
|
blurb: "Hand-piped ganache, boxed like jewels",
|
|
carousel_urls:
|
|
"/products/mozi-27.jpg\n/products/irw05205.jpg\n/products/14-mozimopralinepanetela-9c9a80d8-e4fc-4c4b-9c56-ca7f42b57847.jpg",
|
|
sort: 3,
|
|
visible: true,
|
|
},
|
|
{
|
|
shopify_handle: "spreads",
|
|
name_override: "Spreads",
|
|
blurb: "Stone-ground, silky, single origin",
|
|
carousel_urls:
|
|
"/products/02-pistachiospread60.jpg\n/products/03-hazelnutspread45.jpg\n/products/mozi-23.jpg",
|
|
sort: 4,
|
|
visible: true,
|
|
},
|
|
{
|
|
shopify_handle: "dragees",
|
|
name_override: "Dragees",
|
|
blurb: "Roasted nuts under a thin chocolate shell",
|
|
carousel_urls:
|
|
"/products/04-almonddragees60.jpg\n/products/02-hazelnutdragees45.jpg\n/products/03-pistachiodragees.jpg",
|
|
sort: 5,
|
|
visible: true,
|
|
},
|
|
{
|
|
shopify_handle: "tuilles",
|
|
name_override: "Tuilles & Rocks",
|
|
blurb: "Lace-thin wafers, orangettes & slivers",
|
|
carousel_urls:
|
|
"/products/01-milk45-tuilles-100gm.jpg\n/products/02-whitechocolatetuilles-100gm.jpg\n/products/01-almondsliversrocks-100gm.jpg",
|
|
sort: 6,
|
|
visible: true,
|
|
},
|
|
{
|
|
shopify_handle: "gift-collection",
|
|
name_override: "Gifting",
|
|
blurb: "Hampers composed for every celebration",
|
|
carousel_urls:
|
|
"/products/mozi20.jpg\n/products/mozi-33.jpg\n/products/mozi4.jpg",
|
|
sort: 7,
|
|
visible: true,
|
|
},
|
|
];
|
|
|
|
const GIFTING_PAGE = [
|
|
{
|
|
hero_eyebrow: "Gifts",
|
|
hero_title: "The art of",
|
|
hero_script: "giving",
|
|
hero_lead:
|
|
"Some gifts are opened. Others are remembered. Every Mozimo hamper is composed by hand — chocolate first, packaging worthy of it second.",
|
|
sort: 0,
|
|
},
|
|
];
|
|
|
|
/* ── collection schemas ────────────────────────────────────── */
|
|
|
|
const SCHEMAS = {
|
|
gifting_tabs: {
|
|
name: "gifting_tabs",
|
|
type: "base",
|
|
listRule: "",
|
|
viewRule: "",
|
|
createRule: null,
|
|
updateRule: null,
|
|
deleteRule: null,
|
|
fields: [
|
|
{ name: "name", type: "text", required: true },
|
|
{ name: "note", type: "text" },
|
|
{ name: "title", type: "text" },
|
|
{ name: "blurb", type: "text" },
|
|
{
|
|
name: "panel",
|
|
type: "select",
|
|
values: ["products", "seasonal", "enquiry"],
|
|
maxSelect: 1,
|
|
},
|
|
{ name: "tile", type: "file", maxSelect: 1, mimeTypes: IMAGE_MIME },
|
|
{ name: "tile_url", type: "text" },
|
|
{ name: "product_handles", type: "text" },
|
|
{ name: "collection_handle", type: "text" },
|
|
{ name: "parent_slug", type: "text" },
|
|
{ name: "cta_label", type: "text" },
|
|
{ name: "cta_url", type: "text" },
|
|
{ name: "sort", type: "number" },
|
|
{ name: "visible", type: "bool" },
|
|
],
|
|
},
|
|
collection_rows: {
|
|
name: "collection_rows",
|
|
type: "base",
|
|
listRule: "",
|
|
viewRule: "",
|
|
createRule: null,
|
|
updateRule: null,
|
|
deleteRule: null,
|
|
fields: [
|
|
{ name: "shopify_handle", type: "text", required: true },
|
|
{ name: "name_override", type: "text" },
|
|
{ name: "blurb", type: "text" },
|
|
{ name: "carousel", type: "file", maxSelect: 3, mimeTypes: IMAGE_MIME },
|
|
{ name: "carousel_urls", type: "text" },
|
|
{ name: "sort", type: "number" },
|
|
{ name: "visible", type: "bool" },
|
|
],
|
|
},
|
|
site_settings: {
|
|
name: "site_settings",
|
|
type: "base",
|
|
listRule: "",
|
|
viewRule: "",
|
|
createRule: null,
|
|
updateRule: null,
|
|
deleteRule: null,
|
|
fields: [
|
|
{ name: "key", type: "text", required: true },
|
|
{ name: "value", type: "text" },
|
|
{ name: "sort", type: "number" },
|
|
],
|
|
},
|
|
gifting_page: {
|
|
name: "gifting_page",
|
|
type: "base",
|
|
listRule: "",
|
|
viewRule: "",
|
|
createRule: null,
|
|
updateRule: null,
|
|
deleteRule: null,
|
|
fields: [
|
|
{ name: "hero_eyebrow", type: "text" },
|
|
{ name: "hero_title", type: "text" },
|
|
{ name: "hero_script", type: "text" },
|
|
{ name: "hero_lead", type: "text" },
|
|
{ name: "sort", type: "number" },
|
|
],
|
|
},
|
|
};
|
|
|
|
const SITE_SETTINGS = [
|
|
{
|
|
key: "announcement",
|
|
value:
|
|
"Complimentary delivery in Chandigarh Tricity on orders above ₹1,500 · Intercity shipped at actuals",
|
|
sort: 0,
|
|
},
|
|
];
|
|
|
|
const CORPORATE_CHILDREN = [
|
|
[
|
|
"Welcome Gifts",
|
|
"corporate-welcome-gifts",
|
|
"Day-one boxes that say the onboarding was personal.",
|
|
0,
|
|
],
|
|
[
|
|
"Joining Anniversaries",
|
|
"corporate-joining-anniversaries",
|
|
"Milestone chocolate for the day they said yes to you.",
|
|
1,
|
|
],
|
|
[
|
|
"Farewell & Leaving Gifts",
|
|
"corporate-farewell-gifts",
|
|
"A graceful send-off, wrapped properly.",
|
|
2,
|
|
],
|
|
[
|
|
"5-Year Milestones",
|
|
"corporate-5-year-milestones",
|
|
"Half a decade deserves more than a certificate.",
|
|
3,
|
|
],
|
|
[
|
|
"10-Year Milestones",
|
|
"corporate-10-year-milestones",
|
|
"A decade of craft, honoured in kind.",
|
|
4,
|
|
],
|
|
[
|
|
"CXO & VIP Gifting",
|
|
"corporate-cxo-vip-gifting",
|
|
"Executive gifting composed to the person, not the policy.",
|
|
5,
|
|
],
|
|
].map(([name, handle, blurb, sort]) => ({
|
|
name,
|
|
note: "",
|
|
title: name,
|
|
blurb,
|
|
panel: "products",
|
|
tile_url: "",
|
|
product_handles: "",
|
|
collection_handle: handle,
|
|
parent_slug: "corporate-bulk",
|
|
cta_label: "Enquire",
|
|
cta_url: "/bespoke#enquiry",
|
|
sort,
|
|
visible: true,
|
|
}));
|
|
|
|
const SEEDS = {
|
|
gifting_tabs: [...GIFTING_TABS, ...CORPORATE_CHILDREN],
|
|
collection_rows: COLLECTION_ROWS,
|
|
gifting_page: GIFTING_PAGE,
|
|
site_settings: SITE_SETTINGS,
|
|
};
|
|
|
|
/* ── helpers ───────────────────────────────────────────────── */
|
|
|
|
const api = async (path, options = {}) => {
|
|
const res = await fetch(`${PB_URL}${path}`, {
|
|
...options,
|
|
headers: { "Content-Type": "application/json", ...(options.headers ?? {}) },
|
|
signal: AbortSignal.timeout(10_000),
|
|
});
|
|
const body = await res.json().catch(() => ({}));
|
|
return { status: res.status, ok: res.ok, body };
|
|
};
|
|
|
|
async function main() {
|
|
if (!EMAIL || !PASSWORD) {
|
|
console.log(
|
|
"[pb-seed] PB_SUPERUSER_EMAIL/PASSWORD not set — skipping seed",
|
|
);
|
|
return;
|
|
}
|
|
|
|
const auth = await api("/api/collections/_superusers/auth-with-password", {
|
|
method: "POST",
|
|
body: JSON.stringify({ identity: EMAIL, password: PASSWORD }),
|
|
});
|
|
if (!auth.ok) {
|
|
console.warn(
|
|
`[pb-seed] superuser auth failed (${auth.status}) — skipping seed`,
|
|
);
|
|
return;
|
|
}
|
|
const token = auth.body.token;
|
|
const authed = (path, options = {}) =>
|
|
api(path, {
|
|
...options,
|
|
headers: { Authorization: token, ...(options.headers ?? {}) },
|
|
});
|
|
|
|
for (const [name, schema] of Object.entries(SCHEMAS)) {
|
|
const created = await authed("/api/collections", {
|
|
method: "POST",
|
|
body: JSON.stringify(schema),
|
|
});
|
|
if (created.ok) {
|
|
console.log(`[pb-seed] created collection "${name}"`);
|
|
} else if (created.status === 400) {
|
|
// exists — add any fields the schema gained, keep public read rules
|
|
const cur = await authed(`/api/collections/${name}`);
|
|
if (cur.ok) {
|
|
const curFields = cur.body.fields ?? [];
|
|
const have = new Set(curFields.map((f) => f.name));
|
|
const missing = (schema.fields ?? []).filter((f) => !have.has(f.name));
|
|
const body = missing.length
|
|
? { fields: [...curFields, ...missing], listRule: "", viewRule: "" }
|
|
: { listRule: "", viewRule: "" };
|
|
const patched = await authed(`/api/collections/${name}`, {
|
|
method: "PATCH",
|
|
body: JSON.stringify(body),
|
|
});
|
|
if (!patched.ok && missing.length) {
|
|
console.warn(
|
|
`[pb-seed] field merge for "${name}" failed:`,
|
|
patched.status,
|
|
JSON.stringify(patched.body).slice(0, 160),
|
|
);
|
|
}
|
|
}
|
|
} else {
|
|
console.warn(
|
|
`[pb-seed] collection "${name}":`,
|
|
created.status,
|
|
JSON.stringify(created.body).slice(0, 200),
|
|
);
|
|
continue;
|
|
}
|
|
|
|
// seed only when empty
|
|
const existing = await authed(`/api/collections/${name}/records?perPage=1`);
|
|
if (!existing.ok) {
|
|
console.warn(`[pb-seed] cannot read "${name}" (${existing.status})`);
|
|
continue;
|
|
}
|
|
if (existing.body.totalItems > 0) {
|
|
console.log(
|
|
`[pb-seed] "${name}" already has ${existing.body.totalItems} record(s) — leaving as is`,
|
|
);
|
|
// one-off migration: gifting tab CTAs must keep users on mozimo.in,
|
|
// never bounce them out to the shopify domain
|
|
if (name === "gifting_tabs") {
|
|
const all = await authed(`/api/collections/${name}/records?perPage=100`);
|
|
for (const rec of all.body.items ?? []) {
|
|
const url = rec.cta_url ?? "";
|
|
if (url.startsWith("https://shop.mozimo.in/")) {
|
|
const internal = url.replace("https://shop.mozimo.in", "");
|
|
const patched = await authed(`/api/collections/${name}/records/${rec.id}`, {
|
|
method: "PATCH",
|
|
body: JSON.stringify({ cta_url: internal }),
|
|
});
|
|
if (patched.ok) {
|
|
console.log(`[pb-seed] migrated cta_url for "${rec.name}" -> ${internal}`);
|
|
} else {
|
|
console.warn(`[pb-seed] cta migration failed for "${rec.name}":`, patched.status);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
continue;
|
|
}
|
|
for (const record of SEEDS[name]) {
|
|
const inserted = await authed(`/api/collections/${name}/records`, {
|
|
method: "POST",
|
|
body: JSON.stringify(record),
|
|
});
|
|
if (!inserted.ok) {
|
|
console.warn(
|
|
`[pb-seed] seed record in "${name}":`,
|
|
inserted.status,
|
|
JSON.stringify(inserted.body).slice(0, 200),
|
|
);
|
|
}
|
|
}
|
|
console.log(
|
|
`[pb-seed] seeded ${SEEDS[name].length} record(s) into "${name}"`,
|
|
);
|
|
}
|
|
console.log("[pb-seed] done");
|
|
}
|
|
|
|
main().catch((error) => {
|
|
console.warn("[pb-seed] failed:", error?.message ?? error);
|
|
});
|