sbuilder-mcp 0.75.0 → 0.76.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
@@ -6,6 +6,18 @@ All notable changes to this project are documented in this file.
6
6
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
7
7
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
8
8
 
9
+ ## [0.76.1] - 2026-09-28
10
+
11
+ ### Fixed
12
+ - `sb_review`'s `unreachablePage` gap now reads a page's `/source` response under its `source.document` envelope instead of a bare `document`, so a link found in another page's body is actually counted; previously every page appeared to carry no links and every candidate page was reported unreachable.
13
+ - `sb_review`'s `no_h1` finding no longer points at a shared header's brand line when that header is composed onto every page; it now recognizes a shared section by either its stored `globalRef` stamp or its composed `globalId` stamp, and fires only on a page's own heading, staying silent when a page has no heading of its own.
14
+ - `sb_review`'s `pageSeo` gap now reads "1 page carries" instead of "1 page carry" when only a single page is missing `settings.title` or `settings.description`.
15
+
16
+ ## [0.76.0] - 2026-09-28
17
+
18
+ ### Added
19
+ - `sb_review` gains a `siteFooter` readiness gap: since `sb_store` action `chrome` builds the shared header and treats the footer as an opt-in flag on a separate call, a site built this way can end every page at its last section with no policy links, no contact, and no copyright line; it fires once the site already has at least one global section and two or more pages but no footer among them, pointing at `sb_store` action `chrome` with `footer:true`.
20
+
9
21
  ## [0.75.0] - 2026-09-28
10
22
 
11
23
  ### Added
package/CHANGELOG.vi.md CHANGED
@@ -6,6 +6,18 @@ Mọi thay đổi đáng chú ý của dự án được ghi lại trong file n
6
6
  Định dạng dựa trên [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
7
7
  và dự án tuân theo [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
8
8
 
9
+ ## [0.76.1] - 2026-09-28
10
+
11
+ ### Fixed
12
+ - Gap `unreachablePage` của `sb_review` giờ đọc response `/source` của trang theo đúng envelope `source.document` thay vì `document` trần trụi, nên một link tìm thấy trong thân một trang khác giờ được tính đúng; trước đó mọi trang đều bị coi là không mang link nào và mọi trang ứng viên đều bị báo là không thể tới được.
13
+ - Finding `no_h1` của `sb_review` không còn trỏ vào dòng thương hiệu của header dùng chung khi header đó được dựng vào mọi trang; giờ nhận diện một section dùng chung qua cả stamp `globalRef` đã lưu lẫn stamp `globalId` được dựng, và chỉ báo trên heading của chính trang đó, im lặng khi trang không có heading riêng nào.
14
+ - Gap `pageSeo` của `sb_review` giờ hiển thị "1 page carries" thay vì "1 page carry" khi chỉ có một trang thiếu `settings.title` hoặc `settings.description`.
15
+
16
+ ## [0.76.0] - 2026-09-28
17
+
18
+ ### Added
19
+ - `sb_review` có thêm gap readiness `siteFooter`: vì `sb_store` action `chrome` dựng header dùng chung còn footer là một cờ tùy chọn ở một lệnh riêng, một site dựng theo cách này có thể khiến mọi trang kết thúc ở section cuối cùng, không có link chính sách, không có liên hệ, không có dòng bản quyền; gap báo khi site đã có ít nhất một global section và từ hai trang trở lên nhưng không trang nào có footer, trỏ tới `sb_store` action `chrome` với `footer:true`.
20
+
9
21
  ## [0.75.0] - 2026-09-28
10
22
 
11
23
  ### Added
package/dist/core/tree.js CHANGED
@@ -14,6 +14,18 @@ export const SPEC_GLOBAL_KIND = 'globalKind';
14
14
  export const SPEC_GLOBAL_REV = 'globalRev';
15
15
  export const SPEC_OVERLAY_REV = 'overlayRev';
16
16
  export const SPEC_GLOBAL_REF = 'globalRef';
17
+ /**
18
+ * Is this node a shared section, by EITHER stamp: the stored reference
19
+ * (`globalRef`, what a client writes) or the composed one (`globalId`, what a
20
+ * page read back from the platform carries). A check knowing only one misses
21
+ * the other on every read — measured: a header composed as `globalId` on all
22
+ * six pages of a store, and a rule keyed on `globalRef` treating it as the
23
+ * page's own.
24
+ */
25
+ export function isSharedSection(doc, id) {
26
+ const s = doc.nodes[id]?.specials ?? {};
27
+ return typeof s[SPEC_GLOBAL_ID] === 'string' || typeof s[SPEC_GLOBAL_REF] === 'string';
28
+ }
17
29
  /** The specials stamps a composed APP BLOCK carries (appblocks.go:97-100). */
18
30
  export const SPEC_APP_BLOCK_ID = 'appBlockId';
19
31
  export const SPEC_APP_BLOCK_REF = 'appBlockRef';
@@ -183,8 +183,12 @@ export async function gatherReadiness(ctx, siteId, pageNodes, openPageId) {
183
183
  const others = (input.pages ?? [])
184
184
  .filter((p) => p.status === 'published' && typeof p.id === 'string' && p.id !== openPageId)
185
185
  .slice(0, 40);
186
- const sources = await Promise.all(others.map((p) => get(`/api/sites/${site}/pages/${encodeURIComponent(p.id)}/source`)));
187
- const nodes = sources.flatMap((src) => Object.values(src?.document?.nodes ?? {}));
186
+ const sources = await Promise.all(others.map((p) =>
187
+ // `{ source: { document } }` — the envelope transport/pages.ts loadSource
188
+ // unwraps; read bare, this pass saw no link in any page (measured on a
189
+ // local store, every page's document present and every href missed).
190
+ get(`/api/sites/${site}/pages/${encodeURIComponent(p.id)}/source`)));
191
+ const nodes = sources.flatMap((src) => Object.values(src?.source?.document?.nodes ?? {}));
188
192
  input.menuLinks.hrefs.push(...hrefsIn(nodes));
189
193
  }
190
194
  return input;
@@ -217,6 +217,29 @@ export function readinessGaps(input) {
217
217
  'globalRef instead of a copy, and one edit reaches every page.',
218
218
  });
219
219
  }
220
+ // A HEADER AND NO FOOTER.
221
+ //
222
+ // `sb_store action:"chrome"` builds the header; the footer is an opt-in flag
223
+ // on the same call, so a site built here has a shared header and, on every
224
+ // page, either its own footer or none — no policy links, no contact, no
225
+ // copyright line, and nothing below the last section but the end of the
226
+ // document. The same threshold as siteChrome, asked only once the site HAS
227
+ // shared chrome (an empty list is siteChrome's finding, not this one).
228
+ if (pages &&
229
+ pages.length >= 2 &&
230
+ input.globalKinds &&
231
+ input.globalKinds.length > 0 &&
232
+ !input.globalKinds.includes('footer')) {
233
+ gaps.push({
234
+ id: 'siteFooter',
235
+ draft: false,
236
+ problem: 'The site shares a header and no footer, so every page ends at its last section — ' +
237
+ 'no policy links, no contact, no copyright line, and each page that has a footer of ' +
238
+ 'its own drifts from the next.',
239
+ fix: 'sb_store action:"chrome" footer:true builds one shared footer on a real site menu and ' +
240
+ 'attaches it to every page. Put the policy, contact and about pages in it.',
241
+ });
242
+ }
220
243
  // THE SITE IS DARK RIGHT NOW AND SAYING NOTHING IN ITS OWN VOICE.
221
244
  //
222
245
  // Reported FIRST, because it is the only finding here that is not about what
@@ -310,7 +333,8 @@ export function readinessGaps(input) {
310
333
  id: 'pageSeo',
311
334
  draft: false,
312
335
  problem: pageCount(bare) +
313
- ' carry no settings.title or settings.description. Each serves the SITE NAME as its ' +
336
+ (bare.length === 1 ? ' carries' : ' carry') +
337
+ ' no settings.title or settings.description. Each serves the SITE NAME as its ' +
314
338
  '<title> and no meta description — a search result cannot tell them apart, and ' +
315
339
  'nothing on the canvas or in a screenshot shows it.',
316
340
  fix: 'PATCH /api/v1/pages/{id} with {settings: {...existing, title, description}} — ' +
@@ -1,4 +1,4 @@
1
- import { childrenOf, childrenWithSatellites, isOverlay, pageChildren, appBlockRoot, SPEC_GLOBAL_REF, SPEC_APP_BLOCK_REF } from '../../core/tree.js';
1
+ import { childrenOf, childrenWithSatellites, isOverlay, isSharedSection, pageChildren, appBlockRoot, SPEC_GLOBAL_REF, SPEC_APP_BLOCK_REF } from '../../core/tree.js';
2
2
  import { STUCK_STATE, stickyBlockedBy, stuckHostOf, isPinnedNode } from './sticky.js';
3
3
  import { bandOf } from './traps.js';
4
4
  import { HOVER_STATE, hoverHome } from './hover.js';
@@ -704,7 +704,28 @@ opts = {}) {
704
704
  // level but the one a search engine and a screen reader look for first.
705
705
  // Nothing on the canvas shows the tag. Page bands only (`band` excludes the
706
706
  // overlays): a drawer heading is not the page's.
707
+ //
708
+ // NAMED ON THE PAGE'S OWN HEADING, never a shared section's. The draft
709
+ // composes the global header in, so its brand line is the first heading on
710
+ // EVERY page — measured on a local store: one node id reported on six pages,
711
+ // and the fix would have made the site name the h1 everywhere. A shared h1
712
+ // still counts as present (that is a site's own decision); a page whose only
713
+ // headings are shared has none of its own and is left alone.
707
714
  {
715
+ const shared = new Set();
716
+ for (const sectionId of pageChildren(d)) {
717
+ if (!isSharedSection(d, sectionId))
718
+ continue;
719
+ const stack = [sectionId];
720
+ while (stack.length) {
721
+ const id = stack.pop();
722
+ if (shared.has(id))
723
+ continue;
724
+ shared.add(id);
725
+ for (const k of childrenWithSatellites(d, id))
726
+ stack.push(k);
727
+ }
728
+ }
708
729
  const level = (id) => {
709
730
  const n = d.nodes[id];
710
731
  const tag = n.specials?.htmlTag;
@@ -713,8 +734,9 @@ opts = {}) {
713
734
  return n.data.type === 'heading' ? 'h2' : null;
714
735
  };
715
736
  const headings = walkOrder.filter((id) => band.has(id) && level(id) !== null);
716
- if (headings.length > 0 && !headings.some((id) => level(id) === 'h1')) {
717
- const first = headings[0];
737
+ const own = headings.filter((id) => !shared.has(id));
738
+ if (own.length > 0 && !headings.some((id) => level(id) === 'h1')) {
739
+ const first = own[0];
718
740
  out.push({
719
741
  code: 'no_h1',
720
742
  nodeId: first,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sbuilder-mcp",
3
- "version": "0.75.0",
3
+ "version": "0.76.1",
4
4
  "description": "MCP server that designs and operates a Store Builder site — pages, data, theme and publish — through the platform's own API and live-edit protocol.",
5
5
  "mcpName": "io.github.vuluu2k/sbuilder-mcp",
6
6
  "type": "module",