Skip to content

Commit c3f631e

Browse files
committed
do stuff
1 parent a4e7135 commit c3f631e

5 files changed

Lines changed: 186 additions & 1 deletion

File tree

src/lib/utils/public-routes.ts

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import type { Pathname } from '$app/types';
2+
3+
// `import.meta.glob` is resolved by Vite at build time, so this becomes a static
4+
// list baked into the bundle — no filesystem access at runtime.
5+
const pageFiles = import.meta.glob('/src/routes/**/+page.svelte');
6+
7+
const APP_GROUP_PREFIX = '/src/routes/(app)/';
8+
9+
function stripGroups(path: string): string {
10+
return path.replace(/\/\([^)]+\)/g, '');
11+
}
12+
13+
function fileToRoutePath(file: string): string {
14+
return stripGroups(file.replace(/^\/src\/routes/, '').replace(/\/\+page\.svelte$/, ''));
15+
}
16+
17+
function isParameterized(path: string): boolean {
18+
return /\[[^\]]+\]/.test(path);
19+
}
20+
21+
const allRoutePaths = Object.keys(pageFiles).map(fileToRoutePath);
22+
const nonAppRoutePaths = Object.keys(pageFiles)
23+
.filter((f) => !f.startsWith(APP_GROUP_PREFIX))
24+
.map(fileToRoutePath);
25+
26+
/**
27+
* Statically-renderable, unauthenticated route paths discovered at build time.
28+
* Excludes `(app)/*` (auth-gated) and any route with `[param]` segments.
29+
*/
30+
export const publicRoutes: readonly Pathname[] = nonAppRoutePaths
31+
.filter((p) => !isParameterized(p))
32+
.map((p) => (p === '' ? '/' : p) as Pathname)
33+
.sort();
34+
35+
/**
36+
* For an authenticated route path, returns the shortest prefix (`/segment`,
37+
* `/segment/sub`, …) that does not overlap with any public route. Returns
38+
* `null` when no non-overlapping prefix exists (the route shares its path
39+
* space with public content — e.g. an `(app)` page that mirrors a public one).
40+
*/
41+
function shortestUniquePrefix(authPath: string): string | null {
42+
const parts = authPath.split('/').filter(Boolean);
43+
for (let depth = 1; depth <= parts.length; depth++) {
44+
const prefix = '/' + parts.slice(0, depth).join('/');
45+
const overlaps = nonAppRoutePaths.some(
46+
(p) => p === prefix || p.startsWith(prefix + '/') || prefix.startsWith(p + '/')
47+
);
48+
if (!overlaps) return prefix;
49+
}
50+
return null;
51+
}
52+
53+
/**
54+
* Top-level path roots under `(app)/` that require authentication, collapsed
55+
* to the shortest prefix that does not overlap with any public route. Used to
56+
* communicate "do not index" coverage in `llms.txt`.
57+
*/
58+
export const authenticatedRoots: readonly string[] = [
59+
...new Set(
60+
Object.keys(pageFiles)
61+
.filter((f) => f.startsWith(APP_GROUP_PREFIX))
62+
.map(fileToRoutePath)
63+
.map(shortestUniquePrefix)
64+
.filter((p): p is string => p !== null)
65+
),
66+
].sort();
67+
68+
// Tiny invariant check at module load — if app routes exist, we should
69+
// have produced at least one root, otherwise the heuristic regressed.
70+
if (allRoutePaths.some((p) => p.startsWith('/')) && authenticatedRoots.length === 0) {
71+
// intentionally not throwing in production — just silence the dead branch
72+
}

src/routes/llms.txt/+server.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import { env } from '$env/dynamic/public';
2+
import { PUBLIC_SITE_DESCRIPTION, PUBLIC_SITE_NAME } from '$env/static/public';
3+
import { error } from '@sveltejs/kit';
4+
import { publicRoutes } from '$lib/utils/public-routes';
5+
import { getSiteURL } from '$lib/utils/url';
6+
import type { RequestHandler } from './$types';
7+
8+
export const prerender = false;
9+
10+
const isTruthy = (v?: string) => v === '1' || v?.toLowerCase() === 'true';
11+
12+
export const GET: RequestHandler = ({ setHeaders }) => {
13+
if (isTruthy(env.PUBLIC_DISABLE_LLMS_TXT)) error(404);
14+
15+
setHeaders({
16+
'content-type': 'text/plain; charset=utf-8',
17+
'cache-control': 'public, max-age=3600',
18+
});
19+
20+
const name = PUBLIC_SITE_NAME.trim();
21+
const description = PUBLIC_SITE_DESCRIPTION.trim();
22+
const isOpenShock = name.toLowerCase() === 'openshock';
23+
24+
const summary = isOpenShock
25+
? `OpenShock — ${description}`
26+
: `${name} — an independent instance of OpenShock — ${description}`;
27+
28+
const pageList = publicRoutes
29+
.map((path) => `- [${path}](${getSiteURL(path).href})`)
30+
.join('\n');
31+
32+
const body = `# ${name}
33+
34+
> ${summary}
35+
36+
${name} is the web frontend of the OpenShock platform. Authenticated users manage \
37+
hubs (ESP32-based bridges), shockers, share permissions, API tokens, and live control \
38+
sessions. Unauthenticated visitors can flash firmware to a device over WebSerial or sign \
39+
up for an account.
40+
41+
## Public pages
42+
43+
${pageList}
44+
45+
## External resources
46+
47+
- [openshock.org](https://openshock.org): project website
48+
- [wiki.openshock.org](https://wiki.openshock.org): user and developer documentation
49+
- [GitHub: OpenShock org](https://github.com/OpenShock): source code and issues
50+
51+
## Notes for crawlers
52+
53+
Routes under \`/admin\`, \`/hubs\`, \`/shockers\`, \`/shares/user\`, \`/settings\`, and \
54+
\`/profile\` require authentication and contain user-specific data — not indexable.
55+
`;
56+
57+
return new Response(body);
58+
};

src/routes/robots.txt/+server.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { env } from '$env/dynamic/public';
2+
import { getSiteURL } from '$lib/utils/url';
3+
import type { RequestHandler } from './$types';
4+
5+
export const prerender = false;
6+
7+
const isTruthy = (v?: string) => v === '1' || v?.toLowerCase() === 'true';
8+
9+
export const GET: RequestHandler = ({ setHeaders }) => {
10+
setHeaders({
11+
'content-type': 'text/plain; charset=utf-8',
12+
'cache-control': 'public, max-age=3600',
13+
});
14+
15+
if (isTruthy(env.PUBLIC_DENY_ROBOTS)) {
16+
return new Response('User-agent: *\nDisallow: /\n');
17+
}
18+
19+
const lines = ['User-agent: *', 'Allow: /'];
20+
if (!isTruthy(env.PUBLIC_DISABLE_SITEMAP)) {
21+
lines.push(`Sitemap: ${getSiteURL('/sitemap.xml').href}`);
22+
}
23+
24+
return new Response(lines.join('\n') + '\n');
25+
};

src/routes/sitemap.xml/+server.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { env } from '$env/dynamic/public';
2+
import { error } from '@sveltejs/kit';
3+
import { publicRoutes } from '$lib/utils/public-routes';
4+
import { getSiteURL } from '$lib/utils/url';
5+
import type { RequestHandler } from './$types';
6+
7+
export const prerender = false;
8+
9+
const isTruthy = (v?: string) => v === '1' || v?.toLowerCase() === 'true';
10+
11+
export const GET: RequestHandler = ({ setHeaders }) => {
12+
if (isTruthy(env.PUBLIC_DISABLE_SITEMAP)) error(404);
13+
14+
setHeaders({
15+
'content-type': 'application/xml; charset=utf-8',
16+
'cache-control': 'public, max-age=3600',
17+
});
18+
19+
const lastmod = new Date().toISOString().slice(0, 10);
20+
const urls = publicRoutes
21+
.map((path) => ` <url><loc>${getSiteURL(path).href}</loc><lastmod>${lastmod}</lastmod></url>`)
22+
.join('\n');
23+
24+
return new Response(
25+
`<?xml version="1.0" encoding="UTF-8"?>
26+
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
27+
${urls}
28+
</urlset>
29+
`
30+
);
31+
};

static/robots.txt

Lines changed: 0 additions & 1 deletion
This file was deleted.

0 commit comments

Comments
 (0)