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 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, indexable pages with no sitemap, a sitemap that lists nothing. Needs no config, no decisions and no spec. |
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 site whose pages nothing
452
- points at. It writes nothing and needs nothing, so it is safe to run on the first
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|routes)\//, "").replace(/(^|\/)(index|page)\.(html?|astro|vue|svelte|[jt]sx?|md|mdx)$/i, "$1").replace(/\.(html?|astro|vue|svelte|[jt]sx?|md|mdx)$/i, "").replace(/\/$/, "");
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 (!isReaderText(file)) continue;
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
- if (sitemapFiles.length === 0) {
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.0",
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, indexable pages with no sitemap,
1164
- a sitemap that lists nothing.
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, indexable pages with no sitemap, a sitemap that lists nothing. The per-page half lives in spec, make and check.",
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
  },