├── .stylelintignore ├── .stylelintrc ├── postcss.config.js ├── public └── images │ └── logo.png ├── deployment ├── before-deploying └── rsync_exclude.txt ├── modules ├── theme-default │ ├── ui │ │ └── src │ │ │ ├── scss │ │ │ ├── widgets │ │ │ │ └── _image-widget.scss │ │ │ ├── layout │ │ │ │ ├── _area.scss │ │ │ │ ├── _grid.scss │ │ │ │ └── _main.scss │ │ │ ├── utilities │ │ │ │ ├── _display.scss │ │ │ │ └── _accessibility.scss │ │ │ ├── global │ │ │ │ ├── _document.scss │ │ │ │ └── _typography.scss │ │ │ ├── components │ │ │ │ ├── _image.scss │ │ │ │ ├── _footer.scss │ │ │ │ ├── _header.scss │ │ │ │ ├── _code.scss │ │ │ │ ├── _placeholder.scss │ │ │ │ ├── _button.scss │ │ │ │ └── _link.scss │ │ │ ├── functions │ │ │ │ └── _rem.scss │ │ │ ├── settings │ │ │ │ ├── _color.scss │ │ │ │ └── _font.scss │ │ │ └── pages │ │ │ │ └── _welcome.scss │ │ │ ├── js │ │ │ └── placeholder.js │ │ │ ├── index.js │ │ │ └── index.scss │ ├── views │ │ ├── welcome.html │ │ └── placeholder.html │ └── index.js ├── @apostrophecms │ ├── page │ │ ├── views │ │ │ └── notFound.html │ │ └── index.js │ ├── asset │ │ └── index.js │ ├── admin-bar │ │ └── index.js │ ├── template │ │ ├── views │ │ │ └── outerLayout.html │ │ └── index.js │ ├── home-page │ │ ├── views │ │ │ └── page.html │ │ └── index.js │ └── global │ │ └── index.js ├── widgets │ ├── link-widget │ │ ├── index.js │ │ └── views │ │ │ └── widget.html │ ├── modules.js │ ├── accordion-widget │ │ ├── views │ │ │ └── widget.html │ │ ├── ui │ │ │ └── src │ │ │ │ ├── index.js │ │ │ │ └── index.scss │ │ └── index.js │ ├── column-widget │ │ ├── views │ │ │ └── widget.html │ │ ├── ui │ │ │ └── src │ │ │ │ └── index.scss │ │ └── index.js │ ├── card-widget │ │ ├── ui │ │ │ └── src │ │ │ │ └── index.scss │ │ ├── index.js │ │ └── views │ │ │ └── widget.html │ ├── slideshow-widget │ │ ├── views │ │ │ └── widget.html │ │ ├── ui │ │ │ └── src │ │ │ │ ├── index.scss │ │ │ │ └── index.js │ │ └── index.js │ └── hero-widget │ │ ├── ui │ │ └── src │ │ │ └── index.scss │ │ ├── views │ │ └── widget.html │ │ └── index.js ├── default-page │ ├── views │ │ └── page.html │ └── index.js ├── helpers │ └── index.js └── @apostrophecms-pro │ └── palette │ ├── lib │ └── configs │ │ ├── grid.js │ │ ├── typography.js │ │ └── color.js │ └── index.js ├── eslint.config.js ├── .editorconfig ├── views ├── fragments │ └── link.html ├── includes │ └── attributes.html └── layout.html ├── .gitignore ├── nodemon.json ├── LICENSE.md ├── app.js ├── scripts ├── sync-down └── sync-up ├── package.json ├── lib └── schema │ └── link.js └── README.md /.stylelintignore: -------------------------------------------------------------------------------- 1 | **/node_modules 2 | apos-build -------------------------------------------------------------------------------- /.stylelintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "stylelint-config-apostrophe" 3 | } 4 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | autoprefixer: {} 4 | } 5 | }; 6 | -------------------------------------------------------------------------------- /public/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apostrophecms/starter-kit-pro-essentials/main/public/images/logo.png -------------------------------------------------------------------------------- /deployment/before-deploying: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # This project requires an asset build 4 | 5 | npm run build || exit 1 6 | -------------------------------------------------------------------------------- /modules/theme-default/ui/src/scss/widgets/_image-widget.scss: -------------------------------------------------------------------------------- 1 | .image-widget { 2 | display: inline-block; 3 | max-width: 100%; 4 | } 5 | -------------------------------------------------------------------------------- /modules/@apostrophecms/page/views/notFound.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | 3 | {% block title %}404 - Page Not Found{% endblock %} 4 | -------------------------------------------------------------------------------- /modules/theme-default/ui/src/scss/layout/_area.scss: -------------------------------------------------------------------------------- 1 | [class*='-widget']:not(:is(:last-child, [class*='apos'])) { 2 | margin-bottom: 20px; 3 | } 4 | -------------------------------------------------------------------------------- /modules/theme-default/ui/src/scss/utilities/_display.scss: -------------------------------------------------------------------------------- 1 | .inline-block { 2 | display: inline-block; 3 | } 4 | 5 | .hidden, 6 | .is-hidden { 7 | display: none; 8 | } 9 | -------------------------------------------------------------------------------- /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/theme-default/ui/src/scss/global/_document.scss: -------------------------------------------------------------------------------- 1 | body { 2 | min-height: 100vh; 3 | color: var(--palette-font-color); 4 | font-family: $font-sans-serif; 5 | background-color: var(--palette-color-background); 6 | } 7 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | end_of_line = lf 8 | indent_size = 2 9 | indent_style = space 10 | insert_final_newline = true 11 | trim_trailing_whitespace = true 12 | -------------------------------------------------------------------------------- /modules/theme-default/ui/src/scss/components/_image.scss: -------------------------------------------------------------------------------- 1 | figure { 2 | margin: 0 0 20px; 3 | 4 | img { 5 | max-width: 100%; 6 | } 7 | 8 | figcaption { 9 | margin-top: 5px; 10 | font-size: rem(14); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /modules/theme-default/ui/src/scss/layout/_grid.scss: -------------------------------------------------------------------------------- 1 | .full-width { 2 | position: relative; 3 | right: 50%; 4 | left: 50%; 5 | box-sizing: border-box; 6 | width: 100vw; 7 | margin-right: -50vw; 8 | margin-left: -50vw; 9 | } 10 | -------------------------------------------------------------------------------- /views/fragments/link.html: -------------------------------------------------------------------------------- 1 | {% fragment template(options) %} 2 | {{ rendercaller() }} 7 | {% endfragment %} 8 | -------------------------------------------------------------------------------- /modules/theme-default/ui/src/scss/functions/_rem.scss: -------------------------------------------------------------------------------- 1 | // Converts a px font size to rem unit 2 | @use 'sass:math'; 3 | 4 | // stylelint-disable-next-line at-rule-disallowed-list 5 | @function rem($value) { 6 | @return math.div($value, 16) + rem; 7 | } 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # vim swapfiles 2 | *.swp 3 | .DS_Store 4 | /locales 5 | npm-debug.log 6 | /data 7 | /apos-build 8 | /public/apos-frontend 9 | /public/modules 10 | /public/uploads 11 | /public/svgs/*.svg 12 | node_modules 13 | /public/modules 14 | package-lock.json 15 | -------------------------------------------------------------------------------- /views/includes/attributes.html: -------------------------------------------------------------------------------- 1 | {% for attribute, value in options.attributes %} 2 | {% if attribute | isBooleanAttr %} 3 | {% if value %} 4 | {{ attribute }} 5 | {% endif %} 6 | {% elseif value != '' %} 7 | {{ attribute }}="{{ value }}" 8 | {% endif %} 9 | {% endfor %} 10 | -------------------------------------------------------------------------------- /modules/widgets/link-widget/index.js: -------------------------------------------------------------------------------- 1 | import link from '../../../lib/schema/link.js'; 2 | 3 | export default { 4 | extend: '@apostrophecms/widget-type', 5 | options: { 6 | label: 'Link', 7 | icon: 'cursor-default-click-icon' 8 | }, 9 | fields: { 10 | add: link 11 | } 12 | }; 13 | -------------------------------------------------------------------------------- /modules/@apostrophecms/page/index.js: -------------------------------------------------------------------------------- 1 | export default { 2 | options: { 3 | types: [ 4 | { 5 | name: 'default-page', 6 | label: 'Default' 7 | }, 8 | { 9 | name: '@apostrophecms/home-page', 10 | label: 'Home' 11 | } 12 | ] 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /modules/theme-default/ui/src/scss/settings/_color.scss: -------------------------------------------------------------------------------- 1 | $color-purple: #6236ff; 2 | $color-pink: #fe5599; 3 | $color-green: #0c8; 4 | $color-gold: #f7b500; 5 | 6 | $color-light-yellow: #ffffd8; 7 | 8 | $color-white: #fff; 9 | $color-gray-05: #eee; 10 | $color-gray-15: #dbdbdb; 11 | $color-gray-80: #2b2b2b; 12 | $color-black: #000; 13 | -------------------------------------------------------------------------------- /modules/@apostrophecms/asset/index.js: -------------------------------------------------------------------------------- 1 | export default { 2 | options: { 3 | // When not in production, refresh the page on restart 4 | refreshOnRestart: true 5 | // Change to `apos` for admin UI HMR or `false` to disable. 6 | // The default is `public` - watch your `ui/src` UI code. 7 | // hmr: 'apos' 8 | } 9 | }; 10 | -------------------------------------------------------------------------------- /modules/widgets/modules.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Apostrophe Pro Basic Widgets to kick start development. 3 | * Feel free to remove if not needed or modify to fit your needs. 4 | */ 5 | 6 | export default { 7 | 'accordion-widget': {}, 8 | 'card-widget': {}, 9 | 'column-widget': {}, 10 | 'hero-widget': {}, 11 | 'link-widget': {}, 12 | 'slideshow-widget': {} 13 | }; 14 | -------------------------------------------------------------------------------- /nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "delay": 1000, 3 | "verbose": true, 4 | "watch": ["./app.js", "./modules/**/*", "./lib/**/*.js", "./views/**/*.html"], 5 | "ignoreRoot": [".git"], 6 | "ignore": [ 7 | "**/ui/", 8 | "locales/*.json", 9 | "public/uploads/", 10 | "public/apos-frontend/*.js", 11 | "data/" 12 | ], 13 | "ext": "json, js, cjs, html" 14 | } 15 | -------------------------------------------------------------------------------- /modules/theme-default/ui/src/scss/settings/_font.scss: -------------------------------------------------------------------------------- 1 | $font-monospace: menlo, monaco, consolas, 'Liberation Mono', 'Courier New', monospace; 2 | $font-sans-serif: -apple-system, blinkmacsystemfont, "Segoe UI", roboto, helvetica, arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; 3 | 4 | $font-weight-light: 200; 5 | $font-weight-normal: 400; 6 | $font-weight-bold: 700; -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright 2020, 2021 Apostrophe Technologies. Licensed to enterprise customers of Apostrophe Technologies according to the terms of their individual agreements. This code is intended as a starting point for your own work and license holders may modify it freely for their own purposes related to operating a project based on the Apostrophe Assembly and Apostrophe multisite technologies. 2 | -------------------------------------------------------------------------------- /modules/@apostrophecms/admin-bar/index.js: -------------------------------------------------------------------------------- 1 | export default { 2 | options: { 3 | groups: [ 4 | { 5 | name: 'media', 6 | label: 'Media', 7 | items: [ 8 | '@apostrophecms/image', 9 | '@apostrophecms/file', 10 | '@apostrophecms/image-tag', 11 | '@apostrophecms/file-tag' 12 | ] 13 | } 14 | ] 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /modules/@apostrophecms/template/views/outerLayout.html: -------------------------------------------------------------------------------- 1 | {% extends "outerLayoutBase.html" %} 2 | 3 | {% block standardHead %} 4 | {# Use standardHead and super() rather than extraHead to ensure palette injects after it #} 5 | {{ super() }} 6 | 7 | {# Add a favicon to the page from the /public/images folder #} 8 | {# #} 9 | {% endblock %} -------------------------------------------------------------------------------- /modules/@apostrophecms/home-page/views/page.html: -------------------------------------------------------------------------------- 1 | {% extends 'layout.html' %} 2 | 3 | {# 4 | TODO: The markup below is for demonstration purposes only 5 | Modify this template to meet your specific needs 6 | #} 7 | 8 | {% block main %} 9 |
10 | {% include "theme-default:welcome.html" %} 11 | 12 | {% area data.page, 'main' %} 13 |
14 | {% endblock %} -------------------------------------------------------------------------------- /modules/theme-default/ui/src/scss/components/_footer.scss: -------------------------------------------------------------------------------- 1 | .footer { 2 | padding: 40px 0; 3 | text-align: center; 4 | } 5 | 6 | .footer__header { 7 | margin-bottom: 20px; 8 | font-size: rem(18); 9 | } 10 | 11 | .footer__links { 12 | margin-top: 0; 13 | margin-bottom: 0; 14 | padding-left: 0; 15 | list-style: none; 16 | 17 | li { 18 | display: inline-block; 19 | 20 | &:not(:last-child) { 21 | margin-right: 20px; 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /deployment/rsync_exclude.txt: -------------------------------------------------------------------------------- 1 | # List files and folders that shouldn't be deployed (such as data folders 2 | # and runtime status files) here. Prefix things properly with / so we don't 3 | # have any effect on unrelated subdirectories of node_modules 4 | 5 | /data 6 | /public/uploads 7 | /public/apos-minified 8 | .git 9 | .gitignore 10 | # We DO deploy node_modules to the apostrophe cloud 11 | # We DO deploy release-id to the apostrophe cloud (from the worker 12 | # that generates it) 13 | -------------------------------------------------------------------------------- /modules/theme-default/ui/src/scss/pages/_welcome.scss: -------------------------------------------------------------------------------- 1 | .welcome { 2 | margin-bottom: 30px; 3 | text-align: center; 4 | 5 | p { 6 | max-width: 500px; 7 | margin-right: auto; 8 | margin-left: auto; 9 | } 10 | 11 | .button--cta { 12 | padding: 20px 30px; 13 | } 14 | 15 | .placeholder__help { 16 | margin-bottom: 0; 17 | } 18 | } 19 | 20 | .welcome__code { 21 | margin: 0 auto 30px; 22 | } 23 | 24 | .welcome__help { 25 | font-size: rem(22); 26 | } 27 | -------------------------------------------------------------------------------- /modules/theme-default/ui/src/scss/utilities/_accessibility.scss: -------------------------------------------------------------------------------- 1 | .sr-only:not(:focus, :active) { 2 | position: absolute; 3 | overflow: hidden; 4 | width: 1px; 5 | height: 1px; 6 | margin: -1px; 7 | padding: 0; 8 | clip: rect(0, 0, 0, 0); 9 | white-space: nowrap; 10 | border-width: 0; 11 | } 12 | 13 | .not-sr-only { 14 | position: static; 15 | overflow: visible; 16 | width: auto; 17 | height: auto; 18 | margin: 0; 19 | padding: 0; 20 | clip: auto; 21 | white-space: normal; 22 | } 23 | -------------------------------------------------------------------------------- /modules/default-page/views/page.html: -------------------------------------------------------------------------------- 1 | {% extends 'layout.html' %} 2 | 3 | {# 4 | TODO: The markup below is for demonstration purposes only 5 | Modify this template to meet your specific needs 6 | #} 7 | 8 | {% block main %} 9 |
10 |

{{ data.page.title }}

11 | 12 | {% area data.page, 'main' %} 13 | 14 | {% if apos.area.isEmpty(data.page, 'main') %} 15 | {% include 'theme-default:placeholder.html' %} 16 | {% endif %} 17 |
18 | {% endblock %} 19 | -------------------------------------------------------------------------------- /modules/widgets/accordion-widget/views/widget.html: -------------------------------------------------------------------------------- 1 | {% set widget = data.widget %} 2 | 3 |
4 | 5 | {% for item in widget.items %} 6 |
7 |

8 | 9 |

10 |
11 | {% area item, 'content' %} 12 |
13 |
14 | {% endfor %} 15 | 16 |
-------------------------------------------------------------------------------- /modules/widgets/column-widget/views/widget.html: -------------------------------------------------------------------------------- 1 | {% set widget = data.widget %} 2 | 3 | {% set columns = apos.columnsWidget.getColumns(widget.layout) %} 4 | 5 |
11 | {% for column in range(1, columns+1) %} 12 |
13 | {% area widget, 'column'+column %} 14 |
15 | {% endfor %} 16 |
-------------------------------------------------------------------------------- /modules/default-page/index.js: -------------------------------------------------------------------------------- 1 | export default { 2 | extend: '@apostrophecms/page-type', 3 | fields: { 4 | add: { 5 | main: { 6 | type: 'area', 7 | label: 'Main', 8 | options: { 9 | widgets: { 10 | '@apostrophecms/rich-text': {} 11 | } 12 | } 13 | } 14 | }, 15 | group: { 16 | basics: { 17 | label: 'Basics', 18 | fields: [ 19 | 'title', 20 | 'main' 21 | ] 22 | } 23 | } 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /modules/theme-default/ui/src/scss/layout/_main.scss: -------------------------------------------------------------------------------- 1 | $max-width: 1020px; 2 | 3 | .wrapper { 4 | display: flex; 5 | flex-direction: column; 6 | min-height: 100vh; 7 | justify-content: space-between; 8 | overflow-x: hidden; 9 | 10 | .is-logged-in & { 11 | min-height: calc(100vh - 112px); 12 | } 13 | } 14 | 15 | .header, 16 | .main, 17 | .footer, 18 | .content { 19 | margin-right: auto; 20 | margin-left: auto; 21 | } 22 | 23 | .header, 24 | .main, 25 | .footer { 26 | width: 100%; 27 | max-width: $max-width; 28 | } 29 | -------------------------------------------------------------------------------- /modules/theme-default/ui/src/scss/components/_header.scss: -------------------------------------------------------------------------------- 1 | .header { 2 | display: flex; 3 | flex-wrap: wrap; 4 | align-items: center; 5 | justify-content: space-between; 6 | margin-bottom: 10px; 7 | padding: 40px 0; 8 | 9 | .button { 10 | font-size: 0.9rem; 11 | } 12 | } 13 | 14 | .header__logo { 15 | display: block; 16 | width: 190px; 17 | max-width: 100%; 18 | object-fit: contain; 19 | } 20 | 21 | .navigation__link { 22 | display: inline-block; 23 | font-size: rem(18); 24 | 25 | &:not(:last-of-type) { 26 | margin-right: 15px; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /modules/widgets/link-widget/views/widget.html: -------------------------------------------------------------------------------- 1 | {% import "fragments/link.html" as link %} 2 | 3 | {% set widget = data.widget %} 4 | 5 | {% set path = apos.template.linkPath(widget) %} 6 | 7 | {% set style = 'button' if widget.linkStyle === 'button' else '' %} 8 | {% set variant = 'color--' + widget.linkVariant %} 9 | 10 | -------------------------------------------------------------------------------- /modules/widgets/card-widget/ui/src/index.scss: -------------------------------------------------------------------------------- 1 | .card-widget { 2 | overflow: hidden; 3 | border: 1px solid $color-gray-15; 4 | border-radius: 5px; 5 | 6 | .card-widget__link { 7 | text-decoration: none; 8 | color: unset; 9 | 10 | &:hover { 11 | text-decoration: none; 12 | } 13 | 14 | &:visited { 15 | color: unset; 16 | } 17 | } 18 | 19 | .image { 20 | width: 100%; 21 | height: 300px; 22 | object-fit: cover; 23 | } 24 | 25 | .card-widget__content { 26 | padding: 20px; 27 | 28 | p:last-of-type { 29 | margin-bottom: 0; 30 | } 31 | } 32 | 33 | .card-widget__actions { 34 | margin-top: 20px; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /modules/theme-default/ui/src/js/placeholder.js: -------------------------------------------------------------------------------- 1 | export default () => { 2 | const hiddenClass = 'is-hidden'; 3 | 4 | const adminBar = window.apos.modules['@apostrophecms/admin-bar']; 5 | 6 | if (adminBar) { 7 | setInterval(togglePlaceholder, 300); 8 | } 9 | 10 | function togglePlaceholder() { 11 | const editMode = adminBar.editMode || false; 12 | 13 | const $el = document.querySelector('[data-apos-placeholder]'); 14 | 15 | if ($el) { 16 | if (editMode && !$el.classList.contains(hiddenClass)) { 17 | apos.util.addClass($el, hiddenClass); 18 | } else if (!editMode && $el.classList.contains(hiddenClass)) { 19 | apos.util.removeClass($el, hiddenClass); 20 | } 21 | } 22 | } 23 | }; 24 | -------------------------------------------------------------------------------- /modules/widgets/slideshow-widget/views/widget.html: -------------------------------------------------------------------------------- 1 | {% set widget = data.widget %} 2 | 3 |
4 | 5 |
6 | {% for image in widget.images %} 7 | {% set img = apos.image.first(image._image) %} 8 | {% set src = apos.attachment.url(img) %} 9 | 10 |
11 | {{ img._alt or '' }} 12 |
{{ image.caption }}
13 |
14 | {% endfor %} 15 |
16 | 17 |
18 | 19 |
20 |
21 | 22 |
23 | 24 | -------------------------------------------------------------------------------- /modules/theme-default/ui/src/scss/components/_code.scss: -------------------------------------------------------------------------------- 1 | code { 2 | white-space: normal; 3 | } 4 | 5 | pre { 6 | position: relative; 7 | display: flex; 8 | overflow: auto; 9 | margin: 0 auto; 10 | margin-bottom: 20px; 11 | padding: 20px; 12 | color: $color-white; 13 | background: $color-gray-80; 14 | font-family: $font-monospace; 15 | font-size: rem(14); 16 | text-align: left; 17 | white-space: pre; 18 | word-spacing: normal; 19 | word-break: normal; 20 | word-wrap: normal; 21 | line-height: 1.8; 22 | tab-size: 4; 23 | hyphens: none; 24 | border-radius: 6px; 25 | max-width: 600px; 26 | } 27 | 28 | .code--inline { 29 | padding: 4px 6px; 30 | border: 1px solid $color-gray-15; 31 | font-size: rem(12); 32 | background-color: $color-gray-05; 33 | border-radius: 5px; 34 | } 35 | -------------------------------------------------------------------------------- /modules/theme-default/ui/src/index.js: -------------------------------------------------------------------------------- 1 | import placeholder from './js/placeholder'; 2 | 3 | export default () => { 4 | // eslint-disable-next-line no-console 5 | console.log('Default theme project level js file'); 6 | 7 | // NOTE: Theme specific - 8 | // Adds a class to the body so we can adjust the layout height 9 | 10 | const isLoggedIn = 'is-logged-in'; 11 | 12 | const $body = document.getElementsByTagName('body')[0]; 13 | 14 | const adminBar = window.apos.modules['@apostrophecms/admin-bar']; 15 | 16 | if (adminBar && !$body.classList.contains(isLoggedIn)) { 17 | apos.util.addClass($body, isLoggedIn); 18 | } else { 19 | apos.util.removeClass($body, isLoggedIn); 20 | } 21 | 22 | // NOTE: Theme Specific - 23 | // Adds editor help text when first editing the Home Page 24 | 25 | placeholder(); 26 | }; 27 | -------------------------------------------------------------------------------- /modules/widgets/slideshow-widget/ui/src/index.scss: -------------------------------------------------------------------------------- 1 | @import 'swiper/swiper-bundle.css'; 2 | 3 | :root { 4 | --swiper-pagination-bottom: 0; 5 | } 6 | 7 | .figure { 8 | margin: 0; 9 | 10 | .figure__image { 11 | width: 100%; 12 | height: 385px; 13 | object-fit: cover; 14 | } 15 | 16 | .figure__caption { 17 | padding-top: 10px; 18 | padding-bottom: 10px; 19 | font-size: rem(14); 20 | } 21 | } 22 | 23 | [class^='swiper-button'] { 24 | box-sizing: border-box; 25 | width: 30px; 26 | height: 30px; 27 | padding: 5px; 28 | color: $color-white; 29 | border-radius: 25px; 30 | background-color: var(--palette-color-primary); 31 | 32 | &::after { 33 | font-size: 1rem; 34 | line-height: 1; 35 | } 36 | } 37 | 38 | .swiper-pagination-bullet-active { 39 | background: var(--palette-color-primary); 40 | } 41 | -------------------------------------------------------------------------------- /modules/helpers/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * A Module that can be used to add utility functions 3 | * that are used across multiple modules 4 | */ 5 | 6 | export default { 7 | options: { 8 | alias: 'helpers' 9 | }, 10 | /** 11 | * NOTE: `helpers` can be added to any module 12 | * https://docs.apostrophecms.org/reference/module-api/module-overview.html#helpers-self 13 | */ 14 | helpers(self, options) { 15 | return { 16 | /** 17 | * If the following code is uncommented, the `isInt` helper 18 | * would be available in templates as apos.helpers.isInt() 19 | * 20 | * isInt: (n) => { 21 | * return n % 1 === 0; 22 | * }, 23 | * 24 | * NOTE: async functions are NOT allowed in helpers, you should 25 | * write an async component instead for such cases 26 | */ 27 | }; 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /modules/widgets/accordion-widget/ui/src/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * For Demo Purposes, this Accordion is powered by accordion-js 3 | * You could swap this out another library or your own implementation 4 | */ 5 | 6 | import Accordion from 'accordion-js'; 7 | 8 | export default () => { 9 | 10 | apos.util.widgetPlayers.accordion = { 11 | selector: '[data-accordion-widget]', 12 | player: function (el) { 13 | 14 | /** 15 | * For all available options see: 16 | * https://www.npmjs.com/package/accordion-js#options 17 | */ 18 | const options = { 19 | duration: 300, 20 | ...(el.dataset.accordionWidget && { 21 | ...JSON.parse(el.dataset.accordionWidget).accordionjs 22 | }) 23 | }; 24 | 25 | // eslint-disable-next-line 26 | new Accordion('.accordion-container', options); 27 | } 28 | }; 29 | 30 | }; 31 | -------------------------------------------------------------------------------- /modules/widgets/slideshow-widget/index.js: -------------------------------------------------------------------------------- 1 | export default { 2 | extend: '@apostrophecms/widget-type', 3 | options: { 4 | label: 'Slideshow', 5 | icon: 'image-multiple' 6 | }, 7 | icons: { 8 | 'image-multiple': 'ImageMultiple' 9 | }, 10 | fields: { 11 | add: { 12 | images: { 13 | type: 'array', 14 | titleField: 'caption', 15 | min: 2, 16 | fields: { 17 | add: { 18 | _image: { 19 | type: 'relationship', 20 | label: 'Image', 21 | max: 1, 22 | required: true, 23 | withType: '@apostrophecms/image' 24 | }, 25 | caption: { 26 | type: 'string', 27 | label: 'Caption', 28 | textarea: true 29 | } 30 | } 31 | } 32 | } 33 | } 34 | } 35 | }; 36 | -------------------------------------------------------------------------------- /modules/widgets/column-widget/ui/src/index.scss: -------------------------------------------------------------------------------- 1 | .column-widget:not(:last-of-type) { 2 | margin-bottom: var(--palette-grid-margin); 3 | } 4 | 5 | [class*='grid--']:not(.grid--100) { 6 | display: grid; 7 | gap: var(--palette-grid-margin); 8 | } 9 | 10 | .grid--50 { 11 | grid-template-columns: repeat(2, 1fr); 12 | } 13 | 14 | .grid--66-33, 15 | .grid--33-66, 16 | .grid--33 { 17 | grid-template-columns: repeat(3, 1fr); 18 | } 19 | 20 | .grid--66-33 { 21 | .grid__column-1 { 22 | grid-column: span 2; 23 | } 24 | } 25 | 26 | .grid--33-66 { 27 | .grid__column-2 { 28 | grid-column: span 2; 29 | } 30 | } 31 | 32 | .grid--75-25, 33 | .grid--25-75, 34 | .grid--25 { 35 | grid-template-columns: repeat(4, 1fr); 36 | } 37 | 38 | .grid--75-25 { 39 | .grid__column-1 { 40 | grid-column: span 3; 41 | } 42 | } 43 | 44 | .grid--25-75 { 45 | .grid__column-2 { 46 | grid-column: span 3; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /modules/@apostrophecms/home-page/index.js: -------------------------------------------------------------------------------- 1 | export default { 2 | fields: { 3 | add: { 4 | main: { 5 | type: 'area', 6 | label: 'Main', 7 | options: { 8 | widgets: { 9 | hero: { 10 | /** 11 | * We could allow the editor choose a display option but 12 | * we always want the Hero to cover the width of the screen 13 | * on the Homepage. We're passing an option to our widget 14 | * here so that the appearance always stays consistent 15 | * for this area. 16 | */ 17 | fullWidth: true 18 | }, 19 | column: {} 20 | } 21 | } 22 | } 23 | }, 24 | group: { 25 | basics: { 26 | label: 'Basics', 27 | fields: [ 28 | 'title', 29 | 'main' 30 | ] 31 | } 32 | } 33 | } 34 | }; 35 | -------------------------------------------------------------------------------- /modules/widgets/accordion-widget/index.js: -------------------------------------------------------------------------------- 1 | export default { 2 | extend: '@apostrophecms/widget-type', 3 | options: { 4 | label: 'Accordion', 5 | icon: 'arrow-down-drop-circle' 6 | }, 7 | icons: { 8 | 'arrow-down-drop-circle': 'ArrowDownDropCircle' 9 | }, 10 | fields: { 11 | add: { 12 | items: { 13 | type: 'array', 14 | label: 'Items', 15 | titleField: 'header', 16 | fields: { 17 | add: { 18 | header: { 19 | type: 'string', 20 | label: 'Header' 21 | }, 22 | content: { 23 | type: 'area', 24 | label: 'Content', 25 | options: { 26 | widgets: { 27 | '@apostrophecms/rich-text': {} 28 | }, 29 | max: 1 30 | } 31 | } 32 | } 33 | } 34 | } 35 | } 36 | } 37 | }; 38 | -------------------------------------------------------------------------------- /modules/theme-default/ui/src/scss/global/_typography.scss: -------------------------------------------------------------------------------- 1 | html { 2 | font-size: var(--palette-font-size); 3 | line-height: 1; 4 | text-size-adjust: 100%; 5 | } 6 | 7 | h1, 8 | h2, 9 | h3, 10 | h4, 11 | h5, 12 | h6, 13 | p { 14 | margin-top: 0; 15 | } 16 | 17 | h1, 18 | h2, 19 | h3, 20 | h4, 21 | h5, 22 | h6 { 23 | line-height: 1.15; 24 | } 25 | 26 | h2, 27 | h3, 28 | h4, 29 | h5, 30 | h6, 31 | p:not(:last-child) { 32 | margin-bottom: 30px; 33 | } 34 | 35 | h1, 36 | h2, 37 | h3 { 38 | color: $color-black; 39 | } 40 | 41 | h4, 42 | h5, 43 | h6 { 44 | color: var(--palette-font-color); 45 | font-size: var(--palette-font-size); 46 | } 47 | 48 | h1 { 49 | margin-bottom: 50px; 50 | font-size: rem(64); 51 | font-weight: $font-weight-light; 52 | } 53 | 54 | h2 { 55 | font-size: 3rem; 56 | } 57 | 58 | h3 { 59 | font-size: 2rem; 60 | } 61 | 62 | p { 63 | line-height: 1.5; 64 | 65 | &:last-child { 66 | margin-bottom: 0; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /modules/theme-default/ui/src/index.scss: -------------------------------------------------------------------------------- 1 | // Styles for this theme 2 | // If you need to share styles across themes, just @import them here 3 | 4 | @import 'normalize.css'; 5 | 6 | @import './scss/functions/_rem'; 7 | 8 | @import './scss/settings/_color'; 9 | @import './scss/settings/_font'; 10 | 11 | @import './scss/utilities/_accessibility'; 12 | @import './scss/utilities/_display'; 13 | 14 | @import './scss/global/_document'; 15 | @import './scss/global/_typography'; 16 | 17 | @import './scss/components/_button'; 18 | @import './scss/components/_code'; 19 | @import './scss/components/_footer'; 20 | @import './scss/components/_header'; 21 | @import './scss/components/_image'; 22 | @import './scss/components/_link'; 23 | @import './scss/components/_placeholder'; 24 | 25 | @import './scss/widgets/_image-widget'; 26 | 27 | @import './scss/layout/_area'; 28 | @import './scss/layout/_grid'; 29 | @import './scss/layout/_main'; 30 | 31 | @import './scss/pages/_welcome'; 32 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | import apostrophe from 'apostrophe'; 2 | 3 | apostrophe({ 4 | root: import.meta, 5 | shortName: 'CHANGEME', 6 | nestedModuleSubdirs: true, 7 | modules: { 8 | '@apostrophecms/attachment': { 9 | options: { 10 | uploadfs: { 11 | // Be sure to change 12 | disabledFileKey: 'CHANGEME' 13 | } 14 | } 15 | }, 16 | '@apostrophecms/express': { 17 | options: { 18 | session: { 19 | secret: 'CHANGEME' 20 | } 21 | } 22 | }, 23 | 24 | // Use Vite as the asset bundler and HMR server 25 | '@apostrophecms/vite': {}, 26 | 27 | // Just a nice place to keep helper functions that are 28 | // used across all sites 29 | helpers: {}, 30 | 31 | 'default-page': {}, 32 | 33 | // The @apostrophecms/home-page module always exists, no need to activate it here 34 | 35 | '@apostrophecms-pro/palette': {}, 36 | '@apostrophecms-pro/document-versions': {}, 37 | 38 | 'theme-default': {} 39 | } 40 | }); 41 | -------------------------------------------------------------------------------- /modules/@apostrophecms/global/index.js: -------------------------------------------------------------------------------- 1 | import schema from '../../../lib/schema/link.js'; 2 | 3 | const { 4 | linkText, linkType, linkUrl, _linkFile, _linkPage, linkTarget 5 | } = schema; 6 | 7 | export default { 8 | fields: { 9 | add: { 10 | logo: { 11 | type: 'area', 12 | label: 'Logo', 13 | options: { 14 | widgets: { 15 | '@apostrophecms/image': {} 16 | }, 17 | max: 1 18 | } 19 | }, 20 | headerLinks: { 21 | type: 'array', 22 | label: 'Navigation', 23 | titleField: 'linkText', 24 | fields: { 25 | add: { 26 | linkText, 27 | linkType, 28 | linkUrl, 29 | _linkFile, 30 | _linkPage, 31 | linkTarget 32 | } 33 | } 34 | } 35 | }, 36 | group: { 37 | navigation: { 38 | label: 'Site Header', 39 | fields: [ 'logo', 'headerLinks' ] 40 | } 41 | } 42 | } 43 | }; 44 | -------------------------------------------------------------------------------- /modules/@apostrophecms-pro/palette/lib/configs/grid.js: -------------------------------------------------------------------------------- 1 | /** 2 | * For additional information on using and configuring Palette, see: 3 | * https://github.com/apostrophecms/palette 4 | */ 5 | 6 | export default { 7 | add: { 8 | gridMargin: { 9 | type: 'select', 10 | label: 'Margin', 11 | help: 'Space columns and widgets', 12 | selector: ':root', 13 | property: '--palette-grid-margin', 14 | unit: 'px', 15 | choices: [ 16 | { 17 | label: '16px', 18 | value: '16' 19 | }, 20 | { 21 | label: '20px', 22 | value: '20' 23 | }, 24 | { 25 | label: '24px', 26 | value: '24' 27 | }, 28 | { 29 | label: '28px', 30 | value: '28' 31 | }, 32 | { 33 | label: '32px', 34 | value: '32' 35 | } 36 | ], 37 | def: '20' 38 | } 39 | }, 40 | group: { 41 | grid: { 42 | label: 'Grid', 43 | fields: [ 44 | 'gridMargin' 45 | ] 46 | } 47 | } 48 | }; 49 | -------------------------------------------------------------------------------- /modules/widgets/accordion-widget/ui/src/index.scss: -------------------------------------------------------------------------------- 1 | @import 'accordion-js/dist/accordion.min.css'; 2 | 3 | .ac { 4 | margin-top: 0; 5 | border: none; 6 | background: unset; 7 | 8 | &:not(:last-of-type) { 9 | margin-bottom: 10px; 10 | } 11 | 12 | .ac-header { 13 | border-bottom: 1px solid $color-gray-15; 14 | font-size: rem(16); 15 | font-weight: $font-weight-normal; 16 | } 17 | 18 | .ac-trigger { 19 | font: unset; 20 | padding: 0 30px 10px 0; 21 | 22 | &:hover { 23 | color: var(--palette-color-primary); 24 | } 25 | 26 | &:focus { 27 | color: unset; 28 | } 29 | 30 | &::after { 31 | width: unset; 32 | transform: translate(0, calc(-50% - 7px)); 33 | } 34 | } 35 | 36 | .ac-panel { 37 | transition-property: all; 38 | 39 | p:last-of-type { 40 | margin-bottom: 0; 41 | } 42 | } 43 | 44 | &.is-active { 45 | .ac-trigger { 46 | color: var(--palette-color-primary); 47 | } 48 | 49 | .ac-panel { 50 | padding-top: 10px; 51 | padding-bottom: 10px; 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /modules/theme-default/views/welcome.html: -------------------------------------------------------------------------------- 1 |
2 |

3 | Welcome to Apostrophe Pro 4 |

5 | 6 | {% if not data.user %} 7 |

First time spinning up ApostropheCMS?

8 | 9 |

10 | Use the credentials created during setup with the CLI tool or create a new user with the CLI command: 11 |

12 | 13 |
14 |       
15 |         node app @apostrophecms/user:add myUsername admin
16 |       
17 |     
18 | 19 |

20 | Then log in here 21 |

22 | {% endif %} 23 | 24 |

25 | For a guide on how to configure and customize your project, please check out the Apostrophe documentation. 26 |

27 | 28 | {% if data.user and apos.area.isEmpty(data.page, 'main') %} 29 | 30 |
31 | 32 | {% include 'placeholder.html' %} 33 | 34 |
35 | 36 | {% endif %} 37 |
-------------------------------------------------------------------------------- /modules/theme-default/views/placeholder.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /modules/widgets/slideshow-widget/ui/src/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * For Demo Purposes, this Slideshow is powered by Swiper.js 3 | * You could swap this out another library or your own implementation 4 | */ 5 | 6 | import Swiper from 'swiper'; 7 | import { Navigation, Pagination } from 'swiper/modules'; 8 | 9 | export default () => { 10 | 11 | apos.util.widgetPlayers.slideshow = { 12 | selector: '[data-slideshow-widget]', 13 | player: function (el) { 14 | 15 | /** 16 | * For all available parameters see: 17 | * https://swiperjs.com/swiper-api#parameters 18 | */ 19 | const options = { 20 | navigation: { 21 | nextEl: '.swiper-button-next', 22 | prevEl: '.swiper-button-prev' 23 | }, 24 | 25 | pagination: { 26 | el: '.swiper-pagination' 27 | }, 28 | 29 | modules: [ Navigation, Pagination ], 30 | 31 | ...(el.dataset.slideshowWidget && { 32 | ...JSON.parse(el.dataset.slideshowWidget).swiper 33 | }) 34 | }; 35 | 36 | // eslint-disable-next-line 37 | new Swiper('.swiper', options); 38 | } 39 | }; 40 | 41 | }; 42 | -------------------------------------------------------------------------------- /modules/@apostrophecms-pro/palette/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This is a more advanced setup of Palette where the fields and groups have 3 | * been organized in separate files based on groups. Edit the configs in 4 | * `lib/configs` to if you'd like to change the existing schema or add 5 | * new files in lib/configs to add additional fields and groups. 6 | * The code below should automatically add your fields and groups to Palette 7 | * as long as your config files export an `add` property containing the fields 8 | * and `group` property containing the group definition. 9 | */ 10 | 11 | import color from './lib/configs/color.js'; 12 | import grid from './lib/configs/grid.js'; 13 | import typography from './lib/configs/typography.js'; 14 | 15 | const configs = { 16 | color, 17 | grid, 18 | typography 19 | }; 20 | 21 | export default { 22 | fields: { 23 | add: filter(configs, 'add'), 24 | group: filter(configs, 'group') 25 | } 26 | }; 27 | 28 | function filter(config, key) { 29 | let items = {}; 30 | 31 | for (const config of Object.keys(configs)) { 32 | items = { 33 | ...items, 34 | ...configs[config][key] 35 | }; 36 | }; 37 | 38 | return items; 39 | } 40 | -------------------------------------------------------------------------------- /modules/@apostrophecms-pro/palette/lib/configs/typography.js: -------------------------------------------------------------------------------- 1 | /** 2 | * For additional information on using and configuring Palette, see: 3 | * https://github.com/apostrophecms/palette 4 | */ 5 | 6 | export default { 7 | add: { 8 | fontSize: { 9 | type: 'select', 10 | label: 'Font Size', 11 | help: 'Base font size for body text', 12 | selector: ':root', 13 | property: '--palette-font-size', 14 | unit: 'px', 15 | choices: [ 16 | { 17 | label: '14px', 18 | value: '14' 19 | }, 20 | { 21 | label: '16px', 22 | value: '16' 23 | }, 24 | { 25 | label: '18px', 26 | value: '18' 27 | } 28 | ], 29 | def: '16' 30 | }, 31 | fontColor: { 32 | type: 'color', 33 | label: 'Font Color', 34 | help: 'Base font color for body text', 35 | selector: ':root', 36 | property: '--palette-font-color', 37 | def: '#333333' 38 | } 39 | }, 40 | group: { 41 | typography: { 42 | label: 'Typography', 43 | fields: [ 44 | 'fontSize', 45 | 'fontColor' 46 | ] 47 | } 48 | } 49 | }; 50 | -------------------------------------------------------------------------------- /modules/theme-default/ui/src/scss/components/_placeholder.scss: -------------------------------------------------------------------------------- 1 | .placeholder__svg { 2 | position: fixed; 3 | top: 150px; 4 | right: 20px; 5 | transform: scale(-1, 1) translateY(0); 6 | animation: pointer-float 4000ms ease-in-out infinite; 7 | } 8 | 9 | .placeholder__help { 10 | width: 30%; 11 | margin-right: auto; 12 | margin-left: auto; 13 | padding: 50px; 14 | border: 1px solid $color-gray-15; 15 | color: $color-black; 16 | font-size: rem(14); 17 | text-align: center; 18 | border-radius: 10px; 19 | background-color: $color-light-yellow; 20 | } 21 | 22 | .placeholder__button { 23 | display: inline-block; 24 | padding: 5px; 25 | border: 1px solid $color-black; 26 | border-bottom-color: $color-black; 27 | color: $color-black; 28 | font-family: $font-monospace; 29 | font-size: rem(13); 30 | vertical-align: middle; 31 | background-color: $color-white; 32 | border-radius: 6px; 33 | box-shadow: inset 0 -1px 0 $color-black; 34 | } 35 | 36 | @keyframes pointer-float { 37 | 0% { 38 | transform: translateY(0); 39 | } 40 | 41 | 50% { 42 | transform: translateY(-20px); 43 | } 44 | 45 | 100% { 46 | transform: translateY(0); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /modules/widgets/hero-widget/ui/src/index.scss: -------------------------------------------------------------------------------- 1 | .hero-widget { 2 | --z-index-content: 1; 3 | 4 | position: relative; 5 | display: flex; 6 | align-items: center; 7 | justify-content: center; 8 | margin-bottom: var(--palette-grid-margin); 9 | padding: 20px; 10 | background-size: cover; 11 | background-blend-mode: multiply; 12 | 13 | &.hero-widget--small { 14 | min-height: 20vh; 15 | } 16 | 17 | &.hero-widget--medium { 18 | min-height: 50vh; 19 | } 20 | 21 | &.hero-widget--large { 22 | min-height: 80vh; 23 | } 24 | 25 | p:last-of-type { 26 | margin-bottom: 0; 27 | } 28 | } 29 | 30 | .hero-widget__content { 31 | z-index: var(--z-index-content); 32 | max-width: 620px; 33 | 34 | h1, 35 | h2, 36 | h3, 37 | h4, 38 | h5, 39 | h6 { 40 | margin-bottom: 0; 41 | } 42 | 43 | .button:not(:last-of-type) { 44 | margin-right: 20px; 45 | } 46 | } 47 | 48 | .hero-widget__video, 49 | .hero-widget__screen { 50 | position: absolute; 51 | top: 0; 52 | left: 0; 53 | width: 100%; 54 | height: 100%; 55 | } 56 | 57 | .hero-widget__video { 58 | object-fit: cover; 59 | } 60 | 61 | .hero-widget__screen { 62 | background-color: rgba($color-black, 0.5); 63 | } 64 | 65 | .hero-widget__actions { 66 | margin-top: 20px; 67 | } 68 | -------------------------------------------------------------------------------- /modules/@apostrophecms-pro/palette/lib/configs/color.js: -------------------------------------------------------------------------------- 1 | /** 2 | * For additional information on using and configuring Palette, see: 3 | * https://github.com/apostrophecms/palette 4 | */ 5 | 6 | export default { 7 | add: { 8 | colorPrimary: { 9 | type: 'color', 10 | label: 'Primary Color', 11 | selector: ':root', 12 | property: '--palette-color-primary', 13 | def: '#6236ff' 14 | }, 15 | colorSecondary: { 16 | type: 'color', 17 | label: 'Secondary Color', 18 | selector: ':root', 19 | property: '--palette-color-secondary', 20 | def: '#fe5599' 21 | }, 22 | colorAccent: { 23 | type: 'color', 24 | label: 'Accent Color', 25 | selector: ':root', 26 | property: '--palette-color-accent', 27 | def: '#00cc88' 28 | }, 29 | colorBackground: { 30 | type: 'color', 31 | label: 'Background Color', 32 | help: 'The background color of your website', 33 | selector: ':root', 34 | property: '--palette-color-background' 35 | } 36 | }, 37 | group: { 38 | site: { 39 | label: 'Site Settings', 40 | fields: [ 41 | 'colorPrimary', 42 | 'colorSecondary', 43 | 'colorAccent', 44 | 'colorBackground' 45 | ] 46 | } 47 | } 48 | }; 49 | -------------------------------------------------------------------------------- /modules/theme-default/ui/src/scss/components/_button.scss: -------------------------------------------------------------------------------- 1 | .button { 2 | display: inline-block; 3 | padding: 10px 24px; 4 | color: $color-white; 5 | background: var(--palette-color-primary); 6 | text-decoration: none; 7 | border-radius: 100px; 8 | font-weight: $font-weight-bold; 9 | 10 | &:hover, 11 | &:active { 12 | color: $color-white; 13 | } 14 | 15 | &:hover, 16 | &:focus { 17 | text-decoration: none; 18 | } 19 | 20 | &:hover { 21 | background: color-mix(in srgb, var(--palette-color-primary) 80%, #fff); 22 | } 23 | 24 | &:active { 25 | background: color-mix(in srgb, var(--palette-color-primary) 80%, #000); 26 | } 27 | } 28 | 29 | .color--secondary { 30 | &.button { 31 | background-color: var(--palette-color-secondary); 32 | 33 | &:hover { 34 | background: color-mix(in srgb, var(--palette-color-secondary) 80%, #fff); 35 | } 36 | 37 | &:active { 38 | background: color-mix(in srgb, var(--palette-color-secondary) 80%, #000); 39 | } 40 | } 41 | } 42 | 43 | .color--accent { 44 | &.button { 45 | background-color: var(--palette-color-accent); 46 | 47 | &:hover { 48 | background: color-mix(in srgb, var(--palette-color-accent) 80%, #fff); 49 | } 50 | 51 | &:active { 52 | background: color-mix(in srgb, var(--palette-color-accent) 80%, #000); 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /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-default/ui/src/scss/components/_link.scss: -------------------------------------------------------------------------------- 1 | a:not(.button), 2 | .link:not(.button) { 3 | color: var(--palette-color-primary); 4 | text-decoration: none; 5 | 6 | &:hover, 7 | &:focus { 8 | text-decoration: underline; 9 | } 10 | 11 | &:hover { 12 | color: color-mix(in srgb, var(--palette-color-primary) 80%, #fff); 13 | } 14 | 15 | &:active { 16 | color: color-mix(in srgb, var(--palette-color-primary) 80%, #000); 17 | } 18 | 19 | &:visited { 20 | color: var(--palette-color-primary); 21 | } 22 | } 23 | 24 | .color--secondary { 25 | &.link:not(.button) { 26 | color: var(--palette-color-secondary); 27 | 28 | &:hover { 29 | color: color-mix(in srgb, var(--palette-color-secondary) 80%, #fff); 30 | } 31 | 32 | &:active { 33 | color: color-mix(in srgb, var(--palette-color-secondary) 80%, #000); 34 | } 35 | 36 | &:visited { 37 | color: var(--palette-color-secondary); 38 | } 39 | } 40 | } 41 | 42 | .color--accent { 43 | &.link:not(.button) { 44 | color: var(--palette-color-accent); 45 | 46 | &:hover { 47 | color: color-mix(in srgb, var(--palette-color-accent) 80%, #fff); 48 | } 49 | 50 | &:active { 51 | color: color-mix(in srgb, var(--palette-color-accent) 80%, #000); 52 | } 53 | 54 | &:visited { 55 | color: var(--palette-color-accent); 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "starter-kit-pro-essentials", 3 | "version": "2.0.0", 4 | "description": "A starting point for Apostrophe Pro projects", 5 | "private": true, 6 | "type": "module", 7 | "scripts": { 8 | "build": "cross-env APOS_UPLOADFS_ASSETS=1 NODE_ENV=production bash -c 'node app @apostrophecms/asset:build'", 9 | "//": "because nodemon insists on executing 'start' if it exists, we must distinguish production", 10 | "serve": "cross-env APOS_UPLOADFS_ASSETS=1 NODE_ENV=production npm run start", 11 | "start": "node app", 12 | "dev": "nodemon", 13 | "lint": "eslint . && stylelint '**/*.{scss,vue}'" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "https://github.com/apostrophecms/starter-kit-pro-essentials" 18 | }, 19 | "engines": { 20 | "node": ">=18.0.0" 21 | }, 22 | "author": "Apostrophe Technologies", 23 | "license": "UNLICENSED", 24 | "dependencies": { 25 | "@apostrophecms-pro/document-versions": "^2.3.1", 26 | "@apostrophecms-pro/palette": "^4.3.2", 27 | "@apostrophecms/vite": "^1.0.0", 28 | "accordion-js": "^3.4.1", 29 | "apostrophe": "^4.9.0", 30 | "cross-env": "^10.1.0", 31 | "normalize.css": "^8.0.1", 32 | "require-all": "^3.0.0", 33 | "swiper": "^11.1.3" 34 | }, 35 | "devDependencies": { 36 | "autoprefixer": "^10.4.20", 37 | "eslint-config-apostrophe": "^6.0.1", 38 | "nodemon": "^3.1.1", 39 | "postcss-scss": "^4.0.9", 40 | "stylelint": "^16.6.1", 41 | "stylelint-config-apostrophe": "^4.2.0" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /modules/widgets/card-widget/index.js: -------------------------------------------------------------------------------- 1 | import schema from '../../../lib/schema/link.js'; 2 | 3 | const { 4 | linkText, linkType, linkUrl, _linkFile, _linkPage, linkStyle, linkVariant 5 | } = schema; 6 | 7 | export default { 8 | extend: '@apostrophecms/widget-type', 9 | options: { 10 | label: 'Card', 11 | icon: 'sign-text-icon' 12 | }, 13 | fields: { 14 | add: { 15 | clickable: { 16 | type: 'boolean', 17 | label: 'Clickable', 18 | help: 'Should clicking on this Card take the user somewhere?', 19 | def: true 20 | }, 21 | _image: { 22 | type: 'relationship', 23 | label: 'Card Image', 24 | withType: '@apostrophecms/image', 25 | max: 1 26 | }, 27 | text: { 28 | type: 'area', 29 | label: 'Card Text', 30 | options: { 31 | widgets: { 32 | '@apostrophecms/rich-text': {} 33 | }, 34 | max: 1 35 | } 36 | }, 37 | actions: { 38 | type: 'array', 39 | label: 'Card Actions', 40 | titleField: 'linkText', 41 | fields: { 42 | add: { 43 | linkText, 44 | linkType, 45 | linkUrl, 46 | _linkFile, 47 | _linkPage, 48 | linkStyle, 49 | linkVariant 50 | } 51 | }, 52 | if: { 53 | clickable: false 54 | } 55 | }, 56 | linkType: { 57 | ...linkType, 58 | if: { 59 | clickable: true 60 | } 61 | }, 62 | linkUrl, 63 | _linkFile, 64 | _linkPage 65 | } 66 | } 67 | }; 68 | -------------------------------------------------------------------------------- /modules/widgets/card-widget/views/widget.html: -------------------------------------------------------------------------------- 1 | {% import "fragments/link.html" as link %} 2 | 3 | {% set widget = data.widget %} 4 | 5 |
6 | {% if widget.clickable %} 7 | {% set path = apos.template.linkPath(widget) %} 8 | 9 | {% endif %} 10 | 11 | {% set img = apos.image.first(widget._image) %} 12 | 13 | {% if img %} 14 | {% set imgSrc = apos.attachment.url(img) %} 15 | {% set imgAlt = widget._image._alt or '' %} 16 | 17 | {{ imgAlt }} 18 | {% endif %} 19 | 20 |
21 | 22 | {% area widget, 'text' %} 23 | 24 | {% if widget.actions.length > 0 %} 25 |
26 | 27 | {% for item in widget.actions %} 28 | {% set path = apos.template.linkPath(item) %} 29 | 30 | {% set style = 'button' if item.linkStyle === 'button' else '' %} 31 | {% set variant = 'color--' + item.linkVariant %} 32 | 33 | {% rendercall link.template({ 34 | path: path, 35 | class: style + ' ' + variant, 36 | attributes: { 37 | target: item.linkTarget[0] 38 | } 39 | }) %} 40 | {{ item.linkText }} 41 | {% endrendercall %} 42 | 43 | {% endfor %} 44 | 45 |
46 | {% endif %} 47 | 48 |
49 | 50 | {% if widget.clickable %} 51 |
52 | {% endif %} 53 |
54 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /modules/@apostrophecms/template/index.js: -------------------------------------------------------------------------------- 1 | export default { 2 | init(self) { 3 | self.addFilter({ 4 | isBooleanAttr: self.isBooleanAttr 5 | }); 6 | }, 7 | methods(self) { 8 | return { 9 | /** 10 | * Used in Nunjucks templates to determine if an 11 | * HTML attribute is a boolean attribute 12 | */ 13 | isBooleanAttr(attribute) { 14 | const booleanAttributes = [ 15 | 'allowfullscreen', 16 | 'async', 17 | 'autofocus', 18 | 'autoplay', 19 | 'checked', 20 | 'controls', 21 | 'default', 22 | 'defer', 23 | 'disabled', 24 | 'formnovalidate', 25 | 'inert', 26 | 'ismap', 27 | 'itemscope', 28 | 'loop', 29 | 'multiple', 30 | 'muted', 31 | 'nomodule', 32 | 'novalidate', 33 | 'open', 34 | 'playsinline', 35 | 'readonly', 36 | 'required', 37 | 'reversed', 38 | 'selected' 39 | ]; 40 | 41 | return booleanAttributes.includes(attribute); 42 | } 43 | }; 44 | }, 45 | helpers(self, options) { 46 | return { 47 | /** 48 | * The link schema in `/lib/schema/link.js` allows 49 | * Editors to link to a Page, File, or custom url. 50 | * Used in Nunjucks templates to get the appropriate 51 | * field based on the link type for the href 52 | */ 53 | 54 | linkPath: (link) => { 55 | if (!link) { 56 | return; 57 | } 58 | 59 | const path = { 60 | page: link?._linkPage[0]?._url, 61 | file: link?._linkFile[0]?._url, 62 | custom: link?.linkUrl 63 | }; 64 | 65 | return path[link.linkType]; 66 | } 67 | }; 68 | } 69 | }; 70 | -------------------------------------------------------------------------------- /modules/theme-default/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * A Minimally Styled theme that showcases core Apostrophe functionality 3 | * 4 | * For ease of migration to Assembly later, we've organized the styles and 5 | * site-specific JavaScript in this theme module. 6 | */ 7 | 8 | import path from 'node:path'; 9 | 10 | // Dirname is not available in ESM, so we need to compute it 11 | const __dirname = path.dirname(new URL(import.meta.url).pathname); 12 | 13 | const themeDir = path.resolve( 14 | process.cwd(), 15 | __dirname 16 | ); 17 | 18 | export default { 19 | options: { 20 | alias: 'theme', 21 | // Silence startup warning about the lack of code since this 22 | // is just an empty starting point for your own work 23 | ignoreNoCodeWarning: true, 24 | // Silence startup warning displayed if this module is 25 | // not activated at all, since only one theme module 26 | // will be activated per site 27 | ignoreUnusedFolderWarning: true 28 | }, 29 | /** 30 | * Updates the webpack config so we can use SCSS variables and 31 | * utilities from our theme 32 | */ 33 | webpack: { 34 | extensions: { 35 | themeVariables: { 36 | module: { 37 | rules: [ 38 | { 39 | test: /\.s[ac]ss$/, 40 | use: [ 41 | { 42 | loader: 'sass-loader', 43 | options: { 44 | sourceMap: false, 45 | additionalData: ` 46 | @import "${themeDir}/ui/src/scss/settings/_color"; 47 | @import "${themeDir}/ui/src/scss/settings/_font"; 48 | @import "${themeDir}/ui/src/scss/functions/_rem"; 49 | ` 50 | } 51 | } 52 | ] 53 | } 54 | ] 55 | } 56 | } 57 | } 58 | }, 59 | build: { 60 | vite: { 61 | extensions: { 62 | themeVariables: { 63 | css: { 64 | preprocessorOptions: { 65 | scss: { 66 | additionalData: ` 67 | @import "${themeDir}/ui/src/scss/settings/_color"; 68 | @import "${themeDir}/ui/src/scss/settings/_font"; 69 | @import "${themeDir}/ui/src/scss/functions/_rem"; 70 | ` 71 | } 72 | } 73 | } 74 | } 75 | } 76 | } 77 | } 78 | }; 79 | -------------------------------------------------------------------------------- /modules/widgets/hero-widget/views/widget.html: -------------------------------------------------------------------------------- 1 | {% import 'fragments/link.html' as link %} 2 | 3 | {% set widget = data.widget %} 4 | 5 | {% set img = apos.image.first(widget._image) %} 6 | 7 | {% if img %} 8 | {% set imgUrl = apos.attachment.url(img, { size: 'max' }) %} 9 | {% endif %} 10 | 11 | {% set poster = apos.image.first(widget._videoPoster) %} 12 | 13 | {% if poster %} 14 | {% set posterUrl = apos.attachment.url(poster) %} 15 | {% endif %} 16 | 17 | {% if widget.media === 'image' and widget.backgroundColor %} 18 | {% set background = 'background: center / cover no-repeat url(' + imgUrl + ') ' + widget.backgroundColor %} 19 | {% elif widget.media === 'image' and not widget.backgroundColor %} 20 | {% set background = 'background: center / cover no-repeat url(' + imgUrl + ')' %} 21 | {% elif not widget.media === 'video' and widget.backgroundColor %} 22 | {% set background = 'background-color:' + widget.backgroundColor %} 23 | {% endif %} 24 | 25 |
31 | 32 | {% if widget.media === 'video' %} 33 | 42 |
43 | {% endif %} 44 | 45 |
46 | {% area widget, 'content' %} 47 | 48 | {% if widget.actions.length > 0 %} 49 |
50 | {% for item in widget.actions %} 51 | {% set path = apos.template.linkPath(item) %} 52 | 53 | {% set style = 'button' if item.linkStyle === 'button' else '' %} 54 | {% set variant = 'color--' + item.linkVariant %} 55 | 56 | {% rendercall link.template({ 57 | path: path, 58 | class: style + ' ' + variant, 59 | attributes: { 60 | target: item.linkTarget[0] 61 | } 62 | }) %} 63 | {{ item.linkText }} 64 | {% endrendercall %} 65 | 66 | {% endfor %} 67 |
68 | {% endif %} 69 | 70 |
71 |
72 | -------------------------------------------------------------------------------- /views/layout.html: -------------------------------------------------------------------------------- 1 | {% extends data.outerLayout %} 2 | 3 | {% import 'fragments/link.html' as link %} 4 | 5 | {% block title %} 6 | {% set title = data.piece.title or data.page.title %} 7 | 8 | {{ title }} 9 | 10 | {% if not title %} 11 | {{ 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.') }} 12 | {% endif %} 13 | {% endblock %} 14 | 15 | {% block beforeMain %} 16 |
17 | 18 |
19 | {% set logo = apos.image.first(data.global.logo) %} 20 | 21 | {% if logo %} 22 | 23 | {% else %} 24 | 25 | {% endif %} 26 | 27 | 46 |
47 | 48 |
49 | {% endblock %} 50 | 51 | {% block main %} 52 | {# 53 | Usually, your page templates in the @apostrophecms/pages module will override 54 | this block. It is safe to assume this is where your page-specific content 55 | should go. 56 | #} 57 | {% endblock %} 58 | 59 | {% block afterMain %} 60 |
61 | 70 |
71 | {% endblock %} 72 | -------------------------------------------------------------------------------- /lib/schema/link.js: -------------------------------------------------------------------------------- 1 | /** 2 | * A shared schema field configuration for Links. 3 | * You can use as is in a schema definition or destructure 4 | *destructured to only use the fields you need 5 | */ 6 | 7 | export default { 8 | linkText: { 9 | label: 'Link Text', 10 | type: 'string', 11 | required: true 12 | }, 13 | linkType: { 14 | label: 'Link Type', 15 | type: 'select', 16 | required: true, 17 | choices: [ 18 | { 19 | label: 'Page', 20 | value: 'page' 21 | }, 22 | { 23 | label: 'File', 24 | value: 'file' 25 | }, 26 | { 27 | label: 'Custom URL', 28 | value: 'custom' 29 | } 30 | ] 31 | }, 32 | _linkPage: { 33 | label: 'Page to link', 34 | type: 'relationship', 35 | withType: '@apostrophecms/page', 36 | max: 1, 37 | builders: { 38 | project: { 39 | title: 1, 40 | _url: 1 41 | } 42 | }, 43 | if: { 44 | linkType: 'page' 45 | }, 46 | required: true 47 | }, 48 | _linkFile: { 49 | label: 'File to link', 50 | type: 'relationship', 51 | withType: '@apostrophecms/file', 52 | max: 1, 53 | if: { 54 | linkType: 'file' 55 | }, 56 | required: true 57 | }, 58 | linkUrl: { 59 | label: 'URL for custom link', 60 | type: 'url', 61 | if: { 62 | linkType: 'custom' 63 | }, 64 | required: true 65 | }, 66 | linkTarget: { 67 | label: 'Will the link open a new browser tab?', 68 | type: 'checkboxes', 69 | choices: [ 70 | { 71 | label: 'Open in new tab', 72 | value: '_blank' 73 | } 74 | ] 75 | }, 76 | linkStyle: { 77 | label: 'Link Style', 78 | type: 'select', 79 | choices: [ 80 | { 81 | label: 'Button', 82 | value: 'button' 83 | }, 84 | { 85 | label: 'Inline', 86 | value: 'inline' 87 | } 88 | ], 89 | def: 'button' 90 | }, 91 | linkVariant: { 92 | label: 'Link Color', 93 | type: 'select', 94 | choices: [ 95 | { 96 | label: 'Primary Color', 97 | value: 'primary' 98 | }, 99 | { 100 | label: 'Secondary Color', 101 | value: 'secondary' 102 | }, 103 | { 104 | label: 'Accent Color', 105 | value: 'accent' 106 | } 107 | ], 108 | def: 'primary' 109 | } 110 | }; 111 | -------------------------------------------------------------------------------- /modules/widgets/hero-widget/index.js: -------------------------------------------------------------------------------- 1 | import link from '../../../lib/schema/link.js'; 2 | 3 | export default { 4 | extend: '@apostrophecms/widget-type', 5 | options: { 6 | label: 'Hero', 7 | icon: 'sign-text-icon' 8 | }, 9 | fields: { 10 | add: { 11 | content: { 12 | type: 'area', 13 | label: 'Content', 14 | options: { 15 | widgets: { 16 | '@apostrophecms/rich-text': {} 17 | }, 18 | max: 1 19 | } 20 | }, 21 | actions: { 22 | type: 'array', 23 | label: 'Actions', 24 | titleField: 'linkText', 25 | fields: { 26 | add: link 27 | } 28 | }, 29 | media: { 30 | type: 'select', 31 | label: 'Media', 32 | help: 'Use an image or video for the Hero\'s background', 33 | choices: [ 34 | { 35 | label: 'Image', 36 | value: 'image', 37 | def: true 38 | }, 39 | { 40 | label: 'Video', 41 | value: 'video' 42 | } 43 | ] 44 | }, 45 | _image: { 46 | type: 'relationship', 47 | label: 'Hero Image', 48 | withType: '@apostrophecms/image', 49 | if: { 50 | media: 'image' 51 | } 52 | }, 53 | videoUrl: { 54 | type: 'url', 55 | label: 'External Video URL', 56 | help: 'A URL to an externally hosted .mp4', 57 | if: { 58 | media: 'video' 59 | } 60 | }, 61 | _videoPoster: { 62 | type: 'relationship', 63 | label: 'Video Poster', 64 | help: 'This image will appear as the video is loading', 65 | withType: '@apostrophecms/image', 66 | if: { 67 | background: 'video' 68 | } 69 | }, 70 | size: { 71 | type: 'select', 72 | label: 'Size', 73 | choices: [ 74 | { 75 | label: 'Small', 76 | value: 'small' 77 | }, 78 | { 79 | label: 'Medium', 80 | value: 'medium', 81 | def: true 82 | }, 83 | { 84 | label: 'Large', 85 | value: 'large' 86 | } 87 | ] 88 | }, 89 | backgroundColor: { 90 | type: 'color', 91 | label: 'Background Color', 92 | help: 'This option can also be used to set a screen over an image or video by setting the color\'s opacity to a decimal', 93 | options: { 94 | format: 'rgb' 95 | } 96 | } 97 | } 98 | } 99 | }; 100 | -------------------------------------------------------------------------------- /modules/widgets/column-widget/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Widget configuration that is shared for all columns 3 | */ 4 | const widgets = { 5 | '@apostrophecms/rich-text': {}, 6 | '@apostrophecms/image': { className: 'image-widget' }, 7 | '@apostrophecms/video': {}, 8 | link: {}, 9 | card: {}, 10 | accordion: {} 11 | }; 12 | 13 | export default { 14 | extend: '@apostrophecms/widget-type', 15 | options: { 16 | label: 'Columns', 17 | alias: 'columnsWidget', 18 | icon: 'view-column-icon' 19 | }, 20 | fields: { 21 | add: { 22 | layout: { 23 | type: 'select', 24 | label: 'Column Layout', 25 | required: true, 26 | choices: [ 27 | { 28 | label: '1 column, 100%', 29 | value: '100' 30 | }, 31 | { 32 | label: '2 columns, 50%', 33 | value: '50' 34 | }, 35 | { 36 | label: '2 columns, 66% / 33%', 37 | value: '66-33' 38 | }, 39 | { 40 | label: '2 columns, 75% / 25%', 41 | value: '75-25' 42 | }, 43 | { 44 | label: '2 columns, 33% / 66%', 45 | value: '33-66' 46 | }, 47 | { 48 | label: '2 columns, 25% / 75%', 49 | value: '25-75' 50 | }, 51 | { 52 | label: '3 columns, 33.3%', 53 | value: '33' 54 | }, 55 | { 56 | label: '4 columns, 25%', 57 | value: '25' 58 | } 59 | ], 60 | def: '100' 61 | }, 62 | column1: { 63 | label: 'Column One', 64 | type: 'area', 65 | contextual: true, 66 | options: { 67 | widgets: { 68 | ...widgets, 69 | slideshow: { 70 | swiper: { 71 | /** 72 | * Override the default Swiper configuration by setting any of the 73 | * available Swiper parameters here. Pagination and Navigation are 74 | * enabled by default. 75 | * https://swiperjs.com/swiper-api#parameters 76 | */ 77 | allowTouchMove: false 78 | } 79 | } 80 | } 81 | }, 82 | if: { 83 | $or: [ 84 | { layout: '100' }, 85 | { layout: '50' }, 86 | { layout: '25-75' }, 87 | { layout: '33-66' }, 88 | { layout: '75-25' }, 89 | { layout: '66-33' }, 90 | { layout: '33' }, 91 | { layout: '25' } 92 | ] 93 | } 94 | }, 95 | column2: { 96 | label: 'Column Two', 97 | type: 'area', 98 | contextual: true, 99 | options: { widgets }, 100 | if: { 101 | $or: [ 102 | { layout: '50' }, 103 | { layout: '25-75' }, 104 | { layout: '33-66' }, 105 | { layout: '75-25' }, 106 | { layout: '66-33' }, 107 | { layout: '33' }, 108 | { layout: '25' } 109 | ] 110 | } 111 | }, 112 | column3: { 113 | label: 'Column Three', 114 | type: 'area', 115 | contextual: true, 116 | options: { widgets }, 117 | if: { 118 | $or: [ 119 | { layout: '33' }, 120 | { layout: '25' } 121 | ] 122 | } 123 | }, 124 | column4: { 125 | label: 'Column Four', 126 | type: 'area', 127 | contextual: true, 128 | options: { widgets }, 129 | if: { 130 | $or: [ 131 | { layout: '25' } 132 | ] 133 | } 134 | }, 135 | backgroundColor: { 136 | type: 'color', 137 | label: 'Background Color' 138 | } 139 | } 140 | }, 141 | helpers(self) { 142 | return { 143 | /** 144 | * A helper to get the number of columns the editor has selected 145 | * Used in the template to generate the columns and areas 146 | */ 147 | getColumns(layout) { 148 | 149 | const columns = { 150 | 1: [ '100' ], 151 | 2: [ '50', '66-33', '75-25', '33-66', '25-75' ], 152 | 3: [ '33' ], 153 | 4: [ '25' ] 154 | }; 155 | 156 | let number = 1; 157 | for (const key in columns) { 158 | if (columns[key].includes(layout)) { 159 | number = key; 160 | } 161 | } 162 | return parseInt(number); 163 | } 164 | }; 165 | } 166 | }; 167 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Apostrophe Starter Kit Pro Essentials 2 | 3 | >**Important Notice:** 4 | > 5 | >The code in this starter kit is freely available for use in your projects. However, to use it as a starter kit for a new project, you will need a Pro subscription to install certain dependencies. 6 | > 7 | >To obtain a license, please visit our [website](https://apostrophecms.com/pro) to learn more. You can contact our support team for assistance or purchase a license directly through your [Apostrophe Workspace](https://app.apostrophecms.com/login). 8 | > 9 | >As an alternative to this starter template, we offer several open-source starter kits that are available without any licensing restrictions. These kits, along with links to their GitHub repositories, are listed on our [website](https://apostrophecms.com/starter-kits). 10 | 11 | 13 | - [Apostrophe Starter Kit Pro Essentials](#apostrophe-starter-kit-pro-essentials) 14 | - [Purpose](#purpose) 15 | - [Before you begin](#before-you-begin) 16 | - [First Steps: required before startup](#first-steps-required-before-startup) 17 | - [Setting your shortName](#setting-your-shortname) 18 | - [Disabled File Key](#disabled-file-key) 19 | - [Session Secret](#session-secret) 20 | - [Requirements For Development On Your Computer](#requirements-for-development-on-your-computer) 21 | - [Operating System: Mac, Linux, or Virtual Linux](#operating-system-mac-linux-or-virtual-linux) 22 | - [Software Installation Requirements](#software-installation-requirements) 23 | - [Starting Up In Development](#starting-up-in-development) 24 | - [Site Development](#site-development) 25 | - [The `theme-default` module](#the-theme-default-module) 26 | - [Modern Frontend Assets Without A Custom Build Process](#modern-frontend-assets-without-a-custom-build-process) 27 | - [Frontend Assets With Your Own Build Process](#frontend-assets-with-your-own-build-process) 28 | - [Serving Static Files: Fonts and Static Images](#serving-static-files-fonts-and-static-images) 29 | - [Palette Configuration](#palette-configuration) 30 | - [Hosting and Deployment](#hosting-and-deployment) 31 | - [If we are your host](#if-we-are-your-host) 32 | - [Self-hosting](#self-hosting) 33 | - [Provided widgets](#provided-widgets) 34 | - [`accordion-widget`](#accordion-widget) 35 | - [`card-widget`](#card-widget) 36 | - [`column-widget`](#column-widget) 37 | - [`hero-widget`](#hero-widget) 38 | - [`link-widget`](#link-widget) 39 | - [`slideshow-widget`](#slideshow-widget) 40 | 41 | ## Purpose 42 | 43 | The purpose of this repository and this document is to serve as a quick start for single-site Apostrophe Pro projects. 44 | 45 | As such, it includes several pre-configured Pro modules, although it does not include all, as developer needs vary. Users with a Pro license will have access to additionally `npm install` and configure [any of the Pro modules listed on our website](https://apostrophecms.com/extensions?license=pro). 46 | 47 | It also serves as example code for creating your own custom modules and organizing your files in any ApostropheCMS project. The [section describing the widgets](#provided-widgets) outlines some code practices and features that can be used in your own custom modules. 48 | 49 | This Starter Kit includes: 50 | * Basic Apostrophe Widgets, including an Accordion, Card, Column, Hero, Link, and Slideshow Widget located in `modules/widgets`. 51 | * Basic examples of theme specific front-end code. 52 | 53 | If you have a Pro subscription it will also include: 54 | * Integration of the `@apostrophecms-pro/document-versions` module. 55 | * Example configuration for the `@apostrophecms-pro/palette` module. 56 | 57 | The Pro Essential Starter Kit is intended as a foundation for your projects and should be forked and customized to meet your needs. Once forked, your project can diverge significantly, so we do not recommend merging updates from the starter kit directly. Instead, periodically review the kit repository for ideas and improvements while managing core functionality updates through the usual `npm update` process. 58 | 59 | ## Before you begin 60 | 61 | This document assumes familiarity with Apostrophe concepts. If you are not already familiar with 62 | single-site Apostrophe development, you should pause here and familiarize yourself with the 63 | [ApostropheCMS documentation](https://docs.apostrophecms.org/) as a starting point. 64 | 65 | ## First Steps: required before startup 66 | 67 | > **📌 Note on Dependency Management** 68 | > 69 | > This starter kit ships with `package-lock.json` in `.gitignore` to avoid merge conflicts during development. 70 | > 71 | > **For production use:** Remove `package-lock.json` from `.gitignore` and commit it to lock your dependencies. This ensures stable, reproducible builds. When you're ready to update dependencies, run `npm update` and commit the updated lock file. 72 | 73 | ### Setting your shortName 74 | 75 | **Don't leave this setting in `app.js`, or anything else, set to `CHANGEME`.** The `shortName` should usually be the same 76 | as your repository name, which should usually be the same as the name of your project folder. It will also be the name of 77 | your MongoDB database by default in local development. Do not use punctuation other than hyphens. Good examples include 78 | `smithco-marketing`, to distinguish from other sites built for that company, or just `smithco`. 79 | 80 | This name should be unique among your projects. 81 | 82 | ### Disabled File Key 83 | 84 | In `app.js`, locate `disabledFileKey` and change `CHANGEME` to a random string of your choosing. This is used when disabling access to files in the local backend. Do not leave it set to `CHANGEME`. 85 | 86 | ### Session Secret 87 | 88 | In `app.js`, locate `secret` and change `CHANGEME` to a random string of your choosing. This is used for login session encryption. Do not 89 | leave it set to `CHANGEME`. 90 | 91 | ## Requirements For Development On Your Computer 92 | 93 | ### Operating System: Mac, Linux, or Virtual Linux 94 | 95 | **Your local development environment must be either MacOS or Linux.** If your development computer runs Windows, we recommend development on 96 | Windows Subsystem for Linux (WSL). Microsoft recommends WSL for Node.js development. 97 | 98 | ### Software Installation Requirements 99 | 100 | To test-drive the project in development, make sure you have Apostrophe's usual dependencies on your local machine: 101 | 102 | * MongoDB (5.x or better, we recommend 6.x or better) 103 | * NodeJS (18.x or better) 104 | 105 | For more information see the Apostrophe [Getting Started Tutorial](https://docs.apostrophecms.org/getting-started/setting-up-your-environment.html). 106 | 107 | ## Starting Up In Development 108 | 109 | First, `git clone` the Starter Kit and push it up to your own git repository under an appropriate name, 110 | matching your choice of `shortName`, for ongoing work. 111 | 112 | Then type: 113 | 114 | ``` sh 115 | npm install 116 | ``` 117 | 118 | After installation, add an admin user: 119 | 120 | ``` sh 121 | node app @apostrophecms/user:add admin admin 122 | ``` 123 | 124 | Enter a password when prompted. 125 | 126 | Next launch the application: 127 | 128 | ``` sh 129 | npm run dev 130 | ``` 131 | 132 | When ready, visit: 133 | 134 | ``` sh 135 | http://localhost:3000/login 136 | ``` 137 | 138 | And log in with the admin account you created for the site. Then make some simple edits to the homepage. 139 | 140 | ## Site Development 141 | 142 | Right now we have basic example templates in place. Let's look at where to put our code to customize the experience. 143 | 144 | > Again, if you are not already familiar with single-site Apostrophe development, you should pause here and 145 | [review the ApostropheCMS documentation](https://docs.apostrophecms.org/) as a starting point. 146 | 147 | Just like in any single-site Apostrophe project, modules are configured in `app.js`, and module code lives 148 | in subdirectories of `modules/`. 149 | 150 | Feel free to add page templates and modules as you would with an ordinary open-source Apostrophe project. You can `npm install` modules 151 | like `@apostrophecms/blog` and configure them in a normal way. With a Pro license you can also install additional Pro modules like 152 | `@apostrophecms-pro/advanced-permission` if they are necessary to your plans. 153 | 154 | ### The `theme-default` module 155 | 156 | Apostrophe allows frontend assets to be placed in any module, but as a suggested 157 | code organization, this project contains a `theme-default` module with a sample 158 | `ui/src/index.js` and `ui/src/index.scss` as described further below. This approach 159 | simplifies later migration to an Assembly multisite project especially if you anticipate 160 | implementing a choice of themes at that time. 161 | 162 | The `theme-default` module also modifies the base webpack build to incorporate SCSS variables for colors and fonts. This is included to demonstrate how to set up centralized theme management with global variables in one place. It also adds a function for converting font sizes from `px` to `rem`. While this is a useful function that is used in several of the `theme-default` stylesheets, it primarily serves to illustrate how SCSS functions can be added to your project. A similar approach would be used to add in any SCSS mixins that subsequent stylesheets utilize. 163 | 164 | The `views` folder of the `theme-default` module has two markup files that provide the HTML for the `@apostrophecms/home-page`. The main `welcome.html` file contains a conditional block for displaying different content based on whether there is a user is logged in or not. It has a second conditional block for displaying markup from the `placeholder.html` file if no content has been added to the page. You can choose to maintain this structure and modify the `welcome.html` file, or change the `modules/@apostrophecms/home-page/views/page.html` to contain your own markup. 165 | 166 | #### Modern Frontend Assets Without A Custom Build Process 167 | 168 | There is no need for a custom Webpack configuration in most cases. Specifically, you can follow our documentation and place your modern 169 | JavaScript code in the `ui/src/index.js` file of any module, or use `import` statements in that file to import it there. As noted in 170 | our documentation, it is **important for `ui/src/index.js` to export a function as its default export.** This function will be invoked 171 | to initialize your module at a safe time when `apos.http`, `apos.util`, etc. are already available. 172 | 173 | You may also place Sass SCSS code in the `ui/src/index.scss` file of any module, and use `import` statements in that file to bring in 174 | more Sass SCSS code. 175 | 176 | #### Frontend Assets With Your Own Build Process 177 | 178 | A sample webpack build is not included as standard equipment, as `ui/src` suffices for most needs, and the built-in, automatic Webpack configuration 179 | can be extended, per our public documentation and as illustrated. However, if you prefer to create your own webpack configuration, the typical pattern 180 | is to configure the output of your build process to be a `ui/public/something.js` file in any module in your Apostrophe project. 181 | 182 | #### Serving Static Files: Fonts and Static Images 183 | 184 | If you need to serve static files, you can do this much as you would in standalone Apostrophe development. 185 | 186 | The folder `public` maps to `/` in the URL space of a site. For instance, `public/fonts/myfile.ttf` maps to the URL `/fonts/myfile.ttf`. 187 | For assets like favicons and fonts, you can add `link` tags to the `standardHead` block already present in 188 | `modules/@apostrophecms/template/views/outerLayout.html`. 189 | 190 | ### Palette Configuration 191 | 192 | The `@apostrophecms-pro/palette` module requires Pro licensing and allows styles to be edited visually on the site. It is configured in `modules/@apostrophecms-pro/palette/index.js`. 193 | There you can specify the selectors, CSS properties, and field types to be used to manipulate color, font size, font family 194 | and other aspects of the site as a whole. 195 | 196 | For complete information and a sample configuration, see the [@apostrophecms-pro/palette module documentation](https://apostrophecms.com/extensions/palette-extension). 197 | 198 | > Note that like all other changes, palette changes do not take place for logged-out users until the editor clicks "Publish." 199 | 200 | ## Hosting and Deployment 201 | 202 | ### If we are your host 203 | 204 | If we are hosting Apostrophe for you, then you can deploy updates to your staging and production environments by pushing to your 205 | `staging` and `production` git branches, respectively. You will receive notifications in our shared Slack channel, including links to 206 | access the deployment progress logs. 207 | 208 | Apostrophe will complete the build steps listed in the `build` npm command provided in `package.json` and also execute any 209 | database migrations before restarting with your newest code. 210 | 211 | ### Self-hosting 212 | 213 | Self-hosting is also an option if you have not chosen to host with us. We offer several how-to guides on this, such as 214 | [Ubuntu hosting setup](https://v3.docs.apostrophecms.org/cookbook/ubuntu-hosting.html) and 215 | [deploying Apostrophe in the cloud with Heroku](https://v3.docs.apostrophecms.org/cookbook/deploying-to-heroku.html). The main new element 216 | with Apostrophe Pro is making sure that the `npm install` command has access to the `@apostrophecms-pro` modules during installation. 217 | 218 | Here is the simplest recipe to achieve that: 219 | 220 | 1. Make sure you have been granted access to install our pro modules (that is, make sure `npm install` works for you in this project). If 221 | not, reach out to our support team. 222 | 2. [Log into the npm website](https://www.npmjs.com/) using the account that has been granted access. 223 | 3. Pull down the personal menu and select "Access Tokens." 224 | 4. Select "Classic Token" from the "Generate Tokens" dropdown. 225 | 5. Give the token a name, such as "ops-deployment". 226 | 6. Select "Read-Only." 227 | 7. **Immediately** copy and paste the token that is displayed. It will not be shown again. 228 | 8. Create a `.npmrc` file in the root of your project, like this: 229 | 230 | ``` 231 | //registry.npmjs.org/:_authToken=YOUR-TOKEN-HERE 232 | ``` 233 | 234 | It is also possible to use an environment variable for additional security, depending on your preferred deployment and hosting 235 | solution. Please see the npm documentation for more information on that option. 236 | 237 | ## Provided widgets 238 | There are six basic widget modules located in the `modules/widgets` folder of this starter kit. This supplements the core `rich-text`, `image`, `video`, and `html` widgets. They can be altered to fit the design and functionality of your project or act as a blueprint to build your own custom widgets. Both the `hero` and `column` widgets have been added to the `main` area of the `@apostrophecms/home-page`. The remainder of the basic widgets have been added to the areas of the `column` widget as described below. 239 | 240 | If you look at the `app.js` file you won't see these widget modules in the `modules` object. Instead, they are being registered using the `nestedModuleSubdirs` property. Setting this property to `true` will cause Apostrophe to register all the modules listed in the `modules.js` file of any subfolder in the project-level `modules` folder. You can choose to organize any custom modules, such as grouping all of your piece-types, to keep your `modules` folder and the `app.js` file less cluttered. Note that if you choose to move any of the provided widgets out of the current folder you will need to add them to the `app.js` and remove them from the `modules/widgets/modules.js` file. If you choose to keep this structure, any custom widgets you add to the folder need to be listed in the `modules.js` file. 241 | 242 | All the styling for the supplied widgets, except for the partials added in the custom webpack extensions added in the `theme-default` module, is located in the `ui/src/index.scss` file of each module. You can choose to maintain this structure, move the styling to the `theme-default/ui/src/scss` folder, or organize them in a different project-specific manner. Note that for them to be included in the standard webpack build, they need to be imported into a `/ui/src/index.scss` file. 243 | 244 | ### `accordion-widget` 245 | The `accordion-widget` implements an accordion element powered by the [`accordion-js` npm package](https://www.npmjs.com/package/accordion-js). You can read about additional configuration options in the documentation of that package. The module consists of a main `index.js` file with the content schema fields, plus a `views` folder that contains a `widget.html` file with the Nunjucks markup for the accordion. 246 | 247 | Finally, there is the `ui/src` folder that contains the `index.scss` stylesheet and the `index.js` file that contains the JavaScript that is delivered to the frontend and powers the accordion using a [widget player](https://docs.apostrophecms.org/guide/custom-widgets.html#client-side-javascript-for-widgets). Any custom widgets that require client-side code should be structured in this same way. Data is passed from the schema fields to the browser for use in the player script by adding it to a data attributes in the template. 248 | 249 | ### `card-widget` 250 | The `card-widget` creates a simple card with optional image and text. The card can be made directly clickable, or can have links and buttons added. The schema fields for these elements are provided by the `lib/schema/link.js` file, which serves as a model for implementing reusable parts of widgets. These same schema fields are reused in the `hero` and `link` widgets and can be used in your custom project widgets. The markup for the links is imported into the `card-widget` template from the `views/fragments/link.html` file using the [`rendercall` helper](https://docs.apostrophecms.org/guide/fragments.html#inserting-markup-with-rendercall). This is present in a simpler form in the `links-widget`. Again, all your custom modules (not just widgets) can utilize fragments to replicate similar areas of markup in this same way. 251 | 252 | ### `column-widget` 253 | The `column-widget` implements one method of adding a user-selected number of columns to a page. It uses a select field and conditional fields that restrict the number of columns based on the value of the select. Each column has an area with widgets for the `link`, `card`, and `accordion` basic widgets, plus the core `rich-text`, `image`, and `video` widgets. These are added through a shared configuration object that defines the available widgets for each column. The first column additionally adds the basic `slideshow` widget. 254 | 255 | The widget also provides a `helper(self)` customization function that is used in the Nunjucks template. Depending on the value of the select field it returns the correct number of columns. The `helper(self)` functions can be used in your custom modules to provide computed values from data passed back from the markup. 256 | 257 | ### `hero-widget` 258 | The `hero-widget` implements a hero element with image or color background, text and links. As stated above, this module reuses the `links.js` helper file. It also demonstrates how to use `relationship` schema fields to add an image or video for the background. 259 | 260 | ### `link-widget` 261 | This simple widget adds either a button or inline-link. As described for the `card-widget`, It utilizes the `lib/schema/link.js` helper file and the `views/fragments/link.html` fragment. Within the widget template there is a `rendercall` that passes data from the widget schema fields to the fragment. 262 | 263 | ### `slideshow-widget` 264 | The `slideshow-widget`, much like the `accordion-widget`, utilizes client-side JavaScript. For this widget the `ui/src/index.js` is adding the [`swiper.js` package](https://swiperjs.com/) to the player. 265 | --------------------------------------------------------------------------------