fimo 2.8.0-staging.11 → 2.8.0-staging.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/skills/fimo/SKILL.md +0 -1
- package/assets/skills/fimo-cli/SKILL.md +4 -7
- package/assets/skills/fimo-cli/references/assets.md +2 -0
- package/assets/skills/fimo-cli/references/deploy.md +1 -1
- package/assets/skills/fimo-cli/references/init.md +1 -2
- package/assets/skills/fimo-cli/references/integrations/firecrawl.md +1 -41
- package/assets/skills/fimo-cli/references/integrations.md +2 -38
- package/assets/skills/fimo-cli/references/knowledge.md +1 -1
- package/assets/skills/fimo-cli/references/mcp.md +5 -6
- package/assets/skills/fimo-cli/references/preview.md +1 -1
- package/assets/skills/fimo-cli/references/project-vars-and-secrets.md +0 -16
- package/assets/skills/fimo-cli/references/webhooks.md +0 -2
- package/assets/skills/fimo-code/SKILL.md +1 -0
- package/assets/skills/fimo-code/references/assets.md +5 -5
- package/assets/skills/fimo-code/references/design.md +1 -1
- package/assets/skills/fimo-code/references/ui.md +1 -1
- package/assets/skills/fimo-studio/SKILL.md +6 -8
- package/assets/skills/fimo-studio/references/ai-chat.md +5 -3
- package/assets/skills/fimo-studio/references/billing-and-credits.md +2 -4
- package/assets/skills/fimo-studio/references/command-palette.md +0 -2
- package/assets/skills/fimo-studio/references/connections.md +2 -2
- package/assets/skills/fimo-studio/references/webhooks.md +3 -3
- package/dist/cli/bundle.json +1 -1
- package/dist/cli/index.js +119 -153
- package/dist/runtime/project-layout.d.ts +0 -1
- package/dist/runtime/project-layout.js +1 -5
- package/package.json +8 -8
- package/templates/astro/AGENTS.md +1 -0
- package/templates/astro/package.json +3 -3
- package/templates/nextjs/AGENTS.md +1 -1
- package/templates/nextjs/package.json +4 -4
- package/templates/react-router/AGENTS.md +1 -1
- package/templates/react-router/package.json +5 -5
- package/templates/sveltekit/AGENTS.md +1 -0
- package/templates/sveltekit/package.json +3 -3
- package/templates/vite/AGENTS.md +2 -0
- package/templates/vite/package.json +5 -5
- package/assets/agent-templates/_default/GOAL.md +0 -21
- package/assets/agent-templates/_default/capabilities.yaml +0 -11
- package/assets/agent-templates/_default/config.yaml +0 -13
- package/assets/agent-templates/broken-link-sweeper/GOAL.md +0 -51
- package/assets/agent-templates/broken-link-sweeper/capabilities.yaml +0 -1
- package/assets/agent-templates/broken-link-sweeper/config.yaml +0 -13
- package/assets/agent-templates/content-translator/GOAL.md +0 -33
- package/assets/agent-templates/content-translator/capabilities.yaml +0 -5
- package/assets/agent-templates/content-translator/config.yaml +0 -12
- package/assets/agent-templates/content-translator/scripts/translate-entries.ts +0 -69
- package/assets/agent-templates/post-deploy-check/GOAL.md +0 -23
- package/assets/agent-templates/post-deploy-check/capabilities.yaml +0 -1
- package/assets/agent-templates/post-deploy-check/config.yaml +0 -10
- package/assets/agent-templates/post-deploy-check/scripts/check-logs.ts +0 -39
- package/assets/agent-templates/post-deploy-check/scripts/probe-routes.ts +0 -53
- package/assets/agent-templates/seo-audit/GOAL.md +0 -43
- package/assets/agent-templates/seo-audit/capabilities.yaml +0 -3
- package/assets/agent-templates/seo-audit/config.yaml +0 -14
- package/assets/agent-templates/seo-audit/references/report-template.md +0 -28
- package/assets/agent-templates/seo-audit/references/scoring-methodology.md +0 -30
- package/assets/agent-templates/seo-audit/scripts/analyze-html.ts +0 -82
- package/assets/agent-templates/seo-audit/scripts/fetch-page.ts +0 -39
- package/assets/agent-templates/seo-audit/scripts/list-routes.ts +0 -46
- package/assets/agent-templates/seo-audit/scripts/report.ts +0 -82
- package/assets/agent-templates/seo-audit/scripts/score.ts +0 -131
- package/assets/skills/fimo-cli/references/agents.md +0 -298
- package/assets/skills/fimo-studio/references/agents.md +0 -33
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
# Report template
|
|
2
|
-
|
|
3
|
-
The agent renders `reports/seo-audit-<YYYY-MM-DD>.md` using the shape:
|
|
4
|
-
|
|
5
|
-
```markdown
|
|
6
|
-
# SEO audit · 2026-05-26
|
|
7
|
-
|
|
8
|
-
**Overall score:** 78 / 100
|
|
9
|
-
|
|
10
|
-
## Per-page summary
|
|
11
|
-
|
|
12
|
-
| Path | Score | Status |
|
|
13
|
-
| -------- | ----- | ------ |
|
|
14
|
-
| `/` | 95 | OK |
|
|
15
|
-
| `/about` | 70 | WARN |
|
|
16
|
-
|
|
17
|
-
## Findings
|
|
18
|
-
|
|
19
|
-
### `/about`
|
|
20
|
-
|
|
21
|
-
- ⚠️ **title-length** — Title length 22 chars — recommend 30–60
|
|
22
|
-
- ❌ **h1-present** — No <h1> on the page
|
|
23
|
-
|
|
24
|
-
<!-- agent-summary: { "score": 78, "pages": 12 } -->
|
|
25
|
-
```
|
|
26
|
-
|
|
27
|
-
The `<!-- agent-summary -->` comment is parsed by subsequent runs to
|
|
28
|
-
plot a trend; keep its shape stable.
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
# Scoring methodology
|
|
2
|
-
|
|
3
|
-
A page starts at 100 points and loses:
|
|
4
|
-
|
|
5
|
-
- **-15** per `fail` finding (missing title, missing `<h1>`, `noindex` on a public page, etc.)
|
|
6
|
-
- **-5** per `warn` finding (length out of range, missing OG tag, no canonical, etc.)
|
|
7
|
-
|
|
8
|
-
The overall project score is the arithmetic mean of per-page scores.
|
|
9
|
-
|
|
10
|
-
## Severity guide
|
|
11
|
-
|
|
12
|
-
| Rule | Severity | Why |
|
|
13
|
-
| ---------------- | --------- | -------------------------------------------------------------------- |
|
|
14
|
-
| title-present | fail | Without a title, search snippets fall back to the URL — costly |
|
|
15
|
-
| title-length | warn | Outside 30–60 chars, Google truncates or pads — still ranks |
|
|
16
|
-
| description-\* | warn/fail | Less impact on rank but huge impact on CTR |
|
|
17
|
-
| og-{title,image} | warn | Affects social shares only; no SEO rank impact |
|
|
18
|
-
| h1-present | fail | Page has no semantic root — both screen-readers and search hurt |
|
|
19
|
-
| h1-single | warn | Multiple h1s — accessibility lint, not a ranking penalty |
|
|
20
|
-
| canonical | warn | Self-referential canonical is best practice; rarely required |
|
|
21
|
-
| robots-noindex | fail | Page explicitly excluded from index — review and remove if a mistake |
|
|
22
|
-
| schema-org | warn | Missing rich-result eligibility — won't hurt rank but loses CTR |
|
|
23
|
-
|
|
24
|
-
## How the agent should act on findings
|
|
25
|
-
|
|
26
|
-
Don't write to project files in v1. Instead, the report's "fix recipes"
|
|
27
|
-
appendix should link to the file the agent suspects is responsible (e.g.
|
|
28
|
-
"fix this in `src/routes/[slug].tsx`'s `meta` export"). A follow-up
|
|
29
|
-
agent — the one that resolves recipes into actual PRs — is the
|
|
30
|
-
appropriate writer.
|
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tool: analyze-html
|
|
3
|
-
*
|
|
4
|
-
* Extract SEO signals from HTML — title, meta description, OG tags,
|
|
5
|
-
* schema.org JSON-LD, heading structure, canonical, robots directive.
|
|
6
|
-
* Returns a structured shape `score.ts` consumes.
|
|
7
|
-
*
|
|
8
|
-
* Inputs (stdin): HTML body
|
|
9
|
-
* Outputs (stdout JSON): SEOSignals
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
interface SEOSignals {
|
|
13
|
-
title: string | null;
|
|
14
|
-
description: string | null;
|
|
15
|
-
canonical: string | null;
|
|
16
|
-
robots: string | null;
|
|
17
|
-
openGraph: Record<string, string>;
|
|
18
|
-
schemaOrg: unknown[];
|
|
19
|
-
headings: { h1: string[]; h2: string[]; h3: string[] };
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function getMatch(html: string, regex: RegExp): string | null {
|
|
23
|
-
const m = html.match(regex);
|
|
24
|
-
return m && m[1] ? m[1].trim() : null;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function getAllMatches(html: string, regex: RegExp): string[] {
|
|
28
|
-
const out: string[] = [];
|
|
29
|
-
for (const m of html.matchAll(regex)) {
|
|
30
|
-
if (m[1]) {
|
|
31
|
-
out.push(m[1].trim());
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
return out;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
async function readStdin(): Promise<string> {
|
|
38
|
-
const chunks: Buffer[] = [];
|
|
39
|
-
for await (const chunk of process.stdin) {
|
|
40
|
-
chunks.push(Buffer.from(chunk));
|
|
41
|
-
}
|
|
42
|
-
return Buffer.concat(chunks).toString('utf8');
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
async function main(): Promise<void> {
|
|
46
|
-
const html = await readStdin();
|
|
47
|
-
|
|
48
|
-
const openGraph: Record<string, string> = {};
|
|
49
|
-
for (const m of html.matchAll(/<meta\s+property=["']og:([^"']+)["']\s+content=["']([^"']*)["']/gi)) {
|
|
50
|
-
openGraph[m[1]!] = m[2] ?? '';
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
const schemaOrg: unknown[] = [];
|
|
54
|
-
for (const m of html.matchAll(/<script\s+type=["']application\/ld\+json["'][^>]*>([\s\S]*?)<\/script>/gi)) {
|
|
55
|
-
try {
|
|
56
|
-
schemaOrg.push(JSON.parse(m[1]!.trim()));
|
|
57
|
-
} catch {
|
|
58
|
-
// skip invalid JSON-LD blocks
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
const signals: SEOSignals = {
|
|
63
|
-
title: getMatch(html, /<title>([\s\S]*?)<\/title>/i),
|
|
64
|
-
description: getMatch(html, /<meta\s+name=["']description["']\s+content=["']([^"']*)["']/i),
|
|
65
|
-
canonical: getMatch(html, /<link\s+rel=["']canonical["']\s+href=["']([^"']*)["']/i),
|
|
66
|
-
robots: getMatch(html, /<meta\s+name=["']robots["']\s+content=["']([^"']*)["']/i),
|
|
67
|
-
openGraph,
|
|
68
|
-
schemaOrg,
|
|
69
|
-
headings: {
|
|
70
|
-
h1: getAllMatches(html, /<h1[^>]*>([\s\S]*?)<\/h1>/gi).map((s) => s.replace(/<[^>]+>/g, '').trim()),
|
|
71
|
-
h2: getAllMatches(html, /<h2[^>]*>([\s\S]*?)<\/h2>/gi).map((s) => s.replace(/<[^>]+>/g, '').trim()),
|
|
72
|
-
h3: getAllMatches(html, /<h3[^>]*>([\s\S]*?)<\/h3>/gi).map((s) => s.replace(/<[^>]+>/g, '').trim()),
|
|
73
|
-
},
|
|
74
|
-
};
|
|
75
|
-
|
|
76
|
-
process.stdout.write(JSON.stringify(signals, null, 2));
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
main().catch((err) => {
|
|
80
|
-
console.error(err);
|
|
81
|
-
process.exit(1);
|
|
82
|
-
});
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tool: fetch-page
|
|
3
|
-
*
|
|
4
|
-
* Fetch HTML + headers for a given URL. Used by the audit to walk every
|
|
5
|
-
* route returned by `list-routes` and feed the bytes to `analyze-html`.
|
|
6
|
-
*
|
|
7
|
-
* Inputs (argv):
|
|
8
|
-
* 1. The URL to fetch (must be the project's own domain)
|
|
9
|
-
*
|
|
10
|
-
* Outputs (stdout JSON):
|
|
11
|
-
* {
|
|
12
|
-
* "url": "...",
|
|
13
|
-
* "status": 200,
|
|
14
|
-
* "headers": { "content-type": "text/html; charset=utf-8", ... },
|
|
15
|
-
* "body": "<!doctype html>..."
|
|
16
|
-
* }
|
|
17
|
-
*/
|
|
18
|
-
|
|
19
|
-
async function main(): Promise<void> {
|
|
20
|
-
const url = process.argv[2];
|
|
21
|
-
if (!url) {
|
|
22
|
-
console.error('Usage: fetch-page <url>');
|
|
23
|
-
process.exit(1);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
const res = await fetch(url, { redirect: 'manual' });
|
|
27
|
-
const headers: Record<string, string> = {};
|
|
28
|
-
res.headers.forEach((value, key) => {
|
|
29
|
-
headers[key] = value;
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
const body = await res.text();
|
|
33
|
-
process.stdout.write(JSON.stringify({ url, status: res.status, headers, body }, null, 2));
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
main().catch((err) => {
|
|
37
|
-
console.error(err);
|
|
38
|
-
process.exit(1);
|
|
39
|
-
});
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tool: list-routes
|
|
3
|
-
*
|
|
4
|
-
* Enumerate every public route in the project via the Fimo management
|
|
5
|
-
* API. Returns paths the audit walks one-by-one through `fetch-page`.
|
|
6
|
-
*
|
|
7
|
-
* Inputs (branch):
|
|
8
|
-
* - FIMO_PROJECT_ID — provided by the runtime (M16 sandbox adapter)
|
|
9
|
-
* - FIMO_ENV — provided by the runtime (legacy runtime selector)
|
|
10
|
-
* - FIMO_API_URL — provided by the runtime
|
|
11
|
-
*
|
|
12
|
-
* Outputs (stdout JSON):
|
|
13
|
-
* { "routes": [{ "path": "/", "label": "Home" }, ...] }
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
interface Route {
|
|
17
|
-
path: string;
|
|
18
|
-
label: string;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
async function main(): Promise<void> {
|
|
22
|
-
const projectId = process.env.FIMO_PROJECT_ID;
|
|
23
|
-
const env = process.env.FIMO_ENV ?? 'main';
|
|
24
|
-
const base = process.env.FIMO_API_URL ?? 'http://localhost:3000';
|
|
25
|
-
|
|
26
|
-
if (!projectId) {
|
|
27
|
-
console.error('FIMO_PROJECT_ID is required');
|
|
28
|
-
process.exit(1);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
const url = `${base}/api/management/projects/${projectId}/branches/${encodeURIComponent(env)}/routes`;
|
|
32
|
-
const res = await fetch(url, { credentials: 'include' });
|
|
33
|
-
if (!res.ok) {
|
|
34
|
-
console.error(`Failed to list routes: ${res.status} ${res.statusText}`);
|
|
35
|
-
process.exit(1);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
const payload = (await res.json()) as { data?: { routes?: Record<string, Route> } };
|
|
39
|
-
const routes = Object.values(payload.data?.routes ?? {});
|
|
40
|
-
process.stdout.write(JSON.stringify({ routes }, null, 2));
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
main().catch((err) => {
|
|
44
|
-
console.error(err);
|
|
45
|
-
process.exit(1);
|
|
46
|
-
});
|
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tool: report
|
|
3
|
-
*
|
|
4
|
-
* Render a markdown report from the per-page audit results.
|
|
5
|
-
*
|
|
6
|
-
* Inputs (stdin JSON): { score: number, pages: PageReport[] }
|
|
7
|
-
* Outputs (stdout): markdown
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
interface Finding {
|
|
11
|
-
rule: string;
|
|
12
|
-
severity: 'ok' | 'warn' | 'fail';
|
|
13
|
-
message: string;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
interface PageReport {
|
|
17
|
-
path: string;
|
|
18
|
-
score: number;
|
|
19
|
-
findings: Finding[];
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function statusChip(score: number): string {
|
|
23
|
-
if (score >= 80) {
|
|
24
|
-
return 'OK';
|
|
25
|
-
}
|
|
26
|
-
if (score >= 60) {
|
|
27
|
-
return 'WARN';
|
|
28
|
-
}
|
|
29
|
-
return 'FAIL';
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
async function readStdin(): Promise<string> {
|
|
33
|
-
const chunks: Buffer[] = [];
|
|
34
|
-
for await (const chunk of process.stdin) {
|
|
35
|
-
chunks.push(Buffer.from(chunk));
|
|
36
|
-
}
|
|
37
|
-
return Buffer.concat(chunks).toString('utf8');
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
async function main(): Promise<void> {
|
|
41
|
-
const text = await readStdin();
|
|
42
|
-
const data = JSON.parse(text) as { score: number; pages: PageReport[] };
|
|
43
|
-
|
|
44
|
-
const date = new Date().toISOString().slice(0, 10);
|
|
45
|
-
const lines: string[] = [];
|
|
46
|
-
lines.push(`# SEO audit · ${date}`);
|
|
47
|
-
lines.push('');
|
|
48
|
-
lines.push(`**Overall score:** ${data.score} / 100`);
|
|
49
|
-
lines.push('');
|
|
50
|
-
lines.push('## Per-page summary');
|
|
51
|
-
lines.push('');
|
|
52
|
-
lines.push('| Path | Score | Status |');
|
|
53
|
-
lines.push('|------|-------|--------|');
|
|
54
|
-
for (const page of data.pages) {
|
|
55
|
-
lines.push(`| \`${page.path}\` | ${page.score} | ${statusChip(page.score)} |`);
|
|
56
|
-
}
|
|
57
|
-
lines.push('');
|
|
58
|
-
lines.push('## Findings');
|
|
59
|
-
lines.push('');
|
|
60
|
-
for (const page of data.pages) {
|
|
61
|
-
const failures = page.findings.filter((f) => f.severity !== 'ok');
|
|
62
|
-
if (failures.length === 0) {
|
|
63
|
-
continue;
|
|
64
|
-
}
|
|
65
|
-
lines.push(`### \`${page.path}\``);
|
|
66
|
-
lines.push('');
|
|
67
|
-
for (const finding of failures) {
|
|
68
|
-
const sev = finding.severity === 'fail' ? '❌' : '⚠️';
|
|
69
|
-
lines.push(`- ${sev} **${finding.rule}** — ${finding.message}`);
|
|
70
|
-
}
|
|
71
|
-
lines.push('');
|
|
72
|
-
}
|
|
73
|
-
lines.push('');
|
|
74
|
-
lines.push(`<!-- agent-summary: { "score": ${data.score}, "pages": ${data.pages.length} } -->`);
|
|
75
|
-
|
|
76
|
-
process.stdout.write(lines.join('\n'));
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
main().catch((err) => {
|
|
80
|
-
console.error(err);
|
|
81
|
-
process.exit(1);
|
|
82
|
-
});
|
|
@@ -1,131 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tool: score
|
|
3
|
-
*
|
|
4
|
-
* Apply the SEO scoring methodology (see references/scoring-methodology.md)
|
|
5
|
-
* to a list of `(path, SEOSignals)` tuples and produce a per-page report.
|
|
6
|
-
*
|
|
7
|
-
* Inputs (stdin JSON): { signals: Record<path, SEOSignals> }
|
|
8
|
-
* Outputs (stdout JSON): { score: 0..100, pages: PageReport[] }
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
interface SEOSignals {
|
|
12
|
-
title: string | null;
|
|
13
|
-
description: string | null;
|
|
14
|
-
canonical: string | null;
|
|
15
|
-
robots: string | null;
|
|
16
|
-
openGraph: Record<string, string>;
|
|
17
|
-
schemaOrg: unknown[];
|
|
18
|
-
headings: { h1: string[]; h2: string[]; h3: string[] };
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
interface Finding {
|
|
22
|
-
rule: string;
|
|
23
|
-
severity: 'ok' | 'warn' | 'fail';
|
|
24
|
-
message: string;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
interface PageReport {
|
|
28
|
-
path: string;
|
|
29
|
-
score: number;
|
|
30
|
-
findings: Finding[];
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function audit(signals: SEOSignals): Finding[] {
|
|
34
|
-
const out: Finding[] = [];
|
|
35
|
-
|
|
36
|
-
// Title
|
|
37
|
-
if (!signals.title) {
|
|
38
|
-
out.push({ rule: 'title-present', severity: 'fail', message: 'Missing <title>' });
|
|
39
|
-
} else if (signals.title.length < 30 || signals.title.length > 60) {
|
|
40
|
-
out.push({
|
|
41
|
-
rule: 'title-length',
|
|
42
|
-
severity: 'warn',
|
|
43
|
-
message: `Title length ${signals.title.length} chars — recommend 30–60`,
|
|
44
|
-
});
|
|
45
|
-
} else {
|
|
46
|
-
out.push({ rule: 'title', severity: 'ok', message: 'Title looks good' });
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
// Description
|
|
50
|
-
if (!signals.description) {
|
|
51
|
-
out.push({ rule: 'description-present', severity: 'fail', message: 'Missing meta description' });
|
|
52
|
-
} else if (signals.description.length < 50 || signals.description.length > 160) {
|
|
53
|
-
out.push({
|
|
54
|
-
rule: 'description-length',
|
|
55
|
-
severity: 'warn',
|
|
56
|
-
message: `Description length ${signals.description.length} chars — recommend 50–160`,
|
|
57
|
-
});
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// OG tags
|
|
61
|
-
for (const key of ['title', 'description', 'image', 'url']) {
|
|
62
|
-
if (!signals.openGraph[key]) {
|
|
63
|
-
out.push({ rule: `og-${key}`, severity: 'warn', message: `Missing og:${key}` });
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// Headings
|
|
68
|
-
if (signals.headings.h1.length === 0) {
|
|
69
|
-
out.push({ rule: 'h1-present', severity: 'fail', message: 'No <h1> on the page' });
|
|
70
|
-
} else if (signals.headings.h1.length > 1) {
|
|
71
|
-
out.push({ rule: 'h1-single', severity: 'warn', message: `Multiple <h1> tags (${signals.headings.h1.length})` });
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// Canonical
|
|
75
|
-
if (!signals.canonical) {
|
|
76
|
-
out.push({ rule: 'canonical', severity: 'warn', message: 'Missing canonical link' });
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// Robots
|
|
80
|
-
if (signals.robots && /noindex/i.test(signals.robots)) {
|
|
81
|
-
out.push({ rule: 'robots-noindex', severity: 'fail', message: 'Page is marked noindex' });
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// Schema.org
|
|
85
|
-
if (signals.schemaOrg.length === 0) {
|
|
86
|
-
out.push({ rule: 'schema-org', severity: 'warn', message: 'No JSON-LD schema.org block' });
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
return out;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
function scoreOf(findings: Finding[]): number {
|
|
93
|
-
// Each fail = -15, each warn = -5. Floor at 0.
|
|
94
|
-
let pts = 100;
|
|
95
|
-
for (const f of findings) {
|
|
96
|
-
if (f.severity === 'fail') {
|
|
97
|
-
pts -= 15;
|
|
98
|
-
} else if (f.severity === 'warn') {
|
|
99
|
-
pts -= 5;
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
return Math.max(0, pts);
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
async function readStdin(): Promise<string> {
|
|
106
|
-
const chunks: Buffer[] = [];
|
|
107
|
-
for await (const chunk of process.stdin) {
|
|
108
|
-
chunks.push(Buffer.from(chunk));
|
|
109
|
-
}
|
|
110
|
-
return Buffer.concat(chunks).toString('utf8');
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
async function main(): Promise<void> {
|
|
114
|
-
const text = await readStdin();
|
|
115
|
-
const payload = JSON.parse(text) as { signals: Record<string, SEOSignals> };
|
|
116
|
-
|
|
117
|
-
const pages: PageReport[] = [];
|
|
118
|
-
for (const [path, signals] of Object.entries(payload.signals)) {
|
|
119
|
-
const findings = audit(signals);
|
|
120
|
-
pages.push({ path, score: scoreOf(findings), findings });
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
const overall = pages.length === 0 ? 0 : Math.round(pages.reduce((a, p) => a + p.score, 0) / pages.length);
|
|
124
|
-
|
|
125
|
-
process.stdout.write(JSON.stringify({ score: overall, pages }, null, 2));
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
main().catch((err) => {
|
|
129
|
-
console.error(err);
|
|
130
|
-
process.exit(1);
|
|
131
|
-
});
|