jig-ui 0.14.0 → 0.14.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 +25 -0
- package/README.md +3 -3
- package/dist/index.js +30 -12
- package/package.json +1 -1
- package/rules/00-anti-patterns.md +3 -3
- package/templates/COMMAND.md.tmpl +5 -2
- package/templates/command-metadata.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,30 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.14.1
|
|
4
|
+
|
|
5
|
+
`jig seo` cites only what a rule says.
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- **No finding for a missing sitemap or robots file.** Both were filed under
|
|
10
|
+
rules that do not ask for them: `J-123` is about a page that must not be
|
|
11
|
+
indexed and says `robots.txt` is not it, and `J-124` is about a sitemap that
|
|
12
|
+
contradicts the pages. A project with no domain yet cannot write an honest
|
|
13
|
+
sitemap, so the only way to clear the old finding was to invent one. Both are
|
|
14
|
+
now counts on the `JIG_SEO` line.
|
|
15
|
+
- **A sitemap of paths is a `J-124` error.** A `<loc>` that is `/about` rather
|
|
16
|
+
than a full URL was read as a route, although a crawler drops it. `J-124` now
|
|
17
|
+
names that case and says no sitemap is the honest one until there is an
|
|
18
|
+
origin.
|
|
19
|
+
- **`pages=` counts pages.** It counted any file that mentioned `<title>` or
|
|
20
|
+
`<meta>`, so a build script, a test, a data module and a layout were pages and
|
|
21
|
+
a home page inheriting its layout was not. A page is now a route a framework
|
|
22
|
+
serves, or a whole-document template outside the layout and partial folders.
|
|
23
|
+
A dynamic route counts once.
|
|
24
|
+
- **Framework routes match their sitemap entries.** `src/pages/admin` was read
|
|
25
|
+
as `/pages/admin`, so a `noindex` Astro or Next page listed in the sitemap
|
|
26
|
+
went unreported.
|
|
27
|
+
|
|
3
28
|
## 0.14.0
|
|
4
29
|
|
|
5
30
|
What a stranger meets before the page, what the page must not hand them, and a
|
package/README.md
CHANGED
|
@@ -357,7 +357,7 @@ overwrites a config or brand file you have edited.
|
|
|
357
357
|
| `init [--yes]` | Sets the project up: CSS system, brand colour, token files, `jig.config.json`, wired imports, baseline check. The only command that writes into your repo. |
|
|
358
358
|
| `check [--all] [--ci] [--json]` | Runs the rules a machine can decide. Reports findings by rule id. |
|
|
359
359
|
| `update` | Refreshes an install to a newer version, leaving alone any file you have edited. |
|
|
360
|
-
| `seo [--json]` | Audits what a search engine and a link preview read, across the whole project: a route whose metadata says `noindex` sitting in the sitemap, two pages claiming one title,
|
|
360
|
+
| `seo [--json]` | Audits what a search engine and a link preview read, across the whole project: a route whose metadata says `noindex` sitting in the sitemap, two pages claiming one title, a sitemap that lists nothing or lists paths a crawler drops. Whether a sitemap and a robots file exist is counted, not reported: no rule asks for either, and a site with no domain yet cannot write an honest sitemap. Needs no config, no decisions and no spec. |
|
|
361
361
|
| `verdicts <surface>` | Verifies a critique's verdict files and computes its counts: every rule in each pass judged once, no id that does not exist, no rule in the wrong arm, and no verdict the render probe contradicts. |
|
|
362
362
|
| `probe` | Prints the render probe — one expression the critique runs in a browser at each width. It operates the menu, measures sideways scroll, and reads whether the styles and tokens applied. |
|
|
363
363
|
| `gate` | Run by the Stop hook `install` adds for Claude Code, not by hand. Blocks an agent from finishing while `check` fails on the files it changed, or the step it just ran left its work unfinished. |
|
|
@@ -448,8 +448,8 @@ in it tells strangers where to look; a path is safe to name only when something
|
|
|
448
448
|
else protects it.
|
|
449
449
|
|
|
450
450
|
`jig seo` covers what one file cannot: a route that says `noindex` and sits in
|
|
451
|
-
the sitemap anyway, two pages claiming one title, a
|
|
452
|
-
|
|
451
|
+
the sitemap anyway, two pages claiming one title, a sitemap of paths a crawler
|
|
452
|
+
drops. It writes nothing and needs nothing, so it is safe to run on the first
|
|
453
453
|
day, or on somebody else's codebase.
|
|
454
454
|
|
|
455
455
|
## Using it with a coding agent
|
package/dist/index.js
CHANGED
|
@@ -6177,8 +6177,28 @@ import { join as join36 } from "path";
|
|
|
6177
6177
|
var SITEMAP_FILE = /(^|\/)sitemap(-index)?\.(xml|ts|js|mjs|tsx|jsx|rb|php|py)$/i;
|
|
6178
6178
|
var ROBOTS_FILE = /(^|\/)robots\.(txt|ts|js|mjs|tsx|jsx)$/i;
|
|
6179
6179
|
var ROUTE_IN_SITEMAP = /(?:<loc>\s*([^<\s]+)\s*<\/loc>)|(?:url\s*:\s*["'`]([^"'`]+)["'`])/gi;
|
|
6180
|
+
var TEST_FILE = /(^|\/)(test|tests|__tests__|spec|e2e|fixtures?|stories)\/|\.(test|spec|stories)\.[a-z]+$/i;
|
|
6181
|
+
var ROUTE_DIR = /(?:^|\/)(?:src\/|app\/)?(?:pages|routes)\/(.+)$/;
|
|
6182
|
+
var TEMPLATE = /\.(html?|php|erb|njk|hbs|handlebars|liquid|ejs|twig|jinja2?|j2|mustache)$/i;
|
|
6183
|
+
var PARTS_DIR = /(^|\/)_?(layouts?|components?|partials?|includes?)\//i;
|
|
6184
|
+
var ENDPOINT = /export\s+(?:const|async\s+function|function)\s+(?:GET|POST|PUT|PATCH|DELETE|ALL)\b/;
|
|
6185
|
+
function isPage(file, raw) {
|
|
6186
|
+
if (TEST_FILE.test(file)) return false;
|
|
6187
|
+
if (/(^|\/)app\/(.+\/)?page\.(tsx|jsx|ts|js|mdx|md)$/.test(file)) return true;
|
|
6188
|
+
const route = ROUTE_DIR.exec(file);
|
|
6189
|
+
if (route) {
|
|
6190
|
+
const rest = route[1];
|
|
6191
|
+
const name = rest.split("/").pop();
|
|
6192
|
+
if (name.startsWith("+")) return name === "+page.svelte";
|
|
6193
|
+
if (/(^|\/)api\//.test(rest) || name.startsWith("_")) return false;
|
|
6194
|
+
if (/\.[a-z]+\.[cm]?[jt]s$/i.test(name)) return false;
|
|
6195
|
+
if (/\.[cm]?[jt]s$/i.test(name)) return /export\s+default\b/.test(raw) && !ENDPOINT.test(raw);
|
|
6196
|
+
return /\.(astro|html?|mdx?|vue|svelte|[jt]sx)$/i.test(name);
|
|
6197
|
+
}
|
|
6198
|
+
return TEMPLATE.test(file) && !PARTS_DIR.test(file) && isWholeDocument(raw);
|
|
6199
|
+
}
|
|
6180
6200
|
function routeOf(file) {
|
|
6181
|
-
const path = file.replace(/^(src|app|pages|public|content
|
|
6201
|
+
const path = file.replace(/^(?:src\/pages|app\/pages|app\/routes|src\/routes|pages|routes|app|src|public|content)\//, "").replace(/(^|\/)(index|\+?page)\.(html?|astro|vue|svelte|[jt]sx?|md|mdx)$/i, "$1").replace(/\.(html?|astro|vue|svelte|[jt]sx?|md|mdx)$/i, "").replace(/\/$/, "");
|
|
6182
6202
|
return `/${path}`.replace(/\/+/g, "/");
|
|
6183
6203
|
}
|
|
6184
6204
|
function seo(opts) {
|
|
@@ -6190,6 +6210,7 @@ function seo(opts) {
|
|
|
6190
6210
|
let sitemapFiles = [];
|
|
6191
6211
|
let robotsFiles = [];
|
|
6192
6212
|
const sitemapRoutes = /* @__PURE__ */ new Map();
|
|
6213
|
+
const pathLocs = [];
|
|
6193
6214
|
for (const file of files) {
|
|
6194
6215
|
let raw;
|
|
6195
6216
|
try {
|
|
@@ -6202,6 +6223,7 @@ function seo(opts) {
|
|
|
6202
6223
|
for (const match of raw.matchAll(ROUTE_IN_SITEMAP)) {
|
|
6203
6224
|
const value = (match[1] ?? match[2] ?? "").trim();
|
|
6204
6225
|
if (!value) continue;
|
|
6226
|
+
if (match[1] && !/^https?:\/\//i.test(value)) pathLocs.push({ file, loc: value });
|
|
6205
6227
|
const path = value.replace(/^https?:\/\/[^/]+/i, "") || "/";
|
|
6206
6228
|
sitemapRoutes.set(path.replace(/\/$/, "") || "/", file);
|
|
6207
6229
|
}
|
|
@@ -6211,9 +6233,8 @@ function seo(opts) {
|
|
|
6211
6233
|
robotsFiles.push(file);
|
|
6212
6234
|
continue;
|
|
6213
6235
|
}
|
|
6214
|
-
if (!
|
|
6236
|
+
if (!isPage(file, raw)) continue;
|
|
6215
6237
|
const found = readMetadata(raw);
|
|
6216
|
-
if (!found.declares && !isWholeDocument(raw)) continue;
|
|
6217
6238
|
const settled = found.titles.length === 1 && !found.computedTitle ? found.titles[0].value : void 0;
|
|
6218
6239
|
pages.push({ file, route: routeOf(file), title: settled, noindex: found.noindex });
|
|
6219
6240
|
}
|
|
@@ -6223,6 +6244,10 @@ function seo(opts) {
|
|
|
6223
6244
|
add(listed, "J-124", "error", `the sitemap lists ${page.route}, and ${page.file} says noindex. One answer per route: a crawler reading two picks one.`);
|
|
6224
6245
|
}
|
|
6225
6246
|
}
|
|
6247
|
+
if (pathLocs.length > 0) {
|
|
6248
|
+
const shown = pathLocs.slice(0, 3).map((p) => p.loc).join(", ");
|
|
6249
|
+
add(pathLocs[0].file, "J-124", "error", `${pathLocs.length} sitemap entr${pathLocs.length === 1 ? "y is a path" : "ies are paths"} (${shown}), not a full URL. A crawler drops them; with no origin yet, no sitemap is the honest one.`);
|
|
6250
|
+
}
|
|
6226
6251
|
const byTitle = /* @__PURE__ */ new Map();
|
|
6227
6252
|
for (const page of pages) {
|
|
6228
6253
|
if (!page.title || page.noindex) continue;
|
|
@@ -6235,15 +6260,8 @@ function seo(opts) {
|
|
|
6235
6260
|
}
|
|
6236
6261
|
const indexable = pages.filter((p) => !p.noindex);
|
|
6237
6262
|
const publicSurface = opts.mode !== "product" && opts.mode !== "operator";
|
|
6238
|
-
if (publicSurface && indexable.length > 0) {
|
|
6239
|
-
|
|
6240
|
-
add(".", "J-124", "warning", `${indexable.length} indexable page(s) and no sitemap. A crawler then finds what it happens to link to.`);
|
|
6241
|
-
} else if (sitemapRoutes.size === 0) {
|
|
6242
|
-
add(sitemapFiles[0], "J-127", "error", "the sitemap lists no routes. An empty sitemap is not a missing one: it is a claim that the site has nothing.");
|
|
6243
|
-
}
|
|
6244
|
-
if (robotsFiles.length === 0) {
|
|
6245
|
-
add(".", "J-123", "note", "no robots file. Nothing is blocked by its absence, but nothing points at the sitemap either.");
|
|
6246
|
-
}
|
|
6263
|
+
if (publicSurface && indexable.length > 0 && sitemapFiles.length > 0 && sitemapRoutes.size === 0) {
|
|
6264
|
+
add(sitemapFiles[0], "J-127", "error", "the sitemap lists no routes. An empty sitemap is not a missing one: it is a claim that the site has nothing.");
|
|
6247
6265
|
}
|
|
6248
6266
|
const titleBudget = pages.filter((p) => p.title && p.title.length > TITLE_BUDGET).length;
|
|
6249
6267
|
const line = `JIG_SEO: pages=${pages.length} indexable=${indexable.length} noindex=${pages.length - indexable.length} sitemap=${sitemapFiles.length > 0 ? sitemapRoutes.size : "none"} robots=${robotsFiles.length > 0 ? "yes" : "no"} overlong=${titleBudget} findings=${findings.length}`;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jig-ui",
|
|
3
|
-
"version": "0.14.
|
|
3
|
+
"version": "0.14.1",
|
|
4
4
|
"description": "A design system for coding agents. 130 numbered UI rules, brand x mode design tokens, and an installer for Claude Code, Codex, Cursor and opencode.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -608,9 +608,9 @@ The budget is not a style preference; it is the width of the box someone else re
|
|
|
608
608
|
`robots.txt` is not this. It is public, advisory, and read by strangers as a list of interesting places: naming `/admin` there tells everyone where it is. A path is safe to name only when something else protects it — a session guard, an authenticating API — and never because the file asked politely.
|
|
609
609
|
|
|
610
610
|
### J-124 A sitemap that contradicts the page
|
|
611
|
-
❌ A route listed in the sitemap whose own metadata says `noindex`; a sitemap entry for a page that does not exist
|
|
612
|
-
✅ One answer per route. The sitemap lists what is indexable, and nothing else.
|
|
613
|
-
Contradicting yourself in two files tells a crawler you do not know which is true, and it will decide for you.
|
|
611
|
+
❌ A route listed in the sitemap whose own metadata says `noindex`; a sitemap entry for a page that does not exist; a `<loc>` written as a path, `/pricing`, which the format does not accept and a crawler drops
|
|
612
|
+
✅ One answer per route. The sitemap lists what is indexable, as full URLs, and nothing else.
|
|
613
|
+
Contradicting yourself in two files tells a crawler you do not know which is true, and it will decide for you. A sitemap needs the site's origin, so a project that has none yet has nothing to write: no sitemap is honest, and one full of paths is not.
|
|
614
614
|
|
|
615
615
|
### J-125 Invented facts in metadata
|
|
616
616
|
❌ `lastModified: new Date()` in a sitemap; a `datePublished` filled in because the field existed; an author, rating or price nobody supplied
|
|
@@ -1160,8 +1160,11 @@ Run `{{scripts_path}} seo` and report what it says. It needs no `DECISIONS.md`,
|
|
|
1160
1160
|
no spec and no confirmation: it reads what the project already serves, so it is
|
|
1161
1161
|
safe to run on the first day and on somebody else's codebase. It audits the whole project,
|
|
1162
1162
|
which is what one file cannot: a route whose metadata says `noindex` sitting in
|
|
1163
|
-
the sitemap, two pages claiming the same title,
|
|
1164
|
-
|
|
1163
|
+
the sitemap, two pages claiming the same title, a sitemap that lists nothing or
|
|
1164
|
+
lists paths instead of full URLs. It counts the pages it found, a dynamic route
|
|
1165
|
+
once, and whether a sitemap and a robots file exist; neither is a finding, since
|
|
1166
|
+
no rule asks for them and a project with no domain yet cannot write an honest
|
|
1167
|
+
sitemap. Do not invent one to clear a count.
|
|
1165
1168
|
|
|
1166
1169
|
It is not a phase. The per-page half belongs where the copy is decided and
|
|
1167
1170
|
written — `spec` records whether a page is indexable and what it claims, `make`
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"status": "available"
|
|
26
26
|
},
|
|
27
27
|
"seo": {
|
|
28
|
-
"description": "Audit what a search engine and a link preview read, across the whole project: a noindex route in the sitemap, two pages with one title,
|
|
28
|
+
"description": "Audit what a search engine and a link preview read, across the whole project: a noindex route in the sitemap, two pages with one title, a sitemap that lists nothing or lists paths instead of URLs. The per-page half lives in spec, make and check.",
|
|
29
29
|
"argumentHint": "[--json]",
|
|
30
30
|
"status": "available"
|
|
31
31
|
},
|