Setting the file. One moment. Seed Portfolio · Wix Vibe Headless · wix/skills · Skills DocsWix Blog
This file
- Number
- 20.74
- Position
- 74 of 126
- Type
- JavaScript
- Size
- 11 KB
- Lines
- 236
references/portfolio/seed/seed-portfolio.js
JavaScript·236 lines·11 KB
14// { title, description, collectionIds: [collections[0].id], details: [{ label, text }] },
15// ]);
16// // optional — import each image url to Wix Media first (portfolio binds by file id), then attach.
17// const files = await Promise.all(imageUrls.map((u) => seed.importImage(ctx, u))); // → [{ id, url }]
18// await seed.attachProjectCovers(ctx, projects.map((p,i) => ({ id:p.id, revision:p.revision, imageId:files[i].id, height:1024, width:1024 })));
19// await seed.createProjectItems(ctx, [{ projectId: projects[0].id, sortOrder: 1, title, imageId: files[0].id, height:1024, width:1024 }]);
20//
21// **NOT yet live-verified — transcribed from setup-portfolio.md.** If any call fails with a
22// shape the caller didn't expect, fall back to the wix-docs skill (search + read the live Wix
23// API reference) — never guess. Source recipe (authoritative):
24// wix-headless/references/inline-recipes/setup-portfolio.md.
25
26const API = "https://www.wixapis.com";
27const PORTFOLIO_APP_ID = "d90652a2-f5a1-4c7c-84c4-d4cdcc41f130"; // installPortfolioApp installs this before seeding
28
29async function req(ctx, path, { method = "POST", body } = {}) {
30 const res = await fetch(API + path, {
31 method,
32 headers: {
33 Authorization: `Bearer ${ctx.token}`,
34 "wix-site-id": ctx.siteId,
35 "Content-Type": "application/json",
36 },
37 body: body ? JSON.stringify(body) : undefined,
38 });
39 const json = await res.json().catch(() => ({}));
40 if (!res.ok) throw new Error(`${method} ${path} -> ${res.status}: ${JSON.stringify(json).slice(0, 400)}`);
41 return json;
42}
43
44// ---- exported operations ----
45
46// Read-only listing helpers.
47async function listProjects(ctx) {
48 const r = await req(ctx, "/portfolio/v1/projects", { method: "GET" });
49 return (r.projects ?? []).map((p) => ({ id: p.id, title: p.title }));
50}
51async function listCollections(ctx) {
52 const r = await req(ctx, "/portfolio/v1/collections", { method: "GET" });
53 return (r.collections ?? []).map((c) => ({ id: c.id, title: c.title }));
54}
55
56/**
57 * STEP 1 — Create the collections. MUST run before createProjects: a project's collectionIds
58 * are NOT validated, so projects need the real collection ids read back from here.
59 * @param collections [{ title, description?, hidden? }] (display name is `title`, not `name`;
60 * `slug` auto-generates from title when omitted; `hidden` defaults to false = shown, so omit
61 * it for a visible collection and send `hidden: true` only to hide.)
62 * No bulk-create — one call per collection; concurrent is safe (no 409 race) but sequential
63 * is just as correct and simplest.
64 * @returns [{ id, slug, revision }] (id feeds each project's collectionIds; revision feeds covers)
65 */
66async function createCollections(ctx, collections) {
67 const out = [];
68 for (const c of collections) {
69 const body = { collection: { title: c.title, description: c.description } };
70 if (c.hidden) body.collection.hidden = true; // omit otherwise — defaults to shown
71 const r = await req(ctx, "/portfolio/v1/collections", { body });
72 out.push({ id: r.collection?.id, slug: r.collection?.slug, revision: r.collection?.revision });
73 }
74 return out;
75}
76
77/**
78 * STEP 2 — Create the projects, each assigned to its collection(s) via collectionIds.
79 * @param projects [{ title, description?, collectionIds: [id], details?, hidden? }]
80 * collectionIds MUST hold real ids from createCollections — they are NOT validated, so a
81 * wrong/missing id is accepted silently and orphans the project (reachable only from the
82 * all-projects list). `details` is an optional [{ label, text }] array (Role, Year, Client…).
83 * `hidden` defaults to false = shown. No bulk-create — one call per project; concurrent safe.
84 * @returns [{ id, slug, revision }] (revision feeds attachProjectCovers)
85 */
86async function createProjects(ctx, projects) {
87 const out = [];
88 for (const p of projects) {
89 const project = {
90 title: p.title,
91 description: p.description,
92 collectionIds: p.collectionIds ?? [],
93 };
94 if (p.details) project.details = p.details;
95 if (p.hidden) project.hidden = true; // omit otherwise — defaults to shown
96 const r = await req(ctx, "/portfolio/v1/projects", { body: { project } });
97 out.push({ id: r.project?.id, slug: r.project?.slug, revision: r.project?.revision });
98 }
99 return out;
100}
101
102// Import an external image URL into Wix Media → { id, url }. Portfolio binds covers + gallery items
103// by the Wix Media file **id**, NOT a url — an external url (e.g. a base44 generate_image result)
104// MUST be imported first; the raw url renders nothing. id = wixstatic file id, url = wixstatic url.
105async function importImage(ctx, url, displayName = "image.png") {
106 const r = await req(ctx, "/site-media/v1/files/import", { body: { url, mimeType: "image/png", displayName } });
107 const f = r.file || r;
108 if (!f?.id) throw new Error(`import-file returned no file id: ${JSON.stringify(r).slice(0, 200)}`);
109 return { id: f.id, url: f.url };
110}
111
112// Optional — pass a cover/items to attach, omit to skip. Cover = the listing-card thumbnail. PATCH per entity,
113// echoing the current revision (a missing/stale revision fails). height + width are required
114// alongside the imported WixMedia image id (from importImage). items: [{ id, revision, imageId, height, width }].
115async function attachProjectCovers(ctx, items) {
116 for (const it of items) {
117 await req(ctx, `/portfolio/v1/projects/${it.id}`, {
118 method: "PATCH",
119 body: { project: { id: it.id, revision: it.revision, coverImage: { imageInfo: { id: it.imageId, height: it.height, width: it.width } } } },
120 });
121 }
122}
123async function attachCollectionCovers(ctx, items) {
124 for (const it of items) {
125 await req(ctx, `/portfolio/v1/collections/${it.id}`, {
126 method: "PATCH",
127 body: { collection: { id: it.id, revision: it.revision, coverImage: { imageInfo: { id: it.imageId, height: it.height, width: it.width } } } },
128 });
129 }
130}
131
132// Optional — the project's media gallery (detail-page images) is a SEPARATE `item` entity,
133// one POST per image. sortOrder (1,2,3…) sets render order. lowercase `items` — `/Items` 404s.
134// There is NO public list endpoint. items: [{ projectId, sortOrder, title, imageId, height, width }].
135async function createProjectItems(ctx, items) {
136 const out = [];
137 for (const it of items) {
138 const r = await req(ctx, "/portfolio/v1/items", {
139 body: { item: { projectId: it.projectId, sortOrder: it.sortOrder, title: it.title, image: { imageInfo: { id: it.imageId, height: it.height, width: it.width } } } },
140 });
141 out.push({ id: r.item?.id });
142 }
143 return out;
144}
145
146/**
147 * DEFAULT one-call path — seed a whole portfolio from one plan; ids stay in memory so
148 * collections→projects→items→covers are wired without hand-threading ids. Order matches SEED.md:
149 * createCollections → createProjects (into collections) → createProjectItems → attach*Covers.
150 * @param plan {
151 * collections: [{ title, description?, hidden?, coverImageUrl? }],
152 * projects: [{ title, description?, details?, hidden?,
153 * collection?: "<collection title>", // resolved to that collection's id
154 * items?: [{ sortOrder, title, imageUrl }],
155 * coverImageUrl? }],
156 * }
157 * coverImageUrl / items[].imageUrl are plain image urls — imported to Wix Media here. Covers/items
158 * are optional — a project/collection without one skips it.
159 * @returns { collections:[{id,slug,revision}], projects:[{id,slug,revision}], itemsCreated, coversAttached }
160 */
161// Install the Wix Portfolio app before seeding — base44 sites aren't guaranteed to have it (no
162// separate Setup step here, unlike the wix-headless recipe). Idempotent: re-installing returns 200.
163async function installPortfolioApp(ctx) {
164 return req(ctx, "/apps-installer-service/v1/app-instance/install", { body: {
165 tenant: { tenantType: "SITE", id: ctx.siteId },
166 appInstance: { appDefId: PORTFOLIO_APP_ID, enabled: true },
167 } });
168}
169
170async function setupPortfolio(ctx, { collections = [], projects = [] } = {}) {
171 await installPortfolioApp(ctx);
172 const cols = await createCollections(ctx, collections); // STEP 1
173 const idByName = new Map(collections.map((c, i) => [c.title, cols[i].id]));
174
175 const projs = await createProjects( // STEP 2
176 ctx,
177 projects.map((p) => ({
178 title: p.title, description: p.description, details: p.details, hidden: p.hidden,
179 collectionIds: p.collection ? [idByName.get(p.collection)].filter(Boolean) : [],
180 })),
181 );
182
183 // resolve a plain image url → { imageId, height, width } by importing to Wix Media (binds by file id)
184 const toImage = async (url, name) => {
185 const file = await importImage(ctx, url, name);
186 return { imageId: file.id, height: 1024, width: 1024 };
187 };
188
189 // STEP 3 — project media-gallery items (import each image; a failed import skips that item)
190 const itemsFlat = [];
191 for (let i = 0; i < projects.length; i++) {
192 for (const it of projects[i].items ?? []) {
193 if (!it.imageUrl) continue;
194 try {
195 const img = await toImage(it.imageUrl, `${it.title || "item"}.png`);
196 itemsFlat.push({ projectId: projs[i].id, sortOrder: it.sortOrder, title: it.title, ...img });
197 } catch { /* skip this item's image */ }
198 }
199 }
200 const items = itemsFlat.length ? await createProjectItems(ctx, itemsFlat) : [];
201
202 // STEP 4 — covers (import each; a failed import skips that cover)
203 const projCovers = [];
204 for (let i = 0; i < projects.length; i++) {
205 if (!projects[i].coverImageUrl) continue;
206 try {
207 const img = await toImage(projects[i].coverImageUrl, `${projects[i].title || "project"}-cover.png`);
208 projCovers.push({ id: projs[i].id, revision: projs[i].revision, ...img });
209 } catch { /* skip */ }
210 }
211 if (projCovers.length) await attachProjectCovers(ctx, projCovers);
212
213 const colCovers = [];
214 for (let i = 0; i < collections.length; i++) {
215 if (!collections[i].coverImageUrl) continue;
216 try {
217 const img = await toImage(collections[i].coverImageUrl, `${collections[i].title || "collection"}-cover.png`);
218 colCovers.push({ id: cols[i].id, revision: cols[i].revision, ...img });
219 } catch { /* skip */ }
220 }
221 if (colCovers.length) await attachCollectionCovers(ctx, colCovers);
222
223 return {
224 collections: cols,
225 projects: projs,
226 itemsCreated: items.length,
227 coversAttached: projCovers.length + colCovers.length,
228 };
229}
230
231module.exports = {
232 setupPortfolio, installPortfolioApp,
233 listProjects, listCollections,
234 createCollections, createProjects, importImage,
235 attachProjectCovers, attachCollectionCovers, createProjectItems,
236};