open-local-audit 0.1.0
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/CHANGELOG.md +11 -0
- package/LICENSE +21 -0
- package/README.md +96 -0
- package/dist/audit.d.ts +3 -0
- package/dist/audit.js +126 -0
- package/dist/audit.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +40 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/dist/reporters.d.ts +3 -0
- package/dist/reporters.js +58 -0
- package/dist/reporters.js.map +1 -0
- package/dist/rules.d.ts +3 -0
- package/dist/rules.js +199 -0
- package/dist/rules.js.map +1 -0
- package/dist/schema.d.ts +22 -0
- package/dist/schema.js +24 -0
- package/dist/schema.js.map +1 -0
- package/dist/types.d.ts +49 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +57 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to Open Local Audit will be documented here.
|
|
4
|
+
|
|
5
|
+
## v0.1.0 - 2026-05-08
|
|
6
|
+
|
|
7
|
+
- Project documentation and release planning created.
|
|
8
|
+
- Initial TypeScript CLI scaffold added.
|
|
9
|
+
- JSON and Markdown report generation added.
|
|
10
|
+
- Initial audit rule set and Vitest coverage added.
|
|
11
|
+
- Example report artifacts added.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 TORUT
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# Open Local Audit
|
|
2
|
+
|
|
3
|
+
Open Local Audit is an open-source website and local presence auditor for small businesses. It turns public website signals into practical reports for outreach, customer education, and implementation work.
|
|
4
|
+
|
|
5
|
+
## Current stage
|
|
6
|
+
|
|
7
|
+
Initial CLI implementation and release preparation. The project can run a single URL audit and produce JSON or Markdown output, but it is not published to npm yet.
|
|
8
|
+
|
|
9
|
+
## Business purpose
|
|
10
|
+
|
|
11
|
+
Open Local Audit produces evidence-backed mini audits for local businesses. The tool should be useful as open-source software while keeping paid value in implementation, redesign, maintenance, and custom reporting.
|
|
12
|
+
|
|
13
|
+
## Product principles
|
|
14
|
+
|
|
15
|
+
- Evidence first: every recommendation should point to a concrete finding.
|
|
16
|
+
- Ethical scanning: only scan user-provided public URLs and avoid Google Maps scraping.
|
|
17
|
+
- Small-business language: reports should be readable by non-technical owners.
|
|
18
|
+
- Developer quality: deterministic checks, clear CLI output, tests, and release notes.
|
|
19
|
+
- Commercial clarity: the open-source scanner is useful; paid work is execution and support.
|
|
20
|
+
|
|
21
|
+
## Proposed first stack
|
|
22
|
+
|
|
23
|
+
- Runtime: Node.js with TypeScript.
|
|
24
|
+
- CLI framework: `commander` or `cac`.
|
|
25
|
+
- Validation: `zod`.
|
|
26
|
+
- HTML parsing: `cheerio`.
|
|
27
|
+
- Browser rendering: Playwright where static parsing is not enough.
|
|
28
|
+
- Storage: no persistence by default; optional JSON/Markdown outputs.
|
|
29
|
+
- Test runner: Vitest.
|
|
30
|
+
- Package manager: npm unless implementation chooses pnpm before first commit.
|
|
31
|
+
|
|
32
|
+
## Documents
|
|
33
|
+
|
|
34
|
+
- [Product brief](./docs/product-brief.md)
|
|
35
|
+
- [Technical architecture](./docs/technical-architecture.md)
|
|
36
|
+
- [MVP roadmap](./docs/mvp-roadmap.md)
|
|
37
|
+
- [Security and ethics](./docs/security-and-ethics.md)
|
|
38
|
+
- [Go-to-market plan](./docs/go-to-market.md)
|
|
39
|
+
- [Audit checklist](./docs/research/audit-checklist.md)
|
|
40
|
+
- [Release readiness](./docs/release/release-readiness.md)
|
|
41
|
+
- [npm publishing plan](./docs/release/npm-publishing.md)
|
|
42
|
+
- [Project operating standard](./docs/operations/project-standard.md)
|
|
43
|
+
- [Decision log](./docs/operations/decision-log.md)
|
|
44
|
+
|
|
45
|
+
## Local development
|
|
46
|
+
|
|
47
|
+
Install dependencies and run the checks:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
npm install
|
|
51
|
+
npm test
|
|
52
|
+
npm run lint
|
|
53
|
+
npm run build
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Run the CLI locally:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
npm start -- https://example.com --format markdown
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
After building, run the compiled CLI:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
node dist/cli.js https://example.com --format json --pretty
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## First implementation milestone
|
|
69
|
+
|
|
70
|
+
The first implementation milestone is a CLI that accepts one URL and outputs:
|
|
71
|
+
|
|
72
|
+
- JSON report.
|
|
73
|
+
- Markdown report.
|
|
74
|
+
- Score summary.
|
|
75
|
+
- Evidence table.
|
|
76
|
+
- Clear owner-readable recommendations.
|
|
77
|
+
|
|
78
|
+
Example target command:
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
open-local-audit https://example.com --format markdown --out report.md
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Example report artifacts are available under [`examples/reports`](./examples/reports).
|
|
85
|
+
|
|
86
|
+
## GitHub and npm release intent
|
|
87
|
+
|
|
88
|
+
The project should be prepared for:
|
|
89
|
+
|
|
90
|
+
- Public GitHub repository.
|
|
91
|
+
- Clear README and examples.
|
|
92
|
+
- MIT license, unless maintainers choose a different license before the first public release.
|
|
93
|
+
- GitHub Actions for lint, tests, build, and release checks.
|
|
94
|
+
- npm package after the CLI has real tests and example reports.
|
|
95
|
+
|
|
96
|
+
No npm package should be published until the release checklist in `docs/release/release-readiness.md` is complete.
|
package/dist/audit.d.ts
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { AuditOptions, AuditReport, PageSnapshot } from "./types.js";
|
|
2
|
+
export declare function auditSnapshot(snapshot: PageSnapshot, scannedAt?: string): AuditReport;
|
|
3
|
+
export declare function auditUrl(url: string, options?: Partial<AuditOptions>): Promise<AuditReport>;
|
package/dist/audit.js
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { runRules } from "./rules.js";
|
|
2
|
+
const defaultOptions = {
|
|
3
|
+
timeoutMs: 10000,
|
|
4
|
+
maxRedirects: 5
|
|
5
|
+
};
|
|
6
|
+
const categories = [
|
|
7
|
+
"technical-health",
|
|
8
|
+
"search-basics",
|
|
9
|
+
"mobile-usability",
|
|
10
|
+
"trust-contact"
|
|
11
|
+
];
|
|
12
|
+
function normalizeHeaders(headers) {
|
|
13
|
+
const output = {};
|
|
14
|
+
headers.forEach((value, key) => {
|
|
15
|
+
output[key] = value;
|
|
16
|
+
});
|
|
17
|
+
return output;
|
|
18
|
+
}
|
|
19
|
+
async function fetchWithRedirects(url, options) {
|
|
20
|
+
let currentUrl = url;
|
|
21
|
+
for (let redirectCount = 0; redirectCount <= options.maxRedirects; redirectCount += 1) {
|
|
22
|
+
const controller = new AbortController();
|
|
23
|
+
const timeout = setTimeout(() => controller.abort(), options.timeoutMs);
|
|
24
|
+
try {
|
|
25
|
+
const response = await fetch(currentUrl, {
|
|
26
|
+
redirect: "manual",
|
|
27
|
+
signal: controller.signal,
|
|
28
|
+
headers: {
|
|
29
|
+
"user-agent": "open-local-audit/0.1 (+https://github.com/Esquetta/open-local-audit)"
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
const location = response.headers.get("location");
|
|
33
|
+
if (location && response.status >= 300 && response.status < 400) {
|
|
34
|
+
currentUrl = new URL(location, currentUrl).toString();
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
return {
|
|
38
|
+
url,
|
|
39
|
+
finalUrl: response.url || currentUrl,
|
|
40
|
+
statusCode: response.status,
|
|
41
|
+
headers: normalizeHeaders(response.headers),
|
|
42
|
+
html: await response.text()
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
finally {
|
|
46
|
+
clearTimeout(timeout);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
throw new Error(`Exceeded redirect limit of ${options.maxRedirects}`);
|
|
50
|
+
}
|
|
51
|
+
function summarize(reportFindings) {
|
|
52
|
+
return {
|
|
53
|
+
totalFindings: reportFindings.length,
|
|
54
|
+
high: reportFindings.filter((finding) => finding.severity === "high").length,
|
|
55
|
+
medium: reportFindings.filter((finding) => finding.severity === "medium").length,
|
|
56
|
+
low: reportFindings.filter((finding) => finding.severity === "low").length,
|
|
57
|
+
info: reportFindings.filter((finding) => finding.severity === "info").length
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
function scoreCategories(reportFindings) {
|
|
61
|
+
const labels = {
|
|
62
|
+
"technical-health": "Technical health",
|
|
63
|
+
"search-basics": "Search basics",
|
|
64
|
+
"mobile-usability": "Mobile and usability",
|
|
65
|
+
"trust-contact": "Trust and contact readiness"
|
|
66
|
+
};
|
|
67
|
+
return Object.fromEntries(categories.map((category) => {
|
|
68
|
+
const findings = reportFindings.filter((finding) => finding.category === category);
|
|
69
|
+
const penalty = findings.reduce((total, finding) => {
|
|
70
|
+
if (finding.severity === "high") {
|
|
71
|
+
return total + 25;
|
|
72
|
+
}
|
|
73
|
+
if (finding.severity === "medium") {
|
|
74
|
+
return total + 15;
|
|
75
|
+
}
|
|
76
|
+
if (finding.severity === "low") {
|
|
77
|
+
return total + 8;
|
|
78
|
+
}
|
|
79
|
+
return total + 3;
|
|
80
|
+
}, 0);
|
|
81
|
+
return [
|
|
82
|
+
category,
|
|
83
|
+
{
|
|
84
|
+
label: labels[category],
|
|
85
|
+
max: 100,
|
|
86
|
+
score: Math.max(0, 100 - penalty)
|
|
87
|
+
}
|
|
88
|
+
];
|
|
89
|
+
}));
|
|
90
|
+
}
|
|
91
|
+
export function auditSnapshot(snapshot, scannedAt = new Date().toISOString()) {
|
|
92
|
+
const findings = runRules(snapshot);
|
|
93
|
+
const recommendations = findings.map((finding) => finding.recommendation);
|
|
94
|
+
return {
|
|
95
|
+
url: snapshot.url,
|
|
96
|
+
finalUrl: snapshot.finalUrl,
|
|
97
|
+
scannedAt,
|
|
98
|
+
statusCode: snapshot.statusCode,
|
|
99
|
+
summary: summarize(findings),
|
|
100
|
+
scores: scoreCategories(findings),
|
|
101
|
+
findings,
|
|
102
|
+
recommendations: Array.from(new Set(recommendations)),
|
|
103
|
+
evidence: [
|
|
104
|
+
{
|
|
105
|
+
label: "Status code",
|
|
106
|
+
value: `${snapshot.statusCode}`
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
label: "Final URL",
|
|
110
|
+
value: snapshot.finalUrl
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
label: "Content type",
|
|
114
|
+
value: snapshot.headers["content-type"] ?? "Unknown"
|
|
115
|
+
}
|
|
116
|
+
]
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
export async function auditUrl(url, options = {}) {
|
|
120
|
+
const snapshot = await fetchWithRedirects(url, {
|
|
121
|
+
...defaultOptions,
|
|
122
|
+
...options
|
|
123
|
+
});
|
|
124
|
+
return auditSnapshot(snapshot);
|
|
125
|
+
}
|
|
126
|
+
//# sourceMappingURL=audit.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"audit.js","sourceRoot":"","sources":["../src/audit.ts"],"names":[],"mappings":"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"}
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { writeFile } from "node:fs/promises";
|
|
3
|
+
import { Command } from "commander";
|
|
4
|
+
import { auditUrl } from "./audit.js";
|
|
5
|
+
import { renderJsonReport, renderMarkdownReport } from "./reporters.js";
|
|
6
|
+
import { cliOptionsSchema, inputUrlSchema } from "./schema.js";
|
|
7
|
+
const program = new Command();
|
|
8
|
+
program
|
|
9
|
+
.name("open-local-audit")
|
|
10
|
+
.description("Audit a public local-business website and generate an evidence-backed report.")
|
|
11
|
+
.argument("<url>", "HTTP or HTTPS URL to audit")
|
|
12
|
+
.option("-f, --format <format>", "output format: json or markdown", "markdown")
|
|
13
|
+
.option("-o, --out <path>", "write report to a file instead of stdout")
|
|
14
|
+
.option("--timeout <ms>", "request timeout in milliseconds", "10000")
|
|
15
|
+
.option("--max-redirects <count>", "maximum redirects to follow", "5")
|
|
16
|
+
.option("--pretty", "pretty-print JSON output", false)
|
|
17
|
+
.action(async (rawUrl, rawOptions) => {
|
|
18
|
+
try {
|
|
19
|
+
const url = inputUrlSchema.parse(rawUrl);
|
|
20
|
+
const options = cliOptionsSchema.parse(rawOptions);
|
|
21
|
+
const report = await auditUrl(url, {
|
|
22
|
+
timeoutMs: options.timeout,
|
|
23
|
+
maxRedirects: options.maxRedirects
|
|
24
|
+
});
|
|
25
|
+
const output = options.format === "json" ? renderJsonReport(report, options.pretty) : renderMarkdownReport(report);
|
|
26
|
+
if (options.out) {
|
|
27
|
+
await writeFile(options.out, output, "utf8");
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
process.stdout.write(output);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
35
|
+
process.stderr.write(`open-local-audit: ${message}\n`);
|
|
36
|
+
process.exitCode = 1;
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
program.parseAsync();
|
|
40
|
+
//# sourceMappingURL=cli.js.map
|
package/dist/cli.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAC7C,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AACtC,OAAO,EAAE,gBAAgB,EAAE,oBAAoB,EAAE,MAAM,gBAAgB,CAAC;AACxE,OAAO,EAAE,gBAAgB,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAE/D,MAAM,OAAO,GAAG,IAAI,OAAO,EAAE,CAAC;AAE9B,OAAO;KACJ,IAAI,CAAC,kBAAkB,CAAC;KACxB,WAAW,CAAC,+EAA+E,CAAC;KAC5F,QAAQ,CAAC,OAAO,EAAE,4BAA4B,CAAC;KAC/C,MAAM,CAAC,uBAAuB,EAAE,iCAAiC,EAAE,UAAU,CAAC;KAC9E,MAAM,CAAC,kBAAkB,EAAE,0CAA0C,CAAC;KACtE,MAAM,CAAC,gBAAgB,EAAE,iCAAiC,EAAE,OAAO,CAAC;KACpE,MAAM,CAAC,yBAAyB,EAAE,6BAA6B,EAAE,GAAG,CAAC;KACrE,MAAM,CAAC,UAAU,EAAE,0BAA0B,EAAE,KAAK,CAAC;KACrD,MAAM,CAAC,KAAK,EAAE,MAAc,EAAE,UAAmB,EAAE,EAAE;IACpD,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,cAAc,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;QACzC,MAAM,OAAO,GAAG,gBAAgB,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC;QACnD,MAAM,MAAM,GAAG,MAAM,QAAQ,CAAC,GAAG,EAAE;YACjC,SAAS,EAAE,OAAO,CAAC,OAAO;YAC1B,YAAY,EAAE,OAAO,CAAC,YAAY;SACnC,CAAC,CAAC;QAEH,MAAM,MAAM,GACV,OAAO,CAAC,MAAM,KAAK,MAAM,CAAC,CAAC,CAAC,gBAAgB,CAAC,MAAM,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,oBAAoB,CAAC,MAAM,CAAC,CAAC;QAEtG,IAAI,OAAO,CAAC,GAAG,EAAE,CAAC;YAChB,MAAM,SAAS,CAAC,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC;QAC/C,CAAC;aAAM,CAAC;YACN,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;QAC/B,CAAC;IACH,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,OAAO,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,eAAe,CAAC;QACzE,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,qBAAqB,OAAO,IAAI,CAAC,CAAC;QACvD,OAAO,CAAC,QAAQ,GAAG,CAAC,CAAC;IACvB,CAAC;AACH,CAAC,CAAC,CAAC;AAEL,OAAO,CAAC,UAAU,EAAE,CAAC"}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AACrD,OAAO,EAAE,gBAAgB,EAAE,oBAAoB,EAAE,MAAM,gBAAgB,CAAC"}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
function escapeCell(value) {
|
|
2
|
+
return value.replace(/\|/g, "\\|").replace(/\r?\n/g, " ");
|
|
3
|
+
}
|
|
4
|
+
function severityRank(finding) {
|
|
5
|
+
const ranks = {
|
|
6
|
+
high: 0,
|
|
7
|
+
medium: 1,
|
|
8
|
+
low: 2,
|
|
9
|
+
info: 3
|
|
10
|
+
};
|
|
11
|
+
return ranks[finding.severity];
|
|
12
|
+
}
|
|
13
|
+
export function renderJsonReport(report, pretty = true) {
|
|
14
|
+
return `${JSON.stringify(report, null, pretty ? 2 : 0)}\n`;
|
|
15
|
+
}
|
|
16
|
+
export function renderMarkdownReport(report) {
|
|
17
|
+
const findings = [...report.findings].sort((left, right) => severityRank(left) - severityRank(right));
|
|
18
|
+
const lines = [
|
|
19
|
+
`# Open Local Audit Report`,
|
|
20
|
+
"",
|
|
21
|
+
`- URL: ${report.url}`,
|
|
22
|
+
`- Final URL: ${report.finalUrl}`,
|
|
23
|
+
`- Scanned at: ${report.scannedAt}`,
|
|
24
|
+
`- Status code: ${report.statusCode}`,
|
|
25
|
+
"",
|
|
26
|
+
"## Score Summary",
|
|
27
|
+
"",
|
|
28
|
+
"| Category | Score |",
|
|
29
|
+
"| --- | ---: |",
|
|
30
|
+
...Object.values(report.scores).map((score) => `| ${escapeCell(score.label)} | ${score.score}/${score.max} |`),
|
|
31
|
+
"",
|
|
32
|
+
"## Findings",
|
|
33
|
+
""
|
|
34
|
+
];
|
|
35
|
+
if (findings.length === 0) {
|
|
36
|
+
lines.push("No findings were detected by the current rule set.", "");
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
lines.push("| Severity | Finding | Evidence | Recommendation |", "| --- | --- | --- | --- |");
|
|
40
|
+
for (const finding of findings) {
|
|
41
|
+
const evidence = finding.evidence.map((item) => `${item.label}: ${item.value}`).join("; ");
|
|
42
|
+
lines.push(`| ${finding.severity} | ${escapeCell(finding.title)} | ${escapeCell(evidence)} | ${escapeCell(finding.recommendation)} |`);
|
|
43
|
+
}
|
|
44
|
+
lines.push("");
|
|
45
|
+
}
|
|
46
|
+
lines.push("## Recommendations", "");
|
|
47
|
+
if (report.recommendations.length === 0) {
|
|
48
|
+
lines.push("- Keep monitoring the page as content and templates change.");
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
for (const recommendation of report.recommendations) {
|
|
52
|
+
lines.push(`- ${recommendation}`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
lines.push("");
|
|
56
|
+
return `${lines.join("\n")}\n`;
|
|
57
|
+
}
|
|
58
|
+
//# sourceMappingURL=reporters.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reporters.js","sourceRoot":"","sources":["../src/reporters.ts"],"names":[],"mappings":"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"}
|
package/dist/rules.d.ts
ADDED
package/dist/rules.js
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { load } from "cheerio";
|
|
2
|
+
function finding(rule, context) {
|
|
3
|
+
return {
|
|
4
|
+
id: rule.id,
|
|
5
|
+
title: rule.title,
|
|
6
|
+
severity: rule.severity,
|
|
7
|
+
category: rule.category,
|
|
8
|
+
source: rule.source,
|
|
9
|
+
recommendation: rule.recommendation,
|
|
10
|
+
evidence: [
|
|
11
|
+
{
|
|
12
|
+
label: rule.source,
|
|
13
|
+
value: rule.evidence(context)
|
|
14
|
+
}
|
|
15
|
+
]
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
function hasLink($, matcher) {
|
|
19
|
+
return $("a")
|
|
20
|
+
.toArray()
|
|
21
|
+
.some((element) => matcher($(element).attr("href") ?? ""));
|
|
22
|
+
}
|
|
23
|
+
function hasJsonLdType($, matcher) {
|
|
24
|
+
return $('script[type="application/ld+json"]')
|
|
25
|
+
.toArray()
|
|
26
|
+
.some((element) => {
|
|
27
|
+
const raw = $(element).text();
|
|
28
|
+
try {
|
|
29
|
+
const parsed = JSON.parse(raw);
|
|
30
|
+
const nodes = Array.isArray(parsed) ? parsed : [parsed];
|
|
31
|
+
return nodes.some((node) => {
|
|
32
|
+
if (!node || typeof node !== "object") {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
const typeValue = node["@type"];
|
|
36
|
+
const types = Array.isArray(typeValue) ? typeValue : [typeValue];
|
|
37
|
+
return types.some((type) => typeof type === "string" && matcher(type));
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
const rules = [
|
|
46
|
+
{
|
|
47
|
+
id: "http-status-ok",
|
|
48
|
+
title: "Page does not return a successful HTTP status",
|
|
49
|
+
category: "technical-health",
|
|
50
|
+
severity: "high",
|
|
51
|
+
source: "HTTP status",
|
|
52
|
+
recommendation: "Return a 2xx status for the audited page before investing in content or SEO work.",
|
|
53
|
+
check: ({ snapshot }) => snapshot.statusCode >= 200 && snapshot.statusCode < 300,
|
|
54
|
+
evidence: ({ snapshot }) => `${snapshot.statusCode}`
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
id: "https-enabled",
|
|
58
|
+
title: "Final URL is not HTTPS",
|
|
59
|
+
category: "technical-health",
|
|
60
|
+
severity: "high",
|
|
61
|
+
source: "Final URL",
|
|
62
|
+
recommendation: "Serve the public site over HTTPS and redirect plain HTTP traffic to the secure URL.",
|
|
63
|
+
check: ({ snapshot }) => snapshot.finalUrl.startsWith("https://"),
|
|
64
|
+
evidence: ({ snapshot }) => snapshot.finalUrl
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
id: "title-present",
|
|
68
|
+
title: "Page title is missing",
|
|
69
|
+
category: "search-basics",
|
|
70
|
+
severity: "medium",
|
|
71
|
+
source: "HTML title",
|
|
72
|
+
recommendation: "Add a clear title that includes the business name, service, and location where useful.",
|
|
73
|
+
check: ({ $ }) => $("title").first().text().trim().length > 0,
|
|
74
|
+
evidence: ({ $ }) => $("title").first().text().trim() || "Missing"
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
id: "meta-description-present",
|
|
78
|
+
title: "Meta description is missing",
|
|
79
|
+
category: "search-basics",
|
|
80
|
+
severity: "medium",
|
|
81
|
+
source: "Meta description",
|
|
82
|
+
recommendation: "Add a short owner-readable meta description that explains the service and location.",
|
|
83
|
+
check: ({ $ }) => $('meta[name="description"]').attr("content")?.trim().length ? true : false,
|
|
84
|
+
evidence: ({ $ }) => $('meta[name="description"]').attr("content")?.trim() || "Missing"
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
id: "viewport-present",
|
|
88
|
+
title: "Viewport tag is missing",
|
|
89
|
+
category: "mobile-usability",
|
|
90
|
+
severity: "high",
|
|
91
|
+
source: "Viewport meta tag",
|
|
92
|
+
recommendation: "Add a responsive viewport tag so mobile browsers render the page correctly.",
|
|
93
|
+
check: ({ $ }) => $('meta[name="viewport"]').attr("content")?.trim().length ? true : false,
|
|
94
|
+
evidence: ({ $ }) => $('meta[name="viewport"]').attr("content")?.trim() || "Missing"
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
id: "single-h1",
|
|
98
|
+
title: "Page should have one clear H1",
|
|
99
|
+
category: "search-basics",
|
|
100
|
+
severity: "medium",
|
|
101
|
+
source: "H1 count",
|
|
102
|
+
recommendation: "Use one visible H1 that clearly names the core service or business.",
|
|
103
|
+
check: ({ $ }) => $("h1").length === 1 && $("h1").first().text().trim().length > 0,
|
|
104
|
+
evidence: ({ $ }) => `${$("h1").length} H1 elements`
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
id: "canonical-present",
|
|
108
|
+
title: "Canonical URL is missing",
|
|
109
|
+
category: "search-basics",
|
|
110
|
+
severity: "low",
|
|
111
|
+
source: "Canonical link",
|
|
112
|
+
recommendation: "Add a canonical link to reduce duplicate URL confusion.",
|
|
113
|
+
check: ({ $ }) => $('link[rel="canonical"]').attr("href")?.trim().length ? true : false,
|
|
114
|
+
evidence: ({ $ }) => $('link[rel="canonical"]').attr("href")?.trim() || "Missing"
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
id: "phone-link-present",
|
|
118
|
+
title: "Phone action is missing",
|
|
119
|
+
category: "trust-contact",
|
|
120
|
+
severity: "high",
|
|
121
|
+
source: "Contact links",
|
|
122
|
+
recommendation: "Add a tappable phone link using the tel: format.",
|
|
123
|
+
check: ({ $ }) => hasLink($, (href) => href.toLowerCase().startsWith("tel:")),
|
|
124
|
+
evidence: () => "No tel: link found"
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
id: "email-link-present",
|
|
128
|
+
title: "Email action is missing",
|
|
129
|
+
category: "trust-contact",
|
|
130
|
+
severity: "low",
|
|
131
|
+
source: "Contact links",
|
|
132
|
+
recommendation: "Add an email link if email is an expected contact path for the business.",
|
|
133
|
+
check: ({ $ }) => hasLink($, (href) => href.toLowerCase().startsWith("mailto:")),
|
|
134
|
+
evidence: () => "No mailto: link found"
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
id: "whatsapp-link-present",
|
|
138
|
+
title: "WhatsApp action is missing",
|
|
139
|
+
category: "trust-contact",
|
|
140
|
+
severity: "low",
|
|
141
|
+
source: "Contact links",
|
|
142
|
+
recommendation: "Add a WhatsApp action if customers commonly use WhatsApp for bookings or questions.",
|
|
143
|
+
check: ({ $ }) => hasLink($, (href) => /wa\.me|whatsapp/i.test(href)),
|
|
144
|
+
evidence: () => "No WhatsApp link found"
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
id: "localbusiness-schema-present",
|
|
148
|
+
title: "LocalBusiness structured data is missing",
|
|
149
|
+
category: "search-basics",
|
|
150
|
+
severity: "medium",
|
|
151
|
+
source: "JSON-LD",
|
|
152
|
+
recommendation: "Add LocalBusiness schema when the page represents a local business location.",
|
|
153
|
+
check: ({ $ }) => hasJsonLdType($, (type) => type.endsWith("LocalBusiness") || type === "LocalBusiness"),
|
|
154
|
+
evidence: () => "No LocalBusiness JSON-LD type found"
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
id: "map-link-present",
|
|
158
|
+
title: "Map or directions link is missing",
|
|
159
|
+
category: "trust-contact",
|
|
160
|
+
severity: "medium",
|
|
161
|
+
source: "Links",
|
|
162
|
+
recommendation: "Add a map or directions link so visitors can confirm the business location quickly.",
|
|
163
|
+
check: ({ $ }) => hasLink($, (href) => /google\.com\/maps|maps\.app\.goo\.gl|bing\.com\/maps|directions/i.test(href)),
|
|
164
|
+
evidence: () => "No map or directions link found"
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
id: "image-alt-coverage",
|
|
168
|
+
title: "Some images are missing alt text",
|
|
169
|
+
category: "mobile-usability",
|
|
170
|
+
severity: "low",
|
|
171
|
+
source: "Image alt text",
|
|
172
|
+
recommendation: "Add useful alt text to meaningful images and leave decorative images empty intentionally.",
|
|
173
|
+
check: ({ $ }) => {
|
|
174
|
+
const images = $("img").toArray();
|
|
175
|
+
if (images.length === 0) {
|
|
176
|
+
return true;
|
|
177
|
+
}
|
|
178
|
+
return images.every((element) => $(element).attr("alt") !== undefined);
|
|
179
|
+
},
|
|
180
|
+
evidence: ({ $ }) => {
|
|
181
|
+
const images = $("img").length;
|
|
182
|
+
const missing = $("img")
|
|
183
|
+
.toArray()
|
|
184
|
+
.filter((element) => $(element).attr("alt") === undefined).length;
|
|
185
|
+
return `${missing} of ${images} images missing alt attributes`;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
];
|
|
189
|
+
export function runRules(snapshot) {
|
|
190
|
+
const $ = load(snapshot.html);
|
|
191
|
+
const context = {
|
|
192
|
+
$,
|
|
193
|
+
snapshot,
|
|
194
|
+
text: $("body").text().replace(/\s+/g, " ").trim()
|
|
195
|
+
};
|
|
196
|
+
return rules.filter((rule) => !rule.check(context)).map((rule) => finding(rule, context));
|
|
197
|
+
}
|
|
198
|
+
export const ruleCount = rules.length;
|
|
199
|
+
//# sourceMappingURL=rules.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rules.js","sourceRoot":"","sources":["../src/rules.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAmB,MAAM,SAAS,CAAC;AAoBhD,SAAS,OAAO,CAAC,IAAU,EAAE,OAAoB;IAC/C,OAAO;QACL,EAAE,EAAE,IAAI,CAAC,EAAE;QACX,KAAK,EAAE,IAAI,CAAC,KAAK;QACjB,QAAQ,EAAE,IAAI,CAAC,QAAQ;QACvB,QAAQ,EAAE,IAAI,CAAC,QAAQ;QACvB,MAAM,EAAE,IAAI,CAAC,MAAM;QACnB,cAAc,EAAE,IAAI,CAAC,cAAc;QACnC,QAAQ,EAAE;YACR;gBACE,KAAK,EAAE,IAAI,CAAC,MAAM;gBAClB,KAAK,EAAE,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC;aAC9B;SACF;KACF,CAAC;AACJ,CAAC;AAED,SAAS,OAAO,CAAC,CAAa,EAAE,OAAkC;IAChE,OAAO,CAAC,CAAC,GAAG,CAAC;SACV,OAAO,EAAE;SACT,IAAI,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC;AAC/D,CAAC;AAED,SAAS,aAAa,CAAC,CAAa,EAAE,OAAkC;IACtE,OAAO,CAAC,CAAC,oCAAoC,CAAC;SAC3C,OAAO,EAAE;SACT,IAAI,CAAC,CAAC,OAAO,EAAE,EAAE;QAChB,MAAM,GAAG,GAAG,CAAC,CAAC,OAAO,CAAC,CAAC,IAAI,EAAE,CAAC;QAC9B,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAY,CAAC;YAC1C,MAAM,KAAK,GAAG,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC;YAExD,OAAO,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE;gBACzB,IAAI,CAAC,IAAI,IAAI,OAAO,IAAI,KAAK,QAAQ,EAAE,CAAC;oBACtC,OAAO,KAAK,CAAC;gBACf,CAAC;gBAED,MAAM,SAAS,GAAI,IAA8B,CAAC,OAAO,CAAC,CAAC;gBAC3D,MAAM,KAAK,GAAG,KAAK,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC;gBACjE,OAAO,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,OAAO,IAAI,KAAK,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC;YACzE,CAAC,CAAC,CAAC;QACL,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,KAAK,CAAC;QACf,CAAC;IACH,CAAC,CAAC,CAAC;AACP,CAAC;AAED,MAAM,KAAK,GAAW;IACpB;QACE,EAAE,EAAE,gBAAgB;QACpB,KAAK,EAAE,+CAA+C;QACtD,QAAQ,EAAE,kBAAkB;QAC5B,QAAQ,EAAE,MAAM;QAChB,MAAM,EAAE,aAAa;QACrB,cAAc,EAAE,mFAAmF;QACnG,KAAK,EAAE,CAAC,EAAE,QAAQ,EAAE,EAAE,EAAE,CAAC,QAAQ,CAAC,UAAU,IAAI,GAAG,IAAI,QAAQ,CAAC,UAAU,GAAG,GAAG;QAChF,QAAQ,EAAE,CAAC,EAAE,QAAQ,EAAE,EAAE,EAAE,CAAC,GAAG,QAAQ,CAAC,UAAU,EAAE;KACrD;IACD;QACE,EAAE,EAAE,eAAe;QACnB,KAAK,EAAE,wBAAwB;QAC/B,QAAQ,EAAE,kBAAkB;QAC5B,QAAQ,EAAE,MAAM;QAChB,MAAM,EAAE,WAAW;QACnB,cAAc,EAAE,qFAAqF;QACrG,KAAK,EAAE,CAAC,EAAE,QAAQ,EAAE,EAAE,EAAE,CAAC,QAAQ,CAAC,QAAQ,CAAC,UAAU,CAAC,UAAU,CAAC;QACjE,QAAQ,EAAE,CAAC,EAAE,QAAQ,EAAE,EAAE,EAAE,CAAC,QAAQ,CAAC,QAAQ;KAC9C;IACD;QACE,EAAE,EAAE,eAAe;QACnB,KAAK,EAAE,uBAAuB;QAC9B,QAAQ,EAAE,eAAe;QACzB,QAAQ,EAAE,QAAQ;QAClB,MAAM,EAAE,YAAY;QACpB,cAAc,EAAE,wFAAwF;QACxG,KAAK,EAAE,CAAC,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,KAAK,EAAE,CAAC,IAAI,EAAE,CAAC,IAAI,EAAE,CAAC,MAAM,GAAG,CAAC;QAC7D,QAAQ,EAAE,CAAC,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,KAAK,EAAE,CAAC,IAAI,EAAE,CAAC,IAAI,EAAE,IAAI,SAAS;KACnE;IACD;QACE,EAAE,EAAE,0BAA0B;QAC9B,KAAK,EAAE,6BAA6B;QACpC,QAAQ,EAAE,eAAe;QACzB,QAAQ,EAAE,QAAQ;QAClB,MAAM,EAAE,kBAAkB;QAC1B,cAAc,EAAE,qFAAqF;QACrG,KAAK,EAAE,CAAC,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,CAAC,0BAA0B,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,IAAI,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK;QAC7F,QAAQ,EAAE,CAAC,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,CAAC,0BAA0B,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,IAAI,EAAE,IAAI,SAAS;KACxF;IACD;QACE,EAAE,EAAE,kBAAkB;QACtB,KAAK,EAAE,yBAAyB;QAChC,QAAQ,EAAE,kBAAkB;QAC5B,QAAQ,EAAE,MAAM;QAChB,MAAM,EAAE,mBAAmB;QAC3B,cAAc,EAAE,6EAA6E;QAC7F,KAAK,EAAE,CAAC,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,CAAC,uBAAuB,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,IAAI,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK;QAC1F,QAAQ,EAAE,CAAC,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,CAAC,uBAAuB,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,IAAI,EAAE,IAAI,SAAS;KACrF;IACD;QACE,EAAE,EAAE,WAAW;QACf,KAAK,EAAE,+BAA+B;QACtC,QAAQ,EAAE,eAAe;QACzB,QAAQ,EAAE,QAAQ;QAClB,MAAM,EAAE,UAAU;QAClB,cAAc,EAAE,qEAAqE;QACrF,KAAK,EAAE,CAAC,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,MAAM,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,CAAC,IAAI,EAAE,CAAC,IAAI,EAAE,CAAC,MAAM,GAAG,CAAC;QAClF,QAAQ,EAAE,CAAC,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,MAAM,cAAc;KACrD;IACD;QACE,EAAE,EAAE,mBAAmB;QACvB,KAAK,EAAE,0BAA0B;QACjC,QAAQ,EAAE,eAAe;QACzB,QAAQ,EAAE,KAAK;QACf,MAAM,EAAE,gBAAgB;QACxB,cAAc,EAAE,yDAAyD;QACzE,KAAK,EAAE,CAAC,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,CAAC,uBAAuB,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK;QACvF,QAAQ,EAAE,CAAC,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,CAAC,uBAAuB,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,IAAI,SAAS;KAClF;IACD;QACE,EAAE,EAAE,oBAAoB;QACxB,KAAK,EAAE,yBAAyB;QAChC,QAAQ,EAAE,eAAe;QACzB,QAAQ,EAAE,MAAM;QAChB,MAAM,EAAE,eAAe;QACvB,cAAc,EAAE,kDAAkD;QAClE,KAAK,EAAE,CAAC,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,OAAO,CAAC,CAAC,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC;QAC7E,QAAQ,EAAE,GAAG,EAAE,CAAC,oBAAoB;KACrC;IACD;QACE,EAAE,EAAE,oBAAoB;QACxB,KAAK,EAAE,yBAAyB;QAChC,QAAQ,EAAE,eAAe;QACzB,QAAQ,EAAE,KAAK;QACf,MAAM,EAAE,eAAe;QACvB,cAAc,EAAE,0EAA0E;QAC1F,KAAK,EAAE,CAAC,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,OAAO,CAAC,CAAC,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC,UAAU,CAAC,SAAS,CAAC,CAAC;QAChF,QAAQ,EAAE,GAAG,EAAE,CAAC,uBAAuB;KACxC;IACD;QACE,EAAE,EAAE,uBAAuB;QAC3B,KAAK,EAAE,4BAA4B;QACnC,QAAQ,EAAE,eAAe;QACzB,QAAQ,EAAE,KAAK;QACf,MAAM,EAAE,eAAe;QACvB,cAAc,EAAE,qFAAqF;QACrG,KAAK,EAAE,CAAC,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,OAAO,CAAC,CAAC,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,kBAAkB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACrE,QAAQ,EAAE,GAAG,EAAE,CAAC,wBAAwB;KACzC;IACD;QACE,EAAE,EAAE,8BAA8B;QAClC,KAAK,EAAE,0CAA0C;QACjD,QAAQ,EAAE,eAAe;QACzB,QAAQ,EAAE,QAAQ;QAClB,MAAM,EAAE,SAAS;QACjB,cAAc,EAAE,8EAA8E;QAC9F,KAAK,EAAE,CAAC,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,aAAa,CAAC,CAAC,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,eAAe,CAAC,IAAI,IAAI,KAAK,eAAe,CAAC;QACxG,QAAQ,EAAE,GAAG,EAAE,CAAC,qCAAqC;KACtD;IACD;QACE,EAAE,EAAE,kBAAkB;QACtB,KAAK,EAAE,mCAAmC;QAC1C,QAAQ,EAAE,eAAe;QACzB,QAAQ,EAAE,QAAQ;QAClB,MAAM,EAAE,OAAO;QACf,cAAc,EAAE,qFAAqF;QACrG,KAAK,EAAE,CAAC,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,OAAO,CAAC,CAAC,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,kEAAkE,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACrH,QAAQ,EAAE,GAAG,EAAE,CAAC,iCAAiC;KAClD;IACD;QACE,EAAE,EAAE,oBAAoB;QACxB,KAAK,EAAE,kCAAkC;QACzC,QAAQ,EAAE,kBAAkB;QAC5B,QAAQ,EAAE,KAAK;QACf,MAAM,EAAE,gBAAgB;QACxB,cAAc,EAAE,2FAA2F;QAC3G,KAAK,EAAE,CAAC,EAAE,CAAC,EAAE,EAAE,EAAE;YACf,MAAM,MAAM,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,OAAO,EAAE,CAAC;YAClC,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;gBACxB,OAAO,IAAI,CAAC;YACd,CAAC;YAED,OAAO,MAAM,CAAC,KAAK,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,SAAS,CAAC,CAAC;QACzE,CAAC;QACD,QAAQ,EAAE,CAAC,EAAE,CAAC,EAAE,EAAE,EAAE;YAClB,MAAM,MAAM,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC;YAC/B,MAAM,OAAO,GAAG,CAAC,CAAC,KAAK,CAAC;iBACrB,OAAO,EAAE;iBACT,MAAM,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,SAAS,CAAC,CAAC,MAAM,CAAC;YAEpE,OAAO,GAAG,OAAO,OAAO,MAAM,gCAAgC,CAAC;QACjE,CAAC;KACF;CACF,CAAC;AAEF,MAAM,UAAU,QAAQ,CAAC,QAAsB;IAC7C,MAAM,CAAC,GAAG,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;IAC9B,MAAM,OAAO,GAAgB;QAC3B,CAAC;QACD,QAAQ;QACR,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,CAAC,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,IAAI,EAAE;KACnD,CAAC;IAEF,OAAO,KAAK,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC,CAAC;AAC5F,CAAC;AAED,MAAM,CAAC,MAAM,SAAS,GAAG,KAAK,CAAC,MAAM,CAAC"}
|
package/dist/schema.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export declare const inputUrlSchema: z.ZodEffects<z.ZodPipeline<z.ZodEffects<z.ZodString, string, string>, z.ZodString>, string, string>;
|
|
3
|
+
export declare const outputFormatSchema: z.ZodEnum<["json", "markdown"]>;
|
|
4
|
+
export declare const cliOptionsSchema: z.ZodObject<{
|
|
5
|
+
format: z.ZodDefault<z.ZodEnum<["json", "markdown"]>>;
|
|
6
|
+
out: z.ZodOptional<z.ZodString>;
|
|
7
|
+
timeout: z.ZodDefault<z.ZodNumber>;
|
|
8
|
+
maxRedirects: z.ZodDefault<z.ZodNumber>;
|
|
9
|
+
pretty: z.ZodDefault<z.ZodBoolean>;
|
|
10
|
+
}, "strip", z.ZodTypeAny, {
|
|
11
|
+
maxRedirects: number;
|
|
12
|
+
format: "json" | "markdown";
|
|
13
|
+
timeout: number;
|
|
14
|
+
pretty: boolean;
|
|
15
|
+
out?: string | undefined;
|
|
16
|
+
}, {
|
|
17
|
+
maxRedirects?: number | undefined;
|
|
18
|
+
format?: "json" | "markdown" | undefined;
|
|
19
|
+
out?: string | undefined;
|
|
20
|
+
timeout?: number | undefined;
|
|
21
|
+
pretty?: boolean | undefined;
|
|
22
|
+
}>;
|
package/dist/schema.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export const inputUrlSchema = z
|
|
3
|
+
.string()
|
|
4
|
+
.trim()
|
|
5
|
+
.min(1, "URL is required")
|
|
6
|
+
.transform((value) => {
|
|
7
|
+
if (/^https?:\/\//i.test(value)) {
|
|
8
|
+
return value;
|
|
9
|
+
}
|
|
10
|
+
return `https://${value}`;
|
|
11
|
+
})
|
|
12
|
+
.pipe(z.string().url("URL must be a valid HTTP or HTTPS URL"))
|
|
13
|
+
.refine((value) => /^https?:\/\//i.test(value), {
|
|
14
|
+
message: "Only HTTP and HTTPS URLs are supported"
|
|
15
|
+
});
|
|
16
|
+
export const outputFormatSchema = z.enum(["json", "markdown"]);
|
|
17
|
+
export const cliOptionsSchema = z.object({
|
|
18
|
+
format: outputFormatSchema.default("markdown"),
|
|
19
|
+
out: z.string().optional(),
|
|
20
|
+
timeout: z.coerce.number().int().positive().max(60000).default(10000),
|
|
21
|
+
maxRedirects: z.coerce.number().int().min(0).max(10).default(5),
|
|
22
|
+
pretty: z.boolean().default(false)
|
|
23
|
+
});
|
|
24
|
+
//# sourceMappingURL=schema.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"schema.js","sourceRoot":"","sources":["../src/schema.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC;KAC5B,MAAM,EAAE;KACR,IAAI,EAAE;KACN,GAAG,CAAC,CAAC,EAAE,iBAAiB,CAAC;KACzB,SAAS,CAAC,CAAC,KAAK,EAAE,EAAE;IACnB,IAAI,eAAe,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;QAChC,OAAO,KAAK,CAAC;IACf,CAAC;IAED,OAAO,WAAW,KAAK,EAAE,CAAC;AAC5B,CAAC,CAAC;KACD,IAAI,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,uCAAuC,CAAC,CAAC;KAC7D,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,eAAe,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE;IAC9C,OAAO,EAAE,wCAAwC;CAClD,CAAC,CAAC;AAEL,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC,CAAC;AAE/D,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,CAAC,MAAM,CAAC;IACvC,MAAM,EAAE,kBAAkB,CAAC,OAAO,CAAC,UAAU,CAAC;IAC9C,GAAG,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IAC1B,OAAO,EAAE,CAAC,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC;IACrE,YAAY,EAAE,CAAC,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC;IAC/D,MAAM,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC;CACnC,CAAC,CAAC"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
export type Severity = "high" | "medium" | "low" | "info";
|
|
2
|
+
export type FindingCategory = "technical-health" | "search-basics" | "mobile-usability" | "trust-contact";
|
|
3
|
+
export interface Evidence {
|
|
4
|
+
label: string;
|
|
5
|
+
value: string;
|
|
6
|
+
}
|
|
7
|
+
export interface Finding {
|
|
8
|
+
id: string;
|
|
9
|
+
title: string;
|
|
10
|
+
severity: Severity;
|
|
11
|
+
category: FindingCategory;
|
|
12
|
+
evidence: Evidence[];
|
|
13
|
+
recommendation: string;
|
|
14
|
+
source: string;
|
|
15
|
+
}
|
|
16
|
+
export interface Score {
|
|
17
|
+
label: string;
|
|
18
|
+
score: number;
|
|
19
|
+
max: number;
|
|
20
|
+
}
|
|
21
|
+
export interface AuditSummary {
|
|
22
|
+
totalFindings: number;
|
|
23
|
+
high: number;
|
|
24
|
+
medium: number;
|
|
25
|
+
low: number;
|
|
26
|
+
info: number;
|
|
27
|
+
}
|
|
28
|
+
export interface AuditReport {
|
|
29
|
+
url: string;
|
|
30
|
+
finalUrl: string;
|
|
31
|
+
scannedAt: string;
|
|
32
|
+
statusCode: number;
|
|
33
|
+
summary: AuditSummary;
|
|
34
|
+
scores: Record<FindingCategory, Score>;
|
|
35
|
+
findings: Finding[];
|
|
36
|
+
recommendations: string[];
|
|
37
|
+
evidence: Evidence[];
|
|
38
|
+
}
|
|
39
|
+
export interface PageSnapshot {
|
|
40
|
+
url: string;
|
|
41
|
+
finalUrl: string;
|
|
42
|
+
statusCode: number;
|
|
43
|
+
headers: Record<string, string>;
|
|
44
|
+
html: string;
|
|
45
|
+
}
|
|
46
|
+
export interface AuditOptions {
|
|
47
|
+
timeoutMs: number;
|
|
48
|
+
maxRedirects: number;
|
|
49
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
|
package/package.json
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "open-local-audit",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Open-source website and local presence auditor for small businesses.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"open-local-audit": "dist/cli.js"
|
|
9
|
+
},
|
|
10
|
+
"exports": {
|
|
11
|
+
".": "./dist/index.js"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"dist",
|
|
15
|
+
"README.md",
|
|
16
|
+
"LICENSE",
|
|
17
|
+
"CHANGELOG.md"
|
|
18
|
+
],
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "tsc -p tsconfig.build.json",
|
|
21
|
+
"lint": "tsc -p tsconfig.json --noEmit",
|
|
22
|
+
"test": "vitest run",
|
|
23
|
+
"start": "tsx src/cli.ts"
|
|
24
|
+
},
|
|
25
|
+
"keywords": [
|
|
26
|
+
"local-seo",
|
|
27
|
+
"website-audit",
|
|
28
|
+
"small-business",
|
|
29
|
+
"cli",
|
|
30
|
+
"seo",
|
|
31
|
+
"accessibility",
|
|
32
|
+
"structured-data",
|
|
33
|
+
"local-business"
|
|
34
|
+
],
|
|
35
|
+
"repository": {
|
|
36
|
+
"type": "git",
|
|
37
|
+
"url": "git+https://github.com/Esquetta/open-local-audit.git"
|
|
38
|
+
},
|
|
39
|
+
"bugs": {
|
|
40
|
+
"url": "https://github.com/Esquetta/open-local-audit/issues"
|
|
41
|
+
},
|
|
42
|
+
"homepage": "https://github.com/Esquetta/open-local-audit#readme",
|
|
43
|
+
"engines": {
|
|
44
|
+
"node": ">=20"
|
|
45
|
+
},
|
|
46
|
+
"dependencies": {
|
|
47
|
+
"cheerio": "^1.0.0",
|
|
48
|
+
"commander": "^12.1.0",
|
|
49
|
+
"zod": "^3.24.1"
|
|
50
|
+
},
|
|
51
|
+
"devDependencies": {
|
|
52
|
+
"@types/node": "^22.10.2",
|
|
53
|
+
"tsx": "^4.19.2",
|
|
54
|
+
"typescript": "^5.7.2",
|
|
55
|
+
"vitest": "^4.1.5"
|
|
56
|
+
}
|
|
57
|
+
}
|