seo-lint-next 0.2.0 → 0.2.1
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 +6 -0
- package/README.md +29 -14
- package/dist/cli.cjs +12 -0
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +12 -0
- package/dist/cli.js.map +1 -1
- package/dist/eslint.cjs +12 -0
- package/dist/eslint.cjs.map +1 -1
- package/dist/eslint.js +12 -0
- package/dist/eslint.js.map +1 -1
- package/dist/index.cjs +12 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
package/README.md
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
<img alt="license" src="https://img.shields.io/badge/license-MIT-111827">
|
|
16
16
|
</p>
|
|
17
17
|
|
|
18
|
-
`seo-lint-next` catches the
|
|
18
|
+
`seo-lint-next` catches the SEO bugs that usually escape to production: missing titles, broken canonicals, unsafe `metadataBase`, bad OG tags, accidental `noindex`, sitemap gaps, invalid JSON-LD, heading skips, weak image alt text, broken locale metadata, bad App Router patterns, and crawl-blocking config.
|
|
19
19
|
|
|
20
20
|
## Install
|
|
21
21
|
|
|
@@ -45,18 +45,33 @@ npx seo-lint-next --strict
|
|
|
45
45
|
|
|
46
46
|
## Rule Set
|
|
47
47
|
|
|
48
|
-
| Rule
|
|
49
|
-
|
|
|
50
|
-
| `no-missing-title`
|
|
51
|
-
| `no-missing-metadata-base`
|
|
52
|
-
| `no-missing-description`
|
|
53
|
-
| `no-missing-canonical`
|
|
54
|
-
| `no-missing-og-tags`
|
|
55
|
-
| `no-broken-heading-hierarchy`
|
|
56
|
-
| `no-img-missing-alt`
|
|
57
|
-
| `no-accidental-noindex`
|
|
58
|
-
| `no-missing-sitemap`
|
|
59
|
-
| `no-invalid-json-ld`
|
|
48
|
+
| Rule | Default | Catches |
|
|
49
|
+
| ---------------------------------------- | ------: | ---------------------------------------------------------------- |
|
|
50
|
+
| `no-missing-title` | error | Missing, empty, duplicate, or weak dynamic titles |
|
|
51
|
+
| `no-missing-metadata-base` | error | Missing, localhost, or non-HTTPS root `metadataBase` |
|
|
52
|
+
| `no-missing-description` | warn | Missing, duplicate, too short, or too long descriptions |
|
|
53
|
+
| `no-missing-canonical` | error | Missing canonicals and hardcoded dynamic-route canonicals |
|
|
54
|
+
| `no-missing-og-tags` | warn | Missing Open Graph/Twitter tags |
|
|
55
|
+
| `no-broken-heading-hierarchy` | warn | Missing/multiple H1s and skipped heading levels |
|
|
56
|
+
| `no-img-missing-alt` | warn | Raw `<img>`, missing/generic alt text, layout-shifting images |
|
|
57
|
+
| `no-accidental-noindex` | error | Public `noindex`, blanket robots blocks, risky X-Robots headers |
|
|
58
|
+
| `no-missing-sitemap` | warn | Missing sitemap, relative URLs, missing robots sitemap reference |
|
|
59
|
+
| `no-invalid-json-ld` | error | Invalid JSON-LD syntax, bad context, missing schema fields |
|
|
60
|
+
| `no-missing-lang-attribute` | error | Missing or invalid `<html lang>` in App Router layouts |
|
|
61
|
+
| `no-missing-viewport` | error | Broken viewport overrides and disabled user zoom |
|
|
62
|
+
| `no-use-client-on-page` | warn | Top-level client pages and ignored metadata exports |
|
|
63
|
+
| `no-missing-hreflang` | warn | Locale routes without `alternates.languages` |
|
|
64
|
+
| `no-generic-anchor-text` | warn | Empty or generic link text |
|
|
65
|
+
| `no-missing-404-page` | warn | Missing or weak `app/not-found.tsx` recovery pages |
|
|
66
|
+
| `no-missing-og-image-dimensions` | warn | OG images without width, height, or alt text |
|
|
67
|
+
| `no-blocking-next-static` | error | Robots rules blocking `/_next/` rendering assets |
|
|
68
|
+
| `no-missing-breadcrumb-schema` | warn | Deep pages without `BreadcrumbList` JSON-LD |
|
|
69
|
+
| `no-title-in-pages-head` | error | `next/head` metadata patterns inside `app/` |
|
|
70
|
+
| `no-dynamic-import-ssr-false-on-content` | warn | `dynamic(..., { ssr: false })` around likely content |
|
|
71
|
+
| `no-missing-next-font` | warn | External Google Fonts instead of `next/font` |
|
|
72
|
+
| `no-redirect-chain-in-next-config` | warn | Redirect chains and temporary redirects in `next.config` |
|
|
73
|
+
| `no-missing-article-dates` | warn | Article pages missing freshness metadata |
|
|
74
|
+
| `no-missing-error-boundary-metadata` | warn | Missing error boundaries or weak recovery links |
|
|
60
75
|
|
|
61
76
|
## Configuration
|
|
62
77
|
|
|
@@ -84,4 +99,4 @@ export default [
|
|
|
84
99
|
- run: npm run check
|
|
85
100
|
```
|
|
86
101
|
|
|
87
|
-
The package has no production bundle impact. It statically inspects App Router source files, JSX, `metadata` exports, `generateMetadata()` returns, `app/robots.ts`, `public/robots.txt`, and `app/sitemap.ts`.
|
|
102
|
+
The package has no production bundle impact. It statically inspects App Router source files, JSX, `metadata` exports, `generateMetadata()` returns, `next.config.*`, `app/robots.ts`, `public/robots.txt`, and `app/sitemap.ts`.
|
package/dist/cli.cjs
CHANGED
|
@@ -618,7 +618,9 @@ var noGenericAnchorText = createRule(
|
|
|
618
618
|
if (name !== "a" && name !== "Link") return;
|
|
619
619
|
const text = jsxChildText(node).toLowerCase();
|
|
620
620
|
const label = jsxAttributeString(node.openingElement, "aria-label");
|
|
621
|
+
const hasDynamicText = hasDynamicTextContent(node);
|
|
621
622
|
if (!text && !label) {
|
|
623
|
+
if (hasDynamicText) return;
|
|
622
624
|
const hasImage = hasChildElement(node, "img") || hasChildElement(node, "Image");
|
|
623
625
|
report(
|
|
624
626
|
context,
|
|
@@ -642,6 +644,16 @@ var noGenericAnchorText = createRule(
|
|
|
642
644
|
}),
|
|
643
645
|
"suggestion"
|
|
644
646
|
);
|
|
647
|
+
function hasDynamicTextContent(node) {
|
|
648
|
+
return (node.children ?? []).some((child) => {
|
|
649
|
+
if (child.type === "JSXExpressionContainer") {
|
|
650
|
+
const expression = child.expression;
|
|
651
|
+
return expression?.type !== "JSXEmptyExpression";
|
|
652
|
+
}
|
|
653
|
+
if (child.type === "JSXElement") return hasDynamicTextContent(child);
|
|
654
|
+
return false;
|
|
655
|
+
});
|
|
656
|
+
}
|
|
645
657
|
|
|
646
658
|
// src/rules/no-img-missing-alt.ts
|
|
647
659
|
var meaningless = /* @__PURE__ */ new Set(["image", "img", "photo", "picture", "screenshot"]);
|