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