diff --git a/__tests__/api/robots.test.js b/__tests__/api/robots.test.js index 5d06f70105..96e0dde47f 100644 --- a/__tests__/api/robots.test.js +++ b/__tests__/api/robots.test.js @@ -22,7 +22,7 @@ describe("robots api", () => { }); await handler(req, res); expect(res._getData()).toBe( - "User-agent: *\nDisallow: /api\nDisallow: /projects/*\nDisallow: /notsupported.js\nDisallow: /cdcp-apply\nDisallow: /rsdc-demander\n" + "User-agent: *\nDisallow: /api\nDisallow: /projects/*\nDisallow: /notsupported.js\nDisallow: /rsdc-demander\n" ); }); }); diff --git a/components/fragment_renderer/FragmentRender.js b/components/fragment_renderer/FragmentRender.js index e04d00e8a1..8b98b3704b 100644 --- a/components/fragment_renderer/FragmentRender.js +++ b/components/fragment_renderer/FragmentRender.js @@ -1,4 +1,7 @@ +// Import UUID generator for unique React keys import { v4 as uuid } from "uuid"; + +// Import all fragment-specific components import TextWithImage from "./fragment_components/TextWithImage"; import TextContent from "./fragment_components/TextContent"; import Button from "./fragment_components/Button"; @@ -7,17 +10,33 @@ import QuoteVerticalLineContent from "./fragment_components/QuoteVerticalLineCon import ImageWithCollapse from "./fragment_components/ImageWithCollapse"; import TextRender from "../text_node_renderer/TextRender"; +/** + * Map of fragment types to their corresponding React components + * Each key represents a specific AEM fragment model type + * Values are the React components responsible for rendering that fragment type + */ const FRAGMENTS = { - "SCLabs-Comp-Content-Image-v1": TextWithImage, - "SCLabs-Comp-Content-v1": QuoteVerticalLineContent, - "SCLabs-Content-v1": TextContent, - "SCLabs-Button-v1": Button, - "SCLabs-Feature-v1": ArticleCTA, - "SCLabs-Image-v1": ImageWithCollapse, + "SCLabs-Comp-Content-Image-v1": TextWithImage, // Combined text and image layouts + "SCLabs-Comp-Content-v1": QuoteVerticalLineContent, // Quote blocks with vertical line styling + "SCLabs-Content-v1": TextContent, // Generic text content blocks + "SCLabs-Button-v1": Button, // Interactive button elements + "SCLabs-Feature-v1": ArticleCTA, // Call-to-action article features + "SCLabs-Image-v1": ImageWithCollapse, // Images with collapsible details }; +/** + * Maps AEM fragment data to component-specific props + * Handles bilingual content and different layout variations + * + * @param {Object} fragmentData - Raw fragment data from AEM + * @param {string} fragmentName - Type identifier for the fragment + * @param {string} locale - Current language locale (en/fr) + * @param {boolean} excludeH1 - Whether to exclude h1 headers + * @returns {Object} Props object formatted for the specific component + */ const mapFragmentsToProps = (fragmentData, fragmentName, locale, excludeH1) => { switch (fragmentName) { + // Article CTA Fragment handling case "SCLabs-Feature-v1": return { heading: @@ -44,8 +63,10 @@ const mapFragmentsToProps = (fragmentData, fragmentName, locale, excludeH1) => { }, }; + // Text with Image Fragment handling case "SCLabs-Comp-Content-Image-v1": switch (fragmentData.scLabLayout) { + // Default layout configuration case "default": return { src: @@ -66,6 +87,7 @@ const mapFragmentsToProps = (fragmentData, fragmentName, locale, excludeH1) => { layout: fragmentData.scLabLayout, excludeH1: excludeH1, }; + // Vertical line layout configuration case "image-vertical-line-content": return { src: @@ -106,6 +128,7 @@ const mapFragmentsToProps = (fragmentData, fragmentName, locale, excludeH1) => { break; } + // Quote with vertical line styling case "SCLabs-Comp-Content-v1": return { quoteText: @@ -118,6 +141,7 @@ const mapFragmentsToProps = (fragmentData, fragmentName, locale, excludeH1) => { : fragmentData.scLabContent[1].scContentFr.json, }; + // Generic text content case "SCLabs-Content-v1": return { data: @@ -126,6 +150,7 @@ const mapFragmentsToProps = (fragmentData, fragmentName, locale, excludeH1) => { : fragmentData.scContentFr.json, }; + // Interactive button element case "SCLabs-Button-v1": return { id: fragmentData.scId, @@ -137,6 +162,7 @@ const mapFragmentsToProps = (fragmentData, fragmentName, locale, excludeH1) => { text: locale === "en" ? fragmentData.scTitleEn : fragmentData.scTitleFr, }; + // Image with collapsible details case "SCLabs-Image-v1": return { id: fragmentData.scId, @@ -182,14 +208,28 @@ const mapFragmentsToProps = (fragmentData, fragmentName, locale, excludeH1) => { } }; +/** + * FragmentRender Component + * Main component responsible for rendering AEM content fragments + * Handles the transformation of AEM data into React components + * + * @param {Object} props - Component props + * @param {Array} props.fragments - Array of AEM content fragments + * @param {string} props.locale - Current language locale + * @param {boolean} props.excludeH1 - Flag to exclude h1 headers + */ export default function FragmentRender(props) { - // Create and return array of elements corresponding to - // fragments + // Map each fragment to its corresponding React component const pageFragments = props.fragments.map((fragmentData) => { + // Get the appropriate component based on the fragment model type const Fragment = FRAGMENTS[fragmentData?._model.title]; + + // Skip if no matching component is found if (!Fragment) { return; } + + // Render the component with mapped props return ( + {/* Handle nodes differently based on whether they have child content */} {content && content.length ? ( + // For nodes with children: render the node and recursively render its children {content.map((node) => ( ))} ) : ( + // For leaf nodes: render just the node itself )} diff --git a/components/text_node_renderer/TextRender.jsx b/components/text_node_renderer/TextRender.jsx index b50e8300b1..a052e1ed9c 100644 --- a/components/text_node_renderer/TextRender.jsx +++ b/components/text_node_renderer/TextRender.jsx @@ -1,16 +1,22 @@ import TextRecur from "./TextRecur"; -import { v4 as uuid } from "uuid"; +import { v4 as uuid } from "uuid"; // Import UUID generator for unique keys +/** + * TextRender Component + * Top-level component that initiates the recursive rendering of text content + * Processes an array of content nodes from AEM and renders them recursively + */ export default function TextRender(props) { return ( <> + {/* Map through each content node and render it recursively */} {props.data.map((node, index) => { return ( ); })} diff --git a/next.config.js b/next.config.js index a04168ed3e..2acab5e416 100644 --- a/next.config.js +++ b/next.config.js @@ -1,18 +1,25 @@ +// Import i18n configuration for internationalization const { i18n } = require("./next-i18next.config"); +// URL rewrite rules for French/English bilingual routes +// Maps French routes to their English counterparts const REWRITES = [ + // API endpoint rewrites { source: "/robots.txt", destination: "/api/robots", }, + // Main page rewrites { source: "/accueil", destination: "/home", }, + // Project page rewrites { source: "/projets", destination: "/projects", }, + // Individual project rewrites with their slugs { source: "/projets/estimateur-prestations-sv", destination: "/projects/oas-benefits-estimator", @@ -69,52 +76,51 @@ const REWRITES = [ source: "/projets/transformer-assurance-emploi-peuples-autochtones/:slug", destination: "/projects/transforming-ei-indigenous-peoples/:slug", }, - { - source: "/rsdc-demander", - destination: "/cdcp-apply" - }, + // Updates page rewrite { source: "/mises-a-jour", destination: "/updates" } ]; +// Security headers configuration for enhanced protection securityHeaders = [ - //Enables DNS prefetching, which reduces latency when a user clicks a link + // DNS prefetching for performance optimization { key: "X-DNS-Prefetch-Control", value: "on", }, - //Restrict our page from being rendered within a frame + // Prevent site from being embedded in iframes (clickjacking protection) { key: "X-Frame-Options", value: "DENY", }, - //Restrict browser features + // Restrict access to browser features { key: "Permissions-Policy", value: "camera=(), microphone=(), geolocation=(), interest-cohort=()", }, - // Only ever use HTTPS + // Force HTTPS connection { key: "Strict-Transport-Security", value: "max-age=31536000; includeSubDomains; preload", }, - // Disables use of inline javascript in XSS attacks + // XSS attack protection { key: "X-XSS-Protection", value: "1; mode=block", }, - // Prevents the browser from attempting to guess the type of content + // Prevent MIME type sniffing { key: "X-Content-Type-Options", value: "nosniff", }, - // Only allow secure origin to be delivered over HTTPS + // Control how much referrer information should be included { key: "Referrer-Policy", value: "same-origin", }, + // Content Security Policy configuration { key: "Content-Security-Policy", value: `default-src 'self' dts-stn.com *.dts-stn.com *.adobe.com https://assets.adobedtm.com *.omniture.com *.2o7.net; frame-ancestors 'self'; base-uri 'self'; form-action 'self'; connect-src 'self' *.adobe.com https://assets.adobedtm.com *.demdex.net *.omtrdc.net cm.everesttech.net; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com data:; img-src 'self' data: webpack: *.omtrdc.net *.demdex.net cm.everesttech.net https://assets.adobedtm.com https://www.canada.ca; font-src 'self' https://fonts.googleapis.com https://fonts.gstatic.com data:; frame-src 'self' *.adobe.com https://assets.adobedtm.com *.demdex.net; script-src 'self' 'unsafe-inline' *.adobe.com *.adobedtm.com *.omniture.com *.2o7.net https://*.demdex.net https://cm.everesttech.net ${ @@ -127,12 +133,17 @@ securityHeaders = [ }, ]; +// Main Next.js configuration export module.exports = { + // Build output configuration output: "standalone", swcMinify: true, + // Internationalization settings i18n, + + // Webpack configuration webpack: (config, { buildId, dev, isServer, defaultLoaders, webpack }) => { - //GraphQL loader for .graphql files + // Add GraphQL file loader config.module.rules.push({ test: /\.(graphql|gql)$/, exclude: /node_modules/, @@ -141,6 +152,8 @@ module.exports = { return config; }, + + // Image optimization configuration images: { remotePatterns: [ { @@ -150,24 +163,30 @@ module.exports = { }, ] }, + + // Disable the "Powered by Next.js" header poweredByHeader: false, + + // Custom header configuration async headers() { return [ { - // Apply these headers to all routes in your application. + // Apply security headers to all routes source: "/:path*{/}?", headers: securityHeaders, }, ]; }, + // URL rewrite configuration async rewrites() { return REWRITES; }, + + // Redirect configuration async redirects() { return [ - // Note: pathmatch is removed because subpaths are in different languages, so subpath match doesn't work anymore. - // Redirect to home page when user set prefered language + // Redirect IE (Trident) users to not supported page { source: "/", has: [ @@ -180,6 +199,8 @@ module.exports = { destination: "/notsupported", permanent: false, }, + // Redirect all other routes for IE users to not supported page + // except the not supported page itself { source: "/:slug((?!notsupported$).*)", has: [ @@ -194,4 +215,4 @@ module.exports = { }, ]; }, -}; +}; \ No newline at end of file diff --git a/pages/_app.js b/pages/_app.js index acf3657ae6..1d47697030 100644 --- a/pages/_app.js +++ b/pages/_app.js @@ -9,6 +9,7 @@ import { Noto_Sans, Lato } from "next/font/google"; config.autoAddCss = false; +// Setup fonts for pre-loading const notoSans = Noto_Sans({ subsets: ["latin"], weight: ["300", "400", "700", "900"], diff --git a/pages/api/robots.js b/pages/api/robots.js index f26f61c2ee..159bbf87e7 100644 --- a/pages/api/robots.js +++ b/pages/api/robots.js @@ -10,7 +10,6 @@ export default async function handler(req, res) { res.write("Disallow: /api\n"); res.write("Disallow: /projects/*\n"); res.write("Disallow: /notsupported.js\n"); - res.write("Disallow: /cdcp-apply\n"); res.write("Disallow: /rsdc-demander\n"); } else { res.write("User-agent: *\n"); diff --git a/pages/cdcp-apply.js b/pages/cdcp-apply.js deleted file mode 100644 index c9bd327796..0000000000 --- a/pages/cdcp-apply.js +++ /dev/null @@ -1,143 +0,0 @@ -import Head from "next/head"; -import { useTranslation } from "next-i18next"; -import { serverSideTranslations } from "next-i18next/serverSideTranslations"; -import { Layout } from "../components/organisms/Layout"; -import { ActionButton } from "../components/atoms/ActionButton"; -import { createBreadcrumbs } from "../lib/utils/createBreadcrumbs"; -import { useEffect } from "react"; - -export default function CDCPLanding(props) { - const { t } = useTranslation("common"); - - const breadCrumbs = [ - { - scTitleEn: "Benefits", - scTitleFr: "Prestations", - scPageNameEn: "https://www.canada.ca/en/services/benefits.html", - scPageNameFr: "https://www.canada.ca/fr/services/prestations.html", - }, - { - scTitleEn: "Dental coverage", - scTitleFr: "Couverture dentaire", - scPageNameEn: "https://www.canada.ca/en/services/benefits/dental.html", - scPageNameFr: - "https://www.canada.ca/fr/services/prestations/dentaire.html", - }, - { - scTitleEn: "Canadian Dental Care Plan", - scTitleFr: "Régime canadien de soins dentaires", - scPageNameEn: - "https://www.canada.ca/en/services/benefits/dental/dental-care-plan.html", - scPageNameFr: - "https://www.canada.ca/fr/services/prestations/dentaire/regime-soins-dentaires.html", - }, - ]; - - useEffect(() => { - if (props.adobeAnalyticsUrl) { - window.adobeDataLayer = window.adobeDataLayer || []; - window.adobeDataLayer.push({ event: "pageLoad" }); - } - }, []); - - return ( - - - {/* Primary HTML Meta Tags */} - {t("cdcp.secondaryHeading")} - - {/* DCMI Meta Tags */} - - - - - - - - {/* Open Graph / Facebook */} - - - -
-
-
-

- {t("cdcp.secondaryHeading")} -

-

- {t("cdcp.primaryHeading")} -

-
-
-

{t("cdcp.toCompleteApplication")}

-
    -
  • -

    {t("cdcp.listItems.item1")}

    -
  • -
  • -

    {t("cdcp.listItems.item2")}

    -
  • -
  • -

    {t("cdcp.listItems.item3")}

    -
  • -
  • -

    {t("cdcp.listItems.item4")}

    -
  • -
  • -

    {t("cdcp.listItems.item5")}

    -
  • -
-

{t("cdcp.headingH2")}

-

{t("cdcp.clickButtonToApply")}

-
- -
-
-
-
-
- ); -} - -export const getStaticProps = async ({ locale }) => { - return { - props: { - locale: locale, - adobeAnalyticsUrl: process.env.ADOBE_ANALYTICS_URL ?? null, - ...(await serverSideTranslations(locale, ["common"])), - }, - }; -}; diff --git a/pages/home.js b/pages/home.js index ecc55f6502..11cdb29396 100644 --- a/pages/home.js +++ b/pages/home.js @@ -1,8 +1,11 @@ +// Core Next.js imports for page head management and i18n (internationalization) import Head from "next/head"; import { serverSideTranslations } from "next-i18next/serverSideTranslations"; +// Custom components imports - using atomic design principles (atoms, molecules, organisms) import { Layout } from "../components/organisms/Layout"; import { useEffect } from "react"; import Card from "../components/molecules/Card"; +// Service for interacting with Adobe Experience Manager (AEM) import aemServiceInstance from "../services/aemServiceInstance"; import { ContextualAlert } from "../components/molecules/ContextualAlert"; import { Link as LinkWrapper } from "../components/atoms/Link"; @@ -12,12 +15,20 @@ import FragmentRender from "../components/fragment_renderer/FragmentRender"; import { sortUpdatesByDate } from "../lib/utils/sortUpdatesByDate"; import { SurveyCTA } from "../components/molecules/SurveyCTA"; +/** + * Home Page Component for Service Canada Labs + * Renders the main landing page with projects, updates, and survey CTA + * Supports bilingual content (English/French) based on locale + */ export default function Home(props) { - const pageData = props.pageData?.item; - const experimentsData = props.experimentsData; - const dictionary = props.dictionary; - const updatesData = props.updatesData; + // Extract content data from props + const pageData = props.pageData?.item; // Core page content from AEM + const experimentsData = props.experimentsData; // List of all projects/experiments + const dictionary = props.dictionary; // Translation dictionary for UI elements + const updatesData = props.updatesData; // Recent updates for projects + // Filter to show only currently active projects + // Projects must have a specific AEM status code to be considered "current" const currentProjects = experimentsData.filter((project) => { return ( project.scLabProjectStatus[0] === @@ -25,8 +36,14 @@ export default function Home(props) { ); }); - const sortedProjects = (objects) => { - // Order to sort the projects + /** + * Sorts projects according to a predefined order and returns top 3 + * Order is manually maintained until content strategy is finalized + * @param {Array} objects - Array of project objects to sort + * @returns {Array} - Top 3 sorted projects + */ + const sortProjects = (objects) => { + // Predefined display order for projects const sortOrder = [ "Transforming EI with Indigenous peoples", "Benefits Finder", @@ -36,25 +53,29 @@ export default function Home(props) { "Old Age Security Benefits Estimator", "Benefits Navigator", ]; - // Create a lookup for efficient ordering + // Create lookup object for efficient sorting const titleOrder = {}; for (let i = 0; i < sortOrder.length; i++) { titleOrder[sortOrder[i]] = i; } - // Sort the objects based on the lookup + // Sort projects based on predefined order const sorted = objects.sort((a, b) => { return titleOrder[a.scTitleEn] - titleOrder[b.scTitleEn]; }); - // Trim to first 3 projects - return sorted.slice(0, 3); + return sorted.slice(0, 3); // Return only top 3 projects }; - const displayCurrentProjects = sortedProjects(currentProjects).map( + /** + * Maps current projects to Card components with bilingual support + * Handles image sources, alt text, and "New update" tags + */ + const displayCurrentProjects = sortProjects(currentProjects).map( (project) => (
  • { if (props.adobeAnalyticsUrl) { window.adobeDataLayer = window.adobeDataLayer || []; @@ -108,11 +132,13 @@ export default function Home(props) { <> + {/* Page head component for meta tags */} {/* Primary HTML Meta Tags */} @@ -258,13 +284,17 @@ export default function Home(props) { } /> </Head> + + {/* Main Page Content */} <div id="pageMainTitle" className="mt-24"> <FragmentRender locale={props.locale} fragments={[pageData.scFragments[0]]} /> </div> + <div className="layout-container"> + {/* Survey Call-to-Action Section */} <SurveyCTA heading={ props.locale === "en" @@ -292,12 +322,16 @@ export default function Home(props) { : pageData.scFragments[1].scLabsButton[0].scDestinationURLFr } /> + + {/* Projects Section Title */} <h2> {props.locale === "en" ? pageData.scFragments[2].scContentEn.json[0].content[0].value : pageData.scFragments[2].scContentFr.json[0].content[0] .value}{" "} </h2> + + {/* Information Alert Section */} <div className="mb-8"> <ContextualAlert id="info-alert" @@ -310,6 +344,7 @@ export default function Home(props) { : pageData.scFragments[3].scTitleFr } message_body={ + // Conditional rendering of alert message with embedded link props.locale === "en" ? ( <> { @@ -334,6 +369,7 @@ export default function Home(props) { } </> ) : ( + // French version of alert message <> { pageData.scFragments[3].scContentFr.json[0].content[0] @@ -360,10 +396,13 @@ export default function Home(props) { } /> </div> + + {/* Project Cards Grid Section */} <div className="mb-4"> <ul className="grid lg:grid-cols-3 gap-6 list-none ml-0"> {displayCurrentProjects} </ul> + {/* "See all projects" link */} <div className="mt-6 flex justify-end"> <LinkWrapper component={Link} @@ -387,6 +426,8 @@ export default function Home(props) { </div> </div> </div> + + {/* Updates Section */} <section> <ExploreUpdates locale={props.locale} @@ -416,24 +457,33 @@ export default function Home(props) { ); } +/** + * Next.js Static Site Generation (SSG) function + * Fetches all required data at build time for static page generation + * Enables Incremental Static Regeneration (ISR) if configured + */ export const getStaticProps = async ({ locale }) => { + // Fetch main page content from AEM const { data: pageData } = await fetch( `${process.env.AEM_BASE_URL}/getSclHomeV2` ).then((res) => res.json()); + // Fetch projects/experiments data const { data: experimentsData } = await aemServiceInstance.getFragment( "projectQuery" ); + // Fetch updates data for all projects const { data: updatesData } = await fetch( `${process.env.AEM_BASE_URL}/getSclAllUpdatesV1` ).then((res) => res.json()); - // get dictionary + // Fetch translation dictionary const { data: dictionary } = await aemServiceInstance.getFragment( "dictionaryQuery" ); + // Return all props needed for page rendering return { props: { locale: locale, @@ -444,6 +494,7 @@ export const getStaticProps = async ({ locale }) => { dictionary: dictionary.dictionaryV1List.items, ...(await serverSideTranslations(locale, ["common"])), }, + // Enable ISR if configured in environment revalidate: process.env.ISR_ENABLED === "true" ? 10 : false, }; }; diff --git a/pages/projects.js b/pages/projects.js index 91232f3c52..f4253ffdd6 100644 --- a/pages/projects.js +++ b/pages/projects.js @@ -1,22 +1,39 @@ +// Import required dependencies import { serverSideTranslations } from "next-i18next/serverSideTranslations"; import { Layout } from "../components/organisms/Layout"; import { useEffect, useState } from "react"; import Card from "../components/molecules/Card"; import PageHead from "../components/fragment_renderer/PageHead"; import { MultiSelectField } from "../components/atoms/MultiSelectField"; +// Utility functions for breadcrumb generation and dictionary term lookup import { createBreadcrumbs } from "../lib/utils/createBreadcrumbs"; import { getDictionaryTerm } from "../lib/utils/getDictionaryTerm"; import { useTranslation } from "next-i18next"; +/** + * Projects Page Component + * Displays a filterable grid of all Service Canada Lab projects + * Supports bilingual content and project status filtering + */ export default function ProjectsPage(props) { - const pageData = props.pageData?.item; - const projectsData = props.projectsData; - const dictionary = props.dictionary; + // Extract data from props + const pageData = props.pageData?.item; // Page content from AEM + const projectsData = props.projectsData; // All projects data + const dictionary = props.dictionary; // Translation dictionary + // State for managing selected filter options const [selectedOptions, setSelectedOptions] = useState([]); + // Translation hook for i18n const { t } = useTranslation("common"); + /** + * Extracts unique project statuses and converts them to select options + * @param {Array} arr - Array of project objects + * @returns {Array} Array of formatted options for MultiSelectField + */ const getSelectOptionsFromProjectsData = (arr) => { + // Use Set to track unique status values const seen = new Set(); + // Reduce array to unique status entries let reducedArray = arr.reduce((acc, obj) => { if (!seen.has(obj.scLabProjectStatus[0])) { seen.add(obj.scLabProjectStatus[0]); @@ -24,9 +41,11 @@ export default function ProjectsPage(props) { } return acc; }, []); + // Format options for the MultiSelectField component let optionsArray = reducedArray.map((option) => { return { id: option.scLabProjectStatus[0], + // Translate status labels using i18n label: t(option.scLabProjectStatus[0].substring(3)), value: option.scLabProjectStatus[0], }; @@ -34,14 +53,27 @@ export default function ProjectsPage(props) { return optionsArray; }; + /** + * Filters projects based on selected status options + * @param {Array} projects - Array of all projects + * @param {Array} selectedOptions - Array of selected filter options + * @returns {Array} Filtered projects array + */ const filterProjects = (projects, selectedOptions) => { + // If no filters selected, return all projects if (selectedOptions.length === 0) return projects; + // Create Set of selected status IDs for efficient lookup const selectedIds = new Set(selectedOptions.map((option) => option.id)); + // Return projects matching selected statuses return projects.filter((project) => selectedIds.has(project.scLabProjectStatus[0]) ); }; + /** + * Maps filtered projects to Card components + * Handles bilingual content for each project card + */ const projectsCards = filterProjects(projectsData, selectedOptions).map( (project) => { return ( @@ -50,6 +82,7 @@ export default function ProjectsPage(props) { className="grid col-span-12 md:col-span-6 xl:col-span-4 list-none" > <Card + // Bilingual title handling title={ props.locale === "en" ? project.scTitleEn : project.scTitleFr } @@ -59,6 +92,7 @@ export default function ProjectsPage(props) { : project.scPageNameFr } showImage + // Bilingual image handling imgSrc={ props.locale === "en" ? project.scSocialMediaImageEn._publishUrl @@ -79,6 +113,7 @@ export default function ProjectsPage(props) { ? project.scSocialMediaImageEn.width : "" } + // Bilingual description handling description={ props.locale === "en" ? project.scDescriptionEn.json[0].content[0].value @@ -90,6 +125,7 @@ export default function ProjectsPage(props) { } ); + // Initialize Adobe Analytics useEffect(() => { if (props.adobeAnalyticsUrl) { window.adobeDataLayer = window.adobeDataLayer || []; @@ -101,16 +137,21 @@ export default function ProjectsPage(props) { <> <Layout locale={props.locale} + // Alternate language URL for language toggle langUrl={ props.locale === "en" ? pageData.scPageNameFr : pageData.scPageNameEn } dateModifiedOverride={pageData.scDateModifiedOverwrite} + // Generate breadcrumb navigation breadcrumbItems={createBreadcrumbs( pageData.scBreadcrumbParentPages, props.locale )} > + {/* Page head component for meta tags */} <PageHead locale={props.locale} pageData={pageData} /> + + {/* Hero section with background image */} <div id="pageMainTitle" className="flex flex-col justify-center content-center mt-16 h-[182px] bg-multi-blue-blue70 bg-no-repeat sm:bg-right-bottom" @@ -120,6 +161,7 @@ export default function ProjectsPage(props) { }} > <div className="layout-container text-white"> + {/* Bilingual page title and description */} <h1 className="m-0"> {props.locale === "en" ? pageData.scFragments[0].scContentEn.json[0].content[0].value @@ -133,7 +175,9 @@ export default function ProjectsPage(props) { </p> </div> </div> + <div className="layout-container"> + {/* Project status filter dropdown */} <div className="my-12 max-w-[350px]"> <MultiSelectField label={getDictionaryTerm( @@ -148,6 +192,7 @@ export default function ProjectsPage(props) { selectedOptions={selectedOptions} /> </div> + {/* Grid of project cards */} <ul className="grid grid-cols-12 gap-6 mt-20">{projectsCards}</ul> </div> </Layout> @@ -155,20 +200,28 @@ export default function ProjectsPage(props) { ); } +/** + * Next.js Static Site Generation (SSG) function + * Fetches all required data at build time + * Enables Incremental Static Regeneration (ISR) if configured + */ export const getStaticProps = async ({ locale }) => { - // Get page data + // Fetch main page content from AEM const { data: pageData } = await fetch( `${process.env.AEM_BASE_URL}/getSclProjectsV2` ).then((res) => res.json()); - // Get projects data + + // Fetch all projects data const { data: projectsData } = await fetch( `${process.env.AEM_BASE_URL}/getSclAllProjectsV1` ).then((res) => res.json()); - // get dictionary + + // Fetch translation dictionary const { data: dictionary } = await fetch( `${process.env.AEM_BASE_URL}/getSclDictionaryV1` ).then((res) => res.json()); + // Return props for page rendering return { props: { locale: locale, @@ -176,8 +229,10 @@ export const getStaticProps = async ({ locale }) => { pageData: pageData.sclabsPageV1ByPath, projectsData: projectsData.sclabsPageV1List.items, dictionary: dictionary.dictionaryV1List.items, + // Include translations for common terms and multiSelect component ...(await serverSideTranslations(locale, ["common", "multiSelect"])), }, + // Enable ISR if configured in environment revalidate: process.env.ISR_ENABLED === "true" ? 10 : false, }; }; diff --git a/pages/projects/benefits-finder/[id].js b/pages/projects/benefits-finder/[id].js index 00443ec1f2..95ce3e1ba0 100644 --- a/pages/projects/benefits-finder/[id].js +++ b/pages/projects/benefits-finder/[id].js @@ -9,10 +9,17 @@ import FragmentRender from "../../../components/fragment_renderer/FragmentRender import { Heading } from "../../../components/molecules/Heading"; import { getDictionaryTerm } from "../../../lib/utils/getDictionaryTerm"; +/** + * Benefits Finder Articles Component + * Displays individual article pages for the Benefits Finder project + * Handles dynamic routing and bilingual content + */ export default function BenefitFinderArticles({ key, ...props }) { - const [pageData] = useState(props.pageData); - const [dictionary] = useState(props.dictionary.items); + // Initialize state with page data and dictionary + const [pageData] = useState(props.pageData); // Current article content + const [dictionary] = useState(props.dictionary.items); // Translation dictionary + // Initialize Adobe Analytics useEffect(() => { if (props.adobeAnalyticsUrl) { window.adobeDataLayer = window.adobeDataLayer || []; @@ -24,18 +31,23 @@ export default function BenefitFinderArticles({ key, ...props }) { <> <Layout locale={props.locale} + // Set alternate language URL for language toggle langUrl={ props.locale === "en" ? pageData.scPageNameFr : pageData.scPageNameEn } dateModifiedOverride={pageData.scDateModifiedOverwrite} + // Generate breadcrumb navigation breadcrumbItems={createBreadcrumbs( pageData.scBreadcrumbParentPages, props.locale )} > + {/* Page head component for meta tags */} <PageHead pageData={pageData} locale={props.locale} /> + <section className="mb-12"> <div className="layout-container"> + {/* Article Title */} <Heading tabIndex="-1" id="pageMainTitle" @@ -43,7 +55,10 @@ export default function BenefitFinderArticles({ key, ...props }) { props.locale === "en" ? pageData.scTitleEn : pageData.scTitleFr } /> + + {/* Article Metadata Grid - Posted and Updated dates */} <div id="postedOnUpdatedOnSection" className="grid grid-cols-12"> + {/* Posted On Label - Responsive column widths */} <p className={`col-span-6 sm:col-span-4 ${ props.locale === "en" ? "lg:col-span-2" : "lg:col-span-3" @@ -51,9 +66,12 @@ export default function BenefitFinderArticles({ key, ...props }) { > {getDictionaryTerm(dictionary, "POSTED-ON", props.locale)} </p> + {/* Posted Date */} <p className="col-span-6 col-start-7 sm:col-start-5 lg:col-span-2 md:col-start-5 mt-0"> {pageData.scDateModifiedOverwrite} </p> + + {/* Last Updated Label - Responsive column widths */} <p className={`row-start-2 col-span-6 sm:col-span-4 mt-0 ${ props.locale === "en" ? "lg:col-span-2" : "lg:col-span-3" @@ -61,13 +79,14 @@ export default function BenefitFinderArticles({ key, ...props }) { > {getDictionaryTerm(dictionary, "LAST-UPDATED", props.locale)} </p> + {/* Updated Date */} <p className="row-start-2 col-span-6 col-start-7 sm:col-start-5 lg:col-span-2 md:col-start-5 mt-auto"> {pageData.scDateModifiedOverwrite} </p> </div> </div> - {/* Main */} + {/* Main Article Content */} <div id="mainContentSection"> <FragmentRender fragments={props.pageData.scFragments} @@ -80,31 +99,43 @@ export default function BenefitFinderArticles({ key, ...props }) { ); } +/** + * Generate static paths for all article pages + * Required for dynamic routing in Next.js + * Creates paths for both English and French versions + */ export async function getStaticPaths() { - // Get pages data + // Fetch all Benefits Navigator articles from AEM const { data } = await aemServiceInstance.getFragment( "benefitsNavigatorArticlesQuery" ); - // Get paths for dynamic routes from the page name data + // Generate paths for dynamic routes using page names const paths = getAllUpdateIds(data.sclabsPageV1List.items); + // Extract the last segment of the URL for the ID parameter paths.map((path) => (path.params.id = path.params.id.split("/").at(-1))); + return { paths, - fallback: "blocking", + fallback: "blocking", // Show loading state while generating new pages }; } +/** + * Fetch page data at build time + * Handles data fetching for both languages and 404 cases + */ export const getStaticProps = async ({ locale, params }) => { - // Get pages data + // Fetch all Benefits Finder articles const { data } = await aemServiceInstance.getFragment( "benefitsFinderArticlesQuery" ); - // get dictionary + // Fetch translation dictionary const { data: dictionary } = await aemServiceInstance.getFragment( "dictionaryQuery" ); + const pages = data.sclabsPageV1List.items; - // Return page data that matches the current page being built + // Filter for the current article based on URL parameter const pageData = pages.filter((page) => { return ( (locale === "en" ? page.scPageNameEn : page.scPageNameFr) @@ -113,12 +144,14 @@ export const getStaticProps = async ({ locale, params }) => { ); }); + // Return 404 if page not found if (!pageData || !pageData.length) { return { notFound: true, }; } + // Return props for page rendering return { props: { key: params.id, @@ -126,8 +159,10 @@ export const getStaticProps = async ({ locale, params }) => { pageData: pageData[0], dictionary: dictionary.dictionaryV1List, adobeAnalyticsUrl: process.env.ADOBE_ANALYTICS_URL ?? null, + // Include translations for common terms and virtual clinic ...(await serverSideTranslations(locale, ["common", "vc"])), }, + // Enable ISR if configured in environment revalidate: process.env.ISR_ENABLED === "true" ? 10 : false, }; }; diff --git a/pages/projects/benefits-finder/index.js b/pages/projects/benefits-finder/index.js index d23cad09ee..82543f84dc 100644 --- a/pages/projects/benefits-finder/index.js +++ b/pages/projects/benefits-finder/index.js @@ -17,20 +17,29 @@ import { getDictionaryTerm } from "../../../lib/utils/getDictionaryTerm"; import { filterItems } from "../../../lib/utils/filterItems"; import { shuffle } from "../../../lib/utils/shuffle"; +/** + * Benefits Finder Project Overview Component + * Displays detailed information about the Benefits Finder project + * Includes project summary, stage information, updates, and related projects + */ export default function BenefitsFinderOverview(props) { - const [pageData] = useState(props.pageData.item); - const updatesData = sortUpdatesByDate(props.updatesData); - const allProjects = props.allProjects; + // Initialize state and data management + const [pageData] = useState(props.pageData.item); // Core page content from AEM + const updatesData = sortUpdatesByDate(props.updatesData); // Project updates sorted by date + const allProjects = props.allProjects; // All SC Labs projects + + // Filter dictionary to only include terms needed for this page const [filteredDictionary] = useState( props.dictionary.filter( (item) => - item.scId === "STARTED" || - item.scId === "ENDED" || - item.scId === "PROJECT-STAGE" || - item.scId === "SUMMARY" + item.scId === "STARTED" || // Project start date label + item.scId === "ENDED" || // Project end date label + item.scId === "PROJECT-STAGE" || // Project stage label + item.scId === "SUMMARY" // Summary section label ) ); + // Initialize Adobe Analytics data layer useEffect(() => { if (props.adobeAnalyticsUrl) { window.adobeDataLayer = window.adobeDataLayer || []; @@ -42,10 +51,12 @@ export default function BenefitsFinderOverview(props) { <> <Layout locale={props.locale} + // Set alternate language URL for language toggle langUrl={ props.locale === "en" ? pageData.scPageNameFr : pageData.scPageNameEn } dateModifiedOverride={pageData.scDateModifiedOverwrite} + // Generate breadcrumb navigation from parent pages breadcrumbItems={createBreadcrumbs( pageData.scBreadcrumbParentPages, props.locale @@ -215,9 +226,12 @@ export default function BenefitsFinderOverview(props) { /> </Head> + {/* Main Content Container */} <div className="layout-container"> <section aria-labelledby="pageMainTitle"> + {/* Two-column grid layout for desktop, stack on mobile */} <div className="flex flex-col break-words lg:grid lg:grid-cols-2"> + {/* Page Title - spans full width */} <div className="col-span-2"> <Heading tabIndex="-1" @@ -229,6 +243,8 @@ export default function BenefitsFinderOverview(props) { } /> </div> + + {/* Project Image - hidden on mobile, shown in right column on desktop */} <div className="hidden lg:grid row-span-2 row-start-2 col-start-2 p-0 mx-4"> <div className="flex justify-center"> <div className="object-fill max-w-350px"> @@ -245,22 +261,27 @@ export default function BenefitsFinderOverview(props) { } width={pageData.scFragments[2].scImageEn.width} height={pageData.scFragments[2].scImageEn.height} - priority + priority // Load image with high priority sizes="33vw" quality={100} /> </div> </div> </div> + + {/* Project Description Text */} <p className="row-start-2 font-body text-lg mb-4"> {props.locale === "en" ? pageData.scFragments[0].scContentEn.json[1].content[0].value : pageData.scFragments[0].scContentFr.json[1].content[0] .value} </p> + + {/* Project Information Component - contains key project details */} <div className="row-start-3"> <ProjectInfo locale={props.locale} + // Pass translated terms and labels termStarted={ props.locale === "en" ? filteredDictionary[2].scTermEn @@ -276,6 +297,7 @@ export default function BenefitsFinderOverview(props) { ? filteredDictionary[3].scTermEn : filteredDictionary[3].scTermFr } + // Pass project-specific information dateStarted={ pageData.scFragments[0].scContentEn.json[2].content[0].value } @@ -310,6 +332,8 @@ export default function BenefitsFinderOverview(props) { } /> </div> + + {/* Additional Project Information */} <strong className="font-body text-p pt-8"> {props.locale === "en" ? pageData.scFragments[0].scContentEn.json[5].content[0].value @@ -320,10 +344,13 @@ export default function BenefitsFinderOverview(props) { </section> </div> + {/* Render Additional Content Fragments */} <FragmentRender fragments={pageData.scFragments.slice(3)} locale={props.locale} /> + + {/* Project Updates Section - Only shown if updates exist */} {props.updatesData.length !== 0 ? ( <ExploreUpdates locale={props.locale} @@ -354,6 +381,8 @@ export default function BenefitsFinderOverview(props) { } /> ) : null} + + {/* Related Projects Section - Shows 3 random other projects */} <ExploreProjects heading={getDictionaryTerm( props.dictionary, @@ -368,20 +397,27 @@ export default function BenefitsFinderOverview(props) { ); } +/** + * Next.js Static Site Generation (SSG) function + * Fetches all required data at build time + */ export const getStaticProps = async ({ locale }) => { - // get page data from AEM + // Fetch main Benefits Finder content from AEM const { data: pageData } = await aemServiceInstance.getFragment( "benefitsFinderQuery" ); - // get dictionary + + // Fetch translation dictionary const { data: dictionary } = await aemServiceInstance.getFragment( "dictionaryQuery" ); - // get all projects data + + // Fetch all projects for related projects section const { data: allProjects } = await aemServiceInstance.getFragment( "projectQuery" ); + // Return props for page rendering return { props: { locale: locale, @@ -389,9 +425,12 @@ export const getStaticProps = async ({ locale }) => { pageData: pageData.sclabsPageV1ByPath, updatesData: pageData.sclabsPageV1ByPath.item.scLabProjectUpdates, dictionary: dictionary.dictionaryV1List.items, + // Randomize project order for related projects section allProjects: shuffle(allProjects.sclabsPageV1List.items), + // Include common translations ...(await serverSideTranslations(locale, ["common"])), }, + // Enable ISR if configured in environment revalidate: process.env.ISR_ENABLED === "true" ? 10 : false, }; }; diff --git a/pages/projects/benefits-navigator/[id].js b/pages/projects/benefits-navigator/[id].js index dd2603cf08..a13958cdde 100644 --- a/pages/projects/benefits-navigator/[id].js +++ b/pages/projects/benefits-navigator/[id].js @@ -14,11 +14,19 @@ import { getDictionaryTerm } from "../../../lib/utils/getDictionaryTerm"; import { UpdateInfo } from "../../../components/atoms/UpdateInfo"; import { ExploreProjects } from "../../../components/organisms/ExploreProjects"; +/** + * Benefits Navigator Articles Component + * Displays individual article pages for the Benefits Navigator project + * Supports bilingual content, project metadata, and related content + * Uses dynamic routing to handle multiple article URLs + */ export default function BenefitNavigatorArticles({ key, ...props }) { - const [pageData] = useState(props.pageData); - const [dictionary] = useState(props.dictionary); - const projectData = props.projectData; + // State management for page content and translations + const [pageData] = useState(props.pageData); // Individual article data from AEM + const [dictionary] = useState(props.dictionary); // Translation dictionary for UI elements + const projectData = props.projectData; // Parent project information + // Initialize Adobe Analytics tracking useEffect(() => { if (props.adobeAnalyticsUrl) { window.adobeDataLayer = window.adobeDataLayer || []; @@ -28,49 +36,65 @@ export default function BenefitNavigatorArticles({ key, ...props }) { return ( <> + {/* Layout wrapper component provides consistent page structure */} <Layout locale={props.locale} + // Alternate language URL for language switching langUrl={ props.locale === "en" ? pageData.scPageNameFr : pageData.scPageNameEn } + // Last modified date for the page dateModifiedOverride={pageData.scDateModifiedOverwrite} + // Breadcrumb navigation generated from parent pages breadcrumbItems={createBreadcrumbs( pageData.scBreadcrumbParentPages, props.locale )} > + {/* PageHead component manages meta tags */} <PageHead pageData={pageData} locale={props.locale} /> + + {/* Main article section */} <section className="mb-12"> <div className="layout-container"> + {/* Article title with accessibility support */} <Heading tabIndex="-1" id="pageMainTitle" title={ + // Bilingual title handling props.locale === "en" ? pageData.scTitleEn : pageData.scTitleFr } /> + + {/* Article metadata component showing project context and dates */} <UpdateInfo + // Project label and name with translation projectLabel={`${getDictionaryTerm( dictionary, "PROJECT", props.locale )}`} projectName={ + // Bilingual project name props.locale === "en" ? pageData.scLabProject.scTermEn : pageData.scLabProject.scTermFr } + // Link to parent project page projectHref={ props.locale === "en" ? pageData.scLabProject.scDestinationURLEn : pageData.scLabProject.scDestinationURLFr } + // Posted date label and value postedOnLabel={`${getDictionaryTerm( dictionary, "POSTED-ON", props.locale )}`} postedOn={pageData.scDateModifiedOverwrite} + // Last updated label and value lastUpdatedLabel={`${getDictionaryTerm( dictionary, "LAST-UPDATED", @@ -80,20 +104,24 @@ export default function BenefitNavigatorArticles({ key, ...props }) { /> </div> - {/* Main */} + {/* Main article content rendered from AEM fragments */} <div id="mainContentSection"> <FragmentRender fragments={props.pageData.scFragments} locale={props.locale} - excludeH1={true} + excludeH1={true} // Exclude H1 as it's already rendered in the Heading component /> </div> </section> + + {/* Related Updates Section - Conditionally rendered if updates exist */} {filterItems(props.updatesData, pageData.scId).length !== 0 ? ( <ExploreUpdates locale={props.locale} + // Filter updates related to this article updatesData={filterItems(props.updatesData, pageData.scId)} dictionary={props.dictionary} + // Construct bilingual section heading heading={ props.locale === "en" ? `${projectData.scTitleEn} ${getDictionaryTerm( @@ -107,11 +135,13 @@ export default function BenefitNavigatorArticles({ key, ...props }) { props.locale )} ${projectData.scTitleFr}` } + // "See all updates" link label linkLabel={`${getDictionaryTerm( props.dictionary, "DICTIONARY-SEE-ALL-UPDATES-PROJECT", props.locale )}`} + // Link to filtered updates page href={ props.locale === "en" ? `/en/updates?project=${pageData.scLabProject.scTermEn}` @@ -119,8 +149,10 @@ export default function BenefitNavigatorArticles({ key, ...props }) { } /> ) : null} + + {/* Parent Project Information Section */} <ExploreProjects - projects={[projectData]} + projects={[projectData]} // Show only the parent project heading={getDictionaryTerm( dictionary, "EXPLORE-THE-PROJECT", @@ -133,34 +165,48 @@ export default function BenefitNavigatorArticles({ key, ...props }) { ); } +/** + * Generate static paths for all Benefits Navigator articles + * Required for Next.js dynamic routing + * Creates paths for both English and French versions of each article + */ export async function getStaticPaths() { - // Get pages data + // Fetch all Benefits Navigator articles from AEM const { data } = await aemServiceInstance.getFragment( "benefitsNavigatorArticlesQuery" ); - // Get paths for dynamic routes from the page name data + // Generate paths array for all articles in both languages const paths = getAllUpdateIds(data.sclabsPageV1List.items); + // Extract the article ID from the full path paths.map((path) => (path.params.id = path.params.id.split("/").at(-1))); + return { paths, - fallback: "blocking", + fallback: "blocking", // Show loading state for new pages being generated }; } +/** + * Fetch and prepare data for page rendering at build time + * Handles data fetching, language selection, and 404 cases + * @param {Object} context - Contains locale and URL parameters + */ export const getStaticProps = async ({ locale, params }) => { - // Get pages data + // Fetch all articles data from AEM const { data: updatesData } = await aemServiceInstance.getFragment( "benefitsNavigatorArticlesQuery" ); + // Fetch parent project data from AEM const { data: projectData } = await aemServiceInstance.getFragment( "benefitsNavigatorQuery" ); - // get dictionary + // Fetch translation dictionary const { data: dictionary } = await aemServiceInstance.getFragment( "dictionaryQuery" ); + const pages = updatesData.sclabsPageV1List.items; - // Return page data that matches the current page being built + // Find the specific article based on URL parameter const pageData = pages.filter((page) => { return ( (locale === "en" ? page.scPageNameEn : page.scPageNameFr) @@ -169,23 +215,26 @@ export const getStaticProps = async ({ locale, params }) => { ); }); + // Return 404 response if article not found if (!pageData || !pageData.length) { return { notFound: true, }; } + // Return props for page rendering return { props: { - key: params.id, - locale: locale, - pageData: pageData[0], - updatesData: updatesData.sclabsPageV1List.items, - projectData: projectData.sclabsPageV1ByPath.item, - dictionary: dictionary.dictionaryV1List.items, - adobeAnalyticsUrl: process.env.ADOBE_ANALYTICS_URL ?? null, - ...(await serverSideTranslations(locale, ["common", "vc"])), + key: params.id, // Unique key for React + locale: locale, // Current language + pageData: pageData[0], // Article content + updatesData: updatesData.sclabsPageV1List.items, // All updates for filtering + projectData: projectData.sclabsPageV1ByPath.item, // Parent project data + dictionary: dictionary.dictionaryV1List.items, // Translation dictionary + adobeAnalyticsUrl: process.env.ADOBE_ANALYTICS_URL ?? null, // Analytics configuration + ...(await serverSideTranslations(locale, ["common", "vc"])), // Load translations }, + // Enable Incremental Static Regeneration if configured revalidate: process.env.ISR_ENABLED === "true" ? 10 : false, }; }; diff --git a/pages/projects/benefits-navigator/index.js b/pages/projects/benefits-navigator/index.js index b28b5047b0..c84207c2e8 100644 --- a/pages/projects/benefits-navigator/index.js +++ b/pages/projects/benefits-navigator/index.js @@ -18,20 +18,29 @@ import { sortUpdatesByDate } from "../../../lib/utils/sortUpdatesByDate"; import { getDictionaryTerm } from "../../../lib/utils/getDictionaryTerm"; import { ContextualAlert } from "../../../components/molecules/ContextualAlert"; +/** + * Benefits Navigator Overview Component + * Main landing page for the Benefits Navigator project + * Features bilingual content, project details, and feature showcases + */ export default function BenefitsNavigatorOverview(props) { - const [allProjects] = useState(props.allProjects); - const [pageData] = useState(props.pageData.item); - const updatesData = props.updatesData; + // Initialize state with project data and filtered dictionary terms + const [allProjects] = useState(props.allProjects); // All SC Labs projects for related content + const [pageData] = useState(props.pageData.item); // Current page content from AEM + const updatesData = props.updatesData; // Project updates data + + // Filter dictionary to only include specific terms needed for project info display const [filteredDictionary] = useState( props.dictionary.filter( (item) => - item.scId === "STARTED" || - item.scId === "ENDED" || - item.scId === "PROJECT-STAGE" || - item.scId === "SUMMARY" + item.scId === "STARTED" || // Project start date label + item.scId === "ENDED" || // Project end date label + item.scId === "PROJECT-STAGE" || // Current stage label + item.scId === "SUMMARY" // Summary section label ) ); + // Initialize Adobe Analytics data layer useEffect(() => { if (props.adobeAnalyticsUrl) { window.adobeDataLayer = window.adobeDataLayer || []; @@ -41,12 +50,15 @@ export default function BenefitsNavigatorOverview(props) { return ( <> + {/* Main Layout Component - Provides consistent page structure */} <Layout locale={props.locale} + // Set alternate language URL for language toggle langUrl={ props.locale === "en" ? pageData.scPageNameFr : pageData.scPageNameEn } dateModifiedOverride={pageData.scDateModifiedOverwrite} + // Generate breadcrumb navigation from parent pages breadcrumbItems={createBreadcrumbs( pageData.scBreadcrumbParentPages, props.locale @@ -215,10 +227,13 @@ export default function BenefitsNavigatorOverview(props) { } /> </Head> - + {/* Main Content Container */} <div className="layout-container mb-24"> + {/* Main Content Section with ARIA labelledby */} <section aria-labelledby="pageMainTitle"> + {/* Two-column layout for desktop, single column for mobile */} <div className="flex flex-col break-words lg:grid lg:grid-cols-2"> + {/* Page Title and Alert Section - Full Width */} <div className="col-span-2"> <Heading tabIndex="-1" @@ -229,6 +244,7 @@ export default function BenefitsNavigatorOverview(props) { : pageData.scTitleFr } /> + {/* Warning Alert for Project Status */} <div className="mb-10 max-w-[76ch]"> <ContextualAlert id="alert" @@ -252,6 +268,8 @@ export default function BenefitsNavigatorOverview(props) { /> </div> </div> + + {/* Project Image - Hidden on mobile, shown in right column on desktop */} <div className="hidden lg:grid row-span-2 row-start-2 col-start-2 p-0 mx-4"> <div className="flex justify-center"> <div className="object-fill max-w-350px"> @@ -268,22 +286,27 @@ export default function BenefitsNavigatorOverview(props) { } width={pageData.scFragments[1].scImageEn.width} height={pageData.scFragments[1].scImageEn.height} - priority + priority // Load image with high priority sizes="33vw" quality={100} /> </div> </div> </div> + + {/* Project Description Text */} <p className="row-start-2 mb-4"> {props.locale === "en" ? pageData.scFragments[3].scContentEn.json[1].content[0].value : pageData.scFragments[3].scContentFr.json[1].content[0] .value} </p> + + {/* Project Information Component - Contains key project details */} <div className="row-start-3"> <ProjectInfo locale={props.locale} + // Pass translated terms and labels termStarted={ props.locale === "en" ? filteredDictionary[2].scTermEn @@ -337,12 +360,16 @@ export default function BenefitsNavigatorOverview(props) { </div> </div> </section> + {/* Features Grid Section - Uses 12-column grid system */} <div className="grid grid-cols-12"> + {/* Features Section Title */} <h2 className="col-span-12"> {props.locale === "en" ? pageData.scFragments[4].scContentEn.json[0].content[0].value : pageData.scFragments[4].scContentFr.json[0].content[0].value} </h2> + + {/* Features Introduction Paragraphs - 8 columns on desktop */} <p className="col-span-12 xl:col-span-8"> {props.locale === "en" ? pageData.scFragments[4].scContentEn.json[1].content[0].value @@ -353,7 +380,10 @@ export default function BenefitsNavigatorOverview(props) { ? pageData.scFragments[4].scContentEn.json[2].content[0].value : pageData.scFragments[4].scContentFr.json[2].content[0].value} </p> + + {/* Feature List - Bulleted list of key features */} <ul className="list-disc col-span-12 xl:col-span-8 text-mobilebody lg:text-p"> + {/* Individual feature items with responsive text sizing */} <li className="ml-10"> {props.locale === "en" ? pageData.scFragments[4].scContentEn.json[3].content[0] @@ -376,19 +406,20 @@ export default function BenefitsNavigatorOverview(props) { .content[0].value} </li> </ul> - <p className="col-span-12 xl:col-span-8"> - {props.locale === "en" - ? pageData.scFragments[4].scContentEn.json[4].content[0].value - : pageData.scFragments[4].scContentFr.json[4].content[0].value} - </p> + + {/* Features Section with Images and Descriptions */} <div id="feature-section" className="col-span-12"> + {/* Features Section Title */} <h2 className="col-span-12"> {props.locale === "en" ? pageData.scFragments[5].scContentEn.json[0].content[0].value : pageData.scFragments[5].scContentFr.json[0].content[0] .value} </h2> + + {/* Feature 1 - Grid Layout with Image, Description, and Collapse */} <div id="feature-1" className="grid grid-cols-12 gap-x-6 mb-9"> + {/* Feature Image - Full width on mobile, 8 columns on desktop */} <div className="mb-6 object-fill col-span-12 row-start-1 xl:row-start-1 xl:col-span-8"> <Image src={ @@ -417,6 +448,7 @@ export default function BenefitsNavigatorOverview(props) { quality={100} /> </div> + {/* Feature Description - With blue left border */} <div className="col-span-12 row-start-3 xl:col-span-4 xl:row-start-1"> <div className="py-4 pl-4 border-l-4 border-multi-blue-blue60f"> <TextRender @@ -430,6 +462,7 @@ export default function BenefitsNavigatorOverview(props) { /> </div> </div> + {/* Collapsible Long Description */} <div className="mb-6 col-span-12 xl:col-span-8 row-start-2 xl:row-start-2"> <Collapse id="image-text-collapse-1" @@ -633,20 +666,27 @@ export default function BenefitsNavigatorOverview(props) { ); } +/** + * Next.js Static Site Generation (SSG) function + * Fetches all required data at build time + */ export const getStaticProps = async ({ locale }) => { - // get page data from AEM + // Fetch main page content from AEM const { data: pageData } = await aemServiceInstance.getFragment( "benefitsNavigatorQuery" ); - // get dictionary + + // Fetch translation dictionary const { data: dictionary } = await aemServiceInstance.getFragment( "dictionaryQuery" ); - // get all projects data + + // Fetch all projects for related content const { data: allProjects } = await aemServiceInstance.getFragment( "projectQuery" ); + // Return props for page rendering return { props: { locale: locale, @@ -654,9 +694,12 @@ export const getStaticProps = async ({ locale }) => { pageData: pageData.sclabsPageV1ByPath, updatesData: pageData.sclabsPageV1ByPath.item.scLabProjectUpdates, dictionary: dictionary.dictionaryV1List.items, + // Randomize projects order for variety allProjects: shuffle(allProjects.sclabsPageV1List.items), + // Include common translations ...(await serverSideTranslations(locale, ["common"])), }, + // Enable ISR if configured in environment revalidate: process.env.ISR_ENABLED === "true" ? 10 : false, }; }; diff --git a/pages/projects/dashboard/[id].js b/pages/projects/dashboard/[id].js index 563d726fb1..da05f52c19 100644 --- a/pages/projects/dashboard/[id].js +++ b/pages/projects/dashboard/[id].js @@ -14,11 +14,18 @@ import { getDictionaryTerm } from "../../../lib/utils/getDictionaryTerm"; import { UpdateInfo } from "../../../components/atoms/UpdateInfo"; import { ExploreProjects } from "../../../components/organisms/ExploreProjects"; +/** + * MSCA Dashboard Articles Component + * Displays individual article pages for the My Service Canada Account Dashboard project + * Supports bilingual content, project context, and related updates + */ export default function MscaDashboardArticles({ key, ...props }) { - const [pageData] = useState(props.pageData); - const [dictionary] = useState(props.dictionary.items); - const [projectData] = useState(props.projectData); + // Initialize state with content data + const [pageData] = useState(props.pageData); // Current article content + const [dictionary] = useState(props.dictionary.items); // Translation dictionary + const [projectData] = useState(props.projectData); // Parent project data + // Initialize Adobe Analytics useEffect(() => { if (props.adobeAnalyticsUrl) { window.adobeDataLayer = window.adobeDataLayer || []; @@ -28,20 +35,27 @@ export default function MscaDashboardArticles({ key, ...props }) { return ( <> + {/* Main Layout Component - Provides consistent page structure */} <Layout locale={props.locale} + // Alternate language URL for language toggle langUrl={ props.locale === "en" ? pageData.scPageNameFr : pageData.scPageNameEn } dateModifiedOverride={pageData.scDateModifiedOverwrite} + // Generate breadcrumb navigation breadcrumbItems={createBreadcrumbs( pageData.scBreadcrumbParentPages, props.locale )} > + {/* Head component for meta tags */} <PageHead pageData={pageData} locale={props.locale} /> + + {/* Main Article Section */} <section className="mb-12"> <div className="layout-container"> + {/* Article Title with accessibility support */} <Heading tabIndex="-1" id="pageMainTitle" @@ -49,7 +63,10 @@ export default function MscaDashboardArticles({ key, ...props }) { props.locale === "en" ? pageData.scTitleEn : pageData.scTitleFr } /> + + {/* Article Metadata Component */} <UpdateInfo + // Project context labels and links projectLabel={`${getDictionaryTerm( dictionary, "PROJECT", @@ -65,6 +82,7 @@ export default function MscaDashboardArticles({ key, ...props }) { ? pageData.scLabProject.scDestinationURLEn : pageData.scLabProject.scDestinationURLFr } + // Publishing dates postedOnLabel={`${getDictionaryTerm( dictionary, "POSTED-ON", @@ -80,20 +98,24 @@ export default function MscaDashboardArticles({ key, ...props }) { /> </div> - {/* Main */} + {/* Main Article Content */} <div id="mainContentSection"> <FragmentRender fragments={props.pageData.scFragments} locale={props.locale} - excludeH1={true} + excludeH1={true} // Exclude H1 as it's already rendered in Heading /> </div> </section> + + {/* Related Updates Section - Conditionally rendered if updates exist */} {filterItems(props.updatesData, pageData.scId).length !== 0 ? ( <ExploreUpdates locale={props.locale} + // Filter updates related to this article updatesData={filterItems(props.updatesData, pageData.scId)} dictionary={dictionary} + // Construct bilingual section heading heading={ props.locale === "en" ? `${projectData.scTitleEn} ${getDictionaryTerm( @@ -107,11 +129,13 @@ export default function MscaDashboardArticles({ key, ...props }) { props.locale )} ${projectData.scTitleFr}` } + // "See all updates" link label linkLabel={`${getDictionaryTerm( dictionary, "DICTIONARY-SEE-ALL-UPDATES-PROJECT", props.locale )}`} + // Link to filtered updates page href={ props.locale === "en" ? `/en/updates?project=${pageData.scLabProject.scTermEn}` @@ -119,8 +143,10 @@ export default function MscaDashboardArticles({ key, ...props }) { } /> ) : null} + + {/* Parent Project Information Section */} <ExploreProjects - projects={[projectData]} + projects={[projectData]} // Show only the parent project heading={getDictionaryTerm( dictionary, "EXPLORE-THE-PROJECT", @@ -133,34 +159,47 @@ export default function MscaDashboardArticles({ key, ...props }) { ); } +/** + * Generate static paths for all MSCA Dashboard articles + * Required for Next.js dynamic routing + * Creates paths for both English and French versions + */ export async function getStaticPaths() { - // Get pages data + // Fetch all MSCA Dashboard articles from AEM const { data } = await aemServiceInstance.getFragment( "getMSCADashboardArticles" ); - // Get paths for dynamic routes from the page name data + // Generate paths for dynamic routes using page names const paths = getAllUpdateIds(data.sclabsPageV1List.items); + // Extract the last segment of the URL for the ID parameter paths.map((path) => (path.params.id = path.params.id.split("/").at(-1))); + return { paths, - fallback: "blocking", + fallback: "blocking", // Show loading state while generating new pages }; } +/** + * Fetch page data at build time + * Handles data fetching for both languages and 404 cases + */ export const getStaticProps = async ({ locale, params }) => { - // Get pages data + // Fetch all MSCA Dashboard articles const { data: updatesData } = await aemServiceInstance.getFragment( "getMSCADashboardArticles" ); + // Fetch parent project data const { data: projectData } = await aemServiceInstance.getFragment( "getMSCADashBoardPage" ); - // get dictionary + // Fetch translation dictionary const { data: dictionary } = await aemServiceInstance.getFragment( "dictionaryQuery" ); + const pages = updatesData.sclabsPageV1List.items; - // Return page data that matches the current page being built + // Filter for the current article based on URL parameter const pageData = pages.filter((page) => { return ( (locale === "en" ? page.scPageNameEn : page.scPageNameFr) @@ -169,12 +208,14 @@ export const getStaticProps = async ({ locale, params }) => { ); }); + // Return 404 if page not found if (!pageData || !pageData.length) { return { notFound: true, }; } + // Return props for page rendering return { props: { key: params.id, @@ -186,6 +227,7 @@ export const getStaticProps = async ({ locale, params }) => { adobeAnalyticsUrl: process.env.ADOBE_ANALYTICS_URL ?? null, ...(await serverSideTranslations(locale, ["common", "vc"])), }, + // Enable ISR if configured in environment revalidate: process.env.ISR_ENABLED === "true" ? 10 : false, }; }; diff --git a/pages/projects/dashboard/index.js b/pages/projects/dashboard/index.js index 6cba3892d4..89a231c29a 100644 --- a/pages/projects/dashboard/index.js +++ b/pages/projects/dashboard/index.js @@ -17,18 +17,26 @@ import { sortUpdatesByDate } from "../../../lib/utils/sortUpdatesByDate"; import FragmentRender from "../../../components/fragment_renderer/FragmentRender"; import { getDictionaryTerm } from "../../../lib/utils/getDictionaryTerm"; +/** + * MSCA Dashboard Component + * Main landing page for the My Service Canada Account Dashboard project + * Features project overview, updates, and related project information + */ export default function MscaDashboard(props) { - const pageData = props.pageData?.item; - const [allProjects] = useState(props.allProjects); + // Initialize state and data management + const pageData = props.pageData?.item; // Core page content from AEM + const [allProjects] = useState(props.allProjects); // All SC Labs projects + // Filter dictionary to include only required terms for project information const filteredDictionary = props.dictionary?.filter( (item) => - item.scId === "STARTED" || - item.scId === "ENDED" || - item.scId === "PROJECT-STAGE" || - item.scId === "SUMMARY" + item.scId === "STARTED" || // Project start date label + item.scId === "ENDED" || // Project end date label + item.scId === "PROJECT-STAGE" || // Project stage label + item.scId === "SUMMARY" // Summary section label ); + // Initialize Adobe Analytics useEffect(() => { if (props.adobeAnalyticsUrl) { window.adobeDataLayer = window.adobeDataLayer || []; @@ -38,17 +46,21 @@ export default function MscaDashboard(props) { return ( <> + {/* Main Layout Component - Provides consistent page structure */} <Layout locale={props.locale} + // Alternate language URL for language toggle langUrl={ props.locale === "en" ? pageData.scPageNameFr : pageData.scPageNameEn } dateModifiedOverride={pageData.scDateModifiedOverwrite} + // Generate breadcrumb navigation breadcrumbItems={createBreadcrumbs( pageData.scBreadcrumbParentPages, props.locale )} > + {/* Page Head meta tags */} <Head> {/* Primary HTML Meta Tags */} <title> @@ -209,9 +221,12 @@ export default function MscaDashboard(props) { /> </Head> + {/* Main Content Container */} <div className="layout-container mb-20"> <section aria-labelledby="pageMainTitle"> + {/* Two-column layout for desktop */} <div className="flex flex-col break-words lg:grid lg:grid-cols-2"> + {/* Page Title - Full width */} <div className="col-span-2"> <Heading tabIndex="-1" @@ -223,6 +238,8 @@ export default function MscaDashboard(props) { } /> </div> + + {/* Feature Image - Hidden on mobile, shown in right column on desktop */} <div className="hidden lg:grid row-span-2 row-start-2 col-start-2 p-0 mx-4"> <div className="flex justify-center"> <div className="object-fill h-auto w-auto max-w-450px"> @@ -239,19 +256,23 @@ export default function MscaDashboard(props) { } height={pageData.scFragments[1].scImageEn.height} width={pageData.scFragments[1].scImageEn.width} - priority + priority // Load image with high priority sizes="33vw" quality={100} /> </div> </div> </div> + + {/* Project Description */} <p className="row-start-2 mb-4"> {props.locale === "en" ? pageData.scFragments[0].scContentEn.json[1].content[0].value : pageData.scFragments[0].scContentFr.json[1].content[0] .value} </p> + + {/* Project Information Component */} <div className="row-start-3"> <ProjectInfo locale={props.locale} @@ -309,19 +330,24 @@ export default function MscaDashboard(props) { </div> </section> </div> + + {/* Main Content Section with Project Details */} <section id="pageMainContent"> <FragmentRender locale={props.locale} fragments={pageData.scFragments.slice(3)} - excludeH1={true} + excludeH1={true} // Exclude H1 as it's already rendered in Heading /> </section> + + {/* Updates Section - Conditionally rendered if updates exist */} {props.updatesData.length !== 0 ? ( <ExploreUpdates locale={props.locale} updatesData={sortUpdatesByDate(props.updatesData)} dictionary={props.dictionary} heading={ + // Bilingual heading construction props.locale === "en" ? `${pageData.scTitleEn} ${getDictionaryTerm( props.dictionary, @@ -340,12 +366,15 @@ export default function MscaDashboard(props) { props.locale )}`} href={ + // Link to filtered updates page props.locale === "en" ? `/en/updates?project=${pageData.scTitleEn}` : `/fr/mises-a-jour?projet=${pageData.scTitleFr}` } /> ) : null} + + {/* Related Projects Section */} <ExploreProjects heading={getDictionaryTerm( props.dictionary, @@ -360,20 +389,27 @@ export default function MscaDashboard(props) { ); } +/** + * Next.js Static Site Generation (SSG) function + * Fetches all required data at build time + */ export const getStaticProps = async ({ locale }) => { - // get page data from AEM + // Fetch MSCA Dashboard page content from AEM const { data: pageData } = await aemServiceInstance.getFragment( "getMSCADashBoardPage" ); - // get dictionary + + // Fetch dictionary const { data: dictionary } = await aemServiceInstance.getFragment( "dictionaryQuery" ); - // get all projects data + + // Fetch all projects for related projects section const { data: allProjects } = await aemServiceInstance.getFragment( "projectQuery" ); + // Return props for page rendering return { props: { locale: locale, @@ -381,9 +417,12 @@ export const getStaticProps = async ({ locale }) => { pageData: pageData.sclabsPageV1ByPath, updatesData: pageData.sclabsPageV1ByPath.item.scLabProjectUpdates, dictionary: dictionary.dictionaryV1List.items, + // Randomize projects order for related projects allProjects: shuffle(allProjects.sclabsPageV1List.items), + // Include common translations ...(await serverSideTranslations(locale, ["common"])), }, + // Enable ISR if configured in environment revalidate: process.env.ISR_ENABLED === "true" ? 10 : false, }; }; diff --git a/pages/projects/digital-standards-playbook/[id].js b/pages/projects/digital-standards-playbook/[id].js index 894de79e6d..49a89e4b6e 100644 --- a/pages/projects/digital-standards-playbook/[id].js +++ b/pages/projects/digital-standards-playbook/[id].js @@ -10,17 +10,23 @@ import FragmentRender from "../../../components/fragment_renderer/FragmentRender import { Heading } from "../../../components/molecules/Heading"; import { filterItems } from "../../../lib/utils/filterItems"; import { ExploreUpdates } from "../../../components/organisms/ExploreUpdates"; -import { sortUpdatesByDate } from "../../../lib/utils/sortUpdatesByDate"; import { getDictionaryTerm } from "../../../lib/utils/getDictionaryTerm"; import { UpdateInfo } from "../../../components/atoms/UpdateInfo"; import { ExploreProjects } from "../../../components/organisms/ExploreProjects"; +/** + * Digital Standards Articles Component + * Displays individual article pages for the Digital Standards Playbook project + * Supports bilingual content, project context, and related updates + */ export default function DigitalStandardsArticles({ key, ...props }) { - const { t } = useTranslation("common"); - const [pageData] = useState(props.pageData); - const [projectData] = useState(props.projectData); - const [dictionary] = useState(props.dictionary.items); + // Initialize required hooks and state + const { t } = useTranslation("common"); // Translation hook + const [pageData] = useState(props.pageData); // Current article content + const [projectData] = useState(props.projectData); // Parent project data + const [dictionary] = useState(props.dictionary.items); // Translation dictionary + // Initialize Adobe Analytics useEffect(() => { if (props.adobeAnalyticsUrl) { window.adobeDataLayer = window.adobeDataLayer || []; @@ -30,20 +36,27 @@ export default function DigitalStandardsArticles({ key, ...props }) { return ( <> + {/* Main Layout Component - Provides consistent page structure */} <Layout locale={props.locale} + // Alternate language URL for language toggle langUrl={ props.locale === "en" ? pageData.scPageNameFr : pageData.scPageNameEn } dateModifiedOverride={pageData.scDateModifiedOverwrite} + // Generate breadcrumb navigation breadcrumbItems={createBreadcrumbs( pageData.scBreadcrumbParentPages, props.locale )} > + {/* Meta tags component */} <PageHead pageData={pageData} locale={props.locale} /> + + {/* Main Article Section */} <section className="mb-12"> <div className="layout-container"> + {/* Article Title with accessibility support */} <Heading tabIndex="-1" id="pageMainTitle" @@ -51,7 +64,10 @@ export default function DigitalStandardsArticles({ key, ...props }) { props.locale === "en" ? pageData.scTitleEn : pageData.scTitleFr } /> + + {/* Article Metadata Component */} <UpdateInfo + // Project context labels and links projectLabel={`${getDictionaryTerm( dictionary, "PROJECT", @@ -67,6 +83,7 @@ export default function DigitalStandardsArticles({ key, ...props }) { ? pageData.scLabProject.scDestinationURLEn : pageData.scLabProject.scDestinationURLFr } + // Article dates postedOnLabel={`${getDictionaryTerm( dictionary, "POSTED-ON", @@ -82,20 +99,24 @@ export default function DigitalStandardsArticles({ key, ...props }) { /> </div> - {/* Main */} + {/* Main Article Content */} <div id="mainContentSection"> <FragmentRender fragments={props.pageData.scFragments} locale={props.locale} - excludeH1={true} + excludeH1={true} // Exclude H1 as it's already rendered in Heading /> </div> </section> + + {/* Related Updates Section - Conditionally rendered if updates exist */} {filterItems(props.updatesData, pageData.scId).length !== 0 ? ( <ExploreUpdates locale={props.locale} + // Filter updates related to this article updatesData={filterItems(props.updatesData, pageData.scId)} dictionary={dictionary} + // Construct bilingual section heading heading={ props.locale === "en" ? `${projectData.scTitleEn} ${getDictionaryTerm( @@ -109,11 +130,13 @@ export default function DigitalStandardsArticles({ key, ...props }) { props.locale )} ${projectData.scTitleFr}` } + // "See all updates" link label linkLabel={`${getDictionaryTerm( dictionary, "DICTIONARY-SEE-ALL-UPDATES-PROJECT", props.locale )}`} + // Link to filtered updates page href={ props.locale === "en" ? `/en/updates?project=${pageData.scLabProject.scTermEn}` @@ -121,8 +144,10 @@ export default function DigitalStandardsArticles({ key, ...props }) { } /> ) : null} + + {/* Parent Project Information Section */} <ExploreProjects - projects={[projectData]} + projects={[projectData]} // Show only the parent project heading={getDictionaryTerm( dictionary, "EXPLORE-THE-PROJECT", @@ -135,35 +160,48 @@ export default function DigitalStandardsArticles({ key, ...props }) { ); } +/** + * Generate static paths for all Digital Standards articles + * Required for Next.js dynamic routing + * Creates paths for both English and French versions + */ export async function getStaticPaths() { - // Get pages data + // Fetch all Digital Standards articles from AEM const { data } = await aemServiceInstance.getFragment( "getDigitalStandardsPlaybookArticles" ); - // Get paths for dynamic routes from the page name data + // Generate paths for dynamic routes using page names const paths = getAllUpdateIds(data.sclabsPageV1List.items); - // Remove characters preceding the page name itself i.e. change "/en/projects/oas-benefits-estimator/what-we-learned" to "what-we-learned" + // Extract the last segment of the URL for the ID parameter + // Example: "/en/projects/digital-standards/article" becomes "article" paths.map((path) => (path.params.id = path.params.id.split("/").at(-1))); + return { paths, - fallback: "blocking", + fallback: "blocking", // Show loading state while generating new pages }; } +/** + * Fetch page data at build time + * Handles data fetching for both languages and 404 cases + */ export const getStaticProps = async ({ locale, params }) => { - // Get pages data + // Fetch all Digital Standards articles const { data: updatesData } = await aemServiceInstance.getFragment( "getDigitalStandardsPlaybookArticles" ); + // Fetch parent project data const { data: projectData } = await aemServiceInstance.getFragment( "getDigitalStandardsPlaybookPage" ); - // get dictionary + // Fetch translation dictionary const { data: dictionary } = await aemServiceInstance.getFragment( "dictionaryQuery" ); + const pages = updatesData.sclabsPageV1List.items; - // Return page data that matches the current page being built + // Filter for the current article based on URL parameter const pageData = pages.filter((page) => { return ( (locale === "en" ? page.scPageNameEn : page.scPageNameFr) @@ -172,23 +210,26 @@ export const getStaticProps = async ({ locale, params }) => { ); }); + // Return 404 if page not found if (!pageData || !pageData.length) { return { notFound: true, }; } + // Return props for page rendering return { props: { - key: params.id, - locale: locale, - pageData: pageData[0], - updatesData: updatesData.sclabsPageV1List.items, - projectData: projectData.sclabsPageV1ByPath.item, - dictionary: dictionary.dictionaryV1List, - adobeAnalyticsUrl: process.env.ADOBE_ANALYTICS_URL ?? null, - ...(await serverSideTranslations(locale, ["common"])), + key: params.id, // Unique key for React + locale: locale, // Current language + pageData: pageData[0], // Article content + updatesData: updatesData.sclabsPageV1List.items, // All updates for filtering + projectData: projectData.sclabsPageV1ByPath.item, // Parent project data + dictionary: dictionary.dictionaryV1List, // Translation dictionary + adobeAnalyticsUrl: process.env.ADOBE_ANALYTICS_URL ?? null, // Analytics config + ...(await serverSideTranslations(locale, ["common"])), // Load translations }, + // Enable ISR if configured revalidate: process.env.ISR_ENABLED === "true" ? 10 : false, }; }; diff --git a/pages/projects/digital-standards-playbook/index.js b/pages/projects/digital-standards-playbook/index.js index cc54abf06e..acaf9a7558 100644 --- a/pages/projects/digital-standards-playbook/index.js +++ b/pages/projects/digital-standards-playbook/index.js @@ -17,19 +17,27 @@ import { filterItems } from "../../../lib/utils/filterItems"; import { sortUpdatesByDate } from "../../../lib/utils/sortUpdatesByDate"; import { getDictionaryTerm } from "../../../lib/utils/getDictionaryTerm"; +/** + * Digital Standards Playbook Page Component + * Main landing page for the Digital Standards Playbook project + * Features project overview, survey integration, and related content sections + */ export default function DigitalStandardsPlaybookPage(props) { - const [pageData] = useState(props.pageData.item); - const [updatesData] = useState(props.updatesData); - const [allProjects] = useState(props.allProjects); + // Initialize state with content data + const [pageData] = useState(props.pageData.item); // Page content from AEM + const [updatesData] = useState(props.updatesData); // Project updates + const [allProjects] = useState(props.allProjects); // All SC Labs projects + // Filter dictionary to include only terms needed for project information const filteredDictionary = props.dictionary?.filter( (item) => - item.scId === "STARTED" || - item.scId === "ENDED" || - item.scId === "PROJECT-STAGE" || - item.scId === "SUMMARY" + item.scId === "STARTED" || // Project start date label + item.scId === "ENDED" || // Project end date label + item.scId === "PROJECT-STAGE" || // Project stage label + item.scId === "SUMMARY" // Summary section label ); + // Initialize Adobe Analytics useEffect(() => { if (props.adobeAnalyticsUrl) { window.adobeDataLayer = window.adobeDataLayer || []; @@ -39,17 +47,22 @@ export default function DigitalStandardsPlaybookPage(props) { return ( <> + {/* Main Layout Component - Provides consistent page structure */} <Layout locale={props.locale} + // Alternate language URL for language toggle langUrl={ props.locale === "en" ? pageData.scPageNameFr : pageData.scPageNameEn } + // Page last modified date with fallback dateModifiedOverride={pageData.scDateModifiedOverwrite ?? "2023-11-24"} + // Generate breadcrumb navigation breadcrumbItems={createBreadcrumbs( pageData.scBreadcrumbParentPages, props.locale )} > + {/* Page Head meta tags */} <Head> {/* Primary HTML Meta Tags */} <title> @@ -210,9 +223,13 @@ export default function DigitalStandardsPlaybookPage(props) { /> </Head> + {/* Main Content Container */} <div className="layout-container mb-24"> + {/* Hero Section */} <section aria-labelledby="pageMainTitle"> + {/* Two-column layout for desktop */} <div className="flex flex-col break-words lg:grid lg:grid-cols-2"> + {/* Page Title - Full width */} <div className="col-span-2"> <Heading tabIndex="-1" @@ -224,6 +241,8 @@ export default function DigitalStandardsPlaybookPage(props) { } /> </div> + + {/* Feature Image - Hidden on mobile, shown in right column on desktop */} <div className="hidden lg:grid row-span-2 row-start-2 col-start-2 p-0 mx-4"> <div className="flex justify-center"> <div className="object-fill h-auto w-auto max-w-450px"> @@ -240,19 +259,23 @@ export default function DigitalStandardsPlaybookPage(props) { } height={pageData.scFragments[2].scImageEn.height} width={pageData.scFragments[2].scImageEn.width} - priority + priority // Load image with high priority sizes="33vw" quality={100} /> </div> </div> </div> + + {/* Project Description */} <p className="row-start-2 mb-4"> {props.locale === "en" ? pageData.scFragments[0].scContentEn.json[1].content[0].value : pageData.scFragments[0].scContentFr.json[1].content[0] .value} </p> + + {/* Project Information Component */} <div className="row-start-3"> <ProjectInfo locale={props.locale} @@ -309,8 +332,11 @@ export default function DigitalStandardsPlaybookPage(props) { </div> </div> </section> + + {/* Main Content Section */} <section id="pageMainContent"> <div className="grid grid-cols-12"> + {/* First Content Block */} <h2 className="col-span-12"> {props.locale === "en" ? pageData.scFragments[0].scContentEn.json[5].content[0].value @@ -343,6 +369,7 @@ export default function DigitalStandardsPlaybookPage(props) { .value} </p> + {/* Survey Call-to-Action Button */} <ActionButton id="take-survey" style="primary" @@ -360,6 +387,7 @@ export default function DigitalStandardsPlaybookPage(props) { ariaExpanded={props.ariaExpanded} /> + {/* Additional Content with Link */} <p className="col-span-12 xl:col-span-8"> {props.locale === "en" ? pageData.scFragments[4].scContentEn.json[0].content[0].value @@ -395,6 +423,8 @@ export default function DigitalStandardsPlaybookPage(props) { </div> </section> </div> + + {/* Updates Section - Conditionally rendered if updates exist */} {props.updatesData.length !== 0 ? ( <ExploreUpdates locale={props.locale} @@ -425,6 +455,8 @@ export default function DigitalStandardsPlaybookPage(props) { } /> ) : null} + + {/* Related Projects Section */} <ExploreProjects heading={getDictionaryTerm( props.dictionary, @@ -439,20 +471,27 @@ export default function DigitalStandardsPlaybookPage(props) { ); } +/** + * Next.js Static Site Generation (SSG) function + * Fetches all required data at build time + */ export const getStaticProps = async ({ locale }) => { - // get page data from AEM + // Fetch Digital Standards page content from AEM const { data: pageData } = await aemServiceInstance.getFragment( "getDigitalStandardsPlaybookPage" ); - // get dictionary + + // Fetch translation dictionary const { data: dictionary } = await aemServiceInstance.getFragment( "dictionaryQuery" ); - // get all projects data + + // Fetch all projects for related projects section const { data: allProjects } = await aemServiceInstance.getFragment( "projectQuery" ); + // Return props for page rendering return { props: { locale: locale, @@ -460,9 +499,12 @@ export const getStaticProps = async ({ locale }) => { pageData: pageData.sclabsPageV1ByPath, updatesData: pageData.sclabsPageV1ByPath.item.scLabProjectUpdates, dictionary: dictionary.dictionaryV1List.items, + // Randomize projects order for related projects allProjects: shuffle(allProjects.sclabsPageV1List.items), + // Include common translations ...(await serverSideTranslations(locale, ["common"])), }, + // Enable ISR if configured in environment revalidate: process.env.ISR_ENABLED === "true" ? 10 : false, }; }; diff --git a/pages/projects/making-easier-get-benefits/[id].js b/pages/projects/making-easier-get-benefits/[id].js index b8e80643f6..f6755bb2ed 100644 --- a/pages/projects/making-easier-get-benefits/[id].js +++ b/pages/projects/making-easier-get-benefits/[id].js @@ -1,3 +1,4 @@ +// Import required dependencies and components import { serverSideTranslations } from "next-i18next/serverSideTranslations"; import { useTranslation } from "next-i18next"; import PageHead from "../../../components/fragment_renderer/PageHead"; @@ -15,12 +16,17 @@ import { getDictionaryTerm } from "../../../lib/utils/getDictionaryTerm"; import { UpdateInfo } from "../../../components/atoms/UpdateInfo"; import { ExploreProjects } from "../../../components/organisms/ExploreProjects"; +// Main component for rendering Integrated Channel Strategy Articles export default function IntegratedChannelStrategyArticles({ key, ...props }) { + // Initialize translations const { t } = useTranslation("common"); + + // Set up state using props data const [pageData] = useState(props.pageData); const [projectData] = useState(props.projectData); const [dictionary] = useState(props.dictionary.items); + // Initialize Adobe Analytics on page load if URL is provided useEffect(() => { if (props.adobeAnalyticsUrl) { window.adobeDataLayer = window.adobeDataLayer || []; @@ -30,6 +36,7 @@ export default function IntegratedChannelStrategyArticles({ key, ...props }) { return ( <> + {/* Main layout wrapper with language and breadcrumb navigation */} <Layout locale={props.locale} langUrl={ @@ -41,9 +48,13 @@ export default function IntegratedChannelStrategyArticles({ key, ...props }) { props.locale )} > + {/* Page header metadata */} <PageHead pageData={pageData} locale={props.locale} /> + + {/* Main content section */} <section className="mb-12"> <div className="layout-container"> + {/* Page title */} <Heading tabIndex="-1" id="pageMainTitle" @@ -51,6 +62,8 @@ export default function IntegratedChannelStrategyArticles({ key, ...props }) { props.locale === "en" ? pageData.scTitleEn : pageData.scTitleFr } /> + + {/* Project and update information */} <UpdateInfo projectLabel={`${getDictionaryTerm( dictionary, @@ -82,7 +95,7 @@ export default function IntegratedChannelStrategyArticles({ key, ...props }) { /> </div> - {/* Main */} + {/* Main content fragments */} <div id="mainContentSection"> <FragmentRender fragments={props.pageData.scFragments} @@ -91,6 +104,8 @@ export default function IntegratedChannelStrategyArticles({ key, ...props }) { /> </div> </section> + + {/* Show updates section if there are related updates */} {filterItems(props.updatesData, pageData.scId).length !== 0 ? ( <ExploreUpdates locale={props.locale} @@ -121,6 +136,8 @@ export default function IntegratedChannelStrategyArticles({ key, ...props }) { } /> ) : null} + + {/* Related projects section */} <ExploreProjects projects={[projectData]} heading={getDictionaryTerm( @@ -135,35 +152,41 @@ export default function IntegratedChannelStrategyArticles({ key, ...props }) { ); } +// Generate static paths for all articles export async function getStaticPaths() { - // Get pages data + // Fetch all article data from AEM const { data } = await aemServiceInstance.getFragment( "integratedChannelStrategyArticlesQuery" ); - // Get paths for dynamic routes from the page name data + + // Generate paths for dynamic routing const paths = getAllUpdateIds(data.sclabsPageV1List.items); - // Remove characters preceding the page name itself i.e. change "/en/projects/oas-benefits-estimator/what-we-learned" to "what-we-learned" + + // Extract the final segment of the URL for the page ID paths.map((path) => (path.params.id = path.params.id.split("/").at(-1))); + return { paths, - fallback: "blocking", + fallback: "blocking", // Show loading state while generating new pages }; } +// Generate static props for each article page export const getStaticProps = async ({ locale, params }) => { - // Get pages data + // Fetch required data from AEM const { data: updatesData } = await aemServiceInstance.getFragment( "integratedChannelStrategyArticlesQuery" ); const { data: projectData } = await aemServiceInstance.getFragment( "integratedChannelStrategyQuery" ); - // get dictionary const { data: dictionary } = await aemServiceInstance.getFragment( "dictionaryQuery" ); + const pages = updatesData.sclabsPageV1List.items; - // Return page data that matches the current page being built + + // Find the matching page data for current URL const pageData = pages.filter((page) => { return ( (locale === "en" ? page.scPageNameEn : page.scPageNameFr) @@ -172,12 +195,14 @@ export const getStaticProps = async ({ locale, params }) => { ); }); + // Return 404 if page not found if (!pageData || !pageData.length) { return { notFound: true, }; } + // Return props for page rendering return { props: { key: params.id, @@ -189,6 +214,6 @@ export const getStaticProps = async ({ locale, params }) => { adobeAnalyticsUrl: process.env.ADOBE_ANALYTICS_URL ?? null, ...(await serverSideTranslations(locale, ["common"])), }, - revalidate: process.env.ISR_ENABLED === "true" ? 10 : false, + revalidate: process.env.ISR_ENABLED === "true" ? 10 : false, // Enable ISR if configured }; }; diff --git a/pages/projects/making-easier-get-benefits/index.js b/pages/projects/making-easier-get-benefits/index.js index e645e98475..780b6c732a 100644 --- a/pages/projects/making-easier-get-benefits/index.js +++ b/pages/projects/making-easier-get-benefits/index.js @@ -17,11 +17,18 @@ import { filterItems } from "../../../lib/utils/filterItems"; import { sortUpdatesByDate } from "../../../lib/utils/sortUpdatesByDate"; import { getDictionaryTerm } from "../../../lib/utils/getDictionaryTerm"; +/** + * Component for displaying an Integrated Channel Strategy page + * Handles bilingual content (English/French) and various project-related information + * @param {Object} props Component properties from getStaticProps + */ export default function IntegratedChannelStrategyPage(props) { + // Initialize state with props data, using array destructuring for read-only values const [pageData] = useState(props.pageData.item); const [updatesData] = useState(props.updatesData); const [allProjects] = useState(props.allProjects); + // Filter dictionary to only include specific status-related terms const [filteredDictionary] = useState( props.dictionary.filter( (item) => @@ -32,6 +39,7 @@ export default function IntegratedChannelStrategyPage(props) { ) ); + // Initialize Adobe Analytics on page load if URL is provided useEffect(() => { if (props.adobeAnalyticsUrl) { window.adobeDataLayer = window.adobeDataLayer || []; @@ -41,6 +49,7 @@ export default function IntegratedChannelStrategyPage(props) { return ( <> + {/* Main layout wrapper with language-specific configuration */} <Layout locale={props.locale} langUrl={ @@ -52,6 +61,7 @@ export default function IntegratedChannelStrategyPage(props) { props.locale )} > + {/* Page head metadata */} <Head> {/* Primary HTML Meta Tags */} <title> @@ -211,10 +221,12 @@ export default function IntegratedChannelStrategyPage(props) { } /> </Head> - + {/* Main content container */} <div className="layout-container mb-24"> <section aria-labelledby="pageMainTitle"> + {/* Grid layout for main content area */} <div className="flex flex-col break-words lg:grid lg:grid-cols-2"> + {/* Page title section */} <div className="col-span-2"> <Heading tabIndex="-1" @@ -226,6 +238,7 @@ export default function IntegratedChannelStrategyPage(props) { } /> </div> + {/* Desktop-only image section */} <div className="hidden lg:grid row-span-2 row-start-2 col-start-2 p-0 mx-4"> <div className="flex justify-center"> <div className="object-fill h-auto w-auto max-w-450px"> @@ -249,12 +262,14 @@ export default function IntegratedChannelStrategyPage(props) { </div> </div> </div> + {/* Introduction paragraph */} <p className="row-start-2 mb-4"> {props.locale === "en" ? pageData.scFragments[0].scContentEn.json[1].content[0].value : pageData.scFragments[0].scContentFr.json[1].content[0] .value} </p> + {/* Project information component */} <div className="row-start-3"> <ProjectInfo locale={props.locale} @@ -311,6 +326,7 @@ export default function IntegratedChannelStrategyPage(props) { </div> </div> </section> + {/* Main content text section */} <div id="pageMainContent" className="grid grid-cols-12"> <div className="col-span-12 lg:col-span-7 mt-[48px]"> <TextRender @@ -324,6 +340,7 @@ export default function IntegratedChannelStrategyPage(props) { </div> </div> </div> + {/* Conditional rendering of updates section */} {props.updatesData.length !== 0 ? ( <ExploreUpdates locale={props.locale} @@ -354,6 +371,7 @@ export default function IntegratedChannelStrategyPage(props) { } /> ) : null} + {/* Related projects section */} <ExploreProjects heading={getDictionaryTerm( props.dictionary, @@ -368,20 +386,27 @@ export default function IntegratedChannelStrategyPage(props) { ); } +/** + * Next.js getStaticProps function to fetch data at build time + * Retrieves page data, dictionary terms, and project information from AEM + * @param {Object} context Contains locale information + * @returns {Object} Props for the page component + */ export const getStaticProps = async ({ locale }) => { - // get page data from AEM + // Fetch page-specific data from AEM const { data: pageData } = await aemServiceInstance.getFragment( "integratedChannelStrategyQuery" ); - // get dictionary + // Fetch dictionary terms for translations const { data: dictionary } = await aemServiceInstance.getFragment( "dictionaryQuery" ); - // get all projects data + // Fetch all projects data const { data: allProjects } = await aemServiceInstance.getFragment( "projectQuery" ); + // Return props object with all necessary data return { props: { locale: locale, @@ -392,6 +417,7 @@ export const getStaticProps = async ({ locale }) => { allProjects: shuffle(allProjects.sclabsPageV1List.items), ...(await serverSideTranslations(locale, ["common"])), }, + // Configure ISR (Incremental Static Regeneration) if enabled revalidate: process.env.ISR_ENABLED === "true" ? 10 : false, }; }; diff --git a/pages/projects/oas-benefits-estimator/[id].js b/pages/projects/oas-benefits-estimator/[id].js index a0b32ecb44..2f60be709e 100644 --- a/pages/projects/oas-benefits-estimator/[id].js +++ b/pages/projects/oas-benefits-estimator/[id].js @@ -1,5 +1,8 @@ +// Import necessary Next.js internationalization utilities import { serverSideTranslations } from "next-i18next/serverSideTranslations"; import { useTranslation } from "next-i18next"; + +// Import components from project structure import PageHead from "../../../components/fragment_renderer/PageHead"; import { Layout } from "../../../components/organisms/Layout"; import { useEffect, useState } from "react"; @@ -15,12 +18,21 @@ import { getDictionaryTerm } from "../../../lib/utils/getDictionaryTerm"; import { UpdateInfo } from "../../../components/atoms/UpdateInfo"; import { ExploreProjects } from "../../../components/organisms/ExploreProjects"; +/** + * Component for displaying OAS Benefits Estimator article pages + * Handles bilingual content (English/French) and displays project updates and related information + * This is a dynamic page that renders different articles based on the URL parameter + */ export default function OASBenefitsEstimatorArticles({ key, ...props }) { + // Initialize translation hook for common terms const { t } = useTranslation("common"); + + // Initialize state with props data, using array destructuring for read-only values const [pageData] = useState(props.pageData); const [projectData] = useState(props.projectData); const [dictionary] = useState(props.dictionary); + // Initialize Adobe Analytics on page load if URL is provided useEffect(() => { if (props.adobeAnalyticsUrl) { window.adobeDataLayer = window.adobeDataLayer || []; @@ -30,6 +42,7 @@ export default function OASBenefitsEstimatorArticles({ key, ...props }) { return ( <> + {/* Main layout wrapper with language-specific configuration */} <Layout locale={props.locale} langUrl={ @@ -41,9 +54,13 @@ export default function OASBenefitsEstimatorArticles({ key, ...props }) { props.locale )} > + {/* Page metadata component */} <PageHead pageData={pageData} locale={props.locale} /> + + {/* Main article section */} <section className="mb-12"> <div className="layout-container"> + {/* Page title */} <Heading tabIndex="-1" id="pageMainTitle" @@ -51,6 +68,8 @@ export default function OASBenefitsEstimatorArticles({ key, ...props }) { props.locale === "en" ? pageData.scTitleEn : pageData.scTitleFr } /> + + {/* Article metadata (project info, posting date, last updated) */} <UpdateInfo projectLabel={`${getDictionaryTerm( dictionary, @@ -82,7 +101,7 @@ export default function OASBenefitsEstimatorArticles({ key, ...props }) { /> </div> - {/* Main */} + {/* Main content area rendering AEM fragments */} <div id="mainContentSection"> <FragmentRender fragments={props.pageData.scFragments} @@ -91,6 +110,8 @@ export default function OASBenefitsEstimatorArticles({ key, ...props }) { /> </div> </section> + + {/* Conditional rendering of updates section if updates exist */} {filterItems(props.updatesData, pageData.scId).length !== 0 ? ( <ExploreUpdates locale={props.locale} @@ -121,6 +142,8 @@ export default function OASBenefitsEstimatorArticles({ key, ...props }) { } /> ) : null} + + {/* Related project exploration section */} <ExploreProjects projects={[projectData]} heading={getDictionaryTerm( @@ -135,35 +158,52 @@ export default function OASBenefitsEstimatorArticles({ key, ...props }) { ); } +/** + * Next.js getStaticPaths function to specify dynamic routes + * Generates paths for all OAS Benefits Estimator articles at build time + * @returns {Object} Contains paths for all article pages and fallback behavior + */ export async function getStaticPaths() { - // Get pages data + // Fetch all OAS Benefits Estimator articles data from AEM const { data } = await aemServiceInstance.getFragment( "oasBenefitsEstimatorArticlesQuery" ); - // Get paths for dynamic routes from the page name data + + // Generate paths for each article const paths = getAllUpdateIds(data.sclabsPageV1List.items); - // Remove characters preceding the page name itself i.e. change "/en/projects/oas-benefits-estimator/what-we-learned" to "what-we-learned" + + // Extract the final segment of the URL for use as the dynamic parameter + // Example: "/en/projects/oas-benefits-estimator/what-we-learned" -> "what-we-learned" paths.map((path) => (path.params.id = path.params.id.split("/").at(-1))); + return { paths, + // Use blocking fallback for server-side rendering of new paths fallback: "blocking", }; } +/** + * Next.js getStaticProps function to fetch data at build time + * Retrieves specific article data, project info, and dictionary terms from AEM + * @param {Object} context Contains locale and URL parameters + * @returns {Object} Props for the page component or notFound flag + */ export const getStaticProps = async ({ locale, params }) => { - // Get pages data + // Fetch all necessary data from AEM const { data: updatesData } = await aemServiceInstance.getFragment( "oasBenefitsEstimatorArticlesQuery" ); const { data: projectData } = await aemServiceInstance.getFragment( "oasBenefitsEstimatorQuery" ); - // get dictionary const { data: dictionary } = await aemServiceInstance.getFragment( "dictionaryQuery" ); + const pages = updatesData.sclabsPageV1List.items; - // Return page data that matches the current page being built + + // Find the specific page data matching the current URL parameter const pageData = pages.filter((page) => { return ( (locale === "en" ? page.scPageNameEn : page.scPageNameFr) @@ -172,12 +212,14 @@ export const getStaticProps = async ({ locale, params }) => { ); }); + // Return 404 if page data isn't found if (!pageData || !pageData.length) { return { notFound: true, }; } + // Return props object with all necessary data return { props: { key: params.id, @@ -189,6 +231,7 @@ export const getStaticProps = async ({ locale, params }) => { adobeAnalyticsUrl: process.env.ADOBE_ANALYTICS_URL ?? null, ...(await serverSideTranslations(locale, ["common"])), }, + // Configure ISR (Incremental Static Regeneration) if enabled revalidate: process.env.ISR_ENABLED === "true" ? 10 : false, }; }; diff --git a/pages/projects/oas-benefits-estimator/index.js b/pages/projects/oas-benefits-estimator/index.js index 0bab6a1b74..ae16b9861b 100644 --- a/pages/projects/oas-benefits-estimator/index.js +++ b/pages/projects/oas-benefits-estimator/index.js @@ -16,11 +16,18 @@ import { filterItems } from "../../../lib/utils/filterItems"; import { sortUpdatesByDate } from "../../../lib/utils/sortUpdatesByDate"; import { getDictionaryTerm } from "../../../lib/utils/getDictionaryTerm"; +/** + * Main component for the OAS Benefits Estimator project page + * Displays project information, an interactive estimator tool, and related content + * Handles bilingual content (English/French) throughout + */ export default function OasBenefitsEstimator(props) { + // Initialize state with props data, using array destructuring for read-only values const [pageData] = useState(props.pageData.item); const [updatesData] = useState(props.updatesData); const [allProjects] = useState(props.allProjects); + // Filter dictionary to only include status-related terms needed for project info const [filteredDictionary] = useState( props.dictionary.filter( (item) => @@ -31,6 +38,7 @@ export default function OasBenefitsEstimator(props) { ) ); + // Initialize Adobe Analytics on page load if URL is provided useEffect(() => { if (props.adobeAnalyticsUrl) { window.adobeDataLayer = window.adobeDataLayer || []; @@ -40,6 +48,7 @@ export default function OasBenefitsEstimator(props) { return ( <> + {/* Main layout wrapper with language-specific configuration */} <Layout locale={props.locale} langUrl={ @@ -211,9 +220,12 @@ export default function OasBenefitsEstimator(props) { /> </Head> + {/* Main content container */} <div className="layout-container mb-24"> + {/* Hero section with title, image, and project info */} <section aria-labelledby="pageMainTitle"> <div className="flex flex-col break-words lg:grid lg:grid-cols-2"> + {/* Page title */} <div className="col-span-2"> <Heading tabIndex="-1" @@ -225,6 +237,7 @@ export default function OasBenefitsEstimator(props) { } /> </div> + {/* Desktop-only feature image */} <div className="hidden lg:grid row-span-2 row-start-2 col-start-2 p-0 mx-4"> <div className="flex justify-center"> <div className="object-fill h-auto w-auto max-w-450px"> @@ -248,12 +261,14 @@ export default function OasBenefitsEstimator(props) { </div> </div> </div> + {/* Introduction paragraph */} <p className="row-start-2 mb-4"> {props.locale === "en" ? pageData.scFragments[0].scContentEn.json[1].content[0].value : pageData.scFragments[0].scContentFr.json[1].content[0] .value} </p> + {/* Project information component */} <div className="row-start-3"> <ProjectInfo locale={props.locale} @@ -310,12 +325,16 @@ export default function OasBenefitsEstimator(props) { </div> </div> </section> + + {/* Estimator tool section with CTA */} <div className="grid grid-cols-12"> + {/* Tool introduction heading */} <h2 className="col-span-12 text-[20px]"> {props.locale === "en" ? pageData.scFragments[0].scContentEn.json[5].content[0].value : pageData.scFragments[0].scContentFr.json[5].content[0].value} </h2> + {/* Primary CTA button to launch estimator tool */} <ActionButton id="try-btn" style="primary" @@ -332,11 +351,14 @@ export default function OasBenefitsEstimator(props) { } ariaExpanded={props.ariaExpanded} /> + + {/* Information section about the tool */} <h2 className="col-span-12"> {props.locale === "en" ? pageData.scFragments[0].scContentEn.json[6].content[0].value : pageData.scFragments[0].scContentFr.json[6].content[0].value} </h2> + {/* Descriptive paragraphs about the tool's functionality */} <p className="col-span-12 xl:col-span-8"> {props.locale === "en" ? pageData.scFragments[0].scContentEn.json[7].content[0].value @@ -353,6 +375,8 @@ export default function OasBenefitsEstimator(props) { : pageData.scFragments[0].scContentFr.json[9].content[0].value} </p> </div> + + {/* Feedback section */} <h2 className="text-[20px]"> {props.locale === "en" ? pageData.scFragments[0].scContentEn.json[10].content[0].value @@ -376,6 +400,8 @@ export default function OasBenefitsEstimator(props) { /> </div> </div> + + {/* Conditional rendering of updates section if updates exist */} {props.updatesData.length !== 0 ? ( <ExploreUpdates locale={props.locale} @@ -406,6 +432,8 @@ export default function OasBenefitsEstimator(props) { } /> ) : null} + + {/* Related projects section */} <ExploreProjects heading={getDictionaryTerm( props.dictionary, @@ -420,20 +448,27 @@ export default function OasBenefitsEstimator(props) { ); } +/** + * Next.js getStaticProps function to fetch data at build time + * Retrieves page data, dictionary terms, and project information from AEM + * @param {Object} context Contains locale information + * @returns {Object} Props for the page component + */ export const getStaticProps = async ({ locale }) => { - // get page data from AEM + // Fetch main page data from AEM const { data: pageData } = await aemServiceInstance.getFragment( "oasBenefitsEstimatorQuery" ); - // get dictionary + // Fetch dictionary terms for translations const { data: dictionary } = await aemServiceInstance.getFragment( "dictionaryQuery" ); - // get all projects data + // Fetch all projects data for the related projects section const { data: allProjects } = await aemServiceInstance.getFragment( "projectQuery" ); + // Return props object with all necessary data return { props: { locale: locale, @@ -444,6 +479,7 @@ export const getStaticProps = async ({ locale }) => { allProjects: shuffle(allProjects.sclabsPageV1List.items), ...(await serverSideTranslations(locale, ["common"])), }, + // Configure ISR (Incremental Static Regeneration) if enabled revalidate: process.env.ISR_ENABLED === "true" ? 10 : false, }; }; diff --git a/pages/projects/transforming-ei-indigenous-peoples/[id].js b/pages/projects/transforming-ei-indigenous-peoples/[id].js index 21aed28612..4d23ae23c9 100644 --- a/pages/projects/transforming-ei-indigenous-peoples/[id].js +++ b/pages/projects/transforming-ei-indigenous-peoples/[id].js @@ -1,17 +1,30 @@ import PageHead from "../../../components/fragment_renderer/PageHead"; -import { serverSideTranslations } from "next-i18next/serverSideTranslations"; import { Layout } from "../../../components/organisms/Layout"; +import { Heading } from "../../../components/molecules/Heading"; +import { serverSideTranslations } from "next-i18next/serverSideTranslations"; import { useEffect, useState } from "react"; import aemServiceInstance from "../../../services/aemServiceInstance"; import { getAllUpdateIds } from "../../../lib/utils/getAllUpdateIds"; import { createBreadcrumbs } from "../../../lib/utils/createBreadcrumbs"; import FragmentRender from "../../../components/fragment_renderer/FragmentRender"; -import { Heading } from "../../../components/molecules/Heading"; +/** + * Component for displaying Indigenous EI (Employment Insurance) related articles + * This is a dynamic page component that renders different articles based on the URL parameter + * Handles bilingual content (English/French) and provides metadata about posting/update dates + * + * Part of the Indigenous EI content series, which provides information about EI services + * specifically tailored for Indigenous communities and their unique needs + */ export default function IndigenousEiArticles({ key, ...props }) { + // Initialize state with props data + // pageData contains the specific article content and metadata const [pageData] = useState(props.pageData); + // dictionary contains translation terms used throughout the application const [dictionary] = useState(props.dictionary.items); + // Initialize Adobe Analytics tracking + // This helps track page views and user interactions for analytics purposes useEffect(() => { if (props.adobeAnalyticsUrl) { window.adobeDataLayer = window.adobeDataLayer || []; @@ -21,20 +34,28 @@ export default function IndigenousEiArticles({ key, ...props }) { return ( <> + {/* Main layout wrapper with language-specific configuration + Handles the overall page structure and navigation elements */} <Layout locale={props.locale} langUrl={ + // Set up language toggle URL based on current locale props.locale === "en" ? pageData.scPageNameFr : pageData.scPageNameEn } dateModifiedOverride={pageData.scDateModifiedOverwrite} + // Generate breadcrumb navigation based on page hierarchy breadcrumbItems={createBreadcrumbs( pageData.scBreadcrumbParentPages, props.locale )} > + {/* Page metadata component for SEO and document structure */} <PageHead pageData={pageData} locale={props.locale} /> + + {/* Main content section containing the article */} <section className="mb-12"> <div className="layout-container"> + {/* Article title */} <Heading tabIndex="-1" id="pageMainTitle" @@ -42,17 +63,25 @@ export default function IndigenousEiArticles({ key, ...props }) { props.locale === "en" ? pageData.scTitleEn : pageData.scTitleFr } /> + + {/* Article metadata grid showing posting and update dates + Uses a responsive grid layout that adjusts based on screen size */} <div id="postedOnUpdatedOnSection" className="grid grid-cols-12"> + {/* "Posted On" label - width adjusts based on language and screen size */} <p className={`col-span-6 sm:col-span-4 ${ + // French labels typically need more space due to longer text props.locale === "en" ? "lg:col-span-2" : "lg:col-span-3" } font-bold`} > {getDictionaryTerm(dictionary, "POSTED-ON", props.locale)} </p> + {/* Posted date display */} <p className="col-span-6 col-start-7 sm:col-start-5 lg:col-span-2 md:col-start-5 mt-0"> {pageData.scDateModifiedOverwrite} </p> + + {/* "Last Updated" label - follows same responsive pattern */} <p className={`row-start-2 col-span-6 sm:col-span-4 mt-0 ${ props.locale === "en" ? "lg:col-span-2" : "lg:col-span-3" @@ -60,13 +89,15 @@ export default function IndigenousEiArticles({ key, ...props }) { > {getDictionaryTerm(dictionary, "LAST-UPDATED", props.locale)} </p> + {/* Last updated date display */} <p className="row-start-2 col-span-6 col-start-7 sm:col-start-5 lg:col-span-2 md:col-start-5 mt-auto"> {pageData.scDateModifiedOverwrite} </p> </div> </div> - {/* Main */} + {/* Main article content section + Renders the AEM content fragments that make up the article body */} <div id="mainContentSection"> <FragmentRender fragments={props.pageData.scFragments} @@ -79,31 +110,58 @@ export default function IndigenousEiArticles({ key, ...props }) { ); } +/** + * Next.js getStaticPaths function to specify dynamic routes + * Generates paths for all Indigenous EI articles at build time + * This function is crucial for static site generation with dynamic routes + * + * @returns {Object} Contains paths for all article pages and fallback behavior + */ export async function getStaticPaths() { - // Get pages data + // Fetch all Indigenous EI articles data from AEM const { data } = await aemServiceInstance.getFragment( "indigenousEiArticlesQuery" ); - // Get paths for dynamic routes from the page name data + + // Generate paths for each article using their unique identifiers const paths = getAllUpdateIds(data.sclabsPageV1List.items); + + // Extract the final segment of the URL for use as the dynamic parameter + // Example: "/en/services/indigenous-ei/article-name" -> "article-name" paths.map((path) => (path.params.id = path.params.id.split("/").at(-1))); + return { paths, + // Use blocking fallback for server-side rendering of new paths + // This ensures SEO-friendly content delivery and proper handling of new articles fallback: "blocking", }; } +/** + * Next.js getStaticProps function to fetch data at build time + * Retrieves specific article data and dictionary terms from AEM + * This function runs at build time for each page in getStaticPaths + * + * @param {Object} context Contains locale and URL parameters + * @returns {Object} Props for the page component or notFound flag + */ export const getStaticProps = async ({ locale, params }) => { - // Get pages data + // Fetch all articles data from AEM content repository const { data } = await aemServiceInstance.getFragment( "indigenousEiArticlesQuery" ); - // get dictionary + + // Fetch dictionary terms for translations + // These terms are used throughout the site for consistent language const { data: dictionary } = await aemServiceInstance.getFragment( "dictionaryQuery" ); + const pages = data.sclabsPageV1List.items; - // Return page data that matches the current page being built + + // Find the specific page data matching the current URL parameter + // Filters the full page list to find the matching article const pageData = pages.filter((page) => { return ( (locale === "en" ? page.scPageNameEn : page.scPageNameFr) @@ -112,12 +170,15 @@ export const getStaticProps = async ({ locale, params }) => { ); }); + // Return 404 if page data isn't found + // This handles cases where an invalid article ID is requested if (!pageData || !pageData.length) { return { notFound: true, }; } + // Return props object with all necessary data for page rendering return { props: { key: params.id, @@ -125,8 +186,11 @@ export const getStaticProps = async ({ locale, params }) => { pageData: pageData[0], dictionary: dictionary.dictionaryV1List, adobeAnalyticsUrl: process.env.ADOBE_ANALYTICS_URL ?? null, + // Include necessary translation namespaces for the page ...(await serverSideTranslations(locale, ["common", "vc"])), }, + // Configure ISR (Incremental Static Regeneration) if enabled + // Allows pages to be regenerated after deployment when content changes revalidate: process.env.ISR_ENABLED === "true" ? 10 : false, }; }; diff --git a/pages/projects/transforming-ei-indigenous-peoples/index.js b/pages/projects/transforming-ei-indigenous-peoples/index.js index d57fa986f0..8a72d04b27 100644 --- a/pages/projects/transforming-ei-indigenous-peoples/index.js +++ b/pages/projects/transforming-ei-indigenous-peoples/index.js @@ -1,26 +1,47 @@ +// Import Next.js core components for head management and image optimization import Head from "next/head"; +import Image from "next/image"; import { serverSideTranslations } from "next-i18next/serverSideTranslations"; -import { Layout } from "../../../components/organisms/Layout"; import { useEffect, useState } from "react"; import aemServiceInstance from "../../../services/aemServiceInstance"; +import { Layout } from "../../../components/organisms/Layout"; import { ProjectInfo } from "../../../components/atoms/ProjectInfo"; -import { createBreadcrumbs } from "../../../lib/utils/createBreadcrumbs"; import { Heading } from "../../../components/molecules/Heading"; -import Image from "next/image"; +import TextRender from "../../../components/text_node_renderer/TextRender"; +import { ExploreUpdates } from "../../../components/organisms/ExploreUpdates"; +import { ExploreProjects } from "../../../components/organisms/ExploreProjects"; +import { createBreadcrumbs } from "../../../lib/utils/createBreadcrumbs"; import stageDictionary from "../../../lib/utils/stageDictionary"; import { sortUpdatesByDate } from "../../../lib/utils/sortUpdatesByDate"; -import TextRender from "../../../components/text_node_renderer/TextRender"; import { getDictionaryTerm } from "../../../lib/utils/getDictionaryTerm"; import { shuffle } from "../../../lib/utils/shuffle"; -import { ExploreUpdates } from "../../../components/organisms/ExploreUpdates"; import { filterItems } from "../../../lib/utils/filterItems"; -import { ExploreProjects } from "../../../components/organisms/ExploreProjects"; +/** + * Component for the EI (Employment Insurance) Indigenous Overview page + * Serves as the main landing page for Indigenous-specific EI information + * + * This component presents comprehensive information about EI services and programs + * specifically designed for Indigenous communities, including: + * - Project overview and current status + * - Key information and updates + * - Related projects and resources + * - Bilingual content support (English/French) + */ export default function EiIndigenousOverview(props) { + // Initialize primary page data state + // Contains main content, metadata, and project information from AEM const [pageData] = useState(props.pageData.item); + + // Sort updates by date for chronological display + // Updates don't need state as they don't change after initial sort const updatesData = sortUpdatesByDate(props.updatesData); + + // Initialize state for all related projects const [allProjects] = useState(props.allProjects); + // Filter dictionary to only include status-related terms + // These terms are used in the ProjectInfo component to display project status const [filteredDictionary] = useState( props.dictionary.items.filter( (item) => @@ -31,6 +52,8 @@ export default function EiIndigenousOverview(props) { ) ); + // Initialize Adobe Analytics tracking + // Tracks page views and user interactions for analytics reporting useEffect(() => { if (props.adobeAnalyticsUrl) { window.adobeDataLayer = window.adobeDataLayer || []; @@ -40,6 +63,8 @@ export default function EiIndigenousOverview(props) { return ( <> + {/* Main layout wrapper with language-specific configuration + Provides consistent site structure and navigation */} <Layout locale={props.locale} langUrl={ @@ -215,9 +240,12 @@ export default function EiIndigenousOverview(props) { /> </Head> + {/* Main content container with hero section */} <div className="layout-container"> <section aria-labelledby="pageMainTitle"> + {/* Two-column grid layout for desktop view */} <div className="flex flex-col break-words lg:grid lg:grid-cols-2"> + {/* Page title spanning both columns */} <div className="col-span-2"> <Heading tabIndex="-1" @@ -229,6 +257,8 @@ export default function EiIndigenousOverview(props) { } /> </div> + + {/* Feature image - hidden on mobile, shown on desktop */} <div className="hidden lg:grid row-span-2 row-start-2 col-start-2 p-0 mx-4"> <div className="flex justify-center"> <div className="object-fill max-w-350px"> @@ -245,19 +275,23 @@ export default function EiIndigenousOverview(props) { } width={pageData.scFragments[2].scImageEn.width} height={pageData.scFragments[2].scImageEn.height} - priority + priority // Load image with high priority for above-the-fold content sizes="33vw" quality={100} /> </div> </div> </div> + + {/* Introduction paragraph */} <p className="row-start-2 font-body text-lg mb-4"> {props.locale === "en" ? pageData.scFragments[0].scContentEn.json[1].content[0].value : pageData.scFragments[0].scContentFr.json[1].content[0] .value} </p> + + {/* Project information component displaying key project details */} <div className="row-start-3"> <ProjectInfo locale={props.locale} @@ -314,6 +348,8 @@ export default function EiIndigenousOverview(props) { </section> </div> + {/* Main content text section + Uses grid layout for responsive column sizing */} <div className="layout-container mt-[48px] mb-24 grid grid-cols-12"> <div className="col-span-12 lg:col-span-7"> <TextRender @@ -325,6 +361,8 @@ export default function EiIndigenousOverview(props) { /> </div> </div> + + {/* Conditional rendering of updates section if updates exist */} {props.updatesData.length !== 0 ? ( <ExploreUpdates locale={props.locale} @@ -355,6 +393,9 @@ export default function EiIndigenousOverview(props) { } /> ) : null} + + {/* Related projects section + Shows up to 3 related projects, excluding the current project */} <ExploreProjects heading={getDictionaryTerm( props.dictionary.items, @@ -369,20 +410,30 @@ export default function EiIndigenousOverview(props) { ); } +/** + * Next.js getStaticProps function to fetch data at build time + * Retrieves all necessary page data from AEM content repository + * + * @param {Object} context Contains locale information for internationalization + * @returns {Object} Props for the page component including all required data + */ export const getStaticProps = async ({ locale }) => { - // get page data from AEM + // Fetch main page content and structure from AEM const { data: pageData } = await aemServiceInstance.getFragment( "indigenousEiQuery" ); - // get dictionary + + // Fetch dictionary terms for consistent translations across the site const { data: dictionary } = await aemServiceInstance.getFragment( "dictionaryQuery" ); - // get all projects data + + // Fetch all projects data for the related projects section const { data: allProjects } = await aemServiceInstance.getFragment( "projectQuery" ); + // Return props object with all necessary data and configuration return { props: { locale: locale, @@ -391,8 +442,10 @@ export const getStaticProps = async ({ locale }) => { updatesData: pageData.sclabsPageV1ByPath.item.scLabProjectUpdates, dictionary: dictionary.dictionaryV1List, allProjects: shuffle(allProjects.sclabsPageV1List.items), + // Include necessary translation configuration ...(await serverSideTranslations(locale, ["common"])), }, + // Configure ISR (Incremental Static Regeneration) if enabled revalidate: process.env.ISR_ENABLED === "true" ? 10 : false, }; }; diff --git a/pages/updates.js b/pages/updates.js index 4d8d2a0302..666fbb1daf 100644 --- a/pages/updates.js +++ b/pages/updates.js @@ -1,3 +1,4 @@ +// Import required dependencies for internationalization and routing import { serverSideTranslations } from "next-i18next/serverSideTranslations"; import { Layout } from "../components/organisms/Layout"; import { useEffect, useState } from "react"; @@ -9,15 +10,28 @@ import { createBreadcrumbs } from "../lib/utils/createBreadcrumbs"; import { getDictionaryTerm } from "../lib/utils/getDictionaryTerm"; import { useRouter } from "next/router"; +/** + * Updates Page Component + * Displays a filterable list of project updates + * Supports bilingual content and project-based filtering + */ export default function UpdatesPage(props) { - const router = useRouter(); - const pageData = props.pageData?.item; - const updatesData = props.updatesData; - const dictionary = props.dictionary; + const router = useRouter(); // Next.js router for query params + const pageData = props.pageData?.item; // Page content from AEM + const updatesData = props.updatesData; // Updates data for all projects + const dictionary = props.dictionary; // Translation dictionary + // State for managing selected filter options const [selectedOptions, setSelectedOptions] = useState([]); + /** + * Extracts unique project IDs and creates select options for filtering + * @param {Array} arr - Array of update objects + * @returns {Array} Array of formatted options for MultiSelectField + */ const getSelectOptionsFromUpdateData = (arr) => { + // Use Set to track unique project IDs const seen = new Set(); + // Reduce array to unique project entries let reducedArray = arr.reduce((acc, obj) => { if (!seen.has(obj.scLabProject.scId)) { seen.add(obj.scLabProject.scId); @@ -25,6 +39,7 @@ export default function UpdatesPage(props) { } return acc; }, []); + // Format options for the MultiSelectField component with bilingual labels let optionsArray = reducedArray.map((option) => { return { id: option.scLabProject.scId, @@ -38,14 +53,27 @@ export default function UpdatesPage(props) { return optionsArray; }; + /** + * Filters updates based on selected project options + * @param {Array} updates - Array of all updates + * @param {Array} selectedOptions - Array of selected filter options + * @returns {Array} Filtered updates array + */ const filterUpdates = (updates, selectedOptions) => { + // If no filters selected, return all updates if (selectedOptions.length === 0) return updates; + // Create Set of selected project IDs for efficient lookup const selectedIds = new Set(selectedOptions.map((option) => option.id)); + // Return updates matching selected projects return updates.filter((update) => selectedIds.has(update.scLabProject.scId) ); }; + /** + * Maps filtered and sorted updates to Card components + * Includes project name and posting date for each update + */ const updatesCards = filterUpdates( sortUpdatesByDate(updatesData), selectedOptions @@ -55,10 +83,12 @@ export default function UpdatesPage(props) { <Card customStyling="py-8 border-x-0 border-t-0 border-b-2 !shadow-none rounded-none" cardHeadingStyling="!text-mobileh2 lg:!text-h2l pl-0 no-underline !pt-0 !mt-0" + // Bilingual title and link handling title={props.locale === "en" ? update.scTitleEn : update.scTitleFr} href={ props.locale === "en" ? update.scPageNameEn : update.scPageNameFr } + // Custom HTML description with project name and date htmlDesc={ <div className="flex flex-col lg:pt-5"> <span className="flex flex-row"> @@ -84,15 +114,24 @@ export default function UpdatesPage(props) { ); }); + /** + * Effect to handle Adobe Analytics and URL query parameters + * Sets initial filter based on URL query if present + */ useEffect(() => { + // Initialize Adobe Analytics if configured if (props.adobeAnalyticsUrl) { window.adobeDataLayer = window.adobeDataLayer || []; window.adobeDataLayer.push({ event: "pageLoad" }); } + // Get available filter options const options = getSelectOptionsFromUpdateData(updatesData); + // Handle URL query parameters once router is ready if (router.isReady) { + // Get project query parameter based on language const routerQuery = props.locale === "en" ? router.query.project : router.query.projet; + // Find matching options for the query parameter const selectedOptionsFromQueryString = options.filter( (option) => option.label === routerQuery ); @@ -100,7 +139,7 @@ export default function UpdatesPage(props) { "Selected options from query string:", selectedOptionsFromQueryString ); - selectedOptionsFromQueryString; + // Set initial filter state based on URL query setSelectedOptions(selectedOptionsFromQueryString); } }, [router.isReady, setSelectedOptions]); @@ -109,16 +148,20 @@ export default function UpdatesPage(props) { <> <Layout locale={props.locale} + // Alternate language URL for language toggle langUrl={ props.locale === "en" ? pageData.scPageNameFr : pageData.scPageNameEn } dateModifiedOverride={pageData.scDateModifiedOverwrite} + // Generate breadcrumb navigation breadcrumbItems={createBreadcrumbs( pageData.scBreadcrumbParentPages, props.locale )} > <PageHead locale={props.locale} pageData={pageData} /> + + {/* Hero section with background image */} <div id="pageMainTitle" className="flex flex-col justify-center content-center mt-16 h-[182px] bg-multi-blue-blue70 bg-no-repeat sm:bg-right-bottom" @@ -128,6 +171,7 @@ export default function UpdatesPage(props) { }} > <div className="layout-container text-white"> + {/* Bilingual page title and description */} <h1 className="m-0"> {props.locale === "en" ? pageData.scFragments[0].scContentEn.json[0].content[0].value @@ -141,7 +185,9 @@ export default function UpdatesPage(props) { </p> </div> </div> + <div className="layout-container"> + {/* Project filter dropdown */} <div className="my-12 max-w-[350px]"> <MultiSelectField label={getDictionaryTerm(dictionary, "FILTER-BY", props.locale)} @@ -152,6 +198,7 @@ export default function UpdatesPage(props) { selectedOptions={selectedOptions} /> </div> + {/* Updates list */} <div className="grid grid-cols-12"> <ul className="col-span-12 xl:col-span-8">{updatesCards}</ul> </div> @@ -161,20 +208,28 @@ export default function UpdatesPage(props) { ); } +/** + * Next.js Static Site Generation (SSG) function + * Fetches all required data at build time + * Enables Incremental Static Regeneration (ISR) if configured + */ export const getStaticProps = async ({ locale }) => { - // Get page data + // Fetch main page content from AEM const { data: pageData } = await fetch( `${process.env.AEM_BASE_URL}/getSclUpdatesV1` ).then((res) => res.json()); - // Get updates data + + // Fetch all updates data const { data: updatesData } = await fetch( `${process.env.AEM_BASE_URL}/getSclAllUpdatesV1` ).then((res) => res.json()); - // get dictionary + + // Fetch translation dictionary const { data: dictionary } = await fetch( `${process.env.AEM_BASE_URL}/getSclDictionaryV1` ).then((res) => res.json()); + // Return props for page rendering return { props: { locale: locale, @@ -182,8 +237,10 @@ export const getStaticProps = async ({ locale }) => { pageData: pageData.sclabsPageV1ByPath, updatesData: updatesData.sclabsPageV1List.items, dictionary: dictionary.dictionaryV1List.items, + // Include translations for common terms and multiSelect component ...(await serverSideTranslations(locale, ["common", "multiSelect"])), }, + // Enable ISR if configured in environment revalidate: process.env.ISR_ENABLED === "true" ? 10 : false, }; };