diff --git a/bizmatch/angular.json b/bizmatch/angular.json index cd08e25..b9b8b2b 100644 --- a/bizmatch/angular.json +++ b/bizmatch/angular.json @@ -26,6 +26,12 @@ "ssr": { "entry": "server.ts" }, + "allowedCommonJsDependencies": [ + "quill-delta", + "leaflet", + "dayjs", + "qs" + ], "polyfills": [ "zone.js" ], diff --git a/bizmatch/server.ts b/bizmatch/server.ts index 983e3af..abfc7d3 100644 --- a/bizmatch/server.ts +++ b/bizmatch/server.ts @@ -3,17 +3,24 @@ import './src/ssr-dom-polyfill'; import { APP_BASE_HREF } from '@angular/common'; import { AngularNodeAppEngine, createNodeRequestHandler, writeResponseToNodeResponse } from '@angular/ssr/node'; +import { ɵsetAngularAppEngineManifest as setAngularAppEngineManifest } from '@angular/ssr'; import express from 'express'; import { fileURLToPath } from 'node:url'; import { dirname, join, resolve } from 'node:path'; // The Express app is exported so that it can be used by serverless Functions. -export function app(): express.Express { +export async function app(): Promise { const server = express(); const serverDistFolder = dirname(fileURLToPath(import.meta.url)); const browserDistFolder = resolve(serverDistFolder, '../browser'); const indexHtml = join(serverDistFolder, 'index.server.html'); + // Explicitly load and set the Angular app engine manifest + // This is required for environments where the manifest is not auto-loaded + const manifestPath = join(serverDistFolder, 'angular-app-engine-manifest.mjs'); + const manifest = await import(manifestPath); + setAngularAppEngineManifest(manifest.default); + const angularApp = new AngularNodeAppEngine(); server.set('view engine', 'html'); @@ -27,27 +34,46 @@ export function app(): express.Express { })); // All regular routes use the Angular engine - server.get('*', (req, res, next) => { - angularApp - .handle(req) - .then((response) => { - if (response) { - writeResponseToNodeResponse(response, res); - } else { - res.sendStatus(404); - } - }) - .catch((err) => next(err)); + server.get('*', async (req, res, next) => { + console.log(`[SSR] Handling request: ${req.method} ${req.url}`); + try { + const response = await angularApp.handle(req); + if (response) { + console.log(`[SSR] Response received for ${req.url}, status: ${response.status}`); + writeResponseToNodeResponse(response, res); + } else { + console.log(`[SSR] No response for ${req.url} - Angular engine returned null`); + console.log(`[SSR] This usually means the route couldn't be rendered. Check for: + 1. Browser API usage in components + 2. Missing platform checks + 3. Errors during component initialization`); + res.sendStatus(404); + } + } catch (err) { + console.error(`[SSR] Error handling ${req.url}:`, err); + console.error(`[SSR] Stack trace:`, err.stack); + next(err); + } }); return server; } -function run(): void { +// Global error handlers for debugging +process.on('unhandledRejection', (reason, promise) => { + console.error('[SSR] Unhandled Rejection at:', promise, 'reason:', reason); +}); + +process.on('uncaughtException', (error) => { + console.error('[SSR] Uncaught Exception:', error); + console.error('[SSR] Stack:', error.stack); +}); + +async function run(): Promise { const port = process.env['PORT'] || 4200; // Start up the Node server - const server = app(); + const server = await app(); server.listen(port, () => { console.log(`Node Express server listening on http://localhost:${port}`); }); diff --git a/bizmatch/src/app/app.component.ts b/bizmatch/src/app/app.component.ts index 45ee9a2..df9e676 100644 --- a/bizmatch/src/app/app.component.ts +++ b/bizmatch/src/app/app.component.ts @@ -1,5 +1,5 @@ -import { CommonModule } from '@angular/common'; -import { AfterViewInit, Component, HostListener } from '@angular/core'; +import { CommonModule, isPlatformBrowser } from '@angular/common'; +import { AfterViewInit, Component, HostListener, PLATFORM_ID, inject } from '@angular/core'; import { ActivatedRoute, NavigationEnd, Router, RouterOutlet } from '@angular/router'; import { initFlowbite } from 'flowbite'; import { filter } from 'rxjs/operators'; @@ -29,6 +29,8 @@ export class AppComponent implements AfterViewInit { build = build; title = 'bizmatch'; actualRoute = ''; + private platformId = inject(PLATFORM_ID); + private isBrowser = isPlatformBrowser(this.platformId); public constructor( public loadingService: LoadingService, @@ -48,9 +50,11 @@ export class AppComponent implements AfterViewInit { this.actualRoute = currentRoute.snapshot.url[0].path; // Re-initialize Flowbite after navigation to ensure all components are ready - setTimeout(() => { - initFlowbite(); - }, 50); + if (this.isBrowser) { + setTimeout(() => { + initFlowbite(); + }, 50); + } }); } ngOnInit() { @@ -60,7 +64,9 @@ export class AppComponent implements AfterViewInit { ngAfterViewInit() { // Initialize Flowbite for dropdowns, modals, and other interactive components // Note: Drawers work automatically with data-drawer-target attributes - initFlowbite(); + if (this.isBrowser) { + initFlowbite(); + } } @HostListener('window:keydown', ['$event']) diff --git a/bizmatch/src/app/app.config.server.ts b/bizmatch/src/app/app.config.server.ts index b4d57c9..07e292e 100644 --- a/bizmatch/src/app/app.config.server.ts +++ b/bizmatch/src/app/app.config.server.ts @@ -1,11 +1,15 @@ import { mergeApplicationConfig, ApplicationConfig } from '@angular/core'; import { provideServerRendering } from '@angular/platform-server'; +import { provideServerRouting } from '@angular/ssr'; import { appConfig } from './app.config'; +import { serverRoutes } from './app.routes.server'; const serverConfig: ApplicationConfig = { providers: [ - provideServerRendering() + provideServerRendering(), + provideServerRouting(serverRoutes) ] }; export const config = mergeApplicationConfig(appConfig, serverConfig); + diff --git a/bizmatch/src/app/app.config.ts b/bizmatch/src/app/app.config.ts index 39a88ab..cf058a2 100644 --- a/bizmatch/src/app/app.config.ts +++ b/bizmatch/src/app/app.config.ts @@ -1,5 +1,6 @@ -import { IMAGE_CONFIG } from '@angular/common'; -import { APP_INITIALIZER, ApplicationConfig, ErrorHandler } from '@angular/core'; +import { IMAGE_CONFIG, isPlatformBrowser } from '@angular/common'; +import { APP_INITIALIZER, ApplicationConfig, ErrorHandler, PLATFORM_ID, inject } from '@angular/core'; +import { provideClientHydration } from '@angular/platform-browser'; import { provideRouter, withEnabledBlockingInitialNavigation, withInMemoryScrolling } from '@angular/router'; import { HTTP_INTERCEPTORS, provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'; @@ -19,10 +20,12 @@ import { GlobalErrorHandler } from './services/globalErrorHandler'; import { POSTHOG_INIT_PROVIDER } from './services/posthog.factory'; import { SelectOptionsService } from './services/select-options.service'; import { createLogger } from './utils/utils'; -// provideClientHydration() + const logger = createLogger('ApplicationConfig'); export const appConfig: ApplicationConfig = { providers: [ + // Temporarily disabled for SSR debugging + // provideClientHydration(), provideHttpClient(withInterceptorsFromDi()), { provide: APP_INITIALIZER, @@ -90,7 +93,6 @@ export const appConfig: ApplicationConfig = { }), provideFirebaseApp(() => initializeApp(environment.firebaseConfig)), provideAuth(() => getAuth()), - // provideFirestore(() => getFirestore()), ], }; function initServices(selectOptions: SelectOptionsService) { diff --git a/bizmatch/src/app/app.routes.server.ts b/bizmatch/src/app/app.routes.server.ts new file mode 100644 index 0000000..aa0ea8b --- /dev/null +++ b/bizmatch/src/app/app.routes.server.ts @@ -0,0 +1,8 @@ +import { RenderMode, ServerRoute } from '@angular/ssr'; + +export const serverRoutes: ServerRoute[] = [ + { + path: '**', + renderMode: RenderMode.Server + } +]; diff --git a/bizmatch/src/app/app.routes.ts b/bizmatch/src/app/app.routes.ts index 41b3234..242bb51 100644 --- a/bizmatch/src/app/app.routes.ts +++ b/bizmatch/src/app/app.routes.ts @@ -1,6 +1,7 @@ import { Routes } from '@angular/router'; import { LogoutComponent } from './components/logout/logout.component'; import { NotFoundComponent } from './components/not-found/not-found.component'; +import { TestSsrComponent } from './components/test-ssr/test-ssr.component'; import { EmailAuthorizedComponent } from './components/email-authorized/email-authorized.component'; import { EmailVerificationComponent } from './components/email-verification/email-verification.component'; @@ -26,6 +27,10 @@ import { TermsOfUseComponent } from './pages/legal/terms-of-use.component'; import { PrivacyStatementComponent } from './pages/legal/privacy-statement.component'; export const routes: Routes = [ + { + path: 'test-ssr', + component: TestSsrComponent, + }, { path: 'businessListings', component: BusinessListingsComponent, diff --git a/bizmatch/src/app/components/dropdown/dropdown.component.ts b/bizmatch/src/app/components/dropdown/dropdown.component.ts index 19a7374..a205f84 100644 --- a/bizmatch/src/app/components/dropdown/dropdown.component.ts +++ b/bizmatch/src/app/components/dropdown/dropdown.component.ts @@ -1,4 +1,5 @@ -import { AfterViewInit, Component, ElementRef, HostBinding, Input, OnDestroy, ViewChild } from '@angular/core'; +import { AfterViewInit, Component, ElementRef, HostBinding, Input, OnDestroy, ViewChild, PLATFORM_ID, inject } from '@angular/core'; +import { isPlatformBrowser } from '@angular/common'; import { createPopper, Instance as PopperInstance } from '@popperjs/core'; @Component({ @@ -23,6 +24,8 @@ export class DropdownComponent implements AfterViewInit, OnDestroy { @HostBinding('class.hidden') isHidden: boolean = true; + private platformId = inject(PLATFORM_ID); + private isBrowser = isPlatformBrowser(this.platformId); private popperInstance: PopperInstance | null = null; isVisible: boolean = false; private clickOutsideListener: any; @@ -30,6 +33,8 @@ export class DropdownComponent implements AfterViewInit, OnDestroy { private hoverHideListener: any; ngAfterViewInit() { + if (!this.isBrowser) return; + if (!this.triggerEl) { console.error('Trigger element is not provided to the dropdown component.'); return; @@ -58,6 +63,8 @@ export class DropdownComponent implements AfterViewInit, OnDestroy { } private setupEventListeners() { + if (!this.isBrowser) return; + if (this.triggerType === 'click') { this.triggerEl.addEventListener('click', () => this.toggle()); } else if (this.triggerType === 'hover') { @@ -74,6 +81,8 @@ export class DropdownComponent implements AfterViewInit, OnDestroy { } private removeEventListeners() { + if (!this.isBrowser) return; + if (this.triggerType === 'click') { this.triggerEl.removeEventListener('click', () => this.toggle()); } else if (this.triggerType === 'hover') { @@ -104,7 +113,7 @@ export class DropdownComponent implements AfterViewInit, OnDestroy { } private handleClickOutside(event: MouseEvent) { - if (!this.isVisible) return; + if (!this.isVisible || !this.isBrowser) return; const clickedElement = event.target as HTMLElement; if (this.ignoreClickOutsideClass) { diff --git a/bizmatch/src/app/components/header/header.component.html b/bizmatch/src/app/components/header/header.component.html index 1b6d6b9..4efef63 100644 --- a/bizmatch/src/app/components/header/header.component.html +++ b/bizmatch/src/app/components/header/header.component.html @@ -31,8 +31,7 @@ }