@cogenta/starters 0.3.0 → 0.4.0
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/dist/blueprints/assets/photos/association/coat-bank.jpg +0 -0
- package/dist/blueprints/assets/photos/association/food-bank.jpg +0 -0
- package/dist/blueprints/assets/photos/association/garden-corner.jpg +0 -0
- package/dist/blueprints/assets/photos/association/garden-planting.jpg +0 -0
- package/dist/blueprints/assets/photos/association/hall.jpg +0 -0
- package/dist/blueprints/assets/photos/association/hero.jpg +0 -0
- package/dist/blueprints/assets/photos/association/homework-club.jpg +0 -0
- package/dist/blueprints/assets/photos/association/orientation.jpg +0 -0
- package/dist/blueprints/assets/photos/association/partner-fr-college.png +0 -0
- package/dist/blueprints/assets/photos/association/partner-fr-delcourt.png +0 -0
- package/dist/blueprints/assets/photos/association/partner-fr-fondation.png +0 -0
- package/dist/blueprints/assets/photos/association/partner-fr-habitat.png +0 -0
- package/dist/blueprints/assets/photos/association/partner-fr-mairie.png +0 -0
- package/dist/blueprints/assets/photos/association/partner-fr-pharmacie.png +0 -0
- package/dist/blueprints/assets/photos/association/supper.jpg +0 -0
- package/dist/blueprints/assets/photos/blog/desk-setup.jpg +0 -0
- package/dist/blueprints/assets/photos/blog/library.jpg +0 -0
- package/dist/blueprints/assets/photos/blog/notebook-and-coffee.jpg +0 -0
- package/dist/blueprints/assets/photos/blog/notebooks.jpg +0 -0
- package/dist/blueprints/assets/photos/blog/platform.jpg +0 -0
- package/dist/blueprints/assets/photos/blog/pour-over.jpg +0 -0
- package/dist/blueprints/assets/photos/blog/typing.jpg +0 -0
- package/dist/blueprints/assets/photos/documentation/delivery-flow-en.png +0 -0
- package/dist/blueprints/assets/photos/documentation/delivery-flow-fr.png +0 -0
- package/dist/blueprints/assets/photos/magazine/allotments.jpg +0 -0
- package/dist/blueprints/assets/photos/magazine/archive-stacks.jpg +0 -0
- package/dist/blueprints/assets/photos/magazine/baguettes.jpg +0 -0
- package/dist/blueprints/assets/photos/magazine/dining-room.jpg +0 -0
- package/dist/blueprints/assets/photos/magazine/empty-tram-stop.jpg +0 -0
- package/dist/blueprints/assets/photos/magazine/gallery.jpg +0 -0
- package/dist/blueprints/assets/photos/magazine/office-block-dusk.jpg +0 -0
- package/dist/blueprints/assets/photos/magazine/reading-room.jpg +0 -0
- package/dist/blueprints/assets/photos/magazine/reception-hall.jpg +0 -0
- package/dist/blueprints/assets/photos/magazine/theatre.jpg +0 -0
- package/dist/blueprints/assets/photos/magazine/town-hall.jpg +0 -0
- package/dist/blueprints/assets/photos/magazine/tram-stop.jpg +0 -0
- package/dist/blueprints/assets/photos/portfolio/coast-boats.jpg +0 -0
- package/dist/blueprints/assets/photos/portfolio/coast-cover.jpg +0 -0
- package/dist/blueprints/assets/photos/portfolio/coast-ridge.jpg +0 -0
- package/dist/blueprints/assets/photos/portfolio/coast-water.jpg +0 -0
- package/dist/blueprints/assets/photos/portfolio/forest-beech.jpg +0 -0
- package/dist/blueprints/assets/photos/portfolio/forest-birch.jpg +0 -0
- package/dist/blueprints/assets/photos/portfolio/forest-branches.jpg +0 -0
- package/dist/blueprints/assets/photos/portfolio/forest-cover.jpg +0 -0
- package/dist/blueprints/assets/photos/portfolio/glacier-cave.jpg +0 -0
- package/dist/blueprints/assets/photos/portfolio/glacier-cover.jpg +0 -0
- package/dist/blueprints/assets/photos/portfolio/glacier-melt.jpg +0 -0
- package/dist/blueprints/assets/photos/portfolio/glacier-tongue.jpg +0 -0
- package/dist/blueprints/assets/photos/portfolio/lagoon-cover.jpg +0 -0
- package/dist/blueprints/assets/photos/portfolio/lagoon-egret.jpg +0 -0
- package/dist/blueprints/assets/photos/portfolio/lagoon-shore.jpg +0 -0
- package/dist/blueprints/assets/photos/portfolio/lagoon-storm.jpg +0 -0
- package/dist/blueprints/assets/photos/portfolio/port-cover.jpg +0 -0
- package/dist/blueprints/assets/photos/portfolio/port-dusk.jpg +0 -0
- package/dist/blueprints/assets/photos/portfolio/port-marina.jpg +0 -0
- package/dist/blueprints/assets/photos/portfolio/port-quay.jpg +0 -0
- package/dist/blueprints/assets/photos/portfolio/towers-cover.jpg +0 -0
- package/dist/blueprints/assets/photos/portfolio/towers-green.jpg +0 -0
- package/dist/blueprints/assets/photos/portfolio/towers-single.jpg +0 -0
- package/dist/blueprints/assets/photos/portfolio/towers-valley.jpg +0 -0
- package/dist/blueprints/assets/photos/portfolio/vines-cover.jpg +0 -0
- package/dist/blueprints/assets/photos/portfolio/vines-grapes.jpg +0 -0
- package/dist/blueprints/assets/photos/portfolio/vines-shed.jpg +0 -0
- package/dist/blueprints/assets/photos/portfolio/vines-tree.jpg +0 -0
- package/dist/blueprints/assets/photos/restaurant/beetroot-carpaccio.jpg +0 -0
- package/dist/blueprints/assets/photos/restaurant/chocolate-tart.jpg +0 -0
- package/dist/blueprints/assets/photos/restaurant/cod-vin-jaune.jpg +0 -0
- package/dist/blueprints/assets/photos/restaurant/duck-confit.jpg +0 -0
- package/dist/blueprints/assets/photos/restaurant/grilled-octopus.jpg +0 -0
- package/dist/blueprints/assets/photos/restaurant/hero.jpg +0 -0
- package/dist/blueprints/assets/photos/restaurant/market-stall.jpg +0 -0
- package/dist/blueprints/assets/photos/restaurant/mushroom-risotto.jpg +0 -0
- package/dist/blueprints/assets/photos/restaurant/pike-quenelle.jpg +0 -0
- package/dist/blueprints/assets/photos/restaurant/poached-pear.jpg +0 -0
- package/dist/blueprints/assets/photos/restaurant/praline-cake.jpg +0 -0
- package/dist/blueprints/assets/photos/restaurant/pumpkin-soup.jpg +0 -0
- package/dist/blueprints/assets/photos/restaurant/street-burdeau.jpg +0 -0
- package/dist/blueprints/assets/photos/saas/api-webhooks-en.png +0 -0
- package/dist/blueprints/assets/photos/saas/api-webhooks-fr.png +0 -0
- package/dist/blueprints/assets/photos/saas/approvals-queue-en.png +0 -0
- package/dist/blueprints/assets/photos/saas/approvals-queue-fr.png +0 -0
- package/dist/blueprints/assets/photos/saas/audit-log-en.png +0 -0
- package/dist/blueprints/assets/photos/saas/audit-log-fr.png +0 -0
- package/dist/blueprints/assets/photos/saas/erp-sync-en.png +0 -0
- package/dist/blueprints/assets/photos/saas/erp-sync-fr.png +0 -0
- package/dist/blueprints/assets/photos/saas/logo-castellan-alimentaire.png +0 -0
- package/dist/blueprints/assets/photos/saas/logo-cliniques-du-vivier.png +0 -0
- package/dist/blueprints/assets/photos/saas/logo-marenne-logistique.png +0 -0
- package/dist/blueprints/assets/photos/saas/logo-nordgate-habitat.png +0 -0
- package/dist/blueprints/assets/photos/saas/logo-tessaly-energies.png +0 -0
- package/dist/blueprints/assets/photos/saas/office-london.jpg +0 -0
- package/dist/blueprints/assets/photos/saas/office-montreal.jpg +0 -0
- package/dist/blueprints/assets/photos/saas/office-paris.jpg +0 -0
- package/dist/blueprints/assets/photos/saas/office-toronto.jpg +0 -0
- package/dist/blueprints/assets/photos/saas/policy-editor-en.png +0 -0
- package/dist/blueprints/assets/photos/saas/policy-editor-fr.png +0 -0
- package/dist/blueprints/assets/photos/saas/security-racks.jpg +0 -0
- package/dist/blueprints/assets/photos/saas/spend-reporting-en.png +0 -0
- package/dist/blueprints/assets/photos/saas/spend-reporting-fr.png +0 -0
- package/dist/blueprints/assets/photos/saas/sso-scim-en.png +0 -0
- package/dist/blueprints/assets/photos/saas/sso-scim-fr.png +0 -0
- package/dist/blueprints/assets/photos/store/aleppo-soap.jpg +0 -0
- package/dist/blueprints/assets/photos/store/copper-pan.jpg +0 -0
- package/dist/blueprints/assets/photos/store/crochet-throw.jpg +0 -0
- package/dist/blueprints/assets/photos/store/enamel-mug.jpg +0 -0
- package/dist/blueprints/assets/photos/store/hero.jpg +0 -0
- package/dist/blueprints/assets/photos/store/knit-sweater.jpg +0 -0
- package/dist/blueprints/assets/photos/store/leather-boots.jpg +0 -0
- package/dist/blueprints/assets/photos/store/leather-duffel.jpg +0 -0
- package/dist/blueprints/assets/photos/store/leather-wallet.jpg +0 -0
- package/dist/blueprints/assets/photos/store/moka-pot.jpg +0 -0
- package/dist/blueprints/assets/photos/store/skillet.jpg +0 -0
- package/dist/blueprints/assets/photos/store/stoneware-bottles.jpg +0 -0
- package/dist/blueprints/assets/photos/store/story.jpg +0 -0
- package/dist/blueprints/assets/photos/store/turned-bowl.jpg +0 -0
- package/dist/blueprints/association-copy-en.d.ts +3 -0
- package/dist/blueprints/association-copy-en.d.ts.map +1 -0
- package/dist/blueprints/association-copy-en.js +834 -0
- package/dist/blueprints/association-copy-en.js.map +1 -0
- package/dist/blueprints/association-copy-fr.d.ts +3 -0
- package/dist/blueprints/association-copy-fr.d.ts.map +1 -0
- package/dist/blueprints/association-copy-fr.js +841 -0
- package/dist/blueprints/association-copy-fr.js.map +1 -0
- package/dist/blueprints/association-copy.d.ts +301 -0
- package/dist/blueprints/association-copy.d.ts.map +1 -0
- package/dist/blueprints/association-copy.js +17 -0
- package/dist/blueprints/association-copy.js.map +1 -0
- package/dist/blueprints/association-credits.d.ts +16 -0
- package/dist/blueprints/association-credits.d.ts.map +1 -0
- package/dist/blueprints/association-credits.js +80 -0
- package/dist/blueprints/association-credits.js.map +1 -0
- package/dist/blueprints/association-typography.d.ts +23 -0
- package/dist/blueprints/association-typography.d.ts.map +1 -0
- package/dist/blueprints/association-typography.js +55 -0
- package/dist/blueprints/association-typography.js.map +1 -0
- package/dist/blueprints/association.d.ts +86 -287
- package/dist/blueprints/association.d.ts.map +1 -1
- package/dist/blueprints/association.js +503 -1078
- package/dist/blueprints/association.js.map +1 -1
- package/dist/blueprints/blog-copy-en.d.ts +4 -0
- package/dist/blueprints/blog-copy-en.d.ts.map +1 -0
- package/dist/blueprints/blog-copy-en.js +701 -0
- package/dist/blueprints/blog-copy-en.js.map +1 -0
- package/dist/blueprints/blog-copy-fr.d.ts +12 -0
- package/dist/blueprints/blog-copy-fr.d.ts.map +1 -0
- package/dist/blueprints/blog-copy-fr.js +722 -0
- package/dist/blueprints/blog-copy-fr.js.map +1 -0
- package/dist/blueprints/blog-copy.d.ts +157 -0
- package/dist/blueprints/blog-copy.d.ts.map +1 -0
- package/dist/blueprints/blog-copy.js +2 -0
- package/dist/blueprints/blog-copy.js.map +1 -0
- package/dist/blueprints/blog-credits.d.ts +28 -0
- package/dist/blueprints/blog-credits.d.ts.map +1 -0
- package/dist/blueprints/blog-credits.js +73 -0
- package/dist/blueprints/blog-credits.js.map +1 -0
- package/dist/blueprints/blog-rich.d.ts +19 -0
- package/dist/blueprints/blog-rich.d.ts.map +1 -0
- package/dist/blueprints/blog-rich.js +51 -0
- package/dist/blueprints/blog-rich.js.map +1 -0
- package/dist/blueprints/blog.d.ts +51 -176
- package/dist/blueprints/blog.d.ts.map +1 -1
- package/dist/blueprints/blog.js +358 -947
- package/dist/blueprints/blog.js.map +1 -1
- package/dist/blueprints/content-pack.d.ts.map +1 -1
- package/dist/blueprints/content-pack.js +11 -16
- package/dist/blueprints/content-pack.js.map +1 -1
- package/dist/blueprints/content-packs.d.ts +2 -4
- package/dist/blueprints/content-packs.d.ts.map +1 -1
- package/dist/blueprints/content-packs.js +27 -15
- package/dist/blueprints/content-packs.js.map +1 -1
- package/dist/blueprints/documentation-copy-en.d.ts +3 -0
- package/dist/blueprints/documentation-copy-en.d.ts.map +1 -0
- package/dist/blueprints/documentation-copy-en.js +883 -0
- package/dist/blueprints/documentation-copy-en.js.map +1 -0
- package/dist/blueprints/documentation-copy-fr.d.ts +3 -0
- package/dist/blueprints/documentation-copy-fr.d.ts.map +1 -0
- package/dist/blueprints/documentation-copy-fr.js +939 -0
- package/dist/blueprints/documentation-copy-fr.js.map +1 -0
- package/dist/blueprints/documentation-copy.d.ts +52 -0
- package/dist/blueprints/documentation-copy.d.ts.map +1 -0
- package/dist/blueprints/documentation-copy.js +2 -0
- package/dist/blueprints/documentation-copy.js.map +1 -0
- package/dist/blueprints/documentation-writer.d.ts +67 -0
- package/dist/blueprints/documentation-writer.d.ts.map +1 -0
- package/dist/blueprints/documentation-writer.js +190 -0
- package/dist/blueprints/documentation-writer.js.map +1 -0
- package/dist/blueprints/documentation.d.ts +26 -29
- package/dist/blueprints/documentation.d.ts.map +1 -1
- package/dist/blueprints/documentation.js +106 -1064
- package/dist/blueprints/documentation.js.map +1 -1
- package/dist/blueprints/magazine-copy-en.d.ts +13 -0
- package/dist/blueprints/magazine-copy-en.d.ts.map +1 -0
- package/dist/blueprints/magazine-copy-en.js +553 -0
- package/dist/blueprints/magazine-copy-en.js.map +1 -0
- package/dist/blueprints/magazine-copy-fr.d.ts +14 -0
- package/dist/blueprints/magazine-copy-fr.d.ts.map +1 -0
- package/dist/blueprints/magazine-copy-fr.js +568 -0
- package/dist/blueprints/magazine-copy-fr.js.map +1 -0
- package/dist/blueprints/magazine-copy.d.ts +166 -0
- package/dist/blueprints/magazine-copy.d.ts.map +1 -0
- package/dist/blueprints/magazine-copy.js +58 -0
- package/dist/blueprints/magazine-copy.js.map +1 -0
- package/dist/blueprints/magazine-credits.d.ts +19 -0
- package/dist/blueprints/magazine-credits.d.ts.map +1 -0
- package/dist/blueprints/magazine-credits.js +109 -0
- package/dist/blueprints/magazine-credits.js.map +1 -0
- package/dist/blueprints/magazine.d.ts +30 -78
- package/dist/blueprints/magazine.d.ts.map +1 -1
- package/dist/blueprints/magazine.js +269 -690
- package/dist/blueprints/magazine.js.map +1 -1
- package/dist/blueprints/portfolio-copy-en.d.ts +4 -0
- package/dist/blueprints/portfolio-copy-en.d.ts.map +1 -0
- package/dist/blueprints/portfolio-copy-en.js +745 -0
- package/dist/blueprints/portfolio-copy-en.js.map +1 -0
- package/dist/blueprints/portfolio-copy-fr.d.ts +9 -0
- package/dist/blueprints/portfolio-copy-fr.d.ts.map +1 -0
- package/dist/blueprints/portfolio-copy-fr.js +737 -0
- package/dist/blueprints/portfolio-copy-fr.js.map +1 -0
- package/dist/blueprints/portfolio-copy.d.ts +204 -0
- package/dist/blueprints/portfolio-copy.d.ts.map +1 -0
- package/dist/blueprints/portfolio-copy.js +15 -0
- package/dist/blueprints/portfolio-copy.js.map +1 -0
- package/dist/blueprints/portfolio-credits.d.ts +18 -0
- package/dist/blueprints/portfolio-credits.d.ts.map +1 -0
- package/dist/blueprints/portfolio-credits.js +239 -0
- package/dist/blueprints/portfolio-credits.js.map +1 -0
- package/dist/blueprints/portfolio-typography.d.ts +19 -0
- package/dist/blueprints/portfolio-typography.d.ts.map +1 -0
- package/dist/blueprints/portfolio-typography.js +41 -0
- package/dist/blueprints/portfolio-typography.js.map +1 -0
- package/dist/blueprints/portfolio.d.ts +68 -157
- package/dist/blueprints/portfolio.d.ts.map +1 -1
- package/dist/blueprints/portfolio.js +475 -727
- package/dist/blueprints/portfolio.js.map +1 -1
- package/dist/blueprints/restaurant-copy-en.d.ts +3 -0
- package/dist/blueprints/restaurant-copy-en.d.ts.map +1 -0
- package/dist/blueprints/restaurant-copy-en.js +641 -0
- package/dist/blueprints/restaurant-copy-en.js.map +1 -0
- package/dist/blueprints/restaurant-copy-fr.d.ts +3 -0
- package/dist/blueprints/restaurant-copy-fr.d.ts.map +1 -0
- package/dist/blueprints/restaurant-copy-fr.js +665 -0
- package/dist/blueprints/restaurant-copy-fr.js.map +1 -0
- package/dist/blueprints/restaurant-copy.d.ts +234 -0
- package/dist/blueprints/restaurant-copy.d.ts.map +1 -0
- package/dist/blueprints/restaurant-copy.js +15 -0
- package/dist/blueprints/restaurant-copy.js.map +1 -0
- package/dist/blueprints/restaurant-credits.d.ts +18 -0
- package/dist/blueprints/restaurant-credits.d.ts.map +1 -0
- package/dist/blueprints/restaurant-credits.js +121 -0
- package/dist/blueprints/restaurant-credits.js.map +1 -0
- package/dist/blueprints/restaurant.d.ts +55 -128
- package/dist/blueprints/restaurant.d.ts.map +1 -1
- package/dist/blueprints/restaurant.js +425 -770
- package/dist/blueprints/restaurant.js.map +1 -1
- package/dist/blueprints/saas-copy-en.d.ts +3 -0
- package/dist/blueprints/saas-copy-en.d.ts.map +1 -0
- package/dist/blueprints/saas-copy-en.js +901 -0
- package/dist/blueprints/saas-copy-en.js.map +1 -0
- package/dist/blueprints/saas-copy-fr.d.ts +3 -0
- package/dist/blueprints/saas-copy-fr.d.ts.map +1 -0
- package/dist/blueprints/saas-copy-fr.js +912 -0
- package/dist/blueprints/saas-copy-fr.js.map +1 -0
- package/dist/blueprints/saas-copy.d.ts +266 -0
- package/dist/blueprints/saas-copy.d.ts.map +1 -0
- package/dist/blueprints/saas-copy.js +18 -0
- package/dist/blueprints/saas-copy.js.map +1 -0
- package/dist/blueprints/saas-credits.d.ts +22 -0
- package/dist/blueprints/saas-credits.d.ts.map +1 -0
- package/dist/blueprints/saas-credits.js +44 -0
- package/dist/blueprints/saas-credits.js.map +1 -0
- package/dist/blueprints/saas.d.ts +59 -203
- package/dist/blueprints/saas.d.ts.map +1 -1
- package/dist/blueprints/saas.js +505 -1108
- package/dist/blueprints/saas.js.map +1 -1
- package/dist/blueprints/store-copy-en.d.ts +4 -0
- package/dist/blueprints/store-copy-en.d.ts.map +1 -0
- package/dist/blueprints/store-copy-en.js +521 -0
- package/dist/blueprints/store-copy-en.js.map +1 -0
- package/dist/blueprints/store-copy-fr.d.ts +4 -0
- package/dist/blueprints/store-copy-fr.d.ts.map +1 -0
- package/dist/blueprints/store-copy-fr.js +521 -0
- package/dist/blueprints/store-copy-fr.js.map +1 -0
- package/dist/blueprints/store-copy.d.ts +219 -0
- package/dist/blueprints/store-copy.d.ts.map +1 -0
- package/dist/blueprints/store-copy.js +16 -0
- package/dist/blueprints/store-copy.js.map +1 -0
- package/dist/blueprints/store-credits.d.ts +3 -0
- package/dist/blueprints/store-credits.d.ts.map +1 -0
- package/dist/blueprints/store-credits.js +117 -0
- package/dist/blueprints/store-credits.js.map +1 -0
- package/dist/blueprints/store.d.ts +43 -231
- package/dist/blueprints/store.d.ts.map +1 -1
- package/dist/blueprints/store.js +466 -789
- package/dist/blueprints/store.js.map +1 -1
- package/dist/blueprints/vitrine-copy-en.d.ts.map +1 -1
- package/dist/blueprints/vitrine-copy-en.js +1 -0
- package/dist/blueprints/vitrine-copy-en.js.map +1 -1
- package/dist/blueprints/vitrine-copy-fr.d.ts.map +1 -1
- package/dist/blueprints/vitrine-copy-fr.js +1 -0
- package/dist/blueprints/vitrine-copy-fr.js.map +1 -1
- package/dist/blueprints/vitrine-copy.d.ts +1 -0
- package/dist/blueprints/vitrine-copy.d.ts.map +1 -1
- package/dist/blueprints/vitrine.d.ts +1 -0
- package/dist/blueprints/vitrine.d.ts.map +1 -1
- package/dist/blueprints/vitrine.js +24 -3
- package/dist/blueprints/vitrine.js.map +1 -1
- package/package.json +10 -10
- package/dist/blueprints/assets/photos/association/fundraising-dinner.jpg +0 -0
- package/dist/blueprints/assets/photos/association/volunteer-orientation.jpg +0 -0
- package/dist/blueprints/assets/photos/association/volunteer-portrait.jpg +0 -0
- package/dist/blueprints/assets/photos/association/winter-coat-collection.jpg +0 -0
- package/dist/blueprints/assets/photos/documentation/delivery-flow.png +0 -0
- package/dist/blueprints/assets/photos/magazine/archive-room.jpg +0 -0
- package/dist/blueprints/assets/photos/magazine/banquet-hall.jpg +0 -0
- package/dist/blueprints/assets/photos/magazine/city-hall.jpg +0 -0
- package/dist/blueprints/assets/photos/magazine/downtown-towers.jpg +0 -0
- package/dist/blueprints/assets/photos/magazine/gallery-opening.jpg +0 -0
- package/dist/blueprints/assets/photos/magazine/garden-volunteers.jpg +0 -0
- package/dist/blueprints/assets/photos/magazine/station-platform.jpg +0 -0
- package/dist/blueprints/assets/photos/magazine/theater-stage.jpg +0 -0
- package/dist/blueprints/assets/photos/portfolio/fenmore-branch.jpg +0 -0
- package/dist/blueprints/assets/photos/portfolio/fenmore-cover.jpg +0 -0
- package/dist/blueprints/assets/photos/portfolio/fenmore-stationery.jpg +0 -0
- package/dist/blueprints/assets/photos/portfolio/gannet-cover.jpg +0 -0
- package/dist/blueprints/assets/photos/portfolio/gannet-sardines.jpg +0 -0
- package/dist/blueprints/assets/photos/portfolio/gannet-shelf.jpg +0 -0
- package/dist/blueprints/assets/photos/portfolio/hollis-crane-cover.jpg +0 -0
- package/dist/blueprints/assets/photos/portfolio/hollis-crane-grid.jpg +0 -0
- package/dist/blueprints/assets/photos/portfolio/hollis-crane-salt-year.jpg +0 -0
- package/dist/blueprints/assets/photos/portfolio/hollis-crane-spines.jpg +0 -0
- package/dist/blueprints/assets/photos/portfolio/rookery-hall-brochure.jpg +0 -0
- package/dist/blueprints/assets/photos/portfolio/rookery-hall-cover.jpg +0 -0
- package/dist/blueprints/assets/photos/portfolio/rookery-hall-poster-carnival.jpg +0 -0
- package/dist/blueprints/assets/photos/portfolio/rookery-hall-poster-sibelius.jpg +0 -0
- package/dist/blueprints/assets/photos/portfolio/rookery-hall-series.jpg +0 -0
- package/dist/blueprints/assets/photos/portfolio/saltmarsh-quay-cover.jpg +0 -0
- package/dist/blueprints/assets/photos/portfolio/saltmarsh-quay-gate.jpg +0 -0
- package/dist/blueprints/assets/photos/portfolio/saltmarsh-quay-pictograms.jpg +0 -0
- package/dist/blueprints/assets/photos/portfolio/saltmarsh-quay-totem.jpg +0 -0
- package/dist/blueprints/assets/photos/portfolio/tidewater-cover.jpg +0 -0
- package/dist/blueprints/assets/photos/portfolio/tidewater-data.jpg +0 -0
- package/dist/blueprints/assets/photos/portfolio/tidewater-report-cover.jpg +0 -0
- package/dist/blueprints/assets/photos/portfolio/warp-and-weft-cover.jpg +0 -0
- package/dist/blueprints/assets/photos/portfolio/warp-and-weft-room.jpg +0 -0
- package/dist/blueprints/assets/photos/portfolio/westmoor-rail-components.jpg +0 -0
- package/dist/blueprints/assets/photos/portfolio/westmoor-rail-cover.jpg +0 -0
- package/dist/blueprints/assets/photos/portfolio/westmoor-rail-departures.jpg +0 -0
- package/dist/blueprints/assets/photos/restaurant/beetroot-goats-curd.jpg +0 -0
- package/dist/blueprints/assets/photos/restaurant/ceps-risotto.jpg +0 -0
- package/dist/blueprints/assets/photos/restaurant/duck-leg-red-cabbage.jpg +0 -0
- package/dist/blueprints/assets/photos/restaurant/hake-green-peppercorns.jpg +0 -0
- package/dist/blueprints/assets/photos/restaurant/vegetable-broth.jpg +0 -0
- package/dist/blueprints/assets/photos/saas/adrian-tan.jpg +0 -0
- package/dist/blueprints/assets/photos/saas/api-webhooks.png +0 -0
- package/dist/blueprints/assets/photos/saas/approvals-queue.png +0 -0
- package/dist/blueprints/assets/photos/saas/audit-log.png +0 -0
- package/dist/blueprints/assets/photos/saas/erp-sync.png +0 -0
- package/dist/blueprints/assets/photos/saas/policy-editor.png +0 -0
- package/dist/blueprints/assets/photos/saas/spend-reporting.png +0 -0
- package/dist/blueprints/assets/photos/saas/sso-scim.png +0 -0
- package/dist/blueprints/assets/photos/store/brushed-cotton-overshirt.jpg +0 -0
- package/dist/blueprints/assets/photos/store/cable-knit-beanie.jpg +0 -0
- package/dist/blueprints/assets/photos/store/canvas-shoulder-bag.jpg +0 -0
- package/dist/blueprints/assets/photos/store/canvas-tote.jpg +0 -0
- package/dist/blueprints/assets/photos/store/cast-iron-skillet.jpg +0 -0
- package/dist/blueprints/assets/photos/store/field-jacket.jpg +0 -0
- package/dist/blueprints/assets/photos/store/heavyweight-t-shirt.jpg +0 -0
- package/dist/blueprints/assets/photos/store/leather-card-holder.jpg +0 -0
- package/dist/blueprints/assets/photos/store/linen-table-runner.jpg +0 -0
- package/dist/blueprints/assets/photos/store/porcelain-pour-over-set.jpg +0 -0
- package/dist/blueprints/assets/photos/store/striped-wool-blanket.jpg +0 -0
package/dist/blueprints/saas.js
CHANGED
|
@@ -2,36 +2,15 @@ import { CogentaError } from '@cogenta/core';
|
|
|
2
2
|
import { createContentStore, createMenuStore, defineCollection, ensureMenuTables, f, validateCollectionSet, } from '@cogenta/schema';
|
|
3
3
|
import { coverArt, logoArt } from '../demo-art/compositions.js';
|
|
4
4
|
import { definePageCollection, richTextParagraph, SEO_FIELDS, toBlockZoneEntry, } from './content-pack.js';
|
|
5
|
+
import { SAAS_COPY_EN } from './saas-copy-en.js';
|
|
6
|
+
import { SAAS_COPY_FR } from './saas-copy-fr.js';
|
|
7
|
+
import { SAAS_PHOTO_CREDITS } from './saas-credits.js';
|
|
5
8
|
import { STARTING_SKINS } from './starting-skins.js';
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
* The product is named after the site (`SeedContext.siteName`), falling back
|
|
12
|
-
* to "Ledgerline". Its customers, people and figures are invented and
|
|
13
|
-
* consistent from page to page: the customer quoted on the home page is the
|
|
14
|
-
* workspace shown in every screenshot.
|
|
15
|
-
*
|
|
16
|
-
* Content model:
|
|
17
|
-
*
|
|
18
|
-
* - `feature`: one page per capability, routed at `/features/:slug`, with a
|
|
19
|
-
* short summary, an icon name (`@cogenta/theme-kit`'s `renderIcon`), a
|
|
20
|
-
* screenshot of that part of the product as its cover, and a block zone for
|
|
21
|
-
* the page itself.
|
|
22
|
-
* - `changelog`: dated product updates at `/changelog/:slug`. `publishedAt`
|
|
23
|
-
* is declared so a seeded update carries the day it shipped rather than the
|
|
24
|
-
* instant of the scaffold.
|
|
25
|
-
* - `page`: home, product, pricing, security, changelog, company, book a
|
|
26
|
-
* demo, legal and privacy.
|
|
27
|
-
*
|
|
28
|
-
* Pricing tiers stay page-authored content (a `pricingTable` block): a plan
|
|
29
|
-
* has no lifecycle of its own. There is no signup or billing system behind
|
|
30
|
-
* this site, so no action pretends there is one: every "Start a trial" and
|
|
31
|
-
* "Book a demo" leads to the `demo` page, which says how a trial is set up by
|
|
32
|
-
* a person, and gives a real email address and telephone number.
|
|
33
|
-
*/
|
|
34
|
-
export const DEFAULT_PRODUCT_NAME = 'Ledgerline';
|
|
9
|
+
export const DEFAULT_PRODUCT_NAME = SAAS_COPY_EN.defaultProductName;
|
|
10
|
+
/** The copy for a site's default locale: French for any `fr*` locale, English otherwise. */
|
|
11
|
+
export function saasCopyFor(locale) {
|
|
12
|
+
return locale.toLowerCase().startsWith('fr') ? SAAS_COPY_FR : SAAS_COPY_EN;
|
|
13
|
+
}
|
|
35
14
|
function nameOf(siteName) {
|
|
36
15
|
const trimmed = siteName?.trim();
|
|
37
16
|
return trimmed === undefined || trimmed === '' ? DEFAULT_PRODUCT_NAME : trimmed;
|
|
@@ -46,8 +25,7 @@ export function saasEmail(siteName, mailbox = 'hello') {
|
|
|
46
25
|
return `${mailbox}@${domain === '' ? 'example' : domain}.com`;
|
|
47
26
|
}
|
|
48
27
|
/** A London number in the range Ofcom reserves for drama: it rings nobody. */
|
|
49
|
-
export const SAAS_PHONE =
|
|
50
|
-
const SAAS_PHONE_HREF = 'tel:+442079460321';
|
|
28
|
+
export const SAAS_PHONE = SAAS_COPY_EN.phone.display;
|
|
51
29
|
// ---------------------------------------------------------------------------
|
|
52
30
|
// Collections
|
|
53
31
|
// ---------------------------------------------------------------------------
|
|
@@ -56,62 +34,57 @@ const EDITORIAL_PERMISSIONS = {
|
|
|
56
34
|
create: ['editor', 'admin'],
|
|
57
35
|
update: ['editor', 'admin'],
|
|
58
36
|
delete: ['admin'],
|
|
37
|
+
// An undeclared action grants nobody, admin included: without this line
|
|
38
|
+
// nothing on this site could ever leave draft.
|
|
39
|
+
publish: ['admin'],
|
|
59
40
|
};
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
max:
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
},
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
},
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
...SEO_FIELDS,
|
|
108
|
-
},
|
|
109
|
-
indexes: [['publishedAt', 'desc'], ['slug']],
|
|
110
|
-
permissions: EDITORIAL_PERMISSIONS,
|
|
111
|
-
});
|
|
112
|
-
export const page = definePageCollection('/:slug');
|
|
113
|
-
export const SAAS_COLLECTIONS = [feature, changelog, page];
|
|
114
|
-
validateCollectionSet(SAAS_COLLECTIONS);
|
|
41
|
+
/** The content model, labelled and routed in the copy's language. */
|
|
42
|
+
export function saasSchema(copy) {
|
|
43
|
+
const feature = defineCollection({
|
|
44
|
+
name: 'feature',
|
|
45
|
+
versioning: { drafts: true, history: true },
|
|
46
|
+
labels: copy.schema.labels.feature,
|
|
47
|
+
routing: { pattern: copy.schema.routes.feature },
|
|
48
|
+
fields: {
|
|
49
|
+
name: f.text({ required: true, max: 120 }),
|
|
50
|
+
slug: f.slug({ from: 'name', unique: true }),
|
|
51
|
+
description: f.text({ max: 300, multiline: true }),
|
|
52
|
+
icon: f.text({ max: 64 }),
|
|
53
|
+
coverImage: f.media({ accept: ['image'] }),
|
|
54
|
+
blocks: f.blocks({ required: true }),
|
|
55
|
+
...SEO_FIELDS,
|
|
56
|
+
},
|
|
57
|
+
indexes: [['slug']],
|
|
58
|
+
permissions: EDITORIAL_PERMISSIONS,
|
|
59
|
+
});
|
|
60
|
+
const changelog = defineCollection({
|
|
61
|
+
name: 'changelog',
|
|
62
|
+
versioning: { drafts: true, history: true },
|
|
63
|
+
labels: copy.schema.labels.changelog,
|
|
64
|
+
routing: { pattern: copy.schema.routes.changelog },
|
|
65
|
+
fields: {
|
|
66
|
+
title: f.text({ required: true, max: 200 }),
|
|
67
|
+
slug: f.slug({ from: 'title', unique: true }),
|
|
68
|
+
summary: f.text({ max: 300, multiline: true }),
|
|
69
|
+
// Declared so a seeded update can carry the day it shipped: without it
|
|
70
|
+
// every entry's date would be the instant of the scaffold.
|
|
71
|
+
publishedAt: f.datetime(),
|
|
72
|
+
blocks: f.blocks({ required: true }),
|
|
73
|
+
...SEO_FIELDS,
|
|
74
|
+
},
|
|
75
|
+
indexes: [['publishedAt', 'desc'], ['slug']],
|
|
76
|
+
permissions: EDITORIAL_PERMISSIONS,
|
|
77
|
+
});
|
|
78
|
+
const page = definePageCollection('/:slug');
|
|
79
|
+
const collections = [feature, changelog, page];
|
|
80
|
+
validateCollectionSet(collections);
|
|
81
|
+
return { feature, changelog, page, collections };
|
|
82
|
+
}
|
|
83
|
+
const ENGLISH_SCHEMA = saasSchema(SAAS_COPY_EN);
|
|
84
|
+
export const feature = ENGLISH_SCHEMA.feature;
|
|
85
|
+
export const changelog = ENGLISH_SCHEMA.changelog;
|
|
86
|
+
export const page = ENGLISH_SCHEMA.page;
|
|
87
|
+
export const SAAS_COLLECTIONS = ENGLISH_SCHEMA.collections;
|
|
115
88
|
// ---------------------------------------------------------------------------
|
|
116
89
|
// Rich text
|
|
117
90
|
// ---------------------------------------------------------------------------
|
|
@@ -155,263 +128,48 @@ function qa(key, items) {
|
|
|
155
128
|
answer: richTextParagraph(`${key}-${index}-a`, answer),
|
|
156
129
|
}));
|
|
157
130
|
}
|
|
131
|
+
function block(key, type, data) {
|
|
132
|
+
return { _key: key, _type: type, _version: BLOCK_VERSION, ...data };
|
|
133
|
+
}
|
|
158
134
|
// ---------------------------------------------------------------------------
|
|
159
|
-
//
|
|
135
|
+
// Addresses
|
|
136
|
+
// ---------------------------------------------------------------------------
|
|
137
|
+
/** `/features/:slug` with a slug in it. */
|
|
138
|
+
function routeTo(pattern, slug) {
|
|
139
|
+
return pattern.replace(':slug', slug);
|
|
140
|
+
}
|
|
141
|
+
/** The path of a page of the site, in the copy's language. */
|
|
142
|
+
function pagePath(copy, key) {
|
|
143
|
+
return `/${copy.slugs[key]}`;
|
|
144
|
+
}
|
|
145
|
+
function linkTarget(copy, link) {
|
|
146
|
+
return 'page' in link
|
|
147
|
+
? pagePath(copy, link.page)
|
|
148
|
+
: routeTo(copy.schema.routes.feature, link.feature);
|
|
149
|
+
}
|
|
150
|
+
// ---------------------------------------------------------------------------
|
|
151
|
+
// Features and changelog
|
|
160
152
|
// ---------------------------------------------------------------------------
|
|
161
153
|
/** The customers whose wordmarks appear on the site, in the order of the strip. */
|
|
162
|
-
export const SAAS_CUSTOMERS =
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
{ key: 'logo-orrin-and-vale', name: 'Orrin & Vale' },
|
|
168
|
-
{ key: 'logo-northgate-homes', name: 'Northgate Homes' },
|
|
169
|
-
];
|
|
170
|
-
export const SAAS_DEMO_FEATURES = [
|
|
171
|
-
{
|
|
172
|
-
name: 'Approval routing',
|
|
173
|
-
slug: 'approval-routing',
|
|
174
|
-
short: 'Rules on amount, cost centre and vendor send each request to the right approvers.',
|
|
175
|
-
description: 'Write your approval matrix once, as rules on amount, cost centre, vendor and category, and every request finds its approvers without anyone forwarding it.',
|
|
176
|
-
icon: 'layers',
|
|
177
|
-
screenshot: 'policy-editor',
|
|
178
|
-
body: (name) => [
|
|
179
|
-
{ h2: 'Your approval matrix, as rules' },
|
|
180
|
-
{
|
|
181
|
-
p: `Most finance teams already have an approval matrix: a spreadsheet that says who signs off on what, above which amount, for which cost centre. In ${name} that spreadsheet becomes a policy. Each row is a step with its conditions, its approvers and a deadline, and a request is matched against the policy the moment it is submitted.`,
|
|
182
|
-
},
|
|
183
|
-
{
|
|
184
|
-
p: 'Approvers can be named people, roles such as the budget owner of a cost centre, or groups synced from your identity provider. Steps run in sequence by default. A step can also run in parallel with another, so procurement can review a new vendor while the budget owner reviews the spend.',
|
|
185
|
-
},
|
|
186
|
-
{ h2: 'Change it safely' },
|
|
187
|
-
{
|
|
188
|
-
p: 'Policies are versioned. An edit is saved as a draft, and before you publish it you can run it against the requests of the last 90 days to see which of them would have been routed differently. Every published version stays in the audit log, with the name of the person who published it.',
|
|
189
|
-
},
|
|
190
|
-
{
|
|
191
|
-
ul: [
|
|
192
|
-
'Conditions on amount, currency, cost centre, vendor, category and any custom field',
|
|
193
|
-
'Deadlines in business days, with reminders the day before',
|
|
194
|
-
'Delegates for approvers who are away, and a fallback to their manager',
|
|
195
|
-
'Separate policies for purchase requests, vendor invoices and contract changes',
|
|
196
|
-
],
|
|
197
|
-
},
|
|
198
|
-
],
|
|
199
|
-
details: [
|
|
200
|
-
[
|
|
201
|
-
'What happens when nobody matches a request?',
|
|
202
|
-
'It goes to the default step you choose for the policy, usually the finance controller, and the request is flagged so the gap in the matrix can be fixed.',
|
|
203
|
-
],
|
|
204
|
-
[
|
|
205
|
-
'Can one person approve two steps?',
|
|
206
|
-
'Only if you allow it. By default a person who approved one step is skipped on the next, and the step moves to the next eligible approver.',
|
|
207
|
-
],
|
|
208
|
-
[
|
|
209
|
-
'How many policies can we have?',
|
|
210
|
-
'Ten on the Team plan and as many as you need on Business and Enterprise.',
|
|
211
|
-
],
|
|
212
|
-
],
|
|
213
|
-
},
|
|
214
|
-
{
|
|
215
|
-
name: 'Audit log',
|
|
216
|
-
slug: 'audit-log',
|
|
217
|
-
short: 'Every edit and decision is written once and never rewritten. Auditors read it directly.',
|
|
218
|
-
description: 'Every request, edit, comment and decision is written to a log that cannot be rewritten, and your auditors can filter and export it themselves.',
|
|
219
|
-
icon: 'shield',
|
|
220
|
-
screenshot: 'audit-log',
|
|
221
|
-
body: (name) => [
|
|
222
|
-
{ h2: 'One record for every decision' },
|
|
223
|
-
{
|
|
224
|
-
p: `When a request is created, changed, commented on or decided, ${name} writes an event: who did it, what changed, when, and from where. Events are appended and never edited. Each one carries a hash of the event before it, so a gap or a change anywhere in the chain is detected the next time the log is verified.`,
|
|
225
|
-
},
|
|
226
|
-
{
|
|
227
|
-
p: 'The log records what people do in the browser and what integrations do on their behalf. A bill posted to NetSuite, a budget line read from your ERP and a decision made from a Slack message all appear in the same list, in order.',
|
|
228
|
-
},
|
|
229
|
-
{ h2: 'Evidence your auditors can pull themselves' },
|
|
230
|
-
{
|
|
231
|
-
p: 'Give an external auditor a read-only role limited to a date range. They can filter by request, person, event or amount and export exactly the sample they need, without a screenshot from your team and without seeing anything else in the workspace.',
|
|
232
|
-
},
|
|
233
|
-
{
|
|
234
|
-
ul: [
|
|
235
|
-
'Exports as CSV and PDF, signed so a file can be checked against the log',
|
|
236
|
-
'Retention of one year on Team, seven years on Business and ten years on Enterprise',
|
|
237
|
-
'Chain verification on demand and every night',
|
|
238
|
-
],
|
|
239
|
-
},
|
|
240
|
-
],
|
|
241
|
-
details: [
|
|
242
|
-
[
|
|
243
|
-
'Can an administrator delete an event?',
|
|
244
|
-
'No. Administrators can hide personal data from an event after a valid erasure request, and the hiding is itself recorded as an event.',
|
|
245
|
-
],
|
|
246
|
-
[
|
|
247
|
-
'Which time zone does the log use?',
|
|
248
|
-
'Events are stored in UTC and shown in the time zone of the person reading them. Exports use UTC.',
|
|
249
|
-
],
|
|
250
|
-
],
|
|
251
|
-
},
|
|
252
|
-
{
|
|
253
|
-
name: 'ERP sync',
|
|
254
|
-
slug: 'erp-sync',
|
|
255
|
-
short: 'Approved bills and orders post to NetSuite, Xero, QuickBooks or Business Central.',
|
|
256
|
-
description: 'Approved vendor bills and purchase orders post to your ERP within a minute, and budget lines come back every fifteen minutes so approvers see what is left.',
|
|
257
|
-
icon: 'cloud',
|
|
258
|
-
screenshot: 'erp-sync',
|
|
259
|
-
body: (name) => [
|
|
260
|
-
{ h2: 'Approvals that finish in the ledger' },
|
|
261
|
-
{
|
|
262
|
-
p: `An approval is only finished when the ERP knows about it. ${name} connects to NetSuite, Xero, QuickBooks Online and Microsoft Dynamics 365 Business Central. When the last step approves, the vendor bill or purchase order is created or updated in the ERP with the approvers, the dates and a link back to the request.`,
|
|
263
|
-
},
|
|
264
|
-
{
|
|
265
|
-
p: 'The connection works in both directions. Vendors, cost centres, subsidiaries and budget lines are read from the ERP, so a request is coded the same way your books are, and an approver sees the remaining budget before deciding.',
|
|
266
|
-
},
|
|
267
|
-
{ h2: 'Failures you can see' },
|
|
268
|
-
{
|
|
269
|
-
p: 'Every sync is listed with its result. When the ERP rejects a record or limits the rate of calls, the sync is retried with a delay and the finance team is told if it still fails after an hour. Nothing is dropped quietly.',
|
|
270
|
-
},
|
|
271
|
-
{
|
|
272
|
-
ul: [
|
|
273
|
-
'Mapping for subsidiaries, currencies and tax codes',
|
|
274
|
-
'Posting as draft or approved, per record type',
|
|
275
|
-
'A nightly CSV import for ERPs without an API',
|
|
276
|
-
],
|
|
277
|
-
},
|
|
278
|
-
],
|
|
279
|
-
details: [
|
|
280
|
-
[
|
|
281
|
-
'How long does a connection take to set up?',
|
|
282
|
-
'Usually an hour with your ERP administrator. We send the list of permissions the integration user needs beforehand.',
|
|
283
|
-
],
|
|
284
|
-
[
|
|
285
|
-
'Does the sync write anything we did not approve?',
|
|
286
|
-
'No. It writes approved records and the approval details, and reads the reference data listed above.',
|
|
287
|
-
],
|
|
288
|
-
],
|
|
289
|
-
},
|
|
290
|
-
{
|
|
291
|
-
name: 'SSO and SCIM',
|
|
292
|
-
slug: 'sso-and-scim',
|
|
293
|
-
short: 'Sign in with Okta, Entra ID or Google. Access follows your directory.',
|
|
294
|
-
description: 'Members sign in through your identity provider, and SCIM keeps people, groups and roles in step with your directory.',
|
|
295
|
-
icon: 'lock',
|
|
296
|
-
screenshot: 'sso-scim',
|
|
297
|
-
body: () => [
|
|
298
|
-
{ h2: 'Sign-in through your identity provider' },
|
|
299
|
-
{
|
|
300
|
-
p: 'SAML single sign-on works with Okta, Microsoft Entra ID, Google Workspace and any provider that follows the standard. Once it is on, you can turn password sign-in off for the whole workspace, and the session length follows the policy you set.',
|
|
301
|
-
},
|
|
302
|
-
{ h2: 'Provisioning from your directory' },
|
|
303
|
-
{
|
|
304
|
-
p: 'With SCIM, a person added to a group in your directory appears in the workspace with the right role, and a person removed from the directory loses access at once. Groups map to roles, so the finance controllers group in Okta becomes the finance approvers in the product without anyone updating a list by hand.',
|
|
305
|
-
},
|
|
306
|
-
{ h2: 'Access that ends on time' },
|
|
307
|
-
{
|
|
308
|
-
p: 'Auditors and contractors can be given an end date when they are invited. On that date their access ends, any step still waiting on them moves to the person who invited them, and the change is written to the audit log.',
|
|
309
|
-
},
|
|
310
|
-
{
|
|
311
|
-
ul: [
|
|
312
|
-
'Roles for requesters, approvers, accounts payable, administrators and auditors',
|
|
313
|
-
'Time-limited auditor access, ended automatically on the date you set',
|
|
314
|
-
'Sign-in events recorded in the audit log',
|
|
315
|
-
],
|
|
316
|
-
},
|
|
317
|
-
],
|
|
318
|
-
details: [
|
|
319
|
-
[
|
|
320
|
-
'Which plans include SSO?',
|
|
321
|
-
'Google sign-in is included on every plan. SAML and SCIM are included on Business and Enterprise.',
|
|
322
|
-
],
|
|
323
|
-
[
|
|
324
|
-
'What happens to open approvals when someone leaves?',
|
|
325
|
-
'Their pending steps move to their delegate, or to their manager if they had none, and the reassignment is recorded.',
|
|
326
|
-
],
|
|
327
|
-
],
|
|
328
|
-
},
|
|
329
|
-
{
|
|
330
|
-
name: 'Spend reporting',
|
|
331
|
-
slug: 'spend-reporting',
|
|
332
|
-
short: 'See where requests wait, who is slow and which budgets are running over.',
|
|
333
|
-
description: 'Reports on time to approve, spend against budget and requests waiting too long, built from the same records as the audit log.',
|
|
334
|
-
icon: 'chart',
|
|
335
|
-
screenshot: 'spend-reporting',
|
|
336
|
-
body: () => [
|
|
337
|
-
{ h2: 'Where approvals wait' },
|
|
338
|
-
{
|
|
339
|
-
p: 'The time-to-approve report shows the median time from request to final decision, by week, month or quarter, and breaks it down by step and by approver. When a step is slow, you see which one, and whether it is slow for every amount or only for large ones.',
|
|
340
|
-
},
|
|
341
|
-
{ h2: 'Spend against budget' },
|
|
342
|
-
{
|
|
343
|
-
p: 'Because budget lines are read from the ERP, every approved request is counted against its cost centre as soon as it is approved, weeks before the invoice arrives. Budget owners see committed spend, and finance sees which cost centres will run over before the month closes.',
|
|
344
|
-
},
|
|
345
|
-
{ h2: 'Figures you can trace' },
|
|
346
|
-
{
|
|
347
|
-
p: 'Every figure in a report opens the requests it was computed from, each with its approval chain and its history in the audit log. A median quoted in a board pack can be traced back to the individual requests in two clicks.',
|
|
348
|
-
},
|
|
349
|
-
{
|
|
350
|
-
ul: [
|
|
351
|
-
'Saved views that can be shared with a link',
|
|
352
|
-
'A weekly email to budget owners with their committed spend',
|
|
353
|
-
'CSV export of every report, and a read-only API for your data warehouse',
|
|
354
|
-
],
|
|
355
|
-
},
|
|
356
|
-
],
|
|
357
|
-
details: [
|
|
358
|
-
[
|
|
359
|
-
'Do reports include requests that were rejected?',
|
|
360
|
-
'Yes, as a separate series, so the time spent on rejected requests is visible too.',
|
|
361
|
-
],
|
|
362
|
-
],
|
|
363
|
-
},
|
|
364
|
-
{
|
|
365
|
-
name: 'API and webhooks',
|
|
366
|
-
slug: 'api-and-webhooks',
|
|
367
|
-
short: 'A versioned REST API and signed webhooks, with the permissions of the app.',
|
|
368
|
-
description: 'Everything the product does in the browser can be done through a versioned REST API, and signed webhooks tell your systems when a decision is made.',
|
|
369
|
-
icon: 'code',
|
|
370
|
-
screenshot: 'api-webhooks',
|
|
371
|
-
body: () => [
|
|
372
|
-
{ h2: 'The same product, through an API' },
|
|
373
|
-
{
|
|
374
|
-
p: 'The REST API is versioned by date and covers requests, approvals, policies, vendors, the audit log and reports. API keys are scoped to read or write and to the parts of the workspace they need, and every call they make is written to the audit log like any other action.',
|
|
375
|
-
},
|
|
376
|
-
{ h2: 'Webhooks for every decision' },
|
|
377
|
-
{
|
|
378
|
-
p: 'Webhooks fire when a request is created, approved, rejected or changed. Each delivery is signed with HMAC-SHA256 and retried for up to 24 hours when your endpoint does not answer, and the last deliveries of every endpoint are listed with their response codes.',
|
|
379
|
-
},
|
|
380
|
-
{ h2: 'Changes announced in advance' },
|
|
381
|
-
{
|
|
382
|
-
p: 'A new API version is published with a date, and the previous version keeps working for at least 12 months after it. Every change is listed in the changelog, and the owners of the keys that still call a retiring version are told by email three months before it ends.',
|
|
383
|
-
},
|
|
384
|
-
{
|
|
385
|
-
ul: [
|
|
386
|
-
'Idempotency keys on every write',
|
|
387
|
-
'A sandbox workspace with test data for every customer',
|
|
388
|
-
'Rate limits of 600 requests a minute per key',
|
|
389
|
-
],
|
|
390
|
-
},
|
|
391
|
-
],
|
|
392
|
-
details: [
|
|
393
|
-
[
|
|
394
|
-
'Is there a client library?',
|
|
395
|
-
'We publish clients for TypeScript and Python. Any language that can make an HTTPS request can use the API directly.',
|
|
396
|
-
],
|
|
397
|
-
],
|
|
398
|
-
},
|
|
399
|
-
];
|
|
400
|
-
function featureBlocks(demo, name) {
|
|
154
|
+
export const SAAS_CUSTOMERS = SAAS_COPY_EN.customers;
|
|
155
|
+
export const SAAS_DEMO_FEATURES = SAAS_COPY_EN.features;
|
|
156
|
+
/** Oldest first: seeded in this order, listed newest first by id. */
|
|
157
|
+
export const SAAS_DEMO_UPDATES = SAAS_COPY_EN.updates;
|
|
158
|
+
function featureBlocks(copy, demo, name) {
|
|
401
159
|
return [
|
|
402
160
|
prose(`feature-${demo.slug}-body`, demo.body(name)),
|
|
403
161
|
{
|
|
404
162
|
_key: `feature-${demo.slug}-details`,
|
|
405
163
|
_type: 'accordion',
|
|
406
164
|
_version: BLOCK_VERSION,
|
|
407
|
-
title:
|
|
165
|
+
title: copy.detailsTitle,
|
|
408
166
|
items: qa(`feature-${demo.slug}-details`, demo.details),
|
|
409
167
|
},
|
|
410
168
|
{
|
|
411
169
|
_key: `feature-${demo.slug}-others`,
|
|
412
170
|
_type: 'collectionList',
|
|
413
171
|
_version: BLOCK_VERSION,
|
|
414
|
-
title:
|
|
172
|
+
title: copy.otherFeaturesTitle,
|
|
415
173
|
collection: 'feature',
|
|
416
174
|
sort: { field: 'id', direction: 'asc' },
|
|
417
175
|
limit: 6,
|
|
@@ -420,123 +178,10 @@ function featureBlocks(demo, name) {
|
|
|
420
178
|
];
|
|
421
179
|
}
|
|
422
180
|
/** The block zone of a feature page. Exported for the blueprint test. */
|
|
423
|
-
export function saasFeatureBlocks(demo, siteName) {
|
|
424
|
-
return featureBlocks(demo, nameOf(siteName));
|
|
181
|
+
export function saasFeatureBlocks(demo, siteName, copy = SAAS_COPY_EN) {
|
|
182
|
+
return featureBlocks(copy, demo, nameOf(siteName));
|
|
425
183
|
}
|
|
426
|
-
|
|
427
|
-
export const SAAS_DEMO_UPDATES = [
|
|
428
|
-
{
|
|
429
|
-
title: 'SCIM group mapping',
|
|
430
|
-
slug: 'scim-group-mapping',
|
|
431
|
-
summary: 'Groups in your identity provider now map to roles, so directory changes set permissions too.',
|
|
432
|
-
publishedAt: '2026-05-14T09:00:00.000Z',
|
|
433
|
-
screenshot: 'sso-scim',
|
|
434
|
-
caption: 'Sign-in and provisioning settings, with five Okta groups mapped to roles.',
|
|
435
|
-
body: () => [
|
|
436
|
-
{
|
|
437
|
-
p: 'Until now SCIM created and removed members, and roles were set by hand. From today you can map a group in Okta, Microsoft Entra ID or Google Workspace to a role, and membership of that group sets the role.',
|
|
438
|
-
},
|
|
439
|
-
{
|
|
440
|
-
ul: [
|
|
441
|
-
'A group can map to requester, approver, accounts payable, administrator or auditor',
|
|
442
|
-
'An approver role can be limited to the cost centres the group owns',
|
|
443
|
-
'Changes are applied within a minute of the directory sending them',
|
|
444
|
-
],
|
|
445
|
-
},
|
|
446
|
-
{
|
|
447
|
-
p: 'Existing roles are left as they are until you save a mapping. Settings, then Sign-in and provisioning, lists every group your directory has sent.',
|
|
448
|
-
},
|
|
449
|
-
],
|
|
450
|
-
},
|
|
451
|
-
{
|
|
452
|
-
title: 'Deadlines and reminders on every step',
|
|
453
|
-
slug: 'deadlines-and-reminders',
|
|
454
|
-
summary: 'Each step of a policy can now carry a deadline in business days, with a reminder the day before.',
|
|
455
|
-
publishedAt: '2026-06-04T09:00:00.000Z',
|
|
456
|
-
body: () => [
|
|
457
|
-
{
|
|
458
|
-
p: 'A request that waits on one person holds up everyone after them. Steps now carry a deadline, counted in business days from the moment the step opens and following the public holidays of the approver’s country.',
|
|
459
|
-
},
|
|
460
|
-
{
|
|
461
|
-
p: 'The approver gets a reminder the working day before the deadline, by email and in Slack or Microsoft Teams if connected. When a deadline passes, the request is marked overdue in the queue and the step can move to a delegate automatically.',
|
|
462
|
-
},
|
|
463
|
-
],
|
|
464
|
-
},
|
|
465
|
-
{
|
|
466
|
-
title: 'Delegation while you are away',
|
|
467
|
-
slug: 'delegation-while-away',
|
|
468
|
-
summary: 'Approvers can name a delegate for a date range, and their steps move to that person while they are out.',
|
|
469
|
-
publishedAt: '2026-06-25T09:00:00.000Z',
|
|
470
|
-
body: () => [
|
|
471
|
-
{
|
|
472
|
-
p: 'Set a date range and a delegate from your profile, or let your calendar do it: when Google Calendar or Outlook shows you out of office, the delegate you chose receives your steps for those days.',
|
|
473
|
-
},
|
|
474
|
-
{
|
|
475
|
-
ul: [
|
|
476
|
-
'A delegate cannot approve a request they submitted themselves',
|
|
477
|
-
'Delegated decisions show both names in the request and in the audit log',
|
|
478
|
-
'Administrators can set a delegate for someone on unplanned leave',
|
|
479
|
-
],
|
|
480
|
-
},
|
|
481
|
-
],
|
|
482
|
-
},
|
|
483
|
-
{
|
|
484
|
-
title: 'Microsoft Dynamics 365 Business Central',
|
|
485
|
-
slug: 'business-central',
|
|
486
|
-
summary: 'Business Central joins NetSuite, Xero and QuickBooks Online as a two-way ERP connection.',
|
|
487
|
-
publishedAt: '2026-07-16T09:00:00.000Z',
|
|
488
|
-
screenshot: 'erp-sync',
|
|
489
|
-
caption: 'The recent activity of an ERP connection, with one sync retried after a rate limit.',
|
|
490
|
-
body: () => [
|
|
491
|
-
{
|
|
492
|
-
p: 'The Business Central connection reads vendors, dimensions and budget entries, and writes approved purchase invoices and purchase orders. Dimensions map to cost centres, so an approval policy written for a department works on the dimension your books already use.',
|
|
493
|
-
},
|
|
494
|
-
{
|
|
495
|
-
p: 'Connections are available on Business and Enterprise. Setting one up takes a Business Central user with the permissions listed in the setup screen, and about an hour.',
|
|
496
|
-
},
|
|
497
|
-
],
|
|
498
|
-
},
|
|
499
|
-
{
|
|
500
|
-
title: 'Signed audit exports',
|
|
501
|
-
slug: 'signed-audit-exports',
|
|
502
|
-
summary: 'CSV and PDF exports of the audit log are now signed, so a file can be checked against the log.',
|
|
503
|
-
publishedAt: '2026-08-06T09:00:00.000Z',
|
|
504
|
-
screenshot: 'audit-log',
|
|
505
|
-
caption: 'The audit log filtered to one purchase request, ready to export.',
|
|
506
|
-
body: () => [
|
|
507
|
-
{
|
|
508
|
-
p: 'Each export of the audit log now comes with a signature over its contents and the hash of the last event it includes. Anyone with the file can check it against the workspace’s public key, and an auditor can confirm that no row was added, removed or edited after the export.',
|
|
509
|
-
},
|
|
510
|
-
{
|
|
511
|
-
p: 'Signing is on for every export from Business and Enterprise workspaces, with nothing to set up. The verification steps are described under Settings, then Audit log.',
|
|
512
|
-
},
|
|
513
|
-
],
|
|
514
|
-
},
|
|
515
|
-
{
|
|
516
|
-
title: 'Parallel approval steps',
|
|
517
|
-
slug: 'parallel-approval-steps',
|
|
518
|
-
summary: 'Two steps can now run at the same time, so procurement and a budget owner review a request together.',
|
|
519
|
-
publishedAt: '2026-08-27T09:00:00.000Z',
|
|
520
|
-
screenshot: 'policy-editor',
|
|
521
|
-
caption: 'A purchase request policy with procurement reviewing new vendors in parallel with step 1.',
|
|
522
|
-
body: () => [
|
|
523
|
-
{
|
|
524
|
-
p: 'Steps used to run strictly one after the other. A policy can now open two steps at once: procurement reviews a new vendor while the budget owner reviews the spend, and the request moves on when both have approved.',
|
|
525
|
-
},
|
|
526
|
-
{
|
|
527
|
-
ul: [
|
|
528
|
-
'Mark a step as parallel with the step before it in the policy editor',
|
|
529
|
-
'A rejection on either step ends the request, with the reason shown to the requester',
|
|
530
|
-
'Reports count the time of a parallel step from when it opened',
|
|
531
|
-
],
|
|
532
|
-
},
|
|
533
|
-
{
|
|
534
|
-
p: 'Across the workspaces that tried it in August, requests from new vendors reached a decision a day and a half sooner.',
|
|
535
|
-
},
|
|
536
|
-
],
|
|
537
|
-
},
|
|
538
|
-
];
|
|
539
|
-
function updateBlocks(update, name, media) {
|
|
184
|
+
function updateBlocks(copy, update, name, media) {
|
|
540
185
|
const screenshot = update.screenshot === undefined ? undefined : media[update.screenshot];
|
|
541
186
|
return [
|
|
542
187
|
prose(`update-${update.slug}-body`, update.body(name)),
|
|
@@ -557,7 +202,7 @@ function updateBlocks(update, name, media) {
|
|
|
557
202
|
_key: `update-${update.slug}-more`,
|
|
558
203
|
_type: 'collectionList',
|
|
559
204
|
_version: BLOCK_VERSION,
|
|
560
|
-
title:
|
|
205
|
+
title: copy.moreUpdatesTitle,
|
|
561
206
|
collection: 'changelog',
|
|
562
207
|
sort: { field: 'id', direction: 'desc' },
|
|
563
208
|
limit: 4,
|
|
@@ -567,140 +212,116 @@ function updateBlocks(update, name, media) {
|
|
|
567
212
|
}
|
|
568
213
|
/** The block zone of a changelog entry. Exported for the blueprint test. */
|
|
569
214
|
export function saasUpdateBlocks(update, options = {}) {
|
|
570
|
-
return updateBlocks(update, nameOf(options.siteName), options.media ?? {});
|
|
215
|
+
return updateBlocks(options.copy ?? SAAS_COPY_EN, update, nameOf(options.siteName), options.media ?? {});
|
|
571
216
|
}
|
|
572
217
|
// ---------------------------------------------------------------------------
|
|
573
|
-
// Pricing
|
|
218
|
+
// Pricing
|
|
574
219
|
// ---------------------------------------------------------------------------
|
|
575
|
-
function pricingTiers() {
|
|
576
|
-
return
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
'ERP connections: 1',
|
|
587
|
-
'Audit log history: 1 year',
|
|
588
|
-
'Single sign-on: Google',
|
|
589
|
-
'Support: Email, next business day',
|
|
590
|
-
],
|
|
591
|
-
action: { label: 'Start a trial', target: { href: '/demo' } },
|
|
592
|
-
},
|
|
593
|
-
{
|
|
594
|
-
_key: 'tier-business',
|
|
595
|
-
name: 'Business',
|
|
596
|
-
price: '$24',
|
|
597
|
-
interval: 'per approver, per month, billed yearly',
|
|
598
|
-
features: [
|
|
599
|
-
'Approvers: Up to 250',
|
|
600
|
-
'Requesters: Unlimited, free',
|
|
601
|
-
'Approval policies: Unlimited',
|
|
602
|
-
'ERP connections: 3',
|
|
603
|
-
'Audit log history: 7 years',
|
|
604
|
-
'Single sign-on: SAML and SCIM',
|
|
605
|
-
'Support: Email and chat, same business day',
|
|
606
|
-
'Parallel approval steps',
|
|
607
|
-
'Signed audit exports',
|
|
608
|
-
],
|
|
609
|
-
action: { label: 'Start a trial', target: { href: '/demo' }, emphasis: 'primary' },
|
|
610
|
-
highlighted: true,
|
|
611
|
-
},
|
|
612
|
-
{
|
|
613
|
-
_key: 'tier-enterprise',
|
|
614
|
-
name: 'Enterprise',
|
|
615
|
-
price: 'Custom',
|
|
616
|
-
interval: 'annual contract, from 100 approvers',
|
|
617
|
-
features: [
|
|
618
|
-
'Approvers: Unlimited',
|
|
619
|
-
'Requesters: Unlimited, free',
|
|
620
|
-
'Approval policies: Unlimited',
|
|
621
|
-
'ERP connections: Unlimited',
|
|
622
|
-
'Audit log history: 10 years',
|
|
623
|
-
'Single sign-on: SAML and SCIM',
|
|
624
|
-
'Support: Named engineer, 1-hour response',
|
|
625
|
-
'Parallel approval steps',
|
|
626
|
-
'Signed audit exports',
|
|
627
|
-
'Data residency in the EU or the US',
|
|
628
|
-
'Invoicing on 30-day terms',
|
|
629
|
-
],
|
|
630
|
-
action: { label: 'Talk to sales', target: { href: '/demo' } },
|
|
220
|
+
function pricingTiers(copy) {
|
|
221
|
+
return copy.tiers.map((tier) => ({
|
|
222
|
+
_key: tier.key,
|
|
223
|
+
name: tier.name,
|
|
224
|
+
price: tier.price,
|
|
225
|
+
interval: tier.interval,
|
|
226
|
+
features: tier.features,
|
|
227
|
+
action: {
|
|
228
|
+
label: tier.actionLabel,
|
|
229
|
+
target: { href: pagePath(copy, 'demo') },
|
|
230
|
+
...(tier.primary === true ? { emphasis: 'primary' } : {}),
|
|
631
231
|
},
|
|
632
|
-
|
|
232
|
+
...(tier.highlighted === true ? { highlighted: true } : {}),
|
|
233
|
+
}));
|
|
633
234
|
}
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
[
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
],
|
|
656
|
-
[
|
|
657
|
-
'What happens to our data if we leave?',
|
|
658
|
-
'You can export every request, decision and attachment at any time. We delete the workspace 30 days after the end of the contract and send a certificate of deletion.',
|
|
659
|
-
],
|
|
235
|
+
// ---------------------------------------------------------------------------
|
|
236
|
+
// Pages
|
|
237
|
+
// ---------------------------------------------------------------------------
|
|
238
|
+
/**
|
|
239
|
+
* One bullet of the credits list: title, author, licence (linked when it has a
|
|
240
|
+
* page) and a link to the source, the way an attribution licence asks for it.
|
|
241
|
+
*/
|
|
242
|
+
function creditNode(key, credit, sourceWord) {
|
|
243
|
+
const links = [];
|
|
244
|
+
const span = (index, text, href) => {
|
|
245
|
+
if (href === undefined)
|
|
246
|
+
return { _key: `${key}-s${index}`, _type: 'span', text, marks: [] };
|
|
247
|
+
const markKey = `${key}-l${index}`;
|
|
248
|
+
links.push({ _key: markKey, _type: 'link', href });
|
|
249
|
+
return { _key: `${key}-s${index}`, _type: 'span', text, marks: [markKey] };
|
|
250
|
+
};
|
|
251
|
+
const children = [
|
|
252
|
+
span(0, `${credit.title} · ${credit.author} · `),
|
|
253
|
+
span(1, credit.licence, credit.licenceUrl),
|
|
254
|
+
span(2, ' · '),
|
|
255
|
+
span(3, sourceWord, credit.source),
|
|
660
256
|
];
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
257
|
+
return {
|
|
258
|
+
_key: key,
|
|
259
|
+
_type: 'block',
|
|
260
|
+
style: 'normal',
|
|
261
|
+
listItem: 'bullet',
|
|
262
|
+
level: 1,
|
|
263
|
+
children,
|
|
264
|
+
markDefs: links,
|
|
265
|
+
};
|
|
667
266
|
}
|
|
668
267
|
/**
|
|
669
268
|
* Every page of the site. A function of `media` (the ids `seedDemoMedia`
|
|
670
269
|
* assigned), of the features' real ids (the home page grid links to each
|
|
671
|
-
* feature page) and of the
|
|
270
|
+
* feature page), of the site's name and of the copy's language.
|
|
672
271
|
*
|
|
673
272
|
* A block that needs a picture which was not seeded is left out rather than
|
|
674
273
|
* emitted invalid, so the pages still validate with no media at all.
|
|
675
274
|
*/
|
|
676
|
-
export function buildSaasDemoPages(media, featureIdBySlug, siteName) {
|
|
275
|
+
export function buildSaasDemoPages(media, featureIdBySlug, siteName, copy = SAAS_COPY_EN) {
|
|
677
276
|
const name = nameOf(siteName);
|
|
277
|
+
const mail = (mailbox) => saasEmail(name, mailbox);
|
|
278
|
+
const words = copy.pages(name, mail);
|
|
279
|
+
const demoHref = pagePath(copy, 'demo');
|
|
280
|
+
const demoAction = (label = words.demoAction) => ({
|
|
281
|
+
label,
|
|
282
|
+
target: { href: demoHref },
|
|
283
|
+
emphasis: 'primary',
|
|
284
|
+
});
|
|
678
285
|
const featureLink = (slug) => {
|
|
679
286
|
const id = featureIdBySlug.get(slug);
|
|
680
287
|
return id === undefined ? {} : { link: { collection: 'feature', id } };
|
|
681
288
|
};
|
|
682
|
-
const logos =
|
|
289
|
+
const logos = copy.customers.flatMap((customer) => media[customer.key] === undefined
|
|
683
290
|
? []
|
|
684
291
|
: [{ key: customer.key, name: customer.name, media: media[customer.key] }]);
|
|
685
|
-
const
|
|
292
|
+
const figure = (key, mediaName, caption, align = 'start') => media[mediaName] === undefined
|
|
293
|
+
? []
|
|
294
|
+
: [
|
|
295
|
+
block(key, 'mediaFigure', {
|
|
296
|
+
media: media[mediaName],
|
|
297
|
+
caption,
|
|
298
|
+
ratio: 'original',
|
|
299
|
+
align,
|
|
300
|
+
}),
|
|
301
|
+
];
|
|
302
|
+
const home = words.home;
|
|
303
|
+
const homeBlocks = [
|
|
686
304
|
block('home-hero', 'hero', {
|
|
687
|
-
eyebrow:
|
|
688
|
-
title:
|
|
689
|
-
subtitle:
|
|
305
|
+
eyebrow: home.heroEyebrow,
|
|
306
|
+
title: home.heroTitle,
|
|
307
|
+
subtitle: home.heroSubtitle,
|
|
690
308
|
...(media.app === undefined ? {} : { media: media.app }),
|
|
691
|
-
actions: [
|
|
309
|
+
actions: [
|
|
310
|
+
demoAction(),
|
|
311
|
+
{ label: home.heroSecondary, target: { href: pagePath(copy, 'pricing') } },
|
|
312
|
+
],
|
|
692
313
|
}),
|
|
693
314
|
...(logos.length === 0
|
|
694
315
|
? []
|
|
695
316
|
: [
|
|
696
317
|
block('home-customers', 'logoStrip', {
|
|
697
|
-
caption:
|
|
318
|
+
caption: home.customersCaption,
|
|
698
319
|
logos: logos.map((logo) => ({ _key: `home-customers-${logo.key}`, media: logo.media })),
|
|
699
320
|
}),
|
|
700
321
|
]),
|
|
701
322
|
block('home-features', 'featureGrid', {
|
|
702
|
-
title:
|
|
703
|
-
items:
|
|
323
|
+
title: home.featuresTitle,
|
|
324
|
+
items: copy.features.map((demo) => ({
|
|
704
325
|
_key: `home-feature-${demo.slug}`,
|
|
705
326
|
icon: demo.icon,
|
|
706
327
|
title: demo.name,
|
|
@@ -709,94 +330,61 @@ export function buildSaasDemoPages(media, featureIdBySlug, siteName) {
|
|
|
709
330
|
})),
|
|
710
331
|
}),
|
|
711
332
|
block('home-steps', 'featureGrid', {
|
|
712
|
-
title:
|
|
713
|
-
items:
|
|
714
|
-
{
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
},
|
|
719
|
-
{
|
|
720
|
-
_key: 'home-step-route',
|
|
721
|
-
title: 'Your policy routes it',
|
|
722
|
-
text: 'Amount, cost centre, vendor and category decide who approves, in what order and by when.',
|
|
723
|
-
},
|
|
724
|
-
{
|
|
725
|
-
_key: 'home-step-record',
|
|
726
|
-
title: 'The decision is recorded',
|
|
727
|
-
text: 'Approvers decide from the web, Slack or email. The ERP is updated and the audit log keeps who, what and when.',
|
|
728
|
-
},
|
|
729
|
-
],
|
|
333
|
+
title: home.stepsTitle,
|
|
334
|
+
items: home.steps.map((step, index) => ({
|
|
335
|
+
_key: `home-step-${index + 1}`,
|
|
336
|
+
title: step.title,
|
|
337
|
+
text: step.text,
|
|
338
|
+
})),
|
|
730
339
|
}),
|
|
731
340
|
block('home-tour', 'collectionList', {
|
|
732
|
-
title:
|
|
341
|
+
title: home.tourTitle,
|
|
733
342
|
collection: 'feature',
|
|
734
343
|
sort: { field: 'id', direction: 'asc' },
|
|
735
344
|
limit: 3,
|
|
736
345
|
layout: 'list',
|
|
737
346
|
}),
|
|
738
347
|
block('home-figures', 'stats', {
|
|
739
|
-
title:
|
|
740
|
-
items:
|
|
741
|
-
{
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
},
|
|
747
|
-
{
|
|
748
|
-
_key: 'home-figure-volume',
|
|
749
|
-
value: '2.1',
|
|
750
|
-
unit: 'M',
|
|
751
|
-
label: 'requests and invoices approved',
|
|
752
|
-
},
|
|
753
|
-
{
|
|
754
|
-
_key: 'home-figure-uptime',
|
|
755
|
-
value: '99.99',
|
|
756
|
-
unit: '%',
|
|
757
|
-
label: 'uptime over the last twelve months',
|
|
758
|
-
},
|
|
759
|
-
{
|
|
760
|
-
_key: 'home-figure-history',
|
|
761
|
-
value: '7',
|
|
762
|
-
unit: 'years',
|
|
763
|
-
label: 'of audit history kept on Business',
|
|
764
|
-
},
|
|
765
|
-
],
|
|
348
|
+
title: home.figuresTitle,
|
|
349
|
+
items: home.figures.map((figureItem, index) => ({
|
|
350
|
+
_key: `home-figure-${index + 1}`,
|
|
351
|
+
value: figureItem.value,
|
|
352
|
+
unit: figureItem.unit,
|
|
353
|
+
label: figureItem.label,
|
|
354
|
+
})),
|
|
766
355
|
}),
|
|
767
356
|
block('home-customer', 'testimonial', {
|
|
768
357
|
quote: [
|
|
769
|
-
...richTextParagraph('home-customer-1',
|
|
770
|
-
...richTextParagraph('home-customer-2',
|
|
358
|
+
...richTextParagraph('home-customer-1', home.quote[0]),
|
|
359
|
+
...richTextParagraph('home-customer-2', home.quote[1]),
|
|
771
360
|
],
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
...(media.portrait === undefined ? {} : { avatar: media.portrait }),
|
|
776
|
-
},
|
|
361
|
+
// No portrait: the quoted customer is fictional, and a photograph of a
|
|
362
|
+
// real person would present them as someone they are not.
|
|
363
|
+
attribution: { name: home.quoteName, role: home.quoteRole },
|
|
777
364
|
}),
|
|
778
365
|
block('home-pricing', 'pricingTable', {
|
|
779
|
-
title:
|
|
780
|
-
tiers: pricingTiers(),
|
|
366
|
+
title: home.pricingTitle,
|
|
367
|
+
tiers: pricingTiers(copy),
|
|
781
368
|
}),
|
|
782
369
|
block('home-questions', 'faq', {
|
|
783
|
-
title:
|
|
784
|
-
items: qa('home-questions', homeQuestions(name)),
|
|
370
|
+
title: home.questionsTitle,
|
|
371
|
+
items: qa('home-questions', copy.homeQuestions(name)),
|
|
785
372
|
}),
|
|
786
373
|
block('home-close', 'cta', {
|
|
787
|
-
title:
|
|
788
|
-
text:
|
|
374
|
+
title: home.ctaTitle,
|
|
375
|
+
text: home.ctaText,
|
|
789
376
|
actions: [
|
|
790
377
|
demoAction(),
|
|
791
|
-
{ label:
|
|
378
|
+
{ label: home.ctaSecurity, target: { href: pagePath(copy, 'security') } },
|
|
792
379
|
],
|
|
793
380
|
}),
|
|
794
381
|
];
|
|
795
|
-
const
|
|
382
|
+
const p = words.product;
|
|
383
|
+
const productBlocks = [
|
|
796
384
|
block('product-hero', 'hero', {
|
|
797
|
-
eyebrow:
|
|
798
|
-
title:
|
|
799
|
-
subtitle:
|
|
385
|
+
eyebrow: p.heroEyebrow,
|
|
386
|
+
title: p.heroTitle,
|
|
387
|
+
subtitle: p.heroSubtitle,
|
|
800
388
|
actions: [demoAction()],
|
|
801
389
|
}),
|
|
802
390
|
block('product-tour', 'collectionList', {
|
|
@@ -806,200 +394,96 @@ export function buildSaasDemoPages(media, featureIdBySlug, siteName) {
|
|
|
806
394
|
layout: 'list',
|
|
807
395
|
}),
|
|
808
396
|
block('product-steps', 'featureGrid', {
|
|
809
|
-
title:
|
|
810
|
-
items:
|
|
811
|
-
{
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
},
|
|
816
|
-
{
|
|
817
|
-
_key: 'product-step-2',
|
|
818
|
-
title: 'Week two',
|
|
819
|
-
text: 'The ERP is connected, and requesters start submitting through a form instead of an email.',
|
|
820
|
-
},
|
|
821
|
-
{
|
|
822
|
-
_key: 'product-step-3',
|
|
823
|
-
title: 'Week four',
|
|
824
|
-
text: 'The first month closes with every approval in the log and the evidence exported in one file.',
|
|
825
|
-
},
|
|
826
|
-
],
|
|
397
|
+
title: p.stepsTitle,
|
|
398
|
+
items: p.steps.map((step, index) => ({
|
|
399
|
+
_key: `product-step-${index + 1}`,
|
|
400
|
+
title: step.title,
|
|
401
|
+
text: step.text,
|
|
402
|
+
})),
|
|
827
403
|
}),
|
|
828
404
|
block('product-close', 'cta', {
|
|
829
|
-
title:
|
|
830
|
-
text:
|
|
831
|
-
actions: [demoAction(), { label:
|
|
405
|
+
title: p.ctaTitle,
|
|
406
|
+
text: p.ctaText,
|
|
407
|
+
actions: [demoAction(), { label: p.ctaCompare, target: { href: pagePath(copy, 'pricing') } }],
|
|
832
408
|
}),
|
|
833
409
|
];
|
|
834
|
-
const
|
|
410
|
+
const pr = words.pricing;
|
|
411
|
+
const pricingBlocks = [
|
|
835
412
|
block('pricing-hero', 'hero', {
|
|
836
|
-
eyebrow:
|
|
837
|
-
title:
|
|
838
|
-
subtitle:
|
|
839
|
-
actions: [demoAction(
|
|
413
|
+
eyebrow: pr.heroEyebrow,
|
|
414
|
+
title: pr.heroTitle,
|
|
415
|
+
subtitle: pr.heroSubtitle,
|
|
416
|
+
actions: [demoAction(pr.trialAction)],
|
|
417
|
+
}),
|
|
418
|
+
block('pricing-plans', 'pricingTable', {
|
|
419
|
+
title: pr.plansTitle,
|
|
420
|
+
tiers: pricingTiers(copy),
|
|
840
421
|
}),
|
|
841
|
-
block('pricing-plans', 'pricingTable', { title: 'Compare plans', tiers: pricingTiers() }),
|
|
842
422
|
block('pricing-questions', 'faq', {
|
|
843
|
-
title:
|
|
844
|
-
items: qa('pricing-questions',
|
|
845
|
-
[
|
|
846
|
-
'How does the trial work?',
|
|
847
|
-
'A solutions engineer sets up a workspace with your approval policy and a copy of last month’s requests. You use it for 14 days. It does not turn into a subscription on its own, and no card is asked for.',
|
|
848
|
-
],
|
|
849
|
-
[
|
|
850
|
-
'Who counts as an approver?',
|
|
851
|
-
'Anyone who approved or rejected at least one step in the month. People who only submit requests, upload invoices or read reports are free.',
|
|
852
|
-
],
|
|
853
|
-
[
|
|
854
|
-
'Can we pay by invoice?',
|
|
855
|
-
'Yes. Business and Enterprise are invoiced yearly on 30-day terms, with your purchase order number on the invoice. Team is paid by card.',
|
|
856
|
-
],
|
|
857
|
-
[
|
|
858
|
-
'What if we grow during the year?',
|
|
859
|
-
'Extra approvers are billed at the end of each quarter for the months they were active, at the rate of your plan. Nothing is charged in advance.',
|
|
860
|
-
],
|
|
861
|
-
[
|
|
862
|
-
'Do you offer discounts?',
|
|
863
|
-
'Registered charities and schools get 40 percent off Team and Business. Write to us from your organisation’s address.',
|
|
864
|
-
],
|
|
865
|
-
[
|
|
866
|
-
'Can we change plans later?',
|
|
867
|
-
'Yes. An upgrade applies at once and is prorated to the day. A downgrade applies at the next renewal.',
|
|
868
|
-
],
|
|
869
|
-
]),
|
|
423
|
+
title: pr.billingTitle,
|
|
424
|
+
items: qa('pricing-questions', pr.billing),
|
|
870
425
|
}),
|
|
871
426
|
block('pricing-close', 'cta', {
|
|
872
|
-
title:
|
|
873
|
-
text:
|
|
427
|
+
title: pr.ctaTitle,
|
|
428
|
+
text: pr.ctaText,
|
|
874
429
|
actions: [
|
|
875
430
|
{
|
|
876
|
-
label:
|
|
877
|
-
target: { href: `mailto:${
|
|
431
|
+
label: pr.ctaEmail,
|
|
432
|
+
target: { href: `mailto:${mail(copy.locale === 'fr' ? 'ventes' : 'sales')}` },
|
|
878
433
|
emphasis: 'primary',
|
|
879
434
|
},
|
|
880
435
|
],
|
|
881
436
|
}),
|
|
882
437
|
];
|
|
883
|
-
const
|
|
438
|
+
const s = words.security;
|
|
439
|
+
const securityMailbox = copy.locale === 'fr' ? 'securite' : 'security';
|
|
440
|
+
const securityBlocks = [
|
|
884
441
|
block('security-hero', 'hero', {
|
|
885
|
-
eyebrow:
|
|
886
|
-
title:
|
|
887
|
-
subtitle:
|
|
442
|
+
eyebrow: s.heroEyebrow,
|
|
443
|
+
title: s.heroTitle,
|
|
444
|
+
subtitle: s.heroSubtitle,
|
|
888
445
|
actions: [
|
|
889
446
|
{
|
|
890
|
-
label:
|
|
891
|
-
target: { href: `mailto:${
|
|
447
|
+
label: s.packAction,
|
|
448
|
+
target: { href: `mailto:${mail(securityMailbox)}` },
|
|
892
449
|
emphasis: 'primary',
|
|
893
450
|
},
|
|
894
451
|
],
|
|
895
452
|
}),
|
|
896
453
|
block('security-controls', 'featureGrid', {
|
|
897
|
-
title:
|
|
898
|
-
items:
|
|
899
|
-
{
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
},
|
|
905
|
-
{
|
|
906
|
-
_key: 'security-control-residency',
|
|
907
|
-
icon: 'globe',
|
|
908
|
-
title: 'Data residency',
|
|
909
|
-
text: 'Workspaces are hosted in Frankfurt or northern Virginia, and data stays in the region.',
|
|
910
|
-
},
|
|
911
|
-
{
|
|
912
|
-
_key: 'security-control-access',
|
|
913
|
-
icon: 'users',
|
|
914
|
-
title: 'Access',
|
|
915
|
-
text: 'Single sign-on, SCIM and role-based permissions, with every sign-in recorded.',
|
|
916
|
-
},
|
|
917
|
-
{
|
|
918
|
-
_key: 'security-control-log',
|
|
919
|
-
icon: 'shield',
|
|
920
|
-
title: 'Tamper-evident log',
|
|
921
|
-
text: 'Every event carries the hash of the one before it, and the chain is verified nightly.',
|
|
922
|
-
},
|
|
923
|
-
{
|
|
924
|
-
_key: 'security-control-backups',
|
|
925
|
-
icon: 'refresh',
|
|
926
|
-
title: 'Backups',
|
|
927
|
-
text: 'Encrypted backups every hour, kept for 35 days and restored in a test every month.',
|
|
928
|
-
},
|
|
929
|
-
{
|
|
930
|
-
_key: 'security-control-audits',
|
|
931
|
-
icon: 'check',
|
|
932
|
-
title: 'Independent audits',
|
|
933
|
-
text: 'A SOC 2 Type II report each year and an external penetration test every six months.',
|
|
934
|
-
},
|
|
935
|
-
],
|
|
454
|
+
title: s.controlsTitle,
|
|
455
|
+
items: s.controls.map((control, index) => ({
|
|
456
|
+
_key: `security-control-${index + 1}`,
|
|
457
|
+
icon: control.icon,
|
|
458
|
+
title: control.title,
|
|
459
|
+
text: control.text,
|
|
460
|
+
})),
|
|
936
461
|
}),
|
|
937
|
-
prose('security-detail',
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
p: 'Workspaces run on Amazon Web Services, in eu-central-1 (Frankfurt) or us-east-1 (northern Virginia). The region is chosen when the workspace is created and cannot be changed by our staff. Production runs across three availability zones, and the database is replicated between them.',
|
|
941
|
-
},
|
|
942
|
-
{ h2: 'Access by our staff' },
|
|
943
|
-
{
|
|
944
|
-
p: 'Nobody at the company can read your workspace by default. A support engineer can be granted access to one workspace for 24 hours, only after an administrator of that workspace approves it, and every action taken during that time is written to your audit log.',
|
|
945
|
-
},
|
|
946
|
-
{ h2: 'Keeping the record' },
|
|
947
|
-
{
|
|
948
|
-
p: 'The audit log is append-only. Events are hashed in a chain, the chain is verified every night and on demand, and exports are signed so a file can be checked against the workspace. Deleted requests are kept in the log as deletions.',
|
|
949
|
-
},
|
|
950
|
-
{ h2: 'Reporting a vulnerability' },
|
|
951
|
-
{
|
|
952
|
-
p: `Write to ${saasEmail(name, 'security')}. We acknowledge reports within one business day, keep you informed while we fix the issue, and credit you when it is resolved if you wish.`,
|
|
953
|
-
},
|
|
954
|
-
]),
|
|
955
|
-
...(media['audit-log'] === undefined
|
|
956
|
-
? []
|
|
957
|
-
: [
|
|
958
|
-
block('security-log', 'mediaFigure', {
|
|
959
|
-
media: media['audit-log'],
|
|
960
|
-
caption: 'The audit log of one purchase request, with the chain verified.',
|
|
961
|
-
ratio: 'original',
|
|
962
|
-
align: 'start',
|
|
963
|
-
}),
|
|
964
|
-
]),
|
|
462
|
+
prose('security-detail', s.detail),
|
|
463
|
+
...figure('security-racks', 'security-racks', s.racksCaption),
|
|
464
|
+
...figure('security-log', 'audit-log', s.logCaption),
|
|
965
465
|
block('security-subprocessors', 'accordion', {
|
|
966
|
-
title:
|
|
967
|
-
items: qa('security-subprocessors',
|
|
968
|
-
[
|
|
969
|
-
'Amazon Web Services',
|
|
970
|
-
'Hosting, storage and backups, in the region of the workspace. Data processing agreement in place.',
|
|
971
|
-
],
|
|
972
|
-
[
|
|
973
|
-
'Cloudflare',
|
|
974
|
-
'Network protection and delivery of the web application. Request metadata only, never the contents of a workspace.',
|
|
975
|
-
],
|
|
976
|
-
[
|
|
977
|
-
'Postmark',
|
|
978
|
-
'Transactional email: approval requests, reminders and receipts. The subject and summary of a request, never its attachments.',
|
|
979
|
-
],
|
|
980
|
-
[
|
|
981
|
-
'Slack and Microsoft Teams',
|
|
982
|
-
'Only when a customer connects them. Approval messages sent to the channels the customer chooses.',
|
|
983
|
-
],
|
|
984
|
-
]),
|
|
466
|
+
title: s.subprocessorsTitle,
|
|
467
|
+
items: qa('security-subprocessors', s.subprocessors),
|
|
985
468
|
}),
|
|
986
469
|
block('security-close', 'cta', {
|
|
987
|
-
title:
|
|
988
|
-
text:
|
|
470
|
+
title: s.ctaTitle,
|
|
471
|
+
text: s.ctaText,
|
|
989
472
|
actions: [
|
|
990
473
|
{
|
|
991
|
-
label:
|
|
992
|
-
target: { href: `mailto:${
|
|
474
|
+
label: s.packAction,
|
|
475
|
+
target: { href: `mailto:${mail(securityMailbox)}` },
|
|
993
476
|
emphasis: 'primary',
|
|
994
477
|
},
|
|
995
478
|
],
|
|
996
479
|
}),
|
|
997
480
|
];
|
|
998
|
-
const
|
|
481
|
+
const c = words.changelog;
|
|
482
|
+
const changelogBlocks = [
|
|
999
483
|
block('changelog-hero', 'hero', {
|
|
1000
|
-
eyebrow:
|
|
1001
|
-
title:
|
|
1002
|
-
subtitle:
|
|
484
|
+
eyebrow: c.heroEyebrow,
|
|
485
|
+
title: c.heroTitle,
|
|
486
|
+
subtitle: c.heroSubtitle,
|
|
1003
487
|
}),
|
|
1004
488
|
block('changelog-list', 'collectionList', {
|
|
1005
489
|
collection: 'changelog',
|
|
@@ -1008,47 +492,39 @@ export function buildSaasDemoPages(media, featureIdBySlug, siteName) {
|
|
|
1008
492
|
layout: 'list',
|
|
1009
493
|
}),
|
|
1010
494
|
];
|
|
1011
|
-
const
|
|
495
|
+
const a = words.about;
|
|
496
|
+
const aboutBlocks = [
|
|
1012
497
|
block('about-hero', 'hero', {
|
|
1013
|
-
eyebrow:
|
|
1014
|
-
title:
|
|
1015
|
-
subtitle:
|
|
498
|
+
eyebrow: a.heroEyebrow,
|
|
499
|
+
title: a.heroTitle,
|
|
500
|
+
subtitle: a.heroSubtitle,
|
|
1016
501
|
}),
|
|
1017
|
-
prose('about-story',
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
'A changelog that lists every change that alters how you work',
|
|
1031
|
-
],
|
|
1032
|
-
},
|
|
1033
|
-
{ h2: 'Funding' },
|
|
1034
|
-
{
|
|
1035
|
-
p: 'We raised a seed round in 2020 and a Series A in 2023, and have been profitable since the last quarter of 2025.',
|
|
1036
|
-
},
|
|
1037
|
-
]),
|
|
502
|
+
prose('about-story', a.story),
|
|
503
|
+
prose('about-places', a.places),
|
|
504
|
+
...(media['office-1'] === undefined || media['office-2'] === undefined
|
|
505
|
+
? []
|
|
506
|
+
: [
|
|
507
|
+
block('about-offices', 'gallery', {
|
|
508
|
+
layout: 'grid',
|
|
509
|
+
items: ['office-1', 'office-2'].map((key) => ({
|
|
510
|
+
_key: `about-office-${key}`,
|
|
511
|
+
media: media[key],
|
|
512
|
+
})),
|
|
513
|
+
}),
|
|
514
|
+
]),
|
|
1038
515
|
block('about-numbers', 'statCounter', {
|
|
1039
|
-
title:
|
|
1040
|
-
stats:
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
],
|
|
516
|
+
title: a.numbersTitle,
|
|
517
|
+
stats: a.numbers.map((number, index) => ({
|
|
518
|
+
_key: `about-number-${index + 1}`,
|
|
519
|
+
value: number.value,
|
|
520
|
+
label: number.label,
|
|
521
|
+
})),
|
|
1046
522
|
}),
|
|
1047
523
|
...(logos.length === 0
|
|
1048
524
|
? []
|
|
1049
525
|
: [
|
|
1050
526
|
block('about-customers', 'logos', {
|
|
1051
|
-
title:
|
|
527
|
+
title: a.customersTitle,
|
|
1052
528
|
items: logos.map((logo) => ({
|
|
1053
529
|
_key: `about-customer-${logo.key}`,
|
|
1054
530
|
media: logo.media,
|
|
@@ -1057,125 +533,89 @@ export function buildSaasDemoPages(media, featureIdBySlug, siteName) {
|
|
|
1057
533
|
}),
|
|
1058
534
|
]),
|
|
1059
535
|
block('about-close', 'cta', {
|
|
1060
|
-
title:
|
|
1061
|
-
text:
|
|
536
|
+
title: a.ctaTitle,
|
|
537
|
+
text: a.ctaText,
|
|
1062
538
|
actions: [
|
|
1063
539
|
{
|
|
1064
|
-
label:
|
|
1065
|
-
target: { href: `mailto:${
|
|
540
|
+
label: a.ctaAction,
|
|
541
|
+
target: { href: `mailto:${mail(copy.locale === 'fr' ? 'recrutement' : 'jobs')}` },
|
|
1066
542
|
emphasis: 'primary',
|
|
1067
543
|
},
|
|
1068
544
|
],
|
|
1069
545
|
}),
|
|
1070
546
|
];
|
|
1071
|
-
const
|
|
547
|
+
const d = words.demo;
|
|
548
|
+
const demoBlocks = [
|
|
1072
549
|
block('demo-hero', 'hero', {
|
|
1073
|
-
eyebrow:
|
|
1074
|
-
title:
|
|
1075
|
-
subtitle:
|
|
550
|
+
eyebrow: d.heroEyebrow,
|
|
551
|
+
title: d.heroTitle,
|
|
552
|
+
subtitle: d.heroSubtitle,
|
|
1076
553
|
actions: [
|
|
1077
554
|
{
|
|
1078
|
-
label:
|
|
555
|
+
label: d.emailAction,
|
|
1079
556
|
target: {
|
|
1080
|
-
href: `mailto:${
|
|
557
|
+
href: `mailto:${mail('demo')}?subject=${encodeURIComponent(d.emailSubject)}`,
|
|
1081
558
|
},
|
|
1082
559
|
emphasis: 'primary',
|
|
1083
560
|
},
|
|
1084
561
|
],
|
|
1085
562
|
}),
|
|
1086
563
|
block('demo-contact', 'featureGrid', {
|
|
1087
|
-
title:
|
|
564
|
+
title: d.contactTitle,
|
|
1088
565
|
items: [
|
|
1089
566
|
{
|
|
1090
567
|
_key: 'demo-contact-email',
|
|
1091
568
|
icon: 'mail',
|
|
1092
|
-
title: 'Email',
|
|
1093
|
-
text:
|
|
1094
|
-
link: { href: `mailto:${
|
|
569
|
+
title: copy.locale === 'fr' ? 'E-mail' : 'Email',
|
|
570
|
+
text: d.emailText,
|
|
571
|
+
link: { href: `mailto:${mail('demo')}` },
|
|
1095
572
|
},
|
|
1096
573
|
{
|
|
1097
574
|
_key: 'demo-contact-phone',
|
|
1098
575
|
icon: 'phone',
|
|
1099
|
-
title:
|
|
1100
|
-
text:
|
|
1101
|
-
link: { href:
|
|
576
|
+
title: copy.phone.display,
|
|
577
|
+
text: d.phoneText,
|
|
578
|
+
link: { href: copy.phone.href },
|
|
1102
579
|
},
|
|
1103
580
|
{
|
|
1104
581
|
_key: 'demo-contact-security',
|
|
1105
582
|
icon: 'shield',
|
|
1106
|
-
title:
|
|
1107
|
-
text:
|
|
1108
|
-
link: { href: '
|
|
583
|
+
title: d.securityTitle,
|
|
584
|
+
text: d.securityText,
|
|
585
|
+
link: { href: pagePath(copy, 'security') },
|
|
1109
586
|
},
|
|
1110
587
|
],
|
|
1111
588
|
}),
|
|
1112
|
-
prose('demo-how',
|
|
1113
|
-
{ h2: 'Before the call' },
|
|
1114
|
-
{
|
|
1115
|
-
p: 'Write to us with the number of people who approve spend, the ERP you use, and your current approval matrix if you can share it. A spreadsheet is fine. We reply within one business day to agree a time.',
|
|
1116
|
-
},
|
|
1117
|
-
{ h2: 'On the call' },
|
|
1118
|
-
{
|
|
1119
|
-
p: 'A solutions engineer shows the product with your own policy already built, answers questions from finance, procurement and IT, and goes through what a rollout would take for your team.',
|
|
1120
|
-
},
|
|
1121
|
-
{ h2: 'The trial' },
|
|
1122
|
-
{
|
|
1123
|
-
p: 'After the call, you get a workspace for 14 days with your policy and a copy of last month’s requests from a CSV export. Your team can submit and approve requests in it. The trial does not turn into a subscription on its own, and we do not ask for a card.',
|
|
1124
|
-
},
|
|
1125
|
-
]),
|
|
589
|
+
prose('demo-how', d.how),
|
|
1126
590
|
];
|
|
1127
|
-
const
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
{ h2: 'Data processing' },
|
|
1138
|
-
{
|
|
1139
|
-
p: 'We process personal data in customer workspaces as a processor, under a data processing agreement that includes the standard contractual clauses. The list of subprocessors is on the security page, and customers are told 30 days before a new one is added.',
|
|
1140
|
-
},
|
|
1141
|
-
{ h2: 'Contact' },
|
|
1142
|
-
{ p: `For legal questions, write to ${saasEmail(name, 'legal')}.` },
|
|
1143
|
-
]),
|
|
1144
|
-
];
|
|
1145
|
-
const privacy = [
|
|
1146
|
-
prose('privacy-body', [
|
|
1147
|
-
{
|
|
1148
|
-
p: 'This notice covers the personal data we collect through this website and when you contact us. Data inside a customer workspace is covered by that customer’s agreement with us.',
|
|
1149
|
-
},
|
|
1150
|
-
{ h2: 'What we collect' },
|
|
1151
|
-
{
|
|
1152
|
-
ul: [
|
|
1153
|
-
'The name, work email and company you give us when you ask for a demo or write to us',
|
|
1154
|
-
'Records of our emails and calls with you',
|
|
1155
|
-
'Anonymous page views, counted without cookies and without tracking you across sites',
|
|
1156
|
-
],
|
|
1157
|
-
},
|
|
1158
|
-
{ h2: 'How long we keep it' },
|
|
1159
|
-
{
|
|
1160
|
-
p: 'Enquiries that do not lead to a contract are deleted after 24 months. Customer contacts are kept for the length of the contract and six years after it, as UK law requires for business records.',
|
|
1161
|
-
},
|
|
1162
|
-
{ h2: 'Your rights' },
|
|
1163
|
-
{
|
|
1164
|
-
p: `You can ask to see, correct or delete the data we hold about you by writing to ${saasEmail(name, 'privacy')}. You can also complain to the Information Commissioner’s Office.`,
|
|
1165
|
-
},
|
|
1166
|
-
]),
|
|
591
|
+
const creditsBlocks = [
|
|
592
|
+
{
|
|
593
|
+
_key: 'credits-body',
|
|
594
|
+
_type: 'prose',
|
|
595
|
+
_version: BLOCK_VERSION,
|
|
596
|
+
body: [
|
|
597
|
+
...richText('credits-intro', words.credits.intro),
|
|
598
|
+
...SAAS_PHOTO_CREDITS.filter((credit) => credit.locales.includes(copy.locale)).map((credit, index) => creditNode(`credits-${index}`, credit, words.credits.sourceWord)),
|
|
599
|
+
],
|
|
600
|
+
},
|
|
1167
601
|
];
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
602
|
+
const entries = [
|
|
603
|
+
['home', homeBlocks],
|
|
604
|
+
['product', productBlocks],
|
|
605
|
+
['pricing', pricingBlocks],
|
|
606
|
+
['security', securityBlocks],
|
|
607
|
+
['changelog', changelogBlocks],
|
|
608
|
+
['about', aboutBlocks],
|
|
609
|
+
['demo', demoBlocks],
|
|
610
|
+
['legal', [prose('legal-body', words.legal)]],
|
|
611
|
+
['privacy', [prose('privacy-body', words.privacy)]],
|
|
612
|
+
['credits', creditsBlocks],
|
|
1178
613
|
];
|
|
614
|
+
return entries.map(([key, blocks]) => ({
|
|
615
|
+
title: words.titles[key],
|
|
616
|
+
slug: copy.slugs[key],
|
|
617
|
+
blocks,
|
|
618
|
+
}));
|
|
1179
619
|
}
|
|
1180
620
|
const CHANGELOG_ENTRY = { kind: 'collection', collection: 'changelog' };
|
|
1181
621
|
const CHANGELOG_MONTH = { kind: 'dateArchive' };
|
|
@@ -1184,54 +624,55 @@ const SEARCH_RESULTS = { kind: 'search' };
|
|
|
1184
624
|
function onlyOn(...targets) {
|
|
1185
625
|
return { pages: { mode: 'only', targets } };
|
|
1186
626
|
}
|
|
1187
|
-
export
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
area: 'sidebar',
|
|
1196
|
-
type: 'archives',
|
|
1197
|
-
title: 'Changelog by month',
|
|
1198
|
-
settings: { collection: 'changelog', granularity: 'month', showCounts: true, limit: 12 },
|
|
1199
|
-
visibility: onlyOn(CHANGELOG_ENTRY, CHANGELOG_MONTH, SEARCH_RESULTS),
|
|
1200
|
-
},
|
|
1201
|
-
{
|
|
1202
|
-
area: 'sidebar',
|
|
1203
|
-
type: 'recentEntries',
|
|
1204
|
-
title: 'Recently shipped',
|
|
1205
|
-
settings: { collection: 'changelog', count: 3, showDate: true },
|
|
1206
|
-
visibility: onlyOn(FEATURE_PAGE, SEARCH_RESULTS),
|
|
1207
|
-
},
|
|
1208
|
-
{
|
|
1209
|
-
area: 'sidebar',
|
|
1210
|
-
type: 'links',
|
|
1211
|
-
title: 'Resources',
|
|
1212
|
-
settings: {
|
|
1213
|
-
items: [
|
|
1214
|
-
{ label: 'Product overview', href: '/product' },
|
|
1215
|
-
{ label: 'API and webhooks', href: '/features/api-and-webhooks' },
|
|
1216
|
-
{ label: 'ERP sync', href: '/features/erp-sync' },
|
|
1217
|
-
{ label: 'Security and compliance', href: '/security' },
|
|
1218
|
-
{ label: 'Pricing', href: '/pricing' },
|
|
1219
|
-
],
|
|
627
|
+
export function saasWidgets(copy) {
|
|
628
|
+
const w = copy.widgets;
|
|
629
|
+
return [
|
|
630
|
+
{
|
|
631
|
+
area: 'sidebar',
|
|
632
|
+
type: 'search',
|
|
633
|
+
settings: { placeholder: w.searchPlaceholder },
|
|
634
|
+
visibility: onlyOn(CHANGELOG_ENTRY, CHANGELOG_MONTH, FEATURE_PAGE),
|
|
1220
635
|
},
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
heading: 'See it on your own approval policy',
|
|
1228
|
-
body: 'A 30-minute call with a solutions engineer, then a trial workspace set up with your policy.',
|
|
1229
|
-
label: 'Book a demo',
|
|
1230
|
-
href: '/demo',
|
|
636
|
+
{
|
|
637
|
+
area: 'sidebar',
|
|
638
|
+
type: 'archives',
|
|
639
|
+
title: w.archivesTitle,
|
|
640
|
+
settings: { collection: 'changelog', granularity: 'month', showCounts: true, limit: 12 },
|
|
641
|
+
visibility: onlyOn(CHANGELOG_ENTRY, CHANGELOG_MONTH, SEARCH_RESULTS),
|
|
1231
642
|
},
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
643
|
+
{
|
|
644
|
+
area: 'sidebar',
|
|
645
|
+
type: 'recentEntries',
|
|
646
|
+
title: w.recentTitle,
|
|
647
|
+
settings: { collection: 'changelog', count: 3, showDate: true },
|
|
648
|
+
visibility: onlyOn(FEATURE_PAGE, SEARCH_RESULTS),
|
|
649
|
+
},
|
|
650
|
+
{
|
|
651
|
+
area: 'sidebar',
|
|
652
|
+
type: 'links',
|
|
653
|
+
title: w.resourcesTitle,
|
|
654
|
+
settings: {
|
|
655
|
+
items: w.resources.map((item) => ({
|
|
656
|
+
label: item.label,
|
|
657
|
+
href: linkTarget(copy, item),
|
|
658
|
+
})),
|
|
659
|
+
},
|
|
660
|
+
visibility: onlyOn(CHANGELOG_ENTRY, CHANGELOG_MONTH, FEATURE_PAGE, SEARCH_RESULTS),
|
|
661
|
+
},
|
|
662
|
+
{
|
|
663
|
+
area: 'sidebar',
|
|
664
|
+
type: 'cta',
|
|
665
|
+
settings: {
|
|
666
|
+
heading: w.ctaHeading,
|
|
667
|
+
body: w.ctaBody,
|
|
668
|
+
label: w.ctaLabel,
|
|
669
|
+
href: pagePath(copy, 'demo'),
|
|
670
|
+
},
|
|
671
|
+
visibility: onlyOn(CHANGELOG_ENTRY, CHANGELOG_MONTH, FEATURE_PAGE, SEARCH_RESULTS),
|
|
672
|
+
},
|
|
673
|
+
];
|
|
674
|
+
}
|
|
675
|
+
export const SAAS_WIDGETS = saasWidgets(SAAS_COPY_EN);
|
|
1235
676
|
export const SAAS_RECOMMENDED_AGENTS = [
|
|
1236
677
|
{
|
|
1237
678
|
name: 'performanceAgent',
|
|
@@ -1244,6 +685,9 @@ export const SAAS_RECOMMENDED_AGENTS = [
|
|
|
1244
685
|
reason: 'Tracks dependency vulnerabilities, the first thing a buyer’s security review asks about.',
|
|
1245
686
|
},
|
|
1246
687
|
];
|
|
688
|
+
// ---------------------------------------------------------------------------
|
|
689
|
+
// Media
|
|
690
|
+
// ---------------------------------------------------------------------------
|
|
1247
691
|
/**
|
|
1248
692
|
* `saas`'s own starting skin, asserted present with a real check rather than
|
|
1249
693
|
* a `!`: `STARTING_SKINS` is keyed by blueprint id and TypeScript cannot see
|
|
@@ -1262,136 +706,80 @@ function saasPalette() {
|
|
|
1262
706
|
}
|
|
1263
707
|
/**
|
|
1264
708
|
* Every picture this blueprint seeds, all bundled files (`assets/photos/saas/`):
|
|
1265
|
-
* interface screenshots
|
|
1266
|
-
* Geist and Geist Mono (OFL),
|
|
1267
|
-
* typefaces, and
|
|
1268
|
-
* fallback `DemoMediaSpec` requires when a file is missing.
|
|
709
|
+
* interface screenshots drawn once from the product's own UI language with
|
|
710
|
+
* Geist and Geist Mono (OFL) in each language, customer wordmarks drawn once
|
|
711
|
+
* from OFL typefaces, and three real photographs. The procedural `spec` is only
|
|
712
|
+
* the fallback `DemoMediaSpec` requires when a file is missing.
|
|
1269
713
|
*/
|
|
1270
|
-
export
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
photo: 'saas/spend-reporting.png',
|
|
1306
|
-
},
|
|
1307
|
-
{
|
|
1308
|
-
name: 'api-webhooks',
|
|
1309
|
-
spec: coverArt(saasPalette(), 27),
|
|
1310
|
-
alt: 'The API and webhooks settings, with an HTTP request creating a purchase request and two webhook endpoints',
|
|
1311
|
-
photo: 'saas/api-webhooks.png',
|
|
1312
|
-
},
|
|
1313
|
-
{
|
|
1314
|
-
name: 'portrait',
|
|
1315
|
-
spec: coverArt(saasPalette(), 28),
|
|
1316
|
-
alt: 'Portrait of Adrian Tan, financial controller at Halvorsen Freight',
|
|
1317
|
-
photo: 'saas/adrian-tan.jpg',
|
|
1318
|
-
},
|
|
1319
|
-
...SAAS_CUSTOMERS.map((customer, index) => ({
|
|
1320
|
-
name: customer.key,
|
|
1321
|
-
spec: logoArt(30 + index),
|
|
1322
|
-
alt: `${customer.name} logo`,
|
|
1323
|
-
photo: `saas/${customer.key}.png`,
|
|
1324
|
-
})),
|
|
1325
|
-
];
|
|
1326
|
-
/** Header navigation and the header action. The footer is grouped in columns: see `SAAS_FOOTER`. */
|
|
1327
|
-
export const SAAS_MENUS = {
|
|
1328
|
-
header: [
|
|
1329
|
-
{ label: 'Product', url: '/product' },
|
|
1330
|
-
{ label: 'Security', url: '/security' },
|
|
1331
|
-
{ label: 'Pricing', url: '/pricing' },
|
|
1332
|
-
{ label: 'Changelog', url: '/changelog' },
|
|
1333
|
-
{ label: 'Company', url: '/about' },
|
|
1334
|
-
],
|
|
1335
|
-
// Seeded with the demo content instead (`seedFooterMenu`): a footer in
|
|
1336
|
-
// columns needs an unlinked heading per column, which `BlueprintMenus`
|
|
1337
|
-
// cannot express, and its links lead to pages the demo content creates.
|
|
1338
|
-
footer: [],
|
|
1339
|
-
headerAction: { label: 'Book a demo', url: '/demo' },
|
|
1340
|
-
};
|
|
714
|
+
export function saasMediaSpecs(copy) {
|
|
715
|
+
const palette = saasPalette();
|
|
716
|
+
return [
|
|
717
|
+
...copy.media.map((item, index) => ({
|
|
718
|
+
name: item.name,
|
|
719
|
+
spec: coverArt(palette, 21 + index),
|
|
720
|
+
alt: item.alt,
|
|
721
|
+
photo: `saas/${item.file}`,
|
|
722
|
+
})),
|
|
723
|
+
...copy.customers.map((customer, index) => ({
|
|
724
|
+
name: customer.key,
|
|
725
|
+
spec: logoArt(30 + index),
|
|
726
|
+
alt: customer.name,
|
|
727
|
+
photo: `saas/${customer.key}.png`,
|
|
728
|
+
})),
|
|
729
|
+
];
|
|
730
|
+
}
|
|
731
|
+
export const SAAS_MEDIA_SPECS = saasMediaSpecs(SAAS_COPY_EN);
|
|
732
|
+
// ---------------------------------------------------------------------------
|
|
733
|
+
// Menus and settings
|
|
734
|
+
// ---------------------------------------------------------------------------
|
|
735
|
+
/** Header navigation and the header action. The footer is grouped in columns: see `saasFooter`. */
|
|
736
|
+
export function saasMenus(copy) {
|
|
737
|
+
return {
|
|
738
|
+
header: copy.menus.header.map((item) => ({
|
|
739
|
+
label: item.label,
|
|
740
|
+
url: pagePath(copy, item.page),
|
|
741
|
+
})),
|
|
742
|
+
// Seeded with the demo content instead (`seedFooterMenu`): a footer in
|
|
743
|
+
// columns needs an unlinked heading per column, which `BlueprintMenus`
|
|
744
|
+
// cannot express, and its links lead to pages the demo content creates.
|
|
745
|
+
footer: [],
|
|
746
|
+
headerAction: { label: copy.menus.action, url: pagePath(copy, 'demo') },
|
|
747
|
+
};
|
|
748
|
+
}
|
|
1341
749
|
/** The footer menu: one unlinked heading per column, its links under it. */
|
|
1342
|
-
export
|
|
1343
|
-
{
|
|
1344
|
-
heading:
|
|
1345
|
-
links:
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
{ label: '
|
|
1356
|
-
{ label: '
|
|
1357
|
-
{ label: 'Book a demo', url: '/demo' },
|
|
1358
|
-
],
|
|
1359
|
-
},
|
|
1360
|
-
{
|
|
1361
|
-
heading: 'Resources',
|
|
1362
|
-
links: [
|
|
1363
|
-
{ label: 'Changelog', url: '/changelog' },
|
|
1364
|
-
{ label: 'API and webhooks', url: '/features/api-and-webhooks' },
|
|
1365
|
-
{ label: 'ERP sync', url: '/features/erp-sync' },
|
|
1366
|
-
],
|
|
1367
|
-
},
|
|
1368
|
-
{
|
|
1369
|
-
heading: 'Legal',
|
|
1370
|
-
links: [
|
|
1371
|
-
{ label: 'Terms and company', url: '/legal' },
|
|
1372
|
-
{ label: 'Privacy', url: '/privacy' },
|
|
750
|
+
export function saasFooter(copy) {
|
|
751
|
+
return copy.footer.map((column) => ({
|
|
752
|
+
heading: column.heading,
|
|
753
|
+
links: column.links.map((link) => ({ label: link.label, url: linkTarget(copy, link) })),
|
|
754
|
+
}));
|
|
755
|
+
}
|
|
756
|
+
export const SAAS_MENUS = saasMenus(SAAS_COPY_EN);
|
|
757
|
+
export const SAAS_FOOTER = saasFooter(SAAS_COPY_EN);
|
|
758
|
+
export function saasSiteSettings(copy) {
|
|
759
|
+
return {
|
|
760
|
+
'general.tagline': copy.settings.tagline,
|
|
761
|
+
'general.socialLinks': [
|
|
762
|
+
{ label: 'LinkedIn', url: 'https://linkedin.com/company/example' },
|
|
763
|
+
{ label: 'GitHub', url: 'https://github.com/example' },
|
|
764
|
+
{ label: 'X', url: 'https://x.com/example' },
|
|
1373
765
|
],
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
'general.footerNote': '2nd floor, 41 Tabernacle Street\nLondon EC2A 4AA\nUnited Kingdom\n\nSupport on business days, 08:00 to 18:00 UK time.',
|
|
1384
|
-
// A changelog entry and a feature page are not discussions.
|
|
1385
|
-
'discussion.enabled': false,
|
|
1386
|
-
};
|
|
766
|
+
'general.footerNote': copy.settings.footerNote,
|
|
767
|
+
// A changelog entry and a feature page are not discussions.
|
|
768
|
+
'discussion.enabled': false,
|
|
769
|
+
};
|
|
770
|
+
}
|
|
771
|
+
export const SAAS_SITE_SETTINGS = saasSiteSettings(SAAS_COPY_EN);
|
|
772
|
+
// ---------------------------------------------------------------------------
|
|
773
|
+
// Seeding
|
|
774
|
+
// ---------------------------------------------------------------------------
|
|
1387
775
|
/** Seeds the footer menu at the `footer` location, one placeholder per column with its links as children. */
|
|
1388
|
-
async function seedFooterMenu(db, locale) {
|
|
776
|
+
async function seedFooterMenu(db, locale, columns) {
|
|
1389
777
|
await ensureMenuTables(db);
|
|
1390
778
|
const store = createMenuStore({ db });
|
|
1391
779
|
if ((await store.byLocation('footer', locale)) !== null)
|
|
1392
780
|
return;
|
|
1393
781
|
const menu = await store.create({ name: 'footer', locale, label: 'footer', location: 'footer' });
|
|
1394
|
-
for (const column of
|
|
782
|
+
for (const column of columns) {
|
|
1395
783
|
const heading = await store.createItem(menu.id, {
|
|
1396
784
|
label: column.heading,
|
|
1397
785
|
kind: 'submenu-placeholder',
|
|
@@ -1413,60 +801,69 @@ async function seedFooterMenu(db, locale) {
|
|
|
1413
801
|
* descending shows the newest first. Everything is published: a theme lists
|
|
1414
802
|
* only published entries.
|
|
1415
803
|
*/
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
const
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
804
|
+
function seedSaasDemoContent(copy, model) {
|
|
805
|
+
return async (ctx) => {
|
|
806
|
+
const { db, defaultLocale, adminId, media } = ctx;
|
|
807
|
+
const name = nameOf(ctx.siteName);
|
|
808
|
+
const featureStore = createContentStore({ db, collection: model.feature, defaultLocale });
|
|
809
|
+
const changelogStore = createContentStore({ db, collection: model.changelog, defaultLocale });
|
|
810
|
+
const pageStore = createContentStore({ db, collection: model.page, defaultLocale });
|
|
811
|
+
const featureIdBySlug = new Map();
|
|
812
|
+
for (const demo of copy.features) {
|
|
813
|
+
const screenshot = media[demo.screenshot];
|
|
814
|
+
const created = await featureStore.create({
|
|
815
|
+
status: 'published',
|
|
816
|
+
createdBy: adminId,
|
|
817
|
+
values: {
|
|
818
|
+
name: demo.name,
|
|
819
|
+
slug: demo.slug,
|
|
820
|
+
description: demo.description,
|
|
821
|
+
icon: demo.icon,
|
|
822
|
+
...(screenshot === undefined ? {} : { coverImage: screenshot }),
|
|
823
|
+
},
|
|
824
|
+
blocks: { blocks: featureBlocks(copy, demo, name).map(toBlockZoneEntry) },
|
|
825
|
+
});
|
|
826
|
+
featureIdBySlug.set(demo.slug, created.id);
|
|
827
|
+
}
|
|
828
|
+
for (const update of copy.updates) {
|
|
829
|
+
await changelogStore.create({
|
|
830
|
+
status: 'published',
|
|
831
|
+
createdBy: adminId,
|
|
832
|
+
values: {
|
|
833
|
+
title: update.title,
|
|
834
|
+
slug: update.slug,
|
|
835
|
+
summary: update.summary,
|
|
836
|
+
publishedAt: update.publishedAt,
|
|
837
|
+
},
|
|
838
|
+
blocks: { blocks: updateBlocks(copy, update, name, media).map(toBlockZoneEntry) },
|
|
839
|
+
});
|
|
840
|
+
}
|
|
841
|
+
for (const demo of buildSaasDemoPages(media, featureIdBySlug, name, copy)) {
|
|
842
|
+
await pageStore.create({
|
|
843
|
+
status: 'published',
|
|
844
|
+
createdBy: adminId,
|
|
845
|
+
values: { title: demo.title, slug: demo.slug },
|
|
846
|
+
blocks: { blocks: demo.blocks.map(toBlockZoneEntry) },
|
|
847
|
+
});
|
|
848
|
+
}
|
|
849
|
+
await seedFooterMenu(db, defaultLocale, saasFooter(copy));
|
|
850
|
+
};
|
|
1461
851
|
}
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
852
|
+
/** The whole blueprint, in one language. */
|
|
853
|
+
export function createSaasContentPack(locale) {
|
|
854
|
+
const copy = saasCopyFor(locale);
|
|
855
|
+
const model = saasSchema(copy);
|
|
856
|
+
return {
|
|
857
|
+
collections: model.collections,
|
|
858
|
+
recommendedAgents: SAAS_RECOMMENDED_AGENTS,
|
|
859
|
+
seedDemoContent: seedSaasDemoContent(copy, model),
|
|
860
|
+
defaultTheme: '@cogenta/theme-saas',
|
|
861
|
+
menus: saasMenus(copy),
|
|
862
|
+
widgets: saasWidgets(copy),
|
|
863
|
+
siteSettings: saasSiteSettings(copy),
|
|
864
|
+
mediaSpecs: saasMediaSpecs(copy),
|
|
865
|
+
};
|
|
866
|
+
}
|
|
867
|
+
/** The English pack, which `BLUEPRINT_CONTENT_PACKS` lists; `contentPackFor` picks by locale. */
|
|
868
|
+
export const saasContentPack = createSaasContentPack('en');
|
|
1472
869
|
//# sourceMappingURL=saas.js.map
|