├── frontend ├── Procfile ├── tsconfig.json ├── src │ ├── env.d.ts │ ├── widgets │ │ ├── FileWidget.astro │ │ ├── RichTextWidget.astro │ │ ├── index.js │ │ ├── DataSetWidget.astro │ │ ├── VideoWidget.astro │ │ ├── ImageWidget.astro │ │ ├── LinkWidget.astro │ │ └── AccordionWidget.astro │ ├── templates │ │ ├── DefaultPage.astro │ │ ├── index.js │ │ ├── ArticleShowPage.astro │ │ ├── HomePage.astro │ │ └── ArticleIndexPage.astro │ ├── pages │ │ ├── api │ │ │ └── apos-external-front │ │ │ │ └── render-area.astro │ │ └── [...slug].astro │ ├── components │ │ ├── Figure.astro │ │ ├── DataSetTable.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 │ └── 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 ├── .stylelintignore ├── sites │ ├── views │ │ └── layout.html │ ├── modules │ │ ├── @apostrophecms │ │ │ ├── page │ │ │ │ ├── views │ │ │ │ │ └── notFound.html │ │ │ │ └── index.js │ │ │ ├── home-page │ │ │ │ ├── views │ │ │ │ │ └── page.html │ │ │ │ └── index.js │ │ │ ├── widget-type │ │ │ │ └── index.js │ │ │ ├── uploadfs │ │ │ │ └── index.js │ │ │ ├── image-widget │ │ │ │ ├── index.js │ │ │ │ └── public │ │ │ │ │ └── preview.svg │ │ │ ├── express │ │ │ │ └── index.js │ │ │ ├── video-widget │ │ │ │ ├── index.js │ │ │ │ └── public │ │ │ │ │ └── preview.svg │ │ │ ├── attachment │ │ │ │ └── index.js │ │ │ ├── user │ │ │ │ └── index.js │ │ │ ├── admin-bar │ │ │ │ └── index.js │ │ │ ├── rich-text-widget │ │ │ │ ├── index.js │ │ │ │ └── public │ │ │ │ │ └── preview.svg │ │ │ ├── i18n │ │ │ │ └── index.js │ │ │ ├── asset │ │ │ │ └── index.js │ │ │ └── settings │ │ │ │ └── index.js │ │ ├── theme-demo │ │ │ ├── ui │ │ │ │ └── src │ │ │ │ │ └── index.scss │ │ │ └── index.js │ │ ├── theme-default │ │ │ ├── ui │ │ │ │ └── src │ │ │ │ │ └── index.scss │ │ │ └── index.js │ │ ├── @apostrophecms-pro │ │ │ └── palette │ │ │ │ ├── index.js │ │ │ │ └── lib │ │ │ │ ├── groups.js │ │ │ │ └── fields.js │ │ ├── link-widget │ │ │ └── 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 │ │ ├── accordion-widget │ │ │ ├── public │ │ │ │ └── preview.svg │ │ │ └── index.js │ │ ├── author │ │ │ └── index.js │ │ ├── article │ │ │ └── index.js │ │ └── article-page │ │ │ └── index.js │ ├── lib │ │ ├── theme-demo.js │ │ ├── theme-default.js │ │ ├── helpers │ │ │ ├── color-options.js │ │ │ ├── area-widgets.js │ │ │ └── typography-options.js │ │ └── schema-mixins │ │ │ ├── slideshow-fields.js │ │ │ ├── link-fields.js │ │ │ ├── hero-fields.js │ │ │ └── card-fields.js │ └── index.js ├── .stylelintrc ├── dashboard │ ├── views │ │ └── layout.html │ ├── modules │ │ ├── @apostrophecms │ │ │ ├── admin-bar │ │ │ │ └── index.js │ │ │ ├── uploadfs │ │ │ │ └── index.js │ │ │ ├── express │ │ │ │ └── index.js │ │ │ ├── file │ │ │ │ └── index.js │ │ │ ├── file-tag │ │ │ │ └── index.js │ │ │ ├── image-tag │ │ │ │ └── index.js │ │ │ └── asset │ │ │ │ └── index.js │ │ └── site │ │ │ └── index.js │ └── index.js ├── public │ └── images │ │ └── logo.png ├── deployment │ ├── migrate │ ├── build │ └── for-all-themes ├── eslint.config.js ├── themes.js ├── domains.js ├── modules │ └── @apostrophecms │ │ └── layout-column-widget │ │ └── index.js ├── README.md ├── nodemon.json ├── LICENSE ├── .gitignore ├── app.js ├── package.json └── views │ └── layout.html ├── .gitignore ├── package.json └── LICENSE /frontend/Procfile: -------------------------------------------------------------------------------- 1 | web: node dist/server/entry.mjs 2 | -------------------------------------------------------------------------------- /backend/.stylelintignore: -------------------------------------------------------------------------------- 1 | **/node_modules 2 | apos-build 3 | -------------------------------------------------------------------------------- /backend/sites/views/layout.html: -------------------------------------------------------------------------------- 1 | {% extends "../../views/layout.html" %} 2 | -------------------------------------------------------------------------------- /frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "astro/tsconfigs/base" 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | package-lock.json 2 | /node_modules 3 | .DS_Store 4 | /release-id 5 | -------------------------------------------------------------------------------- /backend/.stylelintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "stylelint-config-apostrophe" 3 | } 4 | -------------------------------------------------------------------------------- /backend/dashboard/views/layout.html: -------------------------------------------------------------------------------- 1 | {% extends "../../views/layout.html" %} 2 | -------------------------------------------------------------------------------- /backend/sites/modules/@apostrophecms/page/views/notFound.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | -------------------------------------------------------------------------------- /backend/sites/modules/@apostrophecms/home-page/views/page.html: -------------------------------------------------------------------------------- 1 | {% extends 'layout.html' %} 2 | -------------------------------------------------------------------------------- /frontend/src/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /backend/dashboard/modules/@apostrophecms/admin-bar/index.js: -------------------------------------------------------------------------------- 1 | export default { 2 | options: { 3 | pageTree: false 4 | } 5 | }; 6 | -------------------------------------------------------------------------------- /backend/sites/modules/@apostrophecms/widget-type/index.js: -------------------------------------------------------------------------------- 1 | export default { 2 | options: { 3 | // preview: false 4 | } 5 | }; 6 | -------------------------------------------------------------------------------- /frontend/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["astro-build.astro-vscode"], 3 | "unwantedRecommendations": [] 4 | } 5 | -------------------------------------------------------------------------------- /backend/public/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apostrophecms/starter-kit-astro-apollo-assembly/main/backend/public/images/logo.png -------------------------------------------------------------------------------- /backend/sites/modules/theme-demo/ui/src/index.scss: -------------------------------------------------------------------------------- 1 | // Styles for this theme 2 | 3 | // If you need to share styles across themes, just @import them here 4 | -------------------------------------------------------------------------------- /backend/sites/modules/theme-default/ui/src/index.scss: -------------------------------------------------------------------------------- 1 | // Styles for this theme 2 | 3 | // If you need to share styles across themes, just @import them here 4 | -------------------------------------------------------------------------------- /backend/deployment/migrate: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | node app @apostrophecms/migration:migrate --site=dashboard 4 | node app @apostrophecms/migration:migrate --all-sites 5 | -------------------------------------------------------------------------------- /backend/sites/modules/@apostrophecms/uploadfs/index.js: -------------------------------------------------------------------------------- 1 | export default { 2 | options: { 3 | uploadfs: { 4 | disabledFileKey: 'CHANGEME' 5 | } 6 | } 7 | }; 8 | -------------------------------------------------------------------------------- /backend/dashboard/modules/@apostrophecms/uploadfs/index.js: -------------------------------------------------------------------------------- 1 | export default { 2 | options: { 3 | uploadfs: { 4 | disabledFileKey: 'CHANGEME' 5 | } 6 | } 7 | }; 8 | -------------------------------------------------------------------------------- /frontend/public/fonts/fontawesome/fa-solid-900.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apostrophecms/starter-kit-astro-apollo-assembly/main/frontend/public/fonts/fontawesome/fa-solid-900.woff2 -------------------------------------------------------------------------------- /frontend/public/images/image-widget-placeholder.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apostrophecms/starter-kit-astro-apollo-assembly/main/frontend/public/images/image-widget-placeholder.jpg -------------------------------------------------------------------------------- /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-brands-400.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apostrophecms/starter-kit-astro-apollo-assembly/main/frontend/public/fonts/fontawesome/fa-brands-400.woff2 -------------------------------------------------------------------------------- /frontend/public/fonts/fontawesome/fa-regular-400.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apostrophecms/starter-kit-astro-apollo-assembly/main/frontend/public/fonts/fontawesome/fa-regular-400.woff2 -------------------------------------------------------------------------------- /frontend/public/fonts/fontawesome/fa-v4compatibility.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apostrophecms/starter-kit-astro-apollo-assembly/main/frontend/public/fonts/fontawesome/fa-v4compatibility.woff2 -------------------------------------------------------------------------------- /backend/sites/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/deployment/build: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | export NODE_ENV=production 4 | export APOS_UPLOADFS_ASSETS=1 5 | node app @apostrophecms/asset:build --site=dashboard 6 | ./deployment/for-all-themes @apostrophecms/asset:build 7 | -------------------------------------------------------------------------------- /backend/sites/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/dashboard/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/dashboard/modules/@apostrophecms/file/index.js: -------------------------------------------------------------------------------- 1 | export default { 2 | options: { 3 | quickCreate: false 4 | }, 5 | methods(self) { 6 | return { 7 | addToAdminBar() {} 8 | }; 9 | } 10 | }; 11 | -------------------------------------------------------------------------------- /backend/sites/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/dashboard/modules/@apostrophecms/file-tag/index.js: -------------------------------------------------------------------------------- 1 | export default { 2 | options: { 3 | quickCreate: false 4 | }, 5 | methods(self) { 6 | return { 7 | addToAdminBar() {} 8 | }; 9 | } 10 | }; 11 | -------------------------------------------------------------------------------- /backend/dashboard/modules/@apostrophecms/image-tag/index.js: -------------------------------------------------------------------------------- 1 | export default { 2 | options: { 3 | quickCreate: false 4 | }, 5 | methods(self) { 6 | return { 7 | addToAdminBar() {} 8 | }; 9 | } 10 | }; 11 | -------------------------------------------------------------------------------- /backend/dashboard/modules/site/index.js: -------------------------------------------------------------------------------- 1 | import themes from '../../../themes.js'; 2 | import baseUrlDomains from '../../../domains.js'; 3 | 4 | export default { 5 | options: { 6 | themes, 7 | baseUrlDomains 8 | } 9 | }; 10 | -------------------------------------------------------------------------------- /backend/sites/modules/@apostrophecms-pro/palette/index.js: -------------------------------------------------------------------------------- 1 | import fields from './lib/fields.js'; 2 | import groups from './lib/groups.js'; 3 | 4 | export default { 5 | fields: { 6 | add: fields, 7 | group: groups 8 | } 9 | }; 10 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /backend/sites/lib/theme-demo.js: -------------------------------------------------------------------------------- 1 | export default function (site, config) { 2 | config.modules = { 3 | ...config.modules, 4 | 'theme-demo': { 5 | options: { 6 | shortName: site.shortName 7 | } 8 | } 9 | }; 10 | }; 11 | -------------------------------------------------------------------------------- /backend/sites/lib/theme-default.js: -------------------------------------------------------------------------------- 1 | export default function(site, config) { 2 | config.modules = { 3 | ...config.modules, 4 | 'theme-default': { 5 | options: { 6 | shortName: site.shortName 7 | } 8 | } 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); -------------------------------------------------------------------------------- /backend/dashboard/index.js: -------------------------------------------------------------------------------- 1 | export default { 2 | root: import.meta, 3 | privateDashboards: true, 4 | modules: { 5 | '@apostrophecms/express': {}, 6 | '@apostrophecms/uploadfs': {}, 7 | '@apostrophecms-pro/multisite-dashboard': {}, 8 | site: {}, 9 | 'site-page': {}, 10 | '@apostrophecms/vite': {} 11 | } 12 | }; 13 | -------------------------------------------------------------------------------- /backend/themes.js: -------------------------------------------------------------------------------- 1 | // Maintained in one place so we don't forget to add them to the 2 | // list-themes task, which apostrophe cloud needs to minify assets 3 | // for each theme 4 | 5 | export default [ 6 | { 7 | value: 'default', 8 | label: 'Default' 9 | }, 10 | { 11 | value: 'demo', 12 | label: 'Demo' 13 | } 14 | ]; 15 | -------------------------------------------------------------------------------- /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/sites/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/domains.js: -------------------------------------------------------------------------------- 1 | export default { 2 | local: 'localhost:4321', 3 | // Should be a real registered domain or subdomain with a 4 | // DNS wildcard pointing to the cloud 5 | staging: 'a3-assembly-staging.apostrophecms.com', 6 | // Should be a real registered domain or subdomain with a 7 | // DNS wildcard pointing to the cloud 8 | prod: 'a3-assembly-demo.apostrophecms.com' 9 | }; 10 | -------------------------------------------------------------------------------- /backend/sites/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/sites/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/sites/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 | -------------------------------------------------------------------------------- /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/sites/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 | -------------------------------------------------------------------------------- /backend/deployment/for-all-themes: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import { spawnSync as spawn } from 'child_process'; 4 | import themes from '../themes.js'; 5 | 6 | for (const theme of themes) { 7 | const args = [ 'app', ...process.argv.slice(2), '--temporary-site', `--theme=${theme.value}` ]; 8 | const result = spawn('node', args, { 9 | encoding: 'utf8', 10 | stdio: 'inherit' 11 | }); 12 | if (result.status !== 0) { 13 | throw new Error(result.status || ('exited on signal ' + result.signal)); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /backend/sites/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/sites/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 | -------------------------------------------------------------------------------- /backend/sites/modules/theme-default/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * An empty theme to use as a starting point for custom development 3 | */ 4 | 5 | export default { 6 | options: { 7 | alias: 'theme', 8 | // Silence startup warning about the lack of code since this 9 | // is just an empty starting point for your own work 10 | ignoreNoCodeWarning: true, 11 | // Silence startup warning displayed if this module is 12 | // not activated at all, since only one theme module 13 | // will be activated per site 14 | ignoreUnusedFolderWarning: true 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /backend/sites/modules/theme-demo/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * An empty theme to use as a starting point for custom development 3 | */ 4 | 5 | export default { 6 | options: { 7 | alias: 'theme', 8 | // Silence startup warning about the lack of code since this 9 | // is just an empty starting point for your own work 10 | ignoreNoCodeWarning: true, 11 | // Silence startup warning displayed if this module is 12 | // not activated at all, since only one theme module 13 | // will be activated per site 14 | ignoreUnusedFolderWarning: true 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /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/sites/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 `dashboard.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/sites/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 | -------------------------------------------------------------------------------- /frontend/src/pages/api/apos-external-front/render-area.astro: -------------------------------------------------------------------------------- 1 | --- 2 | // In certain situations ApostropheCMS must "call back" to Astro for rendered markup, 3 | // for instance when the render-areas query parameter is used with the 4 | // ApostropheCMS REST API. 5 | // 6 | // This file must exist as a bridge between ApostropheCMS and Astro 7 | // because Vite won't compile imports of .astro files from a .js file if both of them 8 | // live in an external module. Otherwise we would ship it as a route in the 9 | // apostrophe-astro module. 10 | 11 | import AposRenderAreaForApi from '@apostrophecms/apostrophe-astro/components/AposRenderAreaForApi.astro'; 12 | 13 | --- 14 | 15 | -------------------------------------------------------------------------------- /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 `dashboard.localhost:4321`. Your ApostropheCMS instance will be served at `dashboard.localhost:3000`, but only provides information about the project status. 8 | -------------------------------------------------------------------------------- /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/nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "delay": 1000, 3 | "verbose": true, 4 | "watch": [ 5 | "./app.js", 6 | "./dashboard/index.js", 7 | "./dashboard/modules/**/*", 8 | "./dashboard/lib/**/*.js", 9 | "./dashboard/views/**/*.html", 10 | "./sites/index.js", 11 | "./sites/modules/**/*", 12 | "./sites/lib/**/*.js", 13 | "./sites/views/**/*.html" 14 | ], 15 | "ignoreRoot": [ 16 | ".git" 17 | ], 18 | "ignore": [ 19 | "**/ui/", 20 | "sites/apos-build/**", 21 | "sites/public/uploads/**", 22 | "sites/public/apos-frontend/**", 23 | "dashboard/apos-build/**", 24 | "dashboard/public/uploads/**", 25 | "dashboard/public/apos-frontend/**", 26 | "locales/*.json", 27 | "data", 28 | "node_modules" 29 | ], 30 | "ext": "json, js, cjs, html, scss, vue" 31 | } 32 | -------------------------------------------------------------------------------- /backend/sites/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 | handlers(self, options) { 31 | return { 32 | '@apostrophecms/page:beforeSend': { 33 | setTheme(req) { 34 | req.data.theme = self.apos.options.theme; 35 | } 36 | } 37 | }; 38 | } 39 | }; 40 | -------------------------------------------------------------------------------- /backend/dashboard/modules/@apostrophecms/asset/index.js: -------------------------------------------------------------------------------- 1 | export default { 2 | options: { 3 | // When not in production, refresh the page on restart 4 | refreshOnRestart: true, 5 | // Not for use with Astro, which has its own 6 | hmr: false, 7 | breakpointPreviewMode: { 8 | enable: true, 9 | screens: { 10 | desktop: { 11 | label: 'Desktop', 12 | width: '1440px', 13 | height: '900px', 14 | icon: 'monitor-icon', 15 | shortcut: true 16 | }, 17 | tablet: { 18 | label: 'Tablet', 19 | width: '769px', 20 | height: '768px', 21 | icon: 'tablet-icon', 22 | shortcut: true 23 | }, 24 | mobile: { 25 | label: 'Mobile', 26 | width: '479px', 27 | height: '896px', 28 | icon: 'cellphone-icon', 29 | shortcut: true 30 | } 31 | } 32 | } 33 | } 34 | }; 35 | -------------------------------------------------------------------------------- /backend/sites/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/sites/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/sites/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/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 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "apollo-assembly", 3 | "version": "1.0.0", 4 | "description": "The base Apollo theme for ApostropheCMS+Astro with Assembly", 5 | "type": "module", 6 | "scripts": { 7 | "test": "npm test --workspaces --if-present", 8 | "build": "npm run build --workspaces", 9 | "migrate": "npm run migrate --workspace=backend", 10 | "serve-frontend": "npm run serve --workspace=frontend", 11 | "serve-backend": "npm run serve --workspace=backend" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/apostrophecms/starter-kit-astro-apollo-assembly.git" 16 | }, 17 | "keywords": [ 18 | "astro", 19 | "apostrophecms", 20 | "apostrophe" 21 | ], 22 | "author": "Apostrophe Technologies Inc.", 23 | "license": "MIT", 24 | "bugs": { 25 | "url": "https://github.com/apostrophecms/starter-kit-astro-apollo-assembly/issues" 26 | }, 27 | "homepage": "https://github.com/apostrophecms/starter-kit-astro-apollo-assembly#readme", 28 | "workspaces": [ 29 | "backend", 30 | "frontend" 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /backend/sites/modules/@apostrophecms/asset/index.js: -------------------------------------------------------------------------------- 1 | export default { 2 | options: { 3 | // When not in production, refresh the page on restart 4 | refreshOnRestart: true, 5 | // Not for use with Astro, which has its own 6 | hmr: false, 7 | breakpointPreviewMode: { 8 | enable: true, 9 | screens: { 10 | desktop: { 11 | label: 'Desktop', 12 | width: '1440px', 13 | height: '900px', 14 | icon: 'monitor-icon', 15 | shortcut: true 16 | }, 17 | tablet: { 18 | label: 'Tablet', 19 | width: '769px', 20 | height: '768px', 21 | icon: 'tablet-icon', 22 | shortcut: true 23 | }, 24 | mobile: { 25 | label: 'Mobile', 26 | width: '479px', 27 | height: '896px', 28 | icon: 'cellphone-icon', 29 | shortcut: true 30 | } 31 | } 32 | } 33 | }, 34 | methods(self) { 35 | return { 36 | getNamespace() { 37 | return self.apos.options.theme; 38 | } 39 | }; 40 | } 41 | }; 42 | -------------------------------------------------------------------------------- /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. -------------------------------------------------------------------------------- /frontend/public/images/missing-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 8 | 10 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /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 DataSetWidget from './DataSetWidget.astro'; 11 | import LayoutWidget from '@apostrophecms/apostrophe-astro/widgets/LayoutWidget.astro'; 12 | import LayoutColumnWidget from '@apostrophecms/apostrophe-astro/widgets/LayoutColumnWidget.astro'; 13 | 14 | const widgetComponents = { 15 | '@apostrophecms/rich-text': RichTextWidget, 16 | '@apostrophecms/image': ImageWidget, 17 | '@apostrophecms/video': VideoWidget, 18 | '@apostrophecms/file': FileWidget, 19 | 'accordion': AccordionWidget, 20 | 'card': CardWidget, 21 | 'hero': HeroWidget, 22 | 'link': LinkWidget, 23 | 'slideshow': SlideshowWidget, 24 | '@apostrophecms-pro/data-set': DataSetWidget, 25 | '@apostrophecms/layout': LayoutWidget, 26 | '@apostrophecms/layout-column': LayoutColumnWidget 27 | }; 28 | 29 | export default widgetComponents; 30 | -------------------------------------------------------------------------------- /backend/sites/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/sites/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 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "apollo-assembly-frontend", 3 | "type": "module", 4 | "description": "Astro frontend for the ApostropheCMS Multisite + Astro Apollo template", 5 | "version": "1.0.0", 6 | "scripts": { 7 | "dev": "APOS_EXTERNAL_FRONT_KEY=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": ">=20.3.0 <21 || >=22.0.0", 16 | "npm": ">=9.0.0" 17 | }, 18 | "repository": { 19 | "type": "git", 20 | "url": "https://github.com/apostrophecms/starter-kit-astro-apollo-assembly" 21 | }, 22 | "dependencies": { 23 | "@apostrophecms/apostrophe-astro": "^1.7.0", 24 | "@astrojs/node": "^9.0.0", 25 | "@fortawesome/fontawesome-svg-core": "^6.6.0", 26 | "@fortawesome/free-brands-svg-icons": "^6.6.0", 27 | "@fortawesome/free-regular-svg-icons": "^6.6.0", 28 | "@fortawesome/free-solid-svg-icons": "^6.6.0", 29 | "accordion-js": "^3.3.4", 30 | "astro": "^5.0.9", 31 | "bulma": "^1.0.2", 32 | "dayjs": "^1.11.10", 33 | "postcss-viewport-to-container-toggle": "^2.0.1", 34 | "vite": "^5.0.7" 35 | }, 36 | "devDependencies": { 37 | "sass-embedded": "^1.81.0" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /frontend/src/components/DataSetTable.astro: -------------------------------------------------------------------------------- 1 | --- 2 | const { widget, dataSet, columns, options = {} } = Astro.props; 3 | 4 | const className = options.className || null; 5 | 6 | // Get the data array - handle nested structure 7 | const tableData = dataSet?.data?.data || []; 8 | 9 | // Get columns array 10 | const tableColumns = columns || []; 11 | 12 | // Check if we're in edit mode (equivalent to data.user in Nunjucks) 13 | const isEdit = widget._edit && Astro.url.searchParams.get('aposEdit'); 14 | --- 15 | { tableData ? ( 16 | 17 |
22 | 23 | 24 | 25 | {tableColumns.map((column) => ( 26 | 27 | ))} 28 | 29 | 30 | 31 | {tableData.map((row) => ( 32 | 33 | {tableColumns.map((column) => ( 34 | 37 | ))} 38 | 39 | ))} 40 | 41 |
{column}
35 | {row[column] || ''} 36 |
42 |
43 | ) : isEdit ? ( 44 | 45 |

46 | Dataset not selected 47 |

48 | ) : null} 49 | -------------------------------------------------------------------------------- /backend/sites/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/sites/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/sites/modules/@apostrophecms-pro/palette/lib/groups.js: -------------------------------------------------------------------------------- 1 | import fields from './fields.js'; 2 | 3 | function getKeysWithPrefix(obj, prefix) { 4 | return Object.keys(obj).filter(key => key.startsWith(prefix)); 5 | } 6 | 7 | export default { 8 | site: { 9 | label: 'Site', 10 | fields: getKeysWithPrefix(fields, 'site') 11 | }, 12 | header: { 13 | label: 'Header', 14 | fields: getKeysWithPrefix(fields, 'header') 15 | }, 16 | headings: { 17 | label: 'Headings', 18 | group: { 19 | h1: { 20 | label: 'Heading One', 21 | fields: getKeysWithPrefix(fields, 'h1') 22 | }, 23 | h2: { 24 | label: 'Heading Two', 25 | fields: getKeysWithPrefix(fields, 'h2') 26 | }, 27 | h3: { 28 | label: 'Heading Three', 29 | fields: getKeysWithPrefix(fields, 'h3') 30 | }, 31 | h4: { 32 | label: 'Heading Four', 33 | fields: getKeysWithPrefix(fields, 'h4') 34 | }, 35 | h5: { 36 | label: 'Heading Five', 37 | fields: getKeysWithPrefix(fields, 'h5') 38 | } 39 | } 40 | }, 41 | paragraphs: { 42 | label: 'Paragraphs', 43 | fields: getKeysWithPrefix(fields, 'p') 44 | }, 45 | buttons: { 46 | label: 'Buttons', 47 | fields: getKeysWithPrefix(fields, 'button') 48 | }, 49 | footer: { 50 | label: 'Footer', 51 | fields: getKeysWithPrefix(fields, 'footer') 52 | } 53 | }; 54 | -------------------------------------------------------------------------------- /backend/.gitignore: -------------------------------------------------------------------------------- 1 | # build output 2 | /public/apos-minified 3 | */apos-build 4 | /apos-build 5 | /public/apos-frontend 6 | /modules/asset/ui/public/site.js 7 | 8 | # dependencies 9 | node_modules/ 10 | 11 | # logs 12 | npm-debug.log 13 | 14 | # locales 15 | /dashboard/locales 16 | /locales 17 | 18 | # content 19 | /public/uploads 20 | # Don't commit masters generated on the fly at startup, these import all the rest 21 | /public/css/master-*.less 22 | */public/apos-frontend 23 | /dashboard/modules/asset/ui/public 24 | /sites/modules/theme-*/ui/public 25 | */public/modules 26 | */public/uploads 27 | */public/svgs/*.svg 28 | */public/apos-minified 29 | # This folder is created on the fly and contains symlinks updated at startup (we'll come up with a Windows solution that actually copies things) 30 | /public/modules 31 | # We don't commit CSS, only LESS 32 | */public/css/*.css 33 | */public/css/*.less 34 | # Don't commit CSS sourcemap files 35 | */public/css/*.map 36 | /public/js/_site-compiled.js 37 | /public/sitemap.xml 38 | dashboard/modules/assets/public/css/site.css 39 | dashboard/modules/assets/public/js/site.js 40 | sites/modules/theme-*/public/js/site.js 41 | sites/modules/theme-*/public/css/site.css 42 | 43 | # local env data 44 | /data 45 | /data/temp/uploadfs 46 | /dashboard/data 47 | /sites/data 48 | 49 | # macOS-specific files 50 | .DS_Store 51 | 52 | # vim swapfiles 53 | *.swp 54 | 55 | # Deployed, but not committed 56 | /release-id 57 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /backend/sites/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 | -------------------------------------------------------------------------------- /frontend/src/widgets/DataSetWidget.astro: -------------------------------------------------------------------------------- 1 | --- 2 | /** 3 | * @typedef {Object} DataSetItem 4 | * @property {string} _id - The dataset ID 5 | * @property {Object} data - The dataset container 6 | * @property {Array} data.data - Array of row data objects 7 | */ 8 | 9 | /** 10 | * @typedef {Object} Widget 11 | * @property {Array} [_dataSet] - Array of datasets 12 | * @property {Array} [columns] - Column names for the table 13 | * @property {string} template - Template to be used for data render 14 | */ 15 | 16 | /** 17 | * @typedef {Object} Props 18 | * @property {Widget} widget - The widget data and configuration 19 | * @property {Object} [options] - Per-area options including className 20 | */ 21 | 22 | // Add any additional template imports here 23 | import DataSetTable from '../components/DataSetTable.astro'; 24 | 25 | // Map template components for easier use 26 | const TEMPLATE_COMPONENTS = { 27 | table: DataSetTable 28 | }; 29 | 30 | const { widget, options } = Astro.props; 31 | const { _dataSet, columns, template = 'table' } = widget; 32 | 33 | const TemplateComponent = TEMPLATE_COMPONENTS[template] || TEMPLATE_COMPONENTS.table; 34 | 35 | // set shared template props 36 | const templateProps = { 37 | widget, 38 | dataSet: _dataSet?.[0], 39 | columns, 40 | options 41 | }; 42 | --- 43 | {TemplateComponent ? ( 44 | 45 | ) : ( 46 |
47 |

Unknown template: "{template}"

48 |

Available templates: {Object.keys(TEMPLATE_COMPONENTS).join(', ')}

49 |
50 | )} -------------------------------------------------------------------------------- /backend/sites/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /backend/app.js: -------------------------------------------------------------------------------- 1 | import multisite from '@apostrophecms-pro/multisite'; 2 | import sites from './sites/index.js'; 3 | import dashboard from './dashboard/index.js'; 4 | 5 | export default await multisite({ 6 | root: import.meta, 7 | 8 | // Port to listen on, or set the `PORT` env var (which Heroku will do for you) 9 | port: 3000, 10 | 11 | // Change to a fallback prefix more appropriate so you can have multiple unrelated 12 | // multisite projects 13 | shortNamePrefix: process.env.SHORTNAME_PREFIX || 'apollo-assembly-', 14 | 15 | // MongoDB URL for database connection. If you have multiple physical 16 | // servers then you MUST configure this to a SHARED server (which 17 | // may be a replica set). Can be set via MONGODB_URL env var 18 | mongodbUrl: process.env.APOS_MONGODB_URI || 'mongodb://localhost:27017', 19 | 20 | // Session secret. Please use a unique string. 21 | sessionSecret: 'CHANGEME', 22 | 23 | // This is our default HTTP Keep-Alive time, in ms, for reuse of 24 | // connections. Should be longer than that of the reverse proxy 25 | // (nginx: 75 seconds, AWS ELB: 60 seconds, etc) 26 | keepAliveTimeout: 100 * 1000, 27 | 28 | // Set to true to proactively initialize all site instances during startup, 29 | // before the server begins listening for traffic. Can also be an array 30 | // of site IDs or shortNames to prewarm only specific sites. 31 | // Reduces initial request latency for prewarmed sites. Defaults to false. 32 | prewarmSites: false, 33 | 34 | // Grace period (in milliseconds) before destroying an old site instance 35 | // after a zero-downtime reload completes. Defaults to 60000 (1 minute). 36 | oldInstanceGracePeriod: 60000, 37 | 38 | orphan(req, res) { 39 | console.error(`method: ${req.method} url: ${req.url} host: ${req.host} host header: ${ req.headers.host}`); 40 | return res.status(404).send('not found'); 41 | }, 42 | sites, 43 | dashboard 44 | }); 45 | -------------------------------------------------------------------------------- /backend/sites/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/astro.config.mjs: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'astro/config'; 2 | import node from '@astrojs/node'; 3 | import apostrophe from '@apostrophecms/apostrophe-astro'; 4 | 5 | // https://astro.build/config 6 | export default defineConfig({ 7 | output: "server", 8 | server: { 9 | port: process.env.PORT ? parseInt(process.env.PORT) : 4321, 10 | // Required for some hosting, like Heroku 11 | host: true 12 | }, 13 | adapter: node({ 14 | mode: 'standalone' 15 | }), 16 | integrations: [ 17 | apostrophe({ 18 | // Yes, just localhost. This is the simplest and most efficient mode of operation: 19 | // apostrophe and astro on the same server. Individual sites such as the dashboard 20 | // or a particular customer site will still automatically be distinguished via the Host header. 21 | // Don't try to use dashboard.localhost here as that will confuse deployment environments 22 | // where subdomains of localhost don't automatically work 23 | aposHost: 'http://localhost:3000', 24 | widgetsMapping: './src/widgets', 25 | templatesMapping: './src/templates', 26 | includeResponseHeaders: [ 27 | 'content-security-policy', 28 | 'strict-transport-security', 29 | 'x-frame-options', 30 | 'referrer-policy', 31 | 'cache-control' 32 | ], 33 | excludeRequestHeaders: [ 34 | // For hosting on multiple servers, block the host header 35 | // 'host' 36 | ] 37 | }), 38 | ], 39 | vite: { 40 | css: { 41 | preprocessorOptions: { 42 | scss: { 43 | quietDeps: true 44 | } 45 | } 46 | }, 47 | ssr: { 48 | // Do not externalize the @apostrophecms/apostrophe-astro plugin, we need 49 | // to be able to use virtual: URLs there 50 | noExternal: ['@apostrophecms/apostrophe-astro'] 51 | } 52 | }, 53 | css: { 54 | preprocessorOptions: { 55 | scss: { 56 | api: 'modern-compiler', 57 | } 58 | } 59 | } 60 | }); 61 | -------------------------------------------------------------------------------- /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/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "apollo-assembly-backend", 3 | "version": "1.0.0", 4 | "description": "Apostrophe backend for the ApostropheCMS Multisite + Astro Apollo template", 5 | "main": "app.js", 6 | "type": "module", 7 | "scripts": { 8 | "start": "node app", 9 | "serve": "APOS_UPLOADFS_ASSETS=1 NODE_ENV=production npm run start", 10 | "dev": "APOS_EXTERNAL_FRONT_KEY=dev nodemon", 11 | "build": "./deployment/build", 12 | "release": "npm install && npm run build && npm run migrate", 13 | "migrate": "./deployment/migrate", 14 | "test": "npm run lint", 15 | "lint": "eslint . && stylelint {dashboard,sites}/modules/**/*.{scss,vue}" 16 | }, 17 | "engines": { 18 | "node": ">=20.3.0 <21 || >=22.0.0", 19 | "npm": ">=9.0.0" 20 | }, 21 | "repository": { 22 | "type": "git", 23 | "url": "https://github.com/apostrophecms/starter-kit-astro-apollo-assembly" 24 | }, 25 | "author": "Apostrophe Technologies, Inc.", 26 | "license": "MIT", 27 | "dependencies": { 28 | "@apostrophecms-pro/advanced-permission": "^3.0.4", 29 | "@apostrophecms-pro/automatic-translation": "^1.4.2", 30 | "@apostrophecms-pro/doc-template-library": "^2.2.6", 31 | "@apostrophecms-pro/document-versions": "^2.6.0", 32 | "@apostrophecms-pro/import-export-translation": "^1.0.3", 33 | "@apostrophecms-pro/multisite": "^4.4.1", 34 | "@apostrophecms-pro/multisite-dashboard": "^1.5.0", 35 | "@apostrophecms-pro/palette": "^4.8.1", 36 | "@apostrophecms-pro/section-template-library": "^1.0.0", 37 | "@apostrophecms-pro/seo-assistant": "^1.2.1", 38 | "@apostrophecms/favicon": "^1.1.3", 39 | "@apostrophecms/mongodb-snapshot": "^1.1.0", 40 | "@apostrophecms/open-graph": "^1.2.3", 41 | "@apostrophecms/seo": "^1.3.1", 42 | "@apostrophecms/vite": "^1.1.0", 43 | "apostrophe": "^4.23.0", 44 | "normalize.css": "^8.0.1" 45 | }, 46 | "devDependencies": { 47 | "eslint-config-apostrophe": "^6.0.1", 48 | "nodemon": "^3.0.1", 49 | "stylelint": "^16.23.1", 50 | "stylelint-config-apostrophe": "^4.3.0" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /backend/sites/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/sites/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 | -------------------------------------------------------------------------------- /frontend/src/lib/use-site-config.js: -------------------------------------------------------------------------------- 1 | export function useSiteConfig(globalData) { 2 | const brandingGroup = globalData?.brandingGroup || {}; 3 | const headerGroup = globalData?.headerGroup || {}; 4 | 5 | const getHeaderClasses = () => { 6 | const classes = ['navbar']; 7 | 8 | // Add position class based on headerMode or headerPosition 9 | const headerPosition = headerGroup.headerPosition; 10 | 11 | if (headerPosition === 'fixed') { 12 | classes.push('is-fixed-top'); // Bulma's fixed top class 13 | } else if (headerPosition === 'fixed-fade') { 14 | classes.push('is-fixed-top', 'is-fixed-fade'); 15 | } 16 | 17 | // Add spacing class 18 | if (headerGroup.spacing) { 19 | classes.push(headerGroup.spacing); 20 | } 21 | 22 | return classes.join(' '); 23 | }; 24 | 25 | const getNavItemClasses = (isActive = false) => { 26 | const classes = ['navbar-item']; 27 | 28 | if (isActive) { 29 | classes.push('is-active'); 30 | } 31 | 32 | return classes.join(' '); 33 | }; 34 | 35 | const getDropdownClasses = () => { 36 | const classes = ['navbar-dropdown']; 37 | return classes.join(' '); 38 | }; 39 | 40 | const renderBranding = (isMobile = false) => { 41 | const displayType = isMobile && brandingGroup.mobileDisplayPreference !== 'same' 42 | ? brandingGroup.mobileDisplayPreference 43 | : brandingGroup.brandingType; 44 | 45 | const elements = []; 46 | 47 | // Add logo if needed 48 | if (displayType === 'logo' || displayType === 'both') { 49 | if (brandingGroup.siteLogo?._urls?.max) { 50 | elements.push( 51 | `` 57 | ); 58 | } 59 | } 60 | 61 | // Add text if needed 62 | if (displayType === 'text' || displayType === 'both') { 63 | elements.push( 64 | ` 65 | ${brandingGroup.siteTitle} 66 | ` 67 | ); 68 | } 69 | 70 | return elements.join(''); 71 | }; 72 | 73 | return { 74 | getHeaderClasses, 75 | getNavItemClasses, 76 | getDropdownClasses, 77 | renderBranding 78 | }; 79 | } -------------------------------------------------------------------------------- /backend/sites/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/sites/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 || 'unknown'} · {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/sites/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 | '@apostrophecms-pro/data-set': {} 26 | } 27 | } 28 | }; 29 | 30 | /** 31 | * Creates the groups configuration for ApostropheCMS widget areas 32 | * @param {Object} options - Configuration options 33 | * @param {boolean} options.includeLayouts - If true, 34 | * includes layout widgets in the groups 35 | * @param {Array} options.exclude - Array of widget names to exclude 36 | * @returns {Object} Returns the groups configuration object 37 | * 38 | * @example 39 | * // In your page type or piece type: 40 | * fields: { 41 | * add: { 42 | * main: { 43 | * type: 'area', 44 | * options: { 45 | * // Get grouped widgets configuration 46 | * ...getWidgetGroups({ 47 | * includeLayouts: true, 48 | * exclude: ['hero'] 49 | * }), 50 | * // Add any additional area options 51 | * max: 10, 52 | * min: 1 53 | * } 54 | * } 55 | * } 56 | * } 57 | */ 58 | export const getWidgetGroups = ({ 59 | includeLayouts = false, 60 | exclude = [] 61 | } = {}) => { 62 | // Initialize our groups object 63 | const groups = {}; 64 | 65 | // Add layout widgets if requested 66 | if (includeLayouts) { 67 | groups.layout = { 68 | ...widgetGroups.layout, 69 | // Filter out any excluded widgets 70 | widgets: Object.fromEntries( 71 | Object.entries(widgetGroups.layout.widgets) 72 | .filter(([ key ]) => !exclude.includes(key)) 73 | ) 74 | }; 75 | } 76 | 77 | // Always add content widgets 78 | groups.content = { 79 | ...widgetGroups.content, 80 | // Filter out any excluded widgets 81 | widgets: Object.fromEntries( 82 | Object.entries(widgetGroups.content.widgets) 83 | .filter(([ key ]) => !exclude.includes(key)) 84 | ) 85 | }; 86 | 87 | // Return just the expanded and groups properties 88 | // This allows other area options to be spread alongside it 89 | return { 90 | expanded: true, 91 | groups 92 | }; 93 | }; 94 | -------------------------------------------------------------------------------- /backend/sites/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/sites/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 | -------------------------------------------------------------------------------- /frontend/src/widgets/LinkWidget.astro: -------------------------------------------------------------------------------- 1 | --- 2 | /** 3 | * @typedef {Object} LinkWidget 4 | * @property {'page'|'file'|'url'} linkType - Type of link 5 | * @property {Object} _linkPage - Page link object 6 | * @property {string} _linkPage._url - URL for page link 7 | * @property {Object} _linkFile - File link object 8 | * @property {string} _linkFile._url - URL for file link 9 | * @property {string} linkUrl - Direct URL 10 | * @property {string} linkText - Text to display 11 | * @property {'button'|'link'} linkStyle - Style of the link 12 | * @property {string} [buttonColor] - Button color variant 13 | * @property {string} [buttonStyle] - Button style variant 14 | * @property {string} [buttonSize] - Button size variant 15 | * @property {boolean} [buttonDisabled] - Disabled state 16 | * @property {string} [linkTarget] - Target attribute value 17 | * @property {string} [icon] - Icon identifier 18 | * @property {'left'|'right'} [iconPosition] - Icon position 19 | * @property {'left'|'center'|'right'} [buttonAlignment='left'] - Button alignment 20 | */ 21 | 22 | // Constants 23 | const ALIGNMENT_MAP = { 24 | 'left': 'is-justify-content-flex-start', 25 | 'center': 'is-justify-content-center', 26 | 'right': 'is-justify-content-flex-end' 27 | }; 28 | 29 | const { widget } = Astro.props; 30 | 31 | // Determine the link path based on type 32 | const path = (() => { 33 | switch (widget.linkType) { 34 | case 'page': 35 | return widget._linkPage?.[0]?._url; 36 | case 'file': 37 | return widget._linkFile?.[0]?._url; 38 | default: 39 | return widget.linkUrl; 40 | } 41 | })(); 42 | 43 | const getButtonStyle = (style) => style ? `is-${style}` : ''; 44 | 45 | // Build button classes 46 | const isButton = widget.linkStyle === 'button'; 47 | const buttonClasses = isButton 48 | ? [ 49 | 'button default', 50 | getButtonStyle(widget.buttonStyle), 51 | widget.buttonSize && `is-${widget.buttonSize}` 52 | ].filter(Boolean).join(' ') 53 | : ''; 54 | 55 | const linkTarget = widget.linkTarget?.includes('_blank') ? '_blank' : ''; 56 | 57 | // Build link attributes 58 | const attributes = { 59 | target: linkTarget, 60 | class: isButton ? buttonClasses : 'link', 61 | href: path, 62 | ...(linkTarget === '_blank' && { rel: 'noopener noreferrer' }), 63 | ...(isButton && { role: 'button' }), 64 | ...(widget.buttonDisabled ? { 65 | 'disabled': true, 66 | 'aria-disabled': 'true', 67 | tabindex: '-1' 68 | } : {}) 69 | }; 70 | 71 | const hasIcon = Boolean(widget.icon); 72 | const alignmentClass = ALIGNMENT_MAP[widget.buttonAlignment || 'left']; 73 | 74 | // Prepare aria-label if link opens in new tab 75 | const ariaLabel = linkTarget === '_blank' 76 | ? `${widget.linkText} (opens in new tab)` 77 | : undefined; 78 | --- 79 | 80 | -------------------------------------------------------------------------------- /backend/sites/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 |