From 628d8495400e13aefc782c2520d736ec92174d50 Mon Sep 17 00:00:00 2001 From: Aaron Date: Wed, 6 Nov 2024 15:59:46 -0500 Subject: [PATCH] Preloaded and Angular Routed This is a solution to using the Angular Router to avoid client side requests --- .gitignore | 11 +- dotenv.config.js | 4 +- package.json | 8 +- prerender-content.ts | 48 +++++++ prerender-pages.ts | 63 +++++++++ .../pages/components/components.component.ts | 11 +- .../featured-post/featured-post.component.ts | 12 ++ .../posts-listing/posts-listing.component.ts | 21 ++- .../textblockwithimage.component.html | 2 +- src/app/agility/pages/pages.component.html | 4 +- src/app/agility/pages/pages.component.ts | 121 ++++++++++++------ .../site-header/site-header.component.html | 2 +- 12 files changed, 250 insertions(+), 57 deletions(-) create mode 100644 prerender-content.ts create mode 100644 prerender-pages.ts diff --git a/.gitignore b/.gitignore index d080675..bf5a1a1 100644 --- a/.gitignore +++ b/.gitignore @@ -6,7 +6,8 @@ /out-tsc /.angular /bazel-out -prerender-routes.js + +# environment files /src/environments/environment.prod.ts /src/environments/environment.ts /src/environments @@ -14,6 +15,14 @@ content.json pages.json agility-routes.txt +#agility prerendering +prerender-routes.js +prerender-pages.js +prerender-content.js +agility-routes.txt +pages.json +content.json + # dependencies /node_modules diff --git a/dotenv.config.js b/dotenv.config.js index 9b1227a..f977ab0 100644 --- a/dotenv.config.js +++ b/dotenv.config.js @@ -4,7 +4,7 @@ const path = require('path'); const development = ` export const environment = { - AGILITY_PREVIEW: true, + AGILITY_PREVIEW: '${process.env.AGILITY_PREVIEW}', AGILITY_GUID: '${process.env.AGILITY_GUID}', AGILITY_API_FETCH_KEY: '${process.env.AGILITY_API_FETCH_KEY}', AGILITY_API_PREVIEW_KEY: '${process.env.AGILITY_API_PREVIEW_KEY}', @@ -14,7 +14,7 @@ export const environment = { `; const production = ` export const environment = { - AGILITY_PREVIEW: false, + AGILITY_PREVIEW: '${process.env.AGILITY_PREVIEW}', AGILITY_GUID: '${process.env.AGILITY_GUID}', AGILITY_API_FETCH_KEY: '${process.env.AGILITY_API_FETCH_KEY}', AGILITY_API_PREVIEW_KEY: '${process.env.AGILITY_API_PREVIEW_KEY}', diff --git a/package.json b/package.json index 11b13c4..cdb38e0 100644 --- a/package.json +++ b/package.json @@ -6,9 +6,13 @@ "test": "ng test", "envinit": "node dotenv.config.js", "dev": "npm run envinit && ng serve", - "build": "npm run envinit && npm run prerender:routes && ng build --configuration production", + "build": "npm run agility-init && ng build --configuration production", "start": "ng serve --configuration production", - "prerender:routes": "tsc prerender-routes.ts && node prerender-routes.js" + + "agility-init": "npm run envinit && npm run prerender:routes && npm run prerender:pages && npm run prerender:content", + "prerender:routes": "tsc prerender-routes.ts && node prerender-routes.js", + "prerender:pages": "tsc prerender-pages.ts && node prerender-pages.js", + "prerender:content": "tsc prerender-content.ts && node prerender-content.js" }, "private": true, "dependencies": { diff --git a/prerender-content.ts b/prerender-content.ts new file mode 100644 index 0000000..c926fc6 --- /dev/null +++ b/prerender-content.ts @@ -0,0 +1,48 @@ +import { getApi } from '@agility/content-fetch'; +import * as fs from 'fs'; +import * as path from 'path'; +import * as dotenv from 'dotenv'; + +dotenv.config(); + + +const client = getApi({ + guid: process.env['AGILITY_GUID'], + apiKey: process.env['AGILITY_API_FETCH_KEY'], + isPreview: Boolean(process.env['AGILITY_PREVIEW']), +}); + +const contentListsToPreRender = ['posts', 'categories']; + +async function prerenderPages() { + + let allContentListsData: { [key: string]: any } = {}; + + for(const contentList of contentListsToPreRender) { + + // Fetch the content list + const list = await client.getContentList({ + referenceName: contentList, + languageCode: process.env['AGILITY_LOCALE'], + locale: process.env['AGILITY_LOCALE'], + }); + + // Add the list to the allContentListsData object + allContentListsData[contentList] = list; + + console.log(`Fetched content list: ${contentList}`); + } + + // Define the JSON output path for the aggregated data + const aggregatedJsonOutputPath = path.join(__dirname, 'src', 'app', 'agility', 'data', 'content.json'); + + // Ensure the directory exists + fs.mkdirSync(path.dirname(aggregatedJsonOutputPath), { recursive: true }); + + // Write the aggregated data to the JSON file + fs.writeFileSync(aggregatedJsonOutputPath, JSON.stringify(allContentListsData, null, 2), 'utf8'); + + +} + +prerenderPages(); diff --git a/prerender-pages.ts b/prerender-pages.ts new file mode 100644 index 0000000..4ba8354 --- /dev/null +++ b/prerender-pages.ts @@ -0,0 +1,63 @@ +import { getApi } from '@agility/content-fetch'; +import * as fs from 'fs'; +import * as path from 'path'; +import * as dotenv from 'dotenv'; + +dotenv.config(); + +const apiKey = process.env['AGILITY_API_FETCH_KEY']; +const guid = process.env['AGILITY_GUID']; +const locale = process.env['AGILITY_LOCALE'] || 'en-use'; +const channel = process.env['AGILITY_WEBSITE'] || 'website'; + +const client = getApi({ + guid, + apiKey, + isPreview: false +}); + +async function prerenderPages() { + // Fetch the sitemap + const sitemap = await client.getSitemapFlat({ languageCode: locale, channelName: channel, locale }); + + // Object to hold all page data + let allPageData: { [key: string]: { page: any, dynamicPageItem: any } } = {}; + + for (const pagePath in sitemap) { + if (sitemap.hasOwnProperty(pagePath)) { + const pageInSitemap = sitemap[pagePath]; + + // Fetch the page content + const pageContent = await client.getPageByPath({ + pagePath, + channelName: channel, + locale + }); + + // Fetch the dynamic page item + let dynamicPageItem = null; + if(pageInSitemap.contentID) { + dynamicPageItem = await client.getContentItem({ + contentID: pageInSitemap.contentID, + locale + }); + } + // Add the page content to the allPageData object + allPageData[pagePath] = { + page: pageContent.page, + dynamicPageItem: dynamicPageItem + }; + } + } + + // Define the JSON output path for the aggregated data + const jsonOutputPath = path.join(__dirname, 'src', 'app', 'agility', 'data', 'pages.json'); + + // Ensure the directory exists + fs.mkdirSync(path.dirname(jsonOutputPath), { recursive: true }); + + // Write the aggregated data to the JSON file + fs.writeFileSync(jsonOutputPath, JSON.stringify(allPageData, null, 2), 'utf8'); +} + +prerenderPages(); \ No newline at end of file diff --git a/src/app/agility/pages/components/components.component.ts b/src/app/agility/pages/components/components.component.ts index ab9a19c..e6a4c2a 100644 --- a/src/app/agility/pages/components/components.component.ts +++ b/src/app/agility/pages/components/components.component.ts @@ -2,7 +2,6 @@ import { Component, Input, OnInit, ViewChild } from '@angular/core'; import { AgilityComponentsDirective } from "./components.directive" import { AgilityComponentsService } from './components.service'; - @Component({ selector: 'agility-component', standalone: true, @@ -33,9 +32,9 @@ export class AgilityComponents implements OnInit { loadComponent() { //get the module name - let moduleName = this.moduleObj?.value.module; - + let moduleName = this.moduleObj?.module; + let moduleType = this.agilityComponentsService.getComponent(moduleName) as any; if (!moduleType) { console.warn(`No module found for ${moduleName}`); @@ -53,14 +52,14 @@ export class AgilityComponents implements OnInit { // Ensure the item property is set if (componentRef.instance?.hasOwnProperty('item')) { - (componentRef.instance as any).item = this.moduleObj?.value.item; + (componentRef.instance as any).item = this.moduleObj?.item; } // Ensure the data property is set if (componentRef.instance?.hasOwnProperty('data')) { (componentRef.instance as any).data = { - item: this.moduleObj?.value.item, - image: this.moduleObj?.value.image, + item: this.moduleObj?.item, + image: this.moduleObj?.item.image, page: this.page, dynamicPageItem: this.dynamicPageItem }; diff --git a/src/app/agility/pages/components/featured-post/featured-post.component.ts b/src/app/agility/pages/components/featured-post/featured-post.component.ts index 431332b..b681023 100644 --- a/src/app/agility/pages/components/featured-post/featured-post.component.ts +++ b/src/app/agility/pages/components/featured-post/featured-post.component.ts @@ -3,6 +3,7 @@ import { AgilityService } from '../../../agility.service'; import { Router, RouterLink } from '@angular/router'; import { NgIf } from '@angular/common'; import { firstValueFrom } from 'rxjs'; +import PrerenderedAgilityContentLists from '../../../data/content.json' function decodeHTML(str: string): string { return str.replace(/&#(\d+);/g, (_, dec) => String.fromCharCode(dec)); @@ -39,6 +40,17 @@ export class ModuleFeaturedPost implements OnInit { } async ngOnInit(): Promise { + + + const categoriesData = PrerenderedAgilityContentLists['categories']; + if(categoriesData){ + this.state.set(CATEGORIES_KEY, categoriesData as any); + } + + + + + try { let categoriesRes = this.state.get(CATEGORIES_KEY, null as any); diff --git a/src/app/agility/pages/components/posts-listing/posts-listing.component.ts b/src/app/agility/pages/components/posts-listing/posts-listing.component.ts index 71acc86..bc481e4 100644 --- a/src/app/agility/pages/components/posts-listing/posts-listing.component.ts +++ b/src/app/agility/pages/components/posts-listing/posts-listing.component.ts @@ -3,6 +3,7 @@ import { AgilityService } from '../../../agility.service'; import { htmlDecode } from 'js-htmlencode'; import { firstValueFrom } from 'rxjs'; import { NgForOf, NgIf } from '@angular/common'; +import PrerenderedAgilityContentLists from '../../../data/content.json' const POSTS_KEY = makeStateKey('posts'); @@ -26,9 +27,25 @@ export class ModulePostsListingComponent implements OnInit { async ngOnInit(): Promise { this.moduleData = this.data.item.fields; - // Check if the posts data is already available in the transfer state - this.posts = this.transferState.get(POSTS_KEY, null as any); + const postData = PrerenderedAgilityContentLists['posts']; + if(postData){ + this.transferState.set(POSTS_KEY, postData as any); + } + + // set the posts to the transfer state + this.posts = this.transferState.get(POSTS_KEY, null as any).items.map((p: any) => { + return { + title: p.fields.title, + slug: p.fields.slug, + date: new Date(p.fields.date).toLocaleDateString(), + image: p.fields.image, + content: p.fields.content, + category: p.fields.category.fields.title || 'Uncategorized' + }; + }) + + // fallback to API if no data in transfer state if (!this.posts) { const postsRes = await firstValueFrom(this.agilityService.getContentList('posts')); diff --git a/src/app/agility/pages/components/text-block-with-image/textblockwithimage.component.html b/src/app/agility/pages/components/text-block-with-image/textblockwithimage.component.html index 1e52dcf..b4e6c51 100644 --- a/src/app/agility/pages/components/text-block-with-image/textblockwithimage.component.html +++ b/src/app/agility/pages/components/text-block-with-image/textblockwithimage.component.html @@ -149,7 +149,7 @@

Page not found. Error loading page. - + - + diff --git a/src/app/agility/pages/pages.component.ts b/src/app/agility/pages/pages.component.ts index a283c20..a52ed49 100644 --- a/src/app/agility/pages/pages.component.ts +++ b/src/app/agility/pages/pages.component.ts @@ -1,4 +1,5 @@ import { isPlatformServer, isPlatformBrowser, JsonPipe, NgIf, NgFor, KeyValuePipe } from '@angular/common'; +import agilityPagesData from '../data/pages.json'; import { Component, Inject, makeStateKey, OnInit, PLATFORM_ID, TransferState, ViewChild, OnDestroy } from '@angular/core'; import { firstValueFrom, Subscription } from 'rxjs'; import { AgilityService } from '../agility.service'; @@ -12,6 +13,40 @@ import { AgilityComponents } from "./components/components.component"; import { isDevMode } from '@angular/core'; + +// Define the structure of a module item +interface ModuleItem { + module: string; + item: { + contentID: number; + properties: { + state: number; + modified: string; + versionID: number; + referenceName: string; + definitionName: string; + itemOrder: number; + }; + fields: { + textblob: string; + }; + seo: any; + }; +} + +// Define the structure of zones +interface Zones { + [key: string]: ModuleItem[]; +} + +// Define the structure of the page +interface Page { + zones: Zones; + title?: string; + contentID?: number | null | undefined; + // Add other properties of the page if necessary +} + @Component({ selector: 'agility-page', standalone: true, @@ -22,7 +57,7 @@ import { isDevMode } from '@angular/core'; export class PageComponent implements OnInit, OnDestroy { @ViewChild(AgilityComponentsDirective, { static: true }) agilityComponentHost!: AgilityComponentsDirective; - public page: { zones: any[] } | null = null; + public page: Page | null = null; public pageStatus: number = 0; public title = 'AgilityCMS Angular SSR Starter'; public isServer: boolean = false; @@ -48,19 +83,22 @@ export class PageComponent implements OnInit, OnDestroy { ngOnInit() { - if (this.isServer) { - this.makeApiRequest(); + if(this.isServer){ + // this pulls data from /data/agility-pages.json and sets it into TrasnferState + this.preloadTransferStateData(); } + // this.loadPage(); + this.routerSubscription = this.router.events.subscribe((event) => { if (event instanceof NavigationEnd) { - this.makeApiRequest(); + this.loadPage(); } }); this.previewModeSubscription = this.agilityService.previewModeChange.subscribe(() => { if (isPlatformBrowser(this.platformId)) { - this.makeApiRequest(); + this.loadPage(); } }); } @@ -74,54 +112,57 @@ export class PageComponent implements OnInit, OnDestroy { } } + async preloadTransferStateData() { + let pagePath = this.location.path().split('?')[0] || '/home'; + if (pagePath === '') pagePath = '/home'; + for (const key in agilityPagesData) { + const pageKey = makeStateKey(key); + this.transferState.set(pageKey, (agilityPagesData as any)[key]); + } + } + + + async loadPage() { - async makeApiRequest() { let currentPath = this.location.path().split('?')[0] || '/home'; if (currentPath === '' || currentPath === '/favicon.png') currentPath = '/home'; - let pageKey = makeStateKey('page' + currentPath.replaceAll('/', '-')); - let sitemapKey = makeStateKey('page-sitemap'); - let dynamicPageItemKey = makeStateKey('dynamicPageItem' + currentPath.replaceAll('/', '-')); + let pageKey = makeStateKey(currentPath); - try { - let sitemap = this.transferState.get(sitemapKey, null); - if (!sitemap) { - sitemap = await firstValueFrom(this.agilityService.getSitemapFlat()); - } - const pageInSitemap = sitemap[currentPath]; - if (!pageInSitemap) { - this.pageStatus = 404; - return; - } + const pageData = this.transferState.get(pageKey, null).page; + const dynamicPageItem = this.transferState.get(pageKey, null).dynamicPageItem; - if (pageInSitemap.contentID) { - this.dynamicPageItem = this.transferState.get(dynamicPageItemKey, null); - this.transferState.remove(dynamicPageItemKey); - if (!this.dynamicPageItem) { - this.dynamicPageItem = await firstValueFrom(this.agilityService.getContentItem(pageInSitemap.contentID)); - } - } + if(pageData){ - this.page = this.transferState.get(pageKey, null); - this.transferState.remove(pageKey); - if (!this.page) { - this.page = await firstValueFrom(this.agilityService.getPage(pageInSitemap.pageID)); - } + this.page = pageData + this.dynamicPageItem = dynamicPageItem - this.titleService.setTitle(pageInSitemap.title); - this.pageStatus = 200; + if(!this.page){ + this.page = await firstValueFrom(this.agilityService.getPage(pageData.pageID)); + if(this.page){ + this.transferState.set(pageKey, this.page); + } else { + this.pageStatus = 404; + return; + } + // if there's a contentID set for the page, get the dynamic page item + if (this.page?.contentID !== undefined && this.page?.contentID !== null) { + this.dynamicPageItem = await firstValueFrom(this.agilityService.getContentItem(this.page.contentID)); + } + + this.transferState.set(pageKey, {page: this.page, dynamicPageItem: this.dynamicPageItem}); - if (this.isServer) { - this.transferState.set(sitemapKey, sitemap); - this.transferState.set(dynamicPageItemKey, this.dynamicPageItem); - this.transferState.set(pageKey, this.page); } - } catch (error) { - console.error('Error making API request', error); - this.pageStatus = 500; + this.titleService.setTitle(this.page?.title || 'AgilityCMS Angular SSR Starter'); + this.pageStatus = 200; + + } else { + + console.log('No page data found in transfer state') } + } } \ No newline at end of file diff --git a/src/app/components/site-header/site-header.component.html b/src/app/components/site-header/site-header.component.html index 716b896..6157f79 100644 --- a/src/app/components/site-header/site-header.component.html +++ b/src/app/components/site-header/site-header.component.html @@ -95,7 +95,7 @@