import * as plugins from '../plugins.js'; import * as paths from '../paths.js'; /** * Built-in routes controller for TypedServer * Handles robots.txt, manifest.json, sitemap, feed, appversion */ @plugins.smartserve.Route('') export class BuiltInRoutesController { private options: { domain?: string; robots?: boolean; manifest?: plugins.smartmanifest.SmartManifest; sitemap?: boolean; feed?: boolean; appVersion?: string; feedMetadata?: plugins.smartfeed.IFeedOptions; articleGetterFunction?: () => Promise; blockWaybackMachine?: boolean; getSitemapUrls: () => plugins.smartsitemap.IUrlInfo[]; }; constructor(options: typeof BuiltInRoutesController.prototype.options) { this.options = options; } @plugins.smartserve.Get('/robots.txt') async getRobots(ctx: plugins.smartserve.IRequestContext): Promise { if (!this.options.robots || !this.options.domain) { throw new plugins.smartserve.RouteNotFoundError(ctx.path, ctx.method); } const robotsContent = [ 'User-agent: *', 'Allow: /', `Sitemap: https://${this.options.domain}/sitemap`, ]; if (this.options.blockWaybackMachine) { robotsContent.push('', 'User-agent: ia_archiver', 'Disallow: /'); } return new Response(robotsContent.join('\n'), { status: 200, headers: { 'Content-Type': 'text/plain' }, }); } @plugins.smartserve.Get('/manifest.json') async getManifest(ctx: plugins.smartserve.IRequestContext): Promise { if (!this.options.manifest) { throw new plugins.smartserve.RouteNotFoundError(ctx.path, ctx.method); } return new Response(this.options.manifest.jsonString(), { status: 200, headers: { 'Content-Type': 'application/json' }, }); } @plugins.smartserve.Get('/sitemap') async getSitemap(ctx: plugins.smartserve.IRequestContext): Promise { if (!this.options.sitemap || !this.options.domain) { throw new plugins.smartserve.RouteNotFoundError(ctx.path, ctx.method); } const smartsitemap = new plugins.smartsitemap.SmartSitemap(); const urls = this.options.getSitemapUrls(); const sitemapXml = await smartsitemap.createSitemapFromUrlInfoArray(urls); return new Response(sitemapXml, { status: 200, headers: { 'Content-Type': 'application/xml' }, }); } @plugins.smartserve.Get('/sitemap-news') async getSitemapNews(ctx: plugins.smartserve.IRequestContext): Promise { if (!this.options.sitemap || !this.options.domain || !this.options.articleGetterFunction) { throw new plugins.smartserve.RouteNotFoundError(ctx.path, ctx.method); } const smartsitemap = new plugins.smartsitemap.SmartSitemap(); const articles = await this.options.articleGetterFunction(); const sitemapNewsXml = await smartsitemap.createSitemapNewsFromArticleArray(articles); return new Response(sitemapNewsXml, { status: 200, headers: { 'Content-Type': 'application/xml' }, }); } @plugins.smartserve.Get('/feed') async getFeed(ctx: plugins.smartserve.IRequestContext): Promise { if (!this.options.feed || !this.options.feedMetadata) { throw new plugins.smartserve.RouteNotFoundError(ctx.path, ctx.method); } const smartfeed = new plugins.smartfeed.Smartfeed(); const articles = this.options.articleGetterFunction ? await this.options.articleGetterFunction() : []; const feedXml = await smartfeed.createFeedFromArticleArray( this.options.feedMetadata, articles ); return new Response(feedXml, { status: 200, headers: { 'Content-Type': 'application/atom+xml' }, }); } @plugins.smartserve.Get('/appversion') async getAppVersion(ctx: plugins.smartserve.IRequestContext): Promise { if (!this.options.appVersion) { throw new plugins.smartserve.RouteNotFoundError(ctx.path, ctx.method); } return new Response(this.options.appVersion, { status: 200, headers: { 'Content-Type': 'text/plain' }, }); } @plugins.smartserve.Get('/sw-dash') async getSwDash(ctx: plugins.smartserve.IRequestContext): Promise { // Import shared HTML from interfaces const { SW_DASH_HTML } = await import('../../dist_ts_interfaces/serviceworker.js'); return new Response(SW_DASH_HTML, { status: 200, headers: { 'Content-Type': 'text/html' }, }); } // SW-dash data routes - return empty/unavailable when SW isn't active @plugins.smartserve.Get('/sw-dash/metrics') async getSwDashMetrics(): Promise { return new Response(JSON.stringify({ error: 'Service worker not active', data: null }), { status: 503, headers: { 'Content-Type': 'application/json' }, }); } @plugins.smartserve.Get('/sw-dash/resources') async getSwDashResources(): Promise { return new Response(JSON.stringify({ error: 'Service worker not active', resources: [], domains: [], contentTypes: [], resourceCount: 0 }), { status: 503, headers: { 'Content-Type': 'application/json' }, }); } @plugins.smartserve.Get('/sw-dash/events') async getSwDashEvents(): Promise { return new Response(JSON.stringify({ error: 'Service worker not active', events: [], total: 0 }), { status: 503, headers: { 'Content-Type': 'application/json' }, }); } @plugins.smartserve.Get('/sw-dash/events/count') async getSwDashEventsCount(): Promise { return new Response(JSON.stringify({ error: 'Service worker not active', count: 0 }), { status: 503, headers: { 'Content-Type': 'application/json' }, }); } @plugins.smartserve.Get('/sw-dash/cumulative-metrics') async getSwDashCumulativeMetrics(): Promise { return new Response(JSON.stringify({ error: 'Service worker not active', data: null }), { status: 503, headers: { 'Content-Type': 'application/json' }, }); } @plugins.smartserve.Get('/sw-dash/speedtest') async getSwDashSpeedtest(): Promise { return new Response(JSON.stringify({ error: 'Service worker not active - speedtest unavailable' }), { status: 503, headers: { 'Content-Type': 'application/json' }, }); } @plugins.smartserve.Get('/sw-dash/bundle.js') async getSwDashBundle(ctx: plugins.smartserve.IRequestContext): Promise { try { const bundleContent = (await plugins.fsInstance .file(paths.swdashBundlePath) .encoding('utf8') .read()) as string; return new Response(bundleContent, { status: 200, headers: { 'Content-Type': 'text/javascript', 'Cache-Control': 'no-cache', }, }); } catch (error) { console.error('Failed to serve sw-dash bundle:', error); return new Response('SW-Dash bundle not found', { status: 404 }); } } @plugins.smartserve.Get('/serviceworker.bundle.js') async getServiceWorkerBundle(): Promise { try { const bundleContent = (await plugins.fsInstance .file(paths.serviceworkerBundlePath) .encoding('utf8') .read()) as string; return new Response(bundleContent, { status: 200, headers: { 'Content-Type': 'text/javascript', 'Cache-Control': 'no-cache', }, }); } catch (error) { console.error('Failed to serve serviceworker bundle:', error); return new Response('ServiceWorker bundle not found', { status: 404 }); } } }