@mohasinac/appkit 4.72.0 → 4.73.0

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.
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "stampVersion": 1,
3
- "version": "4.72.0",
4
- "head": "575e897961a5c80f9a04fbbf12f3f78346e9d654",
3
+ "version": "4.73.0",
4
+ "head": "be53458929a89f0a763476ca3082d1bfe65b3bcb",
5
5
  "branch": "main",
6
6
  "dirty": true,
7
- "builtAt": "2026-11-09T00:00:40.787Z"
7
+ "builtAt": "2026-11-09T00:57:51.740Z"
8
8
  }
@@ -1,5 +1,62 @@
1
1
  import type { MetadataRoute } from "next";
2
+ /**
3
+ * Every section this builder can emit. The names are the same keys the
4
+ * per-section count digest logs and `scripts/deploy.mjs` asserts against, so
5
+ * there is one vocabulary for "which part of the sitemap".
6
+ */
7
+ export declare const SITEMAP_SECTIONS: readonly ["static", "category", "brand", "franchise", "bundle", "group", "blog", "product", "collectorDrop", "preOrder", "clearance", "classified", "digitalCode", "live", "event", "store", "scammer"];
8
+ export type SitemapSection = (typeof SITEMAP_SECTIONS)[number];
2
9
  export interface SitemapOptions {
3
10
  baseUrl: string;
11
+ /**
12
+ * Emit ONLY these sections. Omit for all of them, which is what the main
13
+ * shop wants.
14
+ *
15
+ * ## 🛑 Why this exists, and why it is not a nice-to-have
16
+ *
17
+ * The blog and events surfaces now live on their own deployments
18
+ * (`blogs.`/`events.`). Each needs a sitemap carrying **its own** URLs on
19
+ * **its own** host, and the shop needs to stop advertising them — because the
20
+ * moment the shop 301s `/blog/*` to the subdomain, every `/blog` URL left in
21
+ * the shop's sitemap becomes a redirecting entry.
22
+ *
23
+ * That is not a hypothetical. This file's own note at the `disabledRoutes`
24
+ * filter records the measured version: on 2026-10-27 the production sitemap
25
+ * advertised **17 `/blog` URLs and one `/reviews` URL** that already 404'd at
26
+ * the edge — 18 crawl errors on pages we had explicitly asked Google to
27
+ * fetch. And `seo.server.ts` in the consumer records the worse outcome, when
28
+ * half the URL surface followed one host and half another: *every URL in the
29
+ * sitemap 307-redirected and Google dropped the site.*
30
+ *
31
+ * So a host must be able to say "my sitemap is these sections and nothing
32
+ * else", and the two halves must be derived from one list rather than two
33
+ * hand-maintained ones. Hence an allowlist here instead of a second sitemap
34
+ * builder in each extracted app.
35
+ *
36
+ * 🛑 A section NOT selected is not fetched at all — the point is to avoid the
37
+ * Firestore reads too, not merely to filter the result. A subdomain querying
38
+ * the entire product catalogue to throw it away would make every sitemap
39
+ * request pay for sixteen collections it never emits.
40
+ *
41
+ * 🛑 `static` is a section like any other, so an allowlist that omits it
42
+ * emits no static pages. That is deliberate: the shop's static pages
43
+ * (`/about`, `/contact`, the browse routes) belong to the shop, and a
44
+ * subdomain advertising them under its own host would be inventing URLs that
45
+ * do not exist there.
46
+ */
47
+ sections?: readonly SitemapSection[];
48
+ /**
49
+ * Emit everything EXCEPT these. Mutually exclusive with `sections` in
50
+ * practice; if both are given, `sections` is applied first and this narrows
51
+ * it further.
52
+ *
53
+ * This is the shop's half of the split — `excludeSections: ["blog", "event"]`
54
+ * — and it is spelled as an exclusion rather than a 15-name allowlist so that
55
+ * a NEW section added to this file is carried by the shop automatically. An
56
+ * allowlist there would silently drop the next surface somebody adds, which
57
+ * is the same "measured as empty → looks intentional" failure the digest
58
+ * below exists to catch.
59
+ */
60
+ excludeSections?: readonly SitemapSection[];
4
61
  }
5
- export declare function buildSitemap({ baseUrl }: SitemapOptions): Promise<MetadataRoute.Sitemap>;
62
+ export declare function buildSitemap({ baseUrl, sections: only, excludeSections, }: SitemapOptions): Promise<MetadataRoute.Sitemap>;
@@ -67,6 +67,30 @@ function sitemapSectionFailed(label, err) {
67
67
  serverLogger.error(`sitemap: section "${label}" failed and returned 0 URLs`, { section: label, error: err });
68
68
  return [];
69
69
  }
70
+ /**
71
+ * Every section this builder can emit. The names are the same keys the
72
+ * per-section count digest logs and `scripts/deploy.mjs` asserts against, so
73
+ * there is one vocabulary for "which part of the sitemap".
74
+ */
75
+ export const SITEMAP_SECTIONS = [
76
+ "static",
77
+ "category",
78
+ "brand",
79
+ "franchise",
80
+ "bundle",
81
+ "group",
82
+ "blog",
83
+ "product",
84
+ "collectorDrop",
85
+ "preOrder",
86
+ "clearance",
87
+ "classified",
88
+ "digitalCode",
89
+ "live",
90
+ "event",
91
+ "store",
92
+ "scammer",
93
+ ];
70
94
  /**
71
95
  * Read every document a query matches, in pages.
72
96
  *
@@ -512,26 +536,39 @@ async function fetchScammerUrls(baseUrl) {
512
536
  return sitemapSectionFailed("scammer", err);
513
537
  }
514
538
  }
515
- export async function buildSitemap({ baseUrl }) {
539
+ export async function buildSitemap({ baseUrl, sections: only, excludeSections, }) {
540
+ /*
541
+ * Which sections this host emits. See `SitemapOptions.sections` for why a
542
+ * host must be able to answer this at all.
543
+ *
544
+ * 🛑 `wants()` gates the FETCH, not the output. An unselected section issues
545
+ * no Firestore query — a subdomain must not pay for sixteen collections it
546
+ * throws away on every sitemap request.
547
+ */
548
+ const excluded = new Set(excludeSections ?? []);
549
+ const allowed = only ? new Set(only) : null;
550
+ const wants = (s) => !excluded.has(s) && (allowed === null || allowed.has(s));
551
+ /** `[]` for a section this host does not emit, without running its query. */
552
+ const ifWanted = (s, run) => wants(s) ? run() : Promise.resolve([]);
516
553
  const [productUrls, categoryUrls, brandUrls, franchiseUrls, bundleUrls, groupedListingUrls, eventUrls, blogUrls, collectorDropUrls, preOrderUrls, clearanceUrls, classifiedUrls, digitalCodeUrls, liveUrls, storeUrls, scammerUrls,] = await Promise.all([
517
- fetchProductUrls(baseUrl),
518
- fetchCategoryUrls(baseUrl),
519
- fetchBrandUrls(baseUrl),
520
- fetchFranchiseUrls(baseUrl),
521
- fetchBundleUrls(baseUrl),
522
- fetchGroupedListingUrls(baseUrl),
523
- fetchEventUrls(baseUrl),
524
- fetchBlogPostUrls(baseUrl),
525
- fetchCollectorDropUrls(baseUrl),
526
- fetchPreOrderUrls(baseUrl),
527
- fetchClearanceUrls(baseUrl),
528
- fetchClassifiedUrls(baseUrl),
529
- fetchDigitalCodeUrls(baseUrl),
530
- fetchLiveUrls(baseUrl),
531
- fetchStoreUrls(baseUrl),
532
- fetchScammerUrls(baseUrl),
554
+ ifWanted("product", () => fetchProductUrls(baseUrl)),
555
+ ifWanted("category", () => fetchCategoryUrls(baseUrl)),
556
+ ifWanted("brand", () => fetchBrandUrls(baseUrl)),
557
+ ifWanted("franchise", () => fetchFranchiseUrls(baseUrl)),
558
+ ifWanted("bundle", () => fetchBundleUrls(baseUrl)),
559
+ ifWanted("group", () => fetchGroupedListingUrls(baseUrl)),
560
+ ifWanted("event", () => fetchEventUrls(baseUrl)),
561
+ ifWanted("blog", () => fetchBlogPostUrls(baseUrl)),
562
+ ifWanted("collectorDrop", () => fetchCollectorDropUrls(baseUrl)),
563
+ ifWanted("preOrder", () => fetchPreOrderUrls(baseUrl)),
564
+ ifWanted("clearance", () => fetchClearanceUrls(baseUrl)),
565
+ ifWanted("classified", () => fetchClassifiedUrls(baseUrl)),
566
+ ifWanted("digitalCode", () => fetchDigitalCodeUrls(baseUrl)),
567
+ ifWanted("live", () => fetchLiveUrls(baseUrl)),
568
+ ifWanted("store", () => fetchStoreUrls(baseUrl)),
569
+ ifWanted("scammer", () => fetchScammerUrls(baseUrl)),
533
570
  ]);
534
- const staticUrls = staticPages(baseUrl);
571
+ const staticUrls = wants("static") ? staticPages(baseUrl) : [];
535
572
  // Per-section count digest. A section that drops to zero is otherwise
536
573
  // indistinguishable from a site that genuinely has none of that thing — which
537
574
  // is exactly how `category: 0` survived unnoticed while every other section
@@ -715,6 +752,28 @@ export async function buildSitemap({ baseUrl }) {
715
752
  if (page && closed.has(page))
716
753
  sections[name] = 0;
717
754
  }
755
+ /*
756
+ * 🛑 A section this host does not EMIT is dropped from the digest entirely,
757
+ * not reported as zero.
758
+ *
759
+ * The comment above this block is the reason, and it was written about the
760
+ * `closed` case: *"a warning that fires every build for a reason everyone
761
+ * knows about is how the next real zero gets ignored. 'Closed on purpose'
762
+ * and 'broken' must not look the same in the log."* Section scoping is the
763
+ * same hazard one axis over — without this, the blogs deployment would warn
764
+ * about fifteen empty sections on every single sitemap request, and the shop
765
+ * would warn about `blog` and `event` forever. Two permanent false alarms is
766
+ * how the alarm stops being read.
767
+ *
768
+ * Deleting the key rather than zeroing it also keeps the digest honest for
769
+ * `scripts/deploy.mjs`, which asserts these counts against the deployed
770
+ * sitemap: a section that is absent from the response should be absent here
771
+ * too, not present-and-zero.
772
+ */
773
+ for (const name of Object.keys(sections)) {
774
+ if (!wants(name))
775
+ delete sections[name];
776
+ }
718
777
  const empty = Object.entries(sections)
719
778
  .filter(([name, n]) => n === 0 && !closed.has(sectionPage[name]))
720
779
  .map(([name]) => name);
@@ -724,6 +783,18 @@ export async function buildSitemap({ baseUrl }) {
724
783
  closed: [...closed],
725
784
  });
726
785
  }
786
+ /*
787
+ * Logged so a host's scope is visible in production, not just in its source.
788
+ * "Why is /blog missing from the sitemap" is otherwise answerable only by
789
+ * reading the caller.
790
+ */
791
+ if (only || excludeSections?.length) {
792
+ serverLogger.info("sitemap: section scope applied", {
793
+ ...(only ? { only: [...only] } : {}),
794
+ ...(excludeSections?.length ? { excluded: [...excludeSections] } : {}),
795
+ emitting: Object.keys(sections),
796
+ });
797
+ }
727
798
  if (empty.length > 0) {
728
799
  serverLogger.warn(`sitemap: ${empty.length} section(s) produced 0 URLs`, { empty, sections, total });
729
800
  }
package/dist/server.d.ts CHANGED
@@ -520,7 +520,8 @@ export { buildSitemap } from "./_internal/server/features/seo/sitemap";
520
520
  export { buildRobots } from "./_internal/server/features/seo/robots";
521
521
  export { buildManifest } from "./_internal/server/features/seo/manifest";
522
522
  export { buildDefaultOgImage, DEFAULT_OG_SIZE, resolveOgImageUrl } from "./_internal/server/features/seo/og";
523
- export type { SitemapOptions } from "./_internal/server/features/seo/sitemap";
523
+ export type { SitemapOptions, SitemapSection } from "./_internal/server/features/seo/sitemap";
524
+ export { SITEMAP_SECTIONS } from "./_internal/server/features/seo/sitemap";
524
525
  export type { RobotsOptions } from "./_internal/server/features/seo/robots";
525
526
  export type { ManifestOptions } from "./_internal/server/features/seo/manifest";
526
527
  export type { DefaultOgOptions } from "./_internal/server/features/seo/og";
package/dist/server.js CHANGED
@@ -1475,6 +1475,7 @@ export { buildSitemap } from "./_internal/server/features/seo/sitemap.js";
1475
1475
  export { buildRobots } from "./_internal/server/features/seo/robots.js";
1476
1476
  export { buildManifest } from "./_internal/server/features/seo/manifest.js";
1477
1477
  export { buildDefaultOgImage, DEFAULT_OG_SIZE, resolveOgImageUrl } from "./_internal/server/features/seo/og.js";
1478
+ export { SITEMAP_SECTIONS } from "./_internal/server/features/seo/sitemap.js";
1478
1479
  // -- generateStaticParams slug enumerators --
1479
1480
  // Prerendering a detail route is what puts its <title> in <head> instead of
1480
1481
  // streaming it into <body>; see static-params.ts for the measurement.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mohasinac/appkit",
3
- "version": "4.72.0",
3
+ "version": "4.73.0",
4
4
  "license": "MIT",
5
5
  "publishConfig": {
6
6
  "access": "public"