@fullstackdatasolutions/articles 1.3.0 → 1.3.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fullstackdatasolutions/articles",
3
- "version": "1.3.0",
3
+ "version": "1.3.1",
4
4
  "private": false,
5
5
  "license": "MIT",
6
6
  "funding": {
@@ -1017,6 +1017,27 @@ describe('AI markdown helpers', () => {
1017
1017
  expect(blocked.status).toBe(404)
1018
1018
  })
1019
1019
 
1020
+ it('links the markdown twin back to its HTML article, not to itself', async () => {
1021
+ setupArticleTreeMock([
1022
+ {
1023
+ slug: 'allowed',
1024
+ frontmatter: 'date: 2025-01-01\naiCrawl: true\n',
1025
+ body: '# Allowed',
1026
+ },
1027
+ ])
1028
+
1029
+ const response = await getArticleMarkdownResponse('allowed', {
1030
+ siteUrl: 'https://example.com/',
1031
+ siteName: 'Example',
1032
+ })
1033
+
1034
+ expect(response.headers.get('Link')).toBe(
1035
+ '<https://example.com/articles/allowed>; rel="canonical"'
1036
+ )
1037
+ expect(response.headers.get('Link')).not.toContain('.md')
1038
+ expect(response.headers.get('Link')).not.toContain('alternate')
1039
+ })
1040
+
1020
1041
  it('generates robots rules for AI crawlers and blocked articles only', async () => {
1021
1042
  setupArticleTreeMock([
1022
1043
  { slug: 'allowed', frontmatter: 'date: 2025-01-01\naiCrawl: true\n' },
@@ -775,11 +775,20 @@ export async function getArticleMarkdownResponse(
775
775
  article && config.markdownTwinHeader !== false
776
776
  ? `${buildMarkdownTwinHeader(article, config, markdown)}${markdown.trimStart()}`
777
777
  : markdown
778
+ // `getArticleAiHeaders` builds a `rel="alternate"` link *to* the twin, which
779
+ // is correct on the HTML page and wrong here - on the twin's own response it
780
+ // pointed at the URL being requested, telling a client the alternate of
781
+ // `/articles/x.md` is `/articles/x.md`. `getArticleMarkdown` already returned
782
+ // null for anything not opted in, so that helper's other branch
783
+ // (`X-Robots-Tag: noai`) was unreachable from this call site anyway. Point
784
+ // back at the HTML article instead, which is the relationship a client
785
+ // fetching the twin actually needs: this is a representation of that page.
786
+ const canonicalUrl = `${config.siteUrl.replace(/\/$/, '')}/articles/${slug}`
778
787
  return new Response(body, {
779
788
  headers: {
780
789
  'Content-Type': 'text/markdown; charset=utf-8',
781
790
  'Cache-Control': 'public, max-age=3600, s-maxage=3600',
782
- ...(article ? getArticleAiHeaders(article, config) : {}),
791
+ Link: `<${canonicalUrl}>; rel="canonical"`,
783
792
  },
784
793
  })
785
794
  }