@fullstackdatasolutions/articles 1.3.1 → 1.4.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.
package/dist/server.d.cts CHANGED
@@ -599,6 +599,22 @@ interface ArticlesConfig {
599
599
  * a config is available (i.e. via `getArticleMarkdownResponse`).
600
600
  */
601
601
  markdownTwinHeader?: boolean;
602
+ /**
603
+ * Set to `false` to stop serving the `category/`, `authors/`, and `series/`
604
+ * listing twins with `X-Robots-Tag: noindex`. Default: `true`.
605
+ *
606
+ * Only the listing twins are configurable. Article twins are always
607
+ * `noindex`: an article twin is the same text as exactly one HTML article,
608
+ * so indexing it can only ever split that article's own signal between two
609
+ * URLs. A listing twin is a different case - it is a generated index of a
610
+ * category or author rather than a copy of one page - so a site may have a
611
+ * reason to let it rank, and this leaves that open.
612
+ *
613
+ * Either way AI crawlers can still fetch every twin; `noindex` only removes
614
+ * them from search results, and never blocks the fetch that is the point of
615
+ * publishing them.
616
+ */
617
+ listingTwinNoindex?: boolean;
602
618
  /**
603
619
  * Vendor-neutral event callback (Phase 27F). Fired by components/hooks at
604
620
  * meaningful reader-journey moments (see `ArticleEvent` in `events.ts`).
package/dist/server.d.ts CHANGED
@@ -599,6 +599,22 @@ interface ArticlesConfig {
599
599
  * a config is available (i.e. via `getArticleMarkdownResponse`).
600
600
  */
601
601
  markdownTwinHeader?: boolean;
602
+ /**
603
+ * Set to `false` to stop serving the `category/`, `authors/`, and `series/`
604
+ * listing twins with `X-Robots-Tag: noindex`. Default: `true`.
605
+ *
606
+ * Only the listing twins are configurable. Article twins are always
607
+ * `noindex`: an article twin is the same text as exactly one HTML article,
608
+ * so indexing it can only ever split that article's own signal between two
609
+ * URLs. A listing twin is a different case - it is a generated index of a
610
+ * category or author rather than a copy of one page - so a site may have a
611
+ * reason to let it rank, and this leaves that open.
612
+ *
613
+ * Either way AI crawlers can still fetch every twin; `noindex` only removes
614
+ * them from search results, and never blocks the fetch that is the point of
615
+ * publishing them.
616
+ */
617
+ listingTwinNoindex?: boolean;
602
618
  /**
603
619
  * Vendor-neutral event callback (Phase 27F). Fired by components/hooks at
604
620
  * meaningful reader-journey moments (see `ArticleEvent` in `events.ts`).
package/dist/server.js CHANGED
@@ -1025,15 +1025,17 @@ function getMarkdownTwinResponse(slug, config, options) {
1025
1025
  if (listing !== void 0) {
1026
1026
  if (listing === null) return new Response("Not Found", { status: 404 });
1027
1027
  reportAiCrawl(slug, config, options == null ? void 0 : options.headers);
1028
- return new Response(listing, { headers: LISTING_MARKDOWN_HEADERS });
1028
+ return new Response(listing, { headers: listingMarkdownHeaders(config) });
1029
1029
  }
1030
1030
  return getArticleMarkdownResponse(slug, config, options);
1031
1031
  });
1032
1032
  }
1033
- var LISTING_MARKDOWN_HEADERS = {
1034
- "Content-Type": "text/markdown; charset=utf-8",
1035
- "Cache-Control": "public, max-age=3600, s-maxage=3600"
1036
- };
1033
+ function listingMarkdownHeaders(config) {
1034
+ return __spreadValues({
1035
+ "Content-Type": "text/markdown; charset=utf-8",
1036
+ "Cache-Control": "public, max-age=3600, s-maxage=3600"
1037
+ }, config.listingTwinNoindex !== false && { "X-Robots-Tag": "noindex" });
1038
+ }
1037
1039
  function resolveListingMarkdown(slug, config) {
1038
1040
  return __async(this, null, function* () {
1039
1041
  const [prefix, ...rest] = slug.split("/");
@@ -1057,7 +1059,18 @@ function getArticleMarkdownResponse(slug, config, options) {
1057
1059
  headers: {
1058
1060
  "Content-Type": "text/markdown; charset=utf-8",
1059
1061
  "Cache-Control": "public, max-age=3600, s-maxage=3600",
1060
- Link: `<${canonicalUrl}>; rel="canonical"`
1062
+ Link: `<${canonicalUrl}>; rel="canonical"`,
1063
+ // A twin is a second representation of a page that is already indexed,
1064
+ // so it should not compete with that page in a search index - on a large
1065
+ // corpus it doubles the crawlable URL count with near-duplicate content.
1066
+ // noindex rather than a robots.txt disallow: AI crawlers still need to
1067
+ // fetch these, and a disallow would block the fetch that is the whole
1068
+ // point of publishing them. Set here rather than in the consuming app
1069
+ // because the `.md` path is typically a rewrite, so a host-level header
1070
+ // rule keyed on `.md` never sees the request. Not configurable: an
1071
+ // article twin is the same text as exactly one HTML article, so indexing
1072
+ // it can only ever split that article's own signal across two URLs.
1073
+ "X-Robots-Tag": "noindex"
1061
1074
  }
1062
1075
  });
1063
1076
  });