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.
- package/CHANGELOG.md +10 -0
- package/CLAUDE.md +27 -0
- package/admin/js/lib/seo-redirects.js +1 -1
- package/admin/js/lib/wordpress-import-arrange.js +1 -1
- package/admin/js/templates/docs/usage-tools.html +8 -0
- package/admin/js/templates/wordpress-import.html +27 -0
- package/admin/js/views/wordpress-import.js +12 -10
- package/package.json +1 -1
- package/server/routes/api/wordpress-import.js +4 -1
- package/server/services/content.js +6 -2
- package/server/services/seo/index.js +38 -4
- package/server/services/wpImport/front.js +82 -0
- package/server/services/wpImport/index.js +199 -17
- package/server/services/wpImport/plan.js +188 -1
|
@@ -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
|
-
|
|
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) {
|