Skip to content

Commit 7b24195

Browse files
authored
Merge pull request #12 from sampleXbro/develop
Fix website SEO deployment and link handling
2 parents 0611ac6 + b99e18c commit 7b24195

22 files changed

Lines changed: 283 additions & 66 deletions

.github/workflows/deploy-website.yml

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,9 @@
77
#
88
# Live site: https://samplexbro.github.io/agentsmesh/ (see website/astro.config.mjs `site` + `base`).
99
#
10-
# SEO: set repository variable DEPLOY_SITE_URL to the exact origin you want indexed
11-
# (HTTPS, no trailing slash), e.g. https://samplexbro.github.io or your custom domain.
10+
# SEO: set repository variable DEPLOY_SITE_URL to the exact public site URL
11+
# you want indexed, e.g. https://samplexbro.github.io/agentsmesh/ or
12+
# https://docs.agentsmesh.dev/. Custom-domain builds emit CNAME automatically.
1213
# Configure DNS or CDN to 301 the non-canonical hostname (www ↔ apex) to that URL.
1314
name: Deploy Website
1415

@@ -55,6 +56,10 @@ jobs:
5556
working-directory: website
5657
run: pnpm install --frozen-lockfile
5758

59+
- name: Run website tests
60+
working-directory: website
61+
run: pnpm test
62+
5863
- name: Build website
5964
working-directory: website
6065
env:

tasks/todo.md

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,59 @@
1+
# Website SEO hardening pass
2+
3+
- [x] Reproduce the current website SEO artifact/output state and confirm root causes for the reported issues
4+
- [x] Add failing tests first for deploy URL parsing and generated SEO artifacts (`robots.txt`, canonical URL/base handling, custom-domain `CNAME`)
5+
- [x] Implement the minimal website config/build changes to fix the reproducible issues
6+
- [x] Improve crawlable homepage copy to address the low text-to-code warning without changing product meaning
7+
- [x] Run website verification plus post-feature QA and append review notes
8+
9+
## Review (Website SEO hardening pass)
10+
11+
- Changes implemented:
12+
- replaced the website’s hardcoded GitHub Pages path assumptions with a single resolved `DEPLOY_SITE_URL` contract that derives origin, base path, public URLs, and optional custom-domain `CNAME` output from one source of truth
13+
- expanded the SEO build integration so it always writes `robots.txt` and writes `CNAME` automatically for custom-domain deployments
14+
- converted hardcoded `/agentsmesh/...` doc links to base-agnostic relative links so the docs build works both on the current GitHub Pages project URL and on a root custom domain
15+
- added more crawlable homepage copy to improve the low text-to-code ratio without changing the product positioning
16+
- added website unit tests and wired them into `website/package.json` plus the deploy workflow so the SEO behavior is enforced in automation
17+
- Tests added:
18+
- `website/site-url.test.mjs`
19+
- `website/integrations/seo-robots.test.mjs`
20+
- Verification:
21+
- `node --test website/site-url.test.mjs website/integrations/seo-robots.test.mjs`
22+
- `node ./node_modules/astro/astro.js build` (in `website/`)
23+
- `DEPLOY_SITE_URL=https://docs.agentsmesh.dev/ node ./node_modules/astro/astro.js build` (in `website/`)
24+
- `rg -n 'href="/agentsmesh/|src="/agentsmesh/|https://docs.agentsmesh.dev/agentsmesh' website/dist -g '*.html'`
25+
- inspected generated `website/dist/robots.txt` and custom-domain `website/dist/CNAME`
26+
- QA Report — Website SEO hardening pass
27+
28+
### Acceptance Criteria
29+
30+
| Criterion | Covered by test? | Status |
31+
| --- | --- | --- |
32+
| Deploy URL handling supports both GitHub Pages project paths and root custom domains | `website/site-url.test.mjs`, default/custom Astro builds | OK |
33+
| SEO artifact generation emits the correct `robots.txt` and optional `CNAME` from the same deploy URL | `website/integrations/seo-robots.test.mjs`, custom-domain Astro build | OK |
34+
| Internal docs links remain valid when the website base path changes | default/custom Astro builds plus no `/agentsmesh/` matches in custom-domain HTML output | OK |
35+
| Homepage ships more crawlable explanatory copy to help the text-to-code warning | updated `website/src/content/docs/index.mdx`, verified in Astro build output | OK |
36+
| Website deploy automation exercises the new SEO tests before building | `website/package.json`, `.github/workflows/deploy-website.yml` | OK |
37+
38+
### Edge Cases
39+
40+
| Scenario | Covered? | Test location |
41+
| --- | --- | --- |
42+
| Project-site deployment under `/agentsmesh` keeps the base path in canonical URLs and sitemap links || `website/site-url.test.mjs` |
43+
| Root custom-domain deployment drops the `/agentsmesh` path entirely || `website/site-url.test.mjs`, custom `astro build` |
44+
| GitHub Pages host does not emit an unnecessary `CNAME` file || `website/site-url.test.mjs`, default `astro build` |
45+
| Custom-domain host emits `CNAME` and root sitemap URL || `website/integrations/seo-robots.test.mjs`, custom `astro build` |
46+
| Built HTML for a root custom domain contains no stale `/agentsmesh` href/src references || `rg -n 'href="/agentsmesh/|src="/agentsmesh/|https://docs.agentsmesh.dev/agentsmesh' website/dist -g '*.html'` |
47+
48+
### Gaps Identified
49+
50+
- none in the implemented website changes; the live site still needs `DEPLOY_SITE_URL` pointed at a root custom domain plus DNS hostname redirection for the SEO report to stop seeing the GitHub Pages project-path redirect
51+
52+
### Actions Taken
53+
54+
- proved the current GitHub Pages project-path build already generated `robots.txt`, then fixed the underlying deploy contract so the site can also ship from a root custom domain where root-level SEO files and non-redirected status codes are possible
55+
- protected the new behavior with unit tests and deploy-workflow coverage instead of leaving it as a one-off config tweak
56+
157
# TypeScript error repair pass
258

359
- [x] Run `pnpm typecheck` and capture the full current TypeScript error set
@@ -218,6 +274,13 @@
218274

219275
| Criterion | Covered by test? | Status |
220276
| --- | --- | --- |
277+
278+
# Website SEO issue repair pass
279+
280+
- [ ] Reproduce the current website SEO output and confirm which issues are fixable in-repo vs blocked by hosting/domain constraints
281+
- [ ] Add failing tests first for the website SEO artifacts and URL behavior we can enforce from the repo
282+
- [ ] Implement the minimal website/deploy changes to fix the reproducible SEO issues
283+
- [ ] Run verification plus post-feature QA and append review notes
221284
| Recover the global branch threshold without lowering config | `pnpm test:coverage -- --coverage.reporter=json-summary --coverage.reporter=text-summary` | OK |
222285
| Add targeted branch tests instead of broad fixture churn | `tests/unit/config/git-remote.test.ts`, `tests/unit/install/install-manifest.test.ts`, `tests/unit/install/git-pin.test.ts`, `tests/unit/install/install-conflicts.branches.test.ts` | OK |
223286
| Keep full suite stable under coverage load | `tests/unit/cli/commands/watch.test.ts`, full `pnpm test:coverage` run | OK |

website/astro.config.mjs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,16 @@
22
import { defineConfig } from 'astro/config';
33
import starlight from '@astrojs/starlight';
44
import seoRobotsIntegration from './integrations/seo-robots.mjs';
5-
import { absoluteFromBase, getSiteOrigin } from './site-url.mjs';
5+
import { absoluteFromBase, fromBase, getSiteBase, getSiteOrigin, resolveDeploySite } from './site-url.mjs';
66

7+
const deploySite = resolveDeploySite();
78
const site = getSiteOrigin();
89
const ogImage = absoluteFromBase('/og-image.png');
910

1011
export default defineConfig({
1112
site,
1213
trailingSlash: 'always',
13-
base: '/agentsmesh',
14+
base: getSiteBase(),
1415
integrations: [
1516
starlight({
1617
title: 'AgentsMesh',
@@ -52,7 +53,7 @@ export default defineConfig({
5253
},
5354
{
5455
tag: 'link',
55-
attrs: { rel: 'icon', href: '/agentsmesh/favicon.svg', type: 'image/svg+xml' },
56+
attrs: { rel: 'icon', href: fromBase('/favicon.svg'), type: 'image/svg+xml' },
5657
},
5758
],
5859
sidebar: [
@@ -123,6 +124,6 @@ export default defineConfig({
123124
},
124125
],
125126
}),
126-
seoRobotsIntegration(() => getSiteOrigin()),
127+
seoRobotsIntegration(() => deploySite.publicUrl),
127128
],
128129
});

website/integrations/seo-robots.mjs

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,36 @@
11
import { writeFileSync } from 'node:fs';
22

3+
import { getCnameValue, resolveDeploySite } from '../site-url.mjs';
4+
5+
export function buildRobotsTxt(raw = null) {
6+
const { publicUrl } = resolveDeploySite(raw);
7+
return `User-agent: *
8+
Allow: /
9+
10+
Sitemap: ${publicUrl}/sitemap-index.xml
11+
`;
12+
}
13+
14+
export function buildSeoArtifacts(raw = null) {
15+
const artifacts = [{ fileName: 'robots.txt', content: buildRobotsTxt(raw) }];
16+
const cname = getCnameValue(raw);
17+
if (cname) {
18+
artifacts.push({ fileName: 'CNAME', content: cname });
19+
}
20+
return artifacts;
21+
}
22+
323
/**
4-
* @param {() => string} getOrigin Host-only HTTPS URL, no trailing slash
24+
* @param {() => string | null | undefined} getDeploySiteUrl Full public site URL
525
*/
6-
export default function seoRobotsIntegration(getOrigin) {
26+
export default function seoRobotsIntegration(getDeploySiteUrl) {
727
return {
828
name: 'seo-robots',
929
hooks: {
1030
'astro:build:done': ({ dir }) => {
11-
const origin = getOrigin().replace(/\/$/, '');
12-
const body = `User-agent: *
13-
Allow: /
14-
15-
Sitemap: ${origin}/agentsmesh/sitemap-index.xml
16-
`;
17-
writeFileSync(new URL('robots.txt', dir), body, 'utf8');
31+
for (const artifact of buildSeoArtifacts(getDeploySiteUrl())) {
32+
writeFileSync(new URL(artifact.fileName, dir), artifact.content, 'utf8');
33+
}
1834
},
1935
},
2036
};
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import test from 'node:test';
2+
import assert from 'node:assert/strict';
3+
4+
import { buildSeoArtifacts, buildRobotsTxt } from './seo-robots.mjs';
5+
6+
test('buildRobotsTxt points crawlers at the public sitemap URL', () => {
7+
assert.equal(
8+
buildRobotsTxt('https://samplexbro.github.io/agentsmesh/'),
9+
`User-agent: *
10+
Allow: /
11+
12+
Sitemap: https://samplexbro.github.io/agentsmesh/sitemap-index.xml
13+
`,
14+
);
15+
});
16+
17+
test('buildSeoArtifacts adds CNAME for custom domains and skips it for github.io', () => {
18+
assert.deepEqual(buildSeoArtifacts('https://samplexbro.github.io/agentsmesh/'), [
19+
{
20+
fileName: 'robots.txt',
21+
content: `User-agent: *
22+
Allow: /
23+
24+
Sitemap: https://samplexbro.github.io/agentsmesh/sitemap-index.xml
25+
`,
26+
},
27+
]);
28+
29+
assert.deepEqual(buildSeoArtifacts('https://docs.agentsmesh.dev/'), [
30+
{
31+
fileName: 'robots.txt',
32+
content: `User-agent: *
33+
Allow: /
34+
35+
Sitemap: https://docs.agentsmesh.dev/sitemap-index.xml
36+
`,
37+
},
38+
{
39+
fileName: 'CNAME',
40+
content: 'docs.agentsmesh.dev\n',
41+
},
42+
]);
43+
});

website/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
"private": true,
66
"scripts": {
77
"dev": "astro dev",
8+
"test": "node --test",
89
"build": "astro build",
910
"preview": "astro preview",
1011
"astro": "astro"

website/site-url.mjs

Lines changed: 51 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,62 @@
1+
const DEFAULT_DEPLOY_SITE_URL = 'https://samplexbro.github.io/agentsmesh/';
2+
3+
function normalizeBasePath(pathname) {
4+
const trimmed = pathname.replace(/\/+$/, '');
5+
return trimmed === '' ? '/' : trimmed;
6+
}
7+
18
/**
2-
* Single source of truth for the docs site origin. Set DEPLOY_SITE_URL in CI
3-
* (GitHub repository variable) to your indexed hostname — e.g. apex HTTPS URL
4-
* with no trailing slash. Configure DNS/CDN to 301 the non-canonical host (www vs apex).
9+
* Single source of truth for the docs site's public URL.
10+
* Set DEPLOY_SITE_URL in CI to the exact indexed website URL:
11+
* - GitHub Pages project site: https://samplexbro.github.io/agentsmesh/
12+
* - Custom domain at root: https://docs.agentsmesh.dev/
513
*/
6-
7-
/** @returns {string} e.g. https://samplexbro.github.io */
8-
export function getSiteOrigin() {
9-
const raw =
14+
export function resolveDeploySite(raw = null) {
15+
const value =
16+
raw?.trim() ||
1017
process.env.DEPLOY_SITE_URL?.trim() ||
1118
process.env.SITE_URL?.trim() ||
12-
'https://samplexbro.github.io';
13-
return raw.replace(/\/$/, '');
19+
DEFAULT_DEPLOY_SITE_URL;
20+
const url = new URL(value);
21+
const basePath = normalizeBasePath(url.pathname);
22+
const publicUrl = `${url.origin}${basePath === '/' ? '' : basePath}`;
23+
24+
return {
25+
origin: url.origin,
26+
basePath,
27+
publicUrl,
28+
hostname: url.hostname,
29+
};
30+
}
31+
32+
/** @returns {string} e.g. https://samplexbro.github.io */
33+
export function getSiteOrigin(raw = null) {
34+
return resolveDeploySite(raw).origin;
35+
}
36+
37+
/** @returns {string} e.g. /agentsmesh or / */
38+
export function getSiteBase(raw = null) {
39+
return resolveDeploySite(raw).basePath;
1440
}
1541

1642
/** @param {string} pathWithLeadingSlash path after base, e.g. /og-image.png */
17-
export function absoluteFromBase(pathWithLeadingSlash) {
18-
const origin = getSiteOrigin();
43+
export function fromBase(pathWithLeadingSlash, raw = null) {
1944
const suffix = pathWithLeadingSlash.startsWith('/')
2045
? pathWithLeadingSlash
2146
: `/${pathWithLeadingSlash}`;
22-
return `${origin}/agentsmesh${suffix}`;
47+
const basePath = getSiteBase(raw);
48+
return basePath === '/' ? suffix : `${basePath}${suffix}`;
49+
}
50+
51+
/** @param {string} pathWithLeadingSlash path after base, e.g. /og-image.png */
52+
export function absoluteFromBase(pathWithLeadingSlash, raw = null) {
53+
const suffix = pathWithLeadingSlash.startsWith('/')
54+
? pathWithLeadingSlash
55+
: `/${pathWithLeadingSlash}`;
56+
return `${resolveDeploySite(raw).publicUrl}${suffix}`;
57+
}
58+
59+
export function getCnameValue(raw = null) {
60+
const { hostname } = resolveDeploySite(raw);
61+
return hostname.endsWith('.github.io') ? null : `${hostname}\n`;
2362
}

website/site-url.test.mjs

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import test from 'node:test';
2+
import assert from 'node:assert/strict';
3+
4+
import {
5+
absoluteFromBase,
6+
fromBase,
7+
getCnameValue,
8+
resolveDeploySite,
9+
} from './site-url.mjs';
10+
11+
test('resolveDeploySite keeps the GitHub Pages project path as base', () => {
12+
const deploySite = resolveDeploySite('https://samplexbro.github.io/agentsmesh/');
13+
14+
assert.deepEqual(deploySite, {
15+
origin: 'https://samplexbro.github.io',
16+
basePath: '/agentsmesh',
17+
publicUrl: 'https://samplexbro.github.io/agentsmesh',
18+
hostname: 'samplexbro.github.io',
19+
});
20+
});
21+
22+
test('resolveDeploySite supports a custom domain at the site root', () => {
23+
const deploySite = resolveDeploySite('https://docs.agentsmesh.dev/');
24+
25+
assert.deepEqual(deploySite, {
26+
origin: 'https://docs.agentsmesh.dev',
27+
basePath: '/',
28+
publicUrl: 'https://docs.agentsmesh.dev',
29+
hostname: 'docs.agentsmesh.dev',
30+
});
31+
});
32+
33+
test('fromBase and absoluteFromBase honor the resolved base path', () => {
34+
assert.equal(fromBase('/favicon.svg', 'https://samplexbro.github.io/agentsmesh/'), '/agentsmesh/favicon.svg');
35+
assert.equal(fromBase('/favicon.svg', 'https://docs.agentsmesh.dev/'), '/favicon.svg');
36+
assert.equal(
37+
absoluteFromBase('/og-image.png', 'https://docs.agentsmesh.dev/'),
38+
'https://docs.agentsmesh.dev/og-image.png',
39+
);
40+
});
41+
42+
test('getCnameValue only emits a CNAME record for custom domains', () => {
43+
assert.equal(getCnameValue('https://samplexbro.github.io/agentsmesh/'), null);
44+
assert.equal(getCnameValue('https://docs.agentsmesh.dev/'), 'docs.agentsmesh.dev\n');
45+
});

website/src/content/docs/canonical-config/agents.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ Suggest concrete fixes, not vague recommendations.
6060

6161
## Tool-specific behavior
6262

63-
See the **Agents** row in the [supported tools matrix](/agentsmesh/reference/supported-tools/) for per-target support levels (native, embedded, or unsupported).
63+
See the **Agents** row in the [supported tools matrix](../reference/supported-tools/) for per-target support levels (native, embedded, or unsupported).
6464

6565
<Aside type="tip">
6666
Embedded agents carry metadata so that `agentsmesh import` can restore them from their projected form back to canonical `agents/*.md` format without data loss.

website/src/content/docs/canonical-config/commands.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ The filename (without `.md`) becomes the slash command name. The above file at `
3939

4040
## Tool-specific behavior
4141

42-
See the **Commands** row in the [supported tools matrix](/agentsmesh/reference/supported-tools/) for per-target support levels (native, embedded, or unsupported).
42+
See the **Commands** row in the [supported tools matrix](../reference/supported-tools/) for per-target support levels (native, embedded, or unsupported).
4343

4444
<Aside type="tip">
4545
Embedded commands carry AgentsMesh metadata comments so that `agentsmesh import` can restore them to the original `commands/*.md` format without data loss.

0 commit comments

Comments
 (0)