import { AngularNodeAppEngine, createNodeRequestHandler, isMainModule, writeResponseToNodeResponse, } from '@angular/ssr/node'; import express from 'express'; import { join } from 'node:path'; const browserDistFolder = join(import.meta.dirname, '../browser'); const app = express(); const angularApp = new AngularNodeAppEngine({ allowedHosts: [ 'corifeus.com', 'www.corifeus.com', 'localhost', '127.0.0.1', ], }); const CANONICAL_HOST = 'https://corifeus.com'; const REPO_API_URL = 'https://network.corifeus.com/public/api/repo'; const SITEMAP_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours let sitemapCache: { body: string; builtAt: number } | null = null; async function fetchReposForSitemap(): Promise { const response = await fetch(REPO_API_URL, { headers: { 'accept': 'application/json' }, }); if (!response.ok) { throw new Error(`Repo API returned ${response.status}`); } const data: any = await response.json(); return data.repo || {}; } function buildSitemap(repos: Record): string { const now = new Date(); const nowIso = now.toISOString(); const urls: string[] = []; urls.push(` ${CANONICAL_HOST}/ ${nowIso} daily 1.0 `); urls.push(` ${CANONICAL_HOST}/matrix ${nowIso} daily 1.0 `); const xmlEscape = (s: string) => s.replace(/&/g, '&').replace(//g, '>') .replace(/"/g, '"').replace(/'/g, '''); for (const key of Object.keys(repos)) { const pkg = repos[key]; const repoName = pkg?.corifeus?.reponame; if (!repoName) continue; if (repoName === 'corifeus') continue; const timeStamp = pkg?.corifeus?.['time-stamp']; let lastmod = nowIso; if (timeStamp) { const d = new Date(timeStamp); if (!isNaN(d.getTime())) { lastmod = d.toISOString(); } } const escapedRepo = encodeURIComponent(repoName); const priority = (pkg?.corifeus?.stargazers_count ?? 0) > 100 ? '0.9' : '0.7'; urls.push(` ${CANONICAL_HOST}/${escapedRepo} ${lastmod} weekly ${priority} `); // Also index any documented sub-pages declared in the package's menu. const menu: any[] = Array.isArray(pkg?.corifeus?.menu) ? pkg.corifeus.menu : []; for (const item of menu) { const link: string | undefined = item?.link; if (!link) continue; if (/^https?:\/\//i.test(link)) continue; // skip external links const cleanLink = link.startsWith('/') ? link.slice(1) : link; urls.push(` ${CANONICAL_HOST}/${escapedRepo}/${xmlEscape(cleanLink)} ${lastmod} weekly 0.5 `); } } return ` ${urls.join('\n')} `; } app.get('/sitemap.xml', async (_req, res, next) => { try { const now = Date.now(); if (!sitemapCache || now - sitemapCache.builtAt > SITEMAP_TTL_MS) { const repos = await fetchReposForSitemap(); const body = buildSitemap(repos); sitemapCache = { body, builtAt: now }; } res.setHeader('Content-Type', 'application/xml; charset=utf-8'); res.setHeader('Cache-Control', 'public, max-age=86400, s-maxage=86400'); res.send(sitemapCache.body); } catch (error) { next(error); } }); let manifestCache: { body: string; builtAt: number } | null = null; const MANIFEST_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours app.get('/manifest.webmanifest', async (_req, res, next) => { try { const now = Date.now(); if (!manifestCache || now - manifestCache.builtAt > MANIFEST_TTL_MS) { const repos = await fetchReposForSitemap(); const sorted = Object.values(repos) .filter((pkg: any) => pkg?.corifeus?.reponame && pkg.corifeus.reponame !== 'corifeus') .sort((a: any, b: any) => (b?.corifeus?.stargazers_count ?? 0) - (a?.corifeus?.stargazers_count ?? 0)); const shortcuts = sorted.map((pkg: any) => { const repoName = pkg.corifeus.reponame; const prefix = pkg.corifeus.prefix; const shortName = prefix ? pkg.name.substr(prefix.length) : pkg.name; return { name: pkg.description || shortName, short_name: shortName, description: pkg.description || `${shortName} by Corifeus`, url: `/${repoName}`, icons: [{ src: 'assets/favicon.ico', sizes: '48x48', type: 'image/x-icon' }], }; }); shortcuts.unshift({ name: 'Corifeus Matrix', short_name: 'Matrix', description: 'Browse all Corifeus open source packages', url: '/matrix', icons: [{ src: 'assets/favicon.ico', sizes: '48x48', type: 'image/x-icon' }], }); const manifest = { name: 'Corifeus App Web Pages', short_name: 'Corifeus', description: 'Open source documentation portal for Angular, Node.js packages and developer tools', id: '/', start_url: '/', scope: '/', display: 'standalone', display_override: ['standalone', 'minimal-ui', 'browser'], orientation: 'any', background_color: '#ffffff', theme_color: '#1b5e20', lang: 'en', dir: 'ltr', categories: ['developer', 'productivity', 'utilities'], icons: [ { src: 'assets/favicon.ico', sizes: '48x48', type: 'image/x-icon' }, ], shortcuts, }; manifestCache = { body: JSON.stringify(manifest, null, 2), builtAt: now, }; } res.setHeader('Content-Type', 'application/manifest+json; charset=utf-8'); res.setHeader('Cache-Control', 'public, max-age=86400, s-maxage=86400'); res.send(manifestCache.body); } catch (error) { next(error); } }); app.get('/robots.txt', (_req, res) => { res.setHeader('Content-Type', 'text/plain; charset=utf-8'); res.setHeader('Cache-Control', 'public, max-age=3600'); res.send(`User-agent: * Allow: / User-agent: GPTBot Allow: / User-agent: ClaudeBot Allow: / User-agent: PerplexityBot Allow: / User-agent: Google-Extended Allow: / User-agent: CCBot Allow: / Sitemap: ${CANONICAL_HOST}/sitemap.xml `); }); /** * Serve static files from /browser */ app.use( express.static(browserDistFolder, { maxAge: '1y', index: false, redirect: false, }), ); /** * Handle all other requests by rendering the Angular application. */ app.use((req, res, next) => { angularApp .handle(req) .then((response) => response ? writeResponseToNodeResponse(response, res) : next(), ) .catch(next); }); /** * Start the server if this module is the main entry point, or it is ran via PM2. * The server listens on the port defined by the `PORT` environment variable, or defaults to 4000. */ if (isMainModule(import.meta.url) || process.env['pm_id']) { const port = process.env['PORT'] || 4444; app.listen(port, (error) => { if (error) { throw error; } console.log(`Node Express server listening on http://localhost:${port}`); }); } /** * Request handler used by the Angular CLI (for dev-server and during build) or Firebase Cloud Functions. */ export const reqHandler = createNodeRequestHandler(app);