diff --git a/landing/favicon.svg b/landing/favicon.svg new file mode 100644 index 0000000..66194f9 --- /dev/null +++ b/landing/favicon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/landing/index.html b/landing/index.html index d786ea0..bb1c354 100644 --- a/landing/index.html +++ b/landing/index.html @@ -15,6 +15,9 @@ + + + diff --git a/src/routes/agent-discovery.ts b/src/routes/agent-discovery.ts index 68525d7..72b25e7 100644 --- a/src/routes/agent-discovery.ts +++ b/src/routes/agent-discovery.ts @@ -85,6 +85,15 @@ async function readSkillMd(): Promise { return cachedSkillMd; } +let cachedFaviconSvg: string | null = null; + +async function readFaviconSvg(): Promise { + if (cachedFaviconSvg === null) { + cachedFaviconSvg = await readFile(resolve(process.cwd(), 'landing/favicon.svg'), 'utf-8'); + } + return cachedFaviconSvg; +} + const agentDiscoveryRoutes: FastifyPluginCallback = (fastify, _options, done) => { fastify.get('/robots.txt', async (_req, reply) => { return reply.type('text/plain; charset=utf-8').status(200).send(ROBOTS_TXT); @@ -103,6 +112,17 @@ const agentDiscoveryRoutes: FastifyPluginCallback = (fastify, _options, done) => return reply.type('text/markdown; charset=utf-8').status(200).send(body); }); + // /favicon.ico — legacy fallback path that crawlers and old browsers probe + // unconditionally. Serving the same SVG (with the correct image/svg+xml + // content type) means the 200 satisfies the probe and stops the request, + // which kills the steady stream of /favicon.ico 404s in production logs. + // Modern browsers prefer the in + // landing/index.html and never hit this route. + fastify.get('/favicon.ico', async (_req, reply) => { + const body = await readFaviconSvg(); + return reply.type('image/svg+xml').status(200).send(body); + }); + done(); }; diff --git a/tests/unit/routes/agent-discovery.test.ts b/tests/unit/routes/agent-discovery.test.ts index 53be430..35bf566 100644 --- a/tests/unit/routes/agent-discovery.test.ts +++ b/tests/unit/routes/agent-discovery.test.ts @@ -1,4 +1,4 @@ -import { mkdtempSync, writeFileSync, rmSync } from 'node:fs'; +import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; @@ -26,6 +26,12 @@ describe('Agent-discovery routes', () => { originalCwd = process.cwd(); tmpDir = mkdtempSync(join(tmpdir(), 'cardano402-skill-')); writeFileSync(join(tmpDir, 'SKILL.md'), '# Test SKILL\n\nFixture body.\n'); + // The /favicon.ico route reads `${cwd}/landing/favicon.svg` -- fixture it. + mkdirSync(join(tmpDir, 'landing')); + writeFileSync( + join(tmpDir, 'landing', 'favicon.svg'), + '\n' + ); process.chdir(tmpDir); }); @@ -110,6 +116,22 @@ describe('Agent-discovery routes', () => { }); }); + describe('GET /favicon.ico', () => { + it('returns 200 with image/svg+xml content type (kills bot 404 noise)', async () => { + server = await createServer(); + const res = await server.inject({ method: 'GET', url: '/favicon.ico' }); + expect(res.statusCode).toBe(200); + expect(res.headers['content-type']).toContain('image/svg+xml'); + }); + + it('serves the SVG body so even bots that ignore get a valid image', async () => { + server = await createServer(); + const res = await server.inject({ method: 'GET', url: '/favicon.ico' }); + expect(res.body).toContain(''); + }); + }); + describe('GET /SKILL.md', () => { it('returns 200 with text/markdown content', async () => { server = await createServer();