├── frontend ├── Procfile ├── tsconfig.json ├── src │ ├── env.d.ts │ ├── widgets │ │ ├── FileWidget.astro │ │ ├── RichTextWidget.astro │ │ ├── index.js │ │ ├── VideoWidget.astro │ │ ├── ImageWidget.astro │ │ ├── LinkWidget.astro │ │ ├── AccordionWidget.astro │ │ ├── CardWidget.astro │ │ └── SlideshowWidget.astro │ ├── templates │ │ ├── DefaultPage.astro │ │ ├── index.js │ │ ├── ArticleShowPage.astro │ │ ├── HomePage.astro │ │ └── ArticleIndexPage.astro │ ├── components │ │ ├── Figure.astro │ │ ├── ImageLink.astro │ │ ├── Pagination.astro │ │ ├── ArticlesFilter.astro │ │ ├── Header.astro │ │ └── Footer.astro │ ├── lib │ │ ├── homepage-defaults.js │ │ ├── use-site-config.js │ │ └── attachments.js │ ├── 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-brands-400.woff2 │ │ │ ├── fa-solid-900.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 ├── .gitignore ├── backend ├── modules │ ├── @apostrophecms │ │ ├── home-page │ │ │ ├── views │ │ │ │ └── page.html │ │ │ └── index.js │ │ ├── page │ │ │ ├── views │ │ │ │ └── notFound.html │ │ │ └── index.js │ │ ├── asset │ │ │ └── index.js │ │ ├── image-widget │ │ │ ├── index.js │ │ │ └── public │ │ │ │ └── preview.svg │ │ ├── express │ │ │ └── index.js │ │ ├── video-widget │ │ │ ├── index.js │ │ │ └── public │ │ │ │ └── preview.svg │ │ ├── attachment │ │ │ └── index.js │ │ ├── layout-column-widget │ │ │ └── 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 │ ├── 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 │ ├── accordion-widget │ │ ├── public │ │ │ └── preview.svg │ │ └── index.js │ ├── author │ │ └── index.js │ ├── article │ │ └── index.js │ └── article-page │ │ └── index.js ├── public │ └── images │ │ └── logo.png ├── eslint.config.js ├── .gitignore ├── scripts │ ├── load-starter-content │ └── update-starter-content ├── LICENSE ├── README.md ├── app.js ├── lib │ ├── helpers │ │ ├── color-options.js │ │ ├── area-widgets.js │ │ └── typography-options.js │ └── schema-mixins │ │ ├── slideshow-fields.js │ │ ├── link-fields.js │ │ ├── hero-fields.js │ │ └── card-fields.js ├── package.json └── views │ └── layout.html ├── LICENSE └── package.json /frontend/Procfile: -------------------------------------------------------------------------------- 1 | web: node dist/server/entry.mjs 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | package-lock.json 2 | /node_modules 3 | .DS_Store 4 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /frontend/src/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /backend/public/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apostrophecms/starter-kit-astro-apollo/develop/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-brands-400.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apostrophecms/starter-kit-astro-apollo/develop/frontend/public/fonts/fontawesome/fa-brands-400.woff2 -------------------------------------------------------------------------------- /frontend/public/fonts/fontawesome/fa-solid-900.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apostrophecms/starter-kit-astro-apollo/develop/frontend/public/fonts/fontawesome/fa-solid-900.woff2 -------------------------------------------------------------------------------- /frontend/public/images/image-widget-placeholder.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apostrophecms/starter-kit-astro-apollo/develop/frontend/public/images/image-widget-placeholder.jpg -------------------------------------------------------------------------------- /frontend/public/fonts/fontawesome/fa-regular-400.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apostrophecms/starter-kit-astro-apollo/develop/frontend/public/fonts/fontawesome/fa-regular-400.woff2 -------------------------------------------------------------------------------- /backend/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 | -------------------------------------------------------------------------------- /frontend/public/fonts/fontawesome/fa-v4compatibility.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apostrophecms/starter-kit-astro-apollo/develop/frontend/public/fonts/fontawesome/fa-v4compatibility.woff2 -------------------------------------------------------------------------------- /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 | hmr: false 6 | } 7 | }; 8 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /frontend/src/widgets/FileWidget.astro: -------------------------------------------------------------------------------- 1 | --- 2 | const { widget } = Astro.props; 3 | const { _file } = widget; 4 | const file = _file[0]; 5 | --- 6 |
7 | { (file && {file.title}) || 'No file selected' } 8 |
9 | -------------------------------------------------------------------------------- /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); -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # build output 2 | dist/ 3 | # generated types 4 | .astro/ 5 | 6 | # dependencies 7 | package-lock.json 8 | node_modules/ 9 | package-lock.json 10 | 11 | # logs 12 | npm-debug.log* 13 | yarn-debug.log* 14 | yarn-error.log* 15 | pnpm-debug.log* 16 | 17 | 18 | # environment variables 19 | .env 20 | .env.production 21 | 22 | # macOS-specific files 23 | .DS_Store 24 | -------------------------------------------------------------------------------- /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/.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 | package-lock.json 9 | node_modules/ 10 | package-lock.json 11 | 12 | # logs 13 | npm-debug.log 14 | 15 | # content 16 | /public/uploads 17 | 18 | # local env data 19 | /data 20 | /data/temp/uploadfs 21 | 22 | # macOS-specific files 23 | .DS_Store 24 | -------------------------------------------------------------------------------- /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/layout-column-widget/index.js: -------------------------------------------------------------------------------- 1 | import { getWidgetGroups } from '../../../lib/helpers/area-widgets.js'; 2 | 3 | export default { 4 | fields(self, options) { 5 | return { 6 | add: { 7 | content: { 8 | type: 'area', 9 | label: 'Main Content', 10 | options: getWidgetGroups({ 11 | includeLayouts: false 12 | }) 13 | } 14 | } 15 | }; 16 | } 17 | }; 18 | -------------------------------------------------------------------------------- /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/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 | }; 27 | -------------------------------------------------------------------------------- /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/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/src/components/Figure.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import ImageLink from "./ImageLink.astro"; 3 | 4 | export interface Props { 5 | image: { 6 | src: string; 7 | alt?: string; 8 | style?: string; 9 | srcset?: string; 10 | objectPosition: string; 11 | width?: number | string; 12 | height?: number | string; 13 | aspectRatio?: string; 14 | }; 15 | link?: { 16 | url: string; 17 | title?: string; 18 | target?: string; 19 | rel?: string; 20 | }; 21 | caption?: string; 22 | style?: string; 23 | } 24 | 25 | const { image, caption, link, style } = Astro.props; 26 | --- 27 | 28 |
29 | 30 | {caption &&
{caption}
} 31 |
32 | -------------------------------------------------------------------------------- /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 23 | // in the personal preferences menu 24 | { 25 | label: 'English', 26 | value: 'en' 27 | }, 28 | { 29 | label: 'Spanish', 30 | value: 'es' 31 | }, 32 | { 33 | label: 'French', 34 | value: 'fr' 35 | }, 36 | { 37 | label: 'German', 38 | value: 'de' 39 | } 40 | ] 41 | } 42 | }; 43 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | > **📌 Note on Dependency Management** 8 | > 9 | > This starter kit ships with `package-lock.json` in `.gitignore` to avoid merge conflicts during development. 10 | > 11 | > **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. 12 | 13 | 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`. 14 | -------------------------------------------------------------------------------- /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/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 | > **📌 Note on Dependency Management** 8 | > 9 | > This starter kit ships with `package-lock.json` in `.gitignore` to avoid merge conflicts during development. 10 | > 11 | > **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. 12 | 13 | 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. 14 | -------------------------------------------------------------------------------- /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 FileWidget from './FileWidget.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 LayoutWidget from '@apostrophecms/apostrophe-astro/widgets/LayoutWidget.astro'; 11 | import LayoutColumnWidget from '@apostrophecms/apostrophe-astro/widgets/LayoutColumnWidget.astro'; 12 | 13 | const widgetComponents = { 14 | '@apostrophecms/rich-text': RichTextWidget, 15 | '@apostrophecms/image': ImageWidget, 16 | '@apostrophecms/video': VideoWidget, 17 | '@apostrophecms/file': FileWidget, 18 | 'accordion': AccordionWidget, 19 | 'card': CardWidget, 20 | 'hero': HeroWidget, 21 | 'link': LinkWidget, 22 | 'slideshow': SlideshowWidget, 23 | '@apostrophecms/layout': LayoutWidget, 24 | '@apostrophecms/layout-column': LayoutColumnWidget 25 | }; 26 | 27 | export default widgetComponents; 28 | -------------------------------------------------------------------------------- /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": "cross-env APOS_EXTERNAL_FRONT_KEY=dev astro dev", 8 | "start": "node ./dist/server/entry.mjs", 9 | "build": "astro build", 10 | "serve": "cross-env HOST=0.0.0.0 node ./dist/server/entry.mjs", 11 | "preview": "cross-env 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": "^1.7.1", 20 | "@astrojs/node": "^9.0.0", 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.0.9", 27 | "bulma": "^1.0.2", 28 | "dayjs": "^1.11.10", 29 | "postcss-viewport-to-container-toggle": "^1.1.0", 30 | "vite": "^5.0.7" 31 | }, 32 | "devDependencies": { 33 | "cross-env": "^10.1.0", 34 | "sass-embedded": "^1.81.0" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /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/app.js: -------------------------------------------------------------------------------- 1 | import apostrophe from 'apostrophe'; 2 | 3 | export default apostrophe({ 4 | root: import.meta, 5 | shortName: 'apollo', 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 27 | // URL of your Apostrophe site 28 | '@apostrophecms/seo': {}, 29 | 30 | // pieces 31 | article: {}, 32 | author: {}, 33 | 34 | // pages 35 | 'default-page': {}, 36 | 'article-page': {}, 37 | 38 | // widgets 39 | 'accordion-widget': {}, 40 | 'card-widget': {}, 41 | 'hero-widget': {}, 42 | 'link-widget': {}, 43 | 'slideshow-widget': {} 44 | } 45 | }); 46 | -------------------------------------------------------------------------------- /frontend/src/components/ImageLink.astro: -------------------------------------------------------------------------------- 1 | --- 2 | const { image, link } = Astro.props; 3 | 4 | export interface Props { 5 | image: { 6 | src: string; 7 | alt?: string; 8 | style?: string; 9 | srcset?: string; 10 | objectPosition: string; 11 | width?: number | string; 12 | height?: number | string; 13 | aspectRatio?: string; 14 | }; 15 | link?: { 16 | url: string; 17 | title?: string; 18 | target?: string; 19 | rel?: string; 20 | }; 21 | } 22 | 23 | const style = 24 | `object-position: ${image.objectPosition};${image.aspectRatio ? `--aspect-ratio: ${image.aspectRatio};` : ""} 25 | `.trim(); 26 | --- 27 | 28 | 29 | { 30 | !link && ( 31 | {image.alt} 42 | ) 43 | } 44 | { 45 | link && ( 46 | 47 | {image.alt} 58 | 59 | ) 60 | } 61 | 62 | -------------------------------------------------------------------------------- /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 | }); -------------------------------------------------------------------------------- /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' 10 | // in the `@apostrophecms/user` module. 11 | // Changing this field will **not** change the Username or Slug of the user. 12 | fields: [ 'title' ], 13 | reload: true 14 | }, 15 | fullName: { 16 | // Passing in a label so that it doesn't use the label for `lastName` 17 | // These fields need to be added to the user schema 18 | label: 'Full Name', 19 | // Schema fields added at project level 20 | fields: [ 'lastName', 'firstName' ], 21 | preview: '{{ firstName }} {{lastName}}' 22 | }, 23 | // The `adminLocales` option **must** be configured 24 | // in the `@apostrophecms/i18n` module for this to be allowed 25 | adminLocale: { 26 | fields: [ 'adminLocale' ] 27 | } 28 | }, 29 | groups: { 30 | account: { 31 | label: 'Account', 32 | subforms: [ 'displayName', 'fullName', 'changePassword' ] 33 | }, 34 | preferences: { 35 | label: 'Preferences', 36 | // The `adminLocales` option **must** be configured 37 | // in the `@apostrophecms/i18n` module for this to be allowed 38 | subforms: [ 'adminLocale' ] 39 | } 40 | } 41 | } 42 | }; 43 | -------------------------------------------------------------------------------- /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/lib/helpers/color-options.js: -------------------------------------------------------------------------------- 1 | const colorOptionsHelper = { 2 | getColorOptions() { 3 | return [ 4 | // Main colors 5 | { 6 | label: 'White', 7 | value: 'white' 8 | }, 9 | { 10 | label: 'Black', 11 | value: 'black' 12 | }, 13 | { 14 | label: 'Light', 15 | value: 'light' 16 | }, 17 | { 18 | label: 'Dark', 19 | value: 'dark' 20 | }, 21 | { 22 | label: 'Primary', 23 | value: 'primary' 24 | }, 25 | { 26 | label: 'Link', 27 | value: 'link' 28 | }, 29 | { 30 | label: 'Info', 31 | value: 'info' 32 | }, 33 | { 34 | label: 'Success', 35 | value: 'success' 36 | }, 37 | { 38 | label: 'Warning', 39 | value: 'warning' 40 | }, 41 | { 42 | label: 'Danger', 43 | value: 'danger' 44 | }, 45 | { 46 | label: 'Black Bis', 47 | value: 'black-bis' 48 | }, 49 | { 50 | label: 'Black Ter', 51 | value: 'black-ter' 52 | }, 53 | { 54 | label: 'Grey Darker', 55 | value: 'grey-darker' 56 | }, 57 | { 58 | label: 'Grey Dark', 59 | value: 'grey-dark' 60 | }, 61 | { 62 | label: 'Grey', 63 | value: 'grey' 64 | }, 65 | { 66 | label: 'Grey Light', 67 | value: 'grey-light' 68 | }, 69 | { 70 | label: 'Grey Lighter', 71 | value: 'grey-lighter' 72 | }, 73 | { 74 | label: 'White Ter', 75 | value: 'white-ter' 76 | }, 77 | { 78 | label: 'White Bis', 79 | value: 'white-bis' 80 | }, 81 | { 82 | label: 'Transparent', 83 | value: 'transparent' 84 | } 85 | ]; 86 | } 87 | }; 88 | 89 | export default colorOptionsHelper; 90 | -------------------------------------------------------------------------------- /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 | 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/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": "cross-env APOS_EXTERNAL_FRONT_KEY=dev nodemon", 10 | "build": "cross-env NODE_ENV=production node app @apostrophecms/asset:build", 11 | "serve": "cross-env NODE_ENV=production node app", 12 | "release": "npm install && npm run build && npm run migrate", 13 | "migrate": "cross-env 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/mongodb-snapshot": "^1.1.0", 52 | "@apostrophecms/seo": "^1.3.0", 53 | "@apostrophecms/vite": "^1.0.0", 54 | "apostrophe": "^4.24.0", 55 | "normalize.css": "^8.0.1" 56 | }, 57 | "devDependencies": { 58 | "cross-env": "^10.1.0", 59 | "eslint-config-apostrophe": "^6.0.1", 60 | "nodemon": "^3.0.1" 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /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/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 | { 45 | label: 'Twitter/X', 46 | value: 'twitter' 47 | }, 48 | { 49 | label: 'LinkedIn', 50 | value: 'linkedin' 51 | }, 52 | { 53 | label: 'GitHub', 54 | value: 'github' 55 | }, 56 | { 57 | label: 'Personal Website', 58 | value: 'website' 59 | } 60 | ] 61 | }, 62 | url: { 63 | type: 'url', 64 | label: 'Profile URL' 65 | } 66 | } 67 | } 68 | }, 69 | _articles: { 70 | type: 'relationshipReverse', 71 | withType: 'article', 72 | reverseOf: '_author' 73 | } 74 | }, 75 | group: { 76 | basics: { 77 | label: 'Basic Info', 78 | fields: [ 'title', 'email', 'profileImage', '_articles' ] 79 | }, 80 | content: { 81 | label: 'Content', 82 | fields: [ 'biography' ] 83 | }, 84 | social: { 85 | label: 'Social Media', 86 | fields: [ 'socialLinks' ] 87 | } 88 | } 89 | } 90 | }; 91 | -------------------------------------------------------------------------------- /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/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 | { 55 | label: 'Left', 56 | value: 'left' 57 | }, 58 | { 59 | label: 'Center', 60 | value: 'center' 61 | }, 62 | { 63 | label: 'Right', 64 | value: 'right' 65 | } 66 | ], 67 | def: 'left' 68 | }, 69 | content: { 70 | type: 'area', 71 | label: 'Content', 72 | options: getWidgetGroups({ 73 | exclude: [ 'accordion' ] 74 | }) 75 | } 76 | } 77 | } 78 | } 79 | } 80 | } 81 | }; 82 | -------------------------------------------------------------------------------- /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/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 "@apostrophecms/apostrophe-astro/lib/attachment.js"; 22 | import { slugify } from "@apostrophecms/apostrophe-astro/lib/util.js"; 23 | import Figure from "../components/Figure.astro"; 24 | 25 | const { widget, imageOptions } = Astro.props; 26 | const placeholder = widget?.aposPlaceholder; 27 | const image = widget?._image?.[0]; 28 | 29 | // Handle missing image gracefully 30 | if (!placeholder && !image) { 31 | console.warn("Image widget: No image or placeholder provided"); 32 | } 33 | 34 | const src = placeholder 35 | ? "/images/image-widget-placeholder.jpg" 36 | : getAttachmentUrl(image); 37 | const srcset = placeholder ? "" : getAttachmentSrcset(image); 38 | const objectPosition = placeholder ? "center center" : getFocalPoint(image); 39 | const alt = image?.alt || ""; 40 | 41 | // Only add width/height if they exist to prevent layout shift 42 | const width = image ? getWidth(image) : undefined; 43 | const height = image ? getHeight(image) : undefined; 44 | const aspectRatio = width && height ? `${width}/${height}` : undefined; 45 | const imageProps = { 46 | src, 47 | alt, 48 | style: "img-widget__image", 49 | srcset, 50 | objectPosition, 51 | width, 52 | height, 53 | aspectRatio, 54 | }; 55 | let link = { 56 | url: widget.linkHref, 57 | title: widget.linkHrefTitle || widget.caption, 58 | target: widget.linkTarget, 59 | rel: null, 60 | }; 61 | switch (widget.linkTo) { 62 | case "none": { 63 | link = null; 64 | break; 65 | } 66 | case "_url": { 67 | link.rel = widget.target === "_blank" ? "noopener noreferrer" : null; 68 | break; 69 | } 70 | default: { 71 | // slugify the linkTo value to reference the widget field. 72 | const name = "_" + slugify(widget.linkTo); 73 | const item = widget[name]?.[0]; 74 | link.url = item?._url; 75 | link.title = widget.linkTitle || item?.title; 76 | break; 77 | } 78 | } 79 | --- 80 | 81 |
87 | -------------------------------------------------------------------------------- /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/lib/helpers/area-widgets.js: -------------------------------------------------------------------------------- 1 | // Define our available widgets grouped by type 2 | export const widgetGroups = { 3 | // Layout widgets are structural elements that help organize content 4 | layout: { 5 | label: 'Layout', 6 | columns: 2, 7 | widgets: { 8 | '@apostrophecms/layout': {} 9 | } 10 | }, 11 | // Content widgets are the actual content elements users can add 12 | content: { 13 | label: 'Content', 14 | columns: 3, 15 | widgets: { 16 | '@apostrophecms/rich-text': {}, 17 | '@apostrophecms/image': {}, 18 | '@apostrophecms/video': {}, 19 | '@apostrophecms/file': {}, 20 | slideshow: {}, 21 | hero: {}, 22 | accordion: {}, 23 | card: {}, 24 | link: {} 25 | } 26 | } 27 | }; 28 | 29 | /** 30 | * Creates the groups configuration for ApostropheCMS widget areas 31 | * @param {Object} options - Configuration options 32 | * @param {boolean} options.includeLayouts - If true, 33 | * includes layout widgets in the groups 34 | * @param {Array} options.exclude - Array of widget names to exclude 35 | * @returns {Object} Returns the groups configuration object 36 | * 37 | * @example 38 | * // In your page type or piece type: 39 | * fields: { 40 | * add: { 41 | * main: { 42 | * type: 'area', 43 | * options: { 44 | * // Get grouped widgets configuration 45 | * ...getWidgetGroups({ 46 | * includeLayouts: true, 47 | * exclude: ['hero'] 48 | * }), 49 | * // Add any additional area options 50 | * max: 10, 51 | * min: 1 52 | * } 53 | * } 54 | * } 55 | * } 56 | */ 57 | export const getWidgetGroups = ({ 58 | includeLayouts = false, 59 | exclude = [] 60 | } = {}) => { 61 | // Initialize our groups object 62 | const groups = {}; 63 | 64 | // Add layout widgets if requested 65 | if (includeLayouts) { 66 | groups.layout = { 67 | ...widgetGroups.layout, 68 | // Filter out any excluded widgets 69 | widgets: Object.fromEntries( 70 | Object.entries(widgetGroups.layout.widgets) 71 | .filter(([ key ]) => !exclude.includes(key)) 72 | ) 73 | }; 74 | } 75 | 76 | // Always add content widgets 77 | groups.content = { 78 | ...widgetGroups.content, 79 | // Filter out any excluded widgets 80 | widgets: Object.fromEntries( 81 | Object.entries(widgetGroups.content.widgets) 82 | .filter(([ key ]) => !exclude.includes(key)) 83 | ) 84 | }; 85 | 86 | // Return just the expanded and groups properties 87 | // This allows other area options to be spread alongside it 88 | return { 89 | expanded: true, 90 | groups 91 | }; 92 | }; 93 | -------------------------------------------------------------------------------- /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/helpers/typography-options.js: -------------------------------------------------------------------------------- 1 | const textOptionsHelper = { 2 | getTextSizes() { 3 | return [ 4 | { 5 | label: 'Default', 6 | value: '' 7 | }, 8 | { 9 | label: 'Size 1 (3rem)', 10 | value: 'is-size-1' 11 | }, 12 | { 13 | label: 'Size 2 (2.5rem)', 14 | value: 'is-size-2' 15 | }, 16 | { 17 | label: 'Size 3 (2rem)', 18 | value: 'is-size-3' 19 | }, 20 | { 21 | label: 'Size 4 (1.5rem)', 22 | value: 'is-size-4' 23 | }, 24 | { 25 | label: 'Size 5 (1.25rem)', 26 | value: 'is-size-5' 27 | }, 28 | { 29 | label: 'Size 6 (1rem)', 30 | value: 'is-size-6' 31 | }, 32 | { 33 | label: 'Size 7 (0.75rem)', 34 | value: 'is-size-7' 35 | } 36 | ]; 37 | }, 38 | 39 | getResponsiveSizes() { 40 | const sizes = [ '1', '2', '3', '4', '5', '6', '7' ]; 41 | const breakpoints = [ 'mobile', 'tablet', 'desktop', 'widescreen' ]; 42 | 43 | const choices = [ { 44 | label: 'Default', 45 | value: '' 46 | } ]; 47 | 48 | breakpoints.forEach(breakpoint => { 49 | sizes.forEach(size => { 50 | choices.push({ 51 | label: `Size ${size} (${breakpoint})`, 52 | value: `is-size-${size}-${breakpoint}` 53 | }); 54 | }); 55 | }); 56 | 57 | return choices; 58 | }, 59 | 60 | getTextWeights() { 61 | return [ 62 | { 63 | label: 'Default', 64 | value: '' 65 | }, 66 | { 67 | label: 'Light (300)', 68 | value: 'has-text-weight-light' 69 | }, 70 | { 71 | label: 'Normal (400)', 72 | value: 'has-text-weight-normal' 73 | }, 74 | { 75 | label: 'Medium (500)', 76 | value: 'has-text-weight-medium' 77 | }, 78 | { 79 | label: 'Semi-Bold (600)', 80 | value: 'has-text-weight-semibold' 81 | }, 82 | { 83 | label: 'Bold (700)', 84 | value: 'has-text-weight-bold' 85 | } 86 | ]; 87 | }, 88 | 89 | getTextTransforms() { 90 | return [ 91 | { 92 | label: 'Default', 93 | value: '' 94 | }, 95 | { 96 | label: 'Capitalized', 97 | value: 'is-capitalized' 98 | }, 99 | { 100 | label: 'Lowercase', 101 | value: 'is-lowercase' 102 | }, 103 | { 104 | label: 'Uppercase', 105 | value: 'is-uppercase' 106 | }, 107 | { 108 | label: 'Italic', 109 | value: 'is-italic' 110 | } 111 | ]; 112 | }, 113 | 114 | getTextAlignments() { 115 | return [ 116 | { 117 | label: 'Default', 118 | value: '' 119 | }, 120 | { 121 | label: 'Left', 122 | value: 'has-text-left' 123 | }, 124 | { 125 | label: 'Centered', 126 | value: 'has-text-centered' 127 | }, 128 | { 129 | label: 'Right', 130 | value: 'has-text-right' 131 | }, 132 | { 133 | label: 'Justified', 134 | value: 'has-text-justified' 135 | } 136 | ]; 137 | } 138 | }; 139 | 140 | export default textOptionsHelper; 141 | -------------------------------------------------------------------------------- /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 |