Setting the file. One moment.
Seed Portfolio · Wix Headless Templates · wix/skills · Skills Docs
ContentsBack to the top of the page — line 150
This file
Number 16.545
Position 545 of 828
Type JavaScript
Size 12 KB
Lines 254 portfolio/seed/ seed-portfolio.mjs
JavaScript · 254 lines · 12 KB
14
// "collection"? (title), // resolved to that collection's id
15 // "details"?: [{ "label", "text" }],
16 // "coverImageMediaId"? | "coverImageUrl"? | "coverImagePrompt"?,
17 // "items"?: [{ "sortOrder", "title"?, "imageMediaId" | "imageUrl" | "imagePrompt" }] }] }
18 //
19 // Seeding is ADDITIVE — never deletes or overwrites existing content. A fresh Portfolio
20 // install ships its own sample content ("My Portfolio" + sample projects); removing it is the
21 // owner's call, not this script's. Unexpected shapes → read the live API reference; every call
22 // below carries a docs: line with its reference page.
23 import { readFileSync } from "node:fs" ;
24 import { resolveItemImages } from "../../shared/seed/images.mjs" ;
25 import { seedSiteId } from "../../shared/seed/site-context.mjs" ;
26 import { wixToken } from "../../shared/seed/wix-cli.mjs" ;
27 import { isMain } from "../../shared/seed/main.mjs" ;
28
29 const API = "https://www.wixapis.com" ;
30 const PORTFOLIO_APP_ID = "d90652a2-f5a1-4c7c-84c4-d4cdcc41f130" ;
31
32 export function makeCtx ({ cwd = process. cwd () } = {}) {
33 // The content site: the config's site, or the parent on a migration preview (site-context.mjs stops
34 // a seed there unless --allow-parent is passed after the user confirmed).
35 const siteId = seedSiteId ({ cwd, argv: process.argv });
36 const token = wixToken (siteId, cwd);
37 return { token, siteId };
38 }
39
40 async function req ( ctx , path , { method = "POST" , body } = {}) {
41 const res = await fetch ( API + path, {
42 method,
43 headers: {
44 Authorization: `Bearer ${ ctx . token }` ,
45 "wix-site-id" : ctx.siteId,
46 "Content-Type" : "application/json" ,
47 },
48 body: body ? JSON . stringify (body) : undefined ,
49 });
50 const json = await res. json (). catch (() => ({}));
51 if ( ! res.ok) throw new Error ( `${ method } ${ path } -> ${ res . status }: ${ JSON . stringify ( json ). slice ( 0 , 400 ) }` );
52 return json;
53 }
54
55 // ---- operations ----------------------------------------------------------------------------------
56
57 // Idempotent — re-installing returns 200.
58 // docs: https://dev.wix.com/docs/api-reference/articles/work-with-wix-apis/platform/about-apps-created-by-wix.md
59 export async function installPortfolioApp ( ctx ) {
60 try {
61 await req (ctx, "/apps-installer-service/v1/app-instance/install" , { body: {
62 tenant: { tenantType: "SITE" , id: ctx.siteId },
63 appInstance: { appDefId: PORTFOLIO_APP_ID , enabled: true },
64 } });
65 } catch {
66 /* already installed is fine */
67 }
68 }
69
70 // Read-only listing helpers (partial re-seeds, verification).
71 // docs: https://dev.wix.com/docs/api-reference/business-solutions/portfolio/collections/list-collections.md
72 export async function listCollections ( ctx ) {
73 const r = await req (ctx, "/portfolio/v1/collections" , { method: "GET" });
74 return (r.collections ?? []). map (( c ) => ({ id: c.id, title: c.title, slug: c.slug }));
75 }
76 // docs: https://dev.wix.com/docs/api-reference/business-solutions/portfolio/projects/list-projects.md
77 export async function listProjects ( ctx ) {
78 const r = await req (ctx, "/portfolio/v1/projects" , { method: "GET" });
79 return (r.projects ?? []). map (( p ) => ({ id: p.id, title: p.title, slug: p.slug }));
80 }
81
82 // STEP 1 — collections. The display name is `title`, not `name`; `slug` auto-generates from
83 // the title; `hidden` defaults to false = shown (omit it — send true only to hide). No
84 // bulk-create: one call per collection.
85 // docs: https://dev.wix.com/docs/api-reference/business-solutions/portfolio/collections/create-collection.md
86 export async function createCollections ( ctx , collections ) {
87 const out = [];
88 for ( const c of collections) {
89 const body = { collection: { title: c.title, description: c.description } };
90 if (c.hidden) body.collection.hidden = true ;
91 const r = await req (ctx, "/portfolio/v1/collections" , { body });
92 out. push ({ id: r.collection?.id, slug: r.collection?.slug, revision: r.collection?.revision });
93 }
94 return out;
95 }
96
97 // STEP 2 — projects, AFTER collections: collectionIds must hold real ids from createCollections
98 // (they are NOT validated — a wrong/missing id silently orphans the project). `details` is an
99 // optional [{ label, text }] array. No bulk-create: one call per project.
100 // docs: https://dev.wix.com/docs/api-reference/business-solutions/portfolio/projects/create-project.md
101 export async function createProjects ( ctx , projects ) {
102 const out = [];
103 for ( const p of projects) {
104 const project = {
105 title: p.title,
106 description: p.description,
107 collectionIds: p.collectionIds ?? [],
108 };
109 if (p.details) project.details = p.details;
110 if (p.hidden) project.hidden = true ;
111 const r = await req (ctx, "/portfolio/v1/projects" , { body: { project } });
112 out. push ({ id: r.project?.id, slug: r.project?.slug, revision: r.project?.revision });
113 }
114 return out;
115 }
116
117 // Portfolio binds covers + gallery items by Wix Media file ID — an external url must be
118 // imported first (a raw url renders nothing); a plan `imagePrompt`/`coverImagePrompt` is
119 // generated (Wix AI, 1 credit) then imported. Both live in the shared util (parallel,
120 // resilient, never blocks the seed).
121 export { importImage } from "../../shared/seed/images.mjs" ;
122
123 // Cover = the listing-card thumbnail. PATCH per entity, echoing the current revision (missing/
124 // stale revision fails); height + width are required alongside the imported file id.
125 // docs: https://dev.wix.com/docs/api-reference/business-solutions/portfolio/projects/update-project.md
126 // One PATCH per entity; a failure on one must not cost the rest theirs. Returns
127 // { attached: [id], failures: [{ id, error }] } — the caller counts what persisted, not what was sent.
128 async function attachEach ( ctx , items , urlOf , bodyOf ) {
129 const attached = [];
130 const failures = [];
131 for ( const it of items) {
132 try { await req (ctx, urlOf (it), { method: "PATCH" , body: bodyOf (it) }); attached. push (it.id); }
133 catch (e) { failures. push ({ id: it.id, error: e?.message ?? String (e) }); }
134 }
135 return { attached, failures };
136 }
137 export async function attachProjectCovers ( ctx , items ) {
138 return attachEach (ctx, items, ( it ) => `/portfolio/v1/projects/${ it . id }` ,
139 ( it ) => ({ project: { id: it.id, revision: it.revision, coverImage: { imageInfo: { id: it.imageId, height: it.height, width: it.width } } } }));
140 }
141 // docs: https://dev.wix.com/docs/api-reference/business-solutions/portfolio/collections/update-collection.md
142 export async function attachCollectionCovers ( ctx , items ) {
143 return attachEach (ctx, items, ( it ) => `/portfolio/v1/collections/${ it . id }` ,
144 ( it ) => ({ collection: { id: it.id, revision: it.revision, coverImage: { imageInfo: { id: it.imageId, height: it.height, width: it.width } } } }));
145 }
146
147 // The detail-page gallery is a SEPARATE `item` entity — one POST per image; sortOrder (1,2,3…)
148 // sets render order. Lowercase `items` — `/Items` 404s. There is NO public list endpoint.
149 // docs: https://dev.wix.com/docs/api-reference/business-solutions/portfolio/project-items/create-project-item.md
150 export async function createProjectItems ( ctx , items ) {
151 const out = [];
152 for ( const it of items) {
153 const r = await req (ctx, "/portfolio/v1/items" , {
154 body: { item: { projectId: it.projectId, sortOrder: it.sortOrder, title: it.title, image: { imageInfo: { id: it.imageId, height: it.height, width: it.width } } } },
155 });
156 out. push ({ id: r.item?.id });
157 }
158 return out;
159 }
160
161 /**
162 * ONE-CALL seed: install → collections → projects (into collections) → gallery items →
163 * covers, ids threaded in memory. The default path.
164 */
165 export async function setupPortfolio ( ctx , { collections = [], projects = [] } = {}) {
166 await installPortfolioApp (ctx);
167
168 const cols = await createCollections (ctx, collections);
169 const idByTitle = new Map (collections. map (( c , i ) => [c.title, cols[i].id]));
170
171 const projs = await createProjects (
172 ctx,
173 projects. map (( p ) => ({
174 title: p.title,
175 description: p.description,
176 details: p.details,
177 hidden: p.hidden,
178 collectionIds: p.collection ? [idByTitle. get (p.collection)]. filter (Boolean) : [],
179 })),
180 );
181
182 // Pass 2 — images: gallery items + project covers + collection covers, flattened into ONE
183 // parallel wave (import by url / generate by prompt), then mapped back to each attach.
184 // A failed image skips just that item/cover; the seed's exit never depends on images.
185 const specs = [];
186 const galleryRefs = [];
187 projects. forEach (( p , pi ) => {
188 for ( const it of p.items ?? []) {
189 galleryRefs. push ({ pi, it, spec: specs. length });
190 specs. push ({ mediaId: it.imageMediaId, path: it.imagePath, url: it.imageUrl, prompt: it.imagePrompt, displayName: `${ it . title || "item"}.png` });
191 }
192 });
193 const projCoverAt = specs. length ;
194 projects. forEach (( p ) => specs. push ({ mediaId: p.coverImageMediaId, path: p.coverImagePath, url: p.coverImageUrl, prompt: p.coverImagePrompt, displayName: `${ p . title || "project"}-cover.png` }));
195 const colCoverAt = specs. length ;
196 collections. forEach (( c ) => specs. push ({ mediaId: c.coverImageMediaId, path: c.coverImagePath, url: c.coverImageUrl, prompt: c.coverImagePrompt, displayName: `${ c . title || "collection"}-cover.png` }));
197 const files = await resolveItemImages (ctx, specs);
198 const dims = { height: 1024 , width: 1024 };
199
200 const itemsFlat = galleryRefs
201 . map (({ pi , it , spec }) => (files[spec] && projs[pi]?.id
202 ? { projectId: projs[pi].id, sortOrder: it.sortOrder, title: it.title, imageId: files[spec].id, ... dims }
203 : null ))
204 . filter (Boolean);
205 let items = [];
206 try {
207 if (itemsFlat. length ) items = await createProjectItems (ctx, itemsFlat);
208 } catch {
209 /* the remaining gallery items stay unseeded */
210 }
211
212 const projCovers = projs
213 . map (( p , i ) => (files[projCoverAt + i] && p.id
214 ? { id: p.id, revision: p.revision, imageId: files[projCoverAt + i].id, ... dims }
215 : null ))
216 . filter (Boolean);
217 const colCovers = cols
218 . map (( c , i ) => (files[colCoverAt + i] && c.id
219 ? { id: c.id, revision: c.revision, imageId: files[colCoverAt + i].id, ... dims }
220 : null ))
221 . filter (Boolean);
222 // coversAttached counts the PATCHes that succeeded; coverFailures names the rest. Neither blocks the seed.
223 let coversAttached = 0 ;
224 const coverFailures = [];
225 if (projCovers. length ) { const r = await attachProjectCovers (ctx, projCovers); coversAttached += r.attached. length ; coverFailures. push ( ... r.failures. map (( f ) => ({ project: f.id, error: f.error }))); }
226 if (colCovers. length ) { const r = await attachCollectionCovers (ctx, colCovers); coversAttached += r.attached. length ; coverFailures. push ( ... r.failures. map (( f ) => ({ collection: f.id, error: f.error }))); }
227
228 return {
229 collections: cols,
230 projects: projs,
231 itemsCreated: items. length ,
232 coversAttached,
233 coverFailures,
234 };
235 }
236
237 // ---- CLI entry ----------------------------------------------------------------------------------
238
239 const invokedDirectly = isMain ( import . meta .url);
240 if (invokedDirectly) {
241 const planPath = process.argv[ 2 ];
242 if ( ! planPath) {
243 console. error ( "usage: node seed-portfolio.mjs <plan.json> (run from the project root)" );
244 process. exit ( 1 );
245 }
246 const plan = JSON . parse ( readFileSync (planPath, "utf8" ));
247 const ctx = makeCtx ();
248 setupPortfolio (ctx, plan)
249 . then (( result ) => console. log ( JSON . stringify (result, null , 2 )))
250 . catch (( e ) => {
251 console. error (e.message);
252 process. exit ( 1 );
253 });
254 }