domma-cms 1.8.9 → 1.8.11

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,14 @@
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. A suggested posts
32
+ * page, once confirmed (options.postsPage), is not made: its old address leads
33
+ * to the Blog. With no front page, the latest posts can be the home page
34
+ * (options.latestPosts, a page holding the Blog's [blog-list]).
35
+ *
28
36
  * @module services/wpImport/plan
29
37
  */
30
38
  import {detectBuilder, findShortcodes} from './convert.js';
@@ -34,6 +42,7 @@ import {readWooProducts, shopProductInput, wooPageRole} from './woo.js';
34
42
  import {customFields, customOldPath, customTypesOf, mediaContext, rewriteSlugOf, typeLabel} from './custom.js';
35
43
  import {planPeople} from './people.js';
36
44
  import {planWidgets} from './widgets.js';
45
+ import {detectFrontPage, suggestPostsPage} from './front.js';
37
46
 
38
47
  /** Addresses under the Shop's that are its own pages, never a product's. */
39
48
  const SHOP_RESERVED = ['cart', 'checkout', 'basket', 'account', 'order'];
@@ -219,6 +228,10 @@ export function isoDate(gmt, local) {
219
228
  * @param {object} [options.people] - what the person running it may do with users (never from a request)
220
229
  * @param {object|null} [options.wie] - the widgets file, from readWie() (./widgets.js)
221
230
  * @param {Object<string,false>} [options.footer] - per widget area: false = not placed in the footer
231
+ * @param {boolean} [options.home=true] - the WordPress front page becomes this site's home page
232
+ * @param {number} [options.postsPage] - the suggested posts page, confirmed (its WordPress id)
233
+ * @param {boolean} [options.latestPosts=false] - no front page: the latest posts on the home page
234
+ * @param {string|null} [site.homeTitle] - the title of the page at / now (null: none)
222
235
  * @returns {object}
223
236
  */
224
237
  export function planImport(wxr, site, mapping = {}, options = {}) {
@@ -226,6 +239,7 @@ export function planImport(wxr, site, mapping = {}, options = {}) {
226
239
  posts: site.blog?.available ? 'blog' : 'pages',
227
240
  media: 'download', comments: true, menus: true, redirects: true, drafts: true, products: true, choices: {}, zipPaths: null,
228
241
  types: {}, listPages: {}, users: {}, userRole: '', invite: false, footer: {}, wie: null,
242
+ home: true, postsPage: 0, latestPosts: false,
229
243
  ...options
230
244
  };
231
245
  opt.types ||= {};
@@ -247,7 +261,9 @@ export function planImport(wxr, site, mapping = {}, options = {}) {
247
261
  const notes = [];
248
262
  const byId = new Map(wxr.items.map(it => [it.id, it]));
249
263
 
250
- const takenPaths = new Set(site.pagePaths);
264
+ // An address this site already redirects (and not by this import) would hide a page made there: taken too.
265
+ const ownRedirectFroms = new Set(Object.values(mapping.redirects || {}).map(r => r?.from));
266
+ const takenPaths = new Set([...site.pagePaths, ...[...(site.redirectFroms || [])].filter(f => typeof f === 'string' && !f.includes('*') && !ownRedirectFroms.has(f))]);
251
267
  const takenMedia = new Set(site.mediaNames);
252
268
  const takenMenus = new Set(site.menuSlugs);
253
269
  const takenSlugs = new Set(site.blog?.postSlugs || []);
@@ -294,6 +310,41 @@ export function planImport(wxr, site, mapping = {}, options = {}) {
294
310
  // Parents before children, so a child's address can follow its parent's.
295
311
  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
312
  const orderedPages = [...pages].sort((a, b) => depthOf(a) - depthOf(b) || a.order - b.order || a.id - b.id);
313
+ // ---- The home page: WordPress's front page, its posts page (./front.js) ----
314
+ // `home` in the mapping: an earlier run set this site's home page - it is never moved again.
315
+ // An import of another WordPress site that set it counts too (site.homeSetBy): once per site.
316
+ const homeRec = mapped.home?.kind === 'home' ? mapped.home : site.homeSetBy?.kind === 'home' ? site.homeSetBy : null;
317
+ const leftAsIs = homeRec?.source
318
+ ? `An earlier import (of ${homeRec.source}) set this site's home page - it is left as it is.`
319
+ : 'This site\'s home page was changed here since the last import - it is left as it is.';
320
+ const {page: front, matches: frontMatches} = detectFrontPage(wxr);
321
+ const suggested = opt.posts !== 'skip' ? suggestPostsPage(wxr, {front, blogBase}) : null;
322
+ // Imported before as a page that is still here: the person's page now - never hidden behind a redirect.
323
+ const suggestedAt = suggested && mapped[`page:${suggested.id}`]?.path;
324
+ const postsCandidate = suggested && !(suggestedAt && site.pagePaths.has(suggestedAt)) ? suggested : null;
325
+ const postsPageId = postsCandidate && Number(opt.postsPage) === postsCandidate.id ? postsCandidate.id : 0;
326
+ const latestOffered = !front && !frontMatches && opt.posts === 'blog' && Boolean(site.shortcodes?.has('blog-list'));
327
+ let homeMove = null;
328
+ let homeChanged = '';
329
+ /** Something of this run's takes "/" for the first time: the page there now moves to /previous-home (-2…), never deleted. */
330
+ const claimHome = () => {
331
+ if (site.pagePaths.has('/') && !ownPaths.has('/')) {
332
+ const to = freePath('/previous-home', takenPaths);
333
+ takenPaths.add(to);
334
+ takenPaths.delete('/');
335
+ homeMove = {from: '/', to};
336
+ }
337
+ };
338
+ const homeAddress = (it) => {
339
+ const p = oldPathOf(it.link, wxr.site);
340
+ return p !== null && relative(p, base) === '/';
341
+ };
342
+ /** WordPress's own address for a page by its slugs (/about/team) - the front page answered there too. */
343
+ const slugAddress = (it) => {
344
+ const segs = [];
345
+ for (let c = it, n = 0; c && n < 10; c = c.parent ? byId.get(c.parent) : null, n++) segs.unshift(slugify(c.slug));
346
+ return segs.every(Boolean) ? `${base}/${segs.join('/')}` : '';
347
+ };
297
348
  const WOO_PAGE = {
298
349
  cart: {to: '/cart', what: 'cart'}, checkout: {to: '/cart', what: 'checkout'},
299
350
  account: {to: '/account', what: 'account page'}, shop: {to: '', what: 'shop page'}
@@ -309,8 +360,30 @@ export function planImport(wxr, site, mapping = {}, options = {}) {
309
360
  if (oldPath) extraRedirects.push({from: oldPath, to, key});
310
361
  continue;
311
362
  }
363
+ if (postsPageId === it.id) {
364
+ if (opt.posts === 'blog') {
365
+ // The Blog lists the posts itself: no empty page, and the old address leads there.
366
+ push({...docRow(it, key, 'page', oldPath), status: 'published', action: 'skip', postsPage: true, target: blogBase,
367
+ 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' : ''}.`});
368
+ if (oldPath) extraRedirects.push({from: oldPath, to: blogBase, key: `${key}/posts`});
369
+ continue;
370
+ }
371
+ // Posts as pages: it becomes their index, where they are put.
372
+ const r = planDocument({it, key, kind: 'page', wanted: pagesBase, oldPath, statusMap: PAGE_STATUS});
373
+ r.postsIndex = true;
374
+ if (r.action === 'create' && !r.reason) r.reason = `WordPress's posts page - it lists the posts imported as pages under ${pagesBase}.`;
375
+ if (r.target) pageTarget.set(it.id, r.target);
376
+ continue;
377
+ }
378
+ if (front?.id === it.id && opt.home && choices[key] !== 'skip') {
379
+ const r = planFront(it, key, oldPath);
380
+ if (r.target) pageTarget.set(it.id, r.target);
381
+ continue;
382
+ }
312
383
  let wanted = oldPath ? cleanPath(relative(oldPath, base)) : '';
313
384
  if (!wanted || wanted === '/' && oldPath !== null && relative(oldPath, base) !== '/') wanted = '';
385
+ // Only the one front page takes "/" (and only when chosen): another page at the home address keeps its slug.
386
+ if (wanted === '/' && homeAddress(it)) wanted = '';
314
387
  if (!wanted) {
315
388
  const parentPath = it.parent && pageTarget.get(it.parent);
316
389
  const slug = slugify(it.slug) || slugify(it.title) || `page-${it.id}`;
@@ -320,6 +393,25 @@ export function planImport(wxr, site, mapping = {}, options = {}) {
320
393
  if (row.target) pageTarget.set(it.id, row.target);
321
394
  }
322
395
 
396
+ // ---- No front page: the latest posts on the home page, when chosen ----
397
+ if (latestOffered && opt.latestPosts) {
398
+ const key = 'home:latest';
399
+ const was = mapped[key];
400
+ let wanted = '/';
401
+ if (!was?.path && choices[key] !== 'skip') {
402
+ if (!homeRec) claimHome();
403
+ else if (takenPaths.has('/')) { wanted = ''; homeChanged = leftAsIs; }
404
+ }
405
+ if (wanted) {
406
+ const r = planDocument({it: {id: 0, title: wxr.site.title || 'Latest posts', link: '', status: 'publish', content: '', meta: {}, terms: [], password: ''},
407
+ key, kind: 'page', wanted, oldPath: null, statusMap: PAGE_STATUS});
408
+ r.homeLatest = true;
409
+ r.wpId = 0;
410
+ r.title = 'Home page: the latest posts';
411
+ if (r.action === 'create' && !r.reason) r.reason = 'Shows the latest Blog posts, as WordPress did on its home page.';
412
+ }
413
+ }
414
+
323
415
  // ---- Posts ----
324
416
  const posts = wxr.items.filter(i => i.type === 'post');
325
417
  for (const it of posts) {
@@ -552,6 +644,8 @@ export function planImport(wxr, site, mapping = {}, options = {}) {
552
644
  const query = {p: {}, page_id: {}, attachment_id: {}};
553
645
  const live = (r) => r.target && r.action !== 'skip';
554
646
  for (const r of items) {
647
+ // A confirmed posts page is not made: ?page_id= for it leads to the Blog.
648
+ if (r.postsPage) { query.page_id[r.wpId] = r.target; query.p[r.wpId] = r.target; continue; }
555
649
  // An entry in a collection has no address of its own: its old one leads to the type's list page.
556
650
  if (r.kind === 'custom' && r.as === 'collection' && r.action !== 'skip') {
557
651
  if (r.listAt) query.p[r.wpId] = r.listAt;
@@ -662,6 +756,7 @@ export function planImport(wxr, site, mapping = {}, options = {}) {
662
756
  const productRows = items.filter(r => r.kind === 'product');
663
757
  const people = planPeople(wxr, items, site.users || [], mapping, opt, opt.people || {});
664
758
  const shortcodeCount = items.reduce((n, r) => n + (r.shortcodes?.length || 0), 0);
759
+ const home = homeSummary();
665
760
 
666
761
  const summary = {
667
762
  counts,
@@ -696,11 +791,67 @@ export function planImport(wxr, site, mapping = {}, options = {}) {
696
791
  query,
697
792
  mediaByRel,
698
793
  people,
794
+ home,
699
795
  notes: [...new Set(notes)],
700
796
  summary
701
797
  };
702
798
 
703
799
  // ---- helpers sharing the plan's state ----
800
+ /**
801
+ * WordPress's front page, chosen as the home page. The first run that does it moves the page at "/"
802
+ * away (claimHome); a re-run updates it where it is, and if the home page was changed here by hand
803
+ * since, leaves the home page alone and says so.
804
+ */
805
+ function planFront(it, key, oldPath) {
806
+ const was = mapped[key];
807
+ const here = was?.path && site.pagePaths.has(was.path) ? was.path : '';
808
+ const slugPath = slugAddress(it);
809
+ let row;
810
+ if (homeRec) {
811
+ // Set by an earlier run: never moved again.
812
+ if (here && here !== '/') homeChanged = `This site's home page was changed here since the last import - "${it.title || 'the home page'}" stays at ${here}.`;
813
+ let wanted = '/';
814
+ if (!here && takenPaths.has('/')) {
815
+ wanted = `/${slugify(it.slug) || slugify(it.title) || 'home'}`;
816
+ homeChanged = leftAsIs;
817
+ }
818
+ row = planDocument({it, key, kind: 'page', wanted, oldPath, statusMap: PAGE_STATUS});
819
+ } else {
820
+ // Deleted here since an earlier run made it (not as the home page): that is the person's call, as for any page.
821
+ if (!was?.path || here) claimHome();
822
+ row = planDocument({it, key, kind: 'page', wanted: '/', oldPath, statusMap: PAGE_STATUS});
823
+ // Imported before at another address (not as the home page then): it moves to "/".
824
+ if (here && here !== '/' && row.action !== 'skip') {
825
+ row.moveFrom = here;
826
+ row.target = '/';
827
+ takenPaths.add('/');
828
+ extraRedirects.push({from: here, to: '/', key: `${key}/moved`});
829
+ }
830
+ }
831
+ row.front = true;
832
+ if (row.target === '/' && slugPath && slugPath !== '/') extraRedirects.push({from: slugPath, to: '/', key: `${key}/slug`});
833
+ return row;
834
+ }
835
+ function homeSummary() {
836
+ const frontRow = front ? items.find(r => r.key === `page:${front.id}`) : null;
837
+ const latestRow = items.find(r => r.key === 'home:latest');
838
+ const postsRow = postsCandidate ? items.find(r => r.key === `page:${postsCandidate.id}`) : null;
839
+ return {
840
+ how: front ? 'page' : frontMatches > 1 ? 'unclear' : 'latest',
841
+ matches: frontMatches,
842
+ on: opt.home !== false,
843
+ 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,
844
+ now: site.pagePaths.has('/') ? {title: site.homeTitle ?? ''} : null,
845
+ move: homeMove,
846
+ done: homeRec ? {how: homeRec.how || 'page', previous: homeRec.previous || '', source: homeRec.source || ''} : null,
847
+ changed: homeChanged,
848
+ latest: {offered: latestOffered, on: Boolean(latestOffered && opt.latestPosts), target: latestRow?.target || '', action: latestRow?.action || ''},
849
+ postsPage: postsCandidate ? {
850
+ wpId: postsCandidate.id, title: postsCandidate.title || '(untitled)', oldUrl: postsCandidate.link,
851
+ confirmed: Boolean(postsPageId), to: opt.posts === 'blog' ? blogBase : (postsRow?.postsIndex ? postsRow.target : pagesBase)
852
+ } : null
853
+ };
854
+ }
704
855
  function docRow(it, key, kind, oldPath) {
705
856
  return {
706
857
  key, kind, wpId: it.id, title: it.title || `(untitled ${kind} ${it.id})`, oldUrl: it.link, oldPath,