canopycms 0.3.3 → 0.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/CHANGELOG.md CHANGED
@@ -5,9 +5,9 @@ packages release together, at one version.
5
5
 
6
6
  ## This build
7
7
 
8
- Version `0.3.3`, built from [`bb13b42`](https://github.com/safeinsights/canopycms/tree/bb13b429fb8deec72bbcf7f566ffa60304c5d690).
8
+ Version `0.4.0`, built from [`184d5c6`](https://github.com/safeinsights/canopycms/tree/184d5c62b8158d1fdcfbd85430f17f90aceec00c).
9
9
 
10
- Its entries are [0.3.3](#033), under **Released**.
10
+ Its entries are [0.4.0](#040), under **Released**.
11
11
 
12
12
  If you upgraded from a build on another branch, compare the `<!-- changeset: … -->` slugs in your previous build's `CHANGELOG.md` (`npm pack canopycms@<old>` fetches it) with this one's: entries whose slug your old file lacks are new to you.
13
13
 
@@ -24,7 +24,7 @@ Each entry has up to three parts:
24
24
  `node_modules/canopycms/CHANGELOG.md`, assembled for that build: its **This build** section lists
25
25
  what is new since the previous int on the same branch, or on a stable release links its entries, and its links point at the commit the build
26
26
  came from. The copy in the repository holds released history and the entries from before changesets;
27
- every entry written since is a file in [`.changeset/`](https://github.com/safeinsights/canopycms/blob/bb13b429fb8deec72bbcf7f566ffa60304c5d690/.changeset/README.md) until a stable release
27
+ every entry written since is a file in [`.changeset/`](https://github.com/safeinsights/canopycms/blob/184d5c62b8158d1fdcfbd85430f17f90aceec00c/.changeset/README.md) until a stable release
28
28
  moves it here.
29
29
 
30
30
  **Find your starting point.** From a stable pin, start at the first version under **Released**
@@ -62,6 +62,32 @@ entries under **Released**.
62
62
 
63
63
  ## Released
64
64
 
65
+ ### 0.4.0
66
+
67
+ #### Link-only references to json/yaml entries leave out the body — **behaviour change (resolved data and types)**
68
+
69
+ <!-- changeset: link-only-references-omit-data-body -->
70
+
71
+ _First in 0.4.0._
72
+
73
+ **What changed.** A reference field without `includeBody: true` now resolves a json or yaml
74
+ target without that target's `isBody` field, as it already resolved an md/mdx target without its
75
+ body. A long body is no longer copied into every entry that merely links to it. A field that is
76
+ only named `body`, with no `isBody`, is ordinary data and still resolves. The live-preview resolve
77
+ endpoint and the AI export's `referenceLink` target follow the same rule, and `referenceLink`
78
+ now receives a json/yaml target's `isBody` field as `context.body`. Live preview resolves every
79
+ reference link-only, so an `includeBody: true` reference to a json/yaml target now previews
80
+ without its body, as one to an md/mdx target already did.
81
+
82
+ With `resolvedSchema`, the target's `isBody` field is now optional in the resolved type unless
83
+ the reference sets `includeBody: true`. This applies to md/mdx targets too, which already
84
+ resolved without it.
85
+
86
+ **To adopt.** Where a reference renders its target's prose, set `includeBody: true` on that
87
+ reference field. Code that reads the body from a link-only reference's resolved type now sees
88
+ `string | undefined`; either set `includeBody: true` or handle the missing value. See
89
+ [Resolving References in a Listing](https://github.com/safeinsights/canopycms/blob/184d5c62b8158d1fdcfbd85430f17f90aceec00c/README.md#resolving-references-in-a-listing).
90
+
65
91
  ### 0.3.3
66
92
 
67
93
  #### Bare URLs in a markdown body survive a save
@@ -471,7 +497,7 @@ URL.
471
497
  2. For a branch preview: wrap views with `withCanopyPreview` in a `'use client'` module, add
472
498
  `app/preview/[[...path]]/page.server.tsx`, set `editor.previewPrefix: '/preview'`, and serve that
473
499
  route with `frame-ancestors 'self'`, not `X-Frame-Options: DENY` (see
474
- [Live Preview](https://github.com/safeinsights/canopycms/blob/bb13b429fb8deec72bbcf7f566ffa60304c5d690/README.md#live-preview)).
500
+ [Live Preview](https://github.com/safeinsights/canopycms/blob/184d5c62b8158d1fdcfbd85430f17f90aceec00c/README.md#live-preview)).
475
501
  3. A test reading view content from the route's server HTML must load the page in a browser.
476
502
  Server output rendered during a preview request carries the signed-in asset prefix; keep it out
477
503
  of cross-request caches (`unstable_cache`, module memos).
@@ -486,7 +512,7 @@ kept site styles ahead of Mantine's in the preview.
486
512
  server components. A string path marks the form's spelling (`'a.0.b'` marks `a[0].b`), and an empty
487
513
  path marks nothing. The editor counts and logs marks naming no field. `createEntrySchemaRegistry`
488
514
  refuses a field name that is empty, all digits or contains `.`, `[` or `]`. See
489
- [Live Preview](https://github.com/safeinsights/canopycms/blob/bb13b429fb8deec72bbcf7f566ffa60304c5d690/README.md#live-preview).
515
+ [Live Preview](https://github.com/safeinsights/canopycms/blob/184d5c62b8158d1fdcfbd85430f17f90aceec00c/README.md#live-preview).
490
516
 
491
517
  **To adopt.** Fix each path that stops compiling; the error lists valid ones. Rewrite computed paths
492
518
  as literal segments, or type that component's prop as plain `FieldProps`. Pass `fieldProps` as
@@ -568,7 +594,7 @@ or a failed render) renders as its plain text, with no URL; any other form of th
568
594
  fields are nested or renamed, pass the AI config's `seo` the same object you pass
569
595
  `createNextCanopyContext({ seo })`, so the sitemap and AI content read the flag from the same
570
596
  place. A field that links to a hidden section by plain URL needs a field transform; the README
571
- shows [the recipe](https://github.com/safeinsights/canopycms/blob/bb13b429fb8deec72bbcf7f566ffa60304c5d690/README.md#ai-content-configuration).
597
+ shows [the recipe](https://github.com/safeinsights/canopycms/blob/184d5c62b8158d1fdcfbd85430f17f90aceec00c/README.md#ai-content-configuration).
572
598
 
573
599
  **Now deletable.** An `exclude.where` that re-read the `noindex` flag by hand.
574
600
 
@@ -590,7 +616,7 @@ A `markdown` field with `renderAs: 'mdx'` is checked as MDX, and `mdxAllow` narr
590
616
  field or site-wide. New: field options `renderAs`, `mdxAllow`; config key `mdxAllow`; type
591
617
  `MdxAllowlist`. (int.110) A component prop allowance can be `'string'` (or
592
618
  `{ type: 'string', maxLength }`): a quoted value only, never bare or `{…}`. See
593
- [MDX content cannot run code](https://github.com/safeinsights/canopycms/blob/bb13b429fb8deec72bbcf7f566ffa60304c5d690/README.md#mdx-content-cannot-run-code).
619
+ [MDX content cannot run code](https://github.com/safeinsights/canopycms/blob/184d5c62b8158d1fdcfbd85430f17f90aceec00c/README.md#mdx-content-cannot-run-code).
594
620
 
595
621
  **To adopt.** Set `renderAs: 'mdx'` on each `markdown` field your site compiles as MDX, and
596
622
  `mdxAllow` to what your renderer takes. Set `executable: true` only on a field whose editors you
@@ -727,7 +753,7 @@ a format check.
727
753
 
728
754
  - (int.93) Submit commits gain an `Edited-by: Name (id)` trailer; PR bodies gain a section that
729
755
  re-submits replace, keeping human text. Options: `gitEditedByTrailers`, `gitCoAuthoredByTrailers`
730
- ([reference](https://github.com/safeinsights/canopycms/blob/bb13b429fb8deec72bbcf7f566ffa60304c5d690/README.md#definecanopyconfig-options)).
756
+ ([reference](https://github.com/safeinsights/canopycms/blob/184d5c62b8158d1fdcfbd85430f17f90aceec00c/README.md#definecanopyconfig-options)).
731
757
  - (int.110) Submit-commit trailers name every user who saved since the last submit commit, and
732
758
  the PR-body section every user who edited the branch, not only the submitter; settings commits
733
759
  (permissions, groups) carry the acting user's trailer.
@@ -751,7 +777,7 @@ saves only push the branch, and no failing PR task lands in System health.
751
777
  that entry type goes unavailable: the editor says so, and its entries answer 503 with
752
778
  `code: 'SCHEMA_UNAVAILABLE'`. Builds, static deploys and `generate-ai-content` still fail. The
753
779
  worker holds the base branch for up to 30 minutes while that lasts; see
754
- [New schemas wait for the editor deploy](https://github.com/safeinsights/canopycms/blob/bb13b429fb8deec72bbcf7f566ffa60304c5d690/docs/deploying-to-aws.md#new-schemas-wait-for-the-editor-deploy).
780
+ [New schemas wait for the editor deploy](https://github.com/safeinsights/canopycms/blob/184d5c62b8158d1fdcfbd85430f17f90aceec00c/docs/deploying-to-aws.md#new-schemas-wait-for-the-editor-deploy).
755
781
  Public types gain optional `unavailable` on `EntryTypeConfig` and `FlatSchemaItem`, optional
756
782
  `ApiResponse.code`, and a required `SchemaResolutionResult.issues`.
757
783
 
@@ -789,7 +815,7 @@ hang until the drain heartbeat lets termination continue. Its in-flight task is
789
815
  new stack needs no steps.
790
816
 
791
817
  **What changed.** (int.109) Detailed in
792
- [The worker instance](https://github.com/safeinsights/canopycms/blob/bb13b429fb8deec72bbcf7f566ffa60304c5d690/docs/deploying-to-aws.md#the-worker-instance): IMDSv2 with a hop limit of 1; the
818
+ [The worker instance](https://github.com/safeinsights/canopycms/blob/184d5c62b8158d1fdcfbd85430f17f90aceec00c/docs/deploying-to-aws.md#the-worker-instance): IMDSv2 with a hop limit of 1; the
793
819
  bundle is one file whose sha256 user data checks, readable as that one object; a file-system policy
794
820
  refusing clients without TLS or IAM, replacing the worker's
795
821
  `AmazonElasticFileSystemClientReadWriteAccess`; an encrypted gp3 root volume; a boot-time upgrade to
@@ -843,10 +869,10 @@ failure and exits non-zero on `selfStopped`, with a code outside `RestartPrevent
843
869
  with its hash, as `worker/dist/index.js` and `index.js.sha256`. The default, `'asset'`, is
844
870
  unchanged.
845
871
  - `CanopyCmsService` alarms when the worker logs no git sync for 30 minutes. See
846
- [Worker-down alarm](https://github.com/safeinsights/canopycms/blob/bb13b429fb8deec72bbcf7f566ffa60304c5d690/docs/deploying-to-aws.md#worker-down-alarm).
872
+ [Worker-down alarm](https://github.com/safeinsights/canopycms/blob/184d5c62b8158d1fdcfbd85430f17f90aceec00c/docs/deploying-to-aws.md#worker-down-alarm).
847
873
 
848
874
  **To adopt.** Optional. For parameter-only change sets, see
849
- [Rolling the worker from CI](https://github.com/safeinsights/canopycms/blob/bb13b429fb8deec72bbcf7f566ffa60304c5d690/docs/deploying-to-aws.md#rolling-the-worker-from-ci). For the alarm, pass
875
+ [Rolling the worker from CI](https://github.com/safeinsights/canopycms/blob/184d5c62b8158d1fdcfbd85430f17f90aceec00c/docs/deploying-to-aws.md#rolling-the-worker-from-ci). For the alarm, pass
850
876
  `alarmTopic`.
851
877
 
852
878
  **Now deletable.** A manual `cdk deploy` after each canopycms bump whose only purpose is moving the
@@ -857,7 +883,7 @@ worker, and a hand-built alarm on the worker log group.
857
883
  **What changed.** (int.112) The boot's `dnf upgrade` could be OOM-killed on every retry. A 1 GiB
858
884
  swap file now goes on first; an upgrade that still fails starts the worker unpatched and notifies
859
885
  `alarmTopic` if set; a retried step names itself when it gives up. See
860
- [The worker instance](https://github.com/safeinsights/canopycms/blob/bb13b429fb8deec72bbcf7f566ffa60304c5d690/docs/deploying-to-aws.md#the-worker-instance).
886
+ [The worker instance](https://github.com/safeinsights/canopycms/blob/184d5c62b8158d1fdcfbd85430f17f90aceec00c/docs/deploying-to-aws.md#the-worker-instance).
861
887
 
862
888
  **To adopt.** Deploy, which replaces the worker instance. The bundle needs none of this, so it is
863
889
  not Template first and the worker contract stays at 1.
@@ -923,7 +949,7 @@ and operations may reuse GitHub fetches under 30 seconds old.
923
949
 
924
950
  **To adopt.** Nothing. Seeding `remote.git` stages a copy of the repository beside the gateway's
925
951
  mirror, so the root volume needs room for two
926
- ([details](https://github.com/safeinsights/canopycms/blob/bb13b429fb8deec72bbcf7f566ffa60304c5d690/docs/deploying-to-aws.md#the-worker-instance)).
952
+ ([details](https://github.com/safeinsights/canopycms/blob/184d5c62b8158d1fdcfbd85430f17f90aceec00c/docs/deploying-to-aws.md#the-worker-instance)).
927
953
 
928
954
  #### The worker follows a rebased or deleted GitHub branch
929
955
 
@@ -945,7 +971,7 @@ changes the template too (the fallback bundle's key and hash), so only the contr
945
971
  bundle-only roll is safe.
946
972
 
947
973
  **To adopt.** Parameter mode: gate bundle-only rolls on the contract
948
- ([recipe](https://github.com/safeinsights/canopycms/blob/bb13b429fb8deec72bbcf7f566ffa60304c5d690/docs/deploying-to-aws.md#rolling-the-worker-from-ci)). The gate refuses a stack without the
974
+ ([recipe](https://github.com/safeinsights/canopycms/blob/184d5c62b8158d1fdcfbd85430f17f90aceec00c/docs/deploying-to-aws.md#rolling-the-worker-from-ci)). The gate refuses a stack without the
949
975
  `WorkerContract` output, so `cdk deploy` the template once, which adds it. A hand-installed unit
950
976
  copies its `Environment=CANOPYCMS_WORKER_CONTRACT=` line from `worker/canopy-worker.service`.
951
977
 
@@ -962,8 +988,8 @@ units from the bundle's contract: 2 gets the split, an older bundle the single u
962
988
  template, then roll the bundle; on an older template it stops with `template too old for this
963
989
  bundle`. Inspect EFS only through `sudo /opt/canopy-worker/canopy-inspect <command>`, never from a root shell. A hand install
964
990
  takes `worker/`'s three units and `gateway.env.example`
965
- ([steps](https://github.com/safeinsights/canopycms/blob/bb13b429fb8deec72bbcf7f566ffa60304c5d690/docs/deploying-to-aws.md#installing-the-units-by-hand)). After the first deploy, run the
966
- [checklist](https://github.com/safeinsights/canopycms/blob/bb13b429fb8deec72bbcf7f566ffa60304c5d690/docs/deploying-to-aws.md#first-deploy-checklist).
991
+ ([steps](https://github.com/safeinsights/canopycms/blob/184d5c62b8158d1fdcfbd85430f17f90aceec00c/docs/deploying-to-aws.md#installing-the-units-by-hand)). After the first deploy, run the
992
+ [checklist](https://github.com/safeinsights/canopycms/blob/184d5c62b8158d1fdcfbd85430f17f90aceec00c/docs/deploying-to-aws.md#first-deploy-checklist).
967
993
 
968
994
  #### `canopycms-cdk`: `CanopyCmsService.attachTo`, and editor response headers — **behaviour change if you frame the CMS**
969
995
 
@@ -974,7 +1000,7 @@ response headers policy. `CanopyCmsDistribution` sends the same headers: `frame-
974
1000
  `X-Frame-Options: SAMEORIGIN`, `nosniff`, HSTS and `X-Robots-Tag: noindex`. Unless your app sends
975
1001
  its own framing headers, no other origin can frame the editor or any page the CMS domain serves. Every such page is
976
1002
  marked `noindex`. The editor's default preview is same-origin and unaffected. See
977
- [Serving the editor from a distribution you already own](https://github.com/safeinsights/canopycms/blob/bb13b429fb8deec72bbcf7f566ffa60304c5d690/docs/deploying-to-aws.md#serving-the-editor-from-a-distribution-you-already-own).
1003
+ [Serving the editor from a distribution you already own](https://github.com/safeinsights/canopycms/blob/184d5c62b8158d1fdcfbd85430f17f90aceec00c/docs/deploying-to-aws.md#serving-the-editor-from-a-distribution-you-already-own).
978
1004
 
979
1005
  **To adopt.** If you wired the Function URL into your own distribution by hand, delete those
980
1006
  behaviors and call `attachTo`, passing your CMS build's `assetPrefix` as `editorAssetPrefix` and a
@@ -1150,7 +1176,7 @@ one the exclusions leave out. A throw or any other return is logged and treated
1150
1176
  **To adopt.** If your site's routes, sitemap or bylines decide that some entries have no page,
1151
1177
  apply the same rule here, for example `referenceLink: (target, { body }) =>
1152
1178
  !body?.trim() ? false : undefined` in `defineAIContentConfig` from `canopycms/ai`. See
1153
- [AI Content Configuration](https://github.com/safeinsights/canopycms/blob/bb13b429fb8deec72bbcf7f566ffa60304c5d690/README.md#ai-content-configuration).
1179
+ [AI Content Configuration](https://github.com/safeinsights/canopycms/blob/184d5c62b8158d1fdcfbd85430f17f90aceec00c/README.md#ai-content-configuration).
1154
1180
 
1155
1181
  **Now deletable.** A `fieldTransforms` entry that re-renders a reference field only to drop or
1156
1182
  change its link.
@@ -1225,7 +1251,7 @@ _First in int.115._
1225
1251
  entry the base branch also changed, the "updated on the base branch" notice and the branch's
1226
1252
  conflict count now stay until the branch merges, so a reviewer still sees which entries replaced a
1227
1253
  base change. They used to clear on the next sync, about five minutes later. README's new [Drafts
1228
- and Unpublishing](https://github.com/safeinsights/canopycms/blob/bb13b429fb8deec72bbcf7f566ffa60304c5d690/README.md#drafts-and-unpublishing) section covers long-lived branches and
1254
+ and Unpublishing](https://github.com/safeinsights/canopycms/blob/184d5c62b8158d1fdcfbd85430f17f90aceec00c/README.md#drafts-and-unpublishing) section covers long-lived branches and
1229
1255
  unpublishing.
1230
1256
 
1231
1257
  #### The editor calls the content drawer "All content"
@@ -1537,7 +1563,7 @@ editor carries an unsaved draft of a stamped entry onto its stamped version.
1537
1563
  The flag is refused off a top-level `datetime`, on `list: true`, and with `required: true`.
1538
1564
 
1539
1565
  **To adopt.** Add `stampOnPublish: true` to the date field your site sorts or labels by, such as
1540
- `{ name: 'publishedAt', type: 'datetime', stampOnPublish: true }`. See [Stamping a publication date](https://github.com/safeinsights/canopycms/blob/bb13b429fb8deec72bbcf7f566ffa60304c5d690/README.md#stamping-a-publication-date).
1566
+ `{ name: 'publishedAt', type: 'datetime', stampOnPublish: true }`. See [Stamping a publication date](https://github.com/safeinsights/canopycms/blob/184d5c62b8158d1fdcfbd85430f17f90aceec00c/README.md#stamping-a-publication-date).
1541
1567
 
1542
1568
  **Now deletable.** A build step or hook that writes a missing publication date into new entries.
1543
1569
 
@@ -1584,7 +1610,7 @@ preview/form divider is now keyboard-resizable within the same 15–85% the spli
1584
1610
  stored at, and `react-split-pane` is no longer a dependency.
1585
1611
  `EditorPanes`, `EditorPanesProps`, the `PaneLayout` type and the `SPLIT_PERCENT_*` and `DEFAULT_*_SPLIT_PERCENT` constants are
1586
1612
  no longer exported from `canopycms/client`. See
1587
- [Your root layout](https://github.com/safeinsights/canopycms/blob/bb13b429fb8deec72bbcf7f566ffa60304c5d690/README.md#editor-customization).
1613
+ [Your root layout](https://github.com/safeinsights/canopycms/blob/184d5c62b8158d1fdcfbd85430f17f90aceec00c/README.md#editor-customization).
1588
1614
 
1589
1615
  **To adopt.** Nothing, unless you import `EditorPanes` or those constants: render `CanopyEditor`
1590
1616
  instead, which lays out its own panes. Hiding your site chrome on the editor route is now
@@ -1695,7 +1721,7 @@ never ended. `canopycms-auth-clerk` skips it while the editor is mounted, and ex
1695
1721
  **To adopt.** Nothing for the editor, if your edit page uses `useClerkAuthConfig()` or
1696
1722
  `ClerkSignIn`. If your CMS build renders `<SignIn>`, `UserButton` or `OrganizationSwitcher` on your
1697
1723
  own pages, mount the hook as in
1698
- [Clerk components on your own pages](https://github.com/safeinsights/canopycms/blob/bb13b429fb8deec72bbcf7f566ffa60304c5d690/packages/canopycms-auth-clerk/README.md#clerk-components-on-your-own-pages).
1724
+ [Clerk components on your own pages](https://github.com/safeinsights/canopycms/blob/184d5c62b8158d1fdcfbd85430f17f90aceec00c/packages/canopycms-auth-clerk/README.md#clerk-components-on-your-own-pages).
1699
1725
  Your own Server Actions in the CMS build still 403 behind OAC.
1700
1726
 
1701
1727
  **Now deletable.** Any local patch of `window.__internal_onBeforeSetActive` (or 6.x's
@@ -1736,12 +1762,12 @@ fast naming the ARN, the field and the keys present, never a value.
1736
1762
  **To adopt.** Nothing, unless one of those secrets holds a JSON document: then set the matching
1737
1763
  prop. The scaffolded stack fills them from the repository _variables_
1738
1764
  `CANOPY_GITHUB_TOKEN_SECRET_JSON_FIELD` and `CLERK_SECRET_KEY_SECRET_JSON_FIELD`
1739
- ([why the prefix](https://github.com/safeinsights/canopycms/blob/bb13b429fb8deec72bbcf7f566ffa60304c5d690/docs/deploying-to-aws.md#repository-secrets-and-variables)); a stack scaffolded earlier
1765
+ ([why the prefix](https://github.com/safeinsights/canopycms/blob/184d5c62b8158d1fdcfbd85430f17f90aceec00c/docs/deploying-to-aws.md#repository-secrets-and-variables)); a stack scaffolded earlier
1740
1766
  adds the props to `infrastructure/bin/app.ts` and `infrastructure/lib/cms-stack.ts` and the two `env`
1741
1767
  lines to `.github/workflows/deploy-cms.yml`, or re-runs the generator and diffs. `cdk synth` refuses a `…JsonField` prop without its `…SecretArn`, and an ARN carrying the ECS `:KEY::` suffix
1742
1768
  (also in `secretsArns`). Don't use CDK's `secretValueFromJson`: it resolves the plaintext into the deployed
1743
1769
  resource's configuration, readable by anyone who can describe it. Only `CLERK_SECRET_KEY` goes through Secrets Manager; `CLERK_JWT_KEY` and the publishable
1744
- key are public material ([Security Model](https://github.com/safeinsights/canopycms/blob/bb13b429fb8deec72bbcf7f566ffa60304c5d690/docs/deploying-to-aws.md#security-model)).
1770
+ key are public material ([Security Model](https://github.com/safeinsights/canopycms/blob/184d5c62b8158d1fdcfbd85430f17f90aceec00c/docs/deploying-to-aws.md#security-model)).
1745
1771
 
1746
1772
  **Now deletable.** A wrapper that fetches the secret, parses it and re-exports one field into the
1747
1773
  worker's environment, such as a `jq` step in user data or a wrapper entrypoint around
@@ -1753,7 +1779,7 @@ worker's environment, such as a `jq` step in user data or a wrapper entrypoint a
1753
1779
  `githubToken` is not deprecated and stays the documented default; `canopycms` neither depends on
1754
1780
  nor imports `@octokit/auth-app`. Register one App per site, not one shared across repositories:
1755
1781
  anyone holding an App's key can mint a token for any of its installations
1756
- ([why](https://github.com/safeinsights/canopycms/blob/bb13b429fb8deec72bbcf7f566ffa60304c5d690/ARCHITECTURE.md#why-one-github-app-per-site-not-one-shared-across-an-organisation)).
1782
+ ([why](https://github.com/safeinsights/canopycms/blob/184d5c62b8158d1fdcfbd85430f17f90aceec00c/ARCHITECTURE.md#why-one-github-app-per-site-not-one-shared-across-an-organisation)).
1757
1783
 
1758
1784
  **To adopt with `canopycms-cdk`** (existing stacks need no edit). `CanopyCmsServiceProps` takes
1759
1785
  `githubAppId`, `githubAppInstallationId`, `githubAppPrivateKeySecretArn` and
@@ -1766,7 +1792,7 @@ anyone holding an App's key can mint a token for any of its installations
1766
1792
  `verify` checks an existing installation's permissions: run it, since an App missing a
1767
1793
  permission only shows up later, as `sync-failed`. `create` needs an interactive terminal and
1768
1794
  never edits an existing JSON secret (create one and point the JSON-field prop at it)
1769
- ([details](https://github.com/safeinsights/canopycms/blob/bb13b429fb8deec72bbcf7f566ffa60304c5d690/docs/deploying-to-aws.md#register-it-with-canopycms-init-github-app)).
1795
+ ([details](https://github.com/safeinsights/canopycms/blob/184d5c62b8158d1fdcfbd85430f17f90aceec00c/docs/deploying-to-aws.md#register-it-with-canopycms-init-github-app)).
1770
1796
  2. Set the three props and **remove `githubTokenSecretArn`** (with its JSON field). A partial set
1771
1797
  of the three is refused at synth, and so is an App alongside a token.
1772
1798
  3. In the generated workflow, store them as `CANOPY_GITHUB_APP_ID`,
@@ -1777,7 +1803,7 @@ anyone holding an App's key can mint a token for any of its installations
1777
1803
 
1778
1804
  The private key is ARN-only; passing the key itself is refused at synth. Its ARN joins the worker's
1779
1805
  IAM policy automatically, so you need not add it to `secretsArns`. See
1780
- [Authenticating as a GitHub App](https://github.com/safeinsights/canopycms/blob/bb13b429fb8deec72bbcf7f566ffa60304c5d690/docs/deploying-to-aws.md#authenticating-as-a-github-app).
1806
+ [Authenticating as a GitHub App](https://github.com/safeinsights/canopycms/blob/184d5c62b8158d1fdcfbd85430f17f90aceec00c/docs/deploying-to-aws.md#authenticating-as-a-github-app).
1781
1807
 
1782
1808
  ```bash
1783
1809
  canopycms init-github-app create -- \
@@ -1825,7 +1851,7 @@ downloads the `.pem` from the App's settings page.
1825
1851
 
1826
1852
  **What changed.** The worker re-reads a Secrets Manager secret (the GitHub token or the Clerk secret
1827
1853
  key) when the operation using it fails. Timings, costs and the store-before-revoke order are in
1828
- [Rotating a secret](https://github.com/safeinsights/canopycms/blob/bb13b429fb8deec72bbcf7f566ffa60304c5d690/docs/deploying-to-aws.md#rotating-a-secret).
1854
+ [Rotating a secret](https://github.com/safeinsights/canopycms/blob/184d5c62b8158d1fdcfbd85430f17f90aceec00c/docs/deploying-to-aws.md#rotating-a-secret).
1829
1855
 
1830
1856
  **To adopt.** Nothing for credentials from `*_SECRET_ARN`, which is every scaffolded deployment. A
1831
1857
  GitHub App private key is still read only at boot: store the new key, replace the instance, and
@@ -1879,12 +1905,12 @@ also check the bucket policy for its grant.
1879
1905
  5. `withCanopy()` adds sharp's libvips to Next's file tracing (not for a static export) and, on
1880
1906
  Next 16 and later, sets `turbopack: {}` when your config has neither `turbopack` nor your own
1881
1907
  `webpack` and it can read your Next version. A webpack build still fails its image transforms
1882
- ([Dual Build Support](https://github.com/safeinsights/canopycms/blob/bb13b429fb8deec72bbcf7f566ffa60304c5d690/docs/deploying-to-aws.md#dual-build-support)).
1908
+ ([Dual Build Support](https://github.com/safeinsights/canopycms/blob/184d5c62b8158d1fdcfbd85430f17f90aceec00c/docs/deploying-to-aws.md#dual-build-support)).
1883
1909
 
1884
1910
  **Breaking:** a stack setting `platform` on `fromImageAsset` without `architecture` gets an arm64
1885
1911
  function while `platform` still decides the image, so `Platform.LINUX_AMD64` builds an image the
1886
1912
  function cannot run. A stack setting neither moves to arm64 with a matching image, built under QEMU
1887
- on an x86 host ([Where the image is built](https://github.com/safeinsights/canopycms/blob/bb13b429fb8deec72bbcf7f566ffa60304c5d690/docs/deploying-to-aws.md#where-the-image-is-built)).
1913
+ on an x86 host ([Where the image is built](https://github.com/safeinsights/canopycms/blob/184d5c62b8158d1fdcfbd85430f17f90aceec00c/docs/deploying-to-aws.md#where-the-image-is-built)).
1888
1914
 
1889
1915
  **To adopt.**
1890
1916
 
@@ -1913,7 +1939,7 @@ reason; `platform` on `fromImageAsset`; and, with `withCanopy()`, a hand-written
1913
1939
 
1914
1940
  **What changed.** `media.uploadUrl` (s3 adapter; an absolute http(s) URL or a site-relative path)
1915
1941
  replaces the `url` that `beginUpload()` returns, leaving the presign's `fields` untouched, so
1916
- uploads can be same-origin. See [Media Configuration](https://github.com/safeinsights/canopycms/blob/bb13b429fb8deec72bbcf7f566ffa60304c5d690/README.md#media-configuration) for the
1942
+ uploads can be same-origin. See [Media Configuration](https://github.com/safeinsights/canopycms/blob/184d5c62b8158d1fdcfbd85430f17f90aceec00c/README.md#media-configuration) for the
1917
1943
  CloudFront behavior it expects.
1918
1944
 
1919
1945
  **To adopt.** Optional. Set it from an environment variable: a site-relative value works only where
@@ -1938,7 +1964,7 @@ needs `lazyPublicTransforms: true`) let you pass a deterministically named role,
1938
1964
  bucket's stack computes `arn:aws:iam::<account>:role/<name>` from literals instead of a
1939
1965
  `Fn::GetStackOutput` reference only the CDK CLI resolves. The constructs re-attach
1940
1966
  `AWSLambdaBasicExecutionRole` (and the VPC policy) that `lambda.Function` drops from a role you
1941
- pass. See [Cross-account asset bucket](https://github.com/safeinsights/canopycms/blob/bb13b429fb8deec72bbcf7f566ffa60304c5d690/docs/deploying-to-aws.md#cross-account-asset-bucket).
1967
+ pass. See [Cross-account asset bucket](https://github.com/safeinsights/canopycms/blob/184d5c62b8158d1fdcfbd85430f17f90aceec00c/docs/deploying-to-aws.md#cross-account-asset-bucket).
1942
1968
 
1943
1969
  **To adopt.** Optional:
1944
1970
 
@@ -2048,7 +2074,7 @@ public JWKS PEM, used only to verify signatures.
2048
2074
  to variables (Settings → Secrets and variables → Actions → Variables); `bin/app.ts` refuses the
2049
2075
  deploy at synth while it is unset. Check that `bin/app.ts` passes `CLERK_SECRET_KEY` as
2050
2076
  `clerkSecretKeySecretArn` (read by the worker), never in the Lambda's `environment`, unless you keep
2051
- `clerkMiddleware` ([Security Model](https://github.com/safeinsights/canopycms/blob/bb13b429fb8deec72bbcf7f566ffa60304c5d690/docs/deploying-to-aws.md#security-model)).
2077
+ `clerkMiddleware` ([Security Model](https://github.com/safeinsights/canopycms/blob/184d5c62b8158d1fdcfbd85430f17f90aceec00c/docs/deploying-to-aws.md#security-model)).
2052
2078
 
2053
2079
  #### `readByUrlPath` answers only where `listEntries` publishes — **breaking (routing)**
2054
2080
 
@@ -2147,7 +2173,7 @@ basePath: process.env.NEXT_PUBLIC_BASE_PATH,
2147
2173
  Then pass `baseUrl` to `assetUrl` only if Next serves `/assets` (local adapter, `next dev`, S3 with
2148
2174
  no distribution): `assetUrl(image, { width: 960, baseUrl: BASE_PATH })`. Assets on CloudFront via
2149
2175
  `AssetSupport` stay at the distribution root; pass no `baseUrl` (mount table under "Where `/assets`
2150
- is mounted" in [Media Configuration](https://github.com/safeinsights/canopycms/blob/bb13b429fb8deec72bbcf7f566ffa60304c5d690/README.md#media-configuration)). Two traps:
2176
+ is mounted" in [Media Configuration](https://github.com/safeinsights/canopycms/blob/184d5c62b8158d1fdcfbd85430f17f90aceec00c/README.md#media-configuration)). Two traps:
2151
2177
 
2152
2178
  - Don't pass a deployment `basePath` to `contentStaticParams({ basePath })`; that option filters by
2153
2179
  a nested route prefix and emits zero params, building green.
@@ -2306,7 +2332,7 @@ Under `exactOptionalPropertyTypes: true`, omit keys instead of assigning `undefi
2306
2332
  `withTrailingSlash` and `isAbsoluteUrl` from `canopycms/server`; `defineSeoFieldGroup()` from
2307
2333
  `canopycms`; `generateContentSitemap` and `entryToMetadata` from `canopycms-next`, bound on
2308
2334
  `createNextCanopyContext`'s result. See
2309
- [Sitemap and SEO Metadata](https://github.com/safeinsights/canopycms/blob/bb13b429fb8deec72bbcf7f566ffa60304c5d690/README.md#sitemap-and-seo-metadata). Every routable entry type is in
2335
+ [Sitemap and SEO Metadata](https://github.com/safeinsights/canopycms/blob/184d5c62b8158d1fdcfbd85430f17f90aceec00c/README.md#sitemap-and-seo-metadata). Every routable entry type is in
2310
2336
  the sitemap unless `exclude`d or `noindex`; one `noindex` predicate drives both robots metadata and
2311
2337
  sitemap exclusion. `trailingSlash` is never inferred. `lastModified` defaults to `updatedAt`, a
2312
2338
  filesystem mtime. An empty SEO field counts as unset. `siteUrl` must be absolute, colliding URLs are
@@ -2363,7 +2389,7 @@ underscore-prefixed files are skipped.
2363
2389
 
2364
2390
  **What changed.** New scaffolds write `defaultBranchAccess: 'deny'` and
2365
2391
  `defaultPathAccess: { read: 'allow' }`
2366
- ([Public read on server deployments](https://github.com/safeinsights/canopycms/blob/bb13b429fb8deec72bbcf7f566ffa60304c5d690/README.md#public-read-on-server-deployments)).
2392
+ ([Public read on server deployments](https://github.com/safeinsights/canopycms/blob/184d5c62b8158d1fdcfbd85430f17f90aceec00c/README.md#public-read-on-server-deployments)).
2367
2393
 
2368
2394
  **To adopt.** An older scaffold's `defaultBranchAccess: 'allow'` is wider than recommended;
2369
2395
  consider `'deny'`. If you deleted `defaultPathAccess: { read: 'allow' }`, anonymous visitors get no content until you
@@ -2416,7 +2442,7 @@ re-checked an entry's type (a `switch` on `meta.entryType` can merge them into o
2416
2442
  **What changed.** `BlockComponentRegistry<Blocks>` (types only, from `canopycms`; `BlockValueOf<Blocks, 'hero'>`
2417
2443
  gives one template's value type) requires exactly
2418
2444
  one component per block template name when written as an object literal. See
2419
- [Block Component Registries](https://github.com/safeinsights/canopycms/blob/bb13b429fb8deec72bbcf7f566ffa60304c5d690/README.md#block-component-registries) and
2445
+ [Block Component Registries](https://github.com/safeinsights/canopycms/blob/184d5c62b8158d1fdcfbd85430f17f90aceec00c/README.md#block-component-registries) and
2420
2446
  `apps/example1/app/components/PostView.tsx`.
2421
2447
 
2422
2448
  ```ts
@@ -2438,7 +2464,7 @@ templates against the schema.
2438
2464
 
2439
2465
  **What changed.** Spreading a `const` field array into several schemas, and nesting
2440
2466
  `defineInlineFieldGroup()` inside a block template, are documented
2441
- ([Reusable Field Fragments](https://github.com/safeinsights/canopycms/blob/bb13b429fb8deec72bbcf7f566ffa60304c5d690/README.md#reusable-field-fragments)); `defineFieldFragment()` is an
2467
+ ([Reusable Field Fragments](https://github.com/safeinsights/canopycms/blob/184d5c62b8158d1fdcfbd85430f17f90aceec00c/README.md#reusable-field-fragments)); `defineFieldFragment()` is an
2442
2468
  identity helper for them.
2443
2469
 
2444
2470
  ```ts
@@ -2457,7 +2483,7 @@ field by composing from the same `const` field object.
2457
2483
  A block template holding a `reference` field is a shared block; `read()`/`readByUrlPath()` resolve
2458
2484
  it, and listings do with
2459
2485
  [`resolveReferences`](#listings-resolve-references-and-a-resolved-reference-carries-urlpath-and-optionally-its-body-16--breaking-type-level)
2460
- (0.0.64). See [Shared / Referenced Blocks](https://github.com/safeinsights/canopycms/blob/bb13b429fb8deec72bbcf7f566ffa60304c5d690/README.md#shared--referenced-blocks).
2486
+ (0.0.64). See [Shared / Referenced Blocks](https://github.com/safeinsights/canopycms/blob/184d5c62b8158d1fdcfbd85430f17f90aceec00c/README.md#shared--referenced-blocks).
2461
2487
 
2462
2488
  **Now deletable.** A second `read()` in page code to unwrap a shared block's reference.
2463
2489
 
@@ -2479,7 +2505,7 @@ Not retro-documented, apart from these:
2479
2505
 
2480
2506
  #### The registry is keyed by entry-type name (0.0.42)
2481
2507
 
2482
- **What changed.** `createEntrySchemaRegistry` keys are the entry-type names that `.collection.json` files name in `entry.schema`, and `EntryTypesFromRegistry` derives the typed entry-type map from them. Schema-variable keys (`{ postSchema }`) still work but derive nothing; [README's registry convention](https://github.com/safeinsights/canopycms/blob/bb13b429fb8deec72bbcf7f566ffa60304c5d690/README.md#convention-why-key-the-registry-by-entry-type-name) says when to keep them.
2508
+ **What changed.** `createEntrySchemaRegistry` keys are the entry-type names that `.collection.json` files name in `entry.schema`, and `EntryTypesFromRegistry` derives the typed entry-type map from them. Schema-variable keys (`{ postSchema }`) still work but derive nothing; [README's registry convention](https://github.com/safeinsights/canopycms/blob/184d5c62b8158d1fdcfbd85430f17f90aceec00c/README.md#convention-why-key-the-registry-by-entry-type-name) says when to keep them.
2483
2509
 
2484
2510
  **To adopt.**
2485
2511
 
@@ -9,6 +9,7 @@ import path from 'node:path';
9
9
  import fs from 'node:fs/promises';
10
10
  import { minimatch } from 'minimatch';
11
11
  import { isDataOnlyFormat } from '../utils/format.js';
12
+ import { findExplicitBodyFieldName, linkOnlyReferenceData } from '../utils/body-field.js';
12
13
  import { extractEntryTypeFromFilename, extractIdFromFilename, } from '../content-id-index.js';
13
14
  import { hasTraversalSequence } from '../paths/index.js';
14
15
  import { getErrorMessage, isNodeError } from '../utils/error.js';
@@ -76,19 +77,24 @@ export async function generateAIContent(options) {
76
77
  pendingById.set(pending.contentId, pending);
77
78
  }
78
79
  // The target as `resolveReferenceTarget` would give it, built from this export's own read, so
79
- // an entry outside the export set is never offered and a target cannot change mid-run
80
+ // an entry outside the export set is never offered and a target cannot change mid-run. A
81
+ // json/yaml target's `isBody` field is its body here, as an md/mdx target's body is
80
82
  const subjectOf = (id) => {
81
83
  const pending = pendingById.get(id);
82
84
  if (!pending || !exported.has(id))
83
85
  return undefined;
84
- const { slug } = pending.entry;
85
- const target = buildResolvedReference(structuredClone(pending.storedData), {
86
+ const { format } = pending;
87
+ const { slug, fields } = pending.entry;
88
+ const storedData = structuredClone(pending.storedData);
89
+ const target = buildResolvedReference(linkOnlyReferenceData(storedData, format, fields), {
86
90
  id,
87
91
  slug,
88
92
  collection: pending.collectionPath,
89
93
  urlPath: computeEntryUrl(pending.collectionPath, slug, contentRoot),
90
94
  });
91
- return { target, body: pending.body };
95
+ const dataBodyField = isDataOnlyFormat(format) ? findExplicitBodyFieldName(fields) : undefined;
96
+ const dataBody = dataBodyField === undefined ? undefined : storedData[dataBodyField];
97
+ return { target, body: pending.body ?? (typeof dataBody === 'string' ? dataBody : undefined) };
92
98
  };
93
99
  const linkFor = config?.referenceLink
94
100
  ? await decideReferenceLinks(config.referenceLink, allPending, subjectOf, entryLinkUrl)
@@ -254,6 +260,7 @@ async function collectEntries(read, collection, cleanPath) {
254
260
  entry: aiEntry,
255
261
  body,
256
262
  storedData,
263
+ format: doc.format,
257
264
  linkedIds: [],
258
265
  contentId: extractIdFromFilename(path.basename(listEntry.relativePath)),
259
266
  absolutePath: doc.absolutePath,
@@ -208,8 +208,9 @@ export type EntryTransforms = Record<string, EntryTransformFn>;
208
208
  /** What {@link AIReferenceLinkFn} knows about a target beyond its data. */
209
209
  export interface AIReferenceLinkContext {
210
210
  /**
211
- * The target's MD/MDX body as stored, `undefined` for a data-only entry. It only informs this
212
- * decision: none of it reaches the entries that link to the target.
211
+ * The target's body as stored: an md/mdx entry's body, or a json/yaml entry's `isBody` field,
212
+ * which the target leaves out as a link-only reference does. `undefined` when it has none. It
213
+ * only informs this decision: none of it reaches the entries that link to the target.
213
214
  */
214
215
  body: string | undefined;
215
216
  /**
package/dist/cli/cli.js CHANGED
@@ -4552,16 +4552,26 @@ function flattenGroupFields(fields) {
4552
4552
  return result;
4553
4553
  }
4554
4554
  function findBodyFieldName(fields) {
4555
+ return findExplicitBodyFieldName(fields) ?? "body";
4556
+ }
4557
+ function findExplicitBodyFieldName(fields) {
4555
4558
  for (const field of flattenGroupFields(fields)) {
4556
4559
  if ("isBody" in field && field.isBody)
4557
4560
  return field.name;
4558
4561
  }
4559
- return "body";
4562
+ return void 0;
4563
+ }
4564
+ function linkOnlyReferenceData(data, format, fields) {
4565
+ const bodyFieldName = isDataOnlyFormat(format) && fields ? findExplicitBodyFieldName(fields) : void 0;
4566
+ if (bodyFieldName === void 0 || !(bodyFieldName in data))
4567
+ return data;
4568
+ const { [bodyFieldName]: _body, ...rest } = data;
4569
+ return rest;
4560
4570
  }
4561
4571
  function extractSeoFields(entryData, opts = {}) {
4562
4572
  const names = { ...DEFAULT_SEO_FIELD_NAMES, ...opts.fields };
4563
- const entry = asRecord(entryData);
4564
- const source = opts.group ? asRecord(entry?.[opts.group]) : entry;
4573
+ const entry = asRecord2(entryData);
4574
+ const source = opts.group ? asRecord2(entry?.[opts.group]) : entry;
4565
4575
  return {
4566
4576
  title: asText(source?.[names.title]) ?? asText(opts.fallbackTitle),
4567
4577
  description: asText(source?.[names.description]) ?? asText(opts.fallbackDescription),
@@ -6285,14 +6295,18 @@ async function generateAIContent(options) {
6285
6295
  const pending = pendingById.get(id);
6286
6296
  if (!pending || !exported.has(id))
6287
6297
  return void 0;
6288
- const { slug } = pending.entry;
6289
- const target = buildResolvedReference(structuredClone(pending.storedData), {
6298
+ const { format } = pending;
6299
+ const { slug, fields } = pending.entry;
6300
+ const storedData = structuredClone(pending.storedData);
6301
+ const target = buildResolvedReference(linkOnlyReferenceData(storedData, format, fields), {
6290
6302
  id,
6291
6303
  slug,
6292
6304
  collection: pending.collectionPath,
6293
6305
  urlPath: computeEntryUrl(pending.collectionPath, slug, contentRoot)
6294
6306
  });
6295
- return { target, body: pending.body };
6307
+ const dataBodyField = isDataOnlyFormat(format) ? findExplicitBodyFieldName(fields) : void 0;
6308
+ const dataBody = dataBodyField === void 0 ? void 0 : storedData[dataBodyField];
6309
+ return { target, body: pending.body ?? (typeof dataBody === "string" ? dataBody : void 0) };
6296
6310
  };
6297
6311
  const linkFor = config?.referenceLink ? await decideReferenceLinks(config.referenceLink, allPending, subjectOf, entryLinkUrl) : void 0;
6298
6312
  const bodyLinkUrl = linkFor ? (location) => {
@@ -6434,6 +6448,7 @@ async function collectEntries(read, collection, cleanPath) {
6434
6448
  entry: aiEntry,
6435
6449
  body,
6436
6450
  storedData,
6451
+ format: doc.format,
6437
6452
  linkedIds: [],
6438
6453
  contentId: extractIdFromFilename(path13.basename(listEntry.relativePath)),
6439
6454
  absolutePath: doc.absolutePath,
@@ -7770,7 +7785,7 @@ async function generateAIContentCLI(options) {
7770
7785
  console.log(` Output: ${result.outputDir}
7771
7786
  `);
7772
7787
  }
7773
- var __defProp3, __getOwnPropNames3, __esm3, __export3, init_error3, init_atomic_write2, init_field_traversal, ITEM_KEY, ITEM_KEY_PATTERN, isItemKey, BASE58, isListField, init_item_keys, active2, init_logger2, SENTINEL_BASE2, DOT_SEGMENT2, init_sanitize_href2, init_normalize, ROOT_COLLECTION_ID, init_types2, normalizePathValue, flattenSchema, init_flatten, scalarFieldTypes2, markdownFieldTypes2, primitiveFieldTypes2, fieldTypes2, init_types22, SAFE_HTML_TAGS2, COMPONENT_NAME2, MDX_BINDINGS2, FORBIDDEN_ATTRIBUTES2, init_mdx_allowlist2, fieldBaseSchema2, selectOptionSchema2, referenceOptionSchema2, primitiveFieldSchema2, mdxPropAllowSchema2, mdxAllowlistSchema2, markdownFieldSchema2, ASPECT_RATIO_RE2, imageFieldSchema2, selectFieldSchema2, referenceFieldSchema2, fieldHolder2, blockSchema2, blockFieldSchema2, objectFieldSchema2, inlineGroupFieldSchema2, customFieldSchema2, knownFieldSchema2, fieldSchema2, init_field2, relativePathSchema2, entryTypeSchema2, collectionSchema2, init_collection2, uploadTargetUrlSchema2, previewPrefixSchema2, init_url2, mediaSchema2, init_media2, defaultBranchAccessSchema2, defaultPathAccessLevelSchema2, defaultPathAccessLevelsSchema2, defaultPathAccessSchema2, defaultBaseBranchSchema2, defaultRemoteNameSchema2, defaultRemoteUrlSchema2, gitBotAuthorNameSchema2, gitBotAuthorEmailSchema2, githubTokenEnvVarSchema2, operatingModeSchema2, deployedAsSchema2, contentRootSchema2, sourceRootSchema2, basePathSchema2, deploymentNameSchema2, devContentSyncSchema2, devConfigSchema2, editorConfigSchema2, CanopyConfigSchema2, DEFAULT_PROD_WORKSPACE2, init_config3, SERVER_MODE_ENV_VAR, BROWSER_MODE_ENV_VAR, warned2, init_mode_env, forEachReferenceField, init_validation, ProdClientSafeStrategy2, DevClientSafeStrategy2, clientStrategyCache, init_client_safe_strategy, warned3, VALID_DEPLOYMENT_NAME2, isValidDeploymentName2, init_deployment_name, init_config22, ProdStrategy2, DevStrategy2, strategyCache2, init_client_unsafe_strategy, operating_mode_exports, init_operating_mode, task_queue_config_exports2, init_task_queue_config2, worker_status_exports, WORKER_STATUS_FILE, init_worker_status, BLOCK_DISCRIMINATOR_KEYS, BLOCK_STRUCTURAL_KEYS, envFlag2, LOG_LEVELS2, DebugLogger2, testLogger2, END, FRESH, LINE_WIDTH, MIN_LINE_WIDTH, SourceSplicer, log2, MAX_LCS_CELLS, Splicer, log22, EVIDENCE_MAX_DEPTH, log3, locks, PROVISIONING_LOCK_STALE_MS, sleep, META_DIR, CONTENT_WRITE_LOCK_NAME, DEFAULT_CONTENT_WRITE_LOCK_WAIT_MS, ContentWriteLockBusyError, sleep2, CONTENT_WRITE_LOCK_STALE_MS, heldRoots, DEFAULT_SEO_FIELD_NAMES, asRecord, asText, asFlag, asImageUrl, asOgType, asTwitterCard, RESTRICTED_REFERENCE_MARKER, SEO_GROUP_FIELDS, MAX_WARNED, warned4, SchemaUnavailableError, BASE58_PATTERN, CONTENT_ID_PATTERN, PHYSICAL_SEGMENT_PATTERN, isValidId, EMPTY_LOGICAL_PATH, ContentIdIndex, parseTypedFilename, CONTENT_EXTENSIONS, registry, finalization, log4, META_DIR2, entryTypeMetaSchema, collectionMetaSchema, rootCollectionMetaSchema, fingerprints, editDistance, closestName, forEachEntryType, collectEntryTypeNames, validateReferenceEntryTypes, isDeployedStatic, isBuildMode, readsFromCheckout, STATIC_DEPLOY_USER, storage, log5, BaseBranchUnresolvedError, NETWORK_SCHEME_PATTERN, TRANSPORT_HELPER_PATTERN, LEADING_OPTION_PATTERN, WINDOWS_DRIVE_PATTERN, COLON_BEFORE_SLASH_PATTERN, MISSING_REMOTE_REF_REASONS, CANOPY_META_DIR, SCHEMA_CACHE_VERSION, MTIME_CHECK_DEBOUNCE_MS, SCHEMA_CACHE_FILE, SCHEMA_GENERATION_RESOURCE, reportedIssues, BranchSchemaCache, RESOURCE, getFormatExtension, isDataOnlyFormat, asRecord2, RESERVED_SETTINGS_BRANCH_PREFIX, BranchPathError, resolveContentBranchesRoot, ContentStoreError, ContentConflictError, EntryGoneError, BranchSyncingError, DuplicateContentIdError, UrlPathConflictError, isEmptyFieldValue, DEFAULT_INDEX_FRESHNESS_INTERVAL_MS, FORCED_REFRESH_MIN_INTERVAL_MS, STALE_LOOKUP, isMissingReference, createReferenceResolveCache, ContentStore, ATTR_CONTENT, BLOCK_PREFIX, BLOCK_SUFFIX, BLOCK_RESTORE_RE, INLINE_PREFIX, INLINE_SUFFIX, INLINE_RESTORE_RE, DEFAULT_REFERENCE_RENDERING, FLAT_CELL_TYPES, log6, BASE58_CHAR, ENTRY_LINK_PATTERN, INLINE_LINK_TARGET, BranchMetadataCorruptError, BRANCH_META_DIR, BRANCH_META_FILE, branchMetadataFilePath, UNKNOWN_TIME, enumOf, branchMetadataShape, branchMetadataSchema, branchMetadataFileSchema, log7, REGISTRY_FILE, REGISTRY_STALE_FILE, REGISTRY_TEMP_FILE, REGISTRY_VERSION, RESOURCE2, GET_MISS_REFRESH_MIN_INTERVAL_MS, snapshotSchema, reportedUnreadable, BranchRegistry, log8, OccWriteConflictError, CURRENT_SCHEMA_VERSION, BranchMetadataConflictError, BranchMetadataFileManager, loadBranchContext, log9, GIT_ENV_PASSTHROUGH, FORCE_C_LOCALE, GIT_NETWORK_ENV_PASSTHROUGH, NO_AUTO_GC_CONFIG, REMOTE_GIT_CONFIG, REMOTE_GIT_MAX_LOOSE_OBJECTS, REMOTE_GIT_MAX_PACKS, PARALLEL_CHECKOUT_ARGS, remoteInitLocks, GITHUB_TRACKING_REF_PREFIX, GitConflictError, GitRemoteRefMissingError, RemoteNotReadyError, SettingsRemoteUnreadableError, SettingsBranchDivergedError, SETTINGS_BRANCH_FILES, SettingsBranchHasContentHistoryError, GitManager, PUBLISH_LOCK_WAIT_MS, CREATE_RESIDUE_MIN_QUIET_MS, STAGING_MAX_AGE_MS, REPAIR_MAX_AGE_MS, DELETING_MAX_AGE_MS, SETTINGS_TRASH_MAX_AGE_MS, BranchProvisioningBusyError, BranchDirOccupiedError, STAGING_PREFIX, REPAIR_PREFIX, TRASH_PREFIX, stagingDirName, NO_GIT, RENAME_BLOCKED, SPARSE_CONE_RECORD, defaultSink, sink, ProvisionLog, testHooks, provisionsInFlight, BranchWorkspaceManager, log10, normalizePath, readEntryData, createReferenceResolver, withoutUnavailableEntries, resolveCollectionItemReferences, looksLikeMalformedEntry, listCollectionEntries, MAX_INPUT_PIXELS, CROP_PRECISION, KNOWN_FIELD_TYPES, STRING_FIELD_TYPES, selectOptionValues, GENERATED_RECORD_FILENAME, SAFE_BUILD_ID, BUILD_ID_RULE, jiti;
7788
+ var __defProp3, __getOwnPropNames3, __esm3, __export3, init_error3, init_atomic_write2, init_field_traversal, ITEM_KEY, ITEM_KEY_PATTERN, isItemKey, BASE58, isListField, init_item_keys, active2, init_logger2, SENTINEL_BASE2, DOT_SEGMENT2, init_sanitize_href2, init_normalize, ROOT_COLLECTION_ID, init_types2, normalizePathValue, flattenSchema, init_flatten, scalarFieldTypes2, markdownFieldTypes2, primitiveFieldTypes2, fieldTypes2, init_types22, SAFE_HTML_TAGS2, COMPONENT_NAME2, MDX_BINDINGS2, FORBIDDEN_ATTRIBUTES2, init_mdx_allowlist2, fieldBaseSchema2, selectOptionSchema2, referenceOptionSchema2, primitiveFieldSchema2, mdxPropAllowSchema2, mdxAllowlistSchema2, markdownFieldSchema2, ASPECT_RATIO_RE2, imageFieldSchema2, selectFieldSchema2, referenceFieldSchema2, fieldHolder2, blockSchema2, blockFieldSchema2, objectFieldSchema2, inlineGroupFieldSchema2, customFieldSchema2, knownFieldSchema2, fieldSchema2, init_field2, relativePathSchema2, entryTypeSchema2, collectionSchema2, init_collection2, uploadTargetUrlSchema2, previewPrefixSchema2, init_url2, mediaSchema2, init_media2, defaultBranchAccessSchema2, defaultPathAccessLevelSchema2, defaultPathAccessLevelsSchema2, defaultPathAccessSchema2, defaultBaseBranchSchema2, defaultRemoteNameSchema2, defaultRemoteUrlSchema2, gitBotAuthorNameSchema2, gitBotAuthorEmailSchema2, githubTokenEnvVarSchema2, operatingModeSchema2, deployedAsSchema2, contentRootSchema2, sourceRootSchema2, basePathSchema2, deploymentNameSchema2, devContentSyncSchema2, devConfigSchema2, editorConfigSchema2, CanopyConfigSchema2, DEFAULT_PROD_WORKSPACE2, init_config3, SERVER_MODE_ENV_VAR, BROWSER_MODE_ENV_VAR, warned2, init_mode_env, forEachReferenceField, init_validation, ProdClientSafeStrategy2, DevClientSafeStrategy2, clientStrategyCache, init_client_safe_strategy, warned3, VALID_DEPLOYMENT_NAME2, isValidDeploymentName2, init_deployment_name, init_config22, ProdStrategy2, DevStrategy2, strategyCache2, init_client_unsafe_strategy, operating_mode_exports, init_operating_mode, task_queue_config_exports2, init_task_queue_config2, worker_status_exports, WORKER_STATUS_FILE, init_worker_status, BLOCK_DISCRIMINATOR_KEYS, BLOCK_STRUCTURAL_KEYS, envFlag2, LOG_LEVELS2, DebugLogger2, testLogger2, END, FRESH, LINE_WIDTH, MIN_LINE_WIDTH, SourceSplicer, log2, MAX_LCS_CELLS, Splicer, log22, EVIDENCE_MAX_DEPTH, log3, locks, PROVISIONING_LOCK_STALE_MS, sleep, META_DIR, CONTENT_WRITE_LOCK_NAME, DEFAULT_CONTENT_WRITE_LOCK_WAIT_MS, ContentWriteLockBusyError, sleep2, CONTENT_WRITE_LOCK_STALE_MS, heldRoots, getFormatExtension, isDataOnlyFormat, asRecord, DEFAULT_SEO_FIELD_NAMES, asRecord2, asText, asFlag, asImageUrl, asOgType, asTwitterCard, RESTRICTED_REFERENCE_MARKER, SEO_GROUP_FIELDS, MAX_WARNED, warned4, SchemaUnavailableError, BASE58_PATTERN, CONTENT_ID_PATTERN, PHYSICAL_SEGMENT_PATTERN, isValidId, EMPTY_LOGICAL_PATH, ContentIdIndex, parseTypedFilename, CONTENT_EXTENSIONS, registry, finalization, log4, META_DIR2, entryTypeMetaSchema, collectionMetaSchema, rootCollectionMetaSchema, fingerprints, editDistance, closestName, forEachEntryType, collectEntryTypeNames, validateReferenceEntryTypes, isDeployedStatic, isBuildMode, readsFromCheckout, STATIC_DEPLOY_USER, storage, log5, BaseBranchUnresolvedError, NETWORK_SCHEME_PATTERN, TRANSPORT_HELPER_PATTERN, LEADING_OPTION_PATTERN, WINDOWS_DRIVE_PATTERN, COLON_BEFORE_SLASH_PATTERN, MISSING_REMOTE_REF_REASONS, CANOPY_META_DIR, SCHEMA_CACHE_VERSION, MTIME_CHECK_DEBOUNCE_MS, SCHEMA_CACHE_FILE, SCHEMA_GENERATION_RESOURCE, reportedIssues, BranchSchemaCache, RESOURCE, RESERVED_SETTINGS_BRANCH_PREFIX, BranchPathError, resolveContentBranchesRoot, ContentStoreError, ContentConflictError, EntryGoneError, BranchSyncingError, DuplicateContentIdError, UrlPathConflictError, isEmptyFieldValue, DEFAULT_INDEX_FRESHNESS_INTERVAL_MS, FORCED_REFRESH_MIN_INTERVAL_MS, STALE_LOOKUP, isMissingReference, createReferenceResolveCache, ContentStore, ATTR_CONTENT, BLOCK_PREFIX, BLOCK_SUFFIX, BLOCK_RESTORE_RE, INLINE_PREFIX, INLINE_SUFFIX, INLINE_RESTORE_RE, DEFAULT_REFERENCE_RENDERING, FLAT_CELL_TYPES, log6, BASE58_CHAR, ENTRY_LINK_PATTERN, INLINE_LINK_TARGET, BranchMetadataCorruptError, BRANCH_META_DIR, BRANCH_META_FILE, branchMetadataFilePath, UNKNOWN_TIME, enumOf, branchMetadataShape, branchMetadataSchema, branchMetadataFileSchema, log7, REGISTRY_FILE, REGISTRY_STALE_FILE, REGISTRY_TEMP_FILE, REGISTRY_VERSION, RESOURCE2, GET_MISS_REFRESH_MIN_INTERVAL_MS, snapshotSchema, reportedUnreadable, BranchRegistry, log8, OccWriteConflictError, CURRENT_SCHEMA_VERSION, BranchMetadataConflictError, BranchMetadataFileManager, loadBranchContext, log9, GIT_ENV_PASSTHROUGH, FORCE_C_LOCALE, GIT_NETWORK_ENV_PASSTHROUGH, NO_AUTO_GC_CONFIG, REMOTE_GIT_CONFIG, REMOTE_GIT_MAX_LOOSE_OBJECTS, REMOTE_GIT_MAX_PACKS, PARALLEL_CHECKOUT_ARGS, remoteInitLocks, GITHUB_TRACKING_REF_PREFIX, GitConflictError, GitRemoteRefMissingError, RemoteNotReadyError, SettingsRemoteUnreadableError, SettingsBranchDivergedError, SETTINGS_BRANCH_FILES, SettingsBranchHasContentHistoryError, GitManager, PUBLISH_LOCK_WAIT_MS, CREATE_RESIDUE_MIN_QUIET_MS, STAGING_MAX_AGE_MS, REPAIR_MAX_AGE_MS, DELETING_MAX_AGE_MS, SETTINGS_TRASH_MAX_AGE_MS, BranchProvisioningBusyError, BranchDirOccupiedError, STAGING_PREFIX, REPAIR_PREFIX, TRASH_PREFIX, stagingDirName, NO_GIT, RENAME_BLOCKED, SPARSE_CONE_RECORD, defaultSink, sink, ProvisionLog, testHooks, provisionsInFlight, BranchWorkspaceManager, log10, normalizePath, readEntryData, createReferenceResolver, withoutUnavailableEntries, resolveCollectionItemReferences, looksLikeMalformedEntry, listCollectionEntries, MAX_INPUT_PIXELS, CROP_PRECISION, KNOWN_FIELD_TYPES, STRING_FIELD_TYPES, selectOptionValues, GENERATED_RECORD_FILENAME, SAFE_BUILD_ID, BUILD_ID_RULE, jiti;
7774
7789
  var init_generate_ai_content = __esm({
7775
7790
  "dist/cli/generate-ai-content.js"() {
7776
7791
  "use strict";
@@ -9216,6 +9231,17 @@ var init_generate_ai_content = __esm({
9216
9231
  });
9217
9232
  CONTENT_WRITE_LOCK_STALE_MS = 3e4;
9218
9233
  heldRoots = new AsyncLocalStorage();
9234
+ getFormatExtension = (format) => {
9235
+ if (format === "md")
9236
+ return ".md";
9237
+ if (format === "mdx")
9238
+ return ".mdx";
9239
+ if (format === "yaml")
9240
+ return ".yaml";
9241
+ return ".json";
9242
+ };
9243
+ isDataOnlyFormat = (format) => format === "json" || format === "yaml";
9244
+ asRecord = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : {};
9219
9245
  init_item_keys();
9220
9246
  DEFAULT_SEO_FIELD_NAMES = {
9221
9247
  title: "metaTitle",
@@ -9226,7 +9252,7 @@ var init_generate_ai_content = __esm({
9226
9252
  noindex: "noindex",
9227
9253
  twitterCard: "twitterCard"
9228
9254
  };
9229
- asRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
9255
+ asRecord2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
9230
9256
  asText = (value) => {
9231
9257
  if (typeof value !== "string")
9232
9258
  return void 0;
@@ -9238,7 +9264,7 @@ var init_generate_ai_content = __esm({
9238
9264
  const text3 = asText(value);
9239
9265
  if (text3)
9240
9266
  return text3;
9241
- return asText(asRecord(value)?.src);
9267
+ return asText(asRecord2(value)?.src);
9242
9268
  };
9243
9269
  asOgType = (value) => {
9244
9270
  const text3 = asText(value);
@@ -9939,17 +9965,6 @@ var init_generate_ai_content = __esm({
9939
9965
  RESOURCE = "content-index";
9940
9966
  init_error3();
9941
9967
  init_error3();
9942
- getFormatExtension = (format) => {
9943
- if (format === "md")
9944
- return ".md";
9945
- if (format === "mdx")
9946
- return ".mdx";
9947
- if (format === "yaml")
9948
- return ".yaml";
9949
- return ".json";
9950
- };
9951
- isDataOnlyFormat = (format) => format === "json" || format === "yaml";
9952
- asRecord2 = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : {};
9953
9968
  init_normalize();
9954
9969
  init_operating_mode();
9955
9970
  RESERVED_SETTINGS_BRANCH_PREFIX = "canopycms-settings-";
@@ -10448,7 +10463,7 @@ var init_generate_ai_content = __esm({
10448
10463
  fields = entryTypeConfig?.schema || [];
10449
10464
  }
10450
10465
  if (format === "json") {
10451
- const data = asRecord2(JSON.parse(raw));
10466
+ const data = asRecord(JSON.parse(raw));
10452
10467
  doc = {
10453
10468
  collection: schemaItem.logicalPath,
10454
10469
  collectionName: schemaItem.name,
@@ -10458,7 +10473,7 @@ var init_generate_ai_content = __esm({
10458
10473
  absolutePath
10459
10474
  };
10460
10475
  } else if (format === "yaml") {
10461
- const data = asRecord2(yamlParse(raw));
10476
+ const data = asRecord(yamlParse(raw));
10462
10477
  doc = {
10463
10478
  collection: schemaItem.logicalPath,
10464
10479
  collectionName: schemaItem.name,
@@ -11061,10 +11076,11 @@ var init_generate_ai_content = __esm({
11061
11076
  * Measured (2000 occurrences of one target, local disk, the friendliest case for NOT
11062
11077
  * caching): cloning a snippet-sized target is ~63x cheaper than re-reading and parsing it, a
11063
11078
  * 265KB JSON target ~0.8x -- a wash at the pathological end, never a blow-up, and on the
11064
- * EFS/NFS this targets the syscall the memo removes dominates both. An md/mdx target
11065
- * resolves to its FRONTMATTER unless the field sets `includeBody`, which is why
11066
- * `includeBody` is opt-in per field: an embedded body sits inside the memoized object and is
11067
- * cloned once per referencing entry.
11079
+ * EFS/NFS this targets the syscall the memo removes dominates both. A target resolves
11080
+ * without its body (an md/mdx target to its frontmatter, a json/yaml one without its `isBody`
11081
+ * field) unless the field sets `includeBody`, which is why `includeBody` is opt-in per
11082
+ * field: an embedded body sits inside the memoized object and is cloned once per referencing
11083
+ * entry.
11068
11084
  */
11069
11085
  resolveSingleReference(id, idIndex, includeBody, cache, access) {
11070
11086
  if (!cache)
@@ -11159,7 +11175,8 @@ var init_generate_ai_content = __esm({
11159
11175
  }));
11160
11176
  }
11161
11177
  const bodyForEmbed = includeBody && "body" in doc ? { fieldName: doc.bodyFieldName, value: doc.body } : void 0;
11162
- return buildResolvedReference(doc.data, meta, bodyForEmbed);
11178
+ const data = includeBody ? doc.data : linkOnlyReferenceData(doc.data, doc.format, this.entryTypeOf(location.collection, location.relativePath)?.schema);
11179
+ return buildResolvedReference(data, meta, bodyForEmbed);
11163
11180
  } catch (error) {
11164
11181
  if (isNodeError3(error) && error.code === "ENOENT")
11165
11182
  return STALE_LOOKUP;
@@ -13182,10 +13199,10 @@ var init_generate_ai_content = __esm({
13182
13199
  try {
13183
13200
  const raw = await fs20.readFile(filePath, "utf8");
13184
13201
  if (format === "json") {
13185
- return asRecord2(JSON.parse(raw));
13202
+ return asRecord(JSON.parse(raw));
13186
13203
  }
13187
13204
  if (format === "yaml") {
13188
- return asRecord2(yamlParse2(raw));
13205
+ return asRecord(yamlParse2(raw));
13189
13206
  }
13190
13207
  const parsed = matter3(raw);
13191
13208
  const data = { ...parsed.data ?? {} };
@@ -2912,13 +2912,36 @@ function flattenGroupFields(fields) {
2912
2912
  return result;
2913
2913
  }
2914
2914
 
2915
+ // dist/utils/format.js
2916
+ var getFormatExtension = (format) => {
2917
+ if (format === "md")
2918
+ return ".md";
2919
+ if (format === "mdx")
2920
+ return ".mdx";
2921
+ if (format === "yaml")
2922
+ return ".yaml";
2923
+ return ".json";
2924
+ };
2925
+ var isDataOnlyFormat = (format) => format === "json" || format === "yaml";
2926
+ var asRecord = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : {};
2927
+
2915
2928
  // dist/utils/body-field.js
2916
2929
  function findBodyFieldName(fields) {
2930
+ return findExplicitBodyFieldName(fields) ?? "body";
2931
+ }
2932
+ function findExplicitBodyFieldName(fields) {
2917
2933
  for (const field of flattenGroupFields(fields)) {
2918
2934
  if ("isBody" in field && field.isBody)
2919
2935
  return field.name;
2920
2936
  }
2921
- return "body";
2937
+ return void 0;
2938
+ }
2939
+ function linkOnlyReferenceData(data, format, fields) {
2940
+ const bodyFieldName = isDataOnlyFormat(format) && fields ? findExplicitBodyFieldName(fields) : void 0;
2941
+ if (bodyFieldName === void 0 || !(bodyFieldName in data))
2942
+ return data;
2943
+ const { [bodyFieldName]: _body, ...rest } = data;
2944
+ return rest;
2922
2945
  }
2923
2946
 
2924
2947
  // dist/content-store.js
@@ -2934,7 +2957,7 @@ var DEFAULT_SEO_FIELD_NAMES = {
2934
2957
  noindex: "noindex",
2935
2958
  twitterCard: "twitterCard"
2936
2959
  };
2937
- var asRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
2960
+ var asRecord2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
2938
2961
  var asText = (value) => {
2939
2962
  if (typeof value !== "string")
2940
2963
  return void 0;
@@ -2946,7 +2969,7 @@ var asImageUrl = (value) => {
2946
2969
  const text = asText(value);
2947
2970
  if (text)
2948
2971
  return text;
2949
- return asText(asRecord(value)?.src);
2972
+ return asText(asRecord2(value)?.src);
2950
2973
  };
2951
2974
  var asOgType = (value) => {
2952
2975
  const text = asText(value);
@@ -2958,8 +2981,8 @@ var asTwitterCard = (value) => {
2958
2981
  };
2959
2982
  function extractSeoFields(entryData, opts = {}) {
2960
2983
  const names = { ...DEFAULT_SEO_FIELD_NAMES, ...opts.fields };
2961
- const entry = asRecord(entryData);
2962
- const source = opts.group ? asRecord(entry?.[opts.group]) : entry;
2984
+ const entry = asRecord2(entryData);
2985
+ const source = opts.group ? asRecord2(entry?.[opts.group]) : entry;
2963
2986
  return {
2964
2987
  title: asText(source?.[names.title]) ?? asText(opts.fallbackTitle),
2965
2988
  description: asText(source?.[names.description]) ?? asText(opts.fallbackDescription),
@@ -4459,19 +4482,6 @@ async function readFileWithMtimeIfExists(filePath) {
4459
4482
  }
4460
4483
  }
4461
4484
 
4462
- // dist/utils/format.js
4463
- var getFormatExtension = (format) => {
4464
- if (format === "md")
4465
- return ".md";
4466
- if (format === "mdx")
4467
- return ".mdx";
4468
- if (format === "yaml")
4469
- return ".yaml";
4470
- return ".json";
4471
- };
4472
- var isDataOnlyFormat = (format) => format === "json" || format === "yaml";
4473
- var asRecord2 = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : {};
4474
-
4475
4485
  // dist/paths/index.js
4476
4486
  init_normalize();
4477
4487
 
@@ -5085,7 +5095,7 @@ var ContentStore = class {
5085
5095
  fields = entryTypeConfig?.schema || [];
5086
5096
  }
5087
5097
  if (format === "json") {
5088
- const data = asRecord2(JSON.parse(raw));
5098
+ const data = asRecord(JSON.parse(raw));
5089
5099
  doc = {
5090
5100
  collection: schemaItem.logicalPath,
5091
5101
  collectionName: schemaItem.name,
@@ -5095,7 +5105,7 @@ var ContentStore = class {
5095
5105
  absolutePath
5096
5106
  };
5097
5107
  } else if (format === "yaml") {
5098
- const data = asRecord2(yamlParse(raw));
5108
+ const data = asRecord(yamlParse(raw));
5099
5109
  doc = {
5100
5110
  collection: schemaItem.logicalPath,
5101
5111
  collectionName: schemaItem.name,
@@ -5698,10 +5708,11 @@ var ContentStore = class {
5698
5708
  * Measured (2000 occurrences of one target, local disk, the friendliest case for NOT
5699
5709
  * caching): cloning a snippet-sized target is ~63x cheaper than re-reading and parsing it, a
5700
5710
  * 265KB JSON target ~0.8x -- a wash at the pathological end, never a blow-up, and on the
5701
- * EFS/NFS this targets the syscall the memo removes dominates both. An md/mdx target
5702
- * resolves to its FRONTMATTER unless the field sets `includeBody`, which is why
5703
- * `includeBody` is opt-in per field: an embedded body sits inside the memoized object and is
5704
- * cloned once per referencing entry.
5711
+ * EFS/NFS this targets the syscall the memo removes dominates both. A target resolves
5712
+ * without its body (an md/mdx target to its frontmatter, a json/yaml one without its `isBody`
5713
+ * field) unless the field sets `includeBody`, which is why `includeBody` is opt-in per
5714
+ * field: an embedded body sits inside the memoized object and is cloned once per referencing
5715
+ * entry.
5705
5716
  */
5706
5717
  resolveSingleReference(id, idIndex, includeBody, cache, access) {
5707
5718
  if (!cache)
@@ -5796,7 +5807,8 @@ var ContentStore = class {
5796
5807
  }));
5797
5808
  }
5798
5809
  const bodyForEmbed = includeBody && "body" in doc ? { fieldName: doc.bodyFieldName, value: doc.body } : void 0;
5799
- return buildResolvedReference(doc.data, meta, bodyForEmbed);
5810
+ const data = includeBody ? doc.data : linkOnlyReferenceData(doc.data, doc.format, this.entryTypeOf(location.collection, location.relativePath)?.schema);
5811
+ return buildResolvedReference(data, meta, bodyForEmbed);
5800
5812
  } catch (error) {
5801
5813
  if (isNodeError(error) && error.code === "ENOENT")
5802
5814
  return STALE_LOOKUP;
@@ -6752,14 +6764,18 @@ async function generateAIContent(options) {
6752
6764
  const pending = pendingById.get(id);
6753
6765
  if (!pending || !exported.has(id))
6754
6766
  return void 0;
6755
- const { slug } = pending.entry;
6756
- const target = buildResolvedReference(structuredClone(pending.storedData), {
6767
+ const { format } = pending;
6768
+ const { slug, fields } = pending.entry;
6769
+ const storedData = structuredClone(pending.storedData);
6770
+ const target = buildResolvedReference(linkOnlyReferenceData(storedData, format, fields), {
6757
6771
  id,
6758
6772
  slug,
6759
6773
  collection: pending.collectionPath,
6760
6774
  urlPath: computeEntryUrl(pending.collectionPath, slug, contentRoot)
6761
6775
  });
6762
- return { target, body: pending.body };
6776
+ const dataBodyField = isDataOnlyFormat(format) ? findExplicitBodyFieldName(fields) : void 0;
6777
+ const dataBody = dataBodyField === void 0 ? void 0 : storedData[dataBodyField];
6778
+ return { target, body: pending.body ?? (typeof dataBody === "string" ? dataBody : void 0) };
6763
6779
  };
6764
6780
  const linkFor = config?.referenceLink ? await decideReferenceLinks(config.referenceLink, allPending, subjectOf, entryLinkUrl) : void 0;
6765
6781
  const bodyLinkUrl = linkFor ? (location) => {
@@ -6901,6 +6917,7 @@ async function collectEntries(read, collection, cleanPath) {
6901
6917
  entry: aiEntry,
6902
6918
  body,
6903
6919
  storedData,
6920
+ format: doc.format,
6904
6921
  linkedIds: [],
6905
6922
  contentId: extractIdFromFilename(path13.basename(listEntry.relativePath)),
6906
6923
  absolutePath: doc.absolutePath,
@@ -9794,10 +9811,10 @@ var readEntryData = async (filePath, format, bodyFieldName = "body") => {
9794
9811
  try {
9795
9812
  const raw = await fs20.readFile(filePath, "utf8");
9796
9813
  if (format === "json") {
9797
- return asRecord2(JSON.parse(raw));
9814
+ return asRecord(JSON.parse(raw));
9798
9815
  }
9799
9816
  if (format === "yaml") {
9800
- return asRecord2(yamlParse2(raw));
9817
+ return asRecord(yamlParse2(raw));
9801
9818
  }
9802
9819
  const parsed = matter3(raw);
9803
9820
  const data = { ...parsed.data ?? {} };
@@ -45,10 +45,11 @@ interface BaseFieldConfig {
45
45
  /** Mark this field as the display title for entries using this schema. At most one field per schema. */
46
46
  isTitle?: boolean;
47
47
  /**
48
- * Mark this field as the body content for md/mdx entries.
49
- * The field must be type 'markdown' or 'mdx'. At most one per schema.
50
- * When present, the file's markdown content is mapped to this field,
51
- * making TypeFromEntrySchema include it without manual `& { body: string }`.
48
+ * Mark this field as the entry's body. The field must be type 'markdown' or 'mdx'. At most one
49
+ * per schema. On an md/mdx entry, the file's markdown content is mapped to this field, making
50
+ * TypeFromEntrySchema include it without manual `& { body: string }`. On a json/yaml entry, it
51
+ * is the prose a link-only reference omits. Either way a reference carries it only with
52
+ * `includeBody: true`.
52
53
  */
53
54
  isBody?: boolean;
54
55
  }
@@ -117,7 +118,9 @@ export interface ReferenceFieldConfig extends BaseFieldConfig {
117
118
  options?: ReferenceOption[];
118
119
  /**
119
120
  * Include the referenced entry's body in the resolved value, under that entry type's own
120
- * body field name (`isBody: true`, else `body`). Defaults to `false`.
121
+ * body field name (`isBody: true`, else `body`). Defaults to `false`. The body is an md/mdx
122
+ * target's markdown content, or a json/yaml target's `isBody` field; a json/yaml field merely
123
+ * named `body` is ordinary data and always resolves.
121
124
  *
122
125
  * This is the embed-vs-link distinction, declared where it is actually known. A reference
123
126
  * that **embeds** its target — a shared call-to-action block rendered inline — needs the
@@ -131,9 +134,6 @@ export interface ReferenceFieldConfig extends BaseFieldConfig {
131
134
  * contains both kinds: a page with a shared CTA *and* a related-posts list cannot be served
132
135
  * by a single call-level setting, and a call-level flag would mean the same field resolved
133
136
  * to different shapes in different places.
134
- *
135
- * Only affects formats that HAVE a separate body — md and mdx. A json/yaml target's whole
136
- * document is already its data, so this is a no-op there.
137
137
  */
138
138
  includeBody?: boolean;
139
139
  }
@@ -557,10 +557,11 @@ export declare class ContentStore {
557
557
  * Measured (2000 occurrences of one target, local disk, the friendliest case for NOT
558
558
  * caching): cloning a snippet-sized target is ~63x cheaper than re-reading and parsing it, a
559
559
  * 265KB JSON target ~0.8x -- a wash at the pathological end, never a blow-up, and on the
560
- * EFS/NFS this targets the syscall the memo removes dominates both. An md/mdx target
561
- * resolves to its FRONTMATTER unless the field sets `includeBody`, which is why
562
- * `includeBody` is opt-in per field: an embedded body sits inside the memoized object and is
563
- * cloned once per referencing entry.
560
+ * EFS/NFS this targets the syscall the memo removes dominates both. A target resolves
561
+ * without its body (an md/mdx target to its frontmatter, a json/yaml one without its `isBody`
562
+ * field) unless the field sets `includeBody`, which is why `includeBody` is opt-in per
563
+ * field: an embedded body sits inside the memoized object and is cloned once per referencing
564
+ * entry.
564
565
  */
565
566
  private resolveSingleReference;
566
567
  /**
@@ -20,7 +20,7 @@ import { serializeFrontmatter, serializeYaml } from './utils/content-serialize.j
20
20
  import { serializeJson } from './utils/json-source-splice.js';
21
21
  import { withLock } from './utils/async-mutex.js';
22
22
  import { assertContentWriteLockHeld, ContentWriteLockBusyError, DEFAULT_CONTENT_WRITE_LOCK_WAIT_MS, withContentWriteLock, } from './utils/content-write-lock.js';
23
- import { findBodyFieldName } from './utils/body-field.js';
23
+ import { findBodyFieldName, linkOnlyReferenceData } from './utils/body-field.js';
24
24
  import { withItemKeys } from './item-keys.js';
25
25
  import { buildMissingReference, buildResolvedReference, buildRestrictedReference, } from './entry-schema.js';
26
26
  import { warnDanglingReference } from './dangling-reference-log.js';
@@ -1611,10 +1611,11 @@ export class ContentStore {
1611
1611
  * Measured (2000 occurrences of one target, local disk, the friendliest case for NOT
1612
1612
  * caching): cloning a snippet-sized target is ~63x cheaper than re-reading and parsing it, a
1613
1613
  * 265KB JSON target ~0.8x -- a wash at the pathological end, never a blow-up, and on the
1614
- * EFS/NFS this targets the syscall the memo removes dominates both. An md/mdx target
1615
- * resolves to its FRONTMATTER unless the field sets `includeBody`, which is why
1616
- * `includeBody` is opt-in per field: an embedded body sits inside the memoized object and is
1617
- * cloned once per referencing entry.
1614
+ * EFS/NFS this targets the syscall the memo removes dominates both. A target resolves
1615
+ * without its body (an md/mdx target to its frontmatter, a json/yaml one without its `isBody`
1616
+ * field) unless the field sets `includeBody`, which is why `includeBody` is opt-in per
1617
+ * field: an embedded body sits inside the memoized object and is cloned once per referencing
1618
+ * entry.
1618
1619
  */
1619
1620
  resolveSingleReference(id, idIndex, includeBody, cache, access) {
1620
1621
  if (!cache)
@@ -1738,9 +1739,13 @@ export class ContentStore {
1738
1739
  //
1739
1740
  // `'body' in doc` narrows the ContentDocument union to its markdown variant, so
1740
1741
  // `doc.body`/`doc.bodyFieldName` are reachable at all -- a type guard, not a redundant
1741
- // runtime check. Only a field that asked to EMBED its target passes a body at all.
1742
+ // runtime check. Only a field that asked to EMBED its target passes a body at all, and
1743
+ // only such a field keeps a json/yaml target's `isBody` field.
1742
1744
  const bodyForEmbed = includeBody && 'body' in doc ? { fieldName: doc.bodyFieldName, value: doc.body } : undefined;
1743
- return buildResolvedReference(doc.data, meta, bodyForEmbed);
1745
+ const data = includeBody
1746
+ ? doc.data
1747
+ : linkOnlyReferenceData(doc.data, doc.format, this.entryTypeOf(location.collection, location.relativePath)?.schema);
1748
+ return buildResolvedReference(data, meta, bodyForEmbed);
1744
1749
  }
1745
1750
  catch (error) {
1746
1751
  // Index hit but the file is gone — the typical symptom of an external
@@ -48,7 +48,10 @@ type InferableField = {
48
48
  entryTypes?: readonly string[];
49
49
  /** For reference fields: collection paths to scope the search. */
50
50
  collections?: readonly string[];
51
- /** For reference fields: resolve the target's body too. See ReferenceFieldConfig.includeBody. */
51
+ /**
52
+ * For reference fields: resolve the target's body too. Without it the `resolvedSchema`'s
53
+ * `isBody` field is optional in the resolved type. See ReferenceFieldConfig.includeBody.
54
+ */
52
55
  includeBody?: boolean;
53
56
  };
54
57
  /**
@@ -79,7 +82,9 @@ export interface ResolvedReferenceMeta {
79
82
  }
80
83
  /**
81
84
  * Assemble a resolved reference: the target's own data, then its body if the field asked to
82
- * embed it, then the reserved metadata.
85
+ * embed it, then the reserved metadata. A link-only reference carries no body for any format:
86
+ * an md/mdx target passes none here, and a json/yaml target's `data` arrives without its
87
+ * `isBody` field (`linkOnlyReferenceData`), so both formats resolve alike.
83
88
  *
84
89
  * The ordering is the contract, not a detail. Metadata LAST means a target that models `id` as
85
90
  * a content field cannot shadow the real content ID, which the write boundary recovers from
@@ -272,7 +277,7 @@ type FieldValue<F extends InferableField> = F extends {
272
277
  } ? ScalarValue<F, SelectValue<F>> : F extends {
273
278
  type: 'reference';
274
279
  resolvedSchema: infer S;
275
- } ? ScalarValue<F, (InferContentShape<Extract<S, readonly InferableField[]>> & ResolvedReferenceMeta & {
280
+ } ? ScalarValue<F, (InferContentShape<Extract<S, readonly InferableField[]>, F['includeBody'] extends true ? false : true> & ResolvedReferenceMeta & {
276
281
  unavailable?: undefined;
277
282
  }) | UnavailableReference | null> : F extends {
278
283
  type: 'reference';
@@ -298,11 +303,16 @@ type FieldValue<F extends InferableField> = F extends {
298
303
  *
299
304
  * Works with any structurally compatible array; importing FieldConfig is not required.
300
305
  */
301
- type InferContentShape<Fields extends readonly InferableField[]> = Simplify<{
302
- [F in FlattenInlineGroups<Fields>[number] as F['required'] extends false ? never : F['name']]: FieldValue<F>;
306
+ type InferContentShape<Fields extends readonly InferableField[], BodyOptional extends boolean = false> = Simplify<{
307
+ [F in FlattenInlineGroups<Fields>[number] as IsOptionalKey<F, BodyOptional> extends true ? never : F['name']]: FieldValue<F>;
303
308
  } & {
304
- [F in FlattenInlineGroups<Fields>[number] as F['required'] extends false ? F['name'] : never]?: FieldValue<F>;
309
+ [F in FlattenInlineGroups<Fields>[number] as IsOptionalKey<F, BodyOptional> extends true ? F['name'] : never]?: FieldValue<F>;
305
310
  }>;
311
+ /**
312
+ * Whether InferContentShape gives a field `?:`: an explicit `required: false`, or, with
313
+ * `BodyOptional` (a reference target resolved without `includeBody`), the `isBody` field.
314
+ */
315
+ type IsOptionalKey<F extends InferableField, BodyOptional extends boolean> = F['required'] extends false ? true : BodyOptional extends true ? F['isBody'] extends true ? true : false : false;
306
316
  /** Define an entry schema's field array with literal inference, without sprinkling `as const`. */
307
317
  export declare const defineEntrySchema: <const T extends readonly InferableField[]>(fields: T) => T;
308
318
  /** Content shape derived from a `defineEntrySchema` result. */
@@ -12,7 +12,9 @@ import { DEFAULT_SEO_FIELD_NAMES } from './static/seo.js';
12
12
  export const RESOLVED_REFERENCE_KEYS = ['id', 'slug', 'collection', 'urlPath'];
13
13
  /**
14
14
  * Assemble a resolved reference: the target's own data, then its body if the field asked to
15
- * embed it, then the reserved metadata.
15
+ * embed it, then the reserved metadata. A link-only reference carries no body for any format:
16
+ * an md/mdx target passes none here, and a json/yaml target's `data` arrives without its
17
+ * `isBody` field (`linkOnlyReferenceData`), so both formats resolve alike.
16
18
  *
17
19
  * The ordering is the contract, not a detail. Metadata LAST means a target that models `id` as
18
20
  * a content field cannot shadow the real content ID, which the write boundary recovers from
@@ -1,4 +1,4 @@
1
- import type { FieldConfig } from '../config/index.js';
1
+ import type { ContentFormat, FieldConfig } from '../config/index.js';
2
2
  /**
3
3
  * Count top-level fields marked `isBody: true`. Does NOT recurse into objects — `isBody` maps
4
4
  * to the file's markdown content, which exists only at the top level.
@@ -6,6 +6,14 @@ import type { FieldConfig } from '../config/index.js';
6
6
  export declare function countBodyFields(fields: readonly FieldConfig[]): number;
7
7
  /** Name of the `isBody: true` field, or `'body'`; maps the file's markdown onto a data field. */
8
8
  export declare function findBodyFieldName(fields: readonly FieldConfig[]): string;
9
+ /** Name of the `isBody: true` field, or `undefined`: no `'body'` fallback. */
10
+ export declare function findExplicitBodyFieldName(fields: readonly FieldConfig[]): string | undefined;
11
+ /**
12
+ * The data a link-only reference (one without `includeBody: true`) carries for its target. A
13
+ * json/yaml entry's `isBody` field is its prose, so it is left out, as an md/mdx target's body
14
+ * is. Only an explicit `isBody` counts: a field merely named `body` is ordinary data.
15
+ */
16
+ export declare function linkOnlyReferenceData(data: Record<string, unknown>, format: ContentFormat, fields: readonly FieldConfig[] | undefined): Record<string, unknown>;
9
17
  /**
10
18
  * Find an isBody field whose NAME is reserved by reference resolution.
11
19
  *
@@ -1,4 +1,5 @@
1
1
  import { flattenGroupFields } from './flatten-group-fields.js';
2
+ import { isDataOnlyFormat } from './format.js';
2
3
  const BODY_FIELD_TYPES = new Set(['markdown', 'mdx']);
3
4
  /**
4
5
  * Count top-level fields marked `isBody: true`. Does NOT recurse into objects — `isBody` maps
@@ -14,11 +15,27 @@ export function countBodyFields(fields) {
14
15
  }
15
16
  /** Name of the `isBody: true` field, or `'body'`; maps the file's markdown onto a data field. */
16
17
  export function findBodyFieldName(fields) {
18
+ return findExplicitBodyFieldName(fields) ?? 'body';
19
+ }
20
+ /** Name of the `isBody: true` field, or `undefined`: no `'body'` fallback. */
21
+ export function findExplicitBodyFieldName(fields) {
17
22
  for (const field of flattenGroupFields(fields)) {
18
23
  if ('isBody' in field && field.isBody)
19
24
  return field.name;
20
25
  }
21
- return 'body';
26
+ return undefined;
27
+ }
28
+ /**
29
+ * The data a link-only reference (one without `includeBody: true`) carries for its target. A
30
+ * json/yaml entry's `isBody` field is its prose, so it is left out, as an md/mdx target's body
31
+ * is. Only an explicit `isBody` counts: a field merely named `body` is ordinary data.
32
+ */
33
+ export function linkOnlyReferenceData(data, format, fields) {
34
+ const bodyFieldName = isDataOnlyFormat(format) && fields ? findExplicitBodyFieldName(fields) : undefined;
35
+ if (bodyFieldName === undefined || !(bodyFieldName in data))
36
+ return data;
37
+ const { [bodyFieldName]: _body, ...rest } = data;
38
+ return rest;
22
39
  }
23
40
  /**
24
41
  * Find an isBody field whose NAME is reserved by reference resolution.
package/dist/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const CANOPYCMS_VERSION = "0.3.3";
1
+ export declare const CANOPYCMS_VERSION = "0.4.0";
package/dist/version.js CHANGED
@@ -1,3 +1,3 @@
1
1
  // Generated by scripts/bump-version.mjs, its only writer. The package builds with plain
2
2
  // tsc (rootDir: src), so src/ cannot import ../package.json; version.test.ts keeps the two equal.
3
- export const CANOPYCMS_VERSION = '0.3.3';
3
+ export const CANOPYCMS_VERSION = '0.4.0';
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "//": "@codemirror/language, @lezer/highlight: workaround — @mdxeditor/editor uses cm6-theme-basic-light which peer-requires these but mdxeditor doesn't declare them as dependencies",
3
3
  "name": "canopycms",
4
- "version": "0.3.3",
4
+ "version": "0.4.0",
5
5
  "description": "CanopyCMS core package: schema-driven content, branch-aware editing, and editor UI for Next.js.",
6
6
  "license": "MIT",
7
7
  "repository": {