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 +12 -0
- package/CHANGELOG.vi.md +12 -0
- package/dist/core/tree.js +12 -0
- package/dist/domains/site/readiness-fetch.js +6 -2
- package/dist/domains/site/readiness.js +25 -1
- package/dist/domains/site/review.js +25 -3
- package/package.json +1 -1
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) =>
|
|
187
|
-
|
|
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
|
-
|
|
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
|
-
|
|
717
|
-
|
|
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.
|
|
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",
|