├── docs ├── .gitignore ├── postcss.config.js ├── images │ ├── search.png │ ├── seo-og.png │ ├── brand-sky.jpeg │ ├── brand-lime.jpeg │ ├── brand-pink.jpeg │ ├── heading-hero.png │ ├── product-meta.png │ ├── brand-purple.jpeg │ ├── heading-default.png │ ├── use-codespaces.webp │ ├── use-gh-template.png │ ├── widget-cta-dark.png │ ├── brand-jelly-bean.jpeg │ ├── custom-page-promo.png │ ├── widget-blockquote.png │ ├── widget-cta-image.png │ ├── widget-cta-light.png │ ├── widget-hero-full.png │ ├── widget-hero-split.png │ ├── widget-promo-left.png │ ├── brand-palette-export.png │ ├── custom-page-about-us.png │ ├── design-system-colors.png │ ├── design-system-icons.png │ ├── design-system-story.png │ ├── widget-content-image.png │ ├── widget-promo-right.png │ ├── design-system-mobile-view.png │ ├── design-system-typography.png │ ├── widget-content-rich-text.png │ ├── widget-product-categories.png │ ├── widget-products-condensed.png │ ├── widget-products-featured.png │ └── widget-products-standard.png ├── public │ └── images │ │ ├── home-on-dark.png │ │ └── home-on-light.png ├── package.json ├── .vitepress │ ├── theme │ │ ├── index.js │ │ └── style.css │ ├── njk-html.tmLanguage.json │ └── config.js ├── developer │ ├── resources.md │ ├── index.md │ └── getting-started.md ├── user │ ├── custom-pages.md │ ├── search-and-seo.md │ ├── index.md │ ├── products-and-categories.md │ └── getting-started.md ├── index.md └── README.md ├── public └── images │ └── logo.png ├── postcss.config.js ├── modules ├── content-widget │ ├── views │ │ └── widget.html │ └── index.js ├── i18n │ ├── index.js │ └── i18n │ │ └── app │ │ └── en.json ├── theme │ ├── public │ │ └── fonts │ │ │ ├── inter-roman.var.woff2 │ │ │ └── inter-italic.var.woff2 │ ├── icons │ │ ├── bars-3.svg │ │ ├── magnifying-glass.svg │ │ ├── heart.svg │ │ ├── social-facebook.svg │ │ ├── user-circle.svg │ │ ├── social-linkedin.svg │ │ ├── quote.svg │ │ ├── social-youtube.svg │ │ ├── social-twitter.svg │ │ ├── social-instagram.svg │ │ └── LICENSE.md │ ├── views │ │ ├── design-system │ │ │ ├── icon │ │ │ │ ├── icon.stories.js │ │ │ │ └── svg.njk │ │ │ ├── logo │ │ │ │ ├── logo.stories.js │ │ │ │ └── logo.njk │ │ │ ├── heading │ │ │ │ ├── heading.stories.js │ │ │ │ └── heading.njk │ │ │ ├── promo-widget │ │ │ │ ├── promo.stories.js │ │ │ │ ├── promo.json │ │ │ │ └── promo.njk │ │ │ ├── footer │ │ │ │ ├── footer.stories.js │ │ │ │ ├── footer.njk │ │ │ │ └── footer.json │ │ │ ├── header │ │ │ │ ├── header.stories.js │ │ │ │ ├── header.njk │ │ │ │ └── header.json │ │ │ ├── 00-global │ │ │ │ ├── global.stories.js │ │ │ │ └── colors.njk │ │ │ ├── cta-widget │ │ │ │ ├── cta.stories.js │ │ │ │ ├── image.njk │ │ │ │ ├── data.json │ │ │ │ └── solid.njk │ │ │ ├── hero-widget │ │ │ │ ├── hero-widget.stories.js │ │ │ │ ├── full.njk │ │ │ │ ├── split.njk │ │ │ │ └── data.json │ │ │ ├── button │ │ │ │ ├── button.stories.js │ │ │ │ ├── icon.njk │ │ │ │ ├── primary.njk │ │ │ │ └── outlined.njk │ │ │ └── card │ │ │ │ ├── card.stories.js │ │ │ │ ├── category-list.njk │ │ │ │ ├── product-masonry.njk │ │ │ │ ├── category.njk │ │ │ │ ├── product-list.njk │ │ │ │ ├── product.njk │ │ │ │ └── blockquote.njk │ │ ├── utils.html │ │ ├── heading.html │ │ ├── logo.html │ │ ├── icon.html │ │ ├── helpers.html │ │ ├── blockquote.html │ │ ├── footer.html │ │ ├── gallery.html │ │ ├── promo-widget.html │ │ └── header.html │ └── ui │ │ └── src │ │ ├── index.js │ │ ├── components │ │ ├── mobile-nav.js │ │ ├── tabs.js │ │ └── gallery.js │ │ └── tailwind.css ├── @apostrophecms │ ├── asset │ │ └── index.js │ ├── express │ │ └── index.js │ ├── search │ │ ├── views │ │ │ ├── pager.html │ │ │ └── index.html │ │ └── index.js │ ├── attachment │ │ └── index.js │ ├── home-page │ │ ├── views │ │ │ └── page.html │ │ └── index.js │ ├── settings │ │ └── index.js │ ├── page │ │ ├── views │ │ │ └── notFound.html │ │ └── index.js │ └── admin-bar │ │ └── index.js ├── hero-full-widget │ └── views │ │ └── widget.html ├── hero-split-widget │ └── views │ │ └── widget.html ├── promo-widget │ ├── views │ │ └── widget.html │ └── index.js ├── blockquote-widget │ ├── views │ │ └── widget.html │ └── index.js ├── cta-widget │ └── views │ │ └── widget.html ├── default-page │ ├── views │ │ └── page.html │ └── index.js ├── tag │ └── index.js ├── product-category-page │ ├── views │ │ ├── index.html │ │ └── show.html │ └── index.js ├── product-page │ ├── views │ │ └── index.html │ └── index.js ├── product-category-widget │ └── views │ │ └── widget.html ├── product-featured-widget │ └── views │ │ └── widget.html └── product-widget │ └── views │ └── widget.html ├── eslint.config.js ├── deployment ├── README ├── settings.staging ├── migrate ├── rsync_exclude.txt ├── stop ├── settings ├── dependencies └── start ├── design-system-setup └── @corllete │ ├── apos-ds │ └── index.js │ └── apos-ds-page-type │ └── views │ └── layout │ └── preview.html ├── colors ├── lime.json ├── pink.json ├── sky.json ├── default.json └── purple.json ├── apos.vite.config.js ├── .gitignore ├── .github └── workflows │ ├── test.yml │ └── docs.yml ├── test └── lib │ └── tools.js ├── LICENSE ├── .devcontainer ├── docker-compose.yml ├── Dockerfile └── devcontainer.json ├── scripts ├── sync-down ├── sync-up └── make-svg-sprite ├── views └── layout.html ├── tailwind.config.js ├── app.js └── package.json /docs/.gitignore: -------------------------------------------------------------------------------- 1 | .vitepress/cache 2 | .vitepress/dist 3 | -------------------------------------------------------------------------------- /docs/postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: {} 3 | }; 4 | -------------------------------------------------------------------------------- /docs/images/search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apostrophecms/starter-kit-ecommerce/main/docs/images/search.png -------------------------------------------------------------------------------- /docs/images/seo-og.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apostrophecms/starter-kit-ecommerce/main/docs/images/seo-og.png -------------------------------------------------------------------------------- /public/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apostrophecms/starter-kit-ecommerce/main/public/images/logo.png -------------------------------------------------------------------------------- /docs/images/brand-sky.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apostrophecms/starter-kit-ecommerce/main/docs/images/brand-sky.jpeg -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {} 5 | } 6 | }; 7 | -------------------------------------------------------------------------------- /docs/images/brand-lime.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apostrophecms/starter-kit-ecommerce/main/docs/images/brand-lime.jpeg -------------------------------------------------------------------------------- /docs/images/brand-pink.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apostrophecms/starter-kit-ecommerce/main/docs/images/brand-pink.jpeg -------------------------------------------------------------------------------- /docs/images/heading-hero.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apostrophecms/starter-kit-ecommerce/main/docs/images/heading-hero.png -------------------------------------------------------------------------------- /docs/images/product-meta.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apostrophecms/starter-kit-ecommerce/main/docs/images/product-meta.png -------------------------------------------------------------------------------- /docs/images/brand-purple.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apostrophecms/starter-kit-ecommerce/main/docs/images/brand-purple.jpeg -------------------------------------------------------------------------------- /docs/images/heading-default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apostrophecms/starter-kit-ecommerce/main/docs/images/heading-default.png -------------------------------------------------------------------------------- /docs/images/use-codespaces.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apostrophecms/starter-kit-ecommerce/main/docs/images/use-codespaces.webp -------------------------------------------------------------------------------- /docs/images/use-gh-template.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apostrophecms/starter-kit-ecommerce/main/docs/images/use-gh-template.png -------------------------------------------------------------------------------- /docs/images/widget-cta-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apostrophecms/starter-kit-ecommerce/main/docs/images/widget-cta-dark.png -------------------------------------------------------------------------------- /docs/images/brand-jelly-bean.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apostrophecms/starter-kit-ecommerce/main/docs/images/brand-jelly-bean.jpeg -------------------------------------------------------------------------------- /docs/images/custom-page-promo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apostrophecms/starter-kit-ecommerce/main/docs/images/custom-page-promo.png -------------------------------------------------------------------------------- /docs/images/widget-blockquote.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apostrophecms/starter-kit-ecommerce/main/docs/images/widget-blockquote.png -------------------------------------------------------------------------------- /docs/images/widget-cta-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apostrophecms/starter-kit-ecommerce/main/docs/images/widget-cta-image.png -------------------------------------------------------------------------------- /docs/images/widget-cta-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apostrophecms/starter-kit-ecommerce/main/docs/images/widget-cta-light.png -------------------------------------------------------------------------------- /docs/images/widget-hero-full.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apostrophecms/starter-kit-ecommerce/main/docs/images/widget-hero-full.png -------------------------------------------------------------------------------- /docs/images/widget-hero-split.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apostrophecms/starter-kit-ecommerce/main/docs/images/widget-hero-split.png -------------------------------------------------------------------------------- /docs/images/widget-promo-left.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apostrophecms/starter-kit-ecommerce/main/docs/images/widget-promo-left.png -------------------------------------------------------------------------------- /docs/images/brand-palette-export.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apostrophecms/starter-kit-ecommerce/main/docs/images/brand-palette-export.png -------------------------------------------------------------------------------- /docs/images/custom-page-about-us.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apostrophecms/starter-kit-ecommerce/main/docs/images/custom-page-about-us.png -------------------------------------------------------------------------------- /docs/images/design-system-colors.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apostrophecms/starter-kit-ecommerce/main/docs/images/design-system-colors.png -------------------------------------------------------------------------------- /docs/images/design-system-icons.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apostrophecms/starter-kit-ecommerce/main/docs/images/design-system-icons.png -------------------------------------------------------------------------------- /docs/images/design-system-story.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apostrophecms/starter-kit-ecommerce/main/docs/images/design-system-story.png -------------------------------------------------------------------------------- /docs/images/widget-content-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apostrophecms/starter-kit-ecommerce/main/docs/images/widget-content-image.png -------------------------------------------------------------------------------- /docs/images/widget-promo-right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apostrophecms/starter-kit-ecommerce/main/docs/images/widget-promo-right.png -------------------------------------------------------------------------------- /docs/public/images/home-on-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apostrophecms/starter-kit-ecommerce/main/docs/public/images/home-on-dark.png -------------------------------------------------------------------------------- /docs/public/images/home-on-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apostrophecms/starter-kit-ecommerce/main/docs/public/images/home-on-light.png -------------------------------------------------------------------------------- /docs/images/design-system-mobile-view.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apostrophecms/starter-kit-ecommerce/main/docs/images/design-system-mobile-view.png -------------------------------------------------------------------------------- /docs/images/design-system-typography.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apostrophecms/starter-kit-ecommerce/main/docs/images/design-system-typography.png -------------------------------------------------------------------------------- /docs/images/widget-content-rich-text.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apostrophecms/starter-kit-ecommerce/main/docs/images/widget-content-rich-text.png -------------------------------------------------------------------------------- /docs/images/widget-product-categories.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apostrophecms/starter-kit-ecommerce/main/docs/images/widget-product-categories.png -------------------------------------------------------------------------------- /docs/images/widget-products-condensed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apostrophecms/starter-kit-ecommerce/main/docs/images/widget-products-condensed.png -------------------------------------------------------------------------------- /docs/images/widget-products-featured.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apostrophecms/starter-kit-ecommerce/main/docs/images/widget-products-featured.png -------------------------------------------------------------------------------- /docs/images/widget-products-standard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apostrophecms/starter-kit-ecommerce/main/docs/images/widget-products-standard.png -------------------------------------------------------------------------------- /modules/content-widget/views/widget.html: -------------------------------------------------------------------------------- 1 |
2 | {%- area data.widget, 'content' -%} 3 |
4 | -------------------------------------------------------------------------------- /modules/i18n/index.js: -------------------------------------------------------------------------------- 1 | export default { 2 | i18n: { 3 | app: { 4 | browser: true 5 | } 6 | }, 7 | init(self) { 8 | } 9 | }; 10 | -------------------------------------------------------------------------------- /modules/theme/public/fonts/inter-roman.var.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apostrophecms/starter-kit-ecommerce/main/modules/theme/public/fonts/inter-roman.var.woff2 -------------------------------------------------------------------------------- /modules/theme/public/fonts/inter-italic.var.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apostrophecms/starter-kit-ecommerce/main/modules/theme/public/fonts/inter-italic.var.woff2 -------------------------------------------------------------------------------- /modules/@apostrophecms/asset/index.js: -------------------------------------------------------------------------------- 1 | export default { 2 | // When not in production, refresh the page on restart 3 | options: { 4 | refreshOnRestart: true 5 | } 6 | }; 7 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import apostrophe from 'eslint-config-apostrophe'; 2 | import { defineConfig } from 'eslint/config'; 3 | 4 | export default defineConfig([ 5 | apostrophe 6 | ]); 7 | -------------------------------------------------------------------------------- /modules/@apostrophecms/express/index.js: -------------------------------------------------------------------------------- 1 | export default { 2 | options: { 3 | session: { 4 | // Set a real secret! 5 | secret: process.env.APP_SESSION_SECRET || 'dev' 6 | } 7 | } 8 | }; 9 | -------------------------------------------------------------------------------- /modules/@apostrophecms/search/views/pager.html: -------------------------------------------------------------------------------- 1 | {% import '@apostrophecms/pager:macros.html' as pager %} 2 | {{ pager.render({ 3 | page: data.currentPage, 4 | total: data.totalPages, 5 | class: 'app-pager' 6 | }, data.url) }} 7 | -------------------------------------------------------------------------------- /deployment/README: -------------------------------------------------------------------------------- 1 | This is a deployment folder for use with Stagecoach. 2 | 3 | You don't have to use Stagecoach. 4 | 5 | It's just a neat solution for deploying node apps. 6 | 7 | See: 8 | 9 | http://github.com/apostrophecms/stagecoach 10 | -------------------------------------------------------------------------------- /modules/hero-full-widget/views/widget.html: -------------------------------------------------------------------------------- 1 | {% import 'theme:hero-widget.html' as hero %} 2 |
3 | {% rendercall hero.full(data.widget) %} 4 | {% area data.widget, 'content' %} 5 | {% endrendercall %} 6 |
7 | -------------------------------------------------------------------------------- /modules/@apostrophecms/attachment/index.js: -------------------------------------------------------------------------------- 1 | export default { 2 | options: { 3 | addFileGroups: [ 4 | { 5 | name: 'favicon', 6 | extensions: [ 'ico' ], 7 | extensionMaps: {} 8 | } 9 | ] 10 | } 11 | }; 12 | -------------------------------------------------------------------------------- /modules/hero-split-widget/views/widget.html: -------------------------------------------------------------------------------- 1 | {% import 'theme:hero-widget.html' as hero %} 2 |
3 | {% rendercall hero.split(data.widget) %} 4 | {% area data.widget, 'content' %} 5 | {% endrendercall %} 6 |
7 | -------------------------------------------------------------------------------- /modules/promo-widget/views/widget.html: -------------------------------------------------------------------------------- 1 | {%- import 'theme:promo-widget.html' as promo -%} 2 | 3 |
4 | {%- rendercall promo.render(data.widget) -%} 5 | {%- area data.widget, 'content' -%} 6 | {%- endrendercall -%} 7 |
8 | -------------------------------------------------------------------------------- /modules/theme/icons/bars-3.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /deployment/settings.staging: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Settings specific to the 'master' deployment target. 4 | # USER is the ssh user, SERVER is the ssh host. USER should 5 | # match the USER setting in /opt/stagecoach/settings on 6 | # the server 7 | 8 | USER=nodeapps 9 | SERVER=staging.apos.dev 10 | -------------------------------------------------------------------------------- /design-system-setup/@corllete/apos-ds/index.js: -------------------------------------------------------------------------------- 1 | export default { 2 | init(self) { 3 | // Custom filters, used in the design system only 4 | self.apos.template.addFilter({ 5 | jsonPretty(obj) { 6 | return JSON.stringify(obj, null, 2); 7 | } 8 | }); 9 | } 10 | }; 11 | -------------------------------------------------------------------------------- /modules/theme/views/design-system/icon/icon.stories.js: -------------------------------------------------------------------------------- 1 | export default { 2 | name: 'atoms-icons', 3 | category: 'Media', 4 | stories: [ 5 | { 6 | name: 'svg', 7 | label: 'SVG Icons', 8 | template: 'design-system/icon/svg.njk', 9 | state: 'complete' 10 | } 11 | ] 12 | }; 13 | -------------------------------------------------------------------------------- /modules/theme/views/design-system/logo/logo.stories.js: -------------------------------------------------------------------------------- 1 | export default { 2 | name: 'atoms-logo', 3 | category: 'Media', 4 | stories: [ 5 | { 6 | name: 'logo', 7 | label: 'Logo', 8 | template: 'design-system/logo/logo.njk', 9 | state: 'complete' 10 | } 11 | ] 12 | }; 13 | -------------------------------------------------------------------------------- /modules/theme/icons/magnifying-glass.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /colors/lime.json: -------------------------------------------------------------------------------- 1 | { 2 | "DEFAULT": "#65a30d", 3 | "50": "#f7fee7", 4 | "100": "#ecfccb", 5 | "200": "#d9f99d", 6 | "300": "#bef264", 7 | "400": "#a3e635", 8 | "500": "#84cc16", 9 | "600": "#65a30d", 10 | "700": "#4d7c0f", 11 | "800": "#3f6212", 12 | "900": "#365314", 13 | "950": "#1a2e05" 14 | } 15 | -------------------------------------------------------------------------------- /colors/pink.json: -------------------------------------------------------------------------------- 1 | { 2 | "DEFAULT": "#db2777", 3 | "50": "#fdf2f8", 4 | "100": "#fce7f3", 5 | "200": "#fbcfe8", 6 | "300": "#f9a8d4", 7 | "400": "#f472b6", 8 | "500": "#ec4899", 9 | "600": "#db2777", 10 | "700": "#be185d", 11 | "800": "#9d174d", 12 | "900": "#831843", 13 | "950": "#500724" 14 | } 15 | -------------------------------------------------------------------------------- /colors/sky.json: -------------------------------------------------------------------------------- 1 | { 2 | "DEFALUT": "#0284c7", 3 | "50": "#f0f9ff", 4 | "100": "#e0f2fe", 5 | "200": "#bae6fd", 6 | "300": "#7dd3fc", 7 | "400": "#38bdf8", 8 | "500": "#0ea5e9", 9 | "600": "#0284c7", 10 | "700": "#0369a1", 11 | "800": "#075985", 12 | "900": "#0c4a6e", 13 | "950": "#082f49" 14 | } 15 | -------------------------------------------------------------------------------- /modules/@apostrophecms/home-page/views/page.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% import 'theme:helpers.html' as helpers %} 3 | 4 | {% block main %} 5 | {% rendercall helpers.container() %} 6 | {% area data.page, 'heading' %} 7 | {% area data.page, 'main' %} 8 | {% endrendercall %} 9 | {% endblock %} 10 | -------------------------------------------------------------------------------- /colors/default.json: -------------------------------------------------------------------------------- 1 | { 2 | "DEFAULT": "#22909a", 3 | "50": "#effcfc", 4 | "100": "#d6f7f6", 5 | "200": "#b2efee", 6 | "300": "#7de2e3", 7 | "400": "#41cccf", 8 | "500": "#25b0b5", 9 | "600": "#22909a", 10 | "700": "#22737c", 11 | "800": "#235e67", 12 | "900": "#224f57", 13 | "950": "#11343b" 14 | } 15 | -------------------------------------------------------------------------------- /colors/purple.json: -------------------------------------------------------------------------------- 1 | { 2 | "DEFAULT": "#9333ea", 3 | "50": "#faf5ff", 4 | "100": "#f3e8ff", 5 | "200": "#e9d5ff", 6 | "300": "#d8b4fe", 7 | "400": "#c084fc", 8 | "500": "#a855f7", 9 | "600": "#9333ea", 10 | "700": "#7e22ce", 11 | "800": "#6b21a8", 12 | "900": "#581c87", 13 | "950": "#3b0764" 14 | } 15 | -------------------------------------------------------------------------------- /modules/theme/views/design-system/heading/heading.stories.js: -------------------------------------------------------------------------------- 1 | export default { 2 | name: 'molecules-heading', 3 | category: 'Page', 4 | stories: [ 5 | { 6 | name: 'heading', 7 | label: 'Heading', 8 | template: 'design-system/heading/heading.njk', 9 | state: 'complete' 10 | } 11 | ] 12 | }; 13 | -------------------------------------------------------------------------------- /deployment/migrate: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | export NODE_ENV=production 4 | 5 | # Run any necessary database migration tasks that should happen while the 6 | # site is paused here. 7 | # 8 | # We don't have any, 3.x policy is safe migrations only. -Tom 9 | 10 | # node app @apostrophecms/migration:migrate 11 | # 12 | #echo "Site migrated" 13 | -------------------------------------------------------------------------------- /docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "docs", 3 | "version": "1.0.0", 4 | "description": "Docs", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vitepress dev", 8 | "build": "vitepress build", 9 | "preview": "vitepress preview" 10 | }, 11 | "devDependencies": { 12 | "vitepress": "1.0.0-rc.29", 13 | "vue": "^3.3.4" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /modules/theme/views/design-system/promo-widget/promo.stories.js: -------------------------------------------------------------------------------- 1 | export default { 2 | name: 'molecules-promo-widget', 3 | category: 'Widgets', 4 | data: 'promo.json', 5 | stories: [ 6 | { 7 | name: 'product-card', 8 | label: 'Promo', 9 | template: 'design-system/promo-widget/promo.njk', 10 | state: 'complete' 11 | } 12 | ] 13 | }; 14 | -------------------------------------------------------------------------------- /modules/theme/icons/heart.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /modules/theme/views/design-system/footer/footer.stories.js: -------------------------------------------------------------------------------- 1 | export default { 2 | name: 'organisms-footer', 3 | category: 'Page', 4 | stories: [ 5 | { 6 | name: 'footer', 7 | label: 'Footer', 8 | template: 'design-system/footer/footer.njk', 9 | state: 'complete', 10 | data: 'footer.json', 11 | meta: false, 12 | list: false 13 | } 14 | ] 15 | }; 16 | -------------------------------------------------------------------------------- /modules/theme/views/design-system/header/header.stories.js: -------------------------------------------------------------------------------- 1 | export default { 2 | name: 'organisms-header', 3 | category: 'Page', 4 | stories: [ 5 | { 6 | name: 'header', 7 | label: 'Header', 8 | template: 'design-system/header/header.njk', 9 | state: 'complete', 10 | data: 'header.json', 11 | meta: false, 12 | list: false 13 | } 14 | ] 15 | }; 16 | -------------------------------------------------------------------------------- /modules/theme/icons/social-facebook.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /modules/theme/icons/user-circle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /modules/blockquote-widget/views/widget.html: -------------------------------------------------------------------------------- 1 | {%- import 'theme:blockquote.html' as bq -%} 2 | 3 |
4 | {% if data.widget.sectionTitle %} 5 |

{{ data.widget.sectionTitle }}

6 | {% endif %} 7 | {%- rendercall bq.render(data.widget) -%} 8 | {%- area data.widget, 'content' -%} 9 | {%- endrendercall -%} 10 |
11 | -------------------------------------------------------------------------------- /docs/.vitepress/theme/index.js: -------------------------------------------------------------------------------- 1 | // https://vitepress.dev/guide/custom-theme 2 | import { h } from 'vue'; 3 | import Theme from 'vitepress/theme'; 4 | import './style.css'; 5 | 6 | export default { 7 | ...Theme, 8 | Layout: () => { 9 | return h(Theme.Layout, null, { 10 | // https://vitepress.dev/guide/extending-default-theme#layout-slots 11 | }); 12 | }, 13 | enhanceApp({ 14 | app, router, siteData 15 | }) { 16 | // ... 17 | } 18 | }; 19 | -------------------------------------------------------------------------------- /modules/cta-widget/views/widget.html: -------------------------------------------------------------------------------- 1 | {%- import 'theme:cta-widget.html' as cta -%} 2 |
3 | {%- if data.widget.renderType === 'image' -%} 4 | {%- rendercall cta.image(data.widget) -%} 5 | {%- area data.widget, 'content' -%} 6 | {%- endrendercall -%} 7 | {%- else -%} 8 | {%- rendercall cta.solid(data.widget) -%} 9 | {%- area data.widget, 'content' -%} 10 | {%- endrendercall -%} 11 | {%- endif -%} 12 |
13 | -------------------------------------------------------------------------------- /modules/@apostrophecms/settings/index.js: -------------------------------------------------------------------------------- 1 | export default { 2 | options: { 3 | subforms: { 4 | title: { 5 | fields: [ 'title' ], 6 | protection: true, 7 | reload: true 8 | }, 9 | changePassword: { 10 | fields: [ 'password' ] 11 | } 12 | }, 13 | 14 | groups: { 15 | account: { 16 | label: 'Account', 17 | subforms: [ 'title', 'changePassword' ] 18 | } 19 | } 20 | } 21 | }; 22 | -------------------------------------------------------------------------------- /modules/@apostrophecms/page/views/notFound.html: -------------------------------------------------------------------------------- 1 | {% extends 'layout.html' %} 2 | {% import 'theme:helpers.html' as helpers %} 3 | {% import "theme:heading.html" as heading %} 4 | 5 | {% block title %}{{ __t('app:notFoundTitle') }}{% endblock %} 6 | 7 | {% block main %} 8 | {% rendercall helpers.container('mb-8 md:mb-16') %} 9 | {% render heading.render( 10 | __t('app:notFoundTitle'), 11 | __t('app:notFoundDesc') 12 | ) %} 13 | {% endrendercall %} 14 | {% endblock %} 15 | -------------------------------------------------------------------------------- /modules/theme/views/design-system/00-global/global.stories.js: -------------------------------------------------------------------------------- 1 | export default { 2 | name: 'atoms-global', 3 | category: 'Brand', 4 | stories: [ 5 | { 6 | name: 'typography', 7 | label: 'Typography', 8 | template: 'design-system/00-global/typography.njk', 9 | state: 'complete' 10 | }, 11 | { 12 | name: 'colors', 13 | label: 'Colors', 14 | template: 'design-system/00-global/colors.njk', 15 | state: 'complete' 16 | } 17 | ] 18 | }; 19 | -------------------------------------------------------------------------------- /apos.vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from '@apostrophecms/vite/vite'; 2 | 3 | export default defineConfig({ 4 | server: { 5 | watch: { 6 | // So that Tailwind CSS changes in the nunjucks templates do not trigger Vite 7 | // page reloads. This is done by `nodemon` and apos "refresh on restart" 8 | // because we need a process restart. 9 | ignored: [ 10 | '**/modules/views/**/*.html', 11 | '**/views/**/*.html' 12 | ] 13 | } 14 | } 15 | }); 16 | -------------------------------------------------------------------------------- /modules/default-page/views/page.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% import 'theme:helpers.html' as helpers %} 3 | {% import "theme:heading.html" as heading %} 4 | 5 | {% block main %} 6 | {% rendercall helpers.container() %} 7 | {% if data.page.headerType === 'widget' %} 8 | {% area data.page, 'heading' %} 9 | {% else %} 10 | {% render heading.render(data.page.title, data.page.tagline) %} 11 | {% endif %} 12 | {% area data.page, 'main' %} 13 | {% endrendercall %} 14 | {% endblock %} 15 | -------------------------------------------------------------------------------- /modules/tag/index.js: -------------------------------------------------------------------------------- 1 | export default { 2 | extend: '@apostrophecms/piece-type', 3 | options: { 4 | alias: 'tag', 5 | autopublish: true, 6 | label: 'app:tagLabel', 7 | pluralLabel: 'app:tagPluralLabel', 8 | seoFields: false, 9 | openGraph: false, 10 | searchable: false 11 | }, 12 | fields: { 13 | remove: [ 'visibility' ], 14 | group: { 15 | basics: { 16 | label: 'app:groupBasics', 17 | fields: [ 'title' ] 18 | } 19 | } 20 | } 21 | }; 22 | -------------------------------------------------------------------------------- /modules/@apostrophecms/admin-bar/index.js: -------------------------------------------------------------------------------- 1 | export default { 2 | options: { 3 | groups: [ 4 | { 5 | label: 'app:media', 6 | items: [ 7 | '@apostrophecms/image', 8 | '@apostrophecms/image-tag', 9 | '@apostrophecms/file', 10 | '@apostrophecms/file-tag' 11 | ] 12 | }, 13 | { 14 | label: 'app:content', 15 | items: [ 16 | 'product', 17 | 'product-category', 18 | 'tag' 19 | ] 20 | } 21 | ] 22 | } 23 | }; 24 | -------------------------------------------------------------------------------- /modules/theme/icons/social-linkedin.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /modules/theme/views/design-system/cta-widget/cta.stories.js: -------------------------------------------------------------------------------- 1 | export default { 2 | name: 'molecules-cta-widget', 3 | category: 'Widgets', 4 | data: 'data.json', 5 | stories: [ 6 | { 7 | name: 'cta-widget-image', 8 | label: 'Cta Widget Image', 9 | template: 'design-system/cta-widget/image.njk', 10 | state: 'complete' 11 | }, 12 | { 13 | name: 'cta-widget-solid', 14 | label: 'Cta Widget Solid', 15 | template: 'design-system/cta-widget/solid.njk', 16 | state: 'complete' 17 | } 18 | ] 19 | }; 20 | -------------------------------------------------------------------------------- /modules/theme/views/design-system/hero-widget/hero-widget.stories.js: -------------------------------------------------------------------------------- 1 | export default { 2 | name: 'molecules-hero-widget', 3 | category: 'Widgets', 4 | data: 'data.json', 5 | stories: [ 6 | { 7 | name: 'hero-widget-full', 8 | label: 'Hero - Full', 9 | template: 'design-system/hero-widget/full.njk', 10 | state: 'complete' 11 | }, 12 | { 13 | name: 'hero-widget-split', 14 | label: 'Hero - Split', 15 | template: 'design-system/hero-widget/split.njk', 16 | state: 'complete' 17 | } 18 | ] 19 | }; 20 | -------------------------------------------------------------------------------- /modules/content-widget/index.js: -------------------------------------------------------------------------------- 1 | export default { 2 | extend: '@apostrophecms/widget-type', 3 | options: { 4 | label: 'app:contentWidget' 5 | }, 6 | fields: { 7 | add: { 8 | content: { 9 | type: 'area', 10 | label: 'app:content', 11 | options: { 12 | widgets: { 13 | '@apostrophecms/rich-text': { 14 | className: 't-richtext my-5 md:my-10' 15 | }, 16 | '@apostrophecms/image': {}, 17 | '@apostrophecms/video': {} 18 | } 19 | } 20 | } 21 | } 22 | } 23 | }; 24 | -------------------------------------------------------------------------------- /modules/theme/views/utils.html: -------------------------------------------------------------------------------- 1 | {# 2 | Print attributes string from attrs object. 3 | Examples: 4 | attrs({ one: "1", two: true, three: false }) -> one="1" two 5 | attrs({ one: "1", two: true, three: false }, 'data-') -> data-one="1" data-three 6 | #} 7 | {% macro attrs(attrs, prefix) %} 8 | {%- for key, value in attrs -%} 9 | {%- if apos.theme.typeOf(value) == 'boolean' -%} 10 | {%- if value -%} 11 | {{ " " }}{{ prefix }}{{ key }} 12 | {%- endif -%} 13 | {%- else -%} 14 | {{ " " }}{{ prefix }}{{ key }}="{{ value }}" 15 | {%- endif -%} 16 | {%- endfor -%} 17 | {% endmacro %} 18 | -------------------------------------------------------------------------------- /modules/theme/icons/quote.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /docs/.vitepress/njk-html.tmLanguage.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "njk-html", 3 | "scopeName": "text.html.njk", 4 | "comment": "Nunjucks HTML Templates", 5 | "firstLineMatch": "^{% extends [\"'][^\"']+[\"'] %}", 6 | "foldingStartMarker": "(<(?i:(head|table|tr|div|style|script|ul|ol|form|dl))\\b.*?>|{%\\s*(block|filter|for|if|macro|raw))", 7 | "foldingStopMarker": "(|{%\\s*(endblock|endfilter|endfor|endif|endmacro|endraw)\\s*%})", 8 | "patterns": [ 9 | { 10 | "include": "source.jinja" 11 | }, 12 | { 13 | "include": "text.html.basic" 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /modules/theme/icons/social-youtube.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /deployment/rsync_exclude.txt: -------------------------------------------------------------------------------- 1 | # List files and folders that shouldn't be deployed (such as data folders and runtime status files) here. 2 | # In our projects .git and .gitignore are good candidates, also 'data' which contains persistent files 3 | # that are *not* part of deployment. A good place for things like data/port, data/pid, and any 4 | # sqlite databases or static web content you may need 5 | data 6 | temp 7 | public/uploads 8 | public/apos-frontend 9 | .git 10 | .gitignore 11 | # We don't deploy these anymore, instead we always 'npm install' to ensure 12 | # that any compiled C++ modules are built for the right architecture 13 | node_modules 14 | -------------------------------------------------------------------------------- /modules/theme/ui/src/index.js: -------------------------------------------------------------------------------- 1 | import { toggle as mobileNavToggle } from './components/mobile-nav.js'; 2 | import { init as initGalleries } from './components/gallery.js'; 3 | import { init as initTabs } from './components/tabs.js'; 4 | import './tailwind.css'; 5 | 6 | export default () => { 7 | const theme = window.apos.modules.theme || {}; 8 | 9 | // Components 10 | theme.mobileNavToggle = mobileNavToggle; 11 | 12 | // Register 13 | window.apos.modules.theme = theme; 14 | 15 | // Apostrophe integration - global events 16 | const onReadyAndRefresh = () => { 17 | initGalleries(); 18 | initTabs(); 19 | }; 20 | apos.util.onReady(onReadyAndRefresh); 21 | }; 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | package-lock.json 2 | /locales 3 | npm-debug.log 4 | /data 5 | /public/uploads 6 | /public/apos-minified 7 | /data/temp/uploadfs 8 | node_modules 9 | # This folder is created on the fly and contains symlinks updated at startup (we'll come up with a Windows solution that actually copies things) 10 | /public/modules 11 | # Don't commit build files 12 | /apos-build 13 | /public/apos-frontend 14 | /modules/asset/ui/public/site.js 15 | # Don't commit masters generated on the fly at startup, these import all the rest 16 | /public/css/master-*.less 17 | .jshintrc 18 | 19 | # Auto generated icons sprite 20 | **/icons.svg.html 21 | **/icons.svg.json 22 | 23 | .package-lock.json 24 | 25 | .DS_Store -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v4 15 | - name: Use Node.js 1v8 16 | uses: actions/setup-node@v4 17 | with: 18 | node-version: 22 19 | 20 | - name: Start MongoDB 21 | uses: supercharge/mongodb-github-action@1.12.0 22 | with: 23 | mongodb-version: 6.0 24 | 25 | - name: Install Dependencies 26 | run: npm i 27 | - name: Test Production Build 28 | run: npm run build 29 | - name: Test 30 | run: npm run test 31 | -------------------------------------------------------------------------------- /modules/theme/icons/social-twitter.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /modules/theme/views/design-system/footer/footer.njk: -------------------------------------------------------------------------------- 1 | {% extends dsPreview %} 2 | {% import "theme:footer.html" as footer %} 3 | 4 | {% block tagline %} 5 | Page Header 6 | {% endblock %} 7 | 8 | {% block preview %} 9 | {% render footer.render(story.data) %} 10 | {% endblock %} 11 | 12 | {% block variants %} 13 | {# container #} 14 |
15 | {# Code grid - 2 cols #} 16 |
17 | {% dscode 'njk' %} 18 | {% import "theme:footer.html" as footer %} 19 | 20 | {% render footer.render(config) %} 21 | {% enddscode %} 22 | 23 | {% dscode 'json', label = 'Data', parse = true %}{{ story.data | jsonPretty }}{% enddscode %} 24 |
25 |
26 | {% endblock %} 27 | -------------------------------------------------------------------------------- /modules/theme/icons/social-instagram.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /modules/product-category-page/views/index.html: -------------------------------------------------------------------------------- 1 | {% extends 'layout.html' %} 2 | {% import 'theme:helpers.html' as helpers %} 3 | {% import "theme:heading.html" as heading %} 4 | {% import "theme:card.html" as cards %} 5 | 6 | {% block main %} 7 | {% rendercall helpers.container() %} 8 | {% render heading.render(data.page.title, data.page.tagline) %} 9 | 10 |
11 | {% set cats = apos.theme.maybeFilterEmptyCategories(data.pieces, data.global) %} 12 | {% if not cats | length %} 13 |

{{ __t('app:noResults') }}

14 | {% else %} 15 | {% render cards.categoryList(cats) %} 16 | {% endif %} 17 |
18 | 19 | {% area data.page, 'main' %} 20 | {% endrendercall %} 21 | {% endblock %} 22 | -------------------------------------------------------------------------------- /modules/theme/views/design-system/header/header.njk: -------------------------------------------------------------------------------- 1 | {% extends dsPreview %} 2 | {% import "theme:header.html" as header %} 3 | 4 | {% block tagline %} 5 | Page Header 6 | {% endblock %} 7 | 8 | {% block preview %} 9 | {% render header.render(story.data, currentUrl = data.url) %} 10 | {% endblock %} 11 | 12 | {% block variants %} 13 | {# container #} 14 |
15 | {# Code grid - 2 cols #} 16 |
17 | {% dscode 'njk' %} 18 | {% import "theme:header.html" as header %} 19 | 20 | {% render header.render(config, currentUrl = data.url) %} 21 | {% enddscode %} 22 | 23 | {% dscode 'json', label = 'Data', parse = true %}{{ story.data | jsonPretty }}{% enddscode %} 24 |
25 |
26 | {% endblock %} 27 | -------------------------------------------------------------------------------- /modules/theme/views/heading.html: -------------------------------------------------------------------------------- 1 | {# 2 | Render Page heading. 3 | If `section` is true, render a h2 tag. If it's false (default) 4 | render a h1 tag with appropriate for a page header margin. 5 | #} 6 | {% fragment render(title, tagline, section = false) %} 7 | {%- set vspaceTop = ' mt-16' if not section else '' -%} 8 | {%- set tagName = 'h1' if not section else 'h2' -%} 9 | {%- set defCls = ' text-5xl md:text-6xl' if not section else ' text-4xl md:text-5xl' -%} 10 |
11 | <{{tagName}} class="mb-6 md:max-w-3xl mx-auto font-extrabold text-gray-800{{ defCls }}">{{ title }} 12 |
13 | {%- if tagline -%} 14 |

{{ tagline }}

15 | {%- endif -%} 16 |
17 | {% endfragment %} 18 | -------------------------------------------------------------------------------- /modules/theme/views/design-system/button/button.stories.js: -------------------------------------------------------------------------------- 1 | export default { 2 | name: 'atoms-buttons', 3 | category: 'Buttons', 4 | stories: [ 5 | { 6 | name: 'primary', 7 | label: 'Primary', 8 | // Apostrophe qualified template path, without the module prefix 9 | template: 'design-system/button/primary.njk', 10 | state: 'complete' 11 | }, 12 | { 13 | name: 'outlined', 14 | label: 'Outlined', 15 | // Apostrophe qualified template path, without the module prefix 16 | template: 'design-system/button/outlined.njk', 17 | state: 'complete' 18 | }, 19 | { 20 | name: 'icon', 21 | label: 'Icon', 22 | // Apostrophe qualified template path, without the module prefix 23 | template: 'design-system/button/icon.njk', 24 | state: 'complete' 25 | } 26 | ] 27 | }; 28 | -------------------------------------------------------------------------------- /modules/theme/views/design-system/logo/logo.njk: -------------------------------------------------------------------------------- 1 | {% extends dsPreview %} 2 | {% import "theme:logo.html" as logo %} 3 | 4 | {% block tagline %} 5 | Site Logo 6 | {% endblock %} 7 | 8 | {% block previewSimple %} 9 |
10 | {{ logo.render(alt = 'Company Logo') }} 11 |
{{ logo.render(alt = 'Company Logo') }}
12 |
13 | {% endblock %} 14 | 15 | {% block variants %} 16 |
17 | {% dscode 'njk' %} 18 | {% import "theme:logo.html" as logo %} 19 | 20 | {# default size #} 21 | {{ logo.render(alt = 'Company Logo') }} 22 | {# constrained by the container #} 23 |
{{ logo.render(alt = 'Company Logo') }}
24 | {% enddscode %} 25 |
26 | {% endblock %} 27 | -------------------------------------------------------------------------------- /modules/@apostrophecms/home-page/index.js: -------------------------------------------------------------------------------- 1 | export default { 2 | options: { 3 | label: 'app:homePage' 4 | }, 5 | fields: { 6 | add: { 7 | heading: { 8 | type: 'area', 9 | options: { 10 | widgets: { 11 | 'hero-full': {} 12 | }, 13 | max: 1 14 | } 15 | }, 16 | main: { 17 | type: 'area', 18 | options: { 19 | widgets: { 20 | product: {}, 21 | 'product-featured': {}, 22 | 'product-category': {}, 23 | cta: {}, 24 | promo: {}, 25 | blockquote: {} 26 | } 27 | } 28 | } 29 | }, 30 | group: { 31 | basics: { 32 | label: 'app:groupBasics', 33 | fields: [ 34 | 'title', 35 | 'heading', 36 | 'main' 37 | ] 38 | } 39 | } 40 | } 41 | }; 42 | -------------------------------------------------------------------------------- /modules/@apostrophecms/search/index.js: -------------------------------------------------------------------------------- 1 | export default { 2 | options: { 3 | perPage: 15, 4 | suggestions: { 5 | limit: 10 6 | } 7 | }, 8 | fields: { 9 | group: { 10 | seo: { 11 | label: 'app:searchSEO', 12 | fields: [ 13 | 'seoTitle', 14 | 'seoDescription', 15 | '_seoCanonical', 16 | 'seoRobots', 17 | 'openGraphTitle', 18 | 'openGraphDescription', 19 | 'openGraphType', 20 | '_openGraphImage' 21 | ], 22 | last: true 23 | } 24 | } 25 | }, 26 | 27 | extendMethods(self) { 28 | return { 29 | async indexPage(_super, req) { 30 | if (!req.query.q) { 31 | self.setTemplate(req, 'index'); 32 | return; 33 | } 34 | await _super(req); 35 | } 36 | }; 37 | } 38 | }; 39 | -------------------------------------------------------------------------------- /docs/developer/resources.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Resources 3 | titleTemplate: Developer 4 | --- 5 | 6 | # {{ $frontmatter.title }} 7 | 8 | - [The Apostrophe CMS official website](https://apostrophecms.org/) 9 | - [The Apostrophe CMS official documentation](https://v3.docs.apostrophecms.org/) 10 | - [Heroicons](https://heroicons.com/) - a large set of free and opens source SVG icons 11 | - [Tailwind CSS](https://tailwindcss.com/) - a utility-first CSS framework for rapidly building custom designs 12 | - [UI Colors](https://uicolors.app) - a color palette generator for Tailwind CSS 13 | - [Figma Community - Apostrophe CMS E-commerce Starter Kit](https://www.figma.com/community/file/1250089202074615969) - Design System, component library and page prototypes used to develop the starter kit 14 | - [Corllete Design System](https://github.com/corllete/apos-ds) - an open source module used to develop the starter kit UI in isolation 15 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: Docs 2 | on: 3 | workflow_dispatch: {} 4 | push: 5 | branches: 6 | - main 7 | jobs: 8 | deploy: 9 | runs-on: ubuntu-latest 10 | permissions: 11 | pages: write 12 | id-token: write 13 | environment: 14 | name: github-pages 15 | url: ${{ steps.deployment.outputs.page_url }} 16 | steps: 17 | - uses: actions/checkout@v4 18 | with: 19 | fetch-depth: 0 20 | - uses: actions/setup-node@v4 21 | with: 22 | node-version: 22 23 | # cache: npm 24 | - run: cd docs && npm i && cd .. 25 | - name: Build 26 | run: cd docs && npm run build && cd .. 27 | - uses: actions/configure-pages@v5 28 | - uses: actions/upload-pages-artifact@v3 29 | with: 30 | path: docs/.vitepress/dist 31 | - name: Deploy 32 | id: deployment 33 | uses: actions/deploy-pages@v4 34 | -------------------------------------------------------------------------------- /deployment/stop: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | export NODE_ENV=production 4 | 5 | # Shut the site down, for instance by tweaking a .htaccess file to display 6 | # a 'please wait' notice, or stopping a node server 7 | 8 | if [ ! -f "app.js" ]; then 9 | echo "I don't see app.js in the current directory." 10 | exit 1 11 | fi 12 | 13 | # Stop the node app via 'forever'. You'll get a harmless warning if the app 14 | # was not already running. Use `pwd` to make sure we have a full path, 15 | # forever is otherwise easily confused and will stop every server with 16 | # the same filename 17 | forever stop `pwd`/app.js && echo "Site stopped" 18 | 19 | # Stop the app without 'forever'. We recommend using 'forever' for node apps, 20 | # but this may be your best bet for non-node apps 21 | # 22 | # if [ -f "data/pid" ]; then 23 | # kill `cat data/pid` 24 | # rm data/pid 25 | # echo "Site stopped" 26 | # else 27 | # echo "Site was not running" 28 | # fi 29 | 30 | -------------------------------------------------------------------------------- /modules/theme/ui/src/components/mobile-nav.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Toggle mobile navigation menu. 3 | */ 4 | export function toggle(element) { 5 | /** @type {HTMLElement | undefined} */ 6 | const el = element; 7 | if (!el) { 8 | return; 9 | } 10 | const popup = document.querySelector(el.dataset.target); 11 | const nav = document.querySelector(`#${el.getAttribute('aria-controls')}`); 12 | const active = el.getAttribute('aria-expanded') === 'true'; 13 | if (!popup || !nav) { 14 | return; 15 | } 16 | if (!active) { 17 | el.setAttribute('aria-expanded', 'true'); 18 | nav.setAttribute('aria-hidden', 'false'); 19 | popup.classList.remove('pointer-events-none'); 20 | popup.style = 'transform: translate(0, 0); opacity: 1;'; 21 | return; 22 | } 23 | el.setAttribute('aria-expanded', 'false'); 24 | nav.setAttribute('aria-hidden', 'true'); 25 | popup.style = ''; 26 | popup.classList.add('pointer-events-none'); 27 | } 28 | -------------------------------------------------------------------------------- /modules/product-page/views/index.html: -------------------------------------------------------------------------------- 1 | {% extends 'layout.html' %} 2 | {% import 'theme:helpers.html' as helpers %} 3 | {% import "theme:heading.html" as heading %} 4 | {% import "theme:card.html" as cards %} 5 | {% import '@apostrophecms/pager:macros.html' as pager with context %} 6 | 7 | {% block main %} 8 | {% rendercall helpers.container() %} 9 | {% render heading.render(data.page.title, data.page.tagline) %} 10 | 11 |
12 | {% if not data.pieces | length %} 13 |

{{ __t('app:productNoResults') }}

14 | {% else %} 15 | {% render cards.productList(data.pieces) %} 16 | {% endif %} 17 | 18 | {{ pager.render({ 19 | page: data.currentPage, 20 | total: data.totalPages, 21 | class: 'app-pager' 22 | }, data.url) }} 23 |
24 | {% area data.page, 'main' %} 25 | {% endrendercall %} 26 | {% endblock %} 27 | -------------------------------------------------------------------------------- /modules/theme/views/logo.html: -------------------------------------------------------------------------------- 1 | {# 2 | Render the logo component. The attachment will be used if present, 3 | otherwise the default "demo" logo will be used. The cls argument is an optional 4 | CSS classes variable. 5 | 6 | The alt argument is recommended (a11y), but optional. If not provided, 7 | the attachment's _alt property will be used. 8 | #} 9 | {% macro render(attachment, alt = '', cls = '') %} 10 | {%- set width = 189 -%} 11 | {%- set height = 40 -%} 12 | {%- set url = apos.asset.url('/modules/theme/logo.svg') -%} 13 | {%- if attachment -%} 14 | {%- set width = apos.attachment.getWidth(attachment) -%} 15 | {%- set height = apos.attachment.getHeight(attachment) -%} 16 | {%- set url = apos.attachment.url(attachment, { size: 'one-third' }) -%} 17 | {%- endif -%} 18 | {# {%- endif -%} #} 19 | {{ alt or attachment._alt }} 20 | {% endmacro %} 21 | -------------------------------------------------------------------------------- /deployment/settings: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Settings shared by all targets (staging, production, etc). Usually the 4 | # shortname of the project (which is also the hostname for the frontend 5 | # proxy server used for staging sites) and the directory name. For our 6 | # web apps that use sc-proxy we make sure each is a subdirectory 7 | # of /opt/stagecoach/apps 8 | 9 | # Should match the repo name = short name = everything name! 10 | PROJECT=a3-boilerplate 11 | 12 | DIR=/opt/stagecoach/apps/$PROJECT 13 | 14 | # Adjust the PATH environment variable on the remote host. Here's an example 15 | # for deploying to MacPorts 16 | #ADJUST_PATH='export PATH=/opt/local/bin:$PATH' 17 | 18 | # ... But you probably won't need to on real servers. I just find it handy for 19 | # testing parts of stagecoach locally on a Mac. : is an acceptable "no-op" (do-nothing) statement 20 | ADJUST_PATH=':' 21 | 22 | # ssh port. Sensible people leave this set to 22 but it's common to do the 23 | # "security by obscurity" thing alas 24 | SSH_PORT=22 25 | -------------------------------------------------------------------------------- /modules/theme/views/design-system/header/header.json: -------------------------------------------------------------------------------- 1 | { 2 | "headerNav": [ 3 | { 4 | "label": "Home", 5 | "urlType": "custom", 6 | "url": "#home" 7 | }, 8 | { 9 | "label": "Men", 10 | "urlType": "page", 11 | "_page": [ 12 | { 13 | "_url": "#men" 14 | } 15 | ], 16 | "includeActive": "organisms-header" 17 | }, 18 | { 19 | "label": "Women", 20 | "urlType": "custom", 21 | "url": "#women" 22 | }, 23 | { 24 | "label": "Accessories", 25 | "urlType": "custom", 26 | "url": "#accessories" 27 | } 28 | ], 29 | "headerCtaIcon": { 30 | "label": "Custom Icon CTA", 31 | "urlType": "custom", 32 | "url": "#custom-icon-cta", 33 | "icon": "user-circle" 34 | }, 35 | "headerCtaButton": { 36 | "label": "Custom CTA", 37 | "urlType": "custom", 38 | "url": "#custom-cta" 39 | }, 40 | "searchUrl": { 41 | "urlType": "custom", 42 | "url": "#search" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /test/lib/tools.js: -------------------------------------------------------------------------------- 1 | import chai from 'chai'; 2 | 3 | // Add Chai plugins here. 4 | import spies from 'chai-spies'; 5 | 6 | chai.use(spies); 7 | 8 | const should = chai.should(); 9 | const expect = chai.expect; 10 | 11 | // A very simple single module mock processing. 12 | // `aposModule` is the original module object (e.g. `import('modules/theme/index.js')`). 13 | // `self` is an optional mock to be merged with the module. 14 | function processSelf(aposModule, self = {}) { 15 | const _self = { 16 | apos: {}, 17 | modules: [], 18 | ...aposModule, 19 | ...self, 20 | ...(aposModule.methods?.(aposModule) || {}), 21 | ...(self?.methods?.(aposModule) || {}) 22 | }; 23 | const methods = aposModule.methods?.(_self) || {}; 24 | for (const [ name, fn ] of Object.entries(methods)) { 25 | _self[name] = fn; 26 | } 27 | _self.helpers = aposModule.helpers?.(_self) || {}; 28 | 29 | return _self; 30 | } 31 | 32 | export default { 33 | should, 34 | expect, 35 | chai, 36 | processSelf 37 | }; 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2023 Corllete ltd 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /design-system-setup/@corllete/apos-ds-page-type/views/layout/preview.html: -------------------------------------------------------------------------------- 1 | {% extends "@corllete/apos-ds-page-type:layout/previewBase.html" %} 2 | {# Extend point for applications #} 3 | {# 4 | Allows fine tuning of the design system Story Preview. 5 | Fonts, CSS and JS can be added to the preview. Additional 6 | template includes (e.g. sprites) can be added in the afterAposScripts block. 7 | #} 8 | 9 | {# Before any css - safe to completely overwrite it #} 10 | {% block previewFonts %} 11 | {# Required for the preview icon actions #} 12 | 13 | {% endblock %} 14 | 15 | {# Add any head (css) after the preview styles were included #} 16 | {% block previewCss %} 17 | {# Add any custom CSS to modify the DS preview #} 18 | {% endblock %} 19 | 20 | {# Add any custom js after the preview js source #} 21 | {% block previewJs %}{% endblock %} 22 | 23 | {# Should be the same as your application layout #} 24 | {% block afterAposScripts %} 25 | {{ super() }} 26 | {% include "theme:icons.svg.html" %} 27 | {% endblock %} 28 | -------------------------------------------------------------------------------- /modules/product-category-page/views/show.html: -------------------------------------------------------------------------------- 1 | {% extends 'layout.html' %} 2 | {% import 'theme:helpers.html' as helpers %} 3 | {% import "theme:heading.html" as heading %} 4 | {% import "theme:card.html" as cards %} 5 | {% import '@apostrophecms/pager:macros.html' as pager with context %} 6 | 7 | {% block main %} 8 | {% rendercall helpers.container() %} 9 | {% if data.piece.headerType === 'widget' %} 10 | {% area data.piece, 'heading' %} 11 | {% else %} 12 | {% render heading.render(data.piece.title, data.piece.tagline) %} 13 | {% endif %} 14 | 15 |
16 | {% if not data.pieces | length %} 17 |

{{ __t('app:productNoResults') }}

18 | {% else %} 19 | {% render cards.productList(data.pieces) %} 20 | {% endif %} 21 | 22 | {{ pager.render({ 23 | page: data.currentPage, 24 | total: data.totalPages, 25 | class: 'app-pager' 26 | }, data.url) }} 27 |
28 | {% area data.piece, 'main' %} 29 | {% endrendercall %} 30 | {% endblock %} 31 | -------------------------------------------------------------------------------- /docs/user/custom-pages.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Custom Pages 3 | titleTemplate: User Guide 4 | --- 5 | 6 | # {{ $frontmatter.title }} 7 | 8 | The Apostrophe CMS E-commerce Starter Kit comes with a "Default" page type, that can be chosen for creating Custom Pages. You can create a custom page via the Page Manager (open from "Pages" administration menu item) and the "New Page" button. 9 | 10 | ## Choose a Header Type 11 | 12 | You can choose the heading type of every custom page via the "Header Type" field. By default, a "Title" will be set, resulting in the default Title/Tagline heading. You can choose "Hero" and you'll see a new area for choosing a Hero widget. 13 | 14 | ## Build Your Page 15 | 16 | You can build your page by adding widgets. You can mix and match any of the available widgets to achieve the desired result - being it a generic informational or a landing product page. 17 | 18 | ### Generic Page 19 | 20 | ![Generic Page](../images/custom-page-about-us.png) 21 | 22 | ### Landing Product Page 23 | 24 | ![Landing Product Page](../images/custom-page-promo.png) 25 | 26 | You can learn more about widgets in the [Widgets](./widgets.md) section. 27 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | # https://vitepress.dev/reference/default-theme-home-page 3 | layout: home 4 | titleTemplate: false 5 | 6 | hero: 7 | name: "Apostrophe CMS" 8 | text: "Starter Kit" 9 | image: 10 | light: /images/home-on-light.png 11 | dark: /images/home-on-dark.png 12 | alt: Apostrophe CMS E-commerce Starter Kit 13 | tagline: E-commerce Starter Kit for Apostrophe CMS built with Tailwind CSS. 14 | actions: 15 | # TODO - add demo when ready 16 | # - theme: brand 17 | # text: Demo 18 | # link: \#demo 19 | - theme: alt 20 | text: User Guide 21 | link: /user/ 22 | - theme: alt 23 | text: Developer Guide 24 | link: /developer/ 25 | 26 | features: 27 | - title: Solid Foundation 28 | details: Get your e-commerce website up and running in no time on top of Apostrophe CMS best practices. 29 | - title: Optimized 30 | details: Responsive, mobile image optimization, SEO and Open Graph. 31 | - title: Internationalization 32 | details: Add and manage your content in multiple languages. 33 | - title: Tailwind CSS 34 | details: Built with Tailwind CSS, easy to customize and extend. 35 | --- 36 | 37 | -------------------------------------------------------------------------------- /modules/theme/views/design-system/heading/heading.njk: -------------------------------------------------------------------------------- 1 | {% extends dsPreview %} 2 | {% import "theme:heading.html" as heading %} 3 | {% import "theme:helpers.html" as helpers %} 4 | 5 | {% set tagline = 'Pellentesque auctor neque nec urna. Quisque rutrum. Integer tincidunt. Praesent ut ligula non mi varius sagittis.' %} 6 | 7 | {% block tagline %} 8 | Page/Section Heading 9 | {% endblock %} 10 | 11 | {% block preview %} 12 | {% rendercall helpers.container() %} 13 | {% render heading.render('Page title', tagline) %} 14 | {% endrendercall %} 15 | {% endblock %} 16 | 17 | {% block variants %} 18 | {# container #} 19 | {% rendercall helpers.container('mb-10') %} 20 | {% dscode 'njk' %} 21 | {% import "theme:heading.html" as heading %} 22 | 23 | {% render heading.render('Page title', tagline) %} 24 | {% enddscode %} 25 | 26 |
27 | {% render heading.render('Section title', tagline, section = true) %} 28 | {% dscode 'njk' %} 29 | {% import "theme:heading.html" as heading %} 30 | 31 | {% render heading.render('Section title', tagline, section = true) %} 32 | {% enddscode %} 33 |
34 | {% endrendercall %} 35 | {% endblock %} 36 | -------------------------------------------------------------------------------- /modules/@apostrophecms/page/index.js: -------------------------------------------------------------------------------- 1 | const park = []; 2 | // Design system: park the only when in local environment. 3 | if (process.env.NODE_ENV !== 'production') { 4 | // Enable Corllete design system if installed. 5 | // park.push({ 6 | // parkedId: 'design-system', 7 | // type: '@corllete/apos-ds-page-type', 8 | // _defaults: { 9 | // slug: '/ds', 10 | // title: 'Design System' 11 | // } 12 | // }); 13 | } 14 | 15 | export default { 16 | options: { 17 | types: [ 18 | { 19 | name: 'default-page', 20 | label: 'app:default' 21 | }, 22 | { 23 | name: 'product-page', 24 | label: 'app:productLabel' 25 | }, 26 | { 27 | name: 'product-category-page', 28 | label: 'app:productCategoryLabel' 29 | }, 30 | { 31 | name: '@apostrophecms/home-page', 32 | label: 'app:home' 33 | } 34 | ], 35 | park: [ 36 | ...park, 37 | { 38 | parkedId: 'core-search', 39 | type: '@apostrophecms/search', 40 | slug: '/search', 41 | _defaults: { 42 | title: 'Search' 43 | } 44 | } 45 | ] 46 | } 47 | }; 48 | -------------------------------------------------------------------------------- /.devcontainer/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | 3 | services: 4 | app: 5 | build: 6 | context: . 7 | dockerfile: Dockerfile 8 | volumes: 9 | - ../..:/workspaces:cached 10 | 11 | # Overrides default command so things don't shut down after the process ends. 12 | command: sleep infinity 13 | 14 | # Runs app on the same network as the database container, allows "forwardPorts" in devcontainer.json function. 15 | network_mode: service:db 16 | # Use "forwardPorts" in **devcontainer.json** to forward an app port locally. 17 | # (Adding the "ports" property to this file will not forward from a Codespace.) 18 | 19 | db: 20 | image: mongo:5 21 | restart: unless-stopped 22 | volumes: 23 | - mongodb-data:/data/db 24 | # Uncomment to change startup options 25 | # environment: 26 | # MONGO_INITDB_ROOT_USERNAME: root 27 | # MONGO_INITDB_ROOT_PASSWORD: example 28 | # MONGO_INITDB_DATABASE: your-database-here 29 | 30 | # Add "forwardPorts": ["27017"] to **devcontainer.json** to forward MongoDB locally. 31 | # (Adding the "ports" property to this file will not forward from a Codespace.) 32 | 33 | volumes: 34 | mongodb-data: 35 | -------------------------------------------------------------------------------- /modules/product-category-widget/views/widget.html: -------------------------------------------------------------------------------- 1 | {%- import 'theme:button.html' as buttons -%} 2 | {%- import 'theme:card.html' as card -%} 3 | {%- import 'theme:heading.html' as heading -%} 4 | 5 | {%- if data.widget._categories | length -%} 6 |
7 | {%- if data.widget.title -%} 8 |
9 | {% render heading.render( 10 | data.widget.title, 11 | data.widget.tagline, 12 | section = true 13 | ) %} 14 |
15 | {%- endif -%} 16 | {%- render card.categoryList(data.widget._categories) -%} 17 | {%- set cta = apos.theme.navItems([data.widget.viewMore]) | first -%} 18 | {%- if cta.label and cta.url -%} 19 |
20 | {%- if cta.isPrimary -%} 21 | {{ buttons.primary(cta.label, href = cta.url, { large: true }) }} 22 | {%- else -%} 23 | {{ buttons.outlined(cta.label, href = cta.url, { large: true }) }} 24 | {%- endif -%} 25 |
26 | {%- endif -%} 27 |
28 | {%- elif data.widget._edit -%} 29 | {# Editor only message #} 30 |

{{ __t('app:noResults') }}

31 | {%- endif -%} 32 | -------------------------------------------------------------------------------- /modules/product-featured-widget/views/widget.html: -------------------------------------------------------------------------------- 1 | {%- import 'theme:button.html' as buttons -%} 2 | {%- import 'theme:card.html' as card -%} 3 | {%- import 'theme:heading.html' as heading -%} 4 | 5 | {%- if data.widget._products | length -%} 6 |
7 | {%- if data.widget.title -%} 8 |
9 | {% render heading.render( 10 | data.widget.title, 11 | data.widget.tagline, 12 | section = true 13 | ) %} 14 |
15 | {%- endif -%} 16 | {%- render card.productListMasonry(data.widget._products) -%} 17 | {%- set cta = apos.theme.navItems([data.widget.viewMore]) | first -%} 18 | {%- if cta.label and cta.url -%} 19 |
20 | {%- if cta.isPrimary -%} 21 | {{ buttons.primary(cta.label, href = cta.url, { large: true }) }} 22 | {%- else -%} 23 | {{ buttons.outlined(cta.label, href = cta.url, { large: true }) }} 24 | {%- endif -%} 25 |
26 | {%- endif -%} 27 |
28 | {%- elif data.widget._edit -%} 29 | {# Editor only message #} 30 |

{{ __t('app:noResults') }}

31 | {%- endif -%} 32 | -------------------------------------------------------------------------------- /modules/theme/icons/LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Refactoring UI Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | https://github.com/tailwindlabs/heroicons 24 | -------------------------------------------------------------------------------- /modules/theme/views/design-system/card/card.stories.js: -------------------------------------------------------------------------------- 1 | export default { 2 | name: 'molecules-card', 3 | category: 'Cards', 4 | data: 'card.json', 5 | stories: [ 6 | { 7 | name: 'product-card', 8 | label: 'Product', 9 | template: 'design-system/card/product.njk', 10 | state: 'complete' 11 | }, 12 | { 13 | name: 'category-card', 14 | label: 'Product Category', 15 | template: 'design-system/card/category.njk', 16 | state: 'complete' 17 | }, 18 | { 19 | name: 'product-list', 20 | label: 'Product List', 21 | template: 'design-system/card/product-list.njk', 22 | state: 'complete' 23 | }, 24 | { 25 | name: 'category-list', 26 | label: 'Category List', 27 | template: 'design-system/card/category-list.njk', 28 | state: 'complete' 29 | }, 30 | { 31 | name: 'product-masonry', 32 | label: 'Product Masonry list', 33 | template: 'design-system/card/product-masonry.njk', 34 | state: 'complete' 35 | }, 36 | { 37 | name: 'blockquote', 38 | label: 'Customer Review', 39 | template: 'design-system/card/blockquote.njk', 40 | state: 'complete' 41 | } 42 | ] 43 | }; 44 | -------------------------------------------------------------------------------- /modules/theme/views/design-system/footer/footer.json: -------------------------------------------------------------------------------- 1 | { 2 | "footerNav": [ 3 | { 4 | "label": "About Us", 5 | "urlType": "custom", 6 | "url": "#link1" 7 | }, 8 | { 9 | "label": "Products", 10 | "urlType": "page", 11 | "_page": [ 12 | { 13 | "_url": "#link2" 14 | } 15 | ] 16 | }, 17 | { 18 | "label": "Privacy Policy", 19 | "urlType": "custom", 20 | "url": "#link3" 21 | }, 22 | { 23 | "label": "Contact Us", 24 | "urlType": "custom", 25 | "url": "#link4" 26 | } 27 | ], 28 | "footerSocial": [ 29 | { 30 | "label": "Facebook", 31 | "icon": "social-facebook", 32 | "url": "#facebook" 33 | }, 34 | { 35 | "label": "Instagram", 36 | "icon": "social-instagram", 37 | "url": "#instagram" 38 | }, 39 | { 40 | "label": "Twitter", 41 | "icon": "social-twitter", 42 | "url": "#twitter" 43 | }, 44 | { 45 | "label": "Youtube", 46 | "icon": "social-youtube", 47 | "url": "#youtube" 48 | }, 49 | { 50 | "label": "LinkedIn", 51 | "icon": "social-linkedin", 52 | "url": "#linkedin" 53 | } 54 | ], 55 | "brandName": "Company Name" 56 | } 57 | -------------------------------------------------------------------------------- /modules/product-widget/views/widget.html: -------------------------------------------------------------------------------- 1 | {%- import 'theme:button.html' as buttons -%} 2 | {%- import 'theme:card.html' as card -%} 3 | {%- import 'theme:heading.html' as heading -%} 4 | 5 | {%- if data.widget._products | length -%} 6 |
7 | {%- if data.widget.title -%} 8 |
9 | {% render heading.render( 10 | data.widget.title, 11 | data.widget.tagline, 12 | section = true 13 | ) %} 14 |
15 | {%- endif -%} 16 | {%- render card.productList( 17 | data.widget._products, 18 | condensed = data.widget.condensed 19 | ) -%} 20 | {%- set cta = apos.theme.navItems([data.widget.viewMore]) | first -%} 21 | {%- if cta.label and cta.url -%} 22 |
23 | {%- if cta.isPrimary -%} 24 | {{ buttons.primary(cta.label, href = cta.url, { large: true }) }} 25 | {%- else -%} 26 | {{ buttons.outlined(cta.label, href = cta.url, { large: true }) }} 27 | {%- endif -%} 28 |
29 | {%- endif -%} 30 |
31 | {%- elif data.widget._edit -%} 32 | {# Editor only message #} 33 |

{{ __t('app:noResults') }}

34 | {%- endif -%} 35 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | A `vitepress` app to deliver the documentation for the Apostrophe CMS E-commerce Starter Kit. 2 | 3 | Install: 4 | 5 | ```bash 6 | npm install 7 | ``` 8 | 9 | Optionally update the dependencies: 10 | 11 | ```bash 12 | npm update 13 | ``` 14 | 15 | ## Development 16 | 17 | ```bash 18 | npm run dev 19 | ``` 20 | 21 | ## Build 22 | 23 | ```bash 24 | npm run build 25 | ``` 26 | 27 | ## Preview 28 | 29 | ```bash 30 | npm run preview 31 | ``` 32 | 33 | ## Make it yours 34 | 35 | Open `.vitepress/config.js` and adapt it for your needs. Don't forget to update the repository configuration: 36 | 37 | ```js 38 | // .vitepress/config.js 39 | 40 | // Settings 41 | const project = `ecommerce-starter-kit`; 42 | const repo = `apostrophecms/${project}`; 43 | // Settings end 44 | ``` 45 | Learn more about configuring and customizing VitePress at [https://vitepress.vuejs.org/](https://vitepress.vuejs.org/). 46 | 47 | ## Deploy 48 | 49 | A GitHub Action workflow is configured to auto build & deploy the documentation to GitHub Pages. You can find the workflow configuration in `.github/workflows/docs.yml`. 50 | 51 | Configure GH Pages and find the URL to your documentation in the repository settings under **Pages**. 52 | 53 | ## Remove 54 | 55 | You can completely remove the documentation from your project by deleting: 56 | - `docs/` 57 | - `.github/workflows/docs.yml` 58 | -------------------------------------------------------------------------------- /docs/user/search-and-seo.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Search, SEO and Open Graph 3 | titleTemplate: User Guide 4 | --- 5 | 6 | # {{ $frontmatter.title }} 7 | 8 | The Apostrophe CMS E-commerce Starter Kit comes with a set of features, that will help you build both search engine optimized and social sharing friendly website. Furthermore, it offers a simple but powerful in-site search functionality. 9 | 10 | ## SEO and Open Graph 11 | 12 | Every page and piece, that is visible and have its own URL on your website, has dedicated to SEO and Open Graph (helping the shared content from your website to social platforms to be well offered). You can find them in the respective "SEO" and "Open Graph" tabs of the Editor Modal. 13 | 14 | ![SEO and Open Graph](../images/seo-og.png) 15 | 16 | ## Search 17 | 18 | If you want to show the Apostrophe CMS core search feature (button) in the header of your site, you need to set it from the site configuration - in the "Search URL" field set choose "Page" type, and select the "Search" page from the relation field or modal. 19 | 20 | ::: info 21 | In order to have well looking search results, you need to have filled either the "SEO - Description" field of every piece/page or the "Tagline" field. The search page results will use one or the other, where the first in the order mentioned above will be shown. 22 | ::: 23 | 24 | ![Search URL](../images/search.png) 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /scripts/sync-down: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | TARGET="$1" 4 | if [ -z "$TARGET" ]; then 5 | echo "Usage: ./scripts/sync-down production" 6 | echo "(or as appropriate)" 7 | exit 1 8 | fi 9 | 10 | source deployment/settings || exit 1 11 | source "deployment/settings.$TARGET" || exit 1 12 | 13 | #Enter the Mongo DB name (should be same locally and remotely). 14 | dbName=$PROJECT 15 | 16 | #Enter the Project name (should be what you called it for stagecoach). 17 | projectName=$PROJECT 18 | 19 | #Enter the SSH username/url for the remote server. 20 | remoteSSH="-p $SSH_PORT $USER@$SERVER" 21 | rsyncTransport="ssh -p $SSH_PORT" 22 | rsyncDestination="$USER@$SERVER" 23 | 24 | echo "Syncing MongoDB" 25 | ssh $remoteSSH mongodump -d $dbName -o /tmp/mongodump.$dbName && 26 | rsync -av -e "$rsyncTransport" $rsyncDestination:/tmp/mongodump.$dbName/ /tmp/mongodump.$dbName && 27 | ssh $remoteSSH rm -rf /tmp/mongodump.$dbName && 28 | # noIndexRestore increases compatibility between 3.x and 2.x, 29 | # and Apostrophe will recreate the indexes correctly at startup 30 | mongorestore --noIndexRestore --drop -d $dbName /tmp/mongodump.$dbName/$dbName && 31 | echo "Syncing Files" && 32 | rsync -av --delete -e "$rsyncTransport" $rsyncDestination:/opt/stagecoach/apps/$projectName/uploads/ ./public/uploads && 33 | echo "Synced down from $TARGET" 34 | echo "YOU MUST RESTART THE SITE LOCALLY TO REBUILD THE MONGODB INDEXES." 35 | -------------------------------------------------------------------------------- /modules/theme/views/icon.html: -------------------------------------------------------------------------------- 1 | {# 2 | # In order to control the color, set the "color" CSS property on the parent element. 3 | # 4 | # Arguments: 5 | # - name: the name without the "theme-svg-icon-" prefix, e.g. 'heart' 6 | # - size: (string) sm (16px) | md (24px, default) | l (32px) | xl (40px) 7 | # or any tailwind size class e.g. "w-[20px] h-[20px]" 8 | # - cls: (string) additional CSS class names to be added to the svg tag 9 | #} 10 | {% macro svg(name, size, cls = '') -%} 11 | {%- if size == 'sm' -%} 12 | {# 16px #} 13 | {% set sizeCls = "w-4 h-4" %} 14 | {%- elif size == 'md' -%} 15 | {# 24px #} 16 | {% set sizeCls = "w-6 h-6" %} 17 | {%- elif size == 'l' -%} 18 | {# 32px #} 19 | {% set sizeCls = "w-8 h-8" %} 20 | {%- elif size == 'xl' -%} 21 | {# 40px #} 22 | {% set sizeCls = "w-10 h-10" %} 23 | {%- elif not size -%} 24 | {# default #} 25 | {% set sizeCls = "w-6 h-6" %} 26 | {%- else -%} 27 | {# custom size, e.g. "w-[20px] h-[20px]" 28 | WARNING: we don't build it dynamically 29 | so that tailwind can find it when treeshaking from the caller code #} 30 | {% set sizeCls = size %} 31 | {%- endif -%} 32 | 33 | {%- set name = 'theme-svg-icon-' + name -%} 34 | {%- set _cls = '' -%} 35 | {%- if cls -%} 36 | {%- set _cls = ' ' + cls -%} 37 | {%- endif -%} 38 | 39 | 42 | {%- endmacro %} 43 | -------------------------------------------------------------------------------- /modules/product-page/index.js: -------------------------------------------------------------------------------- 1 | export default { 2 | extend: '@apostrophecms/piece-page-type', 3 | options: { 4 | label: 'app:productPageLabel', 5 | alias: 'productPage', 6 | perPage: 15 7 | }, 8 | fields: { 9 | add: { 10 | tagline: { 11 | type: 'string', 12 | textarea: true, 13 | label: 'app:tagline', 14 | max: 200 15 | }, 16 | main: { 17 | type: 'area', 18 | label: 'app:content', 19 | options: { 20 | widgets: { 21 | product: {}, 22 | 'product-featured': {}, 23 | 'product-category': {}, 24 | cta: {}, 25 | promo: {}, 26 | blockquote: {} 27 | } 28 | } 29 | } 30 | }, 31 | group: { 32 | basics: { 33 | label: 'app:groupBasics', 34 | fields: [ 'tagline', 'main' ] 35 | } 36 | } 37 | }, 38 | methods(self) { 39 | return { 40 | async beforeShow(req) { 41 | if (!req.data.piece.tagsIds.length) { 42 | return; 43 | } 44 | req.data.relatedProducts = await self.apos.product.find(req, { 45 | _id: { 46 | $ne: req.data.piece._id 47 | } 48 | }) 49 | ._tags(req.data.piece.tagsIds) 50 | .sort({ 51 | publishDate: -1, 52 | updatedAt: -1 53 | }) 54 | .limit(4) 55 | .toArray(); 56 | } 57 | }; 58 | } 59 | }; 60 | -------------------------------------------------------------------------------- /modules/theme/views/design-system/cta-widget/image.njk: -------------------------------------------------------------------------------- 1 | {% extends dsPreview %} 2 | {% import "theme:cta-widget.html" as cta %} 3 | {% import "theme:helpers.html" as helpers %} 4 | 5 | {% set storyData = apos.dsp.configData('molecules-cta-widget').image %} 6 | 7 | {% block tagline %} 8 | cta Image 9 | {% endblock %} 10 | 11 | {% block preview %} 12 | {% rendercall helpers.container() %} 13 | 14 | {% rendercall cta.image(storyData) %} 15 |

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut magna, scelerisque vitae augue et, cursus placerat lorem. In nisi lacus, eleifend tincidunt quam et, consequat semper.

16 | {% endrendercall %} 17 | 18 | {% endrendercall %} 19 | {% endblock %} 20 | 21 | {% block variants %} 22 | {# container #} 23 | {% rendercall helpers.container() %} 24 | 25 | {# Code grid - 2 cols #} 26 |
27 | {% dscode 'njk' %} 28 | {% import "theme:cta-widget.html" as cta %} 29 | {# image cta-widget #} 30 | {% rendercall cta.image(storyData) %} 31 |

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut magna, scelerisque vitae augue et, cursus placerat lorem. In nisi lacus, eleifend tincidunt quam et, consequat semper.

32 | {% endrendercall %} 33 | {% enddscode %} 34 | 35 | {% dscode 'json', label = 'Data', parse = true %}{{ storyData | jsonPretty }}{% enddscode %} 36 |
37 | 38 | {# ensure the story finishes with a nice spacing #} 39 |
40 | 41 | {% endrendercall %} 42 | {% endblock %} 43 | -------------------------------------------------------------------------------- /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM mcr.microsoft.com/devcontainers/javascript-node:0-18 2 | 3 | # Install MongoDB command line tools - though mongo-database-tools not available on arm64 4 | ARG MONGO_TOOLS_VERSION=6.0 5 | RUN . /etc/os-release \ 6 | && curl -sSL "https://www.mongodb.org/static/pgp/server-${MONGO_TOOLS_VERSION}.asc" | gpg --dearmor > /usr/share/keyrings/mongodb-archive-keyring.gpg \ 7 | && echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/mongodb-archive-keyring.gpg] http://repo.mongodb.org/apt/debian ${VERSION_CODENAME}/mongodb-org/${MONGO_TOOLS_VERSION} main" | tee /etc/apt/sources.list.d/mongodb-org-${MONGO_TOOLS_VERSION}.list \ 8 | && apt-get update && export DEBIAN_FRONTEND=noninteractive \ 9 | && apt-get install -y mongodb-mongosh \ 10 | && if [ "$(dpkg --print-architecture)" = "amd64" ]; then apt-get install -y mongodb-database-tools; fi \ 11 | && apt-get clean -y && rm -rf /var/lib/apt/lists/* 12 | 13 | # [Optional] Uncomment this section to install additional OS packages. 14 | # RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ 15 | # && apt-get -y install --no-install-recommends 16 | 17 | # [Optional] Uncomment if you want to install an additional version of node using nvm 18 | # ARG EXTRA_NODE_VERSION=10 19 | # RUN su node -c "source /usr/local/share/nvm/nvm.sh && nvm install ${EXTRA_NODE_VERSION}" 20 | 21 | # [Optional] Uncomment if you want to install more global node modules 22 | # RUN su node -c "npm install -g " 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /modules/theme/views/design-system/card/category-list.njk: -------------------------------------------------------------------------------- 1 | {% extends dsPreview %} 2 | {% import "theme:card.html" as cards %} 3 | {% import "theme:helpers.html" as helpers %} 4 | 5 | {% set item = apos.dsp.configData('molecules-card').category %} 6 | {%- set item2 = item | merge ({title: 'Category2 Card Title'}) -%} 7 | {%- set item3 = item | merge ({title: 'Category3 Card Title', productsAvailable:''}) -%} 8 | {%- set item4 = item | merge ({title: 'Category4 Card Title', productsAvailable:null}) -%} 9 | {%- set item5 = item | merge ({title: 'Category5 Card Title'}) -%} 10 | {%- set item6 = item | merge ({title: 'Category6 Card Title'}) -%} 11 | {%- set item7 = item | merge ({title: 'Category7 Card Title'}) -%} 12 | {%- set items = [item, item2, item3, item4, item5, item6, item7]-%} 13 | 14 | {% block tagline %} 15 | List of Category cards 16 | {% endblock %} 17 | 18 | {% block preview %} 19 | {% rendercall helpers.container() %} 20 | {% render cards.categoryList(items) %} 21 | {% endrendercall %} 22 | {% endblock %} 23 | 24 | {% block variants %} 25 | {# container #} 26 | {% rendercall helpers.container() %} 27 | 28 | {# Code grid - 2 cols #} 29 |
30 | {% dscode 'njk' %} 31 | {% import "theme:card.html" as cards %} 32 | {% render cards.categoryList(items) %} 33 | {% enddscode %} 34 | 35 | {% dscode 'json', label = 'Data', parse = true %}{{ items | jsonPretty }}{% enddscode %} 36 |
37 | 38 | {# ensure the story finishes with a nice spacing #} 39 |
40 | 41 | {% endrendercall %} 42 | {% endblock %} 43 | -------------------------------------------------------------------------------- /modules/theme/views/design-system/card/product-masonry.njk: -------------------------------------------------------------------------------- 1 | {% extends dsPreview %} 2 | {% import "theme:card.html" as cards %} 3 | {% import "theme:helpers.html" as helpers %} 4 | 5 | {% set item = apos.dsp.configData('molecules-card').product %} 6 | {%- set item2 = item | merge ({title: 'Product2 Card Title'}) -%} 7 | {%- set item3 = item | merge ({title: 'Product3 Card Title', buyNowUrl:''}) -%} 8 | {%- set item4 = item | merge ({title: 'Product4 Card Title', pricePromo:null, buyNowUrl:''}) -%} 9 | {%- set item5 = item | merge ({title: 'Product5 Card Title'}) -%} 10 | {%- set item6 = item | merge ({title: 'Product6 Card Title'}) -%} 11 | {%- set item7 = item | merge ({title: 'Product7 Card Title'}) -%} 12 | {%- set items = [item, item2, item3, item4, item5, item6, item7]-%} 13 | 14 | {% block tagline %} 15 | Masonry product list - only on large screens above 1280px 16 | {% endblock %} 17 | 18 | {% block preview %} 19 | {% rendercall helpers.container() %} 20 | {% render cards.productListMasonry(items) %} 21 | {% endrendercall %} 22 | {% endblock %} 23 | 24 | {% block variants %} 25 | {# container #} 26 | {% rendercall helpers.container() %} 27 | {# Code grid - 2 cols #} 28 |
29 | {% dscode 'njk' %} 30 | {% import "theme:card.html" as cards %} 31 | {% render cards.productListMasonry(items) %} 32 | {% enddscode %} 33 | 34 | {% dscode 'json', label = 'Data', parse = true %}{{ items | jsonPretty }}{% enddscode %} 35 |
36 | 37 | {# ensure the story finishes with a nice spacing #} 38 |
39 | {% endrendercall %} 40 | {% endblock %} 41 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/devcontainer.json. For config options, see the 2 | // README at: https://github.com/devcontainers/templates/tree/main/src/javascript-node-mongo 3 | { 4 | "name": "Node.js & Mongo DB", 5 | "dockerComposeFile": "docker-compose.yml", 6 | "service": "app", 7 | "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}", 8 | 9 | // Features to add to the dev container. More info: https://containers.dev/features. 10 | // "features": {}, 11 | 12 | // Configure tool-specific properties. 13 | "customizations": { 14 | // Grant organization private repos access 15 | "codespaces": { 16 | // https://docs.github.com/en/codespaces/managing-your-codespaces/managing-repository-access-for-your-codespaces 17 | // "repositories": { 18 | // "your-organization/*": { 19 | // "permissions": "write-all" 20 | // } 21 | // } 22 | }, 23 | // Configure properties specific to VS Code. 24 | "vscode": { 25 | // Add the IDs of extensions you want installed when the container is created. 26 | "extensions": [ 27 | "mongodb.mongodb-vscode", 28 | "bradlc.vscode-tailwindcss", 29 | "esbenp.prettier-vscode", 30 | "eseom.nunjucks-template" 31 | ] 32 | } 33 | }, 34 | 35 | // Use 'forwardPorts' to make a list of ports inside the container available locally. 36 | "forwardPorts": [ 3000, 27017 ], 37 | 38 | // Use 'postCreateCommand' to run commands after the container is created. 39 | "postCreateCommand": "npm install" 40 | 41 | // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. 42 | // "remoteUser": "root" 43 | } 44 | -------------------------------------------------------------------------------- /modules/theme/views/design-system/card/category.njk: -------------------------------------------------------------------------------- 1 | {% extends dsPreview %} 2 | {% import "theme:card.html" as cards %} 3 | {% import "theme:helpers.html" as helpers %} 4 | 5 | {% set storyData = apos.dsp.configData('molecules-card').category %} 6 | 7 | {% block tagline %} 8 | Category Card 9 | {% endblock %} 10 | 11 | {% block preview %} 12 | {% rendercall helpers.container() %} 13 | {% render cards.category(storyData) %} 14 | {% endrendercall %} 15 | {% endblock %} 16 | 17 | {% block variants %} 18 | {% rendercall helpers.container('mb-10') %} 19 |
20 | {% dscode 'njk' %} 21 | {% import "theme:card.html" as cards %} 22 | 23 | {% render cards.category(storyData) %} 24 | {% enddscode %} 25 | 26 | {% dscode 'json', label = 'Data', parse = true %}{{ storyData | jsonPretty }}{% enddscode %} 27 |
28 | 29 |

No "available proucts" data

30 |

It works both when productsAvailable is 0 or productsAvailable is null

31 |
32 | {% render cards.category(storyData | merge ({ productsAvailable: 0})) %} 33 | {% render cards.category(storyData | merge ({ productsAvailable: null})) %} 34 |
35 |
36 | {% dscode 'njk' %} 37 | {% import "theme:card.html" as cards %} 38 | 39 | {% render cards.category(storyData) %} 40 | {% enddscode %} 41 | 42 | {% dscode 'json', label = 'Data', parse = true %}{{ storyData | merge ({ productsAvailable: 0}) | jsonPretty }}{% enddscode %} 43 |
44 | {% endrendercall %} 45 | {% endblock %} 46 | -------------------------------------------------------------------------------- /modules/blockquote-widget/index.js: -------------------------------------------------------------------------------- 1 | export default { 2 | extend: '@apostrophecms/widget-type', 3 | options: { 4 | label: 'app:blockquoteWidget' 5 | }, 6 | fields: { 7 | add: { 8 | image: { 9 | type: 'area', 10 | label: 'app:image', 11 | options: { 12 | min: 1, 13 | max: 1, 14 | widgets: { 15 | '@apostrophecms/image': {} 16 | } 17 | }, 18 | required: true 19 | }, 20 | content: { 21 | type: 'area', 22 | label: 'app:content', 23 | options: { 24 | min: 1, 25 | max: 1, 26 | widgets: { 27 | '@apostrophecms/rich-text': { 28 | toolbar: [ 29 | 'styles', 30 | '|', 31 | 'bold', 32 | 'italic', 33 | 'strike', 34 | 'link', 35 | '|', 36 | 'bulletList', 37 | 'orderedList' 38 | ], 39 | styles: [ 40 | { 41 | tag: 'p', 42 | label: 'Paragraph (P)' 43 | } 44 | ], 45 | insert: [] 46 | } 47 | } 48 | }, 49 | required: true 50 | }, 51 | title: { 52 | type: 'string', 53 | label: 'app:name', 54 | required: true 55 | }, 56 | subtitle: { 57 | type: 'string', 58 | label: 'app:position' 59 | }, 60 | sectionTitle: { 61 | type: 'string', 62 | label: 'app:sectionTitle' 63 | } 64 | } 65 | } 66 | }; 67 | -------------------------------------------------------------------------------- /modules/theme/views/helpers.html: -------------------------------------------------------------------------------- 1 | {% import "theme:utils.html" as u %} 2 | {# A set of (mostly layout) helper fragments #} 3 | 4 | {# 5 | The container is defining the content horizontal (x) width and spacing. 6 | It makes no assumptions about the vertical spacing (y). 7 | Additional CSS classes can be passed via the `cls` argument. 8 | // [1440px] 9 | Usage: 10 | {% import theme:helpers as helpers %} 11 | {% rendercall helpers.container('some class') %} 12 | Your page header/body/footer comes here 13 | {% endrendercall %} 14 | #} 15 | {% fragment container(cls) %} 16 |
17 | {{ rendercaller() }} 18 |
19 | {% endfragment %} 20 | 21 | {# 22 | The section is defining the content vertical spacing (y). 23 | It makes no assumptions about the horizontal spacing (x). 24 | Sections are meant to separate logical content parts while 25 | bringing a consistent vertical spacing. 26 | When/if a content needs more spacing adjustments it 27 | should be done via an inside (inner wrapper) spacing. 28 | `data` and `attrs` arguments can be passed to the section as data/any attributes. 29 | Additional CSS classes can be passed via the `cls` argument. 30 | 31 | Usage: 32 | {% import theme:helpers as helpers %} 33 | {% rendercall helpers.section(cls='some class', data={...}, attrs={...}) %} 34 | Your content blocks, widgets, etc come here. 35 | {% endrendercall %} 36 | #} 37 | {% fragment section(cls = '', data = {}, attrs = {}) %} 38 |
39 | {{ rendercaller() }} 40 |
41 | {% endfragment %} 42 | -------------------------------------------------------------------------------- /modules/default-page/index.js: -------------------------------------------------------------------------------- 1 | export default { 2 | extend: '@apostrophecms/page-type', 3 | options: { 4 | label: 'app:defaultPage' 5 | }, 6 | fields: { 7 | add: { 8 | tagline: { 9 | type: 'string', 10 | label: 'app:tagline', 11 | textarea: true, 12 | max: 200 13 | }, 14 | headerType: { 15 | type: 'select', 16 | label: 'app:headerType', 17 | choices: [ 18 | { 19 | label: 'app:title', 20 | value: 'default' 21 | }, 22 | { 23 | label: 'app:hero', 24 | value: 'widget' 25 | } 26 | ], 27 | required: true, 28 | def: 'default' 29 | }, 30 | heading: { 31 | type: 'area', 32 | label: 'app:hero', 33 | options: { 34 | min: 1, 35 | max: 1, 36 | widgets: { 37 | 'hero-full': {}, 38 | 'hero-split': {} 39 | } 40 | }, 41 | if: { 42 | headerType: 'widget' 43 | } 44 | }, 45 | main: { 46 | type: 'area', 47 | options: { 48 | widgets: { 49 | '@apostrophecms/layout': {}, 50 | product: {}, 51 | 'product-featured': {}, 52 | 'product-category': {}, 53 | cta: {}, 54 | promo: {}, 55 | blockquote: {}, 56 | content: {} 57 | } 58 | } 59 | } 60 | }, 61 | group: { 62 | basics: { 63 | label: 'app:groupBasics', 64 | fields: [ 65 | 'title', 66 | 'tagline', 67 | 'headerType', 68 | 'heading', 69 | 'main' 70 | ] 71 | } 72 | } 73 | } 74 | }; 75 | -------------------------------------------------------------------------------- /scripts/sync-up: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | TARGET="$1" 4 | if [ -z "$TARGET" ]; then 5 | echo "Usage: ./scripts/sync-up production" 6 | echo "(or as appropriate)" 7 | echo 8 | echo "THIS WILL CLOBBER EVERYTHING ON THE" 9 | echo "TARGET SITE. MAKE SURE THAT IS WHAT" 10 | echo "YOU WANT!" 11 | exit 1 12 | fi 13 | 14 | read -p "THIS WILL CRUSH THE SITE'S CONTENT ON $TARGET. Are you sure? " -n 1 -r 15 | echo 16 | if [[ ! $REPLY =~ ^[Yy]$ ]] 17 | then 18 | exit 1 19 | fi 20 | 21 | source deployment/settings || exit 1 22 | source "deployment/settings.$TARGET" || exit 1 23 | 24 | #Enter the Mongo DB name (should be same locally and remotely). 25 | dbName=$PROJECT 26 | 27 | #Enter the Project name (should be what you called it for stagecoach). 28 | projectName=$PROJECT 29 | 30 | #Enter the SSH username/url for the remote server. 31 | remoteSSH="-p $SSH_PORT $USER@$SERVER" 32 | rsyncTransport="ssh -p $SSH_PORT" 33 | rsyncDestination="$USER@$SERVER" 34 | 35 | echo "Syncing MongoDB" 36 | mongodump -d $dbName -o /tmp/mongodump.$dbName && 37 | echo rsync -av -e "$rsyncTransport" /tmp/mongodump.$dbName/ $rsyncDestination:/tmp/mongodump.$dbName && 38 | rsync -av -e "$rsyncTransport" /tmp/mongodump.$dbName/ $rsyncDestination:/tmp/mongodump.$dbName && 39 | rm -rf /tmp/mongodump.$dbName && 40 | # noIndexRestore increases compatibility between 3.x and 2.x, 41 | # and Apostrophe will recreate the indexes correctly at startup 42 | ssh $remoteSSH mongorestore --noIndexRestore --drop -d $dbName /tmp/mongodump.$dbName/$dbName && 43 | echo "Syncing Files" && 44 | rsync -av --delete -e "$rsyncTransport" ./public/uploads/ $rsyncDestination:/opt/stagecoach/apps/$projectName/uploads && 45 | echo "Synced up to $TARGET" 46 | echo "YOU MUST RESTART THE SITE ON $TARGET TO REBUILD THE MONGODB INDEXES." 47 | -------------------------------------------------------------------------------- /views/layout.html: -------------------------------------------------------------------------------- 1 | {% extends data.outerLayout %} 2 | 3 | {% import "theme:header.html" as header %} 4 | {% import "theme:footer.html" as footer %} 5 | 6 | {% set title = data.piece.title or data.page.title %} 7 | {% block title %} 8 | {{ title }} 9 | {% if not title %} 10 | {{ apos.log('Looks like you forgot to override the title block in a template that does not have access to an Apostrophe page or piece.') }} 11 | {% endif %} 12 | {% endblock %} 13 | 14 | {% block extraHead %} 15 | {%- if data.global.favicon -%} 16 | 17 | {%- endif -%} 18 | {# Avoid FOUT by preloading the font files #} 19 | 37 | {% endblock %} 38 | 39 | {% block beforeMain %} 40 |
41 | {% render header.render( 42 | data.global, 43 | homeUrl = data.home._url, 44 | currentUrl = data.url 45 | ) %} 46 |
47 | {% endblock %} 48 | 49 | {% block main %}{% endblock %} 50 | 51 | {% block afterMain %} 52 |
53 | {% render footer.render(data.global, homeUrl = data.home._url) %} 54 |
55 | {% endblock %} 56 | 57 | {# Should be the same as your application layout #} 58 | {% block afterAposScripts %} 59 | {% include "theme:icons.svg.html" %} 60 | {% endblock %} 61 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs'; 2 | import defaultTheme from 'tailwindcss/defaultTheme'; 3 | import tailwindForms from '@tailwindcss/forms'; 4 | import tailwindTypography from '@tailwindcss/typography'; 5 | import tailwindAspectRatio from '@tailwindcss/aspect-ratio'; 6 | 7 | // Brand colors, default Jelly bean 8 | const brand = process.env.APP_BRAND || 'default'; 9 | const brandColors = JSON.parse(fs.readFileSync(`./colors/${brand}.json`)); 10 | 11 | /** @type {import('tailwindcss').Config} */ 12 | export default { 13 | content: [ 14 | './views/**/*.html', 15 | // Do not process .njk files in production, as they are part 16 | // of the Design system. 17 | process.env.NODE_ENV === 'production' 18 | ? './modules/**/*.{html,js,vue}' 19 | : './modules/**/*.{html,njk,js,vue}' 20 | ], 21 | theme: { 22 | fontFamily: { 23 | sans: [ '"Inter var"', ...defaultTheme.fontFamily.sans ] 24 | }, 25 | extend: { 26 | colors: { 27 | brand: brandColors, 28 | gray: { 29 | // gray-300 30 | DEFAULT: '#D1D5DB' 31 | } 32 | }, 33 | screens: { 34 | xs: '475px' 35 | } 36 | } 37 | }, 38 | safelist: [ 39 | // Pagination 40 | 'app-pager__item', 41 | 'is_active', 42 | // Rich text color styles 43 | 'text-brand', 44 | 'text-brand-300', 45 | // Apos select fix caused by form reset 46 | 'apos-input--select', 47 | // Image widget internal classes 48 | 'image-widget__wrapper', 49 | 'image-widget__caption', 50 | 'image-widget' 51 | ], 52 | corePlugins: { 53 | aspectRatio: false 54 | }, 55 | // https://tailwindcss.com/docs/plugins#official-plugins 56 | plugins: [ 57 | tailwindForms({ 58 | strategy: 'class' 59 | }), 60 | tailwindTypography, 61 | tailwindAspectRatio 62 | ] 63 | }; 64 | -------------------------------------------------------------------------------- /modules/theme/ui/src/components/tabs.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Switch tabbed content. It's used in Product View but 3 | * it's abstract enough to be used anywhere. 4 | */ 5 | let components = []; 6 | 7 | function init() { 8 | cleanup(); 9 | const tabs = document.querySelectorAll('[data-app-tabs]'); 10 | tabs.forEach(root => { 11 | const buttons = root.querySelectorAll('[data-tab-target]'); 12 | buttons.forEach((button) => { 13 | addListener(root, button, buttons); 14 | }); 15 | }); 16 | } 17 | 18 | function addListener(root, button, buttons) { 19 | const listener = (ev) => { 20 | const targetQuery = button.getAttribute('data-tab-target'); 21 | const target = root.querySelector(`[data-tab-content="${targetQuery}"]`); 22 | if (target) { 23 | ev.preventDefault(); 24 | // Active state. 25 | // NOTE: this can be done with a tailwind group state 26 | // in the future (see product gallery `is-active` class) 27 | buttons.forEach((button) => { 28 | button.classList.remove('border-b-2'); 29 | button.classList.remove('border-brand'); 30 | button.classList.remove('font-bold'); 31 | }); 32 | button.classList.add('border-b-2'); 33 | button.classList.add('border-brand'); 34 | button.classList.add('font-bold'); 35 | 36 | // Show target. 37 | const targets = root.querySelectorAll('[data-tab-content]'); 38 | targets.forEach((target) => { 39 | target.classList.add('hidden'); 40 | }); 41 | target.classList.remove('hidden'); 42 | } 43 | }; 44 | button.addEventListener('click', listener); 45 | components.push({ 46 | destroy: () => button.removeEventListener('click', listener) 47 | }); 48 | } 49 | 50 | function cleanup() { 51 | components.forEach(component => component.destroy()); 52 | components = []; 53 | } 54 | 55 | export { init }; 56 | -------------------------------------------------------------------------------- /deployment/dependencies: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | export NODE_ENV=production 4 | 5 | # Also a good place to ensure any data folders 6 | # that are *not* supposed to be replaced on every deployment exist 7 | # and create a symlink to them from the latest deployment directory. 8 | 9 | # The real 'data' folder is shared. It lives two levels up and one over 10 | # (we're in a deployment dir, which is a subdir of 'deployments', which 11 | # is a subdir of the project's main dir) 12 | 13 | HERE=`pwd` 14 | mkdir -p ../../data 15 | ln -s ../../data $HERE/data 16 | 17 | # We also have a shared uploads folder which is convenient to keep 18 | # in a separate place so we don't have to have two express.static calls 19 | 20 | mkdir -p ../../uploads 21 | ln -s ../../../uploads $HERE/public/uploads 22 | 23 | # Install any dependencies that can't just be rsynced over with 24 | # the deployment. Example: node apps have npm modules in a 25 | # node_modules folder. These may contain compiled C++ code that 26 | # won't work portably from one server to another. 27 | 28 | # This script runs after the rsync, but before the 'stop' script, 29 | # so your app is not down during the npm installation. 30 | 31 | # Make sure node_modules exists so npm doesn't go searching 32 | # up the filesystem tree 33 | mkdir -p node_modules 34 | 35 | # If there is no package.json file then we don't need npm install 36 | if [ -f './package.json' ]; then 37 | # Install npm modules 38 | # Use a suitable version of Python 39 | # export PYTHON=/usr/bin/python26 40 | npm install 41 | if [ $? -ne 0 ]; then 42 | echo "Error during npm install!" 43 | exit 1 44 | fi 45 | fi 46 | 47 | node app @apostrophecms/migration:migrate 48 | # Generate new static asset files for this 49 | # deployment of the app without shutting down 50 | # (TODO: for 3.0 this is actually disruptive because 51 | # we don't have a generation identifier yet) 52 | npm run build 53 | -------------------------------------------------------------------------------- /modules/theme/ui/src/components/gallery.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Gallery image player, used in Product View but 3 | * it's abstract enough to be used anywhere. 4 | */ 5 | let components = []; 6 | 7 | function init() { 8 | cleanup(); 9 | const galleries = document.querySelectorAll('[data-app-gallery]'); 10 | galleries.forEach(gallery => { 11 | const player = gallery.querySelector('[data-player]'); 12 | const thumbs = gallery.querySelectorAll('[data-thumbnail]'); 13 | const triggers = gallery.querySelectorAll('[data-trigger]'); 14 | thumbs.forEach((thumb, i) => { 15 | addTriggerListener(i, gallery, triggers, thumbs, player); 16 | }); 17 | }); 18 | } 19 | 20 | function addTriggerListener(i, gallery, triggers, thumbs, player) { 21 | const thumb = thumbs[i]; 22 | const trigger = triggers[i]; 23 | const current = gallery.querySelector('.is-active'); 24 | 25 | const listener = (ev) => { 26 | ev.preventDefault(); 27 | const src = thumb.getAttribute('src'); 28 | const style = thumb.getAttribute('style'); 29 | const alt = thumb.getAttribute('alt'); 30 | const width = thumb.getAttribute('width'); 31 | const height = thumb.getAttribute('height'); 32 | const srcset = thumb.getAttribute('srcset'); 33 | 34 | current?.classList.remove('is-active'); 35 | 36 | player.setAttribute('src', src); 37 | player.setAttribute('style', style); 38 | player.setAttribute('alt', alt); 39 | player.setAttribute('width', width); 40 | player.setAttribute('height', height); 41 | player.setAttribute('srcset', srcset); 42 | }; 43 | 44 | trigger.addEventListener('click', listener); 45 | components.push({ 46 | destroy() { 47 | trigger.removeEventListener('click', listener); 48 | } 49 | }); 50 | } 51 | 52 | function cleanup() { 53 | components.forEach(component => { 54 | component.destroy(); 55 | }); 56 | components = []; 57 | } 58 | 59 | export { init }; 60 | -------------------------------------------------------------------------------- /docs/developer/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Introduction 3 | titleTemplate: Developer 4 | --- 5 | 6 | # {{ $frontmatter.title }} 7 | 8 | The E-commerce Starter Kit project is a result of a collaboration between Apostrophe CMS and its partner agency Corllete. It aims to offer a solid ground for Apostrophe CMS e-commerce applications. It's built with the following in mind: 9 | - Provide a solid ground for Apostrophe e-commerce applications and showcase best practices 10 | - Allow developers to easily extend and adapt it to their needs 11 | - Allow developers to turn it into a real-world, production ready application in a matter of minutes 12 | - Keep it simple and newcomer friendly 13 | 14 | This documentation outlines the application architecture, key integration points and provides tips about further extending, scaling and personalizing the application to meet any individual project requirements. 15 | 16 | ## Getting Started 17 | 18 | Learn aboout how to install and run the application in development, get familiar with the application architecture in [the getting started section](/developer/getting-started). 19 | 20 | ## Branding & UI 21 | 22 | A guide to "make it yours" - from color scheme and Tailwind CSS specifics to extending and modifying the UI, can be found in [the Branding and UI](/developer/branding-and-ui) section. 23 | 24 | ## Modules & Widgets 25 | 26 | Learn more about the modules, widgets and custom development tools included in this package in [the dedicated for that ](/developer/modules-and-widgets). 27 | 28 | ## Component Library (aka Design System) 29 | 30 | This project was built using a 3rd party open source module - Corllete Design System. It is not integrated and installed by default. It was used in developing the application UI in isolation, according to design system rules and Figma Prototype. Installation and integration instruction and link to the Figma sources can be found in the [Design System](/developer/design-system) section. 31 | -------------------------------------------------------------------------------- /modules/theme/views/blockquote.html: -------------------------------------------------------------------------------- 1 | {% import 'theme:icon.html' as icons %} 2 | {% fragment render(item) %} 3 | {%- set attachment = apos.image.first(item.image or item._image or {}) -%} 4 |
5 | {%- if attachment | length -%} 6 | {# Should become aspect ratio (aspect-1) when Safari 14 is gone #} 7 |
8 | {{ attachment._alt }} 20 |
21 | {%- endif -%} 22 |
23 | {# Rich text #} 24 |
{{ rendercaller() }}
25 | {# Title, subtitle, quote #} 26 |
27 | {{ item.title }} 28 | {% if item.subtitle %} 29 | {{ item.subtitle }} 30 | {% endif %} 31 |
32 |
33 | {#
#} 34 | {{ icons.svg( 35 | 'quote', 'w-24 h-24 lg:w-44 lg:h-44', 36 | cls='absolute bottom-8 lg:bottom-16 right-8 lg:right-16 -z-[1] opacity-50 text-brand-200' 37 | ) }} 38 | {#
#} 39 |
40 | {% endfragment %} 41 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | import apostrophe from 'apostrophe'; 2 | 3 | apostrophe({ 4 | root: import.meta, 5 | shortName: 'a4-ecommerce-starter', 6 | modules: { 7 | // Design system: register only when not in production. 8 | // Install via `npm install @corllete/apos-ds` first. 9 | // Be sure to add some additonal configuration in 10 | // `modules/@corllete/apos-ds/index.js` and 11 | // `modules/@corllete/apos-ds-page-type/views/layout/preview.html` 12 | // and park the page in `modules/@apostrophecms/page/index.js` as 13 | // described in the docs. 14 | 15 | // ...(process.env.NODE_ENV !== 'production' 16 | // ? { 17 | // '@corllete/apos-ds': { 18 | // options: { 19 | // modules: [ 'theme' ], 20 | // docs: false 21 | // } 22 | // }, 23 | // '@corllete/apos-ds-page-type': { 24 | // options: { 25 | // legacyCodeBlocks: false, 26 | // useReleaseId: false 27 | // } 28 | // } 29 | // } 30 | // : {} 31 | // ), 32 | // END Design system 33 | 34 | '@apostrophecms/image-widget': { 35 | options: { 36 | inlineStyles: false, 37 | className: 'image-widget' 38 | } 39 | }, 40 | '@apostrophecms/video-widget': { 41 | options: { 42 | className: 'my-5 md:my-10' 43 | } 44 | }, 45 | '@apostrophecms/vite': {}, 46 | '@apostrophecms/open-graph': {}, 47 | '@apostrophecms/seo': {}, 48 | i18n: {}, 49 | tag: {}, 50 | 'product-category': {}, 51 | 'product-category-page': {}, 52 | product: {}, 53 | 'product-page': {}, 54 | 'default-page': {}, 55 | 'content-widget': {}, 56 | 'hero-full-widget': {}, 57 | 'hero-split-widget': {}, 58 | 'cta-widget': {}, 59 | 'promo-widget': {}, 60 | 'blockquote-widget': {}, 61 | 'product-widget': {}, 62 | 'product-featured-widget': {}, 63 | 'product-category-widget': {}, 64 | // All assets/client JS and server side templates. 65 | theme: {} 66 | } 67 | }); 68 | -------------------------------------------------------------------------------- /modules/@apostrophecms/search/views/index.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | 3 | {% import 'theme:helpers.html' as helpers %} 4 | {% import "theme:heading.html" as heading %} 5 | {% import "theme:button.html" as buttons %} 6 | 7 | {% block title %}{{ data.page.title }}{% endblock %} 8 | 9 | {% block main %} 10 | {% rendercall helpers.container() %} 11 |
12 | {% render heading.render(__t('app:newSearch')) %} 13 | 14 |
15 |
16 | 19 | {{ buttons.icon( 20 | 'magnifying-glass', 21 | label = 'Search', 22 | { cls: 'text-gray-600', attr: { type: 'submit' } } 23 | ) }} 24 |
25 | 26 | {% if data.docs | length %} 27 |
28 | {% for doc in data.docs %} 29 | {%- set tag = 'a' if doc._url else 'div' -%} 30 | <{{ tag }}{% if doc._url %} href="{{ doc._url }}"{% endif %} class="pt-8 pb-8 first:pt-0 last:pb-0 border-b border-gray last:border-b-0"> 31 |
32 | {{ doc.title }} 33 |
34 | {% if doc.tagline or doc.seoDescription %} 35 |
{{ doc.seoDescription or doc.tagline }}
36 | {% endif %} 37 | 38 | {% endfor %} 39 |
40 | {% include "pager.html" %} 41 | {% elif data.query.q %} 42 |

{{ __t('app:noResults') }}

43 | {% endif %} 44 |
45 | 46 |
47 | {% endrendercall %} 48 | {% endblock %} 49 | -------------------------------------------------------------------------------- /docs/user/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Introduction 3 | titleTemplate: User Guide 4 | --- 5 | 6 | # {{ $frontmatter.title }} 7 | 8 | The Apostrophe CMS E-commerce Starter Kit is a simple and yet fully functional e-commerce starter website. It introduces advanced techniques for content management with UX in mind, while aiming flexible content management and clear path for further development. 9 | 10 | The project comes with a list of predefined colors, so you can choose one or let developers add a new one that fits your brand. 11 | 12 | ## Brand Colors 13 | 14 | ### Jelly Bean 15 | 16 | ![Jelly Bean](./../images/brand-jelly-bean.jpeg) 17 | 18 | ### Lime 19 | 20 | ![Lime](./../images/brand-lime.jpeg) 21 | 22 | ### Purple 23 | 24 | ![Purple](./../images/brand-purple.jpeg) 25 | 26 | ### Sky 27 | 28 | ![Sky](./../images/brand-sky.jpeg) 29 | 30 | ### Pink 31 | 32 | ![Pink](./../images/brand-pink.jpeg) 33 | 34 | ## Getting Started 35 | 36 | A list of tips and recommendations as first steps in building your site content, including recipes for content taxonomy, can be found in [the getting started section](./getting-started.md). 37 | 38 | ## Products & Categories 39 | 40 | Dynamic product metadata and specifications, image gallery, promotional prices and more - out of the box support for the majority of store owner needs. Additionally, hero and promo widgets allow fine control over all the product related pages. Learn more in [the product and categories section](/user/products-and-categories). 41 | 42 | ## Custom Pages & Widgets 43 | 44 | Add any additional content to your website via the Custom Page type. Under "any", we mean really that - mix up rich text, images and videos to build an About Us page, or just create your Monthly Promotions landing page, it's up to you. Find out more in the [custom pages](/user/custom-pages) and [widgets](/user/widgets) sections. 45 | 46 | ## Search & SEO 47 | 48 | Find out about the site search and how to keep your content well presented on the internal search page and external search engines and social media. Learn more in the [dedicated on that topic section](/user/search-and-seo). 49 | -------------------------------------------------------------------------------- /modules/theme/views/design-system/promo-widget/promo.json: -------------------------------------------------------------------------------- 1 | { 2 | "promo": { 3 | "caption": "Promotions", 4 | "ctaPrimary": { 5 | "label": "Do Something", 6 | "urlType": "custom", 7 | "url": "#deal" 8 | }, 9 | "ctaOutlined": { 10 | "label": "Expolore", 11 | "urlType": "page", 12 | "_page": [ 13 | { 14 | "_url": "#explore" 15 | } 16 | ] 17 | }, 18 | "_image": [ 19 | { 20 | "_id": "clgyx91c9000s3eh9b9ji3dto:en:draft", 21 | "attachment": { 22 | "_id": "clgyx90ed000p3eh97uy897xp", 23 | "group": "images", 24 | "name": "some-product", 25 | "title": "some product", 26 | "extension": "jpg", 27 | "type": "attachment", 28 | "width": 1800, 29 | "height": 1350, 30 | "_urls": { 31 | "max": "https://picsum.photos/seed/promo/1600/1200", 32 | "full": "https://picsum.photos/seed/promo/1140/850", 33 | "two-thirds": "https://picsum.photos/seed/promo/760/570", 34 | "one-half": "https://picsum.photos/seed/promo/570/428", 35 | "one-third": "https://picsum.photos/seed/promo/380/285", 36 | "one-sixth": "https://picsum.photos/seed/promo/190/143", 37 | "original": "https://picsum.photos/seed/promo/1800/1350" 38 | }, 39 | "srcset": "https://picsum.photos/seed/promo/1600/1200 1600w, https://picsum.photos/seed/promo/1140/850 1140w, https://picsum.photos/seed/promo/760/570 760w, https://picsum.photos/seed/promo/570/428 570w, https://picsum.photos/seed/promo/380/285 380w, https://picsum.photos/seed/promo/190/143 190w" 40 | }, 41 | "title": "Product Card image", 42 | "alt": "Product card alt", 43 | "slug": "image-product", 44 | "archived": false, 45 | "type": "@apostrophecms/image", 46 | "aposLocale": "en:draft", 47 | "aposMode": "draft", 48 | "aposDocId": "clgyx91c9000s3eh9b9ji3dto", 49 | "metaType": "doc", 50 | "visibility": "public" 51 | } 52 | ] 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /modules/theme/views/design-system/card/product-list.njk: -------------------------------------------------------------------------------- 1 | {% extends dsPreview %} 2 | {% import "theme:card.html" as cards %} 3 | {% import "theme:helpers.html" as helpers %} 4 | 5 | {% set item = apos.dsp.configData('molecules-card').product %} 6 | {%- set item2 = item | merge ({title: 'Product2 Card Title'}) -%} 7 | {%- set item3 = item | merge ({title: 'Product3 Card Title', buyNowUrl:''}) -%} 8 | {%- set item4 = item | merge ({title: 'Product4 Card Title', pricePromo:null, buyNowUrl:''}) -%} 9 | {%- set item5 = item | merge ({title: 'Product5 Card Title'}) -%} 10 | {%- set item6 = item | merge ({title: 'Product6 Card Title'}) -%} 11 | {%- set item7 = item | merge ({title: 'Product7 Card Title'}) -%} 12 | {%- set items = [item, item2, item3, item4, item5, item6, item7]-%} 13 | 14 | 15 | {% block tagline %} 16 | Different grid layouts for product cards on large screens - above 1280px 17 | {% endblock %} 18 | 19 | {% block preview %} 20 | {% rendercall helpers.container() %} 21 | {% render cards.productList(items) %} 22 | {% endrendercall %} 23 | {% endblock %} 24 | 25 | {% block variants %} 26 | {# container #} 27 | {% rendercall helpers.container() %} 28 | 29 | {# Code grid - 2 cols #} 30 |
31 | {% dscode 'njk' %} 32 | {% import "theme:card.html" as cards %} 33 | {% render cards.productList(items) %} 34 | {% enddscode %} 35 | 36 | {% dscode 'json', label = 'Data', parse = true %}{{ items | jsonPretty }}{% enddscode %} 37 |
38 | 39 |

List of product cards in four columns on large screens - above 1280px

40 |

If there is condensed = true, the cards will be displayed in four columns

41 | {% render cards.productList(items, condensed = true) %} 42 |
43 | {% dscode 'njk' %} 44 | {% import "theme:card.html" as cards %} 45 | {% render cards.productList(items, condensed = true) %} 46 | {% enddscode %} 47 | 48 | {% dscode 'json', label = 'Data', parse = true %}{{ items | jsonPretty }}{% enddscode %} 49 |
50 | 51 | {# ensure the story finishes with a nice spacing #} 52 |
53 | 54 | {% endrendercall %} 55 | {% endblock %} 56 | -------------------------------------------------------------------------------- /modules/theme/views/footer.html: -------------------------------------------------------------------------------- 1 | {% import "theme:logo.html" as logo %} 2 | {% import "theme:icon.html" as icons %} 3 | {% import "theme:helpers.html" as helpers %} 4 | 5 | {# For the schema of the `config` parameter see footer.json #} 6 | {% fragment render(config, homeURL = '/') %} 7 | {%- set logoAttachment = apos.image.first(config.logo or {}) -%} 8 |
9 | {% rendercall helpers.container() %} 10 | 33 | {% endrendercall %} 34 |
35 | {% endfragment %} 36 | 37 | {% macro _nav(links) %} 38 | 47 | {% endmacro %} 48 | 49 | {% macro _social(links) %} 50 | {%- for item in links %} 51 | 52 | {{ item.label }} 53 | {{ icons.svg(item.icon) }} 54 | 55 | {%- endfor -%} 56 | {% endmacro %} 57 | -------------------------------------------------------------------------------- /modules/product-category-page/index.js: -------------------------------------------------------------------------------- 1 | export default { 2 | extend: '@apostrophecms/piece-page-type', 3 | options: { 4 | label: 'app:productCategoryPageLabel', 5 | alias: 'productCategoryPage', 6 | perPage: 99 7 | }, 8 | fields: { 9 | add: { 10 | tagline: { 11 | type: 'string', 12 | textarea: true, 13 | label: 'app:tagline', 14 | max: 200 15 | }, 16 | main: { 17 | type: 'area', 18 | label: 'app:content', 19 | options: { 20 | widgets: { 21 | product: {}, 22 | 'product-featured': {}, 23 | 'product-category': {}, 24 | cta: {}, 25 | promo: {}, 26 | blockquote: {} 27 | } 28 | } 29 | } 30 | }, 31 | group: { 32 | basics: { 33 | label: 'app:groupBasics', 34 | fields: [ 'tagline', 'main' ] 35 | } 36 | } 37 | }, 38 | extendMethods(self) { 39 | return { 40 | indexQuery(_super, req) { 41 | const query = _super(req); 42 | query.productCount(true).limit(0); 43 | return query; 44 | } 45 | }; 46 | }, 47 | methods(self) { 48 | return { 49 | // Load products based on the category `_tags` configuration, 50 | // mimic "index page" for products. 51 | async beforeShow(req) { 52 | const category = req.data.piece; 53 | if (!category) { 54 | return; 55 | } 56 | const page = self.apos.productPage; 57 | 58 | const query = page.indexQuery(req); 59 | await page.populatePiecesFilters(query); 60 | query._tags(category.tagsIds) 61 | .page(req.query.page || 1); 62 | 63 | const totalCount = await query.toCount(); 64 | if (query.get('page') > 1 && query.get('page') > query.get('totalPages')) { 65 | req.notFound = true; 66 | return; 67 | } 68 | 69 | req.data.currentPage = query.get('page'); 70 | req.data.totalPieces = totalCount; 71 | req.data.totalPages = query.get('totalPages'); 72 | 73 | req.data.pieces = await query.toArray(); 74 | await page.beforeIndex(req); 75 | } 76 | }; 77 | } 78 | }; 79 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "starter-kit-ecommerce", 3 | "version": "1.0.0", 4 | "description": "ApostropheCMS ecommerce starter kit", 5 | "main": "app.js", 6 | "type": "module", 7 | "scripts": { 8 | "start": "cross-env NODE_ENV=production node app", 9 | "dev": "npm run build:sprite && nodemon", 10 | "test": "npm run lint && npm run test:unit", 11 | "test:unit": "mocha", 12 | "lint": "eslint . --ext .js", 13 | "build": "cross-env NODE_ENV=production npm run build:sprite && NODE_ENV=production node app @apostrophecms/asset:build", 14 | "build:sprite": "./scripts/make-svg-sprite", 15 | "build:ds": "node app @corllete/apos-ds-page-type:publish-assets", 16 | "release": "cross-env npm install && npm run build && NODE_ENV=production node app @apostrophecms/migration:migrate" 17 | }, 18 | "nodemonConfig": { 19 | "delay": 1000, 20 | "verbose": true, 21 | "watch": [ 22 | "./app.js", 23 | "./modules/**/*", 24 | "./lib/**/*.js", 25 | "./views/**/*.html", 26 | "./colors/**/*.json" 27 | ], 28 | "ignoreRoot": [ 29 | ".git" 30 | ], 31 | "ignore": [ 32 | "**/ui/", 33 | "public/uploads/", 34 | "public/apos-frontend/*.js", 35 | "data/", 36 | "docs/", 37 | "design-system-setup/" 38 | ], 39 | "ext": "json, js, cjs, html, njk" 40 | }, 41 | "repository": { 42 | "type": "git", 43 | "url": "https://github.com/apostrophecms/starter-kit-ecommerce" 44 | }, 45 | "author": "Apostrophe Technologies", 46 | "license": "MIT", 47 | "dependencies": { 48 | "@apostrophecms/open-graph": "^1.2.1", 49 | "@apostrophecms/seo": "^1.2.0", 50 | "@apostrophecms/vite": "^1.0.0", 51 | "apostrophe": "^4.18.0", 52 | "cross-env": "^10.1.0", 53 | "lodash": "^4.17.21" 54 | }, 55 | "devDependencies": { 56 | "@tailwindcss/aspect-ratio": "^0.4.2", 57 | "@tailwindcss/forms": "^0.5.3", 58 | "@tailwindcss/typography": "^0.5.9", 59 | "autoprefixer": "^10.4.20", 60 | "chai": "^4.3.7", 61 | "chai-spies": "^1.0.0", 62 | "css-loader": "^6.7.3", 63 | "eslint-config-apostrophe": "^6.0.1", 64 | "mocha": "^10.2.0", 65 | "nodemon": "^2.0.7", 66 | "svgstore": "^3.0.1", 67 | "tailwindcss": "^3.3.1", 68 | "webpack-bundle-analyzer": "^4.8.0" 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /modules/theme/views/design-system/icon/svg.njk: -------------------------------------------------------------------------------- 1 | {% extends dsPreview %} 2 | {% import "theme:icon.html" as icon %} 3 | 4 | {% block tagline %} 5 | SVG icon sprite 6 | {% endblock %} 7 | 8 | {% block previewSimple %} 9 |
10 | {{ icon.svg('heart', 'sm') }} 11 | {{ icon.svg('heart', 'md') }} 12 | {{ icon.svg('heart', 'l', cls='text-brand') }} 13 | {{ icon.svg('heart', 'xl') }} 14 | {{ icon.svg('heart', 'h-[52px] w-[52px]') }} 15 |
16 | {% endblock %} 17 | 18 | {% block variants %} 19 | {# container #} 20 |
21 | {# Section code example #} 22 | {% dscode 'njk' %} 23 | {% import "theme:icon.html" as icon %} 24 | 25 | {# default size, equals to 'md' #} 26 | {{ icon.svg('heart') }} 27 | {# add custom class #} 28 | {{ icon.svg('heart', cls = "text-brand") }} 29 | {# predefined sizes #} 30 | {{ icon.svg('heart', 'sm') }} 31 | {{ icon.svg('heart', 'md') }} 32 | {{ icon.svg('heart', 'l') }} 33 | {{ icon.svg('heart', 'xl') }} 34 | {# custom size #} 35 | {{ icon.svg('heart', 'h-[96px] w-[96px]') }} 36 | {% enddscode %} 37 | 38 | {# Another section - auto list all icons #} 39 |

All Icons

40 |
41 | {% set icons = apos.theme.icons() %} 42 | {% for iconV in icons %} 43 |
44 | {{ icon.svg(iconV.value, 'xl') }} 45 | {# A quick JS to select all on single click #} 46 | {{ iconV.value }} 47 |
48 | {% endfor %} 49 |
50 | {# On dark #} 51 |
52 | {% set icons = apos.theme.icons() %} 53 | {% for iconV in icons %} 54 |
55 | {{ icon.svg(iconV.value, 'xl') }} 56 | {{ iconV.value }} 57 |
58 | {% endfor %} 59 |
60 |
61 | {% endblock %} 62 | -------------------------------------------------------------------------------- /deployment/start: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Make the site live again, for instance by tweaking a .htaccess file 4 | # or starting a node server. In this example we also set up a 5 | # data/port file so that sc-proxy.js can figure out what port 6 | # to forward traffic to for this site. The idea is that every 7 | # folder in /var/webapps represents a separate project with a separate 8 | # node process, each listening on a specific port, and they all 9 | # need traffic forwarded from a reverse proxy server on port 80 10 | 11 | # Useful for debugging 12 | #set -x verbose 13 | 14 | # Express should not reveal information on errors, 15 | # also optimizes Express performance 16 | export NODE_ENV=production 17 | 18 | if [ ! -f "app.js" ]; then 19 | echo "I don't see app.js in the current directory." 20 | exit 1 21 | fi 22 | 23 | # Assign a port number if we don't yet have one 24 | 25 | if [ -f "data/port" ]; then 26 | PORT=`cat data/port` 27 | else 28 | # No port set yet for this site. Scan and sort the existing port numbers if any, 29 | # grab the highest existing one 30 | PORT=`cat ../../../*/data/port 2>/dev/null | sort -n | tail -1` 31 | if [ "$PORT" == "" ]; then 32 | echo "First app ever, assigning port 3000" 33 | PORT=3000 34 | else 35 | # Bash is much nicer than sh! We can do math without tears! 36 | let PORT+=1 37 | fi 38 | echo $PORT > data/port 39 | echo "First startup, chose port $PORT for this site" 40 | fi 41 | 42 | # Run the app via 'forever' so that it restarts automatically if it fails 43 | # Use `pwd` to make sure we have a full path, forever is otherwise easily confused 44 | # and will stop every server with the same filename 45 | 46 | # Use a "for" loop. A classic single-port file will do the 47 | # right thing, but so will a file with multiple port numbers 48 | # for load balancing across multiple cores 49 | for port in $PORT 50 | do 51 | export PORT=$port 52 | forever --minUptime=1000 --spinSleepTime=10000 -o data/console.log -e data/error.log start `pwd`/app.js && echo "Site started" 53 | done 54 | 55 | # Run the app without 'forever'. Record the process id so 'stop' can kill it later. 56 | # We recommend installing 'forever' instead for node apps. For non-node apps this code 57 | # may be helpful 58 | # 59 | # node app.js >> data/console.log 2>&1 & 60 | # PID=$! 61 | # echo $PID > data/pid 62 | # 63 | #echo "Site started" 64 | -------------------------------------------------------------------------------- /modules/theme/ui/src/tailwind.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | /* Typography */ 6 | @layer base { 7 | html { 8 | @apply font-sans 9 | text-base 10 | font-normal 11 | text-gray-700; 12 | } 13 | 14 | h1 { 15 | @apply text-6xl 16 | font-extrabold; 17 | } 18 | 19 | h2 { 20 | @apply text-5xl 21 | font-extrabold; 22 | } 23 | 24 | h3 { 25 | @apply text-4xl 26 | font-extrabold; 27 | } 28 | 29 | h4 { 30 | @apply text-3xl 31 | font-bold; 32 | } 33 | 34 | h5 { 35 | @apply text-2xl 36 | font-bold; 37 | } 38 | 39 | h6 { 40 | @apply text-xl 41 | font-bold; 42 | } 43 | 44 | /* Page layout */ 45 | .app { 46 | @apply min-h-screen; 47 | display: grid; 48 | grid-template-rows: auto 1fr auto; 49 | overflow-x: hidden; 50 | } 51 | 52 | 53 | /* Apos/Tiptap specific */ 54 | 55 | .t-richtext li > p { 56 | @apply m-0; 57 | } 58 | 59 | .apos-input--select { 60 | background-image: none; 61 | } 62 | 63 | .app-pager { 64 | @apply mt-4 md:mt-8 border-b border-gray text-lg text-center; 65 | } 66 | 67 | .app-pager__item, 68 | .app-pager__item > a { 69 | display: inline-block; 70 | } 71 | 72 | .app-pager__item > a { 73 | @apply p-4; 74 | } 75 | 76 | .app-pager__item.is-active { 77 | @apply p-4 border-b-[3px] border-brand; 78 | } 79 | 80 | /* Image widget */ 81 | .image-widget__wrapper { 82 | @apply my-5 md:my-10; 83 | } 84 | .image-widget__caption { 85 | @apply mt-1 text-sm text-gray-600; 86 | } 87 | } 88 | 89 | @layer components { 90 | .t-display { 91 | @apply text-7xl 92 | font-extrabold leading-tight; 93 | } 94 | 95 | .t-subtitle { 96 | @apply text-xl 97 | font-light; 98 | } 99 | 100 | .t-caption { 101 | @apply text-sm 102 | font-semibold 103 | uppercase 104 | tracking-wider; 105 | } 106 | 107 | .t-link { 108 | @apply font-bold underline 109 | text-brand 110 | transition-all ease-in-out delay-150 111 | hover:text-brand-700 112 | active:text-brand-700 113 | visited:text-brand-700 114 | cursor-pointer; 115 | } 116 | 117 | .t-richtext { 118 | @apply prose prose-p:text-base max-w-2xl; 119 | } 120 | 121 | .break-anywhere { 122 | overflow-wrap: anywhere; 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /modules/theme/views/design-system/promo-widget/promo.njk: -------------------------------------------------------------------------------- 1 | {% extends dsPreview %} 2 | {% import "theme:promo-widget.html" as promoWidget %} 3 | {% import "theme:helpers.html" as helpers %} 4 | 5 | {% set storyData = apos.dsp.configData('molecules-promo-widget').promo %} 6 | 7 | {% block tagline %} 8 | Promo Widget 9 | {% endblock %} 10 | 11 | {% block preview %} 12 | {% rendercall helpers.container() %} 13 | {% rendercall promoWidget.render(storyData) %} 14 |

Summer Sale

15 |

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut magna, scelerisque vitae augue et, cursus placerat lorem. In nisi lacus, eleifend tincidunt quam et, consequat semper.

16 | {% endrendercall %} 17 | {% endrendercall %} 18 | {% endblock %} 19 | 20 | {% block variants %} 21 | {# container #} 22 | {% rendercall helpers.container('mb-10') %} 23 |
24 | {% dscode 'njk' %} 25 | {% import "theme:promo-widget.html" as promoWidget %} 26 | 27 | {% rendercall promoWidget.render(storyData) %} 28 |

Summer Sale

29 |

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut magna, scelerisque vitae augue et, cursus placerat lorem. In nisi lacus, eleifend tincidunt quam et, consequat semper.

30 | {% endrendercall %} 31 | {% enddscode %} 32 | 33 | {% dscode 'json', label = 'Data', parse = true %}{{ storyData | jsonPretty }}{% enddscode %} 34 |
35 | 36 |
37 | {% rendercall promoWidget.render(storyData | merge({ imagePosition: 'right' })) %} 38 |

Summer Sale

39 |

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut magna, scelerisque vitae augue et, cursus placerat lorem. In nisi lacus, eleifend tincidunt quam et, consequat semper.

40 | {% endrendercall %} 41 |
42 |
43 | {% dscode 'njk' %} 44 | {% import "theme:promo-widget.html" as promoWidget %} 45 | 46 | {% rendercall promoWidget.render(storyData) %} 47 |

Summer Sale

48 |

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut magna, scelerisque vitae augue et, cursus placerat lorem. In nisi lacus, eleifend tincidunt quam et, consequat semper.

49 | {% endrendercall %} 50 | {% enddscode %} 51 | 52 | {% dscode 'json', label = 'Data', parse = true %}{{ storyData | merge({ imagePosition: 'right' }) | jsonPretty }}{% enddscode %} 53 |
54 | {% endrendercall %} 55 | {% endblock %} 56 | -------------------------------------------------------------------------------- /modules/theme/views/design-system/card/product.njk: -------------------------------------------------------------------------------- 1 | {% extends dsPreview %} 2 | {% import "theme:card.html" as cards %} 3 | {% import "theme:helpers.html" as helpers %} 4 | 5 | {% set storyDataP = apos.dsp.configData('molecules-card').product %} 6 | 7 | {% block tagline %} 8 | Product Card 9 | {% endblock %} 10 | 11 | {% block preview %} 12 | {% rendercall helpers.container() %} 13 | {% render cards.product(storyDataP) %} 14 | {% endrendercall %} 15 | {% endblock %} 16 | 17 | {% block variants %} 18 | {# container #} 19 | {% rendercall helpers.container() %} 20 | 21 | {# Code grid - 2 cols #} 22 |
23 | {% dscode 'njk' %} 24 | {% import "theme:card.html" as cards %} 25 | {# Default product card with all data #} 26 | {% render cards.product(storyDataP) %} 27 | {% enddscode %} 28 | 29 | {% dscode 'json', label = 'Data', parse = true %}{{ storyDataP | jsonPretty }}{% enddscode %} 30 |
31 | 32 |

Product card with all data, except buyNowUrl

33 |

The button "Buy Now" will not be displayed, if there is no buyNowUrl, e.g. buyNowUrl is empty string or null

34 | {% render cards.product(storyDataP | merge ({buyNowUrl: null})) %} 35 | 36 |
37 | {% dscode 'njk' %} 38 | {% import "theme:card.html" as cards %} 39 | {# Product card with all data, except buyNowUrl #} 40 | {% render cards.product(storyDataP | merge ({buyNowUrl: null})) %} 41 | {% enddscode %} 42 | 43 | {% dscode 'json', label = 'Data', parse = true %}{{ storyDataP | jsonPretty }}{% enddscode %} 44 |
45 | 46 | 47 |

Product card with all data, except pricePromo

48 |

The promotional price will not be displayed, if there is no pricePromo. Instead - the regular price will be displayed and styled accordingly.

49 | {% render cards.product(storyDataP | merge ({pricePromo: null})) %} 50 |
51 | {% dscode 'njk' %} 52 | {% import "theme:card.html" as cards %} 53 | {# Default product card with all data, except promoPrice #} 54 | {% render cards.product(storyDataP | merge ({pricePromo: null})) %} 55 | {% enddscode %} 56 | 57 | {% dscode 'json', label = 'Data', parse = true %}{{ storyDataP | jsonPretty }}{% enddscode %} 58 |
59 | 60 | {# ensure the story finishes with a nice spacing #} 61 |
62 | 63 | {% endrendercall %} 64 | {% endblock %} 65 | -------------------------------------------------------------------------------- /modules/theme/views/design-system/cta-widget/data.json: -------------------------------------------------------------------------------- 1 | { 2 | "image": { 3 | "title": "Get inspired by our new catalogue", 4 | "ctaPrimary": 5 | { 6 | "label": "Download", 7 | "urlType": "custom", 8 | "url": "#urlCtaPrimary" 9 | } 10 | , 11 | "ctaOutlined": { 12 | "label": "Explore", 13 | "urlType": "page", 14 | "_page": [ 15 | { 16 | "_url": "#_page_urlCtaOutlined" 17 | } 18 | ] 19 | }, 20 | "_image": [ 21 | { 22 | "_id": "clgyx91c9000s3eh9b9ji3dto:en:draft", 23 | "attachment": { 24 | "_id": "clgyx90ed000p3eh97uy897xp", 25 | "group": "images", 26 | "name": "some-image", 27 | "title": "some image", 28 | "extension": "jpg", 29 | "type": "attachment", 30 | "width": 1800, 31 | "height": 1350, 32 | "_urls": { 33 | "max": "https://picsum.photos/seed/image/1600/1200", 34 | "full": "https://picsum.photos/seed/image/1140/850", 35 | "two-thirds": "https://picsum.photos/seed/image/760/570", 36 | "one-half": "https://picsum.photos/seed/image/570/428", 37 | "one-third": "https://picsum.photos/seed/image/380/285", 38 | "one-sixth": "https://picsum.photos/seed/image/190/143", 39 | "original": "https://picsum.photos/seed/image/1800/1350" 40 | }, 41 | "srcset": "https://picsum.photos/seed/image/1600/1200 1600w, https://picsum.photos/seed/image/1140/850 1140w, https://picsum.photos/seed/image/760/570 760w, https://picsum.photos/seed/image/570/428 570w, https://picsum.photos/seed/image/380/285 380w, https://picsum.photos/seed/image/190/143 190w" 42 | 43 | }, 44 | "title": "CTA Widget image", 45 | "alt": "CTA Widget alt", 46 | "slug": "image-laptop2", 47 | "archived": false, 48 | "type": "@apostrophecms/image", 49 | "aposLocale": "en:draft", 50 | "aposMode": "draft", 51 | "aposDocId": "clgyx91c9000s3eh9b9ji3dto", 52 | "metaType": "doc", 53 | "visibility": "public" 54 | } 55 | ] 56 | }, 57 | "solid": { 58 | "title": "Get inspired by our new catalogue", 59 | "ctaPrimary": 60 | { 61 | "label": "Download", 62 | "urlType": "custom", 63 | "url": "#urlCtaPrimary" 64 | } 65 | , 66 | "ctaOutlined": { 67 | "label": "Explore", 68 | "urlType": "page", 69 | "_page": [ 70 | { 71 | "_url": "#_page_urlCtaOutlined" 72 | } 73 | ] 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /docs/.vitepress/theme/style.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Customize default theme styling by overriding CSS variables: 3 | * https://github.com/vuejs/vitepress/blob/main/src/client/theme-default/styles/vars.css 4 | */ 5 | 6 | /** 7 | * Colors 8 | * -------------------------------------------------------------------------- */ 9 | 10 | :root { 11 | --vp-c-brand: #646cff; 12 | --vp-c-brand-light: #747bff; 13 | --vp-c-brand-lighter: #9499ff; 14 | --vp-c-brand-lightest: #bcc0ff; 15 | --vp-c-brand-dark: #535bf2; 16 | --vp-c-brand-darker: #454ce1; 17 | --vp-c-brand-dimm: rgba(100, 108, 255, 0.08); 18 | } 19 | 20 | /** 21 | * Component: Button 22 | * -------------------------------------------------------------------------- */ 23 | 24 | :root { 25 | --vp-button-brand-border: var(--vp-c-brand-light); 26 | --vp-button-brand-text: var(--vp-c-white); 27 | --vp-button-brand-bg: var(--vp-c-brand); 28 | --vp-button-brand-hover-border: var(--vp-c-brand-light); 29 | --vp-button-brand-hover-text: var(--vp-c-white); 30 | --vp-button-brand-hover-bg: var(--vp-c-brand-light); 31 | --vp-button-brand-active-border: var(--vp-c-brand-light); 32 | --vp-button-brand-active-text: var(--vp-c-white); 33 | --vp-button-brand-active-bg: var(--vp-button-brand-bg); 34 | } 35 | 36 | /** 37 | * Component: Home 38 | * -------------------------------------------------------------------------- */ 39 | 40 | :root { 41 | --vp-home-hero-name-color: transparent; 42 | --vp-home-hero-name-background: -webkit-linear-gradient( 43 | 120deg, 44 | #bd34fe 30%, 45 | #41d1ff 46 | ); 47 | 48 | --vp-home-hero-image-background-image: linear-gradient( 49 | -45deg, 50 | #bd34fe 50%, 51 | #47caff 50% 52 | ); 53 | --vp-home-hero-image-filter: blur(40px); 54 | } 55 | 56 | @media (min-width: 640px) { 57 | :root { 58 | --vp-home-hero-image-filter: blur(56px); 59 | } 60 | } 61 | 62 | @media (min-width: 960px) { 63 | :root { 64 | --vp-home-hero-image-filter: blur(72px); 65 | } 66 | } 67 | 68 | /** 69 | * Component: Custom Block 70 | * -------------------------------------------------------------------------- */ 71 | 72 | :root { 73 | --vp-custom-block-tip-border: var(--vp-c-brand); 74 | --vp-custom-block-tip-text: var(--vp-c-brand-darker); 75 | --vp-custom-block-tip-bg: var(--vp-c-brand-dimm); 76 | } 77 | 78 | .dark { 79 | --vp-custom-block-tip-border: var(--vp-c-brand); 80 | --vp-custom-block-tip-text: var(--vp-c-brand-lightest); 81 | --vp-custom-block-tip-bg: var(--vp-c-brand-dimm); 82 | } 83 | 84 | /** 85 | * Component: Algolia 86 | * -------------------------------------------------------------------------- */ 87 | 88 | .DocSearch { 89 | --docsearch-primary-color: var(--vp-c-brand) !important; 90 | } 91 | 92 | -------------------------------------------------------------------------------- /modules/theme/views/design-system/cta-widget/solid.njk: -------------------------------------------------------------------------------- 1 | {% extends dsPreview %} 2 | {% import "theme:cta-widget.html" as cta %} 3 | {% import "theme:helpers.html" as helpers %} 4 | 5 | {% set storyData = apos.dsp.configData('molecules-cta-widget').solid %} 6 | 7 | {% block tagline %} 8 | cta Image 9 | {% endblock %} 10 | 11 | {% block preview %} 12 | {% rendercall helpers.container() %} 13 | 14 | {% rendercall cta.solid(storyData) %} 15 |

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut magna, scelerisque vitae augue et, cursus placerat lorem. In nisi lacus, eleifend tincidunt quam et, consequat semper.

16 | {% endrendercall %} 17 | 18 | {% endrendercall %} 19 | {% endblock %} 20 | 21 | {% block variants %} 22 | {# container #} 23 | {% rendercall helpers.container() %} 24 | 25 | {# Code grid - 2 cols #} 26 |
27 | {% dscode 'njk' %} 28 | {% import "theme:cta-widget.html" as cta %} 29 | {# Cta-widget with default dark solid background when no renderType is specified #} 30 | {% rendercall cta.solid(storyData) %} 31 |

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut magna, scelerisque vitae augue et, cursus placerat lorem. In nisi lacus, eleifend tincidunt quam et, consequat semper.

32 | {% endrendercall %} 33 | 34 | {% enddscode %} 35 | 36 | {% dscode 'json', label = 'Data', parse = true %}{{ storyData | jsonPretty }}{% enddscode %} 37 |
38 | 39 |

Cta-widget with light solid background

40 |

With light solid background, when renderType: 'light'

41 | {% rendercall cta.solid(storyData | merge({ renderType: 'light' })) %} 42 |

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut magna, scelerisque vitae augue et, cursus placerat lorem. In nisi lacus, eleifend tincidunt quam et, consequat semper.

43 | {% endrendercall %} 44 |
45 | {% dscode 'njk' %} 46 | {% import "theme:cta-widget.html" as cta %} 47 | {# Cta-widget with light solid background when renderType: 'light' #} 48 | {% rendercall cta.solid(storyData | merge({ renderType: 'light' })) %} 49 |

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut magna, scelerisque vitae augue et, cursus placerat lorem. In nisi lacus, eleifend tincidunt quam et, consequat semper.

50 | {% endrendercall %} 51 | {% enddscode %} 52 | 53 | {% dscode 'json', label = 'Data', parse = true %}{{ storyData | jsonPretty }}{% enddscode %} 54 |
55 | 56 | {# ensure the story finishes with a nice spacing #} 57 |
58 | 59 | {% endrendercall %} 60 | {% endblock %} 61 | -------------------------------------------------------------------------------- /modules/theme/views/design-system/card/blockquote.njk: -------------------------------------------------------------------------------- 1 | {% extends dsPreview %} 2 | {% import "theme:blockquote.html" as quote %} 3 | {% import "theme:helpers.html" as helpers %} 4 | 5 | {% set storyData = apos.dsp.configData('molecules-card').blockquote %} 6 | 7 | {% block tagline %} 8 | Customer Review (blockquote) 9 | {% endblock %} 10 | 11 | {% block preview %} 12 | {% rendercall helpers.container() %} 13 | {% rendercall quote.render(storyData) %} 14 |

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut magna, scelerisque vitae augue et, cursus placerat lorem. In nisi lacus, eleifend tincidunt quam et, consequat semper. Ut magna, scelerisque vitae augue et, cursus placerat lorem. In nisi lacus, eleifend tincidunt quam et, consequat semper.

15 | {% endrendercall %} 16 | {% endrendercall %} 17 | {% endblock %} 18 | 19 | {% block variants %} 20 | {# container #} 21 | {% rendercall helpers.container('mb-10') %} 22 |
23 | {% dscode 'njk' %} 24 | {% import "theme:blockquote.html" as quote %} 25 | 26 | {% rendercall quote.render(storyData) %} 27 |

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut magna, scelerisque vitae augue et, cursus placerat lorem. In nisi lacus, eleifend tincidunt quam et, consequat semper. Ut magna, scelerisque vitae augue et, cursus placerat lorem. In nisi lacus, eleifend tincidunt quam et, consequat semper.

28 | {% endrendercall %} 29 | {% enddscode %} 30 | 31 | {% dscode 'json', label = 'Data', parse = true %}{{ storyData | jsonPretty }}{% enddscode %} 32 |
33 | 34 |

No image

35 | {% rendercall quote.render(storyData | merge({ _image: null })) %} 36 |

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut magna, scelerisque vitae augue et, cursus placerat lorem. In nisi lacus, eleifend tincidunt quam et, consequat semper. Ut magna, scelerisque vitae augue et, cursus placerat lorem. In nisi lacus, eleifend tincidunt quam et, consequat semper.

37 | {% endrendercall %} 38 |
39 | {% dscode 'njk' %} 40 | {% import "theme:blockquote.html" as quote %} 41 | 42 | {% rendercall quote.render(storyData) %} 43 |

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut magna, scelerisque vitae augue et, cursus placerat lorem. In nisi lacus, eleifend tincidunt quam et, consequat semper. Ut magna, scelerisque vitae augue et, cursus placerat lorem. In nisi lacus, eleifend tincidunt quam et, consequat semper.

44 | {% endrendercall %} 45 | {% enddscode %} 46 | 47 | {% dscode 'json', label = 'Data', parse = true %}{{ storyData | merge({ _image: null }) | jsonPretty }}{% enddscode %} 48 |
49 | 50 | {% endrendercall %} 51 | {% endblock %} 52 | -------------------------------------------------------------------------------- /scripts/make-svg-sprite: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | // Generate Nunjucks inline SVG sprite from 4 | // all available icons in `modules/theme/icons/` 5 | // to `modules/theme/views/icons.svg.html`. 6 | // Write a meta JSON file `modules/theme/icons/icons.svg.json`, 7 | // used to auto-build icon choices for the schema and optionally 8 | // a preview in the design system. 9 | 10 | import url from 'node:url'; 11 | import path from 'node:path'; 12 | import fs from 'node:fs'; 13 | import svgstore from 'svgstore'; 14 | import _ from 'lodash'; 15 | 16 | const __dirname = path.dirname(url.fileURLToPath(import.meta.url)); 17 | 18 | // --- CONFIGURE BELOW --- 19 | // Ignore svg's by name (e.g. ['logo.svg']) 20 | const ignoreIcons = []; 21 | // Copy attributes from the original svg to the symbol. 22 | // Can be set to `false` or array of attribute names. 23 | // `viewBox`, `aria-labelledby`, and `role` will be always copied 24 | const copyAttrs = [ 'fill', 'stroke-width', 'stroke' ]; 25 | // Prefix for the id of the symbol. The filename will be appended. 26 | // If you change this you need to adapt the `icon.html` component as well. 27 | const idPrefix = 'theme-svg-icon-'; 28 | // --- END CONFIGURE --- 29 | 30 | // Generally you should not need to edit below this line 31 | const iconPath = path.join(__dirname, '../modules/theme/icons'); 32 | const spritePath = path.join(__dirname, '../modules/theme/views/icons.svg.html'); 33 | const metaPath = path.join(__dirname, '../modules/theme/icons/icons.svg.json'); 34 | 35 | // https://www.npmjs.com/package/svgstore#options 36 | const sprite = svgstore({ 37 | cleanDefs: true, 38 | cleanSymbols: true, 39 | copyAttrs 40 | }); 41 | const config = []; 42 | 43 | const files = fs.readdirSync(iconPath); 44 | const icons = files 45 | .filter(file => file.endsWith('.svg') && !ignoreIcons.includes(file)); 46 | 47 | for (const icon of icons) { 48 | const _name = icon.replace('.svg', ''); 49 | const name = `${idPrefix}${_name}`; 50 | sprite.add(name, fs.readFileSync(path.join(iconPath, icon), 'utf8')); 51 | config.push({ 52 | label: _.startCase(_name), 53 | value: _name 54 | }); 55 | } 56 | 57 | // Write the template 58 | const template = `{# Auto generated, do not edit (see /scripts/make-svg-sprite) #} 59 | {% raw %} 60 |
61 | ${sprite.toString({ inline: true })} 62 |
63 | {% endraw %} 64 | `; 65 | fs.writeFileSync(spritePath, template); 66 | 67 | // Write the config 68 | config.sort((a, b) => { 69 | if (a.label < b.label) { 70 | return -1; 71 | } 72 | if (a.label > b.label) { 73 | return 1; 74 | } 75 | return 0; 76 | }); 77 | fs.writeFileSync(metaPath, JSON.stringify(config, null, 2), 'utf8'); 78 | 79 | // eslint-disable-next-line no-console 80 | console.log(`Successfully built ${config.length} icons to sprite.`); 81 | -------------------------------------------------------------------------------- /modules/theme/views/gallery.html: -------------------------------------------------------------------------------- 1 | 2 | {% fragment image(images) %} 3 | {%- if images | length %} 4 | {%- set attachments = apos.image.all(images) -%} 5 | {%- set first = attachments | first -%} 6 |
10 | {# The player #} 11 | {{ first._alt }}{# /player #} 23 | 24 | {# The playlist #} 25 | {%- set hidden = '' if attachments | length > 1 else ' hidden' -%} 26 | {# /playlist #} 65 |
66 | {%- endif -%} 67 | {% endfragment %} 68 | -------------------------------------------------------------------------------- /modules/theme/views/design-system/00-global/colors.njk: -------------------------------------------------------------------------------- 1 | {% extends dsPreview %} 2 | {% import "@corllete/apos-ds-page-type:components/story/colors.html" as colors %} 3 | 4 | {% block css %} 5 | 10 | {% endblock %} 11 | 12 | {% block tagline %} 13 | Color Scheme 14 | {% endblock %} 15 | 16 | {% block preview %} 17 | {% dssection 'Scheme' %} 18 | 19 | {{ colors.palette([ 20 | [ 21 | { 22 | name: "Brand", 23 | value: "bg-brand", 24 | color: "#fff" 25 | } 26 | ], 27 | [ 28 | { 29 | name: "Gray", 30 | value: "bg-gray", 31 | color: "#000" 32 | } 33 | ] 34 | ], { span: 12, detect: true, showClass: true }) }} 35 | {% enddssection %} 36 | 37 | {% dssection 'Palette' %} 38 | {{ colors.palette([ 39 | [ 40 | { 41 | value: "bg-brand-50", 42 | color: "#000" 43 | }, 44 | { 45 | value: "bg-brand-100", 46 | color: "#000" 47 | }, 48 | { 49 | value: "bg-brand-200", 50 | color: "#000" 51 | }, 52 | { 53 | value: "bg-brand-300", 54 | color: "#000" 55 | }, 56 | { 57 | value: "bg-brand-400", 58 | color: "#000" 59 | }, 60 | { 61 | value: "bg-brand-500", 62 | color: "#000" 63 | }, 64 | { 65 | value: "bg-brand-600", 66 | color: "#fff" 67 | }, 68 | { 69 | value: "bg-brand-700", 70 | color: "#fff" 71 | }, 72 | { 73 | value: "bg-brand-800", 74 | color: "#fff" 75 | }, 76 | { 77 | value: "bg-brand-900", 78 | color: "#fff" 79 | }, 80 | { 81 | value: "bg-brand-950", 82 | color: "#fff" 83 | } 84 | ], 85 | [ 86 | { 87 | value: "bg-gray-50", 88 | color: "#000" 89 | }, 90 | { 91 | value: "bg-gray-100", 92 | color: "#000" 93 | }, 94 | { 95 | value: "bg-gray-200", 96 | color: "#000" 97 | }, 98 | { 99 | value: "bg-gray-300", 100 | color: "#000" 101 | }, 102 | { 103 | value: "bg-gray-400", 104 | color: "#000" 105 | }, 106 | { 107 | value: "bg-gray-500", 108 | color: "#000" 109 | }, 110 | { 111 | value: "bg-gray-600", 112 | color: "#fff" 113 | }, 114 | { 115 | value: "bg-gray-700", 116 | color: "#fff" 117 | }, 118 | { 119 | value: "bg-gray-800", 120 | color: "#fff" 121 | }, 122 | { 123 | value: "bg-gray-900", 124 | color: "#fff" 125 | }, 126 | { 127 | value: "bg-gray-950", 128 | color: "#fff" 129 | } 130 | ] 131 | ], { span: 12, detect: true, showClass: true }) }} 132 | 133 | {% enddssection %} 134 | 135 | {% endblock %} 136 | -------------------------------------------------------------------------------- /modules/theme/views/promo-widget.html: -------------------------------------------------------------------------------- 1 | {% import 'theme:button.html' as buttons %} 2 | {# 3 | Render a promo widget. 4 | widget: (object) required 5 | image or _image: Apos area having image widget or image relationship 6 | caption (string) 7 | ctaPrimary: (object) optional 8 | label: (string), 9 | urlType: (string), 'custom' | 'page' | 'file' | 'category' 10 | url: (string) URL if 'custom' 11 | _page, _file, _category: (array) conditional relations depending on urlType 12 | ctaOutlined: (object) optional, same as 'ctaPrimary' 13 | imagePosition: (string) 'left' or 'right', default 'left' 14 | #} 15 | {% fragment render(widget) %} 16 | {%- set onRight = true if widget.imagePosition === 'right' -%} 17 | {%- set attachment = apos.image.first(widget.image or widget._image) -%} 18 | {%- set imagePos = ' lg:inset-x-1/2' if onRight else ' lg:left-0 lg:right-1/2' -%} 19 | {%- set contentPos = ' lg:mr-auto' if onRight else ' lg:ml-auto' -%} 20 | {%- set ctaPrimary = apos.theme.navItems([widget.ctaPrimary]) | first -%} 21 | {%- set ctaOutlined = apos.theme.navItems([widget.ctaOutlined]) | first -%} 22 |
23 | {# Image #} 24 | {{ attachment._alt }} 35 | {# Content #} 36 |
37 | {%- if widget.caption -%} 38 | {{ widget.caption }} 39 | {%- endif -%} 40 |
41 | {{ rendercaller() }} 42 |
43 |
44 | {%- if ctaPrimary.label and ctaPrimary.url -%} 45 | {{ buttons.primary( 46 | ctaPrimary.label, 47 | href = ctaPrimary.url, 48 | { large: true } 49 | ) }} 50 | {%- endif -%} 51 | {%- if ctaOutlined.label and ctaOutlined.url -%} 52 | {{ buttons.outlined( 53 | ctaOutlined.label, 54 | href = ctaOutlined.url, 55 | { large: true, mobileInverse: true } 56 | ) }} 57 | {%- endif -%} 58 |
59 |
60 |
61 | {% endfragment %} 62 | -------------------------------------------------------------------------------- /docs/user/products-and-categories.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Products and Categories 3 | titleTemplate: User Guide 4 | --- 5 | 6 | # {{ $frontmatter.title }} 7 | 8 | The Apostrophe CMS E-commerce Starter Kit comes with Product and Product Category pieces and pages. Be sure to check out the [Getting Started](./getting-started.md) section first for a list of tips and recommendations as first steps in building your site content, including receipes for content taxonomy. 9 | 10 | ## Product Category 11 | 12 | You can create new categories by either using the quick menu (the blue plus button at the top of the page) and choosing "Product Category", or by using the "New Product Category" button after clicking the "Content - Product Categories" administration menu item. 13 | 14 | Beside the essential fields, when adding a new category piece, you can also: 15 | 16 | - Add "Publication Date" to control the order of the categories when shown in the page or widget. Categories having newer publication date will be shown first. If you leave that field empty, it will contain the date when the category was created. 17 | - From the "Page" tab, you can control the heading type of every category view page via the "Header Type" field. By default, a "Title" will be set, resulting in the default Title/Tagline heading. You can choose "Hero" and you'll see a new area for adding the Hero widget. 18 | 19 | **Title** 20 | ![Default Heading](../images/heading-default.png) 21 | 22 | **Hero** 23 | ![Hero Heading](../images/heading-hero.png) 24 | 25 | ::: tip 26 | You can choose your user experience - manage your widgets within the page itself or inside the Page Editor. Keep in mind, that the Heading area will be visible once you choose the "Hero" heading type, which can happen only in the Page Editor. 27 | ::: 28 | 29 | ## Product 30 | 31 | You can create new products by either using the quick menu (the blue plus button at the top of the page) and choosing "Product", or by using the "New Product" button after clicking the "Content - Products" administration menu item. 32 | 33 | Beside the essential fields, when adding a new category piece, you can also: 34 | 35 | - Add Buy Now URL. It will be shown as Buy Now button in the product view page and on any product card on your site. 36 | - Add "Publication Date" to control the order of the products when shown in the page or widget. Products having newer publication date will be shown first. If you leave that field empty, it will contain the date when the product was created. 37 | - Add product metadata and specifications. They will be shown in the product view page. 38 | ![Product Metadata](../images/product-meta.png) 39 | - Add Promo Price. It will be shown in the product view page, beside the regular price. 40 | 41 | ::: info Note 42 | You can choose any vendor, that provides a Buy Now URL service and use the generated links in your products. The Apostrophe CMS E-commerce Starter Kit does not provide any e-commerce functionality, it's just a starter kit for building e-commerce websites. 43 | ::: 44 | 45 | ### Related Products 46 | 47 | The product view page shows a list of related products. This is a fully automated section in matter of filtering. It will show up to 4 other products that have at least one tag in common with the viewed product. You can add tags to the product via the "Tags" field. 48 | 49 | ## Custom Pages and Widgets 50 | 51 | You can turn any custom page into a product landing page. You can learn more about custom pages in the [Custom Pages section](./custom-pages.md). Find out more about widgets in the [dedicated section](./widgets.md). 52 | -------------------------------------------------------------------------------- /docs/.vitepress/config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitepress'; 2 | 3 | // Settings 4 | const project = 'starter-kit-ecommerce'; 5 | const repo = `apostrophecms/${project}`; 6 | // Settings end 7 | 8 | // https://vitepress.dev/reference/site-config 9 | export default defineConfig({ 10 | base: `/${project}/`, 11 | title: 'Starter Kit', 12 | description: 'Apostrophe CMS Starter Kit for e-commerce projects, built with Tailwind CSS.', 13 | appearance: true, 14 | themeConfig: { 15 | editLink: { 16 | pattern: `https://github.com/${repo}/edit/main/docs/:path` 17 | }, 18 | logo: { 19 | dark: '/images/logo-on-dark.svg', 20 | light: '/images/logo-on-light.svg', 21 | alt: 'Apostrophe CMS' 22 | }, 23 | // https://vitepress.dev/reference/default-theme-config 24 | nav: [ 25 | { 26 | text: 'User Guide', 27 | link: '/user/', 28 | activeMatch: '/user/' 29 | }, 30 | { 31 | text: 'Developer Guide', 32 | link: '/developer/', 33 | activeMatch: '/developer/' 34 | } 35 | ], 36 | 37 | sidebar: { 38 | '/user/': [ 39 | { 40 | text: 'User Guide', 41 | collapsed: false, 42 | items: [ 43 | { 44 | text: 'Introduction', 45 | link: '/user/' 46 | }, 47 | { 48 | text: 'Getting Started', 49 | link: '/user/getting-started' 50 | }, 51 | { 52 | text: 'Products & Categories', 53 | link: '/user/products-and-categories' 54 | }, 55 | { 56 | text: 'Custom Pages', 57 | link: '/user/custom-pages' 58 | }, 59 | { 60 | text: 'Widgets', 61 | link: '/user/widgets' 62 | }, 63 | { 64 | text: 'Search & SEO', 65 | link: '/user/search-and-seo' 66 | } 67 | ] 68 | } 69 | ], 70 | '/developer/': [ 71 | { 72 | text: 'Developer Guide', 73 | collapsed: false, 74 | items: [ 75 | { 76 | text: 'Introduction', 77 | link: '/developer/' 78 | }, 79 | { 80 | text: 'Getting Started', 81 | link: '/developer/getting-started' 82 | }, 83 | { 84 | text: 'Branding & UI', 85 | link: '/developer/branding-and-ui' 86 | }, 87 | { 88 | text: 'Modules & Widgets', 89 | link: '/developer/modules-and-widgets' 90 | }, 91 | { 92 | text: 'Design System', 93 | link: '/developer/design-system' 94 | }, 95 | { 96 | text: 'Resources', 97 | link: '/developer/resources' 98 | } 99 | ] 100 | } 101 | ] 102 | }, 103 | 104 | socialLinks: [ 105 | { 106 | icon: 'github', 107 | link: `https://github.com/${repo}` 108 | } 109 | ] 110 | }, 111 | markdown: { 112 | languages: [ 113 | { 114 | id: 'njk-html', 115 | scopeName: 'text.html.njk', 116 | grammar: require('./njk-html.tmLanguage.json'), 117 | displayName: 'Nunjucks', 118 | embeddedLangs: [ 'html' ], 119 | aliases: [ 'njk', 'nunjucks' ] 120 | } 121 | ] 122 | } 123 | }); 124 | -------------------------------------------------------------------------------- /modules/theme/views/design-system/hero-widget/full.njk: -------------------------------------------------------------------------------- 1 | {% extends dsPreview %} 2 | {% import "theme:hero-widget.html" as hero %} 3 | {% import "theme:helpers.html" as helpers %} 4 | 5 | {% set storyData = apos.dsp.configData('molecules-hero-widget').full %} 6 | 7 | {% block tagline %} 8 | Hero Full 9 | {% endblock %} 10 | 11 | {% block preview %} 12 | {% rendercall helpers.container() %} 13 | 14 | {% rendercall hero.full(storyData) %} 15 |

some text here

16 |

some text here

17 | {% endrendercall %} 18 | 19 | {% endrendercall %} 20 | {% endblock %} 21 | 22 | {% block variants %} 23 | {# container #} 24 | {% rendercall helpers.container() %} 25 | 26 | {# Code grid - 2 cols #} 27 |
28 | {% dscode 'njk' %} 29 | {% import "theme:hero-widget.html" as hero %} 30 | {# Full page hero-widget, default - with dark background filter #} 31 | {% rendercall hero.full(storyData) %} 32 |

some text here

33 |

some text here

34 | {% endrendercall %} 35 | {% enddscode %} 36 | 37 | {% dscode 'json', label = 'Data', parse = true %}{{ storyData | jsonPretty }}{% enddscode %} 38 |
39 | 40 |

Full page hero-widget

41 |

With light background filter, when dark: false

42 | {% rendercall hero.full(storyData | merge ({ dark: false })) %} 43 |

New arrival from our collections

44 |

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut magna, scelerisque vitae augue et, cursus placerat lorem. In nisi lacus, eleifend tincidunt quam et, consequat semper.Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut magna, scelerisque vitae augue et, cursus placerat lorem. In nisi lacus, eleifend tincidunt quam et, consequat semper.Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut magna, scelerisque vitae augue et, cursus placerat lorem. In nisi lacus, eleifend tincidunt quam et, consequat semper. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut magna, scelerisque vitae augue et, cursus placerat lorem. In nisi lacus, eleifend tincidunt quam et, consequat semper. 45 |

46 | {% endrendercall %} 47 |
48 | {% dscode 'njk' %} 49 | {% import "theme:hero-widget.html" as hero %} 50 | {# Full page hero-widget with light background filter when dark:false #} 51 | {% rendercall hero.full(storyData | merge ({ dark: false })) %} 52 |

New arrival from our collections

53 |

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut magna, scelerisque vitae augue et, cursus placerat lorem. In nisi lacus, eleifend tincidunt quam et, consequat semper.Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut magna, scelerisque vitae augue et, cursus placerat lorem. In nisi lacus, eleifend tincidunt quam et, consequat semper.Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut magna, scelerisque vitae augue et, cursus placerat lorem. In nisi lacus, eleifend tincidunt quam et, consequat semper. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut magna, scelerisque vitae augue et, cursus placerat lorem. In nisi lacus, eleifend tincidunt quam et, consequat semper. 54 |

55 | {% endrendercall %} 56 | {% enddscode %} 57 | 58 | {% dscode 'json', label = 'Data', parse = true %}{{ story.data | jsonPretty }}{% enddscode %} 59 |
60 | 61 | {# ensure the story finishes with a nice spacing #} 62 |
63 | 64 | {% endrendercall %} 65 | {% endblock %} 66 | -------------------------------------------------------------------------------- /modules/theme/views/design-system/hero-widget/split.njk: -------------------------------------------------------------------------------- 1 | {% extends dsPreview %} 2 | {% import "theme:hero-widget.html" as hero %} 3 | {% import "theme:helpers.html" as helpers %} 4 | 5 | {% set storyData = apos.dsp.configData('molecules-hero-widget').split %} 6 | 7 | {% block tagline %} 8 | Hero Split 9 | {% endblock %} 10 | 11 | {% block preview %} 12 | {% rendercall helpers.container() %} 13 | 14 | {% rendercall hero.split(storyData) %} 15 |

some text here

16 |

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut magna, scelerisque vitae augue et, cursus placerat lorem. In nisi lacus, eleifend tincidunt quam et, consequat semper. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut magna, scelerisque vitae augue et, cursus placerat lorem. In nisi lacus, eleifend tincidunt quam et, consequat semper.

17 | {% endrendercall %} 18 | 19 | {% endrendercall %} 20 | {% endblock %} 21 | 22 | {% block variants %} 23 | {# container #} 24 | {% rendercall helpers.container() %} 25 | 26 | {# Code grid - 2 cols #} 27 |
28 | {% dscode 'njk' %} 29 | {% import "theme:hero-widget.html" as hero %} 30 | {# Default category card with all data #} 31 | {% render hero.split(storyData) %} 32 | {% enddscode %} 33 | 34 | {% dscode 'json', label = 'Data', parse = true %}{{ storyData | jsonPretty }}{% enddscode %} 35 |
36 | 37 |

Hero Widget with image on the right

38 |

The image position will be on the right, if imagePosition: 'right'.

39 | {% rendercall hero.split(storyData | merge ({ imagePosition: 'right' })) %} 40 |

Hero Split Widget

41 |

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut magna, scelerisque vitae augue et, cursus placerat lorem. In nisi lacus, eleifend tincidunt quam et, consequat semper. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut magna, scelerisque vitae augue et, cursus placerat lorem. In nisi lacus, eleifend tincidunt quam et, consequat semper. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut magna, scelerisque vitae augue et, cursus placerat lorem. In nisi lacus, eleifend tincidunt quam et, consequat semper. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut magna, scelerisque vitae augue et, cursus placerat lorem. In nisi lacus, eleifend tincidunt quam et, consequat semper. 42 |

43 | {% endrendercall %} 44 |
45 | {% dscode 'njk' %} 46 | {% import "theme:hero-widget.html" as hero %} 47 | {# Hero Widget with image on the right, with even ratio "image-content" #} 48 | {% rendercall hero.split(storyData | merge ({ imagePosition: 'right' }), { even: true }) %} 49 |

HERO SPLIT

50 |

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut magna, scelerisque vitae augue et, cursus placerat lorem. In nisi lacus, eleifend tincidunt quam et, consequat semper. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut magna, scelerisque vitae augue et, cursus placerat lorem. In nisi lacus, eleifend tincidunt quam et, consequat semper. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut magna, scelerisque vitae augue et, cursus placerat lorem. In nisi lacus, eleifend tincidunt quam et, consequat semper. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut magna, scelerisque vitae augue et, cursus placerat lorem. In nisi lacus, eleifend tincidunt quam et, consequat semper. 51 |

52 | {% endrendercall %} 53 | {% enddscode %} 54 | 55 | {% dscode 'json', label = 'Data', parse = true %}{{ storyData | jsonPretty }}{% enddscode %} 56 |
57 | 58 | {# ensure the story finishes with a nice spacing #} 59 |
60 | 61 | {% endrendercall %} 62 | {% endblock %} 63 | -------------------------------------------------------------------------------- /modules/theme/views/design-system/hero-widget/data.json: -------------------------------------------------------------------------------- 1 | { 2 | "full": { 3 | "caption": "Hero Widget-Full Caption", 4 | "ctaPrimary": 5 | { 6 | "label": "Shop Now", 7 | "urlType": "custom", 8 | "url": "#urlCtaPrimary" 9 | } 10 | , 11 | "ctaOutlined": { 12 | "label": "Explore", 13 | "urlType": "page", 14 | "_page": [ 15 | { 16 | "_url": "#_page_urlCtaOutlined" 17 | } 18 | ] 19 | }, 20 | "dark": true, 21 | "_image": [ 22 | { 23 | "_id": "clgyx91c9000s3eh9b9ji3dto:en:draft", 24 | "attachment": { 25 | "_id": "clgyx90ed000p3eh97uy897xp", 26 | "group": "images", 27 | "name": "some-hero", 28 | "title": "some hero", 29 | "extension": "jpg", 30 | "type": "attachment", 31 | "width": 1800, 32 | "height": 1350, 33 | "_urls": { 34 | "max": "https://picsum.photos/seed/hero/1600/1200", 35 | "full": "https://picsum.photos/seed/hero/1140/850", 36 | "two-thirds": "https://picsum.photos/seed/hero/760/570", 37 | "one-half": "https://picsum.photos/seed/hero/570/428", 38 | "one-third": "https://picsum.photos/seed/hero/380/285", 39 | "one-sixth": "https://picsum.photos/seed/hero/190/143", 40 | "original": "https://picsum.photos/seed/hero/1800/1350" 41 | }, 42 | "srcset": "https://picsum.photos/seed/hero/1600/1200 1600w, https://picsum.photos/seed/hero/1140/850 1140w, https://picsum.photos/seed/hero/760/570 760w, https://picsum.photos/seed/hero/570/428 570w, https://picsum.photos/seed/hero/380/285 380w, https://picsum.photos/seed/hero/190/143 190w" 43 | 44 | }, 45 | "title": "Hero Widget image", 46 | "alt": "Hero Widget alt", 47 | "slug": "image-laptop2", 48 | "archived": false, 49 | "type": "@apostrophecms/image", 50 | "aposLocale": "en:draft", 51 | "aposMode": "draft", 52 | "aposDocId": "clgyx91c9000s3eh9b9ji3dto", 53 | "metaType": "doc", 54 | "visibility": "public" 55 | } 56 | ] 57 | }, 58 | "split": { 59 | "caption": "Hero Widget-Split Caption", 60 | "ctaPrimary": 61 | { 62 | "label": "Shop Now", 63 | "urlType": "custom", 64 | "url": "#urlCtaPrimary" 65 | } 66 | , 67 | "ctaOutlined": { 68 | "label": "Explore", 69 | "urlType": "page", 70 | "_page": [ 71 | { 72 | "_url": "#_page_urlCtaOutlined" 73 | } 74 | ] 75 | }, 76 | "imagePosition": "left", 77 | "_image": [ 78 | { 79 | "_id": "clgyx91c9000s3eh9b9ji3dto:en:draft", 80 | "attachment": { 81 | "_id": "clgyx90ed000p3eh97uy897xp", 82 | "group": "images", 83 | "name": "some-hero", 84 | "title": "some hero", 85 | "extension": "jpg", 86 | "type": "attachment", 87 | "width": 1800, 88 | "height": 1350, 89 | "_urls": { 90 | "max": "https://picsum.photos/seed/hero/1600/1200", 91 | "full": "https://picsum.photos/seed/hero/1140/850", 92 | "two-thirds": "https://picsum.photos/seed/hero/760/570", 93 | "one-half": "https://picsum.photos/seed/hero/570/428", 94 | "one-third": "https://picsum.photos/seed/hero/380/285", 95 | "one-sixth": "https://picsum.photos/seed/hero/190/143", 96 | "original": "https://picsum.photos/seed/hero/1800/1350" 97 | }, 98 | "srcset": "https://picsum.photos/seed/hero/1600/1200 1600w, https://picsum.photos/seed/hero/1140/850 1140w, https://picsum.photos/seed/hero/760/570 760w, https://picsum.photos/seed/hero/570/428 570w, https://picsum.photos/seed/hero/380/285 380w, https://picsum.photos/seed/hero/190/143 190w" 99 | 100 | }, 101 | "title": "Hero Widget image", 102 | "alt": "Hero Widget alt", 103 | "slug": "image-laptop2", 104 | "archived": false, 105 | "type": "@apostrophecms/image", 106 | "aposLocale": "en:draft", 107 | "aposMode": "draft", 108 | "aposDocId": "clgyx91c9000s3eh9b9ji3dto", 109 | "metaType": "doc", 110 | "visibility": "public" 111 | } 112 | ] 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /docs/user/getting-started.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Getting started 3 | titleTemplate: User Guide 4 | --- 5 | 6 | # {{ $frontmatter.title }} 7 | 8 | This section contains a list of tips and recommendations as first steps in building your site content. 9 | 10 | ## Before you start 11 | 12 | Take a moment to think about the structure of your product catalog and generally - your site. Outline the pages needed, beside the essential product related ones. 13 | 14 | ### Content taxonomy 15 | 16 | The tag based product taxonomy requires a bit of planning. You need to understand how it works first. The taxonomy is based entirely on tags. Tags are controlled via the "Content - Content Tags" administration menu. Currently they are used to: 17 | - filter products in the Products widget 18 | - assign products to categories 19 | 20 | Here is how product categories work: 21 | - when you create a product, you assign any number of tags to it 22 | - when you create a product category, you also assign any number of tags to it 23 | - when browsing a category page, only products that have at least one tag in common with the category are shown 24 | 25 | Additionally, when adding a product widget to any page, you can filter the products shown by up to ten tags. This allows you to create landing pages for promotions, or just to show a subset of products on any page. 26 | 27 | Based on that knowledge, you need to decide on the following: 28 | - what are the product categories 29 | - what tags will be used in the product categories in order to filter products for each category 30 | - what tags will be used to filter products in the Products widget, thus on the Home page and the eventual future landing (promo) pages 31 | 32 | The output of this planning should be a list of essential tags, that can be created in advance. This will allow you to assign them to products and categories as you create them, thus saving time and effort. It's also a good way to further scale your product catalog in the future. 33 | 34 | For example, you might have tags "Kids", "Shoes" and "September Sale". This would allow you to have product categories such as "Kids", "Shoes" and/or "Kids Shoes". You can also create a landing page for the September Sale by adding a Products widget to a Custom Page, and filtering the products by the "September Sale" tag. 35 | 36 | If the appropriate tags are added to the products in advance (while creating them), you'll save a lot of forth and back when further arranging your site content. 37 | 38 | ## Essential Pages 39 | 40 | The following pages are essential for your site to function properly, and should be created as a first step: 41 | 42 | - **Products Page**: this page is the landing page for your products. You need to create it once, using the "Product" type from the Type dropdown in the page editor. 43 | - **Product Category Page**: this page is the landing page for your product categories. If you don't plan to use categories, you can skip this step. Otherwise, you need to create it once, using the "Product Category" type from the Type dropdown in the page editor. 44 | 45 | Learn more about products and categories in the [dedicated section](./products-and-categories.md). 46 | 47 | You might also want to create placeholders (empty pages) for any other content you would initially like to have on your site. For example "About Us", "Contact Us", "Terms and Conditions", etc. You can create them as Custom Pages, and add content to them later. You can create a custom page by selecting the "Default" type from the Type dropdown in the page editor. Learn more about custom pages in the [dedicated section](./custom-pages.md). 48 | 49 | ## Configuration 50 | 51 | Use the top right configuration (cog) button to access the site configuration. Here you can: 52 | - create and manage the main navigation menu 53 | - control various header related features 54 | - create and manage the footer navigation menu 55 | - add and manage social links (shown in the footer) 56 | - brand your site with a brand name, logo and a favicon 57 | 58 | ## Home Page 59 | 60 | You should edit your Home page after your entire site content (and most importantly - your [product catalog and categories](./products-and-categories.md)) is in place. It is managed in a similar to a [custom page](./custom-pages.md) way. 61 | 62 | ## SEO 63 | 64 | Add the SEO related configuration while creating your content. Learn more in the [dedicated section](./search-and-seo.md). 65 | -------------------------------------------------------------------------------- /modules/theme/views/design-system/button/icon.njk: -------------------------------------------------------------------------------- 1 | {% extends dsPreview %} 2 | 3 | {# The button macro #} 4 | {% import "theme:button.html" as buttons %} 5 | 6 | 7 | {# The component tagline #} 8 | {% block tagline %} 9 | Icon buttons 10 | {% endblock %} 11 | 12 | {# Preview of the component inside of `previewSimple` or `preview` block #} 13 | {% block preview %} 14 |
15 | {{ buttons.icon('heart', size = 'xl') }} 16 |
17 |
18 | {{ buttons.icon('heart', size = 'xl', { onDark: true }) }} 19 |
20 | 21 | {% endblock %} 22 | 23 | {# The code block, followed by other variations of the component (if any). #} 24 | {% block variants %} 25 | {# container #} 26 |
27 | 28 | {# Section code example #} 29 | {% dscode 'njk' %} 30 | {% import "theme:button.html" as buttons %} 31 | {# Icon button #} 32 | {{ buttons.icon('heart', size = 'xl') }} 33 | {# Icon button on dark background #} 34 | {{ buttons.icon('heart', size = 'xl', { onDark: true }) }} 35 | {% enddscode %} 36 | 37 | {# Variant disabled #} 38 |

Disabled

39 |
40 | {# Icon button, disabled #} 41 | {{ buttons.icon('heart', size = 'xl', { disabled: true }) }} 42 |
43 |
44 | {# Icon button, disabled, onDark #} 45 | {{ buttons.icon('heart', size = 'xl', { onDark: true, disabled: true }) }} 46 |
47 | {% dscode 'njk' %} 48 | {% import "theme:button.html" as buttons %} 49 | {{ buttons.icon('heart', size = 'xl', { disabled: true }) }} 50 | {{ buttons.icon('heart', size = 'xl', { onDark: true, disabled: true }) }} 51 | {% enddscode %} 52 | 53 | {# Variant CTA #} 54 |

CTA

55 |
56 |
57 | {# Icon button as a link with href #} 58 | {{ buttons.icon('heart', size = 'xl', href = '#') }} 59 |
60 |
61 | {% dscode 'njk' %} 62 | {% import "theme:button.html" as buttons %} 63 | {# Icon button as a link with href #} 64 | {{ buttons.icon('heart', size = 'xl', href = '#') }} 65 | {% enddscode %} 66 | 67 | {# Variant standard attributes #} 68 |

HTML attributes

69 |
70 | {{ buttons.icon('heart', size = 'xl', href = '#', { 71 | attr: { name: 'my-button', id: 'my-button-id' } 72 | }) }} 73 |
74 | {% dscode 'njk' %} 75 | {% import "theme:button.html" as buttons %} 76 | {{ buttons.icon('heart', size = 'xl', href = '#', { 77 | attr: { name: 'my-button', id: 'my-button-id' } 78 | }) }} 79 | {% enddscode %} 80 | 81 | {# Variant aria attributes #} 82 |

Aria attributes

83 |
84 | {{ buttons.icon('heart', size = 'xl', { 85 | aria: { labelledby: 'button-aria-label', hidden: 'false' } 86 | }) }} 87 |
88 | {% dscode 'njk' %} 89 | {% import "theme:button.html" as buttons %} 90 | {# aria-labeledby - points to the element with ID which is the label #} 91 | {# aria-hidden like all aria booleans should have STRING "true" #} 92 | {{ buttons.icon('heart', size = 'xl', { 93 | aria: { labelledby: 'button-aria-label', hidden: 'false' } 94 | }) }} 95 | {% enddscode %} 96 | 97 | {# Variant data attributes #} 98 |

Data attributes

99 |
100 | {{ buttons.icon('heart', size = 'xl', { 101 | data: { my: 'data value', secret: true } 102 | }) }} 103 |
104 | {% dscode 'njk' %} 105 | {% import "theme:button.html" as buttons %} 106 | {{ buttons.icon('heart', size = 'xl', { 107 | data: { my: 'data value', secret: true } 108 | }) }} 109 | {% enddscode %} 110 |
111 | {% endblock %} 112 | -------------------------------------------------------------------------------- /modules/theme/views/header.html: -------------------------------------------------------------------------------- 1 | {% import "theme:logo.html" as logo %} 2 | {% import "theme:button.html" as buttons %} 3 | {% import "theme:helpers.html" as helpers %} 4 | 5 | {# For the schema of the `config` parameter see footer.json #} 6 | {% fragment render(config, homeUrl = '/', currentUrl = '') %} 7 | {%- set headerCtaIcon = apos.theme.navItems([config.headerCtaIcon]) | first -%} 8 | {%- set headerCtaIconUrl = headerCtaIcon.url -%} 9 | {%- set headerCtaButton = apos.theme.navItems([config.headerCtaButton]) | first -%} 10 | {%- set headerCtaButtonUrl = headerCtaButton.url -%} 11 | {%- set searchUrl = (apos.theme.navItems([config.searchUrl]) | first).url -%} 12 | {%- set mainNav = apos.theme.navItems(config.headerNav, currentUrl) -%} 13 | {%- set logoAttachment = apos.image.first(config.logo or {}) -%} 14 | {%- rendercall helpers.container() -%} 15 |
16 | {# Responsive logo #} 17 | {{ logo.render(logoAttachment, alt = __t('app:brandHome', { value: config.brandName })) }} 18 | 19 | {# Main desktop navigation #} 20 | {%- render _mainNav(mainNav, cls = 'hidden lg:block lg:mx-4') -%} 21 | 22 | {# Header actions #} 23 |
24 | {%- if searchUrl -%} 25 | {{ buttons.icon( 26 | 'magnifying-glass', 27 | label = __t('app:search'), 28 | href = searchUrl, 29 | { cls: 'text-gray-600' } 30 | ) }} 31 | {%- endif -%} 32 | {%- if headerCtaIconUrl -%} 33 | {{ buttons.icon( 34 | headerCtaIcon.icon, 35 | label = headerCtaIcon.label, 36 | href = headerCtaIconUrl, 37 | { cls: 'text-gray-600' } 38 | ) }} 39 | {%- endif -%} 40 | {%- if headerCtaButtonUrl -%} 41 | {{ buttons.primary( 42 | headerCtaButton.label, 43 | href = headerCtaButtonUrl, 44 | { cls: 'hidden lg:inline-block ml-2' } 45 | ) }} 46 | {%- endif -%} 47 | {{ buttons.icon('bars-3', { 48 | cls: ' lg:hidden text-gray-600', 49 | data: { 50 | target: '#main-menu-mobile-popup' 51 | }, 52 | aria: { 53 | label: __t('app:mainMenu'), 54 | expanded: 'false', 55 | controls: 'main-menu-mobile' 56 | }, 57 | attr: { 58 | onclick: 'apos.modules.theme.mobileNavToggle(this)' 59 | } 60 | }) }} 61 |
62 | 63 | {# Mobile navigation popup #} 64 | 75 |
76 | {% endrendercall %} 77 | {% endfragment %} 78 | 79 | {% fragment _mainNav(navData, id = 'main-menu', hidden = '', cls = '') %} 80 | 94 | {% endfragment %} 95 | -------------------------------------------------------------------------------- /docs/developer/getting-started.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Getting Started 3 | titleTemplate: Developer 4 | --- 5 | 6 | # {{ $frontmatter.title }} 7 | 8 | The e-commerce starter application aims to deliver a good starting point of Apostrophe CSM based e-commerce solutions, using the wide variety of features and techiques prvodied by the Apostrophe core. 9 | 10 | ## Try it on Codespaces 11 | 12 | You can try the starter kit on GitHub Codespaces. On the main page of the `apostrophecms/starter-kit-ecommerce` repository, under the "Template" dropdown, choose "Open in a codespace": 13 | 14 | ![Try it on Codespaces](../images/use-codespaces.webp) 15 | 16 | You can find a detailed guide at using. 17 | 18 | ::: tip 19 | When codespaces is initialized for a first time, it will take some time to install the dependencies and build the application. You can check the progress in the "Terminal" tab. Once the application is built, you can start the application via `npm run dev`. 20 | 21 | You may see an initial screen of the MongoDB extensions. You can safely ignore it and close the tab. No configuration is needed for the application to run. 22 | ::: 23 | 24 | ## Local Installation 25 | 26 | ::: tip 27 | You will need the proper Node.js environment, a MongoDB up and running, etc. Follow the [official documentation](https://v3.docs.apostrophecms.org/guide/setting-up.html) for more information. 28 | ::: 29 | 30 | ### Method 1 31 | 32 | 1. If you haven't already, install the [Apostrophe CLI tool](https://docs.apostrophecms.org/guide/setting-up.html#the-apostrophe-cli-tool) using 33 | 34 | ``` sh 35 | npm install -g @apostrophecms/cli 36 | ``` 37 | 38 | 2. Clone the project locally by navigating to the parent directory you want the repo installed within and run the command: 39 | 40 | ``` sh 41 | apos create my-project-name --starter=ecommerce 42 | ``` 43 | 44 | 3. Change to the new directory and run it: 45 | 46 | ``` sh 47 | npm run dev 48 | ``` 49 | 50 | ### Method 2 51 | 52 | 1. Choose "Use this template" button on the main repository page and create your own repository: 53 | 54 | ![Use Template GitHub](../images/use-gh-template.png) 55 | 56 | 2. Clone it (replace `user/repo-name` as appropriate) 57 | 58 | ```sh 59 | git clone git@github.com:user/repo-name.git 60 | ``` 61 | 62 | 3. Install the dependencies 63 | 64 | ```sh 65 | npm install 66 | ``` 67 | 68 | Optionally, you can update the core dependencies to the latest versions: 69 | 70 | ```sh 71 | npm update 72 | ``` 73 | 74 | 4. Run it 75 | 76 | ```sh 77 | npm run dev 78 | ``` 79 | 80 | ## Initial configuration 81 | 82 | - Change the `shortName` value in your configuration in `app.js` note that installation using the CLI will complete this step automatically. 83 | 84 | ```js 85 | import apostrophe from 'apostrophe'; 86 | 87 | apostrophe({ 88 | root: import.meta, 89 | shortName: 'my-project-name', 90 | // ...rest of the configuration 91 | }); 92 | ``` 93 | 94 | - The control for restarting the application on change (dev mode only) has been moved to `modules/theme/index.js` compared to the default `starter-kit-essentials` application (`asset` module). If you want to disable this feature: 95 | 96 | ```js 97 | // modules/theme/index.js 98 | handlers(self) { 99 | return { 100 | '@apostrophecms/page:beforeSend': { 101 | webpack(req) { 102 | req.data.isDev = (process.env.NODE_ENV !== 'production'); // [!code --] 103 | req.data.isDev = false; // [!code ++] 104 | } 105 | } 106 | }; 107 | }, 108 | ``` 109 | 110 | ## Production 111 | 112 | You need to build the application and execute the migrations when starting it in production: 113 | 114 | ```sh 115 | npm run release 116 | npm run start 117 | ``` 118 | 119 | ## Architecture 120 | 121 | The starter kit is a standard Apostrophe CMS application, built on top of the official [`starter-kit-essentials`](https://github.com/apostrophecms/starter-kit-essentials) template. All the modules (including widgets) can be found in the `modules/` folder. You can learn more about the modules in the [Modules & Widgets](./modules-and-widgets.md) section. 122 | 123 | The entire UI (except the product view page) lives in the `theme` module. The application uses a standard Tailwind CSS configuration. You can learn more in the [Branding & UI](./branding-and-ui.md) section. 124 | 125 | We have developed a tool for automated SVG sprites. It's seamlessly integrated and is explained in detail in the [Branding & UI](./branding-and-ui.md) section. 126 | -------------------------------------------------------------------------------- /modules/theme/views/design-system/button/primary.njk: -------------------------------------------------------------------------------- 1 | {% extends dsPreview %} 2 | 3 | {# The button macro #} 4 | {% import "theme:button.html" as buttons %} 5 | 6 | 7 | {# The component tagline #} 8 | {% block tagline %} 9 | Primary buttons 10 | {% endblock %} 11 | 12 | {# Preview of the component inside of `previewSimple` or `preview` block #} 13 | {% block preview %} 14 |
15 | {{ buttons.primary('Default') }} 16 | {{ buttons.primary('Large', { large: true }) }} 17 |
18 |
19 | {{ buttons.primary('Default', { onDark: true }) }} 20 | {{ buttons.primary('Large', { large: true, onDark:true }) }} 21 |
22 | 23 | {% endblock %} 24 | 25 | {# The code block, followed by other variations of the component (if any). #} 26 | {% block variants %} 27 | {# container #} 28 |
29 | 30 | {# Section code example #} 31 | {% dscode 'njk' %} 32 | {% import "theme:button.html" as buttons %} 33 | {# primary button #} 34 | {{ buttons.primary('Default') }} 35 | {# primary large button #} 36 | {{ buttons.primary('Large', { large: true }) }} 37 | 38 | {# primary button on dark background #} 39 | {{ buttons.primary('Default', { onDark: true }) }} 40 | {# primary large button on dark background #} 41 | {{ buttons.primary('Large', { large: true, onDark:true }) }} 42 | {% enddscode %} 43 | 44 | {# Variant disabled #} 45 |

Disabled

46 |
47 | {# primary button, disabled #} 48 | {{ buttons.primary('Disabled', { disabled: true }) }} 49 |
50 |
51 | {# primary button, disabled, on dark background #} 52 | {{ buttons.primary('Disabled', { onDark: true, disabled: true }) }} 53 |
54 | {% dscode 'njk' %} 55 | {% import "theme:button.html" as buttons %} 56 | {{ buttons.primary('Disabled', { disabled: true }) }} 57 | {{ buttons.primary('Disabled', { onDark: true, disabled: true }) }} 58 | {% enddscode %} 59 | 60 | {# Variant CTA #} 61 |

CTA

62 |
63 |
64 | {{ buttons.primary('Default', href = '#') }} 65 |
66 |
67 | {% dscode 'njk' %} 68 | {% import "theme:button.html" as buttons %} 69 | {{ buttons.primary('Default', href = '#') }} 70 | {% enddscode %} 71 | 72 | {# Variant standard attributes #} 73 |

HTML attributes

74 |
75 | {{ buttons.primary('With name and ID', href = '#', { 76 | attr: { name: 'my-button', id: 'my-button-id' } 77 | }) }} 78 |
79 | {% dscode 'njk' %} 80 | {% import "theme:button.html" as buttons %} 81 | {{ buttons.primary('With name and ID', { 82 | attr: { name: 'my-button', id: 'my-button-id' } 83 | }) }} 84 | {% enddscode %} 85 | 86 | {# Variant aria attributes #} 87 |

Aria attributes

88 |
89 | {{ buttons.primary('Aria Labeled By', { 90 | aria: { labelledby: 'button-aria-label', hidden: 'false' } 91 | }) }} 92 |
93 | {% dscode 'njk' %} 94 | {% import "theme:button.html" as buttons %} 95 | {# aria-labeledby - points to the element with ID which is the label #} 96 | {# aria-hidden like all aria booleans should have STRING "true" #} 97 | {{ buttons.primary('Aria Labeled By', { 98 | aria: { labelledby: 'button-aria-label', hidden: 'false' } 99 | }) }} 100 | {% enddscode %} 101 | 102 | {# Variant data attributes #} 103 |

Data attributes

104 |
105 | {{ buttons.primary('My data value', { 106 | data: { my: 'data value', secret: true } 107 | }) }} 108 |
109 | {% dscode 'njk' %} 110 | {% import "theme:button.html" as buttons %} 111 | {{ buttons.primary('My data value', { 112 | data: { my: 'data value', secret: true } 113 | }) }} 114 | {% enddscode %} 115 |
116 | 117 | {# ensure the story finishes with a nice spacing #} 118 |
119 | {% endblock %} 120 | -------------------------------------------------------------------------------- /modules/theme/views/design-system/button/outlined.njk: -------------------------------------------------------------------------------- 1 | {% extends dsPreview %} 2 | 3 | {# The button macro #} 4 | {% import "theme:button.html" as buttons %} 5 | 6 | 7 | {# The component tagline #} 8 | {% block tagline %} 9 | Outlined buttons 10 | {% endblock %} 11 | 12 | {# Preview of the component inside of `previewSimple` or `preview` block #} 13 | {% block preview %} 14 |
15 | {{ buttons.outlined('Default') }} 16 | {{ buttons.outlined('Large', { large: true }) }} 17 |
18 |
19 | {{ buttons.outlined('Default', { onDark: true }) }} 20 | {{ buttons.outlined('Large', { large: true, onDark: true }) }} 21 |
22 | 23 | {% endblock %} 24 | 25 | {# The code block, followed by other variations of the component (if any). #} 26 | {% block variants %} 27 | {# container #} 28 |
29 | 30 | {# Section code example #} 31 | {% dscode 'njk' %} 32 | {% import "theme:button.html" as buttons %} 33 | {# outlined button #} 34 | {{ buttons.outlined('Default') }} 35 | {# outlined large button #} 36 | {{ buttons.outlined('Large', { large: true }) }} 37 | 38 | {# outlined button on dark background #} 39 | {{ buttons.outlined('Default', { onDark: true }) }} 40 | {# outlined large button on dark background #} 41 | {{ buttons.outlined('Large', { large: true, onDark: true }) }} 42 | {% enddscode %} 43 | 44 | {# Variant disabled #} 45 |

Disabled

46 |
47 | {# outlined button, disabled #} 48 | {{ buttons.outlined('Disabled', { disabled: true }) }} 49 |
50 |
51 | {# outlined button, disabled, onDark #} 52 | {{ buttons.outlined('Disabled', { onDark: true, disabled: true }) }} 53 |
54 | {% dscode 'njk' %} 55 | {% import "theme:button.html" as buttons %} 56 | {{ buttons.outlined('Disabled', { disabled: true }) }} 57 | {{ buttons.outlined('Disabled', { onDark: true, disabled: true }) }} 58 | {% enddscode %} 59 | 60 | {# Variant CTA #} 61 |

CTA

62 |
63 |
64 | {{ buttons.outlined('I am CTA!', href = '#', { disabled: true }) }} 65 |
66 |
67 | {% dscode 'njk' %} 68 | {% import "theme:button.html" as buttons %} 69 | {{ buttons.outlined('Default', href = '#') }} 70 | {% enddscode %} 71 | 72 | {# Variant standard attributes #} 73 |

HTML attributes

74 |
75 | {{ buttons.outlined('With name and ID', href = '#', { 76 | attr: { name: 'my-button', id: 'my-button-id' } 77 | }) }} 78 |
79 | {% dscode 'njk' %} 80 | {% import "theme:button.html" as buttons %} 81 | {{ buttons.outlined('With name and ID', { 82 | attr: { name: 'my-button', id: 'my-button-id' } 83 | }) }} 84 | {% enddscode %} 85 | 86 | {# Variant aria attributes #} 87 |

Aria attributes

88 |
89 | {{ buttons.outlined('Aria Labeled By', { 90 | aria: { labelledby: 'button-aria-label', hidden: 'false' } 91 | }) }} 92 |
93 | {% dscode 'njk' %} 94 | {% import "theme:button.html" as buttons %} 95 | {# aria-labeledby - points to the element with ID which is the label #} 96 | {# aria-hidden like all aria booleans should have STRING "true" #} 97 | {{ buttons.outlined('Aria Labeled By', { 98 | aria: { labelledby: 'button-aria-label', hidden: 'false' } 99 | }) }} 100 | {% enddscode %} 101 | 102 | {# Variant data attributes #} 103 |

Data attributes

104 |
105 | {{ buttons.outlined('My data value', { 106 | data: { my: 'data value', secret: true } 107 | }) }} 108 |
109 | {% dscode 'njk' %} 110 | {% import "theme:button.html" as buttons %} 111 | {{ buttons.outlined('My data value', { 112 | data: { my: 'data value', secret: true } 113 | }) }} 114 | {% enddscode %} 115 |
116 | 117 | {# ensure the story finishes with a nice spacing #} 118 |
119 | {% endblock %} 120 | -------------------------------------------------------------------------------- /modules/i18n/i18n/app/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "currency": "${{value}}", 3 | "title": "Title", 4 | "default": "Default", 5 | "home": "Home", 6 | "subtitle": "Subtitle", 7 | "sectionTitle": "Section Title", 8 | "tagline": "Tagline", 9 | "name": "Name", 10 | "position": "Position", 11 | "image": "Image", 12 | "companyLogo": "Company Logo", 13 | "label": "Label", 14 | "value": "Value", 15 | "type": "Type", 16 | "limit": "Limit", 17 | "headerType": "Header Type", 18 | "branding": "Branding", 19 | "brandName": "Brand Name", 20 | "brandHome": "{{ value }} Home", 21 | "homePage": "Home Page", 22 | "defaultPage": "Default Page", 23 | "mainMenu": "Main Menu", 24 | "mainNav": "Main Navigation", 25 | "footerMenu": "Additional Resources", 26 | "copyright": "Copyright. All rights reserved by", 27 | "header": "Header", 28 | "footer": "Footer", 29 | "hero": "Hero", 30 | "renderType": "Render Type", 31 | "renderTypeDark": "dark", 32 | "renderTypeLight": "light", 33 | "renderTypeImage": "image", 34 | "icon": "Icon", 35 | "page": "Page", 36 | "url": "URL", 37 | "left": "left", 38 | "right": "right", 39 | "customUrl": "URL", 40 | "caption": "Caption", 41 | "featured": "Featured", 42 | "cta": "Call To Action", 43 | "ctaPrimary": "Call To Action - Primary", 44 | "ctaOutlined": "Call To Action - Secondary", 45 | "ctaButton": "Call to Action - Button", 46 | "ctaIcon": "Call To Action - Icon", 47 | "search": "Search", 48 | "searchUrl": "Search URL", 49 | "searchSEO": "SEO & Social", 50 | "footerNav": "Footer Navigation", 51 | "socialLinks": "Social Links", 52 | "isDark": "Dark?", 53 | "isPrimary": "Primary?", 54 | "imagePosition": "Image Position", 55 | "content": "Content", 56 | "category": "Category", 57 | "file": "File", 58 | "media": "Media", 59 | "enlarge": "Enlarge {{ value }}", 60 | "groupBasics": "Basics", 61 | "publishDate": "Publication Date", 62 | "noResults": "No records found.", 63 | "notFoundTitle": "Error 404", 64 | "notFoundDesc": "We're sorry. We couldn't find the page you're looking for.", 65 | "tagLabel": "Content Tag", 66 | "tagPluralLabel": "Content Tags", 67 | "productLabel": "Product", 68 | "productPluralLabel": "Products", 69 | "productCategoryLabel": "Product Category", 70 | "productCategoryPageLabel": "Product Categories Page", 71 | "productCategoryPluralLabel": "Product Categories", 72 | "productCategoryTagsLabel": "Product tags", 73 | "productCategoryTagsHelp": "Choose tags for matching products (with the same tags).", 74 | "productCategoryHideEmptyLabel": "Hide Empty Categories", 75 | "productTaglineHelp": "Short product description", 76 | "productMetaLabel": "Product meta", 77 | "productPriceLabel": "Price", 78 | "productPricePromoLabel": "Promo price", 79 | "productBuyNowUrlLabel": "Buy Now URL", 80 | "productBuyNowUrl": "Buy Now", 81 | "productVatInfoLabel": "VAT description", 82 | "productVatInfoHelp": "Shown alongside prices when possible", 83 | "productImage": "Main Image", 84 | "productImages": "Additional Images", 85 | "productDetailsLabel": "Details", 86 | "productSpecsLabel": "Specification", 87 | "productSpecsFormattedLabel": "Formatted?", 88 | "productSpecsDescLabel": "Description", 89 | "productPageLabel": "Products Page", 90 | "productNoResults": "No products match the current filter criteria.", 91 | "productRelatedProducts": "Related Products", 92 | "productOldPrice": "Old price", 93 | "productPrice": "Price", 94 | "productCardAria": "See more about {{ value }}", 95 | "productsAvailable": "{{ value }} Products Available", 96 | "favicon": "Favicon", 97 | "faviconHelp": "The icon for your website shown in the browser tab.", 98 | "urlType": "URL Type", 99 | "listType": "List Type", 100 | "listTypeRecent": "Recent", 101 | "listTypeCategory": "By Category", 102 | "listTypeTags": "By Tags", 103 | "listTypeManual": "Choose Manually", 104 | "chooseProducts": "Choose Products", 105 | "chooseTags": "Choose Tags", 106 | "chooseCategory": "Choose Category", 107 | "chooseCategories": "Choose Categories", 108 | "condensedView": "Condensed View", 109 | "condensedViewHelp": "Use 4 columns grid", 110 | "ctaWidget": "Call To Action", 111 | "heroSplitWidget": "Hero - Side Image", 112 | "heroFullWidget": "Hero - Background Image", 113 | "promoWidget": "Promo", 114 | "productFeaturedWidget": "Products - Featured", 115 | "productCategoryWidget": "Products Categories", 116 | "blockquoteWidget": "Review", 117 | "contentWidget": "Content", 118 | "newSearch": "Looking for something?" 119 | } 120 | -------------------------------------------------------------------------------- /modules/promo-widget/index.js: -------------------------------------------------------------------------------- 1 | // Common URL scheme 2 | // Common URL scheme 3 | const urlScheme = { 4 | label: { 5 | type: 'string', 6 | label: 'app:label' 7 | }, 8 | urlType: { 9 | label: 'app:urlType', 10 | type: 'select', 11 | choices: [ 12 | { 13 | label: 'app:page', 14 | value: 'page' 15 | }, 16 | { 17 | label: 'app:productCategoryLabel', 18 | value: 'category' 19 | }, 20 | { 21 | label: 'app:file', 22 | value: 'file' 23 | }, 24 | { 25 | label: 'app:customUrl', 26 | value: 'custom' 27 | } 28 | ], 29 | required: true 30 | }, 31 | url: { 32 | type: 'url', 33 | label: 'app:url', 34 | required: true, 35 | if: { 36 | urlType: 'custom' 37 | } 38 | }, 39 | _page: { 40 | label: 'app:page', 41 | type: 'relationship', 42 | withType: '@apostrophecms/any-page-type', 43 | max: 1, 44 | builders: { 45 | areas: false, 46 | relationships: false, 47 | project: { 48 | title: 1, 49 | slug: 1, 50 | _url: 1, 51 | type: 1 52 | } 53 | }, 54 | required: true, 55 | if: { 56 | urlType: 'page' 57 | } 58 | }, 59 | _file: { 60 | label: 'app:file', 61 | type: 'relationship', 62 | withType: '@apostrophecms/file', 63 | max: 1, 64 | builders: { 65 | areas: false, 66 | relationships: false, 67 | project: { 68 | title: 1, 69 | slug: 1, 70 | attachment: 1, 71 | _url: 1, 72 | type: 1 73 | } 74 | }, 75 | required: true, 76 | if: { 77 | urlType: 'file' 78 | } 79 | }, 80 | _category: { 81 | label: 'app:productCategoryLabel', 82 | type: 'relationship', 83 | withType: 'product-category', 84 | max: 1, 85 | builders: { 86 | areas: false, 87 | relationships: false, 88 | project: { 89 | title: 1, 90 | slug: 1, 91 | _url: 1, 92 | type: 1 93 | } 94 | }, 95 | required: true, 96 | if: { 97 | urlType: 'category' 98 | } 99 | } 100 | }; 101 | 102 | export default { 103 | extend: '@apostrophecms/widget-type', 104 | options: { 105 | label: 'app:promoWidget' 106 | }, 107 | fields: { 108 | add: { 109 | caption: { 110 | type: 'string', 111 | label: 'app:caption', 112 | max: 200 113 | }, 114 | image: { 115 | type: 'area', 116 | label: 'app:image', 117 | options: { 118 | min: 1, 119 | max: 1, 120 | widgets: { 121 | '@apostrophecms/image': {} 122 | } 123 | }, 124 | required: true 125 | }, 126 | imagePosition: { 127 | type: 'select', 128 | label: 'app:imagePosition', 129 | choices: [ 130 | { 131 | label: 'app:left', 132 | value: 'left' 133 | }, 134 | { 135 | label: 'app:right', 136 | value: 'right' 137 | } 138 | ], 139 | required: true, 140 | def: 'left' 141 | }, 142 | ctaPrimary: { 143 | type: 'object', 144 | label: 'app:ctaPrimary', 145 | fields: { 146 | add: { ...urlScheme } 147 | } 148 | }, 149 | ctaOutlined: { 150 | type: 'object', 151 | label: 'app:ctaOutlined', 152 | fields: { 153 | add: { ...urlScheme } 154 | } 155 | }, 156 | content: { 157 | type: 'area', 158 | label: 'app:content', 159 | options: { 160 | min: 1, 161 | max: 1, 162 | widgets: { 163 | '@apostrophecms/rich-text': { 164 | toolbar: [ 165 | 'styles', 166 | '|', 167 | 'bold', 168 | 'italic', 169 | 'strike', 170 | 'link', 171 | '|', 172 | 'bulletList', 173 | 'orderedList' 174 | ], 175 | styles: [ 176 | { 177 | tag: 'h3', 178 | label: 'Heading' 179 | }, 180 | { 181 | tag: 'p', 182 | label: 'Paragraph (P)' 183 | } 184 | ], 185 | insert: [] 186 | } 187 | } 188 | } 189 | } 190 | } 191 | } 192 | }; 193 | --------------------------------------------------------------------------------