diff --git a/fe/Dockerfile b/fe/Dockerfile index 5f4ab62..efc5595 100644 --- a/fe/Dockerfile +++ b/fe/Dockerfile @@ -52,6 +52,7 @@ COPY --from=builder /app/public ./public COPY --from=builder /app/.next/standalone ./ COPY --from=builder /app/.next/static ./.next/static COPY --from=builder /app/.env.production ./ +COPY --from=builder /app/scripts-dist ./scripts-dist # Set proper ownership RUN chown -R nextjs:nodejs /app @@ -63,4 +64,4 @@ USER nextjs EXPOSE 3000 # Set the command to run the application -CMD ["node", "server.js"] +CMD ["sh", "-c", "node scripts-dist/preload-static-data.js && node server.js"] diff --git a/fe/scripts-dist/preload-static-data.js b/fe/scripts-dist/preload-static-data.js new file mode 100644 index 0000000..1865afd --- /dev/null +++ b/fe/scripts-dist/preload-static-data.js @@ -0,0 +1,29 @@ +const { businessApi } = require('../src/lib/api-client'); +const { Language } = require('../src/proto/api'); +const fs = require('fs/promises'); +const path = require('path'); + +async function main() { + try { + const [langs, collections, refs] = await Promise.all([ + businessApi.getAllLanguages(), + businessApi.getAllCollections(Language.LANGUAGE_ENGLISH), + businessApi.getAllReferenceTypes(), + ]); + + const data = { + languages: langs.languages || [], + collections: { [Language.LANGUAGE_ENGLISH]: collections.collections || [] }, + referenceTypes: refs.referenceTypes || [], + }; + + const outPath = process.env.STATIC_DATA_PATH || path.join(process.cwd(), 'preloaded-static-data.json'); + await fs.writeFile(outPath, JSON.stringify(data)); + console.log('Static data written to', outPath); + } catch (err) { + console.error('Failed to preload static data', err); + process.exit(1); + } +} + +main(); diff --git a/fe/scripts/preload-static-data.ts b/fe/scripts/preload-static-data.ts new file mode 100644 index 0000000..e7e52d3 --- /dev/null +++ b/fe/scripts/preload-static-data.ts @@ -0,0 +1,29 @@ +import { businessApi } from '../src/lib/api-client'; +import { Language } from '../src/proto/api'; +import fs from 'fs/promises'; +import path from 'path'; + +async function main() { + try { + const [langs, collections, refs] = await Promise.all([ + businessApi.getAllLanguages(), + businessApi.getAllCollections(Language.LANGUAGE_ENGLISH), + businessApi.getAllReferenceTypes(), + ]); + + const data = { + languages: langs.languages ?? [], + collections: { [Language.LANGUAGE_ENGLISH]: collections.collections ?? [] }, + referenceTypes: refs.referenceTypes ?? [], + }; + + const outPath = process.env.STATIC_DATA_PATH || path.join(process.cwd(), 'preloaded-static-data.json'); + await fs.writeFile(outPath, JSON.stringify(data)); + console.log('Static data written to', outPath); + } catch (err) { + console.error('Failed to preload static data', err); + process.exit(1); + } +} + +main(); diff --git a/fe/scripts/tsconfig.preload.json b/fe/scripts/tsconfig.preload.json new file mode 100644 index 0000000..1518b7c --- /dev/null +++ b/fe/scripts/tsconfig.preload.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "commonjs", + "esModuleInterop": true, + "skipLibCheck": true, + "outDir": "../scripts-dist", + "lib": ["es2020"] + }, + "include": ["preload-static-data.ts"] +} diff --git a/fe/src/app/collections/[collectionId]/[bookId]/[hadithId]/page.tsx b/fe/src/app/collections/[collectionId]/[bookId]/[hadithId]/page.tsx index cd06b7e..964edb1 100644 --- a/fe/src/app/collections/[collectionId]/[bookId]/[hadithId]/page.tsx +++ b/fe/src/app/collections/[collectionId]/[bookId]/[hadithId]/page.tsx @@ -16,6 +16,8 @@ import { HadithCard } from "fe/components/hadith-card"; import { StructuredData } from "fe/components/structured-data"; import { generateHadithStructuredData, generateBreadcrumbStructuredData } from "fe/lib/seo-utils"; +export const dynamic = 'force-dynamic'; + interface HadithParams { collectionId: string; bookId: string; diff --git a/fe/src/app/collections/[collectionId]/[bookId]/page.tsx b/fe/src/app/collections/[collectionId]/[bookId]/page.tsx index f123004..6e8e16f 100644 --- a/fe/src/app/collections/[collectionId]/[bookId]/page.tsx +++ b/fe/src/app/collections/[collectionId]/[bookId]/page.tsx @@ -18,6 +18,8 @@ import { HadithCard } from "fe/components/hadith-card"; import { StructuredData } from "fe/components/structured-data"; import { generateBookStructuredData, generateBreadcrumbStructuredData } from "fe/lib/seo-utils"; +export const dynamic = 'force-dynamic'; + interface BookPageProps { params: Promise<{ collectionId: string; diff --git a/fe/src/app/collections/[collectionId]/info/page.tsx b/fe/src/app/collections/[collectionId]/info/page.tsx index e951fbe..cd44137 100644 --- a/fe/src/app/collections/[collectionId]/info/page.tsx +++ b/fe/src/app/collections/[collectionId]/info/page.tsx @@ -4,6 +4,8 @@ import { businessApi } from "fe/lib/api-client" import { Language } from "fe/proto/api" import { Collection, apiDetailedCollectionToCollection } from "fe/types" +export const dynamic = 'force-dynamic'; + interface CollectionInfoPageProps { params: Promise<{ collectionId: string diff --git a/fe/src/app/collections/[collectionId]/page.tsx b/fe/src/app/collections/[collectionId]/page.tsx index db07cf6..bb7227b 100644 --- a/fe/src/app/collections/[collectionId]/page.tsx +++ b/fe/src/app/collections/[collectionId]/page.tsx @@ -14,6 +14,8 @@ import { SearchBar } from "fe/components/search-bar" import { StructuredData } from "fe/components/structured-data" import { generateCollectionStructuredData, generateBreadcrumbStructuredData } from "fe/lib/seo-utils" +export const dynamic = 'force-dynamic'; + interface CollectionPageProps { params: Promise<{ collectionId: string diff --git a/fe/src/app/collections/page.tsx b/fe/src/app/collections/page.tsx index e223fc3..ea6ad7e 100644 --- a/fe/src/app/collections/page.tsx +++ b/fe/src/app/collections/page.tsx @@ -24,6 +24,8 @@ export const metadata = { }, }; +export const dynamic = 'force-dynamic'; + // Helper function to map API CollectionWithoutBooks to frontend Collection type function mapCollectionWithoutBooksToCollection(apiCollection: CollectionWithoutBooks): Collection { return { diff --git a/fe/src/app/layout.tsx b/fe/src/app/layout.tsx index 36a8008..0016c9e 100644 --- a/fe/src/app/layout.tsx +++ b/fe/src/app/layout.tsx @@ -69,7 +69,7 @@ export const metadata: Metadata = { }; // Set revalidation time for the data fetched in this layout -export const revalidate = 3600; // 1 hour +export const dynamic = 'force-dynamic'; // Helper function to map API CollectionWithoutBooks to frontend Collection type // (Same function as used in collections/page.tsx) diff --git a/fe/src/app/page.tsx b/fe/src/app/page.tsx index 6e2916b..9bd62ae 100644 --- a/fe/src/app/page.tsx +++ b/fe/src/app/page.tsx @@ -9,8 +9,7 @@ import { Logo } from "fe/components/logo"; import { StructuredData } from "fe/components/structured-data"; import { generateWebsiteStructuredData } from "fe/lib/seo-utils"; -// Add revalidate option for the page itself -export const revalidate = 3600; // 1 hour +export const dynamic = 'force-dynamic'; // Define metadata for the home page export const metadata = { diff --git a/fe/src/lib/isr-data.ts b/fe/src/lib/isr-data.ts index 42a43b6..d2c3149 100644 --- a/fe/src/lib/isr-data.ts +++ b/fe/src/lib/isr-data.ts @@ -1,68 +1,32 @@ -import 'server-only'; // Ensure this module is only used on the server -import { businessApi } from "./api-client"; -import { Language } from "../proto/api"; -import { unstable_cache } from 'next/cache'; +import 'server-only'; +import { Language } from '../proto/api'; import { GetAllLanguagesResponse, GetAllCollectionsResponse, - GetAllReferenceTypesResponse -} from "fe/proto/business_api"; - -// Define a revalidation time (e.g., 1 hour = 3600 seconds) -// Consider making this configurable via environment variables if needed -const REVALIDATE_TIME = 3600; + GetAllReferenceTypesResponse, +} from 'fe/proto/business_api'; +import { + getLanguagesFromCache, + getCollectionsFromCache, + getReferenceTypesFromCache, +} from './startup-cache'; /** - * Fetches all languages using the business API, wrapped with unstable_cache for ISR. - * @returns {Promise} A promise resolving to the languages response. + * These helpers now read from the startup cache populated by + * `preload-static-data.ts` instead of hitting the backend directly. */ -export const getLanguagesWithISR = unstable_cache( - async (): Promise => { - console.log("Fetching languages via ISR cache wrapper..."); // Add logging for debugging - return await businessApi.getAllLanguages(); - }, - ['languages'], // Cache key parts: Ensures this specific function call is cached uniquely - { - revalidate: REVALIDATE_TIME, // Revalidate the cache every hour - tags: ['languages'] // Tag for potential on-demand revalidation - } -); -/** - * Fetches all collections for a given language using the business API, wrapped with unstable_cache for ISR. - * @param {Language} language - The language for which to fetch collections. - * @returns {Promise} A promise resolving to the collections response. - */ -export const getCollectionsWithISR = unstable_cache( - async (language: Language): Promise => { - console.log(`Fetching collections for language ${language} via ISR cache wrapper...`); // Add logging - // The 'language' parameter automatically becomes part of the cache key, - // ensuring different languages have separate cache entries. - return await businessApi.getAllCollections(language); - }, - ['collections'], // Base cache key part - { - revalidate: REVALIDATE_TIME, - tags: ['collections'] // Tag for on-demand revalidation (general) - // Note: Removed language-specific tag `collections-${language}` due to scope issues. - // Revalidating 'collections' will invalidate cache for all languages. - } -); +export async function getLanguagesWithISR(): Promise { + return { languages: getLanguagesFromCache() }; +} -/** - * Fetches all reference types using the business API, wrapped with unstable_cache for ISR. - * @returns {Promise} A promise resolving to the reference types response. - */ -export const getReferenceTypesWithISR = unstable_cache( - async (): Promise => { - console.log("Fetching reference types via ISR cache wrapper..."); // Add logging - return await businessApi.getAllReferenceTypes(); - }, - ['referenceTypes'], // Cache key parts - { - revalidate: REVALIDATE_TIME, - tags: ['referenceTypes'] // Tag for on-demand revalidation - } -); +export async function getCollectionsWithISR( + language: Language, +): Promise { + return { collections: getCollectionsFromCache(language) }; +} + +export async function getReferenceTypesWithISR(): Promise { + return { referenceTypes: getReferenceTypesFromCache() }; +} -// Add more wrapped functions here as needed for other static data types diff --git a/fe/src/lib/startup-cache.ts b/fe/src/lib/startup-cache.ts new file mode 100644 index 0000000..ccd913d --- /dev/null +++ b/fe/src/lib/startup-cache.ts @@ -0,0 +1,39 @@ +import fs from 'fs'; +import path from 'path'; +import { Language } from '../proto/api'; +import { CollectionWithoutBooks, HadithReferenceType } from '../proto/business_models'; + +interface StaticData { + languages: Language[]; + collections: { [key: number]: CollectionWithoutBooks[] }; + referenceTypes: HadithReferenceType[]; +} + +const cachePath = process.env.STATIC_DATA_PATH || path.join(process.cwd(), 'preloaded-static-data.json'); +let cached: StaticData | null = null; + +function loadCache(): StaticData { + if (!cached) { + try { + const raw = fs.readFileSync(cachePath, 'utf-8'); + cached = JSON.parse(raw) as StaticData; + } catch (err) { + console.error('Failed to load static data cache', err); + cached = { languages: [], collections: {}, referenceTypes: [] }; + } + } + return cached; +} + +export function getLanguagesFromCache(): Language[] { + return loadCache().languages; +} + +export function getCollectionsFromCache(language: Language): CollectionWithoutBooks[] { + return loadCache().collections[language] || []; +} + +export function getReferenceTypesFromCache(): HadithReferenceType[] { + return loadCache().referenceTypes; +} +