From cc07c4166f9192511c05ba8fc0f29ec2ab480a86 Mon Sep 17 00:00:00 2001
From: manmohm <205255727+manmohm@users.noreply.github.com>
Date: Mon, 24 Aug 2026 11:46:31 +0300
Subject: [PATCH] fix: add SEO metadata and generated sitemap
---
README.md | 9 ++++++++
package.json | 4 +++-
src/lib/site.ts | 15 +++++++++++++
src/routes/+page.svelte | 18 ++++++++++++++-
src/routes/robots.txt/+server.ts | 16 +++++++++++++
src/routes/sitemap.xml/+server.ts | 37 +++++++++++++++++++++++++++++++
tests/seo.test.mjs | 37 +++++++++++++++++++++++++++++++
7 files changed, 134 insertions(+), 2 deletions(-)
create mode 100644 src/lib/site.ts
create mode 100644 src/routes/robots.txt/+server.ts
create mode 100644 src/routes/sitemap.xml/+server.ts
create mode 100644 tests/seo.test.mjs
diff --git a/README.md b/README.md
index d4564d5..45e182c 100644
--- a/README.md
+++ b/README.md
@@ -28,6 +28,15 @@ npm run build
npm run preview
```
+## Verify SEO output
+
+```bash
+npm test
+```
+
+The check builds the static site and verifies its page metadata, canonical URL,
+generated `sitemap.xml`, and `robots.txt` sitemap declaration.
+
## Deploy to Cloudflare Pages (accountmap.org)
This project is set up for [Cloudflare Pages](https://developers.cloudflare.com/pages/) with the [GitHub integration](https://developers.cloudflare.com/pages/configuration/git-integration/github-integration/).
diff --git a/package.json b/package.json
index f65a7d3..51d0daf 100644
--- a/package.json
+++ b/package.json
@@ -5,7 +5,9 @@
"scripts": {
"dev": "vite dev",
"build": "vite build",
- "preview": "vite preview"
+ "preview": "vite preview",
+ "check:seo": "npm run build && node --test tests/seo.test.mjs",
+ "test": "npm run check:seo"
},
"devDependencies": {
"@sveltejs/adapter-auto": "^3.0.0",
diff --git a/src/lib/site.ts b/src/lib/site.ts
new file mode 100644
index 0000000..e5da609
--- /dev/null
+++ b/src/lib/site.ts
@@ -0,0 +1,15 @@
+export const siteMetadata = {
+ origin: 'https://accountmap.org',
+ name: 'AccountMap',
+ title: 'AccountMap — Visualize and manage your online accounts',
+ description:
+ 'Open-source account management app for tracking login methods and viewing accounts in 3D, 2D, or list views.',
+ locale: 'en_US',
+ socialImagePath: '/screenshots/1.png'
+} as const;
+
+export const publicRoutes = ['/'] as const;
+
+export function absoluteUrl(pathname: string): string {
+ return new URL(pathname, `${siteMetadata.origin}/`).toString();
+}
diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte
index 79ab360..9c55051 100644
--- a/src/routes/+page.svelte
+++ b/src/routes/+page.svelte
@@ -1,5 +1,6 @@
- AccountMap
+ {siteMetadata.title}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/routes/robots.txt/+server.ts b/src/routes/robots.txt/+server.ts
new file mode 100644
index 0000000..ce20848
--- /dev/null
+++ b/src/routes/robots.txt/+server.ts
@@ -0,0 +1,16 @@
+import type { RequestHandler } from './$types';
+import { absoluteUrl } from '$lib/site';
+
+export const prerender = true;
+
+export const GET: RequestHandler = () =>
+ new Response(`User-agent: *
+Allow: /
+
+Sitemap: ${absoluteUrl('/sitemap.xml')}
+`, {
+ headers: {
+ 'Content-Type': 'text/plain; charset=utf-8',
+ 'Cache-Control': 'public, max-age=3600'
+ }
+ });
diff --git a/src/routes/sitemap.xml/+server.ts b/src/routes/sitemap.xml/+server.ts
new file mode 100644
index 0000000..0e982f3
--- /dev/null
+++ b/src/routes/sitemap.xml/+server.ts
@@ -0,0 +1,37 @@
+import type { RequestHandler } from './$types';
+import { absoluteUrl, publicRoutes } from '$lib/site';
+
+export const prerender = true;
+
+function escapeXml(value: string): string {
+ return value
+ .replaceAll('&', '&')
+ .replaceAll('<', '<')
+ .replaceAll('>', '>')
+ .replaceAll('"', '"')
+ .replaceAll("'", ''');
+}
+
+export const GET: RequestHandler = () => {
+ const urls = publicRoutes
+ .map(
+ (pathname) => `
+ ${escapeXml(absoluteUrl(pathname))}
+ `
+ )
+ .join('\n');
+
+ return new Response(
+ `
+
+${urls}
+
+`,
+ {
+ headers: {
+ 'Content-Type': 'application/xml; charset=utf-8',
+ 'Cache-Control': 'public, max-age=3600'
+ }
+ }
+ );
+};
diff --git a/tests/seo.test.mjs b/tests/seo.test.mjs
new file mode 100644
index 0000000..af8efab
--- /dev/null
+++ b/tests/seo.test.mjs
@@ -0,0 +1,37 @@
+import assert from 'node:assert/strict';
+import { readFile } from 'node:fs/promises';
+import test from 'node:test';
+
+const readBuildFile = (pathname) =>
+ readFile(new URL(`../build/${pathname}`, import.meta.url), 'utf8');
+
+test('build emits complete metadata for the landing page', async () => {
+ const html = await readBuildFile('index.html');
+
+ assert.match(html, /AccountMap — Visualize and manage your online accounts<\/title>/);
+ assert.match(html, //);
+ assert.match(html, //);
+ assert.match(html, //);
+ assert.match(html, //);
+ assert.match(html, //);
+ assert.match(html, //);
+});
+
+test('build emits one canonical URL in a valid sitemap document', async () => {
+ const sitemap = await readBuildFile('sitemap.xml');
+
+ assert.match(sitemap, /^<\?xml version="1\.0" encoding="UTF-8"\?>/);
+ assert.match(sitemap, //);
+ assert.match(sitemap, /https:\/\/accountmap\.org\/<\/loc>/);
+ assert.doesNotMatch(sitemap, //i);
+ assert.equal((sitemap.match(//g) ?? []).length, 1, 'sitemap must not contain duplicate URLs');
+});
+
+test('build emits crawler rules that point to the sitemap', async () => {
+ const robots = await readBuildFile('robots.txt');
+
+ assert.match(robots, /^User-agent: \*$/m);
+ assert.match(robots, /^Allow: \/$/m);
+ assert.match(robots, /^Sitemap: https:\/\/accountmap\.org\/sitemap\.xml$/m);
+ assert.doesNotMatch(robots, //i);
+});