Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion fe/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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"]
29 changes: 29 additions & 0 deletions fe/scripts-dist/preload-static-data.js
Original file line number Diff line number Diff line change
@@ -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();
29 changes: 29 additions & 0 deletions fe/scripts/preload-static-data.ts
Original file line number Diff line number Diff line change
@@ -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();
11 changes: 11 additions & 0 deletions fe/scripts/tsconfig.preload.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"esModuleInterop": true,
"skipLibCheck": true,
"outDir": "../scripts-dist",
"lib": ["es2020"]
},
"include": ["preload-static-data.ts"]
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 2 additions & 0 deletions fe/src/app/collections/[collectionId]/[bookId]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 2 additions & 0 deletions fe/src/app/collections/[collectionId]/info/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions fe/src/app/collections/[collectionId]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions fe/src/app/collections/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
2 changes: 1 addition & 1 deletion fe/src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
3 changes: 1 addition & 2 deletions fe/src/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
82 changes: 23 additions & 59 deletions fe/src/lib/isr-data.ts
Original file line number Diff line number Diff line change
@@ -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<GetAllLanguagesResponse>} 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<GetAllLanguagesResponse> => {
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<GetAllCollectionsResponse>} A promise resolving to the collections response.
*/
export const getCollectionsWithISR = unstable_cache(
async (language: Language): Promise<GetAllCollectionsResponse> => {
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<GetAllLanguagesResponse> {
return { languages: getLanguagesFromCache() };
}

/**
* Fetches all reference types using the business API, wrapped with unstable_cache for ISR.
* @returns {Promise<GetAllReferenceTypesResponse>} A promise resolving to the reference types response.
*/
export const getReferenceTypesWithISR = unstable_cache(
async (): Promise<GetAllReferenceTypesResponse> => {
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<GetAllCollectionsResponse> {
return { collections: getCollectionsFromCache(language) };
}

export async function getReferenceTypesWithISR(): Promise<GetAllReferenceTypesResponse> {
return { referenceTypes: getReferenceTypesFromCache() };
}

// Add more wrapped functions here as needed for other static data types
39 changes: 39 additions & 0 deletions fe/src/lib/startup-cache.ts
Original file line number Diff line number Diff line change
@@ -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;
}