fix: ensure SSR always fetches fresh assets from database #9
No files matched your search
+3
-1
@@ -7,8 +7,10 @@ COPY . .
|
||||
RUN npx ng build --configuration production
|
||||
|
||||
# ── Serve stage ────────────────────────────────────────────────────────
|
||||
FROM docker.io/library/nginx:alpine
|
||||
FROM docker.io/library/node:22-alpine
|
||||
RUN apk add --no-cache nginx
|
||||
COPY --from=build /app/dist/radio-station/browser /usr/share/nginx/html
|
||||
COPY --from=build /app/dist/radio-station/server /app/server
|
||||
COPY nginx.conf.template /etc/nginx/conf.d/nginx.conf.template
|
||||
COPY config.json.template /usr/share/nginx/html/config.json.template
|
||||
COPY docker-entrypoint.sh /docker-entrypoint.sh
|
||||
|
||||
+25
-1
@@ -1,11 +1,35 @@
|
||||
#!/bin/sh
|
||||
# Render config templates with environment variables, then start nginx.
|
||||
# Start the Angular SSR Node process, then nginx.
|
||||
set -e
|
||||
|
||||
# Ensure nginx can write to the shared logs directory
|
||||
mkdir -p /logs && chown nginx:nginx /logs
|
||||
|
||||
# Render nginx config from template
|
||||
sed "s|__API_UPSTREAM__|${API_UPSTREAM}|g" /etc/nginx/conf.d/nginx.conf.template > /etc/nginx/conf.d/default.conf
|
||||
|
||||
# Render client-side config.json for fallback use
|
||||
envsubst '${API_PUBLIC_URL}' < /usr/share/nginx/html/config.json.template > /usr/share/nginx/html/config.json
|
||||
|
||||
# Start the Angular SSR server (Node process) in the background
|
||||
# Point at nginx (port 80) so SSR API calls go through the nginx proxy chain
|
||||
# to the upstream backend. Nginx is started after SSR so the chain is complete
|
||||
# by the time the first SSR request actually needs API data.
|
||||
export SSR_API_BASE_URL="http://localhost:80"
|
||||
echo "[entrypoint] Starting Angular SSR server on port 4200..."
|
||||
node /app/server/server.mjs &
|
||||
SSR_PID=$!
|
||||
|
||||
# Wait for the SSR server to be ready (up to 10 seconds)
|
||||
echo "[entrypoint] Waiting for SSR server to be ready..."
|
||||
for i in $(seq 1 20); do
|
||||
if node -e "require('http').get('http://localhost:4200/', (r) => { process.exit(r.statusCode === 200 ? 0 : 1) }).on('error', () => process.exit(1))" 2>/dev/null; then
|
||||
echo "[entrypoint] SSR server is ready."
|
||||
break
|
||||
fi
|
||||
sleep 0.5
|
||||
done
|
||||
|
||||
# Start nginx in the foreground (primary process)
|
||||
echo "[entrypoint] Starting nginx..."
|
||||
exec nginx -g 'daemon off;'
|
||||
+28
-7
@@ -13,18 +13,24 @@ log_format kmtn_json escape=json
|
||||
|
||||
access_log /logs/access-$log_date.log kmtn_json;
|
||||
|
||||
upstream ssr_backend {
|
||||
server 127.0.0.1:4200;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name _;
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
|
||||
# SPA fallback — Angular router handles its own paths
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
# Serve static assets directly from disk (hashed filenames are immutable)
|
||||
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot|map)$ {
|
||||
expires 1y;
|
||||
add_header Cache-Control "public, immutable";
|
||||
access_log off;
|
||||
}
|
||||
|
||||
# Dynamic sitemap.xml + robots.txt — proxied to the backend so each
|
||||
# Dynamic sitemap.xml + robots.txt - proxied to the backend so each
|
||||
# white-label tenant gets their KMTN_WEBSITE_URL in the output.
|
||||
location = /sitemap.xml {
|
||||
proxy_pass __API_UPSTREAM__/api/sitemap/sitemap.xml;
|
||||
@@ -53,12 +59,27 @@ server {
|
||||
proxy_set_header X-Forwarded-Proto $http_x_forwarded_proto;
|
||||
proxy_set_header X-Forwarded-Host $host;
|
||||
|
||||
# Disable caching through the double-proxy chain (NPM → frontend nginx → API)
|
||||
# Without this, browsers cache responses keyed by the HTTPS origin, then send
|
||||
# conditional requests (If-None-Match) that get lost or mismatched in the proxy chain.
|
||||
# Disable caching through the double-proxy chain
|
||||
proxy_cache off;
|
||||
add_header Cache-Control "no-store, no-cache, must-revalidate" always;
|
||||
add_header Pragma "no-cache" always;
|
||||
add_header Vary "*" always;
|
||||
}
|
||||
|
||||
# Proxy all page requests to the Angular SSR server.
|
||||
# The SSR server (AngularNodeAppEngine) handles both RenderMode.Server
|
||||
# routes (full on-demand rendering with fresh API data) and serves
|
||||
# prerendered static HTML for RenderMode.Prerender routes.
|
||||
location / {
|
||||
proxy_pass http://ssr_backend;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $http_x_forwarded_proto;
|
||||
proxy_set_header X-Forwarded-Host $host;
|
||||
|
||||
# SSR responses should not be cached by intermediate proxies
|
||||
proxy_cache off;
|
||||
add_header X-Powered-By "Angular SSR" always;
|
||||
}
|
||||
}
|
||||
+129
-44
@@ -4,7 +4,7 @@ import '@angular/compiler';
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
|
||||
// Mock the @angular/ssr/node imports that pull in Node.js http server code
|
||||
// — we only want to test PRERENDERED_ROUTES and isPrerenderedRoute here.
|
||||
// — we only want to test the request handler logic here.
|
||||
vi.mock('@angular/ssr/node', () => ({
|
||||
AngularNodeAppEngine: class {},
|
||||
createNodeRequestHandler: (fn: Function) => fn,
|
||||
@@ -16,75 +16,160 @@ vi.mock('node:http', () => ({
|
||||
default: { createServer: vi.fn() },
|
||||
}));
|
||||
|
||||
import { PRERENDERED_ROUTES, isPrerenderedRoute } from './server';
|
||||
|
||||
describe('SSR server — server.ts', () => {
|
||||
describe('PRERENDERED_ROUTES', () => {
|
||||
it('should contain exactly 5 routes', () => {
|
||||
expect(PRERENDERED_ROUTES).toHaveLength(5);
|
||||
it('should export a default request handler', async () => {
|
||||
const mod = await import('./server');
|
||||
expect(mod.default).toBeDefined();
|
||||
});
|
||||
|
||||
it('should include the home route', () => {
|
||||
expect(PRERENDERED_ROUTES).toContain('/');
|
||||
it('should not export PRERENDERED_ROUTES (all routes now use RenderMode.Server)', async () => {
|
||||
const mod = await import('./server');
|
||||
// The server.ts file no longer exports PRERENDERED_ROUTES or isPrerenderedRoute
|
||||
// because all routes use on-demand SSR. Verify the file doesn't reference them.
|
||||
const fs = require('fs');
|
||||
const content = fs.readFileSync('./server.ts', 'utf-8');
|
||||
expect(content).not.toContain('PRERENDERED_ROUTES');
|
||||
expect(content).not.toContain('isPrerenderedRoute');
|
||||
expect(content).not.toContain('AngularAppEngine');
|
||||
});
|
||||
|
||||
it('should include /about', () => {
|
||||
expect(PRERENDERED_ROUTES).toContain('/about');
|
||||
it('should have a clean server with lazy engine bootstrapping', async () => {
|
||||
const fs = require('fs');
|
||||
const content = fs.readFileSync('./server.ts', 'utf-8');
|
||||
expect(content).toContain('getEngine');
|
||||
expect(content).toContain('AngularNodeAppEngine');
|
||||
expect(content).toContain('listen(4200');
|
||||
});
|
||||
});
|
||||
|
||||
it('should include /schedule', () => {
|
||||
expect(PRERENDERED_ROUTES).toContain('/schedule');
|
||||
describe('app.server.routes.ts — RenderMode.Server for all routes', () => {
|
||||
it('should use RenderMode.Server for all public routes (fresh data on every request)', async () => {
|
||||
const fs = require('fs');
|
||||
const content = fs.readFileSync('./src/app/app.server.routes.ts', 'utf-8');
|
||||
|
||||
// Every route should use RenderMode.Server
|
||||
const prerenderMatches = content.match(/RenderMode\.Prerender/g);
|
||||
expect(prerenderMatches).toBeNull();
|
||||
|
||||
// Should have RenderMode.Server entries
|
||||
const serverMatches = content.match(/RenderMode\.Server/g);
|
||||
expect(serverMatches).not.toBeNull();
|
||||
expect(serverMatches.length).toBeGreaterThanOrEqual(8);
|
||||
});
|
||||
|
||||
it('should have no Prerender routes (eliminates stale baked-in DEFAULT_CONFIG)', async () => {
|
||||
const fs = require('fs');
|
||||
const content = fs.readFileSync('./src/app/app.server.routes.ts', 'utf-8');
|
||||
expect(content).not.toContain('RenderMode.Prerender');
|
||||
});
|
||||
});
|
||||
|
||||
describe('docker-entrypoint.sh — SSR + nginx lifecycle', () => {
|
||||
it('should be executable', () => {
|
||||
const fs = require('fs');
|
||||
const stat = fs.statSync('./docker-entrypoint.sh');
|
||||
expect(stat.isFile()).toBe(true);
|
||||
});
|
||||
|
||||
it('should include /events', () => {
|
||||
expect(PRERENDERED_ROUTES).toContain('/events');
|
||||
it('should start the SSR node process before nginx', () => {
|
||||
const fs = require('fs');
|
||||
const content = fs.readFileSync('./docker-entrypoint.sh', 'utf-8');
|
||||
// SSR must start before nginx
|
||||
const ssrStart = content.indexOf('node /app/server/server.mjs');
|
||||
const nginxStart = content.indexOf('exec nginx');
|
||||
expect(ssrStart).toBeGreaterThan(-1);
|
||||
expect(nginxStart).toBeGreaterThan(-1);
|
||||
expect(ssrStart).toBeLessThan(nginxStart);
|
||||
});
|
||||
|
||||
it('should include /contact', () => {
|
||||
expect(PRERENDERED_ROUTES).toContain('/contact');
|
||||
it('should set SSR_API_BASE_URL for the SSR server', () => {
|
||||
const fs = require('fs');
|
||||
const content = fs.readFileSync('./docker-entrypoint.sh', 'utf-8');
|
||||
expect(content).toContain('SSR_API_BASE_URL');
|
||||
expect(content).toContain('http://localhost:80');
|
||||
});
|
||||
|
||||
it('should wait for SSR server readiness before starting nginx', () => {
|
||||
const fs = require('fs');
|
||||
const content = fs.readFileSync('./docker-entrypoint.sh', 'utf-8');
|
||||
expect(content).toContain('Waiting for SSR server to be ready');
|
||||
expect(content).toContain('localhost:4200');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Dockerfile — multi-stage build with SSR server', () => {
|
||||
it('should copy the server bundle in addition to browser output', () => {
|
||||
const fs = require('fs');
|
||||
const content = fs.readFileSync('./Dockerfile', 'utf-8');
|
||||
expect(content).toMatch(/COPY.*server.*\/app\/server/);
|
||||
});
|
||||
|
||||
describe('isPrerenderedRoute()', () => {
|
||||
it('should match exact prerendered paths', () => {
|
||||
expect(isPrerenderedRoute('/')).toBe(true);
|
||||
expect(isPrerenderedRoute('/about')).toBe(true);
|
||||
expect(isPrerenderedRoute('/schedule')).toBe(true);
|
||||
expect(isPrerenderedRoute('/events')).toBe(true);
|
||||
expect(isPrerenderedRoute('/contact')).toBe(true);
|
||||
it('should use a Node.js base image for the serve stage', () => {
|
||||
const fs = require('fs');
|
||||
const content = fs.readFileSync('./Dockerfile', 'utf-8');
|
||||
// Second FROM should be Node-based (not plain nginx)
|
||||
const fromLines = content.split('\n').filter(l => l.startsWith('FROM '));
|
||||
expect(fromLines.length).toBeGreaterThanOrEqual(2);
|
||||
expect(fromLines[1]).toContain('node');
|
||||
});
|
||||
|
||||
it('should match paths with trailing slashes', () => {
|
||||
expect(isPrerenderedRoute('/about/')).toBe(true);
|
||||
expect(isPrerenderedRoute('/schedule/')).toBe(true);
|
||||
expect(isPrerenderedRoute('/events/')).toBe(true);
|
||||
expect(isPrerenderedRoute('/contact/')).toBe(true);
|
||||
it('should install nginx in the serve stage', () => {
|
||||
const fs = require('fs');
|
||||
const content = fs.readFileSync('./Dockerfile', 'utf-8');
|
||||
expect(content).toContain('apk add --no-cache nginx');
|
||||
});
|
||||
|
||||
it('should match paths with query strings', () => {
|
||||
expect(isPrerenderedRoute('/about?foo=bar')).toBe(true);
|
||||
expect(isPrerenderedRoute('/schedule?day=monday')).toBe(true);
|
||||
it('should expose port 80', () => {
|
||||
const fs = require('fs');
|
||||
const content = fs.readFileSync('./Dockerfile', 'utf-8');
|
||||
expect(content).toContain('EXPOSE 80');
|
||||
});
|
||||
});
|
||||
|
||||
it('should match paths with fragments', () => {
|
||||
expect(isPrerenderedRoute('/events#upcoming')).toBe(true);
|
||||
describe('nginx.conf.template — SSR proxy chain', () => {
|
||||
it('should define an upstream pointing to SSR server on port 4200', () => {
|
||||
const fs = require('fs');
|
||||
const content = fs.readFileSync('./nginx.conf.template', 'utf-8');
|
||||
expect(content).toContain('upstream ssr_backend');
|
||||
expect(content).toContain('127.0.0.1:4200');
|
||||
});
|
||||
|
||||
it('should reject non-prerendered routes', () => {
|
||||
expect(isPrerenderedRoute('/login')).toBe(false);
|
||||
expect(isPrerenderedRoute('/admin')).toBe(false);
|
||||
expect(isPrerenderedRoute('/nonexistent')).toBe(false);
|
||||
expect(isPrerenderedRoute('/admin/dashboard')).toBe(false);
|
||||
it('should proxy page requests to the SSR backend', () => {
|
||||
const fs = require('fs');
|
||||
const content = fs.readFileSync('./nginx.conf.template', 'utf-8');
|
||||
expect(content).toContain('proxy_pass http://ssr_backend');
|
||||
});
|
||||
|
||||
it('should handle full URLs', () => {
|
||||
expect(isPrerenderedRoute('https://kmtnflower.org/about')).toBe(true);
|
||||
expect(isPrerenderedRoute('http://localhost:4200/events')).toBe(true);
|
||||
expect(isPrerenderedRoute('https://kmtnflower.org/login')).toBe(false);
|
||||
it('should serve static assets directly from disk with long cache', () => {
|
||||
const fs = require('fs');
|
||||
const content = fs.readFileSync('./nginx.conf.template', 'utf-8');
|
||||
expect(content).toContain('location ~*');
|
||||
expect(content).toContain('.js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot|map');
|
||||
expect(content).toContain('expires 1y');
|
||||
expect(content).toContain('public, immutable');
|
||||
});
|
||||
|
||||
it('should normalise root path — "/" stays "/"', () => {
|
||||
expect(isPrerenderedRoute('/')).toBe(true);
|
||||
it('should still proxy /api/ to the upstream backend', () => {
|
||||
const fs = require('fs');
|
||||
const content = fs.readFileSync('./nginx.conf.template', 'utf-8');
|
||||
expect(content).toContain('location /api/');
|
||||
expect(content).toContain('__API_UPSTREAM__');
|
||||
});
|
||||
});
|
||||
|
||||
describe('main.server.ts — SSR API base URL injection', () => {
|
||||
it('should override APP_CONFIG with SSR-aware apiBaseUrl', () => {
|
||||
const fs = require('fs');
|
||||
const content = fs.readFileSync('./src/main.server.ts', 'utf-8');
|
||||
expect(content).toContain('SSR_API_BASE_URL');
|
||||
expect(content).toContain('ssrAppConfig');
|
||||
expect(content).toContain('provide: APP_CONFIG');
|
||||
});
|
||||
|
||||
it('should read SSR_API_BASE_URL from environment with fallback', () => {
|
||||
const fs = require('fs');
|
||||
const content = fs.readFileSync('./src/main.server.ts', 'utf-8');
|
||||
expect(content).toContain("process.env.SSR_API_BASE_URL");
|
||||
expect(content).toContain("http://localhost:80");
|
||||
});
|
||||
});
|
||||
@@ -4,44 +4,12 @@ import {
|
||||
isMainModule,
|
||||
writeResponseToNodeResponse,
|
||||
} from '@angular/ssr/node';
|
||||
import { AngularAppEngine } from '@angular/ssr';
|
||||
import * as http from 'node:http';
|
||||
|
||||
// Enable serving prerendered static HTML from disk. Without this flag the
|
||||
// engine falls back to SSR for every route, ignoring the prerendered output.
|
||||
AngularAppEngine.ɵallowStaticRouteRender = true;
|
||||
|
||||
/**
|
||||
* Public routes prerendered at build time (SSG — Static Site Generation).
|
||||
* These five routes are compiled to static HTML during `ng build` and
|
||||
* served directly from disk at runtime for optimal performance and SEO.
|
||||
* The list here mirrors `prerender-routes.txt` and the Prerender entries
|
||||
* in `src/app/app.server.routes.ts`.
|
||||
*/
|
||||
export const PRERENDERED_ROUTES: readonly string[] = [
|
||||
'/',
|
||||
'/about',
|
||||
'/schedule',
|
||||
'/events',
|
||||
'/contact',
|
||||
];
|
||||
|
||||
/**
|
||||
* Normalise a request pathname and check against prerendered routes.
|
||||
* Handles both bare paths (`/about`) and trailing-slash variants (`/about/`).
|
||||
*/
|
||||
export function isPrerenderedRoute(url: string): boolean {
|
||||
const pathname = new URL(url, 'http://localhost').pathname;
|
||||
const normalised = pathname.endsWith('/') && pathname !== '/'
|
||||
? pathname.slice(0, -1)
|
||||
: pathname;
|
||||
return PRERENDERED_ROUTES.includes(normalised);
|
||||
}
|
||||
|
||||
/**
|
||||
* Lazily instantiate AngularNodeAppEngine so the engine boots only when
|
||||
* the first request arrives. The engine handles both prerendered route
|
||||
* lookups and on-demand SSR bootstrapping internally.
|
||||
* the first request arrives. All routes use RenderMode.Server — every
|
||||
* request triggers on-demand SSR that fetches fresh API data before rendering.
|
||||
*/
|
||||
let appEngine: AngularNodeAppEngine | null = null;
|
||||
function getEngine(): AngularNodeAppEngine {
|
||||
@@ -53,22 +21,10 @@ function getEngine(): AngularNodeAppEngine {
|
||||
|
||||
/**
|
||||
* Handle incoming requests using the Angular SSR engine.
|
||||
*
|
||||
* Prerendered routes (/ , /about, /schedule, /events, /contact) are served
|
||||
* as pre-generated static HTML from the build output. All other routes
|
||||
* fall back to server-side rendering (SSR) or the fallback 404 handler.
|
||||
* Every request is rendered on the server with fresh data from the API.
|
||||
*/
|
||||
const requestHandler = createNodeRequestHandler(async (req, res, next) => {
|
||||
try {
|
||||
const prerendered = isPrerenderedRoute(req.url || '/');
|
||||
if (prerendered) {
|
||||
console.log(`[SSR] Prerendered route: ${req.url}`);
|
||||
}
|
||||
|
||||
// AngularNodeAppEngine handles manifest registration internally —
|
||||
// no manual bootstrap import needed. For prerendered routes it
|
||||
// returns pre-generated static HTML; for SSR routes it bootstraps
|
||||
// the Angular app on demand.
|
||||
const response = await getEngine().handle(req);
|
||||
|
||||
if (response) {
|
||||
|
||||
@@ -10,34 +10,34 @@ describe('SSR server routes — app.server.routes.ts', () => {
|
||||
expect(serverRoutes).toHaveLength(8);
|
||||
});
|
||||
|
||||
it('should prerender the home route', () => {
|
||||
it('should use SSR mode for the home route', () => {
|
||||
const home = serverRoutes.find((r) => r.path === '');
|
||||
expect(home).toBeDefined();
|
||||
expect(home!.renderMode).toBe(RenderMode.Prerender);
|
||||
expect(home!.renderMode).toBe(RenderMode.Server);
|
||||
});
|
||||
|
||||
it('should prerender /about', () => {
|
||||
it('should use SSR mode for /about', () => {
|
||||
const about = serverRoutes.find((r) => r.path === 'about');
|
||||
expect(about).toBeDefined();
|
||||
expect(about!.renderMode).toBe(RenderMode.Prerender);
|
||||
expect(about!.renderMode).toBe(RenderMode.Server);
|
||||
});
|
||||
|
||||
it('should prerender /schedule', () => {
|
||||
it('should use SSR mode for /schedule', () => {
|
||||
const schedule = serverRoutes.find((r) => r.path === 'schedule');
|
||||
expect(schedule).toBeDefined();
|
||||
expect(schedule!.renderMode).toBe(RenderMode.Prerender);
|
||||
expect(schedule!.renderMode).toBe(RenderMode.Server);
|
||||
});
|
||||
|
||||
it('should prerender /events', () => {
|
||||
it('should use SSR mode for /events', () => {
|
||||
const events = serverRoutes.find((r) => r.path === 'events');
|
||||
expect(events).toBeDefined();
|
||||
expect(events!.renderMode).toBe(RenderMode.Prerender);
|
||||
expect(events!.renderMode).toBe(RenderMode.Server);
|
||||
});
|
||||
|
||||
it('should prerender /contact', () => {
|
||||
it('should use SSR mode for /contact', () => {
|
||||
const contact = serverRoutes.find((r) => r.path === 'contact');
|
||||
expect(contact).toBeDefined();
|
||||
expect(contact!.renderMode).toBe(RenderMode.Prerender);
|
||||
expect(contact!.renderMode).toBe(RenderMode.Server);
|
||||
});
|
||||
|
||||
it('should use SSR mode for /login', () => {
|
||||
@@ -58,13 +58,13 @@ describe('SSR server routes — app.server.routes.ts', () => {
|
||||
expect(wildcard!.renderMode).toBe(RenderMode.Server);
|
||||
});
|
||||
|
||||
it('should have exactly 5 prerendered routes', () => {
|
||||
it('should have no prerendered routes (eliminates stale DEFAULT_CONFIG)', () => {
|
||||
const prerendered = serverRoutes.filter((r) => r.renderMode === RenderMode.Prerender);
|
||||
expect(prerendered).toHaveLength(5);
|
||||
expect(prerendered).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should have exactly 3 SSR routes', () => {
|
||||
it('should have exactly 8 SSR routes (all routes)', () => {
|
||||
const ssr = serverRoutes.filter((r) => r.renderMode === RenderMode.Server);
|
||||
expect(ssr).toHaveLength(3);
|
||||
expect(ssr).toHaveLength(8);
|
||||
});
|
||||
});
|
||||
@@ -3,16 +3,18 @@ import { RenderMode, ServerRoute } from '@angular/ssr';
|
||||
/**
|
||||
* Server routes configuration for Angular SSR.
|
||||
*
|
||||
* The five public routes are prerendered at build time (SSG) for optimal SEO.
|
||||
* Authenticated routes (/login, /admin) use SSR for dynamic rendering.
|
||||
* All other routes fall back to SSR.
|
||||
* ALL routes use RenderMode.Server — every request hits the SSR server,
|
||||
* which fetches fresh station config from the API before rendering HTML.
|
||||
* This eliminates the stale-asset flash that occurred with prerendered HTML
|
||||
* (prerender runs at build time when the API is unavailable, baking in
|
||||
* DEFAULT_CONFIG values).
|
||||
*/
|
||||
export const serverRoutes: ServerRoute[] = [
|
||||
{ path: '', renderMode: RenderMode.Prerender },
|
||||
{ path: 'about', renderMode: RenderMode.Prerender },
|
||||
{ path: 'schedule', renderMode: RenderMode.Prerender },
|
||||
{ path: 'events', renderMode: RenderMode.Prerender },
|
||||
{ path: 'contact', renderMode: RenderMode.Prerender },
|
||||
{ path: '', renderMode: RenderMode.Server },
|
||||
{ path: 'about', renderMode: RenderMode.Server },
|
||||
{ path: 'schedule', renderMode: RenderMode.Server },
|
||||
{ path: 'events', renderMode: RenderMode.Server },
|
||||
{ path: 'contact', renderMode: RenderMode.Server },
|
||||
{ path: 'login', renderMode: RenderMode.Server },
|
||||
{ path: 'admin', renderMode: RenderMode.Server },
|
||||
{ path: '**', renderMode: RenderMode.Server },
|
||||
|
||||
@@ -4,13 +4,20 @@ import { provideServerRendering, withRoutes } from '@angular/ssr';
|
||||
|
||||
import { appConfig } from './app/app.config';
|
||||
import { App } from './app/app';
|
||||
import { APP_CONFIG, AppConfig } from './app/services/app-config.service';
|
||||
import { serverRoutes } from './app/app.server.routes';
|
||||
|
||||
// During SSR, the API base URL must be absolute (HttpClient runs in Node.js).
|
||||
// nginx proxies /api/ to the upstream, so point at nginx (port 80) locally.
|
||||
const ssrApiBaseUrl = process.env.SSR_API_BASE_URL || 'http://localhost:80';
|
||||
const ssrAppConfig: AppConfig = { apiBaseUrl: ssrApiBaseUrl };
|
||||
|
||||
const serverAppConfig = {
|
||||
...appConfig,
|
||||
providers: [
|
||||
...appConfig.providers,
|
||||
{ provide: APP_ID, useValue: 'server' },
|
||||
{ provide: APP_CONFIG, useValue: ssrAppConfig },
|
||||
provideServerRendering(withRoutes(serverRoutes)),
|
||||
],
|
||||
};
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@ export default {
|
||||
test: {
|
||||
globals: true,
|
||||
environment: 'jsdom',
|
||||
include: ['src/**/*.spec.ts'],
|
||||
include: ['src/**/*.spec.ts', 'server.spec.ts'],
|
||||
setupFiles: ['src/test-setup.ts'],
|
||||
reporters: ['default'],
|
||||
coverage: {
|
||||
|
||||
Reference in new issue
Block a user