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('