@morphemecms/cms 0.1.0 → 0.2.0-next-20261104150211
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 +12 -0
- package/LICENSE +21 -21
- package/README.md +51 -51
- package/astro.mjs +162 -162
- package/bin/morpheme.mjs +38 -38
- package/client.d.ts +10 -10
- package/defaults/blockTexts.ts +33 -33
- package/defaults/collections.ts +774 -774
- package/defaults/colors.ts +125 -125
- package/defaults/company.ts +20 -20
- package/defaults/editor.ts +178 -178
- package/defaults/internalLinks.ts +30 -30
- package/defaults/opens.ts +18 -18
- package/defaults/popup.tsx +93 -93
- package/defaults/privacy.ts +79 -79
- package/defaults/rules.ts +450 -450
- package/defaults/schema.ts +16 -16
- package/defaults/sitedata.ts +129 -129
- package/defaults/standard.ts +106 -106
- package/defaults/standardPages.ts +87 -87
- package/defaults/texts.ts +481 -481
- package/demo/README.md +26 -26
- package/demo/src/content/blog/build-a-page-from-blocks.md +19 -19
- package/demo/src/content/blog/colors-everyone-can-read.md +19 -19
- package/demo/src/content/blog/edit-your-website-from-your-phone.md +21 -21
- package/demo/src/content/blog/every-save-is-a-version.md +21 -21
- package/demo/src/content/blog/schedule-a-post.md +17 -17
- package/demo/src/content/blog/secure-from-day-one.md +17 -17
- package/demo/src/content/blog/seo-without-the-guesswork.md +23 -23
- package/demo/src/content/blog/switch-themes-without-losing-anything.md +23 -23
- package/demo/src/content/blog/the-app-store.md +21 -21
- package/demo/src/content/pages/about.md +22 -22
- package/demo/src/content/pages/app-store.md +30 -30
- package/demo/src/content/pages/blocks.md +98 -98
- package/demo/src/content/pages/blog-page.md +11 -11
- package/demo/src/content/pages/contact.md +16 -16
- package/demo/src/content/pages/get-started.md +20 -20
- package/demo/src/content/pages/home.md +26 -26
- package/demo/src/content/pages/page-not-found.md +11 -11
- package/demo/src/content/pages/privacy-policy.md +24 -24
- package/demo/src/data/blocks.json +25 -25
- package/demo/src/data/booking.json +6 -6
- package/demo/src/data/categories/apps.json +6 -6
- package/demo/src/data/categories/design.json +6 -6
- package/demo/src/data/categories/editing.json +6 -6
- package/demo/src/data/categories/security.json +6 -6
- package/demo/src/data/categories/seo.json +6 -6
- package/demo/src/data/colors.json +75 -75
- package/demo/src/data/contact.json +8 -8
- package/demo/src/data/cookies.json +21 -21
- package/demo/src/data/footer.json +27 -27
- package/demo/src/data/menu.json +35 -35
- package/demo/src/data/mobile-menu.json +25 -25
- package/demo/src/data/popups/booking-bar.json +13 -13
- package/demo/src/data/popups/try-the-editor.json +16 -16
- package/demo/src/data/standard-pages.json +8 -8
- package/demo/src/data/testimonials/sample-bakery.json +10 -10
- package/demo/src/data/testimonials/sample-clinic.json +10 -10
- package/demo/src/data/testimonials/sample-studio.json +10 -10
- package/demo/src/site/colors.ts +11 -11
- package/demo/src/site/config.ts +19 -19
- package/icons/lucide/LICENSE +43 -43
- package/icons/lucide/address-book.svg +1 -1
- package/icons/lucide/app-window.svg +1 -1
- package/icons/lucide/arrow-counter-clockwise.svg +1 -1
- package/icons/lucide/arrow-down.svg +1 -1
- package/icons/lucide/arrow-left.svg +1 -1
- package/icons/lucide/arrow-right.svg +1 -1
- package/icons/lucide/arrow-square-out.svg +1 -1
- package/icons/lucide/arrow-u-up-left.svg +1 -1
- package/icons/lucide/arrow-u-up-right.svg +1 -1
- package/icons/lucide/arrow-up-right.svg +1 -1
- package/icons/lucide/arrow-up.svg +1 -1
- package/icons/lucide/arrows-down-up.svg +1 -1
- package/icons/lucide/arrows-in-line-horizontal.svg +1 -1
- package/icons/lucide/arrows-out-line-horizontal.svg +1 -1
- package/icons/lucide/article.svg +1 -1
- package/icons/lucide/bank.svg +1 -1
- package/icons/lucide/bell.svg +1 -1
- package/icons/lucide/bicycle.svg +1 -1
- package/icons/lucide/binoculars.svg +1 -1
- package/icons/lucide/book-open-text.svg +1 -1
- package/icons/lucide/book-open.svg +1 -1
- package/icons/lucide/browser.svg +1 -1
- package/icons/lucide/buildings.svg +1 -1
- package/icons/lucide/bus.svg +1 -1
- package/icons/lucide/calendar-blank.svg +1 -1
- package/icons/lucide/calendar-check.svg +1 -1
- package/icons/lucide/car.svg +1 -1
- package/icons/lucide/cards.svg +1 -1
- package/icons/lucide/caret-down.svg +1 -1
- package/icons/lucide/caret-left.svg +1 -1
- package/icons/lucide/caret-right.svg +1 -1
- package/icons/lucide/caret-up.svg +1 -1
- package/icons/lucide/certificate.svg +1 -1
- package/icons/lucide/chart-line-up.svg +1 -1
- package/icons/lucide/chats.svg +1 -1
- package/icons/lucide/check-circle.svg +1 -1
- package/icons/lucide/check.svg +1 -1
- package/icons/lucide/circle-half.svg +1 -1
- package/icons/lucide/circle.svg +1 -1
- package/icons/lucide/clock-counter-clockwise.svg +1 -1
- package/icons/lucide/clock.svg +1 -1
- package/icons/lucide/code.svg +1 -1
- package/icons/lucide/coffee.svg +1 -1
- package/icons/lucide/columns.svg +1 -1
- package/icons/lucide/cookie.svg +1 -1
- package/icons/lucide/copy.svg +1 -1
- package/icons/lucide/credit-card.svg +1 -1
- package/icons/lucide/currency-circle-dollar.svg +1 -1
- package/icons/lucide/cursor-click.svg +1 -1
- package/icons/lucide/database.svg +1 -1
- package/icons/lucide/desktop.svg +1 -1
- package/icons/lucide/device-mobile.svg +1 -1
- package/icons/lucide/door-open.svg +1 -1
- package/icons/lucide/download-simple.svg +1 -1
- package/icons/lucide/envelope-simple.svg +1 -1
- package/icons/lucide/eye-slash.svg +1 -1
- package/icons/lucide/eye.svg +1 -1
- package/icons/lucide/file-pdf.svg +1 -1
- package/icons/lucide/file-text.svg +1 -1
- package/icons/lucide/file-video.svg +1 -1
- package/icons/lucide/file-zip.svg +1 -1
- package/icons/lucide/flower-lotus.svg +1 -1
- package/icons/lucide/folder-simple.svg +1 -1
- package/icons/lucide/funnel.svg +1 -1
- package/icons/lucide/gauge.svg +1 -1
- package/icons/lucide/gear.svg +1 -1
- package/icons/lucide/github-logo.svg +1 -1
- package/icons/lucide/globe.svg +1 -1
- package/icons/lucide/google-logo.svg +1 -1
- package/icons/lucide/graduation-cap.svg +1 -1
- package/icons/lucide/hammer.svg +1 -1
- package/icons/lucide/hand-heart.svg +1 -1
- package/icons/lucide/handshake.svg +1 -1
- package/icons/lucide/hard-drives.svg +1 -1
- package/icons/lucide/heart.svg +1 -1
- package/icons/lucide/house.svg +1 -1
- package/icons/lucide/image-square.svg +1 -1
- package/icons/lucide/image.svg +1 -1
- package/icons/lucide/info.svg +1 -1
- package/icons/lucide/keyboard.svg +1 -1
- package/icons/lucide/leaf.svg +1 -1
- package/icons/lucide/lifebuoy.svg +1 -1
- package/icons/lucide/lightbulb.svg +1 -1
- package/icons/lucide/link-break.svg +1 -1
- package/icons/lucide/link-simple.svg +1 -1
- package/icons/lucide/list-bullets.svg +1 -1
- package/icons/lucide/list-checks.svg +1 -1
- package/icons/lucide/list-numbers.svg +1 -1
- package/icons/lucide/list.svg +1 -1
- package/icons/lucide/lock-simple-open.svg +1 -1
- package/icons/lucide/lock-simple.svg +1 -1
- package/icons/lucide/magnifying-glass.svg +1 -1
- package/icons/lucide/map-pin.svg +1 -1
- package/icons/lucide/map-trifold.svg +1 -1
- package/icons/lucide/medal.svg +1 -1
- package/icons/lucide/megaphone.svg +1 -1
- package/icons/lucide/minus.svg +1 -1
- package/icons/lucide/moon.svg +1 -1
- package/icons/lucide/newspaper.svg +1 -1
- package/icons/lucide/pack.json +23 -23
- package/icons/lucide/paint-brush-broad.svg +1 -1
- package/icons/lucide/palette.svg +1 -1
- package/icons/lucide/paper-plane-tilt.svg +1 -1
- package/icons/lucide/pencil-simple.svg +1 -1
- package/icons/lucide/phone.svg +1 -1
- package/icons/lucide/play-fill.svg +1 -1
- package/icons/lucide/plus.svg +1 -1
- package/icons/lucide/prohibit.svg +1 -1
- package/icons/lucide/puzzle-piece.svg +1 -1
- package/icons/lucide/question.svg +1 -1
- package/icons/lucide/quotes.svg +1 -1
- package/icons/lucide/rows.svg +1 -1
- package/icons/lucide/ruler.svg +1 -1
- package/icons/lucide/scan.svg +1 -1
- package/icons/lucide/shapes.svg +1 -1
- package/icons/lucide/share-network.svg +1 -1
- package/icons/lucide/shield-check.svg +1 -1
- package/icons/lucide/sidebar-simple.svg +1 -1
- package/icons/lucide/sign-out.svg +1 -1
- package/icons/lucide/signpost.svg +1 -1
- package/icons/lucide/sliders-horizontal.svg +1 -1
- package/icons/lucide/smiley.svg +1 -1
- package/icons/lucide/sparkle.svg +1 -1
- package/icons/lucide/square-half-bottom.svg +1 -1
- package/icons/lucide/square.svg +1 -1
- package/icons/lucide/squares-four.svg +1 -1
- package/icons/lucide/sticker.svg +1 -1
- package/icons/lucide/storefront.svg +1 -1
- package/icons/lucide/sun.svg +1 -1
- package/icons/lucide/swatches.svg +1 -1
- package/icons/lucide/tag.svg +1 -1
- package/icons/lucide/text-aa.svg +1 -1
- package/icons/lucide/text-align-left.svg +1 -1
- package/icons/lucide/text-b.svg +1 -1
- package/icons/lucide/text-h-one.svg +1 -1
- package/icons/lucide/text-h-three.svg +1 -1
- package/icons/lucide/text-h-two.svg +1 -1
- package/icons/lucide/text-italic.svg +1 -1
- package/icons/lucide/text-t.svg +1 -1
- package/icons/lucide/ticket.svg +1 -1
- package/icons/lucide/toggle-right.svg +1 -1
- package/icons/lucide/train.svg +1 -1
- package/icons/lucide/trash.svg +1 -1
- package/icons/lucide/upload-simple.svg +1 -1
- package/icons/lucide/user-circle.svg +1 -1
- package/icons/lucide/user.svg +1 -1
- package/icons/lucide/users-three.svg +1 -1
- package/icons/lucide/users.svg +1 -1
- package/icons/lucide/video-camera.svg +1 -1
- package/icons/lucide/warning-circle.svg +1 -1
- package/icons/lucide/warning.svg +1 -1
- package/icons/lucide/wave-sine.svg +1 -1
- package/icons/lucide/wheelchair.svg +1 -1
- package/icons/lucide/wrench.svg +1 -1
- package/icons/lucide/x.svg +1 -1
- package/icons/material-symbols/LICENSE +201 -201
- package/icons/material-symbols/address-book.svg +1 -1
- package/icons/material-symbols/app-window.svg +1 -1
- package/icons/material-symbols/arrow-counter-clockwise.svg +1 -1
- package/icons/material-symbols/arrow-down.svg +1 -1
- package/icons/material-symbols/arrow-left.svg +1 -1
- package/icons/material-symbols/arrow-right.svg +1 -1
- package/icons/material-symbols/arrow-square-out.svg +1 -1
- package/icons/material-symbols/arrow-u-up-left.svg +1 -1
- package/icons/material-symbols/arrow-u-up-right.svg +1 -1
- package/icons/material-symbols/arrow-up-right.svg +1 -1
- package/icons/material-symbols/arrow-up.svg +1 -1
- package/icons/material-symbols/arrows-down-up.svg +1 -1
- package/icons/material-symbols/arrows-in-line-horizontal.svg +1 -1
- package/icons/material-symbols/arrows-out-line-horizontal.svg +1 -1
- package/icons/material-symbols/article.svg +1 -1
- package/icons/material-symbols/bank.svg +1 -1
- package/icons/material-symbols/bell.svg +1 -1
- package/icons/material-symbols/bicycle.svg +1 -1
- package/icons/material-symbols/binoculars.svg +1 -1
- package/icons/material-symbols/book-open-text.svg +1 -1
- package/icons/material-symbols/book-open.svg +1 -1
- package/icons/material-symbols/browser.svg +1 -1
- package/icons/material-symbols/buildings.svg +1 -1
- package/icons/material-symbols/bus.svg +1 -1
- package/icons/material-symbols/calendar-blank.svg +1 -1
- package/icons/material-symbols/calendar-check.svg +1 -1
- package/icons/material-symbols/car.svg +1 -1
- package/icons/material-symbols/cards.svg +1 -1
- package/icons/material-symbols/caret-down.svg +1 -1
- package/icons/material-symbols/caret-left.svg +1 -1
- package/icons/material-symbols/caret-right.svg +1 -1
- package/icons/material-symbols/caret-up.svg +1 -1
- package/icons/material-symbols/certificate.svg +1 -1
- package/icons/material-symbols/chart-line-up.svg +1 -1
- package/icons/material-symbols/chats.svg +1 -1
- package/icons/material-symbols/check-circle.svg +1 -1
- package/icons/material-symbols/check.svg +1 -1
- package/icons/material-symbols/circle-half.svg +1 -1
- package/icons/material-symbols/circle.svg +1 -1
- package/icons/material-symbols/clock-counter-clockwise.svg +1 -1
- package/icons/material-symbols/clock.svg +1 -1
- package/icons/material-symbols/code.svg +1 -1
- package/icons/material-symbols/coffee.svg +1 -1
- package/icons/material-symbols/columns.svg +1 -1
- package/icons/material-symbols/cookie.svg +1 -1
- package/icons/material-symbols/copy.svg +1 -1
- package/icons/material-symbols/credit-card.svg +1 -1
- package/icons/material-symbols/currency-circle-dollar.svg +1 -1
- package/icons/material-symbols/cursor-click.svg +1 -1
- package/icons/material-symbols/database.svg +1 -1
- package/icons/material-symbols/desktop.svg +1 -1
- package/icons/material-symbols/device-mobile.svg +1 -1
- package/icons/material-symbols/door-open.svg +1 -1
- package/icons/material-symbols/download-simple.svg +1 -1
- package/icons/material-symbols/envelope-simple.svg +1 -1
- package/icons/material-symbols/eye-slash.svg +1 -1
- package/icons/material-symbols/eye.svg +1 -1
- package/icons/material-symbols/file-pdf.svg +1 -1
- package/icons/material-symbols/file-text.svg +1 -1
- package/icons/material-symbols/file-video.svg +1 -1
- package/icons/material-symbols/file-zip.svg +1 -1
- package/icons/material-symbols/flower-lotus.svg +1 -1
- package/icons/material-symbols/folder-simple.svg +1 -1
- package/icons/material-symbols/funnel.svg +1 -1
- package/icons/material-symbols/gauge.svg +1 -1
- package/icons/material-symbols/gear.svg +1 -1
- package/icons/material-symbols/github-logo.svg +1 -1
- package/icons/material-symbols/globe.svg +1 -1
- package/icons/material-symbols/google-logo.svg +1 -1
- package/icons/material-symbols/graduation-cap.svg +1 -1
- package/icons/material-symbols/hammer.svg +1 -1
- package/icons/material-symbols/hand-heart.svg +1 -1
- package/icons/material-symbols/handshake.svg +1 -1
- package/icons/material-symbols/hard-drives.svg +1 -1
- package/icons/material-symbols/heart.svg +1 -1
- package/icons/material-symbols/house.svg +1 -1
- package/icons/material-symbols/image-square.svg +1 -1
- package/icons/material-symbols/image.svg +1 -1
- package/icons/material-symbols/info.svg +1 -1
- package/icons/material-symbols/keyboard.svg +1 -1
- package/icons/material-symbols/leaf.svg +1 -1
- package/icons/material-symbols/lifebuoy.svg +1 -1
- package/icons/material-symbols/lightbulb.svg +1 -1
- package/icons/material-symbols/link-break.svg +1 -1
- package/icons/material-symbols/link-simple.svg +1 -1
- package/icons/material-symbols/list-bullets.svg +1 -1
- package/icons/material-symbols/list-checks.svg +1 -1
- package/icons/material-symbols/list-numbers.svg +1 -1
- package/icons/material-symbols/list.svg +1 -1
- package/icons/material-symbols/lock-simple-open.svg +1 -1
- package/icons/material-symbols/lock-simple.svg +1 -1
- package/icons/material-symbols/magnifying-glass.svg +1 -1
- package/icons/material-symbols/map-pin.svg +1 -1
- package/icons/material-symbols/map-trifold.svg +1 -1
- package/icons/material-symbols/medal.svg +1 -1
- package/icons/material-symbols/megaphone.svg +1 -1
- package/icons/material-symbols/minus.svg +1 -1
- package/icons/material-symbols/moon.svg +1 -1
- package/icons/material-symbols/newspaper.svg +1 -1
- package/icons/material-symbols/pack.json +29 -29
- package/icons/material-symbols/paint-brush-broad.svg +1 -1
- package/icons/material-symbols/palette.svg +1 -1
- package/icons/material-symbols/paper-plane-tilt.svg +1 -1
- package/icons/material-symbols/pencil-simple.svg +1 -1
- package/icons/material-symbols/phone.svg +1 -1
- package/icons/material-symbols/play-fill.svg +1 -1
- package/icons/material-symbols/plus.svg +1 -1
- package/icons/material-symbols/prohibit.svg +1 -1
- package/icons/material-symbols/puzzle-piece.svg +1 -1
- package/icons/material-symbols/question.svg +1 -1
- package/icons/material-symbols/quotes.svg +1 -1
- package/icons/material-symbols/rows.svg +1 -1
- package/icons/material-symbols/ruler.svg +1 -1
- package/icons/material-symbols/scan.svg +1 -1
- package/icons/material-symbols/shapes.svg +1 -1
- package/icons/material-symbols/share-network.svg +1 -1
- package/icons/material-symbols/shield-check.svg +1 -1
- package/icons/material-symbols/sidebar-simple.svg +1 -1
- package/icons/material-symbols/sign-out.svg +1 -1
- package/icons/material-symbols/signpost.svg +1 -1
- package/icons/material-symbols/sliders-horizontal.svg +1 -1
- package/icons/material-symbols/smiley.svg +1 -1
- package/icons/material-symbols/sparkle.svg +1 -1
- package/icons/material-symbols/square-half-bottom.svg +1 -1
- package/icons/material-symbols/square.svg +1 -1
- package/icons/material-symbols/squares-four.svg +1 -1
- package/icons/material-symbols/sticker.svg +1 -1
- package/icons/material-symbols/storefront.svg +1 -1
- package/icons/material-symbols/sun.svg +1 -1
- package/icons/material-symbols/swatches.svg +1 -1
- package/icons/material-symbols/tag.svg +1 -1
- package/icons/material-symbols/text-aa.svg +1 -1
- package/icons/material-symbols/text-align-left.svg +1 -1
- package/icons/material-symbols/text-b.svg +1 -1
- package/icons/material-symbols/text-h-one.svg +1 -1
- package/icons/material-symbols/text-h-three.svg +1 -1
- package/icons/material-symbols/text-h-two.svg +1 -1
- package/icons/material-symbols/text-italic.svg +1 -1
- package/icons/material-symbols/text-t.svg +1 -1
- package/icons/material-symbols/ticket.svg +1 -1
- package/icons/material-symbols/toggle-right.svg +1 -1
- package/icons/material-symbols/train.svg +1 -1
- package/icons/material-symbols/trash.svg +1 -1
- package/icons/material-symbols/upload-simple.svg +1 -1
- package/icons/material-symbols/user-circle.svg +1 -1
- package/icons/material-symbols/user.svg +1 -1
- package/icons/material-symbols/users-three.svg +1 -1
- package/icons/material-symbols/users.svg +1 -1
- package/icons/material-symbols/video-camera.svg +1 -1
- package/icons/material-symbols/warning-circle.svg +1 -1
- package/icons/material-symbols/warning.svg +1 -1
- package/icons/material-symbols/wave-sine.svg +1 -1
- package/icons/material-symbols/wheelchair.svg +1 -1
- package/icons/material-symbols/wrench.svg +1 -1
- package/icons/material-symbols/x.svg +1 -1
- package/icons/phosphor/pack.json +8 -8
- package/lib/apps.mjs +142 -139
- package/lib/apps.test.d.mts +1 -0
- package/lib/apps.test.mjs +50 -0
- package/lib/blocks.mjs +90 -90
- package/lib/caseCollisions.d.mts +4 -0
- package/lib/caseCollisions.mjs +62 -0
- package/lib/caseCollisions.test.d.mts +1 -0
- package/lib/caseCollisions.test.mjs +74 -0
- package/lib/colors.ts +86 -86
- package/lib/contactBar.ts +18 -18
- package/lib/contracts.d.mts +15 -0
- package/lib/contracts.mjs +17 -0
- package/lib/cookieCategories.mjs +28 -28
- package/lib/crop.mjs +83 -83
- package/lib/csp.mjs +130 -130
- package/lib/cspSources.mjs +60 -60
- package/lib/dynamic-colors.ts +426 -426
- package/lib/git.mjs +64 -64
- package/lib/hooks.ts +51 -51
- package/lib/inline.ts +20 -20
- package/lib/license-texts/Apache-2.0.txt +190 -190
- package/lib/license-texts/BSD-2-Clause.txt +24 -24
- package/lib/license-texts/BSD-3-Clause.txt +28 -28
- package/lib/license-texts/ISC.txt +15 -15
- package/lib/license-texts/MIT.txt +21 -21
- package/lib/licenses.d.mts +1 -1
- package/lib/licenses.mjs +683 -683
- package/lib/markdown.ts +42 -42
- package/lib/motion-schemes.mjs +5 -5
- package/lib/motion.ts +131 -131
- package/lib/notice.mjs +34 -34
- package/lib/scheduled.mjs +37 -37
- package/lib/snackbar.ts +149 -149
- package/lib/status.mjs +18 -18
- package/lib/storeEntries.d.mts +18 -0
- package/lib/storeEntries.mjs +64 -0
- package/lib/storeEntries.test.d.mts +1 -0
- package/lib/storeEntries.test.mjs +71 -0
- package/lib/svg.mjs +231 -231
- package/lib/text-colors.mjs +43 -43
- package/lib/video-id.mjs +72 -72
- package/lib/video.mjs +91 -91
- package/package.json +136 -131
- package/pages/404.astro +10 -10
- package/pages/[...slug].astro +51 -51
- package/pages/_headers.txt +31 -31
- package/pages/blog/index.astro +10 -10
- package/pages/blog/kategori/[category].astro +18 -18
- package/pages/editor/code-images.json.js +25 -25
- package/pages/editor/index.astro +75 -75
- package/pages/editor/manifest.webmanifest.js +23 -23
- package/pages/editor/sw.js +33 -33
- package/pages/editor/sw.js.js +4 -4
- package/pages/editor/thumbnails.json.js +21 -21
- package/pages/editor/version.json.js +22 -22
- package/pages/headers.js +4 -4
- package/pages/index.astro +10 -10
- package/pages/ny.astro +6 -6
- package/pages/robots.txt.js +3 -3
- package/pages/rss.xml.js +20 -20
- package/scripts/app-screenshots.mjs +164 -164
- package/scripts/app-workers.mjs +6 -6
- package/scripts/bin.mjs +12 -12
- package/scripts/built-site.mjs +134 -99
- package/scripts/check-consent.mjs +113 -113
- package/scripts/check-content.mjs +151 -149
- package/scripts/check-demo.mjs +24 -24
- package/scripts/check-store.mjs +160 -0
- package/scripts/check-themes.mjs +45 -45
- package/scripts/check-worker.mjs +569 -569
- package/scripts/compare-html.mjs +81 -81
- package/scripts/declarations.mjs +10 -0
- package/scripts/declarations.test.mjs +32 -0
- package/scripts/demo-site.mjs +42 -42
- package/scripts/editor-icons.mjs +56 -56
- package/scripts/editor-roundtrip.mjs +134 -134
- package/scripts/editor-screens.mjs +101 -101
- package/scripts/git-env.mjs +9 -9
- package/scripts/icon-packs.mjs +151 -151
- package/scripts/link-counts.mjs +22 -22
- package/scripts/page-colors.mjs +42 -42
- package/scripts/releases.mjs +80 -80
- package/scripts/responsive-images.mjs +94 -94
- package/scripts/scheduled-list.mjs +15 -15
- package/scripts/theme-screenshots.mjs +88 -88
- package/scripts/types.mjs +17 -21
- package/scripts/video-thumbnails.mjs +89 -89
- package/scripts/worker-modules.mjs +34 -34
- package/site/appCookies.mjs +74 -74
- package/site/apps.ts +21 -21
- package/site/autolinks.mjs +200 -200
- package/site/blockScripts.ts +31 -31
- package/site/categories.ts +16 -16
- package/site/consent.ts +26 -26
- package/site/content.config.ts +84 -84
- package/site/content.ts +104 -104
- package/site/cookieList.ts +17 -17
- package/site/defineSite.d.mts +2 -14
- package/site/defineSite.mjs +131 -136
- package/site/directives/interact.mjs +31 -31
- package/site/editor-version.mjs +26 -26
- package/site/fill.ts +13 -13
- package/site/format.ts +11 -11
- package/site/home.ts +66 -66
- package/site/iconPacks.mjs +43 -43
- package/site/icons.mjs +605 -605
- package/site/images.mjs +157 -157
- package/site/motion.ts +13 -13
- package/site/noindex.mjs +21 -21
- package/site/pageColors.ts +110 -110
- package/site/popups.ts +98 -98
- package/site/schema.ts +189 -189
- package/site/sections.ts +3 -3
- package/site/seo.ts +28 -28
- package/site/site.ts +76 -76
- package/site/siteConfig.mjs +8 -8
- package/site/siteCsp.mjs +75 -75
- package/site/siteModules.mjs +27 -27
- package/site/standardPages.ts +59 -59
- package/site/storeTheme.d.mts +6 -0
- package/site/storeTheme.mjs +29 -0
- package/site/storeTheme.test.d.mts +1 -0
- package/site/storeTheme.test.mjs +53 -0
- package/site/testimonials.ts +48 -48
- package/site/text.ts +43 -43
- package/site/theme.mjs +483 -472
- package/site/tracking.client.ts +225 -225
- package/site/tracking.ts +9 -9
- package/src/ActionParts.tsx +205 -205
- package/src/Advanced.tsx +152 -152
- package/src/App.tsx +901 -904
- package/src/AppList.tsx +1362 -1345
- package/src/BlockDialog.tsx +360 -360
- package/src/BlockParts.tsx +652 -652
- package/src/BlockSettings.tsx +533 -533
- package/src/BrandForm.tsx +164 -164
- package/src/Colors.tsx +476 -476
- package/src/CommunicationParts.tsx +199 -199
- package/src/CompanyForm.tsx +209 -209
- package/src/Consents.tsx +175 -175
- package/src/ContainmentParts.tsx +185 -185
- package/src/Crop.tsx +152 -152
- package/src/DataForm.tsx +434 -434
- package/src/Deleted.tsx +177 -177
- package/src/EntryForm.tsx +740 -740
- package/src/FileView.tsx +30 -30
- package/src/FormFrame.tsx +237 -237
- package/src/Keywords.tsx +225 -225
- package/src/Library.tsx +1563 -1563
- package/src/Licenses.tsx +336 -336
- package/src/LinkSheet.tsx +138 -138
- package/src/MarkdownEditor.tsx +1075 -1075
- package/src/Media.tsx +677 -677
- package/src/NavigationParts.tsx +350 -350
- package/src/NavigationRestParts.tsx +247 -247
- package/src/Notifications.tsx +59 -59
- package/src/PagePicker.tsx +136 -136
- package/src/Parts.tsx +991 -991
- package/src/PartsShown.tsx +168 -168
- package/src/PickerParts.tsx +183 -183
- package/src/Redirects.tsx +450 -450
- package/src/Screen.tsx +184 -184
- package/src/SectionsField.tsx +65 -65
- package/src/SelectionParts.tsx +134 -134
- package/src/SeoScores.tsx +184 -184
- package/src/Sheet.tsx +35 -35
- package/src/ThemeStore.tsx +376 -376
- package/src/Tokens.tsx +346 -346
- package/src/Versions.tsx +214 -214
- package/src/addresses.ts +75 -75
- package/src/allIcons.ts +7 -7
- package/src/appFiles.ts +29 -29
- package/src/apps.ts +281 -281
- package/src/ask.tsx +123 -123
- package/src/blocks.ts +251 -251
- package/src/brand.ts +153 -153
- package/src/cache.ts +42 -42
- package/src/check.ts +285 -285
- package/src/checkSite.ts +256 -256
- package/src/{consents.ts → consentData.ts} +26 -26
- package/src/cookieList.ts +70 -70
- package/src/countries.ts +59 -59
- package/src/deploy.ts +155 -155
- package/src/editor.css +602 -602
- package/src/examples.ts +85 -85
- package/src/fields.tsx +942 -942
- package/src/frontmatter.mjs +42 -42
- package/src/github.ts +376 -376
- package/src/i18n/da-DK/advanced.ts +28 -28
- package/src/i18n/da-DK/app.ts +71 -71
- package/src/i18n/da-DK/apps.ts +90 -90
- package/src/i18n/da-DK/blockSettings.ts +38 -38
- package/src/i18n/da-DK/blocks.ts +408 -408
- package/src/i18n/da-DK/check.ts +25 -25
- package/src/i18n/da-DK/colors.ts +64 -64
- package/src/i18n/da-DK/common.ts +118 -118
- package/src/i18n/da-DK/company.ts +24 -24
- package/src/i18n/da-DK/consents.ts +23 -23
- package/src/i18n/da-DK/forms.ts +116 -116
- package/src/i18n/da-DK/history.ts +35 -35
- package/src/i18n/da-DK/index.ts +49 -49
- package/src/i18n/da-DK/keywords.ts +19 -19
- package/src/i18n/da-DK/library.ts +135 -135
- package/src/i18n/da-DK/licenses.ts +25 -25
- package/src/i18n/da-DK/media.ts +79 -79
- package/src/i18n/da-DK/parts.ts +696 -696
- package/src/i18n/da-DK/preview.ts +13 -13
- package/src/i18n/da-DK/redirects.ts +51 -51
- package/src/i18n/da-DK/seo.ts +20 -20
- package/src/i18n/da-DK/text.ts +35 -35
- package/src/i18n/da-DK/theme.ts +41 -41
- package/src/i18n/en-US/advanced.ts +32 -32
- package/src/i18n/en-US/app.ts +84 -84
- package/src/i18n/en-US/apps.ts +134 -134
- package/src/i18n/en-US/blockSettings.ts +48 -48
- package/src/i18n/en-US/blocks.ts +441 -441
- package/src/i18n/en-US/check.ts +31 -31
- package/src/i18n/en-US/colors.ts +82 -82
- package/src/i18n/en-US/common.ts +144 -144
- package/src/i18n/en-US/company.ts +25 -25
- package/src/i18n/en-US/consents.ts +25 -25
- package/src/i18n/en-US/forms.ts +145 -145
- package/src/i18n/en-US/history.ts +43 -43
- package/src/i18n/en-US/index.ts +51 -51
- package/src/i18n/en-US/keywords.ts +24 -24
- package/src/i18n/en-US/library.ts +176 -176
- package/src/i18n/en-US/licenses.ts +36 -36
- package/src/i18n/en-US/media.ts +80 -80
- package/src/i18n/en-US/parts.ts +718 -718
- package/src/i18n/en-US/preview.ts +15 -15
- package/src/i18n/en-US/redirects.ts +66 -66
- package/src/i18n/en-US/seo.ts +26 -26
- package/src/i18n/en-US/text.ts +41 -41
- package/src/i18n/en-US/theme.ts +41 -41
- package/src/i18n/index.ts +124 -124
- package/src/icon.tsx +160 -160
- package/src/iconPacks.ts +89 -89
- package/src/images.ts +163 -163
- package/src/{licenses.ts → licenseData.ts} +79 -79
- package/src/markdown.mjs +204 -204
- package/src/nav.ts +68 -68
- package/src/notFound.ts +28 -28
- package/src/{notifications.ts → notificationData.ts} +223 -223
- package/src/pagePreview.tsx +204 -204
- package/src/r2Caching.ts +55 -55
- package/src/{redirects.ts → redirectData.ts} +149 -149
- package/src/registry.tsx +188 -188
- package/src/schema.ts +417 -417
- package/src/sections.mjs +303 -303
- package/src/seo.ts +333 -333
- package/src/shared.ts +44 -44
- package/src/sidebar.ts +31 -31
- package/src/site.ts +317 -317
- package/src/standard/Block.astro +19 -19
- package/src/standard/BlockSite.astro +18 -18
- package/src/standard/accordion/index.tsx +47 -47
- package/src/standard/accordion/load.ts +23 -23
- package/src/standard/accordion/shared.ts +15 -15
- package/src/standard/accordion/view.ts +32 -32
- package/src/standard/buttons/fields.ts +98 -98
- package/src/standard/buttons/index.tsx +23 -23
- package/src/standard/buttons/load.ts +34 -34
- package/src/standard/buttons/shared.ts +12 -12
- package/src/standard/buttons/view.ts +30 -30
- package/src/standard/cards/index.tsx +71 -71
- package/src/standard/cards/load.ts +17 -17
- package/src/standard/cards/shared.ts +27 -27
- package/src/standard/cards/view.ts +29 -29
- package/src/standard/checklist/index.tsx +27 -27
- package/src/standard/checklist/load.ts +5 -5
- package/src/standard/checklist/view.ts +13 -13
- package/src/standard/cookies/index.tsx +17 -17
- package/src/standard/cookies/list.ts +56 -56
- package/src/standard/cookies/load.ts +11 -11
- package/src/standard/cookies/shared.ts +20 -20
- package/src/standard/cookies/view.ts +28 -28
- package/src/standard/faq/index.tsx +29 -29
- package/src/standard/faq/load.ts +14 -14
- package/src/standard/faq/view.ts +21 -21
- package/src/standard/head.ts +23 -23
- package/src/standard/hero/fields.ts +48 -48
- package/src/standard/hero/index.tsx +57 -57
- package/src/standard/hero/load.ts +23 -23
- package/src/standard/hero/view.ts +38 -38
- package/src/standard/links/index.tsx +41 -41
- package/src/standard/links/load.ts +17 -17
- package/src/standard/links/view.ts +25 -25
- package/src/standard/list/index.tsx +50 -50
- package/src/standard/list/load.ts +5 -5
- package/src/standard/list/view.ts +18 -18
- package/src/standard/load.ts +7 -7
- package/src/standard/notice/index.tsx +30 -30
- package/src/standard/notice/load.ts +16 -16
- package/src/standard/notice/view.ts +10 -10
- package/src/standard/points/index.tsx +42 -42
- package/src/standard/points/load.ts +5 -5
- package/src/standard/points/view.ts +12 -12
- package/src/standard/posts/index.tsx +132 -132
- package/src/standard/posts/load.ts +36 -36
- package/src/standard/posts/shared.ts +21 -21
- package/src/standard/posts/view.ts +63 -63
- package/src/standard/pricecards/index.tsx +57 -57
- package/src/standard/pricecards/load.ts +11 -11
- package/src/standard/pricecards/view.ts +50 -50
- package/src/standard/sharedTexts.ts +6 -6
- package/src/standard/site.ts +124 -124
- package/src/standard/siteLooks.ts +19 -19
- package/src/standard/sites.ts +13 -13
- package/src/standard/steps/index.tsx +28 -28
- package/src/standard/steps/load.ts +5 -5
- package/src/standard/steps/view.ts +11 -11
- package/src/standard/testimonials/index.tsx +87 -87
- package/src/standard/testimonials/list.ts +43 -43
- package/src/standard/testimonials/load.ts +58 -58
- package/src/standard/testimonials/shared.ts +13 -13
- package/src/standard/testimonials/view.ts +76 -76
- package/src/standard/themeBlocks.ts +13 -13
- package/src/standard/video/index.tsx +23 -23
- package/src/standard/video/load.ts +10 -10
- package/src/standard/video/view.ts +42 -42
- package/src/standard/view.tsx +105 -105
- package/src/standard/views.ts +37 -37
- package/src/store.ts +479 -499
- package/src/theme.ts +90 -90
- package/src/themeUi.ts +97 -97
- package/src/themes.ts +91 -91
- package/src/ui.tsx +3402 -3402
- package/src/virtual.d.ts +5 -5
- package/src/when.ts +22 -22
- package/starter/.githooks/pre-push +4 -4
- package/starter/.nvmrc +1 -1
- package/starter/.prettierignore +11 -11
- package/starter/.prettierrc.json +7 -7
- package/starter/README.md +27 -26
- package/starter/astro.config.mjs +5 -5
- package/starter/package.json +67 -67
- package/starter/public/_redirects +1 -1
- package/starter/public/favicon.svg +1 -1
- package/starter/public/logo.svg +1 -1
- package/starter/src/content/pages/blog.md +8 -8
- package/starter/src/content/pages/forside.md +9 -9
- package/starter/src/content/pages/kontakt.md +7 -7
- package/starter/src/content/pages/privatlivspolitik.md +9 -9
- package/starter/src/content/pages/siden-findes-ikke.md +7 -7
- package/starter/src/content.config.ts +2 -2
- package/starter/src/data/apps.json +1 -1
- package/starter/src/data/block-settings.json +3 -3
- package/starter/src/data/blocks.json +1 -1
- package/starter/src/data/booking.json +6 -6
- package/starter/src/data/colors.json +70 -70
- package/starter/src/data/contact.json +8 -8
- package/starter/src/data/cookies.json +21 -21
- package/starter/src/data/csp.json +1 -1
- package/starter/src/data/footer.json +15 -15
- package/starter/src/data/internal-links.json +4 -4
- package/starter/src/data/menu.json +4 -4
- package/starter/src/data/mobile-menu.json +4 -4
- package/starter/src/data/old-addresses.json +1 -1
- package/starter/src/data/redirects.json +1 -1
- package/starter/src/data/standard-pages.json +8 -8
- package/starter/src/data/theme.json +1 -1
- package/starter/src/env.d.ts +2 -2
- package/starter/src/site/config.ts +13 -13
- package/starter/src/site/worker.js +14 -14
- package/starter/tsconfig.json +17 -17
- package/starter/worker/apps.js +6 -6
- package/starter/wrangler.jsonc +39 -39
- package/theme/LICENSE +21 -21
- package/theme/blocks/ArrowLink.tsx +14 -14
- package/theme/blocks/BlockHead.tsx +26 -26
- package/theme/blocks/Panel.tsx +32 -32
- package/theme/blocks/Person.tsx +26 -26
- package/theme/blocks/accordion/PageRow.tsx +25 -25
- package/theme/blocks/accordion/View.tsx +17 -17
- package/theme/blocks/buttons/Buttons.tsx +28 -28
- package/theme/blocks/buttons/View.tsx +12 -12
- package/theme/blocks/cards/LinkCards.tsx +72 -72
- package/theme/blocks/cards/View.tsx +20 -20
- package/theme/blocks/checklist/Checks.tsx +15 -15
- package/theme/blocks/checklist/View.tsx +14 -14
- package/theme/blocks/cookies/View.tsx +36 -36
- package/theme/blocks/faq/Accordion.tsx +34 -34
- package/theme/blocks/faq/View.tsx +16 -16
- package/theme/blocks/faq/script.ts +56 -56
- package/theme/blocks/hero/View.tsx +44 -44
- package/theme/blocks/links/LinkGroups.tsx +24 -24
- package/theme/blocks/links/View.tsx +17 -17
- package/theme/blocks/list/Rows.tsx +24 -24
- package/theme/blocks/list/View.tsx +14 -14
- package/theme/blocks/notice/View.tsx +13 -13
- package/theme/blocks/points/PointList.tsx +34 -34
- package/theme/blocks/points/View.tsx +14 -14
- package/theme/blocks/posts/View.tsx +26 -26
- package/theme/blocks/pricecards/View.tsx +40 -40
- package/theme/blocks/steps/StepList.tsx +23 -23
- package/theme/blocks/steps/View.tsx +14 -14
- package/theme/blocks/testimonials/List.tsx +46 -46
- package/theme/blocks/testimonials/Slider.tsx +44 -44
- package/theme/blocks/testimonials/Testimonial.tsx +124 -124
- package/theme/blocks/testimonials/View.tsx +21 -21
- package/theme/blocks/testimonials/script.ts +52 -52
- package/theme/blocks/video/View.tsx +41 -41
- package/theme/components/ActionBar.astro +63 -63
- package/theme/components/AppSlot.astro +21 -21
- package/theme/components/ArrowLink.tsx +21 -21
- package/theme/components/BlogIndex.astro +48 -48
- package/theme/components/BlogPosts.astro +18 -18
- package/theme/components/BlogPosts.tsx +45 -45
- package/theme/components/BookButton.tsx +22 -22
- package/theme/components/Container.astro +11 -11
- package/theme/components/CookieConsent.astro +121 -121
- package/theme/components/EndBlock.astro +21 -21
- package/theme/components/FontPreload.astro +6 -6
- package/theme/components/Footer.astro +85 -85
- package/theme/components/Header.astro +186 -186
- package/theme/components/Icon.astro +16 -16
- package/theme/components/Img.astro +29 -29
- package/theme/components/Img.tsx +33 -33
- package/theme/components/Inline.astro +11 -11
- package/theme/components/Logo.astro +23 -23
- package/theme/components/LogoMark.astro +10 -10
- package/theme/components/Markdown.astro +17 -17
- package/theme/components/MobileMenu.tsx +224 -224
- package/theme/components/MobileNav.astro +33 -33
- package/theme/components/PageHeader.astro +22 -22
- package/theme/components/PageSection.astro +24 -24
- package/theme/components/PhoneButton.tsx +11 -11
- package/theme/components/Popup.astro +145 -145
- package/theme/components/PopupButton.tsx +24 -24
- package/theme/components/PopupView.tsx +48 -48
- package/theme/components/PostList.tsx +58 -58
- package/theme/components/PostMeta.tsx +17 -17
- package/theme/components/RichText.astro +28 -28
- package/theme/components/Section.astro +29 -29
- package/theme/components/SectionHeading.tsx +8 -8
- package/theme/components/TextSection.astro +16 -16
- package/theme/components/home/PageBlock.astro +19 -19
- package/theme/components/home/Text.astro +13 -13
- package/theme/components/loadPopup.ts +22 -22
- package/theme/components/loadPosts.ts +16 -16
- package/theme/components/posts.ts +15 -15
- package/theme/editor/looks.mjs +7 -7
- package/theme/editor/previews.tsx +56 -56
- package/theme/layouts/Base.astro +273 -273
- package/theme/layouts/PageLayout.astro +126 -126
- package/theme/layouts/PostLayout.astro +137 -137
- package/theme/scripts/motion.ts +29 -29
- package/theme/styles/base.css +320 -320
- package/theme/styles/blocks.css +1808 -1808
- package/theme/styles/components.css +206 -206
- package/theme/styles/global.css +1302 -1302
- package/theme/styles/ui.css +6544 -6544
- package/theme/templates/Blog.astro +38 -38
- package/theme/templates/Category.astro +19 -19
- package/theme/templates/Home.astro +36 -36
- package/theme/templates/New.astro +68 -68
- package/theme/templates/NotFound.astro +117 -117
- package/theme/theme.json +28 -28
- package/theme/ui/Badge.tsx +45 -45
- package/theme/ui/BottomSheet.tsx +148 -148
- package/theme/ui/Button.tsx +79 -79
- package/theme/ui/ButtonGroup.tsx +31 -31
- package/theme/ui/Card.tsx +45 -45
- package/theme/ui/Carousel.tsx +124 -124
- package/theme/ui/Checkbox.tsx +59 -59
- package/theme/ui/Chip.tsx +101 -101
- package/theme/ui/ChipSet.tsx +27 -27
- package/theme/ui/DatePicker.tsx +149 -149
- package/theme/ui/DatePickerContainer.tsx +316 -316
- package/theme/ui/Dialog.tsx +100 -100
- package/theme/ui/Divider.tsx +27 -27
- package/theme/ui/ExposedDropdownMenu.tsx +77 -77
- package/theme/ui/Fab.tsx +88 -88
- package/theme/ui/FabMenu.tsx +67 -67
- package/theme/ui/IconButton.tsx +78 -78
- package/theme/ui/List.tsx +287 -287
- package/theme/ui/LoadingIndicator.tsx +29 -29
- package/theme/ui/Menu.tsx +278 -278
- package/theme/ui/NavigationBar.tsx +62 -62
- package/theme/ui/NavigationDrawer.tsx +80 -80
- package/theme/ui/NavigationRail.tsx +154 -154
- package/theme/ui/ProgressIndicator.tsx +162 -162
- package/theme/ui/RadioButton.tsx +50 -50
- package/theme/ui/Search.tsx +129 -129
- package/theme/ui/SegmentedButton.tsx +49 -49
- package/theme/ui/SideSheet.tsx +54 -54
- package/theme/ui/Slider.tsx +155 -155
- package/theme/ui/Snackbar.tsx +67 -67
- package/theme/ui/SplitButton.tsx +67 -67
- package/theme/ui/Switch.tsx +60 -60
- package/theme/ui/Tabs.tsx +114 -114
- package/theme/ui/TextField.tsx +278 -278
- package/theme/ui/TimePicker.tsx +128 -128
- package/theme/ui/TimePickerContainer.tsx +234 -234
- package/theme/ui/Toolbar.tsx +67 -67
- package/theme/ui/Tooltip.tsx +58 -58
- package/theme/ui/TopAppBar.tsx +77 -77
- package/theme/ui/bottomSheet.script.ts +8 -8
- package/theme/ui/{bottomSheet.ts → bottomSheetControl.ts} +211 -211
- package/theme/ui/carousel.script.ts +28 -28
- package/theme/ui/carouselLayout.ts +229 -229
- package/theme/ui/checkbox.script.ts +8 -8
- package/theme/ui/datePicker.script.ts +302 -302
- package/theme/ui/dates.ts +185 -185
- package/theme/ui/{dialog.ts → dialogControl.ts} +48 -48
- package/theme/ui/dropdownMenu.script.ts +166 -166
- package/theme/ui/fabMenu.script.ts +57 -57
- package/theme/ui/navigationRail.script.ts +47 -47
- package/theme/ui/ripple.script.ts +159 -159
- package/theme/ui/search.script.ts +92 -92
- package/theme/ui/{sideSheet.ts → sideSheetControl.ts} +48 -48
- package/theme/ui/slider.script.ts +12 -12
- package/theme/ui/sliderUpdate.ts +52 -52
- package/theme/ui/snackbar.script.ts +16 -16
- package/theme/ui/splitButton.script.ts +63 -63
- package/theme/ui/tabs.script.ts +48 -48
- package/theme/ui/textField.script.ts +17 -17
- package/theme/ui/textFieldUpdate.ts +44 -44
- package/theme/ui/timePicker.script.ts +248 -248
- package/theme/ui/toolbar.script.ts +43 -43
- package/theme/ui/tooltip.script.ts +68 -68
- package/worker/consent.js +141 -141
- package/worker/editor-github.js +167 -167
- package/worker/editor-host.js +74 -74
- package/worker/files.js +401 -401
- package/worker/github.js +36 -36
- package/worker/index.js +188 -184
- package/worker/not-found.js +97 -97
- package/worker/oauth.js +95 -95
- package/worker/publish.js +52 -52
- package/worker/push.js +270 -270
- package/worker/r2-status.js +50 -50
- package/worker/session.js +58 -58
- package/worker/store.js +46 -0
package/site/tracking.client.ts
CHANGED
|
@@ -1,225 +1,225 @@
|
|
|
1
|
-
// The visitor's yes or no to each category of cookies in the cookie banner (@morphemecms/cms/lib/cookieCategories.mjs:
|
|
2
|
-
// preferences, statistics, marketing, targeting; necessary ones are not asked about), and the site's events. The apps that measure
|
|
3
|
-
// (Google Analytics, Google Ads, Google Tag Manager: src/apps/) load their tags with afterConsent, only after a yes to
|
|
4
|
-
// their category (Google consent mode, "basic": the Google tag is not loaded at all until the visitor says yes, and it
|
|
5
|
-
// is told which categories the visitor said yes to), and send the events on. Where events are sent from:
|
|
6
|
-
// `tracking.hosts` in packages/editor/site/tracking.ts.
|
|
7
|
-
//
|
|
8
|
-
// Events (GA4 event names; mark them as key events in GA4 and import them in Google Ads):
|
|
9
|
-
// - book_appointment: a time was booked (the app Calendly: in the booking sheet or on the booking page)
|
|
10
|
-
// - open_booking: the booking calendar was opened
|
|
11
|
-
// - click_to_call: a phone link was tapped
|
|
12
|
-
// - email_click: an e-mail link was clicked
|
|
13
|
-
import { CONSENT_CATEGORIES, isConsentCategory, type ConsentCategory } from '@morphemecms/cms/lib/cookieCategories.mjs';
|
|
14
|
-
import { cookieList } from './cookieList';
|
|
15
|
-
import { tracking } from './tracking';
|
|
16
|
-
import { site } from './site';
|
|
17
|
-
|
|
18
|
-
/** The visitor's yes (true) or no to each category the cookie banner asked about. */
|
|
19
|
-
export type Consent = Partial<Record<ConsentCategory, boolean>>;
|
|
20
|
-
const KEY = 'consent';
|
|
21
|
-
const MAX_AGE = 365 * 24 * 60 * 60 * 1000; // ask again after a year
|
|
22
|
-
|
|
23
|
-
declare global {
|
|
24
|
-
interface Window {
|
|
25
|
-
dataLayer: unknown[];
|
|
26
|
-
gtag: (...args: unknown[]) => void;
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
const params = new URLSearchParams(location.search);
|
|
31
|
-
try {
|
|
32
|
-
if (params.get('tracking') === 'test') sessionStorage.setItem('tracking-test', '1');
|
|
33
|
-
} catch {}
|
|
34
|
-
/** Whether events are sent from another address than the site's own (?tracking=test): GA4's debug mode. */
|
|
35
|
-
export const testing = (() => {
|
|
36
|
-
try {
|
|
37
|
-
return sessionStorage.getItem('tracking-test') === '1';
|
|
38
|
-
} catch {
|
|
39
|
-
return false;
|
|
40
|
-
}
|
|
41
|
-
})();
|
|
42
|
-
const live = tracking.hosts.includes(location.hostname) || testing;
|
|
43
|
-
|
|
44
|
-
/** The visitor's answer in the cookie banner, within the last year; undefined without one (or with one from before
|
|
45
|
-
* the banner asked about each category, which is asked again). */
|
|
46
|
-
export function storedConsent(): Consent | undefined {
|
|
47
|
-
try {
|
|
48
|
-
const saved = JSON.parse(localStorage.getItem(KEY) ?? 'null');
|
|
49
|
-
if (!saved || typeof saved.choices !== 'object' || !saved.choices || Date.now() - saved.at >= MAX_AGE) return undefined;
|
|
50
|
-
return Object.fromEntries(
|
|
51
|
-
Object.entries(saved.choices).filter(([category, yes]) => isConsentCategory(category) && typeof yes === 'boolean'),
|
|
52
|
-
) as Consent;
|
|
53
|
-
} catch {
|
|
54
|
-
return undefined;
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
const granted = (category: ConsentCategory) => storedConsent()?.[category] === true;
|
|
59
|
-
|
|
60
|
-
// Google's consent mode: what its tags may store, by the visitor's answer (statistics: Analytics; marketing: Ads). A yes
|
|
61
|
-
// to marketing is a yes to measuring whether the ads work; personalised ads (remarketing) only after a yes to targeted
|
|
62
|
-
// ads, which the banner asks about only when an app is set to use them (`cookies` in its index.ts), so a site that does
|
|
63
|
-
// not use them never allows them. Targeted ads store what ads do (ad_storage, ad_user_data) too.
|
|
64
|
-
function googleSignals() {
|
|
65
|
-
const yes = (...categories: ConsentCategory[]) => (categories.some(granted) ? 'granted' : 'denied');
|
|
66
|
-
return {
|
|
67
|
-
analytics_storage: yes('statistics'),
|
|
68
|
-
ad_storage: yes('marketing', 'targeting'),
|
|
69
|
-
ad_user_data: yes('marketing', 'targeting'),
|
|
70
|
-
ad_personalization: yes('targeting'),
|
|
71
|
-
};
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// Google signals (Analytics data joined with Google accounts, for ads and demographics) and ad personalisation in any
|
|
75
|
-
// tag, likewise only after a yes to targeted ads, whatever the accounts' own settings say.
|
|
76
|
-
function googleSettings() {
|
|
77
|
-
window.gtag('set', 'allow_google_signals', granted('targeting'));
|
|
78
|
-
window.gtag('set', 'allow_ad_personalization_signals', granted('targeting'));
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/** Google's data layer with the visitor's answer in consent mode, for a tag of Google's (googleTag, Google Tag
|
|
82
|
-
* Manager's container, src/apps/gtm/), set up once, before the first tag loads; a new answer updates it (choose). */
|
|
83
|
-
export function googleConsent() {
|
|
84
|
-
window.dataLayer = window.dataLayer || [];
|
|
85
|
-
if (typeof window.gtag === 'function') return;
|
|
86
|
-
window.gtag = function gtag() {
|
|
87
|
-
// eslint-disable-next-line prefer-rest-params
|
|
88
|
-
window.dataLayer.push(arguments);
|
|
89
|
-
};
|
|
90
|
-
window.gtag('consent', 'default', googleSignals());
|
|
91
|
-
googleSettings();
|
|
92
|
-
window.gtag('js', new Date());
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
let tagged = false;
|
|
96
|
-
|
|
97
|
-
/** The Google tag (gtag.js) with this ID (Google Analytics' G-…, Google Ads' AW-…), for the apps that measure with it
|
|
98
|
-
* (src/apps/google-analytics/, src/apps/google-ads/), in afterConsent: loaded once for all of them, each ID configured. */
|
|
99
|
-
export function googleTag(id: string, config: Record<string, unknown> = {}) {
|
|
100
|
-
googleConsent();
|
|
101
|
-
window.gtag('config', id, config);
|
|
102
|
-
if (tagged) return;
|
|
103
|
-
tagged = true;
|
|
104
|
-
const s = document.createElement('script');
|
|
105
|
-
s.async = true;
|
|
106
|
-
s.src = `https://www.googletagmanager.com/gtag/js?id=${encodeURIComponent(id)}`;
|
|
107
|
-
document.head.appendChild(s);
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
// The cookies of each category on the site's own address, to be removed after a no to it: those of the cookie list
|
|
111
|
-
// ("Lister" › "Cookies", `*` for the rest of a name), and the others Google's tag may set (GA4: _ga, _ga_<id>, and
|
|
112
|
-
// _gid and _gat from older tags; Ads: _gcl_au and the other _gcl_ ones).
|
|
113
|
-
const pattern = (name: string) => new RegExp(`^${name.replace(/[.+?^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '.*')}$`);
|
|
114
|
-
const COOKIES: Record<ConsentCategory, RegExp[]> = {
|
|
115
|
-
preferences: [],
|
|
116
|
-
statistics: [/^(_ga(_.+)?|_gid|_gat(_.+)?)$/],
|
|
117
|
-
marketing: [/^_gcl_.+$/],
|
|
118
|
-
targeting: [],
|
|
119
|
-
};
|
|
120
|
-
for (const c of cookieList) if (isConsentCategory(c.category)) COOKIES[c.category].push(pattern(c.name));
|
|
121
|
-
|
|
122
|
-
// Each one is removed on the hostname and on every parent domain (Google's are set on example.dk for
|
|
123
|
-
// www.example.dk).
|
|
124
|
-
function removeCookies(categories: ConsentCategory[]) {
|
|
125
|
-
const patterns = categories.flatMap((category) => COOKIES[category]);
|
|
126
|
-
const names = document.cookie
|
|
127
|
-
.split(';')
|
|
128
|
-
.map((c) => c.split('=')[0].trim())
|
|
129
|
-
.filter((name) => patterns.some((p) => p.test(name)));
|
|
130
|
-
if (!names.length) return;
|
|
131
|
-
const labels = location.hostname.split('.');
|
|
132
|
-
const domains = ['', ...labels.slice(0, -1).map((_, i) => `; domain=.${labels.slice(i).join('.')}`)];
|
|
133
|
-
for (const name of names)
|
|
134
|
-
for (const domain of domains) document.cookie = `${name}=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT${domain}`;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
/** The random id of this browser's answers in the consent log (packages/editor/worker/consent.js), shown in the cookie banner so the
|
|
138
|
-
* visitor can point to their own; null before the first answer. */
|
|
139
|
-
export function consentId(): string | null {
|
|
140
|
-
try {
|
|
141
|
-
const id = JSON.parse(localStorage.getItem(KEY) ?? 'null')?.id;
|
|
142
|
-
return typeof id === 'string' ? id : null;
|
|
143
|
-
} catch {
|
|
144
|
-
return null;
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
/** Saves the visitor's answer (a yes or no to each category the banner asks about), in the browser and in the consent
|
|
149
|
-
* log, starts what waited for a yes and removes the cookies of each category said no to. A no to all the categories of
|
|
150
|
-
* something already started on this page reloads it, so it stops. */
|
|
151
|
-
export function choose(consent: Consent) {
|
|
152
|
-
const id = consentId() ?? crypto.randomUUID();
|
|
153
|
-
try {
|
|
154
|
-
localStorage.setItem(KEY, JSON.stringify({ choices: consent, at: Date.now(), id }));
|
|
155
|
-
} catch {}
|
|
156
|
-
logConsent(id, consent);
|
|
157
|
-
if (typeof window.gtag === 'function') {
|
|
158
|
-
window.gtag('consent', 'update', googleSignals());
|
|
159
|
-
googleSettings();
|
|
160
|
-
}
|
|
161
|
-
removeCookies(CONSENT_CATEGORIES.filter((category) => !granted(category)));
|
|
162
|
-
if (started.some((categories) => !categories.some(granted))) return location.reload();
|
|
163
|
-
for (const wait of waiting.splice(0)) afterConsent(wait.categories, wait.run);
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
// The answer to the consent log on the site's Worker (/api/consent), also when the page reloads right after
|
|
167
|
-
// (keepalive). Not the answers of a test (?tracking=test: the cookie scan's visitors, src/apps/cookiescanner/).
|
|
168
|
-
function logConsent(id: string, consent: Consent) {
|
|
169
|
-
if (testing) return;
|
|
170
|
-
fetch('/api/consent', { method: 'POST', keepalive: true, body: JSON.stringify({ id, choices: consent }) }).catch(() => {});
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
export function track(name: string, data: Record<string, unknown> = {}) {
|
|
174
|
-
if (!CONSENT_CATEGORIES.some(granted)) return;
|
|
175
|
-
if (!live) return console.info('[tracking]', name, data);
|
|
176
|
-
// The apps that measure send it on (Google Analytics, Google Ads, Google Tag Manager: src/apps/).
|
|
177
|
-
dispatchEvent(new CustomEvent('tracking', { detail: { name, data } }));
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
// What the apps start after a yes (afterConsent), waiting for one; and the categories of each started on this page.
|
|
181
|
-
const waiting: { categories: ConsentCategory[]; run: () => void }[] = [];
|
|
182
|
-
const started: ConsentCategory[][] = [];
|
|
183
|
-
|
|
184
|
-
let told = false;
|
|
185
|
-
|
|
186
|
-
function start(categories: ConsentCategory[], run: () => void) {
|
|
187
|
-
if (!live) {
|
|
188
|
-
if (!told)
|
|
189
|
-
console.info(
|
|
190
|
-
`[tracking] Ikke på ${tracking.hosts[0]}: tags hentes ikke, og events logges kun her. Tilføj ?tracking=test for at sende dem.`,
|
|
191
|
-
);
|
|
192
|
-
told = true;
|
|
193
|
-
return;
|
|
194
|
-
}
|
|
195
|
-
started.push(categories);
|
|
196
|
-
run();
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
/** The categories an app's tag waits for, as its slot writes them on its <meta> (data-consent, from `cookies` in its
|
|
200
|
-
* index.ts). */
|
|
201
|
-
export const consentOf = (meta: HTMLElement | null) => (meta?.dataset.consent ?? '').split(' ').filter(isConsentCategory);
|
|
202
|
-
|
|
203
|
-
/** Runs `run` once the visitor has said yes to one of `categories` in the cookie banner (now, if they already have),
|
|
204
|
-
* on the site's own address (`hosts`, or with ?tracking=test): for an app that loads something that sets cookies
|
|
205
|
-
* (`cookies` in its index.ts, e.g. Google Analytics, src/apps/google-analytics/). A no to all of them after a yes
|
|
206
|
-
* reloads the page, so it stops. */
|
|
207
|
-
export function afterConsent(categories: ConsentCategory[], run: () => void) {
|
|
208
|
-
if (categories.some(granted)) start(categories, run);
|
|
209
|
-
else waiting.push({ categories, run });
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
// The cookies of each category the visitor has not said yes to, also any left from before (a yes earlier).
|
|
213
|
-
removeCookies(CONSENT_CATEGORIES.filter((category) => !granted(category)));
|
|
214
|
-
|
|
215
|
-
// A time was booked: an app that books says so with the event "booking:done" on the window, with how
|
|
216
|
-
// (`method`, e.g. "calendly": src/apps/calendly/).
|
|
217
|
-
addEventListener('booking:done', (e) => track('book_appointment', { method: (e as CustomEvent).detail?.method }));
|
|
218
|
-
|
|
219
|
-
document.addEventListener('click', (e) => {
|
|
220
|
-
const link = (e.target as Element | null)?.closest?.('a');
|
|
221
|
-
const href = link?.getAttribute('href') ?? '';
|
|
222
|
-
if (href.startsWith('tel:')) track('click_to_call', { link_url: href });
|
|
223
|
-
else if (href.startsWith('mailto:')) track('email_click', { link_url: href });
|
|
224
|
-
else if (href === site.bookingPath) track('open_booking', { page: location.pathname });
|
|
225
|
-
});
|
|
1
|
+
// The visitor's yes or no to each category of cookies in the cookie banner (@morphemecms/cms/lib/cookieCategories.mjs:
|
|
2
|
+
// preferences, statistics, marketing, targeting; necessary ones are not asked about), and the site's events. The apps that measure
|
|
3
|
+
// (Google Analytics, Google Ads, Google Tag Manager: src/apps/) load their tags with afterConsent, only after a yes to
|
|
4
|
+
// their category (Google consent mode, "basic": the Google tag is not loaded at all until the visitor says yes, and it
|
|
5
|
+
// is told which categories the visitor said yes to), and send the events on. Where events are sent from:
|
|
6
|
+
// `tracking.hosts` in packages/editor/site/tracking.ts.
|
|
7
|
+
//
|
|
8
|
+
// Events (GA4 event names; mark them as key events in GA4 and import them in Google Ads):
|
|
9
|
+
// - book_appointment: a time was booked (the app Calendly: in the booking sheet or on the booking page)
|
|
10
|
+
// - open_booking: the booking calendar was opened
|
|
11
|
+
// - click_to_call: a phone link was tapped
|
|
12
|
+
// - email_click: an e-mail link was clicked
|
|
13
|
+
import { CONSENT_CATEGORIES, isConsentCategory, type ConsentCategory } from '@morphemecms/cms/lib/cookieCategories.mjs';
|
|
14
|
+
import { cookieList } from './cookieList';
|
|
15
|
+
import { tracking } from './tracking';
|
|
16
|
+
import { site } from './site';
|
|
17
|
+
|
|
18
|
+
/** The visitor's yes (true) or no to each category the cookie banner asked about. */
|
|
19
|
+
export type Consent = Partial<Record<ConsentCategory, boolean>>;
|
|
20
|
+
const KEY = 'consent';
|
|
21
|
+
const MAX_AGE = 365 * 24 * 60 * 60 * 1000; // ask again after a year
|
|
22
|
+
|
|
23
|
+
declare global {
|
|
24
|
+
interface Window {
|
|
25
|
+
dataLayer: unknown[];
|
|
26
|
+
gtag: (...args: unknown[]) => void;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const params = new URLSearchParams(location.search);
|
|
31
|
+
try {
|
|
32
|
+
if (params.get('tracking') === 'test') sessionStorage.setItem('tracking-test', '1');
|
|
33
|
+
} catch {}
|
|
34
|
+
/** Whether events are sent from another address than the site's own (?tracking=test): GA4's debug mode. */
|
|
35
|
+
export const testing = (() => {
|
|
36
|
+
try {
|
|
37
|
+
return sessionStorage.getItem('tracking-test') === '1';
|
|
38
|
+
} catch {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
})();
|
|
42
|
+
const live = tracking.hosts.includes(location.hostname) || testing;
|
|
43
|
+
|
|
44
|
+
/** The visitor's answer in the cookie banner, within the last year; undefined without one (or with one from before
|
|
45
|
+
* the banner asked about each category, which is asked again). */
|
|
46
|
+
export function storedConsent(): Consent | undefined {
|
|
47
|
+
try {
|
|
48
|
+
const saved = JSON.parse(localStorage.getItem(KEY) ?? 'null');
|
|
49
|
+
if (!saved || typeof saved.choices !== 'object' || !saved.choices || Date.now() - saved.at >= MAX_AGE) return undefined;
|
|
50
|
+
return Object.fromEntries(
|
|
51
|
+
Object.entries(saved.choices).filter(([category, yes]) => isConsentCategory(category) && typeof yes === 'boolean'),
|
|
52
|
+
) as Consent;
|
|
53
|
+
} catch {
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const granted = (category: ConsentCategory) => storedConsent()?.[category] === true;
|
|
59
|
+
|
|
60
|
+
// Google's consent mode: what its tags may store, by the visitor's answer (statistics: Analytics; marketing: Ads). A yes
|
|
61
|
+
// to marketing is a yes to measuring whether the ads work; personalised ads (remarketing) only after a yes to targeted
|
|
62
|
+
// ads, which the banner asks about only when an app is set to use them (`cookies` in its index.ts), so a site that does
|
|
63
|
+
// not use them never allows them. Targeted ads store what ads do (ad_storage, ad_user_data) too.
|
|
64
|
+
function googleSignals() {
|
|
65
|
+
const yes = (...categories: ConsentCategory[]) => (categories.some(granted) ? 'granted' : 'denied');
|
|
66
|
+
return {
|
|
67
|
+
analytics_storage: yes('statistics'),
|
|
68
|
+
ad_storage: yes('marketing', 'targeting'),
|
|
69
|
+
ad_user_data: yes('marketing', 'targeting'),
|
|
70
|
+
ad_personalization: yes('targeting'),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Google signals (Analytics data joined with Google accounts, for ads and demographics) and ad personalisation in any
|
|
75
|
+
// tag, likewise only after a yes to targeted ads, whatever the accounts' own settings say.
|
|
76
|
+
function googleSettings() {
|
|
77
|
+
window.gtag('set', 'allow_google_signals', granted('targeting'));
|
|
78
|
+
window.gtag('set', 'allow_ad_personalization_signals', granted('targeting'));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Google's data layer with the visitor's answer in consent mode, for a tag of Google's (googleTag, Google Tag
|
|
82
|
+
* Manager's container, src/apps/gtm/), set up once, before the first tag loads; a new answer updates it (choose). */
|
|
83
|
+
export function googleConsent() {
|
|
84
|
+
window.dataLayer = window.dataLayer || [];
|
|
85
|
+
if (typeof window.gtag === 'function') return;
|
|
86
|
+
window.gtag = function gtag() {
|
|
87
|
+
// eslint-disable-next-line prefer-rest-params
|
|
88
|
+
window.dataLayer.push(arguments);
|
|
89
|
+
};
|
|
90
|
+
window.gtag('consent', 'default', googleSignals());
|
|
91
|
+
googleSettings();
|
|
92
|
+
window.gtag('js', new Date());
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
let tagged = false;
|
|
96
|
+
|
|
97
|
+
/** The Google tag (gtag.js) with this ID (Google Analytics' G-…, Google Ads' AW-…), for the apps that measure with it
|
|
98
|
+
* (src/apps/google-analytics/, src/apps/google-ads/), in afterConsent: loaded once for all of them, each ID configured. */
|
|
99
|
+
export function googleTag(id: string, config: Record<string, unknown> = {}) {
|
|
100
|
+
googleConsent();
|
|
101
|
+
window.gtag('config', id, config);
|
|
102
|
+
if (tagged) return;
|
|
103
|
+
tagged = true;
|
|
104
|
+
const s = document.createElement('script');
|
|
105
|
+
s.async = true;
|
|
106
|
+
s.src = `https://www.googletagmanager.com/gtag/js?id=${encodeURIComponent(id)}`;
|
|
107
|
+
document.head.appendChild(s);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// The cookies of each category on the site's own address, to be removed after a no to it: those of the cookie list
|
|
111
|
+
// ("Lister" › "Cookies", `*` for the rest of a name), and the others Google's tag may set (GA4: _ga, _ga_<id>, and
|
|
112
|
+
// _gid and _gat from older tags; Ads: _gcl_au and the other _gcl_ ones).
|
|
113
|
+
const pattern = (name: string) => new RegExp(`^${name.replace(/[.+?^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '.*')}$`);
|
|
114
|
+
const COOKIES: Record<ConsentCategory, RegExp[]> = {
|
|
115
|
+
preferences: [],
|
|
116
|
+
statistics: [/^(_ga(_.+)?|_gid|_gat(_.+)?)$/],
|
|
117
|
+
marketing: [/^_gcl_.+$/],
|
|
118
|
+
targeting: [],
|
|
119
|
+
};
|
|
120
|
+
for (const c of cookieList) if (isConsentCategory(c.category)) COOKIES[c.category].push(pattern(c.name));
|
|
121
|
+
|
|
122
|
+
// Each one is removed on the hostname and on every parent domain (Google's are set on example.dk for
|
|
123
|
+
// www.example.dk).
|
|
124
|
+
function removeCookies(categories: ConsentCategory[]) {
|
|
125
|
+
const patterns = categories.flatMap((category) => COOKIES[category]);
|
|
126
|
+
const names = document.cookie
|
|
127
|
+
.split(';')
|
|
128
|
+
.map((c) => c.split('=')[0].trim())
|
|
129
|
+
.filter((name) => patterns.some((p) => p.test(name)));
|
|
130
|
+
if (!names.length) return;
|
|
131
|
+
const labels = location.hostname.split('.');
|
|
132
|
+
const domains = ['', ...labels.slice(0, -1).map((_, i) => `; domain=.${labels.slice(i).join('.')}`)];
|
|
133
|
+
for (const name of names)
|
|
134
|
+
for (const domain of domains) document.cookie = `${name}=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT${domain}`;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** The random id of this browser's answers in the consent log (packages/editor/worker/consent.js), shown in the cookie banner so the
|
|
138
|
+
* visitor can point to their own; null before the first answer. */
|
|
139
|
+
export function consentId(): string | null {
|
|
140
|
+
try {
|
|
141
|
+
const id = JSON.parse(localStorage.getItem(KEY) ?? 'null')?.id;
|
|
142
|
+
return typeof id === 'string' ? id : null;
|
|
143
|
+
} catch {
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Saves the visitor's answer (a yes or no to each category the banner asks about), in the browser and in the consent
|
|
149
|
+
* log, starts what waited for a yes and removes the cookies of each category said no to. A no to all the categories of
|
|
150
|
+
* something already started on this page reloads it, so it stops. */
|
|
151
|
+
export function choose(consent: Consent) {
|
|
152
|
+
const id = consentId() ?? crypto.randomUUID();
|
|
153
|
+
try {
|
|
154
|
+
localStorage.setItem(KEY, JSON.stringify({ choices: consent, at: Date.now(), id }));
|
|
155
|
+
} catch {}
|
|
156
|
+
logConsent(id, consent);
|
|
157
|
+
if (typeof window.gtag === 'function') {
|
|
158
|
+
window.gtag('consent', 'update', googleSignals());
|
|
159
|
+
googleSettings();
|
|
160
|
+
}
|
|
161
|
+
removeCookies(CONSENT_CATEGORIES.filter((category) => !granted(category)));
|
|
162
|
+
if (started.some((categories) => !categories.some(granted))) return location.reload();
|
|
163
|
+
for (const wait of waiting.splice(0)) afterConsent(wait.categories, wait.run);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// The answer to the consent log on the site's Worker (/api/consent), also when the page reloads right after
|
|
167
|
+
// (keepalive). Not the answers of a test (?tracking=test: the cookie scan's visitors, src/apps/cookiescanner/).
|
|
168
|
+
function logConsent(id: string, consent: Consent) {
|
|
169
|
+
if (testing) return;
|
|
170
|
+
fetch('/api/consent', { method: 'POST', keepalive: true, body: JSON.stringify({ id, choices: consent }) }).catch(() => {});
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function track(name: string, data: Record<string, unknown> = {}) {
|
|
174
|
+
if (!CONSENT_CATEGORIES.some(granted)) return;
|
|
175
|
+
if (!live) return console.info('[tracking]', name, data);
|
|
176
|
+
// The apps that measure send it on (Google Analytics, Google Ads, Google Tag Manager: src/apps/).
|
|
177
|
+
dispatchEvent(new CustomEvent('tracking', { detail: { name, data } }));
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// What the apps start after a yes (afterConsent), waiting for one; and the categories of each started on this page.
|
|
181
|
+
const waiting: { categories: ConsentCategory[]; run: () => void }[] = [];
|
|
182
|
+
const started: ConsentCategory[][] = [];
|
|
183
|
+
|
|
184
|
+
let told = false;
|
|
185
|
+
|
|
186
|
+
function start(categories: ConsentCategory[], run: () => void) {
|
|
187
|
+
if (!live) {
|
|
188
|
+
if (!told)
|
|
189
|
+
console.info(
|
|
190
|
+
`[tracking] Ikke på ${tracking.hosts[0]}: tags hentes ikke, og events logges kun her. Tilføj ?tracking=test for at sende dem.`,
|
|
191
|
+
);
|
|
192
|
+
told = true;
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
started.push(categories);
|
|
196
|
+
run();
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** The categories an app's tag waits for, as its slot writes them on its <meta> (data-consent, from `cookies` in its
|
|
200
|
+
* index.ts). */
|
|
201
|
+
export const consentOf = (meta: HTMLElement | null) => (meta?.dataset.consent ?? '').split(' ').filter(isConsentCategory);
|
|
202
|
+
|
|
203
|
+
/** Runs `run` once the visitor has said yes to one of `categories` in the cookie banner (now, if they already have),
|
|
204
|
+
* on the site's own address (`hosts`, or with ?tracking=test): for an app that loads something that sets cookies
|
|
205
|
+
* (`cookies` in its index.ts, e.g. Google Analytics, src/apps/google-analytics/). A no to all of them after a yes
|
|
206
|
+
* reloads the page, so it stops. */
|
|
207
|
+
export function afterConsent(categories: ConsentCategory[], run: () => void) {
|
|
208
|
+
if (categories.some(granted)) start(categories, run);
|
|
209
|
+
else waiting.push({ categories, run });
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// The cookies of each category the visitor has not said yes to, also any left from before (a yes earlier).
|
|
213
|
+
removeCookies(CONSENT_CATEGORIES.filter((category) => !granted(category)));
|
|
214
|
+
|
|
215
|
+
// A time was booked: an app that books says so with the event "booking:done" on the window, with how
|
|
216
|
+
// (`method`, e.g. "calendly": src/apps/calendly/).
|
|
217
|
+
addEventListener('booking:done', (e) => track('book_appointment', { method: (e as CustomEvent).detail?.method }));
|
|
218
|
+
|
|
219
|
+
document.addEventListener('click', (e) => {
|
|
220
|
+
const link = (e.target as Element | null)?.closest?.('a');
|
|
221
|
+
const href = link?.getAttribute('href') ?? '';
|
|
222
|
+
if (href.startsWith('tel:')) track('click_to_call', { link_url: href });
|
|
223
|
+
else if (href.startsWith('mailto:')) track('email_click', { link_url: href });
|
|
224
|
+
else if (href === site.bookingPath) track('open_booking', { page: location.pathname });
|
|
225
|
+
});
|
package/site/tracking.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
// Where the site measures from: only these hostnames load the apps' tags (Google Analytics, Google Ads, Google Tag
|
|
2
|
-
// Manager, src/apps/) and send events (src/scripts/tracking.ts). Elsewhere (staging) events are only logged to the
|
|
3
|
-
// console, unless the address has ?tracking=test, which sends them (in GA4's debug mode). The IDs are the apps'
|
|
4
|
-
// settings (src/data/apps/).
|
|
5
|
-
import { config } from '@site/config';
|
|
6
|
-
|
|
7
|
-
export const tracking = {
|
|
8
|
-
hosts: [config.domain, `www.${config.domain}`],
|
|
9
|
-
};
|
|
1
|
+
// Where the site measures from: only these hostnames load the apps' tags (Google Analytics, Google Ads, Google Tag
|
|
2
|
+
// Manager, src/apps/) and send events (src/scripts/tracking.ts). Elsewhere (staging) events are only logged to the
|
|
3
|
+
// console, unless the address has ?tracking=test, which sends them (in GA4's debug mode). The IDs are the apps'
|
|
4
|
+
// settings (src/data/apps/).
|
|
5
|
+
import { config } from '@site/config';
|
|
6
|
+
|
|
7
|
+
export const tracking = {
|
|
8
|
+
hosts: [config.domain, `www.${config.domain}`],
|
|
9
|
+
};
|