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 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.
@@ -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
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
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
@@ -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"}
@@ -0,0 +1,3 @@
1
+ export { auditUrl, auditSnapshot } from "./audit.js";
2
+ export { renderJsonReport, renderMarkdownReport } from "./reporters.js";
3
+ export type { AuditOptions, AuditReport, AuditSummary, Evidence, Finding, FindingCategory, PageSnapshot, Score, Severity } from "./types.js";
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ export { auditUrl, auditSnapshot } from "./audit.js";
2
+ export { renderJsonReport, renderMarkdownReport } from "./reporters.js";
3
+ //# sourceMappingURL=index.js.map
@@ -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,3 @@
1
+ import type { AuditReport } from "./types.js";
2
+ export declare function renderJsonReport(report: AuditReport, pretty?: boolean): string;
3
+ export declare function renderMarkdownReport(report: AuditReport): string;
@@ -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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"}
@@ -0,0 +1,3 @@
1
+ import type { Finding, PageSnapshot } from "./types.js";
2
+ export declare function runRules(snapshot: PageSnapshot): Finding[];
3
+ export declare const ruleCount: number;
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":"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"}
@@ -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"}
@@ -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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -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
+ }