domma-cms 1.8.10 → 1.8.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -25,6 +25,18 @@
25
25
  * beside the export (./widgets.js), become one block per widget area, and a
26
26
  * footer area's block joins the footer (summary.widgets).
27
27
  *
28
+ * The home page (./front.js, plan.home): the page WordPress showed at its home
29
+ * address becomes this site's home page (options.home, on by default when one
30
+ * is found); the page there now moves to /previous-home as a draft - once, on
31
+ * the first run that does it (mapping `home`), never again without asking: when
32
+ * an earlier import set the home page and this export's front page is another
33
+ * page, plan.home.question asks (options.homeReplace, no default - the run is
34
+ * refused until it is answered). Yes moves the home page aside the same way;
35
+ * No imports the page at its own address. A suggested posts
36
+ * page, once confirmed (options.postsPage), is not made: its old address leads
37
+ * to the Blog. With no front page, the latest posts can be the home page
38
+ * (options.latestPosts, a page holding the Blog's [blog-list]).
39
+ *
28
40
  * @module services/wpImport/plan
29
41
  */
30
42
  import {detectBuilder, findShortcodes} from './convert.js';
@@ -34,6 +46,7 @@ import {readWooProducts, shopProductInput, wooPageRole} from './woo.js';
34
46
  import {customFields, customOldPath, customTypesOf, mediaContext, rewriteSlugOf, typeLabel} from './custom.js';
35
47
  import {planPeople} from './people.js';
36
48
  import {planWidgets} from './widgets.js';
49
+ import {detectFrontPage, suggestPostsPage} from './front.js';
37
50
 
38
51
  /** Addresses under the Shop's that are its own pages, never a product's. */
39
52
  const SHOP_RESERVED = ['cart', 'checkout', 'basket', 'account', 'order'];
@@ -219,6 +232,12 @@ export function isoDate(gmt, local) {
219
232
  * @param {object} [options.people] - what the person running it may do with users (never from a request)
220
233
  * @param {object|null} [options.wie] - the widgets file, from readWie() (./widgets.js)
221
234
  * @param {Object<string,false>} [options.footer] - per widget area: false = not placed in the footer
235
+ * @param {boolean} [options.home=true] - the WordPress front page becomes this site's home page
236
+ * @param {number} [options.postsPage] - the suggested posts page, confirmed (its WordPress id)
237
+ * @param {boolean} [options.latestPosts=false] - no front page: the latest posts on the home page
238
+ * @param {boolean|null} [options.homeReplace=null] - the answer to plan.home.question: make this export's
239
+ * front page the home page instead of the one an earlier import set (null: not answered)
240
+ * @param {string|null} [site.homeTitle] - the title of the page at / now (null: none)
222
241
  * @returns {object}
223
242
  */
224
243
  export function planImport(wxr, site, mapping = {}, options = {}) {
@@ -226,6 +245,7 @@ export function planImport(wxr, site, mapping = {}, options = {}) {
226
245
  posts: site.blog?.available ? 'blog' : 'pages',
227
246
  media: 'download', comments: true, menus: true, redirects: true, drafts: true, products: true, choices: {}, zipPaths: null,
228
247
  types: {}, listPages: {}, users: {}, userRole: '', invite: false, footer: {}, wie: null,
248
+ home: true, postsPage: 0, latestPosts: false, homeReplace: null,
229
249
  ...options
230
250
  };
231
251
  opt.types ||= {};
@@ -247,7 +267,9 @@ export function planImport(wxr, site, mapping = {}, options = {}) {
247
267
  const notes = [];
248
268
  const byId = new Map(wxr.items.map(it => [it.id, it]));
249
269
 
250
- const takenPaths = new Set(site.pagePaths);
270
+ // An address this site already redirects (and not by this import) would hide a page made there: taken too.
271
+ const ownRedirectFroms = new Set(Object.values(mapping.redirects || {}).map(r => r?.from));
272
+ const takenPaths = new Set([...site.pagePaths, ...[...(site.redirectFroms || [])].filter(f => typeof f === 'string' && !f.includes('*') && !ownRedirectFroms.has(f))]);
251
273
  const takenMedia = new Set(site.mediaNames);
252
274
  const takenMenus = new Set(site.menuSlugs);
253
275
  const takenSlugs = new Set(site.blog?.postSlugs || []);
@@ -294,6 +316,62 @@ export function planImport(wxr, site, mapping = {}, options = {}) {
294
316
  // Parents before children, so a child's address can follow its parent's.
295
317
  const depthOf = (it, seen = new Set()) => (it.parent && byId.has(it.parent) && !seen.has(it.id) ? 1 + depthOf(byId.get(it.parent), seen.add(it.id)) : 0);
296
318
  const orderedPages = [...pages].sort((a, b) => depthOf(a) - depthOf(b) || a.order - b.order || a.id - b.id);
319
+ // ---- The home page: WordPress's front page, its posts page (./front.js) ----
320
+ // `home` in the mapping: an earlier run set this site's home page - it is not moved again without asking.
321
+ // An import of another WordPress site that set it counts too (site.homeSetBy): the latest one is the home page.
322
+ const ownHome = mapped.home?.kind === 'home' ? mapped.home : null;
323
+ const otherHome = site.homeSetBy?.kind === 'home' ? site.homeSetBy : null;
324
+ const homeRec = otherHome && (!ownHome || String(otherHome.at || '') > String(ownHome.at || '')) ? otherHome : ownHome;
325
+ const leftAsIs = homeRec?.source
326
+ ? `An earlier import (of ${homeRec.source}) set this site's home page - it is left as it is.`
327
+ : 'This site\'s home page was changed here since the last import - it is left as it is.';
328
+ const {page: front, matches: frontMatches} = detectFrontPage(wxr);
329
+ const suggested = opt.posts !== 'skip' ? suggestPostsPage(wxr, {front, blogBase}) : null;
330
+ // Imported before as a page that is still here: the person's page now - never hidden behind a redirect.
331
+ const suggestedAt = suggested && mapped[`page:${suggested.id}`]?.path;
332
+ const postsCandidate = suggested && !(suggestedAt && site.pagePaths.has(suggestedAt)) ? suggested : null;
333
+ const postsPageId = postsCandidate && Number(opt.postsPage) === postsCandidate.id ? postsCandidate.id : 0;
334
+ const latestOffered = !front && !frontMatches && opt.posts === 'blog' && Boolean(site.shortcodes?.has('blog-list'));
335
+ let homeMove = null;
336
+ let homeChanged = '';
337
+ /**
338
+ * Something of this run's takes "/": the page there now moves to /previous-home (-2…), never deleted.
339
+ * `replace`: the person said Yes to the question - even a page an earlier run of this import put there moves.
340
+ */
341
+ const claimHome = (replace = false) => {
342
+ if (homeMove) return;
343
+ if (site.pagePaths.has('/') && (replace || !ownPaths.has('/'))) {
344
+ const to = freePath('/previous-home', takenPaths);
345
+ takenPaths.add(to);
346
+ takenPaths.delete('/');
347
+ homeMove = {from: '/', to};
348
+ }
349
+ };
350
+ // ---- The question: an earlier import set the home page, and this export's front page is another page ----
351
+ const frontKey = front ? `page:${front.id}` : '';
352
+ const frontWas = front ? mapped[frontKey] : null;
353
+ // Deleted here since an earlier run made it: not imported, or ("Import again") brought back at its old address -
354
+ // either way it does not take "/", so there is nothing to ask.
355
+ const frontGone = Boolean(frontWas?.path && !site.pagePaths.has(frontWas.path));
356
+ // The same page this import made the home page (a re-run), or already at "/" anyway: nothing to ask.
357
+ const sameHome = Boolean(homeRec && !homeRec.source && homeRec.how !== 'latest' && Number(homeRec.wpId) === front?.id);
358
+ const frontIsHome = frontWas?.path === '/' && site.pagePaths.has('/');
359
+ const askHome = Boolean(front && opt.home && choices[frontKey] !== 'skip' && !frontGone && homeRec && !sameHome && !frontIsHome && site.pagePaths.has('/'));
360
+ const homeAnswer = askHome && typeof opt.homeReplace === 'boolean' ? opt.homeReplace : null;
361
+ const replaceHome = homeAnswer === true;
362
+ // Yes: what is at "/" moves aside before any page is planned, so a page of this import's that is there now
363
+ // is planned at its new address (planDocument), not over the new home page.
364
+ if (replaceHome) claimHome(true);
365
+ const homeAddress = (it) => {
366
+ const p = oldPathOf(it.link, wxr.site);
367
+ return p !== null && relative(p, base) === '/';
368
+ };
369
+ /** WordPress's own address for a page by its slugs (/about/team) - the front page answered there too. */
370
+ const slugAddress = (it) => {
371
+ const segs = [];
372
+ for (let c = it, n = 0; c && n < 10; c = c.parent ? byId.get(c.parent) : null, n++) segs.unshift(slugify(c.slug));
373
+ return segs.every(Boolean) ? `${base}/${segs.join('/')}` : '';
374
+ };
297
375
  const WOO_PAGE = {
298
376
  cart: {to: '/cart', what: 'cart'}, checkout: {to: '/cart', what: 'checkout'},
299
377
  account: {to: '/account', what: 'account page'}, shop: {to: '', what: 'shop page'}
@@ -309,8 +387,30 @@ export function planImport(wxr, site, mapping = {}, options = {}) {
309
387
  if (oldPath) extraRedirects.push({from: oldPath, to, key});
310
388
  continue;
311
389
  }
390
+ if (postsPageId === it.id) {
391
+ if (opt.posts === 'blog') {
392
+ // The Blog lists the posts itself: no empty page, and the old address leads there.
393
+ push({...docRow(it, key, 'page', oldPath), status: 'published', action: 'skip', postsPage: true, target: blogBase,
394
+ reason: `WordPress's posts page - the Blog lists the posts at ${blogBase}, so no empty page is made${oldPath ? ' and the old address leads there' : ''}.`});
395
+ if (oldPath) extraRedirects.push({from: oldPath, to: blogBase, key: `${key}/posts`});
396
+ continue;
397
+ }
398
+ // Posts as pages: it becomes their index, where they are put.
399
+ const r = planDocument({it, key, kind: 'page', wanted: pagesBase, oldPath, statusMap: PAGE_STATUS});
400
+ r.postsIndex = true;
401
+ if (r.action === 'create' && !r.reason) r.reason = `WordPress's posts page - it lists the posts imported as pages under ${pagesBase}.`;
402
+ if (r.target) pageTarget.set(it.id, r.target);
403
+ continue;
404
+ }
405
+ if (front?.id === it.id && opt.home && choices[key] !== 'skip') {
406
+ const r = planFront(it, key, oldPath);
407
+ if (r.target) pageTarget.set(it.id, r.target);
408
+ continue;
409
+ }
312
410
  let wanted = oldPath ? cleanPath(relative(oldPath, base)) : '';
313
411
  if (!wanted || wanted === '/' && oldPath !== null && relative(oldPath, base) !== '/') wanted = '';
412
+ // Only the one front page takes "/" (and only when chosen): another page at the home address keeps its slug.
413
+ if (wanted === '/' && homeAddress(it)) wanted = '';
314
414
  if (!wanted) {
315
415
  const parentPath = it.parent && pageTarget.get(it.parent);
316
416
  const slug = slugify(it.slug) || slugify(it.title) || `page-${it.id}`;
@@ -320,6 +420,25 @@ export function planImport(wxr, site, mapping = {}, options = {}) {
320
420
  if (row.target) pageTarget.set(it.id, row.target);
321
421
  }
322
422
 
423
+ // ---- No front page: the latest posts on the home page, when chosen ----
424
+ if (latestOffered && opt.latestPosts) {
425
+ const key = 'home:latest';
426
+ const was = mapped[key];
427
+ let wanted = '/';
428
+ if (!was?.path && choices[key] !== 'skip') {
429
+ if (!homeRec) claimHome();
430
+ else if (takenPaths.has('/')) { wanted = ''; homeChanged = leftAsIs; }
431
+ }
432
+ if (wanted) {
433
+ const r = planDocument({it: {id: 0, title: wxr.site.title || 'Latest posts', link: '', status: 'publish', content: '', meta: {}, terms: [], password: ''},
434
+ key, kind: 'page', wanted, oldPath: null, statusMap: PAGE_STATUS});
435
+ r.homeLatest = true;
436
+ r.wpId = 0;
437
+ r.title = 'Home page: the latest posts';
438
+ if (r.action === 'create' && !r.reason) r.reason = 'Shows the latest Blog posts, as WordPress did on its home page.';
439
+ }
440
+ }
441
+
323
442
  // ---- Posts ----
324
443
  const posts = wxr.items.filter(i => i.type === 'post');
325
444
  for (const it of posts) {
@@ -552,6 +671,8 @@ export function planImport(wxr, site, mapping = {}, options = {}) {
552
671
  const query = {p: {}, page_id: {}, attachment_id: {}};
553
672
  const live = (r) => r.target && r.action !== 'skip';
554
673
  for (const r of items) {
674
+ // A confirmed posts page is not made: ?page_id= for it leads to the Blog.
675
+ if (r.postsPage) { query.page_id[r.wpId] = r.target; query.p[r.wpId] = r.target; continue; }
555
676
  // An entry in a collection has no address of its own: its old one leads to the type's list page.
556
677
  if (r.kind === 'custom' && r.as === 'collection' && r.action !== 'skip') {
557
678
  if (r.listAt) query.p[r.wpId] = r.listAt;
@@ -662,6 +783,7 @@ export function planImport(wxr, site, mapping = {}, options = {}) {
662
783
  const productRows = items.filter(r => r.kind === 'product');
663
784
  const people = planPeople(wxr, items, site.users || [], mapping, opt, opt.people || {});
664
785
  const shortcodeCount = items.reduce((n, r) => n + (r.shortcodes?.length || 0), 0);
786
+ const home = homeSummary();
665
787
 
666
788
  const summary = {
667
789
  counts,
@@ -696,11 +818,71 @@ export function planImport(wxr, site, mapping = {}, options = {}) {
696
818
  query,
697
819
  mediaByRel,
698
820
  people,
821
+ home,
699
822
  notes: [...new Set(notes)],
700
823
  summary
701
824
  };
702
825
 
703
826
  // ---- helpers sharing the plan's state ----
827
+ /**
828
+ * WordPress's front page, chosen as the home page. The first run that does it moves the page at "/"
829
+ * away (claimHome); a re-run updates it where it is, and if the home page was changed here by hand
830
+ * since, leaves the home page alone and says so.
831
+ */
832
+ function planFront(it, key, oldPath) {
833
+ const was = mapped[key];
834
+ const here = was?.path && site.pagePaths.has(was.path) ? was.path : '';
835
+ const slugPath = slugAddress(it);
836
+ let row;
837
+ if (homeRec && !replaceHome) {
838
+ // Set by an earlier run: never moved again (unless the person said Yes to the question).
839
+ if (here && here !== '/') homeChanged = `This site's home page was changed here since the last import - "${it.title || 'the home page'}" stays at ${here}.`;
840
+ let wanted = '/';
841
+ if (!here && takenPaths.has('/')) {
842
+ wanted = `/${slugify(it.slug) || slugify(it.title) || 'home'}`;
843
+ homeChanged = leftAsIs;
844
+ }
845
+ row = planDocument({it, key, kind: 'page', wanted, oldPath, statusMap: PAGE_STATUS});
846
+ // Asked: No keeps this site's home page and the page arrives at its own address; unanswered, the card asks.
847
+ if (askHome) homeChanged = homeAnswer === false ? `You chose to keep this site's home page - "${it.title || 'the page'}" is imported${row.target ? ` at ${row.target}` : ''}.` : '';
848
+ } else {
849
+ // Deleted here since an earlier run made it (not as the home page): that is the person's call, as for any page.
850
+ if (!was?.path || here) claimHome(replaceHome);
851
+ row = planDocument({it, key, kind: 'page', wanted: '/', oldPath, statusMap: PAGE_STATUS});
852
+ // Imported before at another address (not as the home page then): it moves to "/".
853
+ if (here && here !== '/' && row.action !== 'skip') {
854
+ row.moveFrom = here;
855
+ row.target = '/';
856
+ takenPaths.add('/');
857
+ extraRedirects.push({from: here, to: '/', key: `${key}/moved`});
858
+ }
859
+ }
860
+ row.front = true;
861
+ if (row.target === '/' && slugPath && slugPath !== '/') extraRedirects.push({from: slugPath, to: '/', key: `${key}/slug`});
862
+ return row;
863
+ }
864
+ function homeSummary() {
865
+ const frontRow = front ? items.find(r => r.key === `page:${front.id}`) : null;
866
+ const latestRow = items.find(r => r.key === 'home:latest');
867
+ const postsRow = postsCandidate ? items.find(r => r.key === `page:${postsCandidate.id}`) : null;
868
+ return {
869
+ how: front ? 'page' : frontMatches > 1 ? 'unclear' : 'latest',
870
+ matches: frontMatches,
871
+ on: opt.home !== false,
872
+ page: front ? {wpId: front.id, title: front.title || '(untitled)', oldUrl: front.link, target: frontRow?.target || '', action: frontRow?.action || 'skip', isHome: frontRow?.target === '/' && frontRow.action !== 'skip'} : null,
873
+ now: site.pagePaths.has('/') ? {title: site.homeTitle ?? ''} : null,
874
+ move: homeMove,
875
+ // An earlier import set the home page and this front page is another: asked, never assumed.
876
+ question: askHome ? {title: front.title || '(untitled)', current: site.homeTitle ?? '', answer: homeAnswer} : null,
877
+ done: homeRec ? {how: homeRec.how || 'page', previous: homeRec.previous || '', source: homeRec.source || ''} : null,
878
+ changed: homeChanged,
879
+ latest: {offered: latestOffered, on: Boolean(latestOffered && opt.latestPosts), target: latestRow?.target || '', action: latestRow?.action || ''},
880
+ postsPage: postsCandidate ? {
881
+ wpId: postsCandidate.id, title: postsCandidate.title || '(untitled)', oldUrl: postsCandidate.link,
882
+ confirmed: Boolean(postsPageId), to: opt.posts === 'blog' ? blogBase : (postsRow?.postsIndex ? postsRow.target : pagesBase)
883
+ } : null
884
+ };
885
+ }
704
886
  function docRow(it, key, kind, oldPath) {
705
887
  return {
706
888
  key, kind, wpId: it.id, title: it.title || `(untitled ${kind} ${it.id})`, oldUrl: it.link, oldPath,
@@ -718,6 +900,11 @@ export function planImport(wxr, site, mapping = {}, options = {}) {
718
900
  if (it.password) row.status = 'draft';
719
901
  const was = mapped[key];
720
902
  const why = excluded(it);
903
+ // The home page until now, made by an earlier run of this import: Yes to the question moves it aside as a draft.
904
+ if (replaceHome && homeMove && was?.path === '/' && key !== frontKey && site.pagePaths.has('/')) {
905
+ return push({...row, action: 'keep', target: homeMove.to, movedAside: true,
906
+ reason: `This site's home page until now - it moves to ${homeMove.to} as a draft, nothing is deleted.`});
907
+ }
721
908
  if (choices[key] === 'skip') return push({...row, action: 'skip', reason: 'You chose to skip it.'});
722
909
  if (why) return push({...row, action: 'skip', reason: why});
723
910
  if (was?.path) {