@morphemecms/cms 0.11.0 → 0.13.0-next-20261106203613
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 +31 -0
- package/astro.mjs +2 -1
- package/defaults/blockTexts.ts +9 -9
- package/defaults/collections.ts +257 -257
- package/defaults/colors.ts +74 -74
- package/defaults/company.ts +9 -9
- package/defaults/editor.ts +65 -65
- package/defaults/i18n/da-DK/blockTexts.ts +12 -0
- package/defaults/i18n/da-DK/collections.ts +220 -0
- package/defaults/i18n/da-DK/colors.ts +102 -0
- package/defaults/i18n/da-DK/company.ts +7 -0
- package/defaults/i18n/da-DK/editor.ts +13 -0
- package/defaults/i18n/da-DK/index.ts +31 -0
- package/defaults/i18n/da-DK/internalLinks.ts +12 -0
- package/defaults/i18n/da-DK/opens.ts +7 -0
- package/defaults/i18n/da-DK/popup.ts +9 -0
- package/defaults/i18n/da-DK/privacy.ts +38 -0
- package/defaults/i18n/da-DK/rules.ts +56 -0
- package/defaults/i18n/da-DK/sitedata.ts +8 -0
- package/defaults/i18n/da-DK/standard.ts +8 -0
- package/defaults/i18n/da-DK/standardPages.ts +17 -0
- package/defaults/internalLinks.ts +8 -8
- package/defaults/opens.ts +2 -2
- package/defaults/popup.tsx +7 -7
- package/defaults/privacy.ts +37 -37
- package/defaults/rules.ts +68 -66
- package/defaults/schema.ts +1 -1
- package/defaults/sitedata.ts +4 -4
- package/defaults/standard.ts +9 -6
- package/defaults/standardPages.ts +14 -14
- package/defaults/texts.ts +7 -510
- package/lib/appId.d.mts +1 -0
- package/lib/appId.mjs +6 -0
- package/lib/appRoute.d.mts +2 -2
- package/lib/appRoute.mjs +3 -3
- package/lib/appRoute.test.mjs +8 -8
- package/lib/apps.mjs +13 -3
- package/lib/apps.test.mjs +10 -1
- package/lib/contracts.d.mts +3 -2
- package/lib/contracts.mjs +3 -2
- package/lib/folderChanges.d.mts +29 -0
- package/lib/folderChanges.mjs +34 -0
- package/lib/licenses.mjs +2 -2
- package/lib/licenses.owner.test.mjs +9 -0
- package/lib/menu.mjs +4 -4
- package/lib/menu.test.mjs +101 -101
- package/lib/scheduled.mjs +1 -1
- package/lib/section-ranges.d.mts +1 -1
- package/lib/section-ranges.mjs +8 -14
- package/lib/section-ranges.test.mjs +26 -5
- package/lib/storeEntries.d.mts +3 -1
- package/lib/storeEntries.mjs +22 -8
- package/lib/storeEntries.test.mjs +59 -7
- package/lib/text-sections.d.mts +11 -1
- package/lib/text-sections.mjs +18 -8
- package/lib/text-sections.test.mjs +34 -1
- package/lib/translate.d.mts +11 -0
- package/lib/translate.mjs +13 -1
- package/lib/translate.test.mjs +23 -2
- package/lib/video-id.d.mts +6 -2
- package/lib/video-id.mjs +5 -1
- package/lib/video.d.mts +6 -2
- package/lib/video.mjs +5 -1
- package/lib/without-h1.d.mts +1 -0
- package/lib/without-h1.mjs +19 -4
- package/lib/without-h1.test.mjs +30 -1
- package/package.json +3 -2
- package/scripts/app-screenshots.mjs +9 -8
- package/scripts/check-content.mjs +12 -2
- package/scripts/check-language.mjs +0 -1
- package/scripts/check-site-types.mjs +26 -0
- package/scripts/editor-screens.mjs +3 -3
- package/scripts/site-types.mjs +14 -0
- package/scripts/site-types.test.mjs +21 -0
- package/site/activeTheme.test.mjs +52 -0
- package/site/appSelf.d.mts +2 -0
- package/site/appSelf.mjs +23 -0
- package/site/appSelf.test.mjs +35 -0
- package/site/content.ts +5 -2
- package/site/defineSite.mjs +2 -2
- package/site/schema.ts +3 -3
- package/site/siteModules.mjs +2 -1
- package/site/theme.mjs +19 -8
- package/src/Advanced.tsx +2 -2
- package/src/App.tsx +79 -78
- package/src/AppAdds.tsx +1 -1
- package/src/AppList.tsx +39 -24
- package/src/BlockDialog.tsx +39 -18
- package/src/BlockParts.tsx +6 -6
- package/src/BlockSettings.tsx +2 -2
- package/src/BrandForm.tsx +2 -2
- package/src/Colors.tsx +6 -6
- package/src/CompanyForm.tsx +1 -1
- package/src/Consents.tsx +3 -3
- package/src/CreateFab.tsx +8 -0
- package/src/DataForm.tsx +10 -20
- package/src/Deleted.tsx +3 -3
- package/src/EditorFooter.tsx +2 -2
- package/src/EntryForm.tsx +3 -3
- package/src/FooterSettings.tsx +2 -2
- package/src/FormFrame.tsx +4 -3
- package/src/HeaderSettings.tsx +3 -3
- package/src/Keywords.tsx +5 -5
- package/src/Library.tsx +75 -82
- package/src/Licenses.tsx +8 -8
- package/src/ListFab.tsx +13 -0
- package/src/MainDrawerParts.tsx +3 -3
- package/src/MainMenu.tsx +1 -1
- package/src/MarkdownEditor.tsx +88 -47
- package/src/Media.tsx +11 -20
- package/src/MediaTags.tsx +2 -2
- package/src/Notifications.tsx +2 -2
- package/src/Parts.tsx +17 -17
- package/src/Redirects.tsx +14 -22
- package/src/Screen.tsx +16 -6
- package/src/SeoScores.tsx +6 -6
- package/src/SideColumn.tsx +2 -2
- package/src/StartFab.tsx +6 -8
- package/src/ThemeStore.tsx +37 -16
- package/src/Tokens.tsx +2 -2
- package/src/apps.ts +7 -8
- package/src/checkSite.ts +86 -20
- package/src/drawerYield.d.mts +3 -0
- package/src/drawerYield.mjs +10 -0
- package/src/drawerYield.test.mjs +22 -0
- package/src/editor.css +59 -38
- package/src/examples.ts +1 -1
- package/src/fabScreens.test.mjs +28 -0
- package/src/fabScreens.ts +16 -0
- package/src/i18n/da-DK/library.ts +2 -2
- package/src/i18n/da-DK/text.ts +1 -1
- package/src/i18n/en-US/apps.ts +1 -1
- package/src/i18n/en-US/common.ts +8 -8
- package/src/i18n/en-US/library.ts +5 -5
- package/src/i18n/en-US/parts.ts +3 -3
- package/src/i18n/en-US/text.ts +1 -1
- package/src/i18n/index.ts +4 -8
- package/src/images.ts +2 -2
- package/src/legacyMove.test.mjs +137 -0
- package/src/legacyMove.ts +131 -0
- package/src/licenseData.ts +3 -3
- package/src/markdown.mjs +10 -7
- package/src/markdown.test.mjs +16 -2
- package/src/menuChildren.ts +5 -5
- package/src/menuOpen.ts +20 -4
- package/src/movedRoutes.d.mts +8 -1
- package/src/movedRoutes.mjs +102 -21
- package/src/movedRoutes.test.mjs +128 -6
- package/src/nav.ts +1 -1
- package/src/notificationData.ts +1 -1
- package/src/pageH1Panel.d.mts +4 -0
- package/src/pageH1Panel.mjs +31 -0
- package/src/pageH1Panel.test.mjs +56 -0
- package/src/pagePreview.tsx +22 -12
- package/src/schema.ts +2 -2
- package/src/sectionFrame.ts +3 -3
- package/src/sectionState.d.mts +3 -3
- package/src/sectionState.mjs +6 -6
- package/src/sectionState.test.mjs +14 -2
- package/src/sheetPlacement.d.mts +6 -4
- package/src/sheetPlacement.mjs +15 -10
- package/src/sheetPlacement.test.mjs +16 -9
- package/src/sidePanel.ts +38 -0
- package/src/site.ts +37 -13
- package/src/standard/accordion/index.tsx +1 -1
- package/src/standard/accordion/shared.ts +1 -1
- package/src/standard/buttons/index.tsx +1 -1
- package/src/standard/cards/index.tsx +1 -1
- package/src/standard/cards/shared.ts +1 -1
- package/src/standard/cookies/index.tsx +1 -1
- package/src/standard/posts/index.tsx +1 -1
- package/src/standard/testimonials/index.tsx +1 -1
- package/src/standard/testimonials/list.ts +1 -1
- package/src/store.ts +36 -31
- package/src/tagUses.d.mts +1 -1
- package/src/tagUses.mjs +2 -2
- package/src/tagUses.test.mjs +1 -1
- package/src/topBarSlot.tsx +7 -15
- package/src/ui.tsx +33 -22
- package/src/virtual.d.ts +6 -0
- package/starter/package.json +1 -1
- package/theme/components/PageHeading.astro +22 -0
- package/theme/layouts/Base.astro +2 -2
- package/theme/layouts/PageLayout.astro +37 -20
- package/theme/styles/global.css +2 -1
- package/theme/styles/ui.css +40 -10
package/defaults/privacy.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
// "
|
|
2
|
-
// at #
|
|
3
|
-
// "
|
|
4
|
-
// same file, config.standardPages; standardPages.ts), and "
|
|
1
|
+
// "Privacy": what the site asks and tells its visitors about their data, as screens of their own (CompanyForm.tsx),
|
|
2
|
+
// at #privacy/<id>, and the cookie list's row: the cookie banner (src/data/cookies.json, read by packages/editor/theme/components/CookieConsent.astro),
|
|
3
|
+
// "Privacy pages", which pages are the privacy policy and the cookie policy, as a list like "Default pages" (in the
|
|
4
|
+
// same file, config.standardPages; standardPages.ts), and "Consents", the visitors' answers in the banner.
|
|
5
5
|
import { Cookie, FileText, ListChecks } from '@phosphor-icons/react';
|
|
6
6
|
import type { CookieCategory } from '../lib/cookieCategories.mjs';
|
|
7
7
|
import type { Field, PartScreen } from '../src/schema';
|
|
@@ -9,74 +9,74 @@ import { standardPageFieldsOf } from '@site/collections';
|
|
|
9
9
|
import { config } from '@site/config';
|
|
10
10
|
|
|
11
11
|
/** The fields of a category of cookies in the banner: its name and what it is for. */
|
|
12
|
-
function categoryFields(category: CookieCategory, place: string, hint = '
|
|
12
|
+
function categoryFields(category: CookieCategory, place: string, hint = 'Shown when an app asks for it.'): Field[] {
|
|
13
13
|
return [
|
|
14
|
-
{ name: category, label: `
|
|
15
|
-
{ name: `${category}Text`, label: `
|
|
14
|
+
{ name: category, label: `Name (${place})`, type: 'string', required: true, max: 30, hint },
|
|
15
|
+
{ name: `${category}Text`, label: `Text (${place})`, type: 'text', required: true },
|
|
16
16
|
];
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
export const privacy: PartScreen = {
|
|
20
|
-
id: '
|
|
21
|
-
label: '
|
|
20
|
+
id: 'privacy',
|
|
21
|
+
label: 'Privacy',
|
|
22
22
|
parts: [
|
|
23
23
|
{
|
|
24
|
-
id: '
|
|
25
|
-
label: '
|
|
24
|
+
id: 'cookie-banner',
|
|
25
|
+
label: 'Cookie banner',
|
|
26
26
|
icon: Cookie,
|
|
27
27
|
file: 'src/data/cookies.json',
|
|
28
28
|
fields: [
|
|
29
29
|
{
|
|
30
30
|
name: 'enabled',
|
|
31
|
-
label: '
|
|
31
|
+
label: 'Activated',
|
|
32
32
|
type: 'boolean',
|
|
33
|
-
toggle: ['
|
|
34
|
-
hint: '
|
|
35
|
-
edit: { to: 'src/content/pages/privatlivspolitik.md', text: '
|
|
33
|
+
toggle: ['On', 'Off'],
|
|
34
|
+
hint: 'When the banner is off, there is no analytics or ads.',
|
|
35
|
+
edit: { to: 'src/content/pages/privatlivspolitik.md', text: 'Edit privacy policy' },
|
|
36
36
|
},
|
|
37
|
-
{ name: 'heading', label: '
|
|
38
|
-
{ name: 'text', label: '
|
|
37
|
+
{ name: 'heading', label: 'Heading', type: 'string', required: true },
|
|
38
|
+
{ name: 'text', label: 'Text', type: 'text', required: true },
|
|
39
39
|
{
|
|
40
40
|
name: 'more',
|
|
41
|
-
label: '
|
|
41
|
+
label: 'Link text',
|
|
42
42
|
type: 'string',
|
|
43
43
|
required: true,
|
|
44
|
-
hint: '
|
|
45
|
-
edit: { to: '
|
|
44
|
+
hint: 'The link goes to the cookie policy.',
|
|
45
|
+
edit: { to: 'privacy/privacy-pages', text: 'Edit privacy pages' },
|
|
46
46
|
},
|
|
47
|
-
{ name: 'no', label: '
|
|
48
|
-
{ name: 'yes', label: '
|
|
49
|
-
{ name: 'choose', label: '
|
|
50
|
-
{ name: 'save', label: '
|
|
47
|
+
{ name: 'no', label: 'Button text (no)', type: 'string', required: true, max: 20 },
|
|
48
|
+
{ name: 'yes', label: 'Button text (yes)', type: 'string', required: true, max: 20 },
|
|
49
|
+
{ name: 'choose', label: 'Button text (advanced)', type: 'string', required: true, max: 30 },
|
|
50
|
+
{ name: 'save', label: 'Button text (save choice)', type: 'string', required: true, max: 30 },
|
|
51
51
|
{
|
|
52
52
|
name: 'consentId',
|
|
53
|
-
label: '
|
|
53
|
+
label: 'Text (consent ID)',
|
|
54
54
|
type: 'string',
|
|
55
55
|
required: true,
|
|
56
56
|
max: 30,
|
|
57
|
-
hint: '
|
|
57
|
+
hint: 'Shown with the visitor’s ID after an answer.',
|
|
58
58
|
},
|
|
59
|
-
// Each category of cookies, with what it is for, when the banner's "
|
|
59
|
+
// Each category of cookies, with what it is for, when the banner's "Advanced" shows them (the necessary ones
|
|
60
60
|
// always, the others when an app asks about them, packages/editor/site/consent.ts).
|
|
61
|
-
...categoryFields('necessary', '
|
|
62
|
-
...categoryFields('preferences', '
|
|
63
|
-
...categoryFields('statistics', '
|
|
61
|
+
...categoryFields('necessary', 'necessary', 'Always shown; they cannot be switched off.'),
|
|
62
|
+
...categoryFields('preferences', 'preferences'),
|
|
63
|
+
...categoryFields('statistics', 'statistics'),
|
|
64
64
|
...categoryFields('marketing', 'marketing'),
|
|
65
|
-
...categoryFields('targeting', '
|
|
65
|
+
...categoryFields('targeting', 'targeted ads'),
|
|
66
66
|
],
|
|
67
67
|
},
|
|
68
|
-
// The site's list of cookies ("Cookies", the standard block "
|
|
69
|
-
// address space (#
|
|
70
|
-
{ id: 'cookies', label: 'Cookies', icon: Cookie, href: `#
|
|
68
|
+
// The site's list of cookies ("Cookies", the standard block "Cookie list"), a screen of its own under "Lists"'s
|
|
69
|
+
// address space (#list/<id>): only its row is here, and the list goes back to this screen (Library.tsx).
|
|
70
|
+
{ id: 'cookies', label: 'Cookies', icon: Cookie, href: `#list/${config.lists.cookies}` },
|
|
71
71
|
{
|
|
72
|
-
id: '
|
|
73
|
-
label: '
|
|
72
|
+
id: 'privacy-pages',
|
|
73
|
+
label: 'Privacy pages',
|
|
74
74
|
icon: FileText,
|
|
75
75
|
file: config.standardPages,
|
|
76
76
|
fields: standardPageFieldsOf('privacy', 'cookies'),
|
|
77
77
|
rows: true,
|
|
78
78
|
},
|
|
79
79
|
// The visitors' answers in the banner, kept by the Worker (packages/editor/worker/consent.js): the editor's own screen (Consents.tsx).
|
|
80
|
-
{ id: '
|
|
80
|
+
{ id: 'consents', label: 'Consents', icon: ListChecks, opens: 'consents' },
|
|
81
81
|
],
|
|
82
82
|
};
|
package/defaults/rules.ts
CHANGED
|
@@ -20,7 +20,7 @@ const fm = (file: RepoFile) => readFields(splitFrontmatter(file.text).yaml);
|
|
|
20
20
|
// A page's address: `path`, or /<file name>/ for pages made in the editor.
|
|
21
21
|
const pagePath = (f: RepoFile) => String(fm(f).path ?? `/${slugOf(f.path, config.folders.pages)}/`);
|
|
22
22
|
const isPage = (f: RepoFile) => f.path.startsWith(`${config.folders.pages}/`) && f.path.endsWith('.md');
|
|
23
|
-
// The addresses of the standard pages, by their key ("
|
|
23
|
+
// The addresses of the standard pages, by their key ("Settings" › "Appearance" › "Default pages", standardPages.ts).
|
|
24
24
|
const standardOf = (files: { path: string; text: string }[]) => standardPaths(files.find((f) => f.path === config.standardPages)?.text);
|
|
25
25
|
// The page that is a standard page (the front page: 'front'), if it is there.
|
|
26
26
|
const standardFile = (files: RepoFile[], key: string) => {
|
|
@@ -64,7 +64,7 @@ export function relationsOf(files: RepoFile[]): Record<Relation, Option[]> {
|
|
|
64
64
|
return {
|
|
65
65
|
// Pages are stored by their address: `path`, or /<file name>/ for pages made in the editor.
|
|
66
66
|
pages: inFolder(config.folders.pages, '.md').map(page).sort(byLabel),
|
|
67
|
-
// The pages that can be a standard page ("
|
|
67
|
+
// The pages that can be a standard page ("Settings" › "Appearance" › "Default pages"): those on the site, not a
|
|
68
68
|
// draft, one waiting for its time or a new version of a page (/ny/).
|
|
69
69
|
standardPages: inFolder(config.folders.pages, '.md')
|
|
70
70
|
.filter((f) => !['draft', 'scheduled'].includes(String(fm(f).status ?? '')) && !pagePath(f).startsWith('/ny/'))
|
|
@@ -72,7 +72,7 @@ export function relationsOf(files: RepoFile[]): Record<Relation, Option[]> {
|
|
|
72
72
|
.sort(byLabel),
|
|
73
73
|
// The front page (/) and the blog (/blog/), whichever pages they are, the blog by its page's title.
|
|
74
74
|
fixed: [
|
|
75
|
-
{ value: '/', label: siteText('
|
|
75
|
+
{ value: '/', label: siteText('Front page'), inMenu: menu.has('/') },
|
|
76
76
|
{
|
|
77
77
|
value: '/blog/',
|
|
78
78
|
label: String((standardFile(files, 'blog') && fm(standardFile(files, 'blog')!).title) || siteText('Blog')),
|
|
@@ -93,9 +93,9 @@ export function relationsOf(files: RepoFile[]): Record<Relation, Option[]> {
|
|
|
93
93
|
testimonials: inFolder('src/data/testimonials', '.json')
|
|
94
94
|
.map((f) => ({ value: slugOf(f.path, 'src/data/testimonials'), label: String(json(f).name) }))
|
|
95
95
|
.sort(byLabel),
|
|
96
|
-
// The promotions turned on that a button can open ("
|
|
97
|
-
// the promotion's name ("
|
|
98
|
-
// page) and the button's text: a pop-up's heading, or that of the target it stands for ("Book
|
|
96
|
+
// The promotions turned on that a button can open ("Buttons": "Open promotion", opens.ts), by what opens with
|
|
97
|
+
// the promotion's name ("Booking sheet (Calendly booking sheet)"), where they are shown (a button offers those on its
|
|
98
|
+
// page) and the button's text: a pop-up's heading, or that of the target it stands for ("Book an appointment").
|
|
99
99
|
promotions: inFolder('src/data/popups', '.json')
|
|
100
100
|
.flatMap((f) => {
|
|
101
101
|
const d = json(f);
|
|
@@ -112,7 +112,7 @@ export function relationsOf(files: RepoFile[]): Record<Relation, Option[]> {
|
|
|
112
112
|
];
|
|
113
113
|
})
|
|
114
114
|
.sort(byLabel),
|
|
115
|
-
// The PDFs in "
|
|
115
|
+
// The PDFs in "Files", by their address on the site (/filer/<name>.pdf), named by their file name.
|
|
116
116
|
files: inFolder(config.folders.files, '.pdf')
|
|
117
117
|
.map((f) => ({ value: f.path.replace(/^public/, ''), label: f.path.split('/').pop()! }))
|
|
118
118
|
.sort(byLabel),
|
|
@@ -121,16 +121,16 @@ export function relationsOf(files: RepoFile[]): Record<Relation, Option[]> {
|
|
|
121
121
|
|
|
122
122
|
const frontPage = (files: RepoFile[]) => ({
|
|
123
123
|
to: standardFile(files, 'front')?.path ?? config.standardPages,
|
|
124
|
-
text: siteText('
|
|
124
|
+
text: siteText('Edit the front page'),
|
|
125
125
|
});
|
|
126
126
|
|
|
127
127
|
// Where a button that opens a promotion is: a page (`path`), the shared buttons (on any page: neither), or an action bar
|
|
128
128
|
// (`bar`: the pages it is shown on, or none for every page or the posts with its tags).
|
|
129
129
|
type Opener = { file: string; title: string; path?: string; bar?: { pages?: string[] } };
|
|
130
130
|
|
|
131
|
-
// The buttons that open a promotion ("
|
|
131
|
+
// The buttons that open a promotion ("Buttons": "Open promotion", the button's `open`), where they are: the page with
|
|
132
132
|
// the block (its file, title and the address it is shown at: the front page's /), the shared buttons
|
|
133
|
-
// ("
|
|
133
|
+
// ("Standard blocks" › "Buttons", on any page: no address), or an action bar ("Promotion").
|
|
134
134
|
function openersOf(slug: string, files: { path: string; text: string }[]): Opener[] {
|
|
135
135
|
const opens = (items: unknown) =>
|
|
136
136
|
Array.isArray(items) &&
|
|
@@ -143,7 +143,7 @@ function openersOf(slug: string, files: { path: string; text: string }[]): Opene
|
|
|
143
143
|
const found: Opener[] = [];
|
|
144
144
|
for (const f of files) {
|
|
145
145
|
if (f.path.startsWith(`${config.folders.pages}/`) && f.path.endsWith('.md')) {
|
|
146
|
-
// Also a "
|
|
146
|
+
// Also a "Buttons" block in a block's text (a "Banner"'s), where its quotes are written as ".
|
|
147
147
|
if (!/data-block=(?:"|\\")buttons/.test(f.text)) continue;
|
|
148
148
|
const d = fm(f as RepoFile);
|
|
149
149
|
if (
|
|
@@ -155,12 +155,12 @@ function openersOf(slug: string, files: { path: string; text: string }[]): Opene
|
|
|
155
155
|
if (d.type === 'bookingbar' && d.enabled !== false && opens(d.buttons))
|
|
156
156
|
found.push({
|
|
157
157
|
file: f.path,
|
|
158
|
-
title: siteText('
|
|
158
|
+
title: siteText('Action bar'),
|
|
159
159
|
bar: { pages: d.where === 'all' || d.where === 'tags' ? undefined : Array.isArray(d.pages) ? d.pages.map(String) : [] },
|
|
160
160
|
});
|
|
161
161
|
} else if (f.path === config.blockTexts) {
|
|
162
162
|
if (opens((json(f as RepoFile).buttons as Record<string, unknown> | undefined)?.items))
|
|
163
|
-
found.push({ file: '
|
|
163
|
+
found.push({ file: 'blocks/buttons', title: siteText('Default buttons') });
|
|
164
164
|
}
|
|
165
165
|
}
|
|
166
166
|
return found;
|
|
@@ -180,20 +180,20 @@ function shownFor(d: Record<string, unknown>, at: Opener) {
|
|
|
180
180
|
const openerText = (at: Opener) =>
|
|
181
181
|
at.bar
|
|
182
182
|
? siteText(
|
|
183
|
-
'
|
|
183
|
+
'A button in the action bar opens the promotion, so it must be turned on and shown everywhere the bar is shown. Change the button first.',
|
|
184
184
|
)
|
|
185
185
|
: at.path
|
|
186
|
-
? siteText('
|
|
186
|
+
? siteText('A button on the page "{side}" opens the promotion, so it must be on and shown there. Change the button first.', {
|
|
187
187
|
side: at.title,
|
|
188
188
|
})
|
|
189
|
-
: siteText('
|
|
189
|
+
: siteText('The default buttons open the promotion, so it must be on and shown on every page. Change them first.');
|
|
190
190
|
const openerEdit = (at: Opener) => ({
|
|
191
191
|
to: at.file,
|
|
192
|
-
text: siteText(at.bar ? '
|
|
192
|
+
text: siteText(at.bar ? 'Edit the action bar' : at.path ? 'Edit the page' : 'Edit the default buttons'),
|
|
193
193
|
});
|
|
194
194
|
|
|
195
195
|
/** Why a promotion cannot be saved like this (turned off, or no longer shown on a page where a button opens it,
|
|
196
|
-
* "
|
|
196
|
+
* "Buttons": "Open promotion"), with a link to the button, or the standard pages (the same page twice, or a page
|
|
197
197
|
* without the block it must have, with a link to it), or null. Before it is saved (DataForm.tsx; the standard pages in
|
|
198
198
|
* CompanyForm.tsx, without a collection). */
|
|
199
199
|
export function keptBy(collection: Collection | null, path: string, data: Record<string, unknown>, files: RepoFile[]): Status {
|
|
@@ -208,18 +208,18 @@ export function keptBy(collection: Collection | null, path: string, data: Record
|
|
|
208
208
|
function standardKept(data: Record<string, unknown>, files: RepoFile[]): Status {
|
|
209
209
|
const chosen = standardPages.map((p) => ({ ...p, path: String(data[p.key] ?? '') })).filter((p) => p.path);
|
|
210
210
|
if (!differentPages(Object.fromEntries(chosen.map((p) => [p.key, p.path]))))
|
|
211
|
-
return { text: siteText('
|
|
211
|
+
return { text: siteText('The default pages must be different pages.'), error: true };
|
|
212
212
|
for (const p of chosen) {
|
|
213
213
|
const page = files.find((f) => isPage(f) && pagePath(f) === p.path);
|
|
214
214
|
if (!p.needs || !page || blocksIn(page.text).some((b: { type: string }) => b.type === p.needs!.type)) continue;
|
|
215
215
|
return {
|
|
216
|
-
text: siteText('
|
|
216
|
+
text: siteText('The page "{side}" does not have the block {blok}, which {hvad} must have. Put it in on the page first.', {
|
|
217
217
|
side: String(fm(page).title),
|
|
218
218
|
blok: siteText(p.needs.label),
|
|
219
219
|
hvad: siteText(p.the).toLowerCase(),
|
|
220
220
|
}),
|
|
221
221
|
error: true,
|
|
222
|
-
edit: { to: page.path, text: siteText('
|
|
222
|
+
edit: { to: page.path, text: siteText('Edit the page') },
|
|
223
223
|
};
|
|
224
224
|
}
|
|
225
225
|
return null;
|
|
@@ -230,16 +230,16 @@ function standardKept(data: Record<string, unknown>, files: RepoFile[]): Status
|
|
|
230
230
|
* shown on its page. */
|
|
231
231
|
export function siteProblems(files: { path: string; text: string }[]): string[] {
|
|
232
232
|
const problems: string[] = [];
|
|
233
|
-
// The standard pages are pages on the site ("
|
|
233
|
+
// The standard pages are pages on the site ("Settings" › "Appearance" › "Default pages"), different ones (but for
|
|
234
234
|
// the cookie policy, which may be the privacy policy).
|
|
235
235
|
if (files.some((f) => f.path === config.standardPages)) {
|
|
236
236
|
for (const p of standardPages) {
|
|
237
237
|
const page = standardFile(files as RepoFile[], p.key);
|
|
238
238
|
if (!page || ['draft', 'scheduled'].includes(String(fm(page).status ?? '')))
|
|
239
|
-
problems.push(`${config.standardPages}: ${siteText('{side}
|
|
239
|
+
problems.push(`${config.standardPages}: ${siteText('{side} must be a published page.', { side: siteText(p.the) })}`);
|
|
240
240
|
}
|
|
241
241
|
if (!differentPages(standardOf(files)))
|
|
242
|
-
problems.push(`${config.standardPages}: ${siteText('
|
|
242
|
+
problems.push(`${config.standardPages}: ${siteText('The default pages must be different pages.')}`);
|
|
243
243
|
}
|
|
244
244
|
const promotions = files.filter((f) => f.path.startsWith('src/data/popups/') && f.path.endsWith('.json'));
|
|
245
245
|
const slugs = new Set<string>();
|
|
@@ -249,8 +249,8 @@ export function siteProblems(files: { path: string; text: string }[]): string[]
|
|
|
249
249
|
for (const at of openersOf(slug, files)) {
|
|
250
250
|
if (file && shownFor(json(file as RepoFile), at)) continue;
|
|
251
251
|
problems.push(
|
|
252
|
-
`${at.file.startsWith('
|
|
253
|
-
file ? openerText(at) : siteText('
|
|
252
|
+
`${at.file.startsWith('blocks/') ? config.blockTexts : at.file}: ${
|
|
253
|
+
file ? openerText(at) : siteText('A button opens the promotion "{navn}", which does not exist.', { navn: slug })
|
|
254
254
|
}`,
|
|
255
255
|
);
|
|
256
256
|
}
|
|
@@ -272,16 +272,16 @@ const TAG_FOLDERS: TagFolders = {
|
|
|
272
272
|
export function usedBy(collection: Collection, file: RepoFile, files: RepoFile[]): Status {
|
|
273
273
|
if (collection.kind === 'files') return null;
|
|
274
274
|
const slug = slugOf(file.path, collection.folder);
|
|
275
|
-
// A promotion a button opens ("
|
|
275
|
+
// A promotion a button opens ("Buttons": "Open promotion").
|
|
276
276
|
if (collection.id === 'popups') {
|
|
277
277
|
const [at] = openersOf(slug, files);
|
|
278
278
|
if (at)
|
|
279
279
|
return {
|
|
280
280
|
text: at.bar
|
|
281
|
-
? siteText('
|
|
281
|
+
? siteText('A button in the action bar opens the promotion. Change the button first.')
|
|
282
282
|
: at.path
|
|
283
|
-
? siteText('
|
|
284
|
-
: siteText('
|
|
283
|
+
? siteText('A button on the page "{side}" opens the promotion. Change the button first.', { side: at.title })
|
|
284
|
+
: siteText('The default buttons open the promotion. Change them first.'),
|
|
285
285
|
error: true,
|
|
286
286
|
edit: openerEdit(at),
|
|
287
287
|
};
|
|
@@ -290,69 +290,71 @@ export function usedBy(collection: Collection, file: RepoFile, files: RepoFile[]
|
|
|
290
290
|
const front = Object.fromEntries(
|
|
291
291
|
(home ? (blocksIn(home.text) as { type: string; props: Record<string, unknown> }[]) : []).map((b) => [b.type, b.props]),
|
|
292
292
|
);
|
|
293
|
-
if (collection.id === '
|
|
293
|
+
if (collection.id === 'tags') {
|
|
294
294
|
const { posts, pages, testimonials: quotes, records } = taggedWith(files, slug, TAG_FOLDERS);
|
|
295
295
|
if (posts.length)
|
|
296
296
|
return {
|
|
297
297
|
text:
|
|
298
298
|
posts.length === 1
|
|
299
|
-
? siteText('
|
|
300
|
-
: siteText('
|
|
299
|
+
? siteText('The tag is used on a blog post. Remove it from the post first.')
|
|
300
|
+
: siteText('The tag is used on {antal} blog posts. Remove it from them first.', { antal: posts.length }),
|
|
301
301
|
error: true,
|
|
302
302
|
};
|
|
303
303
|
if (pages.length)
|
|
304
304
|
return {
|
|
305
305
|
text:
|
|
306
306
|
pages.length === 1
|
|
307
|
-
? siteText('
|
|
308
|
-
: siteText('
|
|
307
|
+
? siteText('The tag is used on a page. Remove it from the page first.')
|
|
308
|
+
: siteText('The tag is used on {antal} pages. Remove it from them first.', { antal: pages.length }),
|
|
309
309
|
error: true,
|
|
310
|
-
...(pages.length === 1 ? { edit: { to: pages[0].path, text: siteText('
|
|
310
|
+
...(pages.length === 1 ? { edit: { to: pages[0].path, text: siteText('Edit the page') } } : {}),
|
|
311
311
|
};
|
|
312
312
|
if (quotes.length)
|
|
313
313
|
return {
|
|
314
314
|
text:
|
|
315
315
|
quotes.length === 1
|
|
316
|
-
? siteText('
|
|
317
|
-
: siteText('
|
|
316
|
+
? siteText('The tag is used on a testimonial. Remove it from the testimonial first.')
|
|
317
|
+
: siteText('The tag is used on {antal} testimonials. Remove it from them first.', { antal: quotes.length }),
|
|
318
318
|
error: true,
|
|
319
319
|
};
|
|
320
320
|
if (records.length)
|
|
321
321
|
return {
|
|
322
322
|
text:
|
|
323
323
|
records.length === 1
|
|
324
|
-
? siteText('
|
|
325
|
-
: siteText('
|
|
324
|
+
? siteText('The tag is used on a file. Remove it from the file first.')
|
|
325
|
+
: siteText('The tag is used on {antal} files. Remove it from them first.', { antal: records.length }),
|
|
326
326
|
error: true,
|
|
327
|
-
...(records.length === 1 ? { edit: { to: recordUse(records[0], config.fileRecords)!.path, text: siteText('
|
|
327
|
+
...(records.length === 1 ? { edit: { to: recordUse(records[0], config.fileRecords)!.path, text: siteText('Open the file') } } : {}),
|
|
328
328
|
};
|
|
329
|
-
// A promotion shown on the posts with the tag (
|
|
329
|
+
// A promotion shown on the posts with the tag (Promotion, "Blog posts with tags").
|
|
330
330
|
const promotion = files.find(
|
|
331
331
|
(f) => f.path.startsWith('src/data/popups/') && f.path.endsWith('.json') && promotionTags(json(f)).includes(slug),
|
|
332
332
|
);
|
|
333
333
|
if (promotion) {
|
|
334
334
|
const d = json(promotion);
|
|
335
335
|
return {
|
|
336
|
-
text: siteText('
|
|
336
|
+
text: siteText('The tag is used in the promotion "{navn}". Remove it from the promotion first.', {
|
|
337
|
+
navn: String(d.heading ?? d.name ?? ''),
|
|
338
|
+
}),
|
|
337
339
|
error: true,
|
|
338
|
-
edit: { to: promotion.path, text: siteText('
|
|
340
|
+
edit: { to: promotion.path, text: siteText('Edit the promotion') },
|
|
339
341
|
};
|
|
340
342
|
}
|
|
341
343
|
const fp = front.posts as Record<string, unknown> | undefined;
|
|
342
344
|
if ((fp?.categories as string[] | undefined)?.includes(slug) || fp?.link === slug)
|
|
343
345
|
return {
|
|
344
|
-
text: siteText('
|
|
346
|
+
text: siteText('The tag picks the posts on the front page under "From the blog". Pick another one first.'),
|
|
345
347
|
error: true,
|
|
346
348
|
edit: frontPage(files),
|
|
347
349
|
};
|
|
348
350
|
}
|
|
349
351
|
if (collection.id === 'blog' && ((front.posts as Record<string, unknown>)?.picked as string[])?.includes(slug))
|
|
350
352
|
return {
|
|
351
|
-
text: siteText('
|
|
353
|
+
text: siteText('The post is picked for the front page under "From the blog". Pick another one first.'),
|
|
352
354
|
error: true,
|
|
353
355
|
edit: frontPage(files),
|
|
354
356
|
};
|
|
355
|
-
// A "
|
|
357
|
+
// A "Blog posts" block in a page that picks the post (its props are in the text).
|
|
356
358
|
if (collection.id === 'blog') {
|
|
357
359
|
const page = files.find(
|
|
358
360
|
(f) =>
|
|
@@ -360,28 +362,28 @@ export function usedBy(collection: Collection, file: RepoFile, files: RepoFile[]
|
|
|
360
362
|
);
|
|
361
363
|
if (page)
|
|
362
364
|
return {
|
|
363
|
-
text: siteText('
|
|
365
|
+
text: siteText('The post is picked in a block of blog posts on the page "{side}". Pick another one first.', {
|
|
364
366
|
side: String(fm(page).title),
|
|
365
367
|
}),
|
|
366
368
|
error: true,
|
|
367
|
-
edit: { to: page.path, text: siteText('
|
|
369
|
+
edit: { to: page.path, text: siteText('Edit the page') },
|
|
368
370
|
};
|
|
369
371
|
}
|
|
370
|
-
// Any other place under "
|
|
372
|
+
// Any other place under "Used on" (usesOf): a "Blog posts" or "Testimonials" block that picks the tag, on a
|
|
371
373
|
// page or in a post. Files with the tag are handled above (records), so the first use is never a file record.
|
|
372
|
-
if (collection.id === '
|
|
374
|
+
if (collection.id === 'tags') {
|
|
373
375
|
const use = usesOf(collection, files)(file)[0];
|
|
374
376
|
if (use) {
|
|
375
377
|
const post = use.path.startsWith(`${config.folders.blog}/`);
|
|
376
378
|
return {
|
|
377
379
|
text: siteText(
|
|
378
380
|
post
|
|
379
|
-
? '
|
|
380
|
-
: '
|
|
381
|
+
? 'The tag is picked in a block in the blog post "{side}". Pick another one first.'
|
|
382
|
+
: 'The tag is picked in a block on the page "{side}". Pick another one first.',
|
|
381
383
|
{ side: use.name },
|
|
382
384
|
),
|
|
383
385
|
error: true,
|
|
384
|
-
edit: { to: use.path, text: siteText(post ? '
|
|
386
|
+
edit: { to: use.path, text: siteText(post ? 'Edit the post' : 'Edit the page') },
|
|
385
387
|
};
|
|
386
388
|
}
|
|
387
389
|
}
|
|
@@ -402,12 +404,12 @@ function nameOf(file: RepoFile): string {
|
|
|
402
404
|
|
|
403
405
|
export type Use = { path: string; name: string };
|
|
404
406
|
|
|
405
|
-
// The places on the site that use an entry, by the entry's file, each once (the tag's "
|
|
407
|
+
// The places on the site that use an entry, by the entry's file, each once (the tag's "Used on", DataForm.tsx,
|
|
406
408
|
// and the number at it in its list, Library.tsx); [] for none. For a tag: the blog posts, pages, testimonials,
|
|
407
|
-
// promotions and files (by the route "
|
|
408
|
-
// picks it (its tags, or the tag "
|
|
409
|
+
// promotions and files (by the route "files/<path>" and the file's name) that have it, and the pages and posts with a "Blog posts" or "Testimonials" block that
|
|
410
|
+
// picks it (its tags, or the tag "All posts" goes to). Made once for the whole list.
|
|
409
411
|
export function usesOf(collection: Collection, files: RepoFile[]): (file: RepoFile) => Use[] {
|
|
410
|
-
if (collection.kind === 'files' || collection.id !== '
|
|
412
|
+
if (collection.kind === 'files' || collection.id !== 'tags') return () => [];
|
|
411
413
|
const uses = tagUses(files, TAG_FOLDERS);
|
|
412
414
|
return (file) =>
|
|
413
415
|
[...(uses.get(slugOf(file.path, collection.folder))?.values() ?? [])]
|
|
@@ -418,7 +420,7 @@ export function usesOf(collection: Collection, files: RepoFile[]): (file: RepoFi
|
|
|
418
420
|
// The number at the right of an entry in the list of a part, by the entry's file, or null for none
|
|
419
421
|
// (Library.tsx): at each tag, the number of places that use it (usesOf).
|
|
420
422
|
export function countsOf(collection: Collection, files: RepoFile[]): (file: RepoFile) => number | null {
|
|
421
|
-
if (collection.kind === 'files' || collection.id !== '
|
|
423
|
+
if (collection.kind === 'files' || collection.id !== 'tags') return () => null;
|
|
422
424
|
const uses = tagUses(files, TAG_FOLDERS);
|
|
423
425
|
return (file) => uses.get(slugOf(file.path, collection.folder))?.size ?? 0;
|
|
424
426
|
}
|
|
@@ -430,19 +432,19 @@ export const testimonialsOf = (files: RepoFile[]) =>
|
|
|
430
432
|
.map((f) => ({ ...(json(f) as TestimonialRow & { order?: number }), slug: slugOf(f.path, 'src/data/testimonials') }))
|
|
431
433
|
.sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
|
|
432
434
|
|
|
433
|
-
/** What is at an address, as said in a message:
|
|
435
|
+
/** What is at an address, as said in a message: 'the front page', 'the page "About me" (/about-me/)',
|
|
434
436
|
* or the address (where a redirect sends to, App.tsx). */
|
|
435
437
|
export function whatIsAt(address: string, relations: Record<Relation, Option[]>): string {
|
|
436
438
|
const base = address.split('#')[0];
|
|
437
|
-
if (base === '/') return siteText('
|
|
438
|
-
if (base === '/blog/') return siteText('
|
|
439
|
+
if (base === '/') return siteText('the front page');
|
|
440
|
+
if (base === '/blog/') return siteText('the blog');
|
|
439
441
|
const page = [...relations.fixed, ...relations.pages].find((o) => o.value === base);
|
|
440
|
-
if (page) return siteText('
|
|
442
|
+
if (page) return siteText('the page "{navn}" ({adresse})', { navn: page.label, adresse: address });
|
|
441
443
|
const post = relations.blog.find((o) => `/${o.value}/` === base);
|
|
442
|
-
if (post) return siteText('
|
|
444
|
+
if (post) return siteText('the blog post "{navn}" ({adresse})', { navn: post.label, adresse: address });
|
|
443
445
|
const tag = relations.categories.find((o) => `/blog/kategori/${o.value}/` === base);
|
|
444
|
-
if (tag) return siteText('
|
|
446
|
+
if (tag) return siteText('the tag "{navn}" on the blog ({adresse})', { navn: tag.label, adresse: address });
|
|
445
447
|
if (base.startsWith(config.folders.media.replace(/^public/, '') + '/'))
|
|
446
|
-
return siteText('
|
|
448
|
+
return siteText('the picture {navn}', { navn: base.split('/').pop()! });
|
|
447
449
|
return address;
|
|
448
450
|
}
|
package/defaults/schema.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { config } from '@site/config';
|
|
|
5
5
|
import type { SiteSchema } from '../site/schema';
|
|
6
6
|
|
|
7
7
|
export const schema: SiteSchema = {
|
|
8
|
-
ids: { business: '
|
|
8
|
+
ids: { business: 'business', person: 'person' },
|
|
9
9
|
businessType: 'LocalBusiness',
|
|
10
10
|
country: config.locale.split('-')[1] ?? 'DK',
|
|
11
11
|
terms: {},
|
package/defaults/sitedata.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// What the previews of the blocks (src/apps/<app>/blocks/, previews.tsx) show from elsewhere on the site, from the files
|
|
2
2
|
// as the editor has them now: the pages (their titles and texts, for the topics and the services),
|
|
3
3
|
// the blog posts, the phone, e-mail and address (contact.json), the payment details (payment.json) and
|
|
4
|
-
// the texts of the "Book
|
|
4
|
+
// the texts of the "Book an appointment" box (booking.json), the settings of the apps (src/apps/) and the cookie list.
|
|
5
5
|
import type { RepoFile } from '../src/github';
|
|
6
6
|
import type { StandardSiteData } from '../src/standard/site';
|
|
7
7
|
import { categoryOf } from '../lib/cookieCategories.mjs';
|
|
@@ -30,13 +30,13 @@ export type SiteData = {
|
|
|
30
30
|
/** The bank details, and what else the site keeps in payment.json (a site's other ways to pay). */
|
|
31
31
|
payment: { bank: string; reg: string; account: string } & Record<string, string>;
|
|
32
32
|
booking: { heading: string; text: string };
|
|
33
|
-
/** The labels of the buttons "Book
|
|
33
|
+
/** The labels of the buttons "Book an appointment" and "Call …" (booking.json). */
|
|
34
34
|
labels: { book: string; call: string };
|
|
35
35
|
/** The texts every copy of a block has (its shared.ts, saved in blocks.json). */
|
|
36
36
|
blocks: BlockTexts;
|
|
37
|
-
/** The texts of the buttons of "
|
|
37
|
+
/** The texts of the buttons of "Buttons" that go elsewhere than a page (config.buttonTargets). */
|
|
38
38
|
buttons: StandardSiteData['buttons'];
|
|
39
|
-
/** The tags not shown on the blog ("
|
|
39
|
+
/** The tags not shown on the blog ("Blog category" off, packages/editor/site/categories.ts). */
|
|
40
40
|
tagsOffBlog: string[];
|
|
41
41
|
/** The settings of the apps as saved (config.apps.settings), by the app's id; an app's blocks put
|
|
42
42
|
* them over those in its settings.ts. */
|
package/defaults/standard.ts
CHANGED
|
@@ -27,7 +27,7 @@ import { blockTexts, nav, site } from '../site/site';
|
|
|
27
27
|
import { popups } from '../site/popups';
|
|
28
28
|
import { opensOf } from '@site/opens';
|
|
29
29
|
|
|
30
|
-
// A post as "
|
|
30
|
+
// A post as "Blog posts" shows it.
|
|
31
31
|
const listedPost = (p: Post): ListedPost => ({
|
|
32
32
|
href: postPath(p),
|
|
33
33
|
title: p.data.title,
|
|
@@ -61,14 +61,14 @@ export const standard: StandardSite = {
|
|
|
61
61
|
const page = (await byPath()).get(href);
|
|
62
62
|
return page ? displayTitle(page.data.title) : nav.flatMap((n) => [n, ...(n.children ?? [])]).find((n) => n.href === href)?.label;
|
|
63
63
|
},
|
|
64
|
-
// "Book
|
|
65
|
-
// goes to the booking page without one there; "
|
|
64
|
+
// "Book an appointment" opens the booking sheet on the page (`data-open="booking"`, the app Calendly's), and
|
|
65
|
+
// goes to the booking page without one there; "Call" has the number after its text.
|
|
66
66
|
buttons: {
|
|
67
67
|
booking: { href: site.bookingPath, text: site.labels.book, open: 'booking' },
|
|
68
68
|
phone: { href: site.phoneHref, text: site.labels.call, after: site.phone },
|
|
69
69
|
},
|
|
70
|
-
// "
|
|
71
|
-
// what it does (the booking sheet: "Book
|
|
70
|
+
// "Open promotion": a promotion turned on that a button can open (opens.ts). One that stands for a target does
|
|
71
|
+
// what it does (the booking sheet: "Book an appointment"); a pop-up opens itself (`data-open="promotion:<file name>"`,
|
|
72
72
|
// Popup.astro), with its heading as the text.
|
|
73
73
|
picked(target, value) {
|
|
74
74
|
const promotion = target === 'promotion' ? popups.find((p) => p.slug === value) : undefined;
|
|
@@ -85,7 +85,10 @@ export const standard: StandardSite = {
|
|
|
85
85
|
// Only a tag with posts has a page (/blog/kategori/<slug>/).
|
|
86
86
|
async tag(slug) {
|
|
87
87
|
tagPages ??= getCategories().then((all) => new Set(all.map((c) => c.slug)));
|
|
88
|
-
return {
|
|
88
|
+
return {
|
|
89
|
+
name: categoryName(slug),
|
|
90
|
+
href: (await tagPages).has(slug) ? `/blog/kategori/${slug}/` : undefined,
|
|
91
|
+
};
|
|
89
92
|
},
|
|
90
93
|
},
|
|
91
94
|
posts: {
|