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.
- package/CHANGELOG.md +9 -0
- package/CLAUDE.md +15 -0
- 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 +17 -0
- package/admin/js/views/wordpress-import.js +12 -10
- package/docs/cache.md +2 -0
- package/package.json +1 -1
- package/server/routes/api/wordpress-import.js +4 -1
- package/server/routes/docs-public.js +9 -3
- package/server/routes/public.js +3 -4
- package/server/services/content.js +6 -2
- package/server/services/docs.js +7 -3
- package/server/services/seo/index.js +3 -2
- package/server/services/userRoles.js +15 -0
- package/server/services/wpImport/front.js +82 -0
- package/server/services/wpImport/index.js +171 -17
- package/server/services/wpImport/plan.js +152 -1
|
@@ -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
|
-
|
|
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,
|