### Start Development Server
Source: https://github.com/thanush-babu-fleet/fs-apos-starter-kit-forked/blob/main/README.md
Launch the development environment to watch for file changes and trigger automatic builds.
```sh
npm run dev
```
--------------------------------
### Create and Initialize an ol/Map Instance
Source: https://github.com/thanush-babu-fleet/fs-apos-starter-kit-forked/blob/main/README.md
Instantiate the 'Map' class with a target element, layers, and view settings. This is a basic setup for displaying a map.
```javascript
const map = new Map({
target: 'map',
layers: [
new TileLayer({
source: new OSM(),
}),
],
view: new View({
center: [0, 0],
zoom: 2,
}),
});
```
--------------------------------
### Import ol/Map, View, TileLayer, and OSM
Source: https://github.com/thanush-babu-fleet/fs-apos-starter-kit-forked/blob/main/README.md
Import necessary components from the 'ol' package for map creation. Ensure the 'ol' package is installed.
```javascript
import { Map, View } from 'ol';
import TileLayer from 'ol/layer/Tile';
import OSM from 'ol/source/OSM';
```
--------------------------------
### Render an Image with Alt Text and URL
Source: https://github.com/thanush-babu-fleet/fs-apos-starter-kit-forked/blob/main/views/ui.html
This macro renders an image using the `apos.image.first` helper to get the attachment and `apos.attachment.url` to construct the image source. It includes alt text from the attachment's `_alt` property. The `loading` attribute can be set for lazy loading.
```nunjucks
{# Image #} {% macro image (image, className = '', loading = '') %}
{% set attachment = apos.image.first(image) %}
{% if attachment %}  }}) {% endif %}
{% endmacro %}
```
--------------------------------
### Pagination Logic (Nunjucks)
Source: https://github.com/thanush-babu-fleet/fs-apos-starter-kit-forked/blob/main/views/fragments/paginator.html
This section of the Nunjucks fragment calculates the start and end page numbers for XS, Mobile, and Desktop views based on the current page and total pages. It also handles the display of first/last page buttons and ellipsis.
```Nunjucks
Prev Previous {# XS range (< 375px): show only current page and last page #} {% set xsRangeStart = data.currentPage %} {% set xsRangeEnd = data.currentPage %} {# Mobile range (375px - 1023px): show 3 pages centered on current (current ± 1) #} {% if data.currentPage == 1 %} {% set mobileRangeStart = 1 %} {% set mobileRangeEnd = 3 if 3 <= data.totalPages else data.totalPages %} {% elif data.currentPage == data.totalPages %} {% set mobileRangeStart = (data.totalPages - 2) if (data.totalPages - 2) >= 1 else 1 %} {% set mobileRangeEnd = data.totalPages %} {% else %} {% set mobileRangeStart = data.currentPage - 1 %} {% set mobileRangeEnd = data.currentPage + 1 if data.currentPage + 1 <= data.totalPages else data.totalPages %} {% endif %} {# Desktop range: show 5 pages (current ± 2) #} {% set desktopRangeStart = data.currentPage - 2 if data.currentPage > 2 else 1 %} {% set desktopRangeEnd = desktopRangeStart + 4 if desktopRangeStart + 4 <= data.totalPages else data.totalPages %} {# XS: Show only current page #} {% if data.currentPage == data.totalPages %} {# If on last page, show only current #} {{ '0' ~ data.currentPage if data.currentPage < 10 else data.currentPage }} {% else %} {# Show current page and last page #} {{ '0' ~ data.currentPage if data.currentPage < 10 else data.currentPage }} ... {{ '0' ~ data.totalPages if data.totalPages < 10 else data.totalPages }} {% endif %} {# First page button (show on mobile only if needed, always on desktop) #} {% if mobileRangeStart > 1 %} 01 ... {% endif %} {% if desktopRangeStart > 1 %} 01 ... {% endif %} {# Mobile page numbers #} {% for page in range(mobileRangeStart, mobileRangeEnd + 1) %} {% if page == data.currentPage %} {{ '0' ~ page if page < 10 else page }} {% else %} {{ '0' ~ page if page < 10 else page }} {% endif %} {% endfor %} {# Desktop page numbers #} {% for page in range(desktopRangeStart, desktopRangeEnd + 1) %} {% if page == data.currentPage %} {{ '0' ~ page if page < 10 else page }} {% else %} {{ '0' ~ page if page < 10 else page }} {% endif %} {% endfor %} {# Last page button #} {% if mobileRangeEnd < data.totalPages %} ... {{ '0' ~ data.totalPages if data.totalPages < 10 else data.totalPages }} {% endif %} {% if desktopRangeEnd < data.totalPages %} ... {{ '0' ~ data.totalPages if data.totalPages < 10 else data.totalPages }} {% endif %} Next
{% endfragment %}
```
--------------------------------
### Initialize Project with CLI
Source: https://github.com/thanush-babu-fleet/fs-apos-starter-kit-forked/blob/main/README.md
Create a new project using the Apostrophe CLI tool with the marketing starter template.
```sh
apos create my-project --starter=marketing
```
--------------------------------
### Initialize ApostropheCMS Application
Source: https://context7.com/thanush-babu-fleet/fs-apos-starter-kit-forked/llms.txt
The main entry point configures the environment, detects available ports, and initializes the ApostropheCMS instance with required modules and S3 storage settings.
```javascript
// app.js - Main application entry point
require('dotenv').config();
const DEFAULT_PORT = 3000;
async function startApp() {
let port = process.env.PORT || DEFAULT_PORT;
// Automatic port detection in development
if (process.env.NODE_ENV !== 'production') {
const detectPort = require('detect-port').default || require('detect-port');
const detectedPort = await detectPort(DEFAULT_PORT);
port = detectedPort;
}
process.env.PORT = port;
const appName = 'a3-starter-kit-marketing';
const baseUrl = process.env.APOS_BASE_URL || `http://localhost:${port}`;
if (!process.env.APOS_MONGODB_URI) {
throw new Error('APOS_MONGODB_URI is missing.');
}
require('apostrophe')({
shortName: appName,
baseUrl,
nestedModuleSubdirs: true,
options: {
uploadfs: {
storage: 's3',
secret: process.env.APOS_S3_SECRET,
key: process.env.APOS_S3_KEY,
bucket: process.env.APOS_S3_BUCKET,
region: process.env.APOS_S3_REGION,
contentTypes: {
'video/mp4': 'mp4',
'video/webm': 'webm',
},
params: { CacheControl: 'public, max-age=31536000, immutable' },
acl: null,
optimize: true,
},
},
modules: {
'@apostrophecms/rich-text-widget': {},
'@apostrophecms/image-widget': { options: { className: 'img-fluid' } },
'@apostrophecms/video-widget': {},
'@apostrophecms/form': { options: { shortcut: 'a,f' } },
'@apostrophecms/email': {
options: {
nodemailer: {
host: process.env.SMTP_HOST || 'smtp-relay.brevo.com',
port: parseInt(process.env.SMTP_PORT) || 587,
auth: {
user: process.env.SMTP_USER,
pass: process.env.SMTP_PASSWORD
}
}
}
},
'@apostrophecms/sitemap': { options: { excludeTypes: ['team-member', 'product'] } },
'@apostrophecms/seo': {},
'@apostrophecms/open-graph': {},
'@apostrophecms/redirect': { options: { statusCode: 301 } },
// Custom modules
helper: {},
asset: {},
settings: {},
'default-page': {},
'content-widget-modules': { options: { ignoreNoCodeWarning: true } },
'pieces-modules': { options: { ignoreNoCodeWarning: true } },
blog: {},
'blog-page': {},
// ... additional modules
},
bundles: ['@bodonkey/rich-text-enhancement']
});
}
startApp().catch((err) => {
console.error('Failed to start app:', err);
process.exit(1);
});
```
--------------------------------
### Initialize PhotoSwipe Lightbox
Source: https://github.com/thanush-babu-fleet/fs-apos-starter-kit-forked/blob/main/README.md
Import and initialize 'PhotoSwipeLightbox' for creating responsive image galleries with lightbox functionality. Configure options like background opacity, animations, and navigation.
```javascript
import PhotoSwipeLightbox from 'photoswipe/lightbox';
```
```javascript
import PhotoSwipe from 'photoswipe';
```
```javascript
// Photoswiper lightbox and gallery
const photoSwipeOptions = {
gallery: '#imageGallery',
pswpModule: PhotoSwipe,
// set background opacity
bgOpacity: 1,
showHideOpacity: true,
children: 'a',
loop: true,
showHideAnimationType: 'fade', /* options: fade, zoom, none */
/* Click on image moves to the next slide */
imageClickAction: 'next',
tapAction: 'next',
/* ## Hiding a specific UI element ## */
zoom: false,
close: true,
counter: true,
arrowKeys: true
};
const lightbox = new PhotoSwipeLightbox(photoSwipeOptions);
lightbox.init();
```
--------------------------------
### Environment Variables Sample - Bash
Source: https://context7.com/thanush-babu-fleet/fs-apos-starter-kit-forked/llms.txt
Provides a sample `.env` file with essential environment variables for application, database, storage, and third-party service configuration. These variables are crucial for running the application in different environments.
```bash
# .env.sample
# App / Server
NODE_ENV=development
APOS_BASE_URL="http://localhost:3002"
APOS_BASE_PORT=3002
APOS_SHORT_NAME=a3-starter-kit-marketing
# MongoDB (required)
APOS_MONGODB_URI="mongodb://mongo:YOUR_PASSWORD@YOUR_DOMAIN/a3-starter-kit-marketing?authSource=admin&tls=false"
# S3 Storage (optional - for file uploads)
APOS_S3_SECRET=your-s3-secret
APOS_S3_KEY=your-s3-key
APOS_S3_BUCKET=your-bucket-name
APOS_S3_REGION=us-east-1
# Geocoder (for map widget)
GEOCODER_API_KEY=your-mapbox-api-key
```
--------------------------------
### Add Admin User
Source: https://github.com/thanush-babu-fleet/fs-apos-starter-kit-forked/blob/main/README.md
Create an administrative user account via the command line.
```bash
node app @apostrophecms/user:add admin admin
```
--------------------------------
### Testimonial Slider Initialization
Source: https://github.com/thanush-babu-fleet/fs-apos-starter-kit-forked/blob/main/modules/content-widget-modules/testimonial-widget/views/widget.html
JavaScript to initialize the Slick slider and apply dynamic CSS variables for testimonial styling.
```javascript
document.addEventListener('DOMContentLoaded', function() { setTimeout(function () { const $slider = $('.testimonial-slider'); if (!$slider.length || $slider.hasClass('slick-initialized')) { return; } function applyTestimonialSlideColors($sliderInstance) { $sliderInstance .find('.slick-slide .testimonial-item, > .testimonial-item') .each(function () { const $item = $(this); const bg = $item.data('bg'); const author = $item.data('author'); const designation = $item.data('designation'); const stars = $item.data('stars'); const quoteBg = $item.data('quoteBg'); const quoteIcon = $item.data('quoteIcon'); const separator = $item.data('separator'); if (bg) { this.style.setProperty('--testimonial-bg', bg); } if (author) { this.style.setProperty('--testimonial-author', author); } if (designation) { this.style.setProperty('--testimonial-designation', designation); } if (stars) { this.style.setProperty('--testimonial-stars', stars); } if (quoteBg) { this.style.setProperty('--testimonial-quote-circle-bg', quoteBg); } if (quoteIcon) { this.style.setProperty('--testimonial-quote-icon', quoteIcon); } if (separator) { this.style.setProperty('--testimonial-separator-color', separator); } }); } // Ensure initial server-rendered slides use schema colors applyTestimonialSlideColors($slider); $slider.on('init reInit setPosition', function () { applyTestimonialSlideColors($slider); }); $slider.slick({ accessibility: true, dots: true, arrows: true, slidesToShow: 3.5, slidesToScroll: 1, adaptiveHeight: true, initialSlide: 0, centerMode: false, infinite: false, prevArrow: '', nextArrow: '', responsive: [ { breakpoint: 1280, settings: { slidesToShow: 3.5 } }, { breakpoint: 1024, settings: { slidesToShow: 2.5 } }, { breakpoint: 768, settings: { slidesToShow: 1 } } ] }); const $prevArrow = $slider.find('.slick-prev'); const $nextArrow = $slider.find('.slick-next'); const $dots = $slider.find('.slick-dots'); $prevArrow.insertBefore($dots); $nextArrow.insertAfter($dots); $slider.on('afterChange', function (event, slick, currentSlide) { const $prevArrow = $('.slick-prev'); const $nextArrow = $('.slick-next'); if (currentSlide === 0) { $prevArrow.prop('disabled', true); } else { $prevArrow.prop('disabled', false); } if ($nextArrow.hasClass('slick-disabled')) { $nextArrow.prop('disabled', true); } else { $nextArrow.prop('disabled', false); } }); $slider.slick('slickGoTo', 0); }, 100); });
```
--------------------------------
### Define Pricing Widget Configuration
Source: https://context7.com/thanush-babu-fleet/fs-apos-starter-kit-forked/llms.txt
Configures a pricing widget with an intro area and an array of up to 4 pricing cards containing content, features, and buttons.
```javascript
// modules/content-widget-modules/pricing-widget/index.js
const areaConfig = require('../../../lib/area');
const aosSchema = require('../../../lib/aosSchema.js');
const customAttributesSchema = require('../../../lib/customAttributesSchema');
module.exports = {
extend: '@apostrophecms/widget-type',
options: {
label: 'Pricing',
icon: 'cards-icon',
description: 'Display pricing cards on your page',
previewImage: 'svg'
},
fields: {
add: {
intro: {
type: 'area',
label: 'Intro',
options: { widgets: areaConfig.richText }
},
cards: {
type: 'array',
label: 'Cards',
titleField: 'label',
inline: true,
max: 4,
fields: {
add: {
label: {
type: 'string',
label: 'Label'
},
content: {
type: 'area',
label: 'Content',
options: { widgets: areaConfig.richText }
},
features: {
type: 'array',
label: 'Features list',
titleField: 'title',
fields: {
add: {
title: { type: 'string', label: 'Title' }
}
}
},
buttons: {
type: 'area',
label: 'Buttons',
options: {
max: 2,
widgets: { button: {} }
}
}
}
}
},
...aosSchema,
...customAttributesSchema
}
}
};
```
--------------------------------
### Initialize Swiper Slider
Source: https://github.com/thanush-babu-fleet/fs-apos-starter-kit-forked/blob/main/README.md
Import and initialize the 'swiper' library for creating responsive sliders and carousels. Configure navigation and other options as needed.
```javascript
import Swiper from 'swiper/bundle';
```
```javascript
const mySwiper = new Swiper('.swiper-container', {
// Optional parameters
direction: 'horizontal',
loop: true,
// Navigation arrows
navigation: {
nextEl: '.swiper-button-next',
prevEl: '.swiper-button-prev',
},
// And more...
});
```
--------------------------------
### Configure Rich Text Editor Styles and Tools
Source: https://context7.com/thanush-babu-fleet/fs-apos-starter-kit-forked/llms.txt
Sets up Tiptap editor styles and toolbar configurations for rich text widgets. Includes predefined styles for headings and highlights, and a comprehensive set of toolbar tools for formatting.
```javascript
// lib/area.js
const tiptapStyles = {
all: [
{ tag: 'h1', label: 'Heading 1 (90px)', class: 'h1' },
{ tag: 'h2', label: 'Heading 2 (67px)', class: 'h2' },
{ tag: 'h3', label: 'Heading 3 (64px)', class: 'h3' },
{ tag: 'p', label: 'Paragraph-md (22px)', class: 'p' },
{ tag: 'span', label: 'Highlight: Primary', class: 'highlight-primary' },
{ tag: 'span', label: 'Highlight: Secondary', class: 'highlight-secondary' }
// ... more styles
]
};
const tiptapTools = {
all: [
'styles', '|', 'bold', 'italic', 'strike', 'superscript', 'subscript', '|',
'link', 'anchor', 'horizontalRule', '|', 'bulletList', 'orderedList', 'blockquote', '|',
'alignLeft', 'alignCenter', 'alignRight', 'alignJustify', '|',
'colorButton', '|', 'image', 'codeBlock', '|', 'undo', 'redo'
],
simple: ['styles', '|', 'bold', 'italic', '|', 'link', '|', 'colorButton', '|', 'bulletList', 'orderedList']
};
const area = {
all: {
columns: {}, 'call-to-action': {}, 'image-gallery': {}, 'side-by-side': {},
'rich-text': {}, image: {}, map: {}, accordion: {}, pricing: {},
'hero-banner': {}, testimonial: {}, card: {}
// ... more widgets
},
columnExpandedGroup: {
basic: {
label: 'Basic Tools',
widgets: { image: {}, 'rich-text': {}, columns: {} },
columns: 2
},
layout: {
label: 'Layout Tools',
widgets: { accordion: {}, 'call-to-action': {}, 'side-by-side': {} },
columns: 2
}
},
richText: {
'@apostrophecms/rich-text': {
toolbar: tiptapTools.all,
styles: tiptapStyles.all
}
},
fullExpandedGroup: {
layout: {
label: 'Layout Tools',
widgets: { columns: {}, 'side-by-side': {} },
columns: 2
},
media: {
label: 'Media Widgets',
widgets: { image: {}, '@apostrophecms/video': {}, 'image-gallery': {} },
columns: 2
},
general: {
label: 'Content Widgets',
widgets: { 'rich-text': {}, accordion: {}, pricing: {}, testimonial: {} },
columns: 3
}
}
};
module.exports = area;
```
--------------------------------
### Initialize Swiper and PhotoSwipe Lightbox
Source: https://context7.com/thanush-babu-fleet/fs-apos-starter-kit-forked/llms.txt
Client-side JavaScript for integrating Swiper carousel and PhotoSwipe lightbox for image galleries. Requires Swiper and PhotoSwipe libraries. Configure via data attributes on the widget element.
```javascript
// modules/content-widget-modules/image-gallery-widget/ui/src/index.js
import Swiper from 'swiper/bundle';
import PhotoSwipeLightbox from 'photoswipe/lightbox';
import PhotoSwipe from 'photoswipe';
export default () => {
apos.util.widgetPlayers['image-gallery'] = {
selector: '[data-image-gallery-widget]',
player(el) {
const slidesPerView = parseInt(el.dataset.displayType) || 1;
// Initialize Swiper
const swiper = new Swiper(el.querySelector('.swiper'), {
slidesPerView: slidesPerView,
spaceBetween: 20,
loop: true,
navigation: {
nextEl: el.querySelector('.swiper-button-next'),
prevEl: el.querySelector('.swiper-button-prev')
},
pagination: {
el: el.querySelector('.swiper-pagination'),
clickable: true
},
breakpoints: {
320: { slidesPerView: 1 },
768: { slidesPerView: Math.min(slidesPerView, 2) },
1024: { slidesPerView: slidesPerView }
}
});
// Initialize PhotoSwipe Lightbox
const lightbox = new PhotoSwipeLightbox({
gallery: el.querySelector('.swiper-wrapper'),
children: 'a',
pswpModule: PhotoSwipe,
bgOpacity: 1,
showHideOpacity: true,
loop: true,
showHideAnimationType: 'fade',
imageClickAction: 'next',
tapAction: 'next',
zoom: false,
close: true,
counter: true,
arrowKeys: true
});
lightbox.init();
}
};
};
```
--------------------------------
### Render Image Widget Template
Source: https://github.com/thanush-babu-fleet/fs-apos-starter-kit-forked/blob/main/modules/content-widget-modules/image-card-widget/views/widget.html
Processes widget configuration and iterates through image collections to render them with optional link redirection.
```nunjucks
{% import "ui.html" as ui %} {% set bgImg = "" %} {% if data.widget.bgImg and apos.image.first(data.widget.bgImg) and apos.image.first(data.widget.bgImg)._urls.original %} {% set bgImg = apos.image.first(data.widget.bgImg)._urls.original %} {% endif %} {% set animationEffects = data.widget.animationEffects | default('no-animation') %} {% set customClassName = data.widget.customClassName | default('') %} {% set customId = data.widget.customId | default('') %} {% set isBorder = data.widget.isBorder | default(false) %} {% set images = data.widget.images | default([]) %}
{% for image in images %} {% set cardImage = image.cardImage | default(null) %} {% set isRedirect = image.isRedirectToAnotherPage | default(false) %} {% set imageClass = image.imageClass | default('') %} {% set ariaLabel = image.ariaLabel | default('') %} {% if isRedirect and cardImage %} {% set linkPath = apos.helper.linkPath(image) | default('#') %} {% set linkTarget = apos.helper.linkTarget(image) | default('_self') %} [{{ ui.image(cardImage, "w-20", 'lazy') }}]({{ linkPath }}) {% elif cardImage %}
{{ ui.image(cardImage, "w-20", 'lazy') }}
{% endif %} {% endfor %}
```
--------------------------------
### Define Home Page Template with Nunjucks
Source: https://github.com/thanush-babu-fleet/fs-apos-starter-kit-forked/blob/main/modules/@apostrophecms/home-page/views/page.html
Use this template to structure the home page by extending a base layout and injecting content into defined blocks.
```nunjucks
{# Home page template. Inherits and extends the shared layout. #} {% extends "layout.html" %} {# Dynamic page title: "Page Title - Site Name" #} {% block title %}{{ data.page.title }} | {{ data.global.title }}{% endblock %} {% block main %}
{% area data.page, 'main' %}
{% endblock %}
```
--------------------------------
### Initialize Filter and Search Forms
Source: https://github.com/thanush-babu-fleet/fs-apos-starter-kit-forked/blob/main/modules/blog-page/views/index.html
Sets up event listeners for filter forms and populates inputs based on current URL parameters upon DOM content load.
```javascript
let tempMobileFilters = { category: "", blogType: "" }; document.addEventListener("DOMContentLoaded", () => { const urlParams = new URLSearchParams(window.location.search); const formSelectors = [ { prefix: "mobile", form: document.getElementById("filterFormMobile"), searchName: "search_mobile", categoryName: "category_mobile", typeName: "blog_type_mobile", applyBtn: "applyFiltersBtn", searchBtn: "searchButtonMobile", }, { prefix: "desktop", form: document.getElementById("filterFormDesktop"), searchName: "search_desktop", categoryName: "category_desktop", typeName: "blog_type_desktop", searchBtn: "searchButtonDesktop", }, ]; formSelectors.forEach( ({ prefix, form, searchName, categoryName, typeName, applyBtn, searchBtn, }) => { if (!form) return; const category = urlParams.get("category"); const blogType = urlParams.get("blog_type"); const search = urlParams.get("search"); if (category) { const categoryRadio = form.querySelector( `input[name="${categoryName}"][value="${category}"]` ); if (categoryRadio) categoryRadio.checked = true; } if (blogType) { const typeRadio = form.querySelector( `input[name="${typeName}"][value="${blogType}"]` ); if (typeRadio) typeRadio.checked = true; } if (search) { const searchBoxElement = form.querySelector( `input[name="${searchName}"]` ); if (searchBoxElement) searchBoxElement.value = search; } if (prefix === "mobile") { form .querySelectorAll( `input[name="${categoryName}"], input[name="${typeName}"]` ) .forEach((radio) => { radio.addEventListener("change", () => { tempMobileFilters.category = form.querySelector( `input[name="${categoryName}"]:checked` )?.value || ""; tempMobileFilters.blogType = form.querySelector(`input[name="${typeName}"]:checked`) ?.value || ""; }); }); const searchBoxElement = form.querySelector( `input[name="${searchName}"]` ); if (searchBoxElement) { searchBoxElement.addEventListener("keydown", (event) => { if (event.key === "Enter") { event.preventDefault(); applySearch("mobile"); } }); } document .getElementById(applyBtn) ?.addEventListener("click", () => { applyFilters("mobile"); }); document .getElementById(searchBtn) ?.addEventListener("click", () => { applySearch("mobile"); }); } else { form .querySelectorAll( `input[name="${categoryName}"], input[name="${typeName}"]` ) .forEach((radio) => { radio.addEventListener("change", () => { applyFilters("desktop"); }); }); const searchBoxElement = form.querySelector( `input[name="${searchName}"]` ); if (searchBoxElement) { searchBoxElement.addEventListener("keydown", (event) => { if (event.key === "Enter") { event.preventDefault(); applySearch("desktop"); } }); } document .getElementById(searchBtn) ?.addEventListener("click", () => { applySearch("desktop"); }); } } ); });
```
--------------------------------
### Render Blog Posts with Images and Links
Source: https://github.com/thanush-babu-fleet/fs-apos-starter-kit-forked/blob/main/modules/review-page/views/index.html
Iterates through posts, extracts image URLs and alt text, and displays post titles, descriptions, and links. Handles cases with and without featured images.
```html
{% for post in data.pieces %} {% set image = apos.image.first(post.featuredImage) %} {% if image %} {% set imageUrl = apos.attachment.url(image) %} {% else %} {% set imageUrl = 'https://picsum.photos/200' %} {% endif %} {% set altText = '' %} {% if post.featuredImage and post.featuredImage.items and post.featuredImage.items\[0\] and post.featuredImage.items\[0\]\[\'_image\] and post.featuredImage.items\[0\]\[\'_image\]\[0\] and post.featuredImage.items\[0\]\[\'_image\]\[0\]\.alt %} {% set altText = post.featuredImage.items\[0\]\[\'_image\]\[0\]\.alt %} {% elif image and image.\_alt %} {% set altText = image.\_alt %} {% endif %}
[ 
###### {{ post.title }}
{{ post.description }}
]({% if post.externalUrl %} {{ post.externalUrl }} {% else %} {{ post._url }} {% endif %})
[
{{post.duration}}
]({% if post.externalUrl %} {{ post.externalUrl }} {% else %} {{ post._url }} {% endif %})
Link Copied!
{% endfor %}
```
--------------------------------
### Initialize UI Event Listeners
Source: https://github.com/thanush-babu-fleet/fs-apos-starter-kit-forked/blob/main/modules/blog-page/views/index.html
Sets up event listeners for filter toggles, search box interactions, and overlay closing when the DOM is fully loaded. Ensures UI elements are interactive.
```javascript
document.addEventListener("DOMContentLoaded", () => {
const filterHamburger = document.getElementById("filterHamburger");
const filterOverlay = document.getElementById("filterOverlay");
const closeFilter = document.getElementById("closeFilter");
const searchBoxMobile = document.getElementById("searchBoxMobile");
// Open filter overlay
if (filterHamburger && filterOverlay) {
filterHamburger.addEventListener("click", () => {
filterOverlay.classList.remove("hidden");
});
}
// Close filter overlay
if (closeFilter && filterOverlay) {
closeFilter.addEventListener("click", () => {
filterOverlay.classList.add("hidden");
});
}
// Close overlay when clicking outside
if (filterOverlay) {
filterOverlay.addEventListener("click", (event) => {
if (event.target === filterOverlay) {
filterOverlay.classList.add("hidden");
}
});
}
// Expand search box to w-full and hide filterHamburger when focused
if (searchBoxMobile && filterHamburger) {
searchBoxMobile.addEventListener("focus", () => {
searchBoxMobile.classList.add("w-full");
filterHamburger.style.display = "none"; // Hide the filter button
});
// Ensure search box remains expanded and restore filterHamburger only if empty
searchBoxMobile.addEventListener("blur", () => {
searchBoxMobile.classList.add("w-full"); // Keep it expanded
if (!searchBoxMobile.value.trim()) {
filterHamburger.style.display = "flex"; // Restore filter button if input is empty
}
});
// Handle Enter key for searching
searchBoxMobile.addEventListener("keydown", (event) => {
if (event.key === "Enter") {
event.preventDefault();
applySearch("mobile");
}
});
}
});
```
--------------------------------
### Audit Slider Initialization with Slick
Source: https://github.com/thanush-babu-fleet/fs-apos-starter-kit-forked/blob/main/modules/content-widget-modules/audit-slider-widget/views/widget.html
Initializes the Slick slider for audit items. Ensure jQuery and Slick Slider library are loaded before this script. Configures autoplay, speed, and disables navigation arrows and dots.
```javascript
document.addEventListener('DOMContentLoaded', function() { // Check if jQuery and Slick are available if (typeof jQuery === 'undefined' || typeof jQuery.fn.slick === 'undefined') { console.warn('Slick slider is not available. Audit slider requires jQuery and Slick slider library.'); return; } const autoplaySpeed = 3000; jQuery('.audit-slider').slick({ accessibility: true, dots: false, arrows: false, autoplay: true, autoplaySpeed: autoplaySpeed, speed: 600, }); const slides = document.querySelectorAll('.slide'); slides.forEach((slide) => { const progressBar = slide.querySelector('.progress-fill'); if (progressBar) { progressBar.style.animationDuration =
`${autoplaySpeed}ms
`; } }); jQuery('.audit-slider').on('beforeChange', function (event, slick, currentSlide, nextSlide) { const progressBars = document.querySelectorAll('.progress-fill'); // Batch DOM operations using requestAnimationFrame to avoid forced reflow requestAnimationFrame(() => { progressBars.forEach((bar) => { bar.style.animation = 'none'; }); requestAnimationFrame(() => { progressBars.forEach((bar) => { bar.style.animation =
`progress-bar-fill ${autoplaySpeed}ms linear`
; }); }); }); }); jQuery('.audit-slider').on('afterChange', function () { const activeProgressBar = document.querySelector('.slick-active .progress-fill'); if (activeProgressBar) { activeProgressBar.style.animation =
`progress-bar-fill ${autoplaySpeed}ms linear forwards`
; } }); });
```
--------------------------------
### Render Button Widget Template
Source: https://github.com/thanush-babu-fleet/fs-apos-starter-kit-forked/blob/main/modules/content-widget-modules/button-strip-widget/views/widget.html
Uses a loop to iterate through button data and renders each via a fragment. Requires the button.html fragment to be imported.
```nunjucks
{% import "button.html" as buttonFrag %} {% set spacing = data.widget.buttonSpacing or 16 %}
{% for button in data.widget.buttons %} {% render buttonFrag.render(button) %} {% endfor %}
```
--------------------------------
### Load Environment Variables with dotenv
Source: https://github.com/thanush-babu-fleet/fs-apos-starter-kit-forked/blob/main/README.md
Use 'dotenv' to load environment variables from a .env file. Import it in your main app.js file.
```javascript
require('dotenv').config();
```
```javascript
const port = process.env.PORT || 3000;
const dbHost = process.env.DB_HOST || 'localhost';
const dbUser = process.env.DB_USER;
const dbPassword = process.env.DB_PASSWORD;
```
--------------------------------
### Render Widget Images with Conditional Links
Source: https://github.com/thanush-babu-fleet/fs-apos-starter-kit-forked/blob/main/modules/content-widget-modules/enterprise-widget/views/widget.html
Uses the ui.html macro to display images and conditionally wraps them in links using the apos.helper.linkPath helper.
```nunjucks
{% import "ui.html" as ui %}
{{ data.widget.title }}
{% for image in data.widget.images %} {% if image.isRedirectToAnotherPage %} [{{ ui.image(image, "w-36", 'lazy') }}]({{ apos.helper.linkPath(image) }}) {% else %}
{{ ui.image(image, "w-24") }}
{% endif %} {% endfor %}
```
--------------------------------
### Initialize Slick Slider with Progress Bar
Source: https://github.com/thanush-babu-fleet/fs-apos-starter-kit-forked/blob/main/modules/content-widget-modules/progressBar-slider-widget/views/widget.html
Initializes a Slick slider with autoplay and custom progress bar animation. It checks for jQuery and Slick slider availability before proceeding. The autoplay speed is configurable.
```JavaScript
document.addEventListener('DOMContentLoaded', function() {
// Check if jQuery and Slick are available
if (typeof jQuery === 'undefined' || typeof jQuery.fn.slick === 'undefined') {
console.warn('Slick slider is not available. Progress bar slider requires jQuery and Slick slider library.');
return;
}
const autoplaySpeed = 3000;
jQuery('.progress_slider').slick({
accessibility: true,
dots: false,
arrows: false,
autoplay: true,
autoplaySpeed: autoplaySpeed,
speed: 600,
accessibility: true
});
const slides = document.querySelectorAll('.report_slider .auditContent');
const uniqueContent = new Set();
slides.forEach((slide) => {
const auditCount = slide.querySelector('.stat-number').textContent.trim();
const description = slide.querySelector('.stat-description').textContent.trim();
uniqueContent.add(`Audit count: ${auditCount}, Description: ${description}.`);
});
const allSlidesContent = Array.from(uniqueContent).join(' ');
slides.forEach((slide) => {
slide.setAttribute('aria-label', allSlidesContent);
});
jQuery('.progress_slider').on('beforeChange', function (event, slick, currentSlide, nextSlide) {
const progressBars = document.querySelectorAll('.progress-fill');
// Batch DOM operations using requestAnimationFrame to avoid forced reflow
requestAnimationFrame(() => {
progressBars.forEach((bar) => {
bar.style.animation = 'none';
});
requestAnimationFrame(() => {
progressBars.forEach((bar) => {
bar.style.animation = `progress-bar-fill ${autoplaySpeed}ms linear`;
});
});
});
});
jQuery('.progress_slider').on('afterChange', function () {
const activeProgressBar = document.querySelector('.slick-active .progress-fill');
if (activeProgressBar) {
activeProgressBar.style.animation = `progress-bar-fill ${autoplaySpeed}ms linear forwards`;
}
});
});
```
--------------------------------
### Initialize Clear Button States
Source: https://github.com/thanush-babu-fleet/fs-apos-starter-kit-forked/blob/main/modules/blog-page/views/index.html
Ensures the correct visibility state for clear search buttons on both mobile and desktop interfaces when the DOM is loaded. Call this after elements are ready.
```javascript
document.addEventListener("DOMContentLoaded", () => {
toggleClearButton("mobile");
toggleClearButton("desktop");
});
```
--------------------------------
### Render Widget Menu Items
Source: https://github.com/thanush-babu-fleet/fs-apos-starter-kit-forked/blob/main/modules/pieces-modules/product-widget/views/widget.html
Iterates through menu items in a widget and displays their title, price, and description. Requires the widget data object to contain a _menuItems array and a currencySymbol property.
```nunjucks
{% set splitClass = 'split' if data.widget.style === 'split'%}
{% area data.widget, 'headingIntro' %}
{% for item in data.widget._menuItems %}
{{ item.title }} {{ data.widget.currencySybmol }}{{ item.price }}
{{ item.description }}
{% endfor %}
```
--------------------------------
### Render a Button with Optional Icon
Source: https://github.com/thanush-babu-fleet/fs-apos-starter-kit-forked/blob/main/views/ui.html
Use this macro to generate a link that functions as a button. It supports adding an icon to the left or right of the button label. Ensure the `button` object has `linkLabel` and `linkURL` properties.
```nunjucks
{# Button #} {% macro button(button, className = "", iconName = "", iconPosition = "l") %}
[{% if iconName %} {% if iconPosition === 'l' %} {% endif %} {% endif %} {{ button.linkLabel }} {% if iconName %} {% if iconPosition === 'r' %} {% endif %} {% endif %}](button.linkURL "{{ button.linkLabel }}")
{% endmacro %}
```
--------------------------------
### Responsive Pagination Fragment (Nunjucks)
Source: https://github.com/thanush-babu-fleet/fs-apos-starter-kit-forked/blob/main/views/fragments/paginator.html
This Nunjucks fragment generates pagination links that adapt to different screen sizes (XS, Mobile, Desktop). It requires a 'data' object with 'currentPage' and 'totalPages'.
```Nunjucks
{% fragment pagination(data) %} .xs-only { display: block; } .mobile-only { display: none; } .desktop-only { display: none; } @media (min-width: 375px) { .xs-only { display: none; } .mobile-only { display: block; } } @media (min-width: 1024px) { .mobile-only { display: none; } .desktop-only { display: block; } } .prev-short { display: block; } .prev-full { display: none; } @media (min-width: 640px) { .prev-short { display: none; } .prev-full { display: block; } } function goToPage(page) { const url = new URL(window.location.href); url.searchParams.set('page', page); window.location.href = url.toString(); } {% endfragment %}
```
--------------------------------
### Configure Global Site Settings - JavaScript
Source: https://context7.com/thanush-babu-fleet/fs-apos-starter-kit-forked/llms.txt
Defines site-wide settings such as branding, header/footer layouts, navigation, social media links, and custom code injection. This configuration is managed within the global module.
```javascript
// modules/@apostrophecms/global/index.js (simplified)
const linkSchema = require('../../../lib/linkSchema');
const buttonSchema = require('../../../lib/buttonSchema');
const backgroundSchema = require('../../../lib/backgroundSchema');
module.exports = {
fields: {
add: {
// Brand Settings
logo: { label: 'Logo', type: 'area', options: { max: 1, widgets: { '@apostrophecms/image': {} } } },
title: { type: 'string', label: 'Website Title', required: true },
primaryColor: { type: 'color', label: 'Primary Color', def: '#000000' },
secondaryColor: { type: 'color', label: 'Secondary Color', def: '#2850EB' },
headingFontFamily: { type: 'string', label: 'Heading Font Family', def: 'Lato' },
bodyFontFamily: { type: 'string', label: 'Body Font Family', def: 'Merriweather' },
// Header Settings
headerLayout: {
type: 'select',
label: 'Header Layout',
def: 'default',
choices: [
{ label: 'Default Header', value: 'default' },
{ label: 'Header Layout 1', value: 'header-layout-1' },
{ label: 'Header Layout 2', value: 'header-layout-2' }
]
},
headerNav: {
label: 'Header Navigation Items',
type: 'array',
titleField: 'linkText',
fields: { add: { ...linkSchema } }
},
headerBtns: {
label: 'Header Button/s',
type: 'array',
titleField: 'linkText',
fields: { add: { ...buttonSchema.button } }
},
headerBackgroundColor: { ...backgroundSchema.backgroundColor },
headerTextColor: { ...backgroundSchema.textColor },
// Footer Settings
footerLayout: {
type: 'select',
label: 'Footer Layout',
def: 'default',
choices: [
{ label: 'Default Footer', value: 'default' },
{ label: 'Footer Layout 1', value: 'footer-layout-1' },
{ label: 'Footer Layout 2', value: 'footer-layout-2' }
]
},
footerNav: {
label: 'Footer Navigation Items',
type: 'array',
titleField: 'linkText',
fields: { add: { ...linkSchema } }
},
social: {
label: 'Social Media Accounts',
type: 'array',
limit: 5,
inline: true,
fields: {
add: {
link: { type: 'url', label: 'Social link', required: true },
icon: {
type: 'select',
required: true,
choices: [
{ label: 'Instagram', value: 'instagram' },
{ label: 'Facebook', value: 'facebook' },
{ label: 'Twitter', value: 'twitter' },
{ label: 'LinkedIn', value: 'linkedin' }
]
}
}
}
},
// Custom Code
customJavaScriptHead: { type: 'codeEditor', label: 'Custom JavaScript (Head)' },
customJavaScript: { type: 'codeEditor', label: 'Custom JavaScript (Body)' },
customCSS: { type: 'codeEditor', label: 'Custom CSS' },
// Payment Modal (Razorpay)
razorpayMode: {
type: 'select',
label: 'Razorpay Integration Mode',
def: 'button',
choices: [
{ label: 'Hosted Payment Button', value: 'button' },
{ label: 'Custom Checkout', value: 'checkout' }
]
},
razorpayKeyId: { type: 'string', label: 'Razorpay Key ID', if: { razorpayMode: 'checkout' } },
razorpayAmount: { type: 'integer', label: 'Amount (in paise)', if: { razorpayMode: 'checkout' } }
},
group: {
brand: { label: 'Brand', fields: ['title', 'logo', 'primaryColor', 'secondaryColor', 'headingFontFamily', 'bodyFontFamily'] },
header: { label: 'Header', fields: ['headerLayout', 'headerNav', 'headerBtns', 'headerBackgroundColor', 'headerTextColor'] },
footer: { label: 'Footer', fields: ['footerLayout', 'footerNav', 'social'] },
custom: { label: 'Custom Code', fields: ['customCSS', 'customJavaScriptHead', 'customJavaScript'] },
paymentModal: { label: 'Payment Modal', fields: ['razorpayMode', 'razorpayKeyId', 'razorpayAmount'] }
}
}
};
```
--------------------------------
### Render Specialization Cards with Links
Source: https://github.com/thanush-babu-fleet/fs-apos-starter-kit-forked/blob/main/modules/content-widget-modules/specialization-widget/views/widget.html
This Nunjucks template code iterates through a list of specializations, rendering each as a card. If a card has a URL, it's presented as a link; otherwise, it's displayed without a link. It utilizes a UI macro for image rendering.
```Nunjucks
{% import "ui.html" as ui %}
{% for card in data.widget.specializations %} {% if card.url %}[
{{ ui.image(card.logo, '', 'lazy') }}
{{ card.title }}
{% area card, 'description' %}
]({{ card.url }}){% else %}
{{ ui.image(card.logo, '', 'lazy') }}
{{ card.title }}
{% area card, 'description' %}
{% endif %} {% endfor %}
```
--------------------------------
### Render Image Widget with Nunjucks
Source: https://github.com/thanush-babu-fleet/fs-apos-starter-kit-forked/blob/main/modules/content-widget-modules/image-widget/views/widget.html
This Nunjucks template code handles rendering an image widget. It includes logic to display a placeholder if an image is not present, and to render the actual image with specified sizes and alt text. It also allows for custom CSS classes and dimensions.
```Nunjucks
{% if data.widget.aposPlaceholder and data.manager.options.placeholderUrl %}  {% else %} {% set className = data.options.className or data.manager.options.className %} {% set dimensionAttrs = data.options.dimensionAttrs or data.manager.options.dimensionAttrs %} {% set loadingType = data.options.loadingType or data.manager.options.loadingType %} {% set size = data.options.size or data.manager.options.size or 'full' %} {% set attachment = apos.image.first(data.widget._image) %} {% if attachment %}  }}) {% endif %} {% endif %} {% area data.widget, 'content' %}
```
--------------------------------
### Configure Geocoder Provider Settings
Source: https://context7.com/thanush-babu-fleet/fs-apos-starter-kit-forked/llms.txt
Sets up the geocoder provider options within the widget modules configuration file.
```javascript
// modules/content-widget-modules/modules.js
module.exports = {
'accordion-widget': {},
'button-widget': {},
'columns-widget': {},
'hero-banner-widget': {},
'image-gallery-widget': {},
'map-widget': {
options: {
geocoderSettings: {
// node-geocoder options: https://www.npmjs.com/package/node-geocoder
provider: 'mapbox',
apiKey: process.env.GEOCODER_API_KEY,
formatter: null,
minConfidence: 0.5,
limit: 1
}
}
},
'pricing-widget': {},
'testimonial-widget': {},
'card-widget': {},
// ... more widgets
};
```