├── .gitignore ├── frontend ├── Procfile ├── tsconfig.json ├── src │ ├── env.d.ts │ ├── templates │ │ ├── DefaultPage.astro │ │ ├── index.js │ │ ├── ArticleShowPage.astro │ │ ├── HomePage.astro │ │ └── ArticleIndexPage.astro │ ├── widgets │ │ ├── RichTextWidget.astro │ │ ├── index.js │ │ ├── ImageWidget.astro │ │ ├── VideoWidget.astro │ │ ├── LinkWidget.astro │ │ ├── RowsWidget.astro │ │ ├── AccordionWidget.astro │ │ └── GridLayoutWidget.astro │ ├── lib │ │ ├── homepage-defaults.js │ │ ├── use-site-config.js │ │ └── attachments.js │ ├── components │ │ ├── Pagination.astro │ │ ├── ArticlesFilter.astro │ │ ├── Header.astro │ │ └── Footer.astro │ ├── layouts │ │ └── article-layouts │ │ │ ├── ShowMagazine.astro │ │ │ ├── ShowMinimal.astro │ │ │ ├── Standard.astro │ │ │ ├── HeroGrid.astro │ │ │ ├── ListAside.astro │ │ │ └── ShowFullWidth.astro │ ├── pages │ │ └── [...slug].astro │ └── styles │ │ └── main.scss ├── .vscode │ ├── extensions.json │ └── launch.json ├── public │ ├── fonts │ │ └── fontawesome │ │ │ ├── fa-solid-900.woff2 │ │ │ ├── fa-brands-400.woff2 │ │ │ ├── fa-regular-400.woff2 │ │ │ └── fa-v4compatibility.woff2 │ ├── images │ │ ├── image-widget-placeholder.jpg │ │ └── missing-icon.svg │ ├── scripts │ │ ├── dynamic-navbar-padding.js │ │ └── VideoWidget.js │ └── favicon.svg ├── .gitignore ├── postcss.config.js ├── README.md ├── package.json └── astro.config.mjs ├── backend ├── modules │ ├── @apostrophecms │ │ ├── home-page │ │ │ ├── views │ │ │ │ └── page.html │ │ │ └── index.js │ │ ├── page │ │ │ ├── views │ │ │ │ └── notFound.html │ │ │ └── index.js │ │ ├── image-widget │ │ │ ├── index.js │ │ │ └── public │ │ │ │ └── preview.svg │ │ ├── express │ │ │ └── index.js │ │ ├── video-widget │ │ │ ├── index.js │ │ │ └── public │ │ │ │ └── preview.svg │ │ ├── asset │ │ │ └── index.js │ │ ├── attachment │ │ │ └── index.js │ │ ├── user │ │ │ └── index.js │ │ ├── admin-bar │ │ │ └── index.js │ │ ├── rich-text-widget │ │ │ ├── index.js │ │ │ └── public │ │ │ │ └── preview.svg │ │ ├── i18n │ │ │ └── index.js │ │ └── settings │ │ │ └── index.js │ ├── link-widget │ │ ├── index.js │ │ └── public │ │ │ └── preview.svg │ ├── @apostrophecms-pro │ │ └── palette │ │ │ └── index.js │ ├── card-widget │ │ ├── index.js │ │ └── public │ │ │ └── preview.svg │ ├── hero-widget │ │ ├── index.js │ │ └── public │ │ │ └── preview.svg │ ├── slideshow-widget │ │ ├── index.js │ │ └── public │ │ │ └── preview.svg │ ├── default-page │ │ └── index.js │ ├── rows-widget │ │ ├── public │ │ │ └── preview.svg │ │ └── index.js │ ├── accordion-widget │ │ ├── public │ │ │ └── preview.svg │ │ └── index.js │ ├── grid-layout-widget │ │ └── public │ │ │ └── preview.svg │ ├── author │ │ └── index.js │ ├── article │ │ └── index.js │ └── article-page │ │ └── index.js ├── .eslintignore ├── public │ └── images │ │ └── logo.png ├── .eslintrc ├── .gitignore ├── scripts │ ├── load-starter-content │ └── update-starter-content ├── README.md ├── LICENSE ├── lib │ ├── helpers │ │ ├── color-options.js │ │ ├── typography-options.js │ │ └── area-widgets.js │ └── schema-mixins │ │ ├── slideshow-fields.js │ │ ├── link-fields.js │ │ ├── card-fields.js │ │ └── hero-fields.js ├── app.js ├── package.json └── views │ └── layout.html ├── LICENSE └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | .DS_Store -------------------------------------------------------------------------------- /frontend/Procfile: -------------------------------------------------------------------------------- 1 | web: node dist/server/entry.mjs 2 | -------------------------------------------------------------------------------- /frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "astro/tsconfigs/base" 3 | } 4 | -------------------------------------------------------------------------------- /backend/modules/@apostrophecms/home-page/views/page.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | -------------------------------------------------------------------------------- /backend/modules/@apostrophecms/page/views/notFound.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | -------------------------------------------------------------------------------- /backend/.eslintignore: -------------------------------------------------------------------------------- 1 | /public/apos-frontend 2 | /data/temp 3 | /apos-build 4 | /modules/asset/ui/public 5 | -------------------------------------------------------------------------------- /frontend/src/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /backend/public/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apostrophecms/astro-testbed/main/backend/public/images/logo.png -------------------------------------------------------------------------------- /frontend/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["astro-build.astro-vscode"], 3 | "unwantedRecommendations": [] 4 | } 5 | -------------------------------------------------------------------------------- /frontend/public/fonts/fontawesome/fa-solid-900.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apostrophecms/astro-testbed/main/frontend/public/fonts/fontawesome/fa-solid-900.woff2 -------------------------------------------------------------------------------- /frontend/public/images/image-widget-placeholder.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apostrophecms/astro-testbed/main/frontend/public/images/image-widget-placeholder.jpg -------------------------------------------------------------------------------- /frontend/public/fonts/fontawesome/fa-brands-400.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apostrophecms/astro-testbed/main/frontend/public/fonts/fontawesome/fa-brands-400.woff2 -------------------------------------------------------------------------------- /frontend/public/fonts/fontawesome/fa-regular-400.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apostrophecms/astro-testbed/main/frontend/public/fonts/fontawesome/fa-regular-400.woff2 -------------------------------------------------------------------------------- /frontend/public/fonts/fontawesome/fa-v4compatibility.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apostrophecms/astro-testbed/main/frontend/public/fonts/fontawesome/fa-v4compatibility.woff2 -------------------------------------------------------------------------------- /backend/modules/@apostrophecms/image-widget/index.js: -------------------------------------------------------------------------------- 1 | export default { 2 | options: { 3 | label: 'Image', 4 | description: 'Display images on your page', 5 | previewImage: 'svg' 6 | } 7 | }; 8 | -------------------------------------------------------------------------------- /backend/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "apostrophe" 4 | ], 5 | "globals": { 6 | "apos": true 7 | }, 8 | "rules": { 9 | "no-var": "error", 10 | "no-console": 0 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /backend/modules/@apostrophecms/express/index.js: -------------------------------------------------------------------------------- 1 | export default { 2 | options: { 3 | session: { 4 | // If this still says `undefined`, set a real secret! 5 | secret: undefined 6 | } 7 | } 8 | }; 9 | -------------------------------------------------------------------------------- /backend/modules/@apostrophecms/video-widget/index.js: -------------------------------------------------------------------------------- 1 | export default { 2 | options: { 3 | label: 'Video', 4 | description: 'Add a video player from services like YouTube', 5 | previewImage: 'svg' 6 | } 7 | }; 8 | -------------------------------------------------------------------------------- /backend/modules/@apostrophecms/asset/index.js: -------------------------------------------------------------------------------- 1 | export default { 2 | // When not in production, refresh the page on restart 3 | options: { 4 | refreshOnRestart: true, 5 | breakpointPreviewMode: { 6 | enabled: false 7 | }, 8 | hmr: false 9 | } 10 | }; 11 | -------------------------------------------------------------------------------- /frontend/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "command": "./node_modules/.bin/astro dev", 6 | "name": "Development server", 7 | "request": "launch", 8 | "type": "node-terminal" 9 | } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /frontend/src/templates/DefaultPage.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import AposArea from '@apostrophecms/apostrophe-astro/components/AposArea.astro'; 3 | const { page, user, query } = Astro.props.aposData; 4 | const { main } = page; 5 | --- 6 |
7 | 8 |
9 | -------------------------------------------------------------------------------- /frontend/public/scripts/dynamic-navbar-padding.js: -------------------------------------------------------------------------------- 1 | const adjustBodyPadding = () => { 2 | const navbar = document.querySelector('.navbar.is-fixed-top'); 3 | if (navbar) { 4 | document.body.style.paddingTop = `${navbar.offsetHeight}px`; 5 | } 6 | }; 7 | 8 | document.addEventListener('DOMContentLoaded', adjustBodyPadding); -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # build output 2 | dist/ 3 | # generated types 4 | .astro/ 5 | 6 | # dependencies 7 | node_modules/ 8 | 9 | # logs 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | pnpm-debug.log* 14 | 15 | 16 | # environment variables 17 | .env 18 | .env.production 19 | 20 | # macOS-specific files 21 | .DS_Store 22 | -------------------------------------------------------------------------------- /backend/modules/@apostrophecms/attachment/index.js: -------------------------------------------------------------------------------- 1 | export default { 2 | options: { 3 | addFileGroups: [ 4 | { 5 | name: 'videos', 6 | label: 'Videos', 7 | extensions: [ 8 | 'mp4', 9 | 'webm', 10 | 'gif' 11 | ], 12 | extensionMaps: {} 13 | } 14 | ] 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /backend/.gitignore: -------------------------------------------------------------------------------- 1 | # build output 2 | /public/apos-minified 3 | /apos-build 4 | /public/apos-frontend 5 | /modules/asset/ui/public/site.js 6 | 7 | # dependencies 8 | node_modules/ 9 | 10 | # logs 11 | npm-debug.log 12 | 13 | # content 14 | /public/uploads 15 | 16 | # local env data 17 | /data 18 | /data/temp/uploadfs 19 | 20 | # macOS-specific files 21 | .DS_Store -------------------------------------------------------------------------------- /backend/modules/link-widget/index.js: -------------------------------------------------------------------------------- 1 | import linkFields from '../../lib/schema-mixins/link-fields.js'; 2 | export default { 3 | extend: '@apostrophecms/widget-type', 4 | options: { 5 | label: 'Link', 6 | icon: 'link-icon', 7 | previewImage: 'svg', 8 | description: 'Add a button that links to a page or URL' 9 | }, 10 | fields: { 11 | add: linkFields 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /backend/modules/@apostrophecms-pro/palette/index.js: -------------------------------------------------------------------------------- 1 | export default { 2 | fields: { 3 | add: { 4 | backgroundColor: { 5 | type: 'color', 6 | label: 'Page Background', 7 | selector: 'body', 8 | property: 'color' 9 | } 10 | }, 11 | group: { 12 | colors: { 13 | label: 'Colors', 14 | fields: ['backgroundColor'] 15 | } 16 | } 17 | } 18 | }; 19 | -------------------------------------------------------------------------------- /backend/modules/card-widget/index.js: -------------------------------------------------------------------------------- 1 | import { cardFields, cardGroups } from '../../lib/schema-mixins/card-fields.js'; 2 | 3 | export default { 4 | extend: '@apostrophecms/widget-type', 5 | options: { 6 | label: 'Card', 7 | icon: 'sign-text-icon', 8 | previewImage: 'svg', 9 | description: 'Cards from simple to complex.' 10 | }, 11 | fields: { 12 | add: cardFields, 13 | group: cardGroups 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /backend/modules/hero-widget/index.js: -------------------------------------------------------------------------------- 1 | import heroFields from '../../lib/schema-mixins/hero-fields.js'; 2 | 3 | export default { 4 | extend: '@apostrophecms/widget-type', 5 | options: { 6 | label: 'Hero Section', 7 | icon: 'sign-text-icon', 8 | previewImage: 'svg', 9 | description: 'A full-width or split hero section with background image or video.' 10 | }, 11 | fields: { 12 | add: heroFields 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /frontend/postcss.config.js: -------------------------------------------------------------------------------- 1 | import postcssViewportToContainerToggle from 'postcss-viewport-to-container-toggle'; 2 | 3 | export default { 4 | // Is the site still editable in production like a normal Apos Site, 5 | // if yes we need the plugin for all builds 6 | // plugins: [ 7 | // postcssViewportToContainerToggle({ 8 | // modifierAttr: 'data-breakpoint-preview-mode', 9 | // debug: true, 10 | // transform: null 11 | // }) 12 | // ] 13 | } -------------------------------------------------------------------------------- /backend/modules/@apostrophecms/user/index.js: -------------------------------------------------------------------------------- 1 | export default { 2 | fields: { 3 | add: { 4 | firstName: { 5 | type: 'string', 6 | label: 'First Name' 7 | }, 8 | lastName: { 9 | type: 'string', 10 | label: 'Last Name' 11 | } 12 | }, 13 | group: { 14 | account: { 15 | label: 'Account', 16 | fields: [ 17 | 'firstName', 18 | 'lastName' 19 | ] 20 | } 21 | } 22 | } 23 | }; 24 | -------------------------------------------------------------------------------- /frontend/src/templates/index.js: -------------------------------------------------------------------------------- 1 | import HomePage from './HomePage.astro'; 2 | import DefaultPage from './DefaultPage.astro'; 3 | import ArticleIndexPage from './ArticleIndexPage.astro'; 4 | import ArticleShowPage from './ArticleShowPage.astro'; 5 | 6 | const templateComponents = { 7 | '@apostrophecms/home-page': HomePage, 8 | 'default-page': DefaultPage, 9 | 'article-page:index': ArticleIndexPage, 10 | 'article-page:show': ArticleShowPage 11 | }; 12 | 13 | export default templateComponents; 14 | -------------------------------------------------------------------------------- /backend/modules/slideshow-widget/index.js: -------------------------------------------------------------------------------- 1 | import slideshowFields from '../../lib/schema-mixins/slideshow-fields.js'; 2 | 3 | export default { 4 | extend: '@apostrophecms/widget-type', 5 | options: { 6 | label: 'Slideshow', 7 | icon: 'view-carousel-outline', 8 | previewImage: 'svg', 9 | description: 'A slideshow of images with optional titles and content.' 10 | }, 11 | icons: { 12 | 'view-carousel-outline': 'ViewCarouselOutline' 13 | }, 14 | fields: { 15 | add: slideshowFields 16 | } 17 | }; 18 | -------------------------------------------------------------------------------- /backend/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 | name: 'blog', 16 | label: 'Blog', 17 | items: [ 18 | 'article', 19 | 'author' 20 | ] 21 | } 22 | ] 23 | } 24 | }; 25 | -------------------------------------------------------------------------------- /frontend/src/widgets/RichTextWidget.astro: -------------------------------------------------------------------------------- 1 | --- 2 | /** 3 | * Rich Text Widget Component for ApostropheCMS 4 | * Renders editable rich text content within an Apostrophe area 5 | * 6 | * @component 7 | * @param {Object} props 8 | * @param {Object} props.widget - ApostropheCMS widget object 9 | * @param {string} props.widget.content - HTML content from Apostrophe's rich text editor 10 | */ 11 | 12 | const { widget } = Astro.props; 13 | const { content } = widget; 14 | --- 15 |
16 | 17 |
18 | -------------------------------------------------------------------------------- /backend/modules/default-page/index.js: -------------------------------------------------------------------------------- 1 | import { getWidgetGroups } from '../../lib/helpers/area-widgets.js'; 2 | 3 | export default { 4 | extend: '@apostrophecms/page-type', 5 | options: { 6 | label: 'Default Page' 7 | }, 8 | fields: { 9 | add: { 10 | main: { 11 | type: 'area', 12 | options: getWidgetGroups({ 13 | includeLayouts: true 14 | }) 15 | } 16 | }, 17 | group: { 18 | basics: { 19 | label: 'Basics', 20 | fields: [ 21 | 'main' 22 | ] 23 | } 24 | } 25 | } 26 | }; 27 | -------------------------------------------------------------------------------- /frontend/README.md: -------------------------------------------------------------------------------- 1 | # apollo-frontend 2 | 3 | This is an Astro-based website, with editable content powered by ApostropheCMS. 4 | 5 | The project consist of the code in this sub-directory that provides an Astro-based frontend with customization to use ApostropheCMS as a backend, and the sub-directory with the ApostropheCMS code. For more information, consult the README at the root of this repository. 6 | 7 | To run the project locally, set the `APOS_EXTERNAL_FRONT_KEY` environment variable to the same string in both projects. Then issue the command `npm run dev` and point your browser at `localhost:4321`. 8 | -------------------------------------------------------------------------------- /frontend/src/templates/ArticleShowPage.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import FullWidth from '../layouts/article-layouts/ShowFullWidth.astro'; 3 | import Magazine from '../layouts/article-layouts/ShowMagazine.astro'; 4 | import Minimal from '../layouts/article-layouts/ShowMinimal.astro'; 5 | 6 | const { 7 | page, 8 | piece 9 | } = Astro.props.aposData; 10 | 11 | const layouts = { 12 | fullWidth: FullWidth, 13 | magazine: Magazine, 14 | minimal: Minimal 15 | }; 16 | 17 | const SelectedLayout = layouts[page.showLayout] || FullWidth; 18 | --- 19 | 20 |
21 | 22 |
-------------------------------------------------------------------------------- /backend/modules/@apostrophecms/rich-text-widget/index.js: -------------------------------------------------------------------------------- 1 | export default { 2 | options: { 3 | label: 'Rich Text', 4 | description: 'Add styled text to your page', 5 | previewImage: 'svg', 6 | className: 'o-widget', 7 | imageStyles: [ 8 | { 9 | value: 'image-full', 10 | label: 'Full Width' 11 | }, 12 | { 13 | value: 'image-center', 14 | label: 'Center' 15 | }, 16 | { 17 | value: 'image-float-left', 18 | label: 'Float Left' 19 | }, 20 | { 21 | value: 'image-float-right', 22 | label: 'Float Right' 23 | } 24 | ] 25 | } 26 | }; -------------------------------------------------------------------------------- /backend/scripts/load-starter-content: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | if [ -z "$APOS_MONGODB_URI" ]; then 4 | APOS_MONGODB_URI="mongodb://localhost:27017/apollo" 5 | fi 6 | 7 | curl -o /tmp/starter-database.snapshot https://static.apostrophecms.com/apollo/starter-database.snapshot && 8 | mongodb-snapshot-read --erase --from=/tmp/starter-database.snapshot --to=$APOS_MONGODB_URI && 9 | curl -o /tmp/starter-uploads.tar https://static.apostrophecms.com/apollo/starter-uploads.tar && 10 | mkdir -p public/uploads && 11 | (cd public/uploads && tar -xf /tmp/starter-uploads.tar) && 12 | echo "Starter content loaded. Now set an admin password." && 13 | node app @apostrophecms/user:add admin admin 14 | -------------------------------------------------------------------------------- /backend/README.md: -------------------------------------------------------------------------------- 1 | # apollo-backend 2 | 3 | This is an Astro-based website, with editable content powered by ApostropheCMS. 4 | 5 | The project consist of the code in this sub-directory that provides an ApostropheCMS-based backend with customization to use Astro as a frontend, and the sub-directory with the Astro code. For more information, consult the README at the root of this repository. 6 | 7 | To run the project locally, set the `APOS_EXTERNAL_FRONT_KEY` environment variable to the same string in both projects. Then issue the command `npm run dev` and point your browser at `localhost:4321`. Your ApostropheCMS instance will be served at `localhost:3000`, but only provides information about the project status. 8 | -------------------------------------------------------------------------------- /backend/modules/@apostrophecms/page/index.js: -------------------------------------------------------------------------------- 1 | // This configures the @apostrophecms/page module to add a "home" page type to the 2 | // pages menu 3 | 4 | export default { 5 | options: { 6 | builders: { 7 | children: true, 8 | ancestors: { 9 | children: { 10 | depth: 2, 11 | relationships: false 12 | } 13 | } 14 | }, 15 | types: [ 16 | { 17 | name: 'default-page', 18 | label: 'Default' 19 | }, 20 | { 21 | name: 'article-page', 22 | label: 'Article Page' 23 | }, 24 | { 25 | name: '@apostrophecms/home-page', 26 | label: 'Home' 27 | } 28 | ] 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /frontend/public/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | -------------------------------------------------------------------------------- /backend/scripts/update-starter-content: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Our team uses this script to update the starter content. You may 4 | # safely remove this script from your own project 5 | 6 | if [ -z "$APOS_MONGODB_URI" ]; then 7 | APOS_MONGODB_URI="mongodb://localhost:27017/apollo" 8 | fi 9 | 10 | mongodb-snapshot-write --to=/tmp/starter-database.snapshot --from=$APOS_MONGODB_URI \ 11 | --exclude=aposUsersSafe,aposLocks,aposBearerTokens,aposCache,aposNotifications,sessions \ 12 | --filter-aposDocs='{"type":{"$ne":"@apostrophecms/user"}}' && 13 | scp /tmp/starter-database.snapshot static@static.apostrophecms.com:/opt/static/static/apollo/starter-database.snapshot && 14 | tar -cf /tmp/starter-uploads.tar -C ./public/uploads . && 15 | scp /tmp/starter-uploads.tar static@static.apostrophecms.com:/opt/static/static/apollo/starter-uploads.tar && 16 | echo "Starter content updated." 17 | -------------------------------------------------------------------------------- /backend/modules/@apostrophecms/video-widget/public/preview.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | Video Widget Preview 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /backend/modules/@apostrophecms/i18n/index.js: -------------------------------------------------------------------------------- 1 | export default { 2 | options: { 3 | locales: { 4 | en: { 5 | label: 'English' 6 | }, 7 | fr: { 8 | label: 'French', 9 | prefix: '/fr' 10 | }, 11 | es: { 12 | label: 'Spanish', 13 | prefix: '/es' 14 | }, 15 | de: { 16 | label: 'German', 17 | prefix: '/de' 18 | } 19 | }, 20 | adminLocales: [ 21 | // you can add an object for as many or few of the locales as desired 22 | // the user will only be able to select from these locales in the personal preferences menu 23 | { 24 | label: 'English', 25 | value: 'en' 26 | }, 27 | { 28 | label: 'Spanish', 29 | value: 'es' 30 | }, 31 | { 32 | label: 'French', 33 | value: 'fr' 34 | }, 35 | { 36 | label: 'German', 37 | value: 'de' 38 | } 39 | ] 40 | } 41 | }; 42 | -------------------------------------------------------------------------------- /frontend/src/widgets/index.js: -------------------------------------------------------------------------------- 1 | import RichTextWidget from './RichTextWidget.astro'; 2 | import ImageWidget from './ImageWidget.astro'; 3 | import VideoWidget from './VideoWidget.astro'; 4 | import GridLayoutWidget from './GridLayoutWidget.astro'; 5 | import AccordionWidget from './AccordionWidget.astro'; 6 | import CardWidget from './CardWidget.astro'; 7 | import HeroWidget from './HeroWidget.astro'; 8 | import LinkWidget from './LinkWidget.astro'; 9 | import SlideshowWidget from './SlideshowWidget.astro'; 10 | import RowsWidget from './RowsWidget.astro'; 11 | 12 | const widgetComponents = { 13 | '@apostrophecms/rich-text': RichTextWidget, 14 | '@apostrophecms/image': ImageWidget, 15 | '@apostrophecms/video': VideoWidget, 16 | 'grid-layout': GridLayoutWidget, 17 | 'accordion': AccordionWidget, 18 | 'card': CardWidget, 19 | 'hero': HeroWidget, 20 | 'link': LinkWidget, 21 | 'slideshow': SlideshowWidget, 22 | 'rows': RowsWidget 23 | }; 24 | 25 | export default widgetComponents; 26 | -------------------------------------------------------------------------------- /backend/modules/@apostrophecms/image-widget/public/preview.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | Image Widget Preview 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /backend/modules/rows-widget/public/preview.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | Row Widget Preview 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /backend/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2024 Apostrophe Technologies 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 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2025 Apostrophe Technologies 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 14 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 15 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 16 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 17 | DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 18 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE 19 | OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /backend/lib/helpers/color-options.js: -------------------------------------------------------------------------------- 1 | const colorOptionsHelper = { 2 | getColorOptions() { 3 | return [ 4 | // Main colors 5 | { label: 'White', value: 'white' }, 6 | { label: 'Black', value: 'black' }, 7 | { label: 'Light', value: 'light' }, 8 | { label: 'Dark', value: 'dark' }, 9 | { label: 'Primary', value: 'primary' }, 10 | { label: 'Link', value: 'link' }, 11 | { label: 'Info', value: 'info' }, 12 | { label: 'Success', value: 'success' }, 13 | { label: 'Warning', value: 'warning' }, 14 | { label: 'Danger', value: 'danger' }, 15 | { label: 'Black Bis', value: 'black-bis' }, 16 | { label: 'Black Ter', value: 'black-ter' }, 17 | { label: 'Grey Darker', value: 'grey-darker' }, 18 | { label: 'Grey Dark', value: 'grey-dark' }, 19 | { label: 'Grey', value: 'grey' }, 20 | { label: 'Grey Light', value: 'grey-light' }, 21 | { label: 'Grey Lighter', value: 'grey-lighter' }, 22 | { label: 'White Ter', value: 'white-ter' }, 23 | { label: 'White Bis', value: 'white-bis' }, 24 | { label: 'Transparent', value: 'transparent' } 25 | ]; 26 | } 27 | }; 28 | 29 | export default colorOptionsHelper; 30 | -------------------------------------------------------------------------------- /frontend/public/images/missing-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 8 | 10 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "apollo-frontend", 3 | "type": "module", 4 | "description": "Astro frontend for the ApostropheCMS + Astro Apollo template", 5 | "version": "1.0.0", 6 | "scripts": { 7 | "dev": "astro dev", 8 | "start": "node ./dist/server/entry.mjs", 9 | "build": "astro build", 10 | "serve": "HOST=0.0.0.0 node ./dist/server/entry.mjs", 11 | "preview": "DEBUG=* astro preview", 12 | "astro": "astro" 13 | }, 14 | "engines": { 15 | "node": "^18.17.1 || ^20.3.0 || >=22.0.0", 16 | "npm": ">=9.0.0" 17 | }, 18 | "dependencies": { 19 | "@apostrophecms/apostrophe-astro": "github:apostrophecms/apostrophe-astro", 20 | "@astrojs/node": "^9.1.1", 21 | "@fortawesome/fontawesome-svg-core": "^6.6.0", 22 | "@fortawesome/free-brands-svg-icons": "^6.6.0", 23 | "@fortawesome/free-regular-svg-icons": "^6.6.0", 24 | "@fortawesome/free-solid-svg-icons": "^6.6.0", 25 | "accordion-js": "^3.3.4", 26 | "astro": "^5.3.1", 27 | "bulma": "^1.0.2", 28 | "dayjs": "^1.11.10", 29 | "postcss-viewport-to-container-toggle": "^1.0.0", 30 | "vite": "^5.0.7" 31 | }, 32 | "devDependencies": { 33 | "sass-embedded": "^1.81.0" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /backend/modules/@apostrophecms/rich-text-widget/public/preview.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | Rich Text Widget Preview 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /backend/modules/hero-widget/public/preview.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | Hero Widget Preview 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /frontend/src/templates/HomePage.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import Hero from '../widgets/HeroWidget.astro'; 3 | import Slideshow from '../widgets/SlideshowWidget.astro'; 4 | import AposArea from '@apostrophecms/apostrophe-astro/components/AposArea.astro'; 5 | 6 | import { heroDefaults, slideshowDefaults } from '../lib/homepage-defaults.js'; 7 | 8 | const { page, user, query, global } = Astro.props.aposData; 9 | const { layout = 'foundation', heroSection = {}, showcaseSlideshow = {}, main = {} } = page; 10 | 11 | // Use default data if no data is provided 12 | const heroData = heroSection?.mainContent?.title ? heroSection : heroDefaults; 13 | const slideshowData = showcaseSlideshow?.slides?.length ? showcaseSlideshow : slideshowDefaults; 14 | 15 | --- 16 |
17 | {/* Foundation Layout */} 18 | {layout === 'foundation' && ( 19 | <> 20 | {heroData && } 21 | 22 | )} 23 | 24 | {/* Showcase Layout */} 25 | {layout === 'showcase' && ( 26 | <> 27 | {slideshowData && ( 28 |
29 | 30 |
31 | )} 32 | 33 | )} 34 | 35 | {/* Main Content Area */} 36 |
37 | 38 |
39 |
40 | 41 | -------------------------------------------------------------------------------- /backend/modules/slideshow-widget/public/preview.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | Slideshow Widget Preview 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /backend/modules/card-widget/public/preview.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | Card Widget Preview 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /frontend/src/lib/homepage-defaults.js: -------------------------------------------------------------------------------- 1 | export const heroDefaults = { 2 | layout: 'split', 3 | splitSide: 'right', 4 | background: 'image', 5 | height: 'large', 6 | contentAlignment: 'left', 7 | mainContent: { 8 | title: 'Welcome to Your Site', 9 | subtitle: 'Start customizing your homepage', 10 | titleColor: 'primary', 11 | subtitleColor: 'primary', 12 | } 13 | }; 14 | 15 | export const slideshowDefaults = { 16 | slideDuration: 5000, 17 | transitionSpeed: 300, 18 | autoplay: true, 19 | showControls: true, 20 | slides: [ 21 | { 22 | slideTitle: 'Welcome to Our Site', 23 | titleColor: 'warning', 24 | cardContent: 'Edit this slideshow to add your own content and images.', 25 | contentColor: 'success', 26 | textBlockBackground: 'dark', 27 | textBlockOpacity: '65', 28 | }, 29 | { 30 | slideTitle: 'Customizable Design', 31 | titleColor: 'primary', 32 | cardContent: 'Add your own slides with content.', 33 | contentColor: 'warning', 34 | textBlockBackground: 'dark', 35 | textBlockOpacity: '65', 36 | }, 37 | { 38 | slideTitle: 'Getting Started', 39 | titleColor: 'info', 40 | cardContent: 'Click edit to begin customizing your slideshow.', 41 | contentColor: 'white', 42 | textBlockBackground: 'dark', 43 | textBlockOpacity: '65', 44 | } 45 | ] 46 | }; 47 | -------------------------------------------------------------------------------- /backend/modules/@apostrophecms/settings/index.js: -------------------------------------------------------------------------------- 1 | export default { 2 | options: { 3 | subforms: { 4 | changePassword: { 5 | // This will have `protection: true` automatically. 6 | fields: [ 'password' ] 7 | }, 8 | displayName: { 9 | // The default `title` field is labeled 'Display Name' in the `@apostrophecms/user` module. 10 | // Changing this field will **not** change the Username or Slug of the user. 11 | fields: [ 'title' ], 12 | reload: true 13 | }, 14 | fullName: { 15 | // Passing in a label so that it doesn't use the label for `lastName` 16 | // These fields need to be added to the user schema 17 | label: 'Full Name', 18 | // Schema fields added at project level 19 | fields: [ 'lastName', 'firstName' ], 20 | preview: '{{ firstName }} {{lastName}}' 21 | }, 22 | // The `adminLocales` option **must** be configured in the `@apostrophecms/i18n` module for this to be allowed 23 | adminLocale: { 24 | fields: [ 'adminLocale' ] 25 | } 26 | }, 27 | groups: { 28 | account: { 29 | label: 'Account', 30 | subforms: [ 'displayName', 'fullName', 'changePassword' ] 31 | }, 32 | preferences: { 33 | label: 'Preferences', 34 | // The `adminLocales` option **must** be configured in the `@apostrophecms/i18n` module for this to be allowed 35 | subforms: [ 'adminLocale' ] 36 | } 37 | } 38 | } 39 | }; 40 | -------------------------------------------------------------------------------- /frontend/astro.config.mjs: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'astro/config'; 2 | import node from '@astrojs/node'; 3 | import apostrophe from '@apostrophecms/apostrophe-astro'; 4 | import path from 'path'; 5 | 6 | // https://astro.build/config 7 | export default defineConfig({ 8 | output: "server", 9 | server: { 10 | port: process.env.PORT ? parseInt(process.env.PORT) : 4321, 11 | // Required for some hosting, like Heroku 12 | // host: true 13 | }, 14 | adapter: node({ 15 | mode: 'standalone' 16 | }), 17 | integrations: [apostrophe({ 18 | aposHost: 'http://localhost:3000', 19 | widgetsMapping: './src/widgets', 20 | templatesMapping: './src/templates', 21 | includeResponseHeaders: [ 22 | 'content-security-policy', 23 | 'strict-transport-security', 24 | 'x-frame-options', 25 | 'referrer-policy', 26 | 'cache-control' 27 | ], 28 | excludeRequestHeaders: [ 29 | // For hosting on multiple servers, block the host header 30 | // 'host' 31 | ] 32 | })], 33 | vite: { 34 | css: { 35 | preprocessorOptions: { 36 | scss: { 37 | quietDeps: true 38 | } 39 | } 40 | }, 41 | ssr: { 42 | // Do not externalize the @apostrophecms/apostrophe-astro plugin, we need 43 | // to be able to use virtual: URLs there 44 | noExternal: ['@apostrophecms/apostrophe-astro'] 45 | } 46 | }, 47 | css: { 48 | preprocessorOptions: { 49 | scss: { 50 | api: 'modern-compiler', 51 | } 52 | } 53 | } 54 | }); -------------------------------------------------------------------------------- /frontend/src/components/Pagination.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import setParameter from '@apostrophecms/apostrophe-astro/lib/aposSetQueryParameter.js'; 3 | 4 | const { class: className = '', currentPage, totalPages, url } = Astro.props; 5 | 6 | const pages = Array.from({ length: totalPages }, (_, i) => ({ 7 | number: i + 1, 8 | current: i + 1 === currentPage, 9 | url: setParameter(url, 'page', i + 1) 10 | })); 11 | 12 | const showPrevNext = totalPages > 1; 13 | const prevUrl = currentPage > 1 ? setParameter(url, 'page', currentPage - 1) : null; 14 | const nextUrl = currentPage < totalPages ? setParameter(url, 'page', currentPage + 1) : null; 15 | --- 16 | 17 | -------------------------------------------------------------------------------- /backend/modules/accordion-widget/public/preview.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | Accordion Widget Preview 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /backend/app.js: -------------------------------------------------------------------------------- 1 | import apostrophe from 'apostrophe'; 2 | 3 | export default apostrophe({ 4 | root: import.meta, 5 | shortName: 'astro-testbed', 6 | modules: { 7 | // Apostrophe module configuration 8 | // ******************************* 9 | // 10 | // Most configuration occurs in the respective modules' directories. 11 | // See modules/@apostrophecms/page/index.js for an example. 12 | // 13 | // Any modules that are not present by default in Apostrophe must at least 14 | // have a minimal configuration here to turn them on: `moduleName: {}` 15 | 16 | // ********************************* 17 | '@apostrophecms/vite': {}, 18 | 19 | // `className` options set custom CSS classes for Apostrophe core widgets. 20 | '@apostrophecms/rich-text-widget': {}, 21 | '@apostrophecms/image-widget': {}, 22 | '@apostrophecms/video-widget': {}, 23 | '@apostrophecms/asset': {}, 24 | 25 | // Custom extensions 26 | // Make sure to set the `APOS_BASE_URL` environment variable to the base URL of your Apostrophe site 27 | '@apostrophecms/seo': {}, 28 | '@apostrophecms-pro/palette': {}, 29 | '@apostrophecms-pro/document-versions': {}, 30 | '@apostrophecms-pro/advanced-permission-group': {}, 31 | '@apostrophecms-pro/advanced-permission': {}, 32 | '@apostrophecms-pro/doc-template-library': {}, 33 | 34 | // pieces 35 | article: {}, 36 | author: {}, 37 | 38 | // pages 39 | 'default-page': {}, 40 | 'article-page': {}, 41 | 42 | // widgets 43 | 'grid-layout-widget': {}, 44 | 'accordion-widget': {}, 45 | 'card-widget': {}, 46 | 'hero-widget': {}, 47 | 'link-widget': {}, 48 | 'slideshow-widget': {}, 49 | 'rows-widget': {} 50 | } 51 | }); 52 | -------------------------------------------------------------------------------- /backend/modules/grid-layout-widget/public/preview.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | Bitmap 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /frontend/src/components/ArticlesFilter.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import setParameter from '@apostrophecms/apostrophe-astro/lib/aposSetQueryParameter.js'; 3 | 4 | const { currentUrl, currentCategory, articles } = Astro.props; 5 | 6 | // Extract unique categories from articles 7 | const uniqueCategories = [...new Set(articles.map(article => article.category))].filter(Boolean); 8 | 9 | // Create category objects array with counts 10 | const categories = [ 11 | { 12 | label: 'All Articles', 13 | value: '', 14 | count: articles.length 15 | }, 16 | ...uniqueCategories.map(cat => ({ 17 | label: cat.charAt(0).toUpperCase() + cat.slice(1), 18 | value: cat, 19 | count: articles.filter(article => article.category === cat).length 20 | })) 21 | ]; 22 | 23 | function buildUrl(currentUrl, category) { 24 | let url = currentUrl; 25 | if (category) { 26 | url = setParameter(url, 'category', category); 27 | } else { 28 | url = setParameter(url, 'category', null); // Removes the parameter 29 | } 30 | if (url.searchParams.get('page')) { 31 | url = setParameter(url, 'page', null); // Reset to first page 32 | } 33 | return url; 34 | } 35 | --- 36 | 37 |
38 |

Filter by Category

39 |
40 | 49 |
50 |
51 | 52 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "combined-astro-starter-kit", 3 | "version": "1.0.0", 4 | "description": "Combined ApostropheCMS plus Astro starter kit", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "postinstall": "npm run install-frontend && npm run install-backend && echo '\\n💡 combined npm install complete'", 9 | "install-frontend": "cd frontend && echo '\\n💡 running npm install in frontend/' && npm install", 10 | "install-backend": "cd backend && echo '\\n💡 running npm install in backend/' && npm install", 11 | "update": "npm run update-frontend && npm run update-backend && echo '\\n💡 combined npm update complete'", 12 | "update-frontend": "cd frontend && echo '\\n💡 running npm update in frontend/' && npm update", 13 | "update-backend": "cd backend && echo '\\n💡 running npm update in backend/' && npm update", 14 | "build": "npm run build-frontend && npm run build-backend && echo '\\n💡 combined npm build complete'", 15 | "build-frontend": "cd frontend && echo '\\n💡 running npm run build in frontend/' && npm run build", 16 | "build-backend": " cd backend && echo '\\n💡 running npm run build in backend/' && npm run build", 17 | "migrate": "cd backend && npm run migrate", 18 | "serve-frontend": "cd frontend && npm run serve", 19 | "serve-backend": "cd backend && npm run serve", 20 | "load-starter-content": "cd backend && npm run load-starter-content" 21 | }, 22 | "repository": { 23 | "type": "git", 24 | "url": "git+https://github.com/apostrophecms/combined-astro-starter-kit.git" 25 | }, 26 | "keywords": [ 27 | "astro", 28 | "apostrophecms", 29 | "apostrophe" 30 | ], 31 | "author": "Apostrophe Technologies Inc.", 32 | "license": "MIT", 33 | "bugs": { 34 | "url": "https://github.com/apostrophecms/combined-astro-starter-kit/issues" 35 | }, 36 | "homepage": "https://github.com/apostrophecms/combined-astro-starter-kit#readme" 37 | } 38 | -------------------------------------------------------------------------------- /backend/modules/author/index.js: -------------------------------------------------------------------------------- 1 | export default { 2 | extend: '@apostrophecms/piece-type', 3 | options: { 4 | label: 'Author', 5 | pluralLabel: 'Authors' 6 | }, 7 | fields: { 8 | add: { 9 | title: { 10 | type: 'string', 11 | label: 'Name' 12 | }, 13 | profileImage: { 14 | type: 'area', 15 | options: { 16 | max: 1, 17 | widgets: { 18 | '@apostrophecms/image': {} 19 | } 20 | } 21 | }, 22 | biography: { 23 | type: 'area', 24 | options: { 25 | widgets: { 26 | '@apostrophecms/rich-text': {} 27 | } 28 | } 29 | }, 30 | email: { 31 | type: 'email', 32 | label: 'Email Address' 33 | }, 34 | socialLinks: { 35 | type: 'array', 36 | label: 'Social Media Links', 37 | titleField: 'platform', 38 | inline: true, 39 | fields: { 40 | add: { 41 | platform: { 42 | type: 'select', 43 | choices: [ 44 | { label: 'Twitter/X', value: 'twitter' }, 45 | { label: 'LinkedIn', value: 'linkedin' }, 46 | { label: 'GitHub', value: 'github' }, 47 | { label: 'Personal Website', value: 'website' } 48 | ] 49 | }, 50 | url: { 51 | type: 'url', 52 | label: 'Profile URL' 53 | } 54 | } 55 | } 56 | }, 57 | _articles: { 58 | type: 'relationshipReverse', 59 | withType: 'article', 60 | reverseOf: '_author' 61 | } 62 | }, 63 | group: { 64 | basics: { 65 | label: 'Basic Info', 66 | fields: ['title', 'email', 'profileImage', '_articles'] 67 | }, 68 | content: { 69 | label: 'Content', 70 | fields: ['biography'] 71 | }, 72 | social: { 73 | label: 'Social Media', 74 | fields: ['socialLinks'] 75 | } 76 | } 77 | } 78 | }; 79 | -------------------------------------------------------------------------------- /frontend/src/widgets/ImageWidget.astro: -------------------------------------------------------------------------------- 1 | --- 2 | /** 3 | * Image widget component for ApostropheCMS 4 | * Renders a responsive image with proper sizing and focal point support. 5 | * Content editors only need to: 6 | * - Upload an image 7 | * - Set alt text 8 | * - Optionally set a focal point 9 | * 10 | * @param {Object} props 11 | * @param {Object} props.widget - The widget data from ApostropheCMS 12 | * @returns {astro.AstroComponent} - Rendered image component 13 | */ 14 | 15 | import { 16 | getAttachmentUrl, 17 | getAttachmentSrcset, 18 | getFocalPoint, 19 | getWidth, 20 | getHeight 21 | } from '../lib/attachments.js'; 22 | 23 | const { widget, imageOptions } = Astro.props; 24 | const placeholder = widget?.aposPlaceholder; 25 | const image = widget?._image?.[0]; 26 | 27 | // Handle missing image gracefully 28 | if (!placeholder && !image) { 29 | console.warn('Image widget: No image or placeholder provided'); 30 | } 31 | 32 | const src = placeholder 33 | ? '/images/image-widget-placeholder.jpg' 34 | : getAttachmentUrl(image); 35 | const srcset = placeholder ? '' : getAttachmentSrcset(image); 36 | const objectPosition = placeholder ? 'center center' : getFocalPoint(image); 37 | const alt = image?.alt || ''; 38 | 39 | // Only add width/height if they exist to prevent layout shift 40 | const width = image ? getWidth(image) : undefined; 41 | const height = image ? getHeight(image) : undefined; 42 | const aspectRatio = width && height ? `${width}/${height}` : undefined; 43 | --- 44 | 45 | 54 | 55 | {alt} -------------------------------------------------------------------------------- /frontend/src/widgets/VideoWidget.astro: -------------------------------------------------------------------------------- 1 | --- 2 | /** 3 | * Video Widget Astro Component 4 | * 5 | * This component integrates with ApostropheCMS's video widget and renders videos 6 | * using a custom web component. It supports multiple video providers through 7 | * ApostropheCMS's oEmbed implementation - editors can simply paste any standard 8 | * video sharing URL from supported platforms like YouTube or Vimeo. 9 | * 10 | * @typedef {Object} VideoWidget 11 | * @property {boolean} [aposPlaceholder] - Indicates if this is a placeholder instance in the editor 12 | * @property {Object} [video] - Video data object 13 | * @property {string} [video.url] - The URL of the video to embed 14 | * @property {string} [video.title] - The title of the video 15 | * 16 | * @param {Object} props - Component properties 17 | * @param {VideoWidget} [props.widget] - The widget data from ApostropheCMS 18 | */ 19 | 20 | const { widget } = Astro.props; 21 | 22 | // Use placeholder video for the widget editor UI when no video is selected 23 | const placeholder = widget?.aposPlaceholder ? 'true' : ''; 24 | const url = widget?.video?.url; 25 | const videoTitle = widget?.video?.title; 26 | 27 | // Default placeholder video used in the ApostropheCMS editor 28 | const PLACEHOLDER_VIDEO_URL = 'https://youtu.be/Q5UX9yexEyM'; 29 | --- 30 | 31 | 42 | 43 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /backend/modules/accordion-widget/index.js: -------------------------------------------------------------------------------- 1 | import colorOptionsHelper from '../../lib/helpers/color-options.js'; 2 | import { getWidgetGroups } from '../../lib/helpers/area-widgets.js'; 3 | 4 | export default { 5 | extend: '@apostrophecms/widget-type', 6 | options: { 7 | label: 'Accordion', 8 | icon: 'arrow-down-drop-circle', 9 | previewImage: 'svg', 10 | description: 'An accordion of items with headers and content.' 11 | }, 12 | icons: { 13 | 'arrow-down-drop-circle': 'ArrowDownDropCircle' 14 | }, 15 | fields: { 16 | add: { 17 | itemBackgroundColor: { 18 | type: 'select', 19 | label: 'Item Background Color', 20 | choices: colorOptionsHelper.getColorOptions(), 21 | def: 'white' 22 | }, 23 | allowMultipleOpen: { 24 | type: 'boolean', 25 | label: 'Allow Multiple Items Open', 26 | def: false 27 | }, 28 | openIndex: { 29 | type: 'integer', 30 | label: 'Default Open Item (-1 for none, 1 for first item, etc...)', 31 | def: -1 32 | }, 33 | items: { 34 | type: 'array', 35 | label: 'Items', 36 | titleField: 'header', 37 | inline: true, 38 | fields: { 39 | add: { 40 | header: { 41 | type: 'string', 42 | label: 'Header' 43 | }, 44 | headerColor: { 45 | type: 'select', 46 | label: 'Header Color', 47 | choices: colorOptionsHelper.getColorOptions(), 48 | def: 'black' 49 | }, 50 | headerAlignment: { 51 | type: 'select', 52 | label: 'Header Alignment', 53 | choices: [ 54 | { label: 'Left', value: 'left' }, 55 | { label: 'Center', value: 'center' }, 56 | { label: 'Right', value: 'right' } 57 | ], 58 | def: 'left' 59 | }, 60 | content: { 61 | type: 'area', 62 | label: 'Content', 63 | options: getWidgetGroups({ 64 | exclude: ['accordion'] 65 | }) 66 | } 67 | } 68 | } 69 | } 70 | } 71 | } 72 | }; 73 | -------------------------------------------------------------------------------- /backend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "apollo-backend", 3 | "version": "1.0.0", 4 | "description": "Apostrophe backend for the ApostropheCMS + Astro Apollo template", 5 | "main": "app.js", 6 | "type": "module", 7 | "scripts": { 8 | "start": "node app", 9 | "dev": "nodemon", 10 | "build": "NODE_ENV=production node app @apostrophecms/asset:build", 11 | "serve": "NODE_ENV=production node app", 12 | "release": "npm install && npm run build && npm run migrate", 13 | "migrate": "NODE_ENV=production node app @apostrophecms/migration:migrate", 14 | "load-starter-content": "./scripts/load-starter-content", 15 | "update-starter-content": "./scripts/update-starter-content" 16 | }, 17 | "engines": { 18 | "node": "^18.17.1 || ^20.3.0 || >=22.0.0", 19 | "npm": ">=9.0.0" 20 | }, 21 | "nodemonConfig": { 22 | "delay": 1000, 23 | "verbose": true, 24 | "watch": [ 25 | "./app.js", 26 | "./modules/**/*", 27 | "./lib/**/*.js", 28 | "./views/**/*.html" 29 | ], 30 | "ignoreRoot": [ 31 | ".git" 32 | ], 33 | "ignore": [ 34 | "**/ui/apos/", 35 | "**/ui/src/", 36 | "**ui/public/", 37 | "locales/*.json", 38 | "public/uploads/", 39 | "public/apos-frontend/*.js", 40 | "data/" 41 | ], 42 | "ext": "json, js, html, scss, vue" 43 | }, 44 | "repository": { 45 | "type": "git", 46 | "url": "https://github.com/apostrophecms/apollo" 47 | }, 48 | "author": "Apostrophe Technologies, Inc.", 49 | "license": "MIT", 50 | "dependencies": { 51 | "@apostrophecms-pro/advanced-permission": "apostrophecms/advanced-permission", 52 | "@apostrophecms-pro/doc-template-library": "apostrophecms/doc-template-library", 53 | "@apostrophecms-pro/document-versions": "apostrophecms/document-versions", 54 | "@apostrophecms-pro/palette": "apostrophecms/palette", 55 | "@apostrophecms/mongodb-snapshot": "apostrophecms/mongodb-snapshot", 56 | "@apostrophecms/seo": "apostrophecms/seo", 57 | "@apostrophecms/vite": "apostrophecms/vite", 58 | "apostrophe": "apostrophecms/apostrophe", 59 | "normalize.css": "^8.0.1" 60 | }, 61 | "devDependencies": { 62 | "eslint": "^8.0.0", 63 | "eslint-config-apostrophe": "^4.0.0", 64 | "nodemon": "^3.0.1" 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /backend/lib/helpers/typography-options.js: -------------------------------------------------------------------------------- 1 | const textOptionsHelper = { 2 | getTextSizes() { 3 | return [ 4 | { label: 'Default', value: '' }, 5 | { label: 'Size 1 (3rem)', value: 'is-size-1' }, 6 | { label: 'Size 2 (2.5rem)', value: 'is-size-2' }, 7 | { label: 'Size 3 (2rem)', value: 'is-size-3' }, 8 | { label: 'Size 4 (1.5rem)', value: 'is-size-4' }, 9 | { label: 'Size 5 (1.25rem)', value: 'is-size-5' }, 10 | { label: 'Size 6 (1rem)', value: 'is-size-6' }, 11 | { label: 'Size 7 (0.75rem)', value: 'is-size-7' } 12 | ]; 13 | }, 14 | 15 | getResponsiveSizes() { 16 | const sizes = ['1', '2', '3', '4', '5', '6', '7']; 17 | const breakpoints = ['mobile', 'tablet', 'desktop', 'widescreen']; 18 | 19 | const choices = [{ label: 'Default', value: '' }]; 20 | 21 | breakpoints.forEach(breakpoint => { 22 | sizes.forEach(size => { 23 | choices.push({ 24 | label: `Size ${size} (${breakpoint})`, 25 | value: `is-size-${size}-${breakpoint}` 26 | }); 27 | }); 28 | }); 29 | 30 | return choices; 31 | }, 32 | 33 | getTextWeights() { 34 | return [ 35 | { label: 'Default', value: '' }, 36 | { label: 'Light (300)', value: 'has-text-weight-light' }, 37 | { label: 'Normal (400)', value: 'has-text-weight-normal' }, 38 | { label: 'Medium (500)', value: 'has-text-weight-medium' }, 39 | { label: 'Semi-Bold (600)', value: 'has-text-weight-semibold' }, 40 | { label: 'Bold (700)', value: 'has-text-weight-bold' } 41 | ]; 42 | }, 43 | 44 | getTextTransforms() { 45 | return [ 46 | { label: 'Default', value: '' }, 47 | { label: 'Capitalized', value: 'is-capitalized' }, 48 | { label: 'Lowercase', value: 'is-lowercase' }, 49 | { label: 'Uppercase', value: 'is-uppercase' }, 50 | { label: 'Italic', value: 'is-italic' } 51 | ]; 52 | }, 53 | 54 | getTextAlignments() { 55 | return [ 56 | { label: 'Default', value: '' }, 57 | { label: 'Left', value: 'has-text-left' }, 58 | { label: 'Centered', value: 'has-text-centered' }, 59 | { label: 'Right', value: 'has-text-right' }, 60 | { label: 'Justified', value: 'has-text-justified' } 61 | ]; 62 | } 63 | }; 64 | 65 | export default textOptionsHelper; 66 | -------------------------------------------------------------------------------- /backend/modules/article/index.js: -------------------------------------------------------------------------------- 1 | import { getWidgetGroups } from '../../lib/helpers/area-widgets.js'; 2 | 3 | export default { 4 | extend: '@apostrophecms/piece-type', 5 | options: { 6 | label: 'Article', 7 | pluralLabel: 'Articles', 8 | shortcut: 'Shift+Alt+A' 9 | }, 10 | fields: { 11 | add: { 12 | category: { 13 | type: 'select', 14 | label: 'Category', 15 | help: 'Choose a category for this article', 16 | choices: [ 17 | { 18 | label: 'News', 19 | value: 'news' 20 | }, 21 | { 22 | label: 'Opinion', 23 | value: 'opinion' 24 | }, 25 | { 26 | label: 'Feature', 27 | value: 'feature' 28 | }, 29 | { 30 | label: 'Review', 31 | value: 'review' 32 | } 33 | ] 34 | }, 35 | _heroImage: { 36 | type: 'relationship', 37 | label: 'Hero Image', 38 | withType: '@apostrophecms/image', 39 | max: 1 40 | }, 41 | excerpt: { 42 | type: 'string', 43 | textarea: true, 44 | label: 'Article Excerpt', 45 | help: 'Brief summary for listings and previews' 46 | }, 47 | mainContent: { 48 | type: 'area', 49 | options: getWidgetGroups({ 50 | includeLayouts: true 51 | }) 52 | 53 | }, 54 | _author: { 55 | type: 'relationship', 56 | label: 'Author', 57 | withType: 'author', 58 | withRelationships: [ '_articles' ] 59 | }, 60 | publishDate: { 61 | lable: 'Publication Date', 62 | type: 'date', 63 | required: true 64 | }, 65 | _related: { 66 | type: 'relationship', 67 | label: 'Related Articles', 68 | withType: 'article', 69 | max: 4, 70 | builders: { 71 | project: { 72 | title: 1, 73 | _url: 1 74 | } 75 | }, 76 | withRelationships: [ '_heroImage' ] 77 | } 78 | }, 79 | group: { 80 | basics: { 81 | label: 'Basic Info', 82 | fields: ['_author', 'category', 'publishDate', '_related'] 83 | }, 84 | content: { 85 | label: 'Content', 86 | fields: ['_heroImage', 'excerpt', 'mainContent'] 87 | } 88 | } 89 | } 90 | }; 91 | -------------------------------------------------------------------------------- /backend/modules/@apostrophecms/home-page/index.js: -------------------------------------------------------------------------------- 1 | import { getWidgetGroups } from '../../../lib/helpers/area-widgets.js'; 2 | import heroFields from '../../../lib/schema-mixins/hero-fields.js'; 3 | import slideshowFields from '../../../lib/schema-mixins/slideshow-fields.js'; 4 | 5 | export default { 6 | options: { 7 | label: 'Home Page' 8 | }, 9 | fields: { 10 | add: { 11 | layout: { 12 | type: 'select', 13 | label: 'Layout', 14 | choices: [ 15 | { 16 | label: 'Foundation - with Hero', 17 | value: 'foundation', 18 | help: 'Traditional layout with hero section at the top' 19 | }, 20 | { 21 | label: 'Showcase - with Slideshow', 22 | value: 'showcase', 23 | help: 'Features a prominent slideshow at the top' 24 | }, 25 | { 26 | label: 'Minimal', 27 | value: 'minimal', 28 | help: 'Clean slate for custom designs' 29 | } 30 | ], 31 | def: 'foundation' 32 | }, 33 | // Hero Section - For Foundation Layout 34 | heroSection: { 35 | type: 'object', 36 | label: 'Hero Section', 37 | fields: { 38 | add: heroFields 39 | }, 40 | if: { 41 | layout: 'foundation' 42 | } 43 | }, 44 | // Showcase Layout Sections 45 | showcaseSlideshow: { 46 | type: 'object', 47 | label: 'Showcase Slideshow', 48 | fields: { 49 | add: slideshowFields 50 | }, 51 | if: { 52 | layout: 'showcase' 53 | } 54 | }, 55 | // Main Content Area - Available for all layouts 56 | main: { 57 | type: 'area', 58 | label: 'Main Content', 59 | options: getWidgetGroups({ 60 | includeLayouts: true 61 | }) 62 | } 63 | }, 64 | group: { 65 | utility: { 66 | label: 'Layout Settings', 67 | fields: [ 68 | 'layout' 69 | ] 70 | }, 71 | hero: { 72 | label: 'Hero Section', 73 | fields: [ 74 | 'heroSection' 75 | ] 76 | }, 77 | showcase: { 78 | label: 'Showcase Content', 79 | fields: [ 80 | 'showcaseSlideshow' 81 | ] 82 | }, 83 | content: { 84 | label: 'Content', 85 | fields: [ 86 | 'main' 87 | ] 88 | } 89 | } 90 | } 91 | }; 92 | -------------------------------------------------------------------------------- /frontend/src/layouts/article-layouts/ShowMagazine.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import AposArea from '@apostrophecms/apostrophe-astro/components/AposArea.astro'; 3 | import { 4 | getAttachmentUrl, 5 | getAttachmentSrcset, 6 | getFocalPoint, 7 | getWidth, 8 | getHeight 9 | } from '../../lib/attachments.js'; 10 | 11 | const { article } = Astro.props; 12 | const heroImage = article?._heroImage?.[0]; 13 | --- 14 | 15 |
16 |
17 |
18 |
19 |
20 |
21 |

22 | By {article._author[0].title} · {new Date(article.publishDate).toLocaleDateString()} 23 |

24 |

{article.title}

25 |

{article.excerpt}

26 |
27 | 28 | {heroImage && ( 29 |
30 | {heroImage.alt 41 |
42 | )} 43 | 44 |
45 | 46 |
47 | 48 | {article._tags?.length > 0 && ( 49 |
50 | {article._tags.map(tag => ( 51 | {tag.title} 52 | ))} 53 |
54 | )} 55 |
56 |
57 |
58 |
59 |
60 | -------------------------------------------------------------------------------- /backend/modules/article-page/index.js: -------------------------------------------------------------------------------- 1 | import { getWidgetGroups } from '../../lib/helpers/area-widgets.js'; 2 | 3 | export default { 4 | extend: '@apostrophecms/piece-page-type', 5 | options: { 6 | label: 'Article Page', 7 | perPage: 7, 8 | piecesFilters: [ 9 | { 10 | name: 'category' 11 | } 12 | ] 13 | }, 14 | fields: { 15 | add: { 16 | masthead: { 17 | type: 'area', 18 | options: getWidgetGroups({ 19 | includeLayouts: true 20 | }) 21 | }, 22 | beforeContent: { 23 | type: 'area', 24 | label: 'Before Articles Section', 25 | options: getWidgetGroups({ 26 | includeLayouts: true 27 | }) 28 | }, 29 | sidebarContent: { 30 | type: 'area', 31 | label: 'Sidebar Content', 32 | if: { 33 | indexLayout: 'listAside' 34 | }, 35 | options: getWidgetGroups({ 36 | includeLayouts: true 37 | }) 38 | }, 39 | afterContent: { 40 | type: 'area', 41 | label: 'After Articles Section', 42 | options: getWidgetGroups({ 43 | includeLayouts: true 44 | }) 45 | }, 46 | indexLayout: { 47 | type: 'select', 48 | label: 'Index Page Layout', 49 | def: 'heroGrid', 50 | choices: [ 51 | { 52 | label: 'Hero Grid', 53 | value: 'heroGrid', 54 | help: 'Featured article with grid layout below' 55 | }, 56 | { 57 | label: 'List with Aside', 58 | value: 'listAside', 59 | help: 'Articles in a list with side navigation' 60 | }, 61 | { 62 | label: 'Standard', 63 | value: 'standard', 64 | help: 'Traditional blog-style listing' 65 | } 66 | ] 67 | }, 68 | showLayout: { 69 | type: 'select', 70 | label: 'Article Display Layout', 71 | def: 'fullWidth', 72 | choices: [ 73 | { 74 | label: 'Full Width', 75 | value: 'fullWidth', 76 | help: 'Hero image and content spanning full width' 77 | }, 78 | { 79 | label: 'Magazine Style', 80 | value: 'magazine', 81 | help: 'Enhanced typography with pull quotes and featured sections' 82 | }, 83 | { 84 | label: 'Minimal', 85 | value: 'minimal', 86 | help: 'Simple, clean layout with focus on content' 87 | } 88 | ] 89 | } 90 | }, 91 | group: { 92 | basics: { 93 | label: 'Basics', 94 | fields: ['masthead', 'beforeContent', 'sidebarContent', 'afterContent'] 95 | }, 96 | utility: { 97 | label: 'Display Options', 98 | fields: ['indexLayout', 'showLayout'] 99 | } 100 | } 101 | } 102 | }; 103 | -------------------------------------------------------------------------------- /backend/lib/schema-mixins/slideshow-fields.js: -------------------------------------------------------------------------------- 1 | import colorOptionsHelper from '../../lib/helpers/color-options.js'; 2 | 3 | export default { 4 | slideDuration: { 5 | type: 'integer', 6 | label: 'Slide Duration (ms)', 7 | def: 5000, 8 | min: 1000, 9 | max: 20000 10 | }, 11 | transitionSpeed: { 12 | type: 'integer', 13 | label: 'Transition Speed (ms)', 14 | def: 1000, 15 | min: 100, 16 | max: 2000 17 | }, 18 | autoplay: { 19 | type: 'boolean', 20 | label: 'Enable Autoplay', 21 | def: true 22 | }, 23 | showControls: { 24 | type: 'boolean', 25 | label: 'Show Navigation Controls', 26 | def: true 27 | }, 28 | slides: { 29 | type: 'array', 30 | label: 'Slides', 31 | titleField: 'slideTitle', 32 | inline: true, 33 | fields: { 34 | add: { 35 | slideTitle: { 36 | type: 'string', 37 | label: 'Slide Title', 38 | required: true 39 | }, 40 | titleColor: { 41 | type: 'select', 42 | label: 'Title Color', 43 | choices: colorOptionsHelper.getColorOptions().filter(color => 44 | color.value !== 'transparent' 45 | ) 46 | }, 47 | titleSize: { 48 | type: 'select', 49 | label: 'Title Size', 50 | choices: [ 51 | { 52 | label: 'Small', 53 | value: 'is-5' 54 | }, 55 | { 56 | label: 'Medium', 57 | value: 'is-4' 58 | }, 59 | { 60 | label: 'Large', 61 | value: 'is-3' 62 | } 63 | ], 64 | def: 'is-4' 65 | }, 66 | cardContent: { 67 | type: 'string', 68 | label: 'Slide Content', 69 | textarea: true 70 | }, 71 | contentColor: { 72 | type: 'select', 73 | label: 'Content Text Color', 74 | choices: colorOptionsHelper.getColorOptions().filter(color => 75 | color.value !== 'transparent' 76 | ) 77 | }, 78 | contentSize: { 79 | type: 'select', 80 | label: 'Content Text Size', 81 | choices: [ 82 | { 83 | label: 'Small', 84 | value: 'is-size-6' 85 | }, 86 | { 87 | label: 'Medium', 88 | value: 'is-size-5' 89 | }, 90 | { 91 | label: 'Large', 92 | value: 'is-size-4' 93 | } 94 | ], 95 | def: 'is-size-6' 96 | }, 97 | textBlockBackground: { 98 | type: 'select', 99 | label: 'Text Block Background Color', 100 | choices: colorOptionsHelper.getColorOptions().filter(color => 101 | color.value !== 'transparent' 102 | ), 103 | def: 'white' 104 | }, 105 | textBlockOpacity: { 106 | type: 'range', 107 | label: 'Text Block Opacity', 108 | min: 0, 109 | max: 100, 110 | step: 5, 111 | def: 70 112 | }, 113 | _image: { 114 | type: 'relationship', 115 | label: 'Slide Image', 116 | withType: '@apostrophecms/image', 117 | max: 1 118 | } 119 | } 120 | } 121 | } 122 | }; 123 | -------------------------------------------------------------------------------- /frontend/src/templates/ArticleIndexPage.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import setParameter from '@apostrophecms/apostrophe-astro/lib/aposSetQueryParameter.js'; 3 | import AposArea from '@apostrophecms/apostrophe-astro/components/AposArea.astro'; 4 | 5 | import Pagination from '../components/Pagination.astro'; 6 | 7 | import HeroGrid from '../layouts/article-layouts/HeroGrid.astro'; 8 | import ListAside from '../layouts/article-layouts/ListAside.astro'; 9 | import Standard from '../layouts/article-layouts/Standard.astro'; 10 | 11 | const { 12 | page, 13 | user, 14 | query, 15 | piecesFilters = [], 16 | pieces, 17 | currentPage, 18 | totalPages 19 | } = Astro.props.aposData; 20 | 21 | const pages = []; 22 | for (let i = 1; i <= totalPages; i++) { 23 | pages.push({ 24 | number: i, 25 | current: i === currentPage, 26 | url: setParameter(Astro.url, 'page', i) 27 | }); 28 | } 29 | --- 30 |
31 |
32 |

{page.title}

33 | 34 | {/* Global Masthead - Shows for all layouts */} 35 | { 36 | page.masthead && ( 37 |
38 |
39 | 40 |
41 |
42 | ) 43 | } 44 | 45 | { 46 | Array.isArray(piecesFilters) && piecesFilters.length > 0 && ( 47 |
48 | {piecesFilters.map((filter) => ( 49 | 53 | {filter.label} 54 | 55 | ))} 56 |
57 | ) 58 | } 59 | 60 | {/* Before Content Area */} 61 | {page.beforeContent && ( 62 |
63 | 64 |
65 | )} 66 | 67 | {page.indexLayout === 'heroGrid' && ( 68 | 73 | )} 74 | 75 | {page.indexLayout === 'listAside' && ( 76 | 83 | )} 84 | 85 | {page.indexLayout === 'standard' && ( 86 | 93 | )} 94 | 95 | {/* After Content Area */} 96 | {page.afterContent && ( 97 |
98 | 99 |
100 | )} 101 | 102 | {totalPages > 1 && ( 103 | 109 | )} 110 |
111 |
112 | -------------------------------------------------------------------------------- /frontend/src/pages/[...slug].astro: -------------------------------------------------------------------------------- 1 | --- 2 | import aposPageFetch from '@apostrophecms/apostrophe-astro/lib/aposPageFetch.js'; 3 | import AposLayout from '@apostrophecms/apostrophe-astro/components/layouts/AposLayout.astro'; 4 | import AposTemplate from '@apostrophecms/apostrophe-astro/components/AposTemplate.astro'; 5 | 6 | const aposData = await aposPageFetch(Astro.request); 7 | 8 | if (aposData.redirect) { 9 | return Astro.redirect(aposData.url, aposData.status); 10 | } 11 | if (aposData.notFound) { 12 | Astro.response.status = 404; 13 | } 14 | // Extract header information 15 | const headerPosition = aposData.global?.headerGroup?.headerPosition || 'static'; 16 | 17 | 18 | // Initialize an array to hold body classes 19 | const bodyClasses = []; 20 | 21 | // Conditionally add 'has-navbar-fixed-top' based on headerPosition 22 | if (headerPosition === 'fixed' || headerPosition === 'fixed-fade') { 23 | bodyClasses.push('has-navbar-fixed-top'); 24 | } 25 | 26 | // Join the classes into a single string 27 | const bodyClass = bodyClasses.join(' '); 28 | 29 | import '../styles/main.scss'; 30 | 31 | import Header from '../components/Header.astro'; 32 | import Footer from '../components/Footer.astro'; 33 | --- 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 45 | 46 | 89 | 90 | 91 |
92 | 93 |