From 575ff9ac44d43d18640a0f1bb9902faf19a938a0 Mon Sep 17 00:00:00 2001 From: Sridhar Ratnakumar Date: Mon, 4 May 2026 12:05:13 -0400 Subject: [PATCH 1/5] fix(sdata): union arrays in metadata cascade so cascaded tags survive (#697) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `Emanote.Model.SData.mergeAeson` previously delegated to `aeson-extra`'s `lodashMerge`, which `alignWith`-merges arrays element-by-element. For list-valued frontmatter fields like `tags`, this meant a parent `folder.yaml` declaring `tags: [team-doc]` plus a child note declaring `tags: [internal-note]` produced `[internal-note]` — the cascaded tag was silently clobbered, both in the per-page chip strip and in the post-#352 global tag index. The render path had carried this behavior forever; #352 surfaced the same boundary on the index path. Replace `lodashMerge` with a direct merger that matches cascade semantics: objects deep-merge by key, arrays concatenate then dedup, scalars right-win. The change is local to `mergeAeson` so every cascade caller (`parseSDataCascading`, `getEffectiveRouteMetaWith`, `withAesonDefault`) inherits the new behavior in one place. Drops the `aeson-extra` dependency since `lodashMerge` was its only user. Adds `Emanote.Model.SDataSpec` (8 hspec cases) plus an `issue-697` fixture and two smoke scenarios that exercise both the chip strip and the tag-index page when a note carries cascaded, frontmatter, and inline-body tags simultaneously. Refreshes the now-obsolete fixture comment on `issue-352/note.md` warning future contributors away from inline `#tags` — the underlying constraint no longer holds. Documents the cascade merge rules in `docs/guide/yaml-config.md` so "arrays extend, scalars override" is discoverable next to the cascade description, instead of folklore. --- docs/guide/yaml-config.md | 10 ++++ emanote/CHANGELOG.md | 1 + emanote/emanote.cabal | 3 +- emanote/src/Emanote/Model/SData.hs | 17 +++++- emanote/test/Emanote/Model/SDataSpec.hs | 63 +++++++++++++++++++++++ tests/features/smoke.feature | 10 ++++ tests/fixtures/notebook/issue-352/note.md | 10 ---- tests/fixtures/notebook/issue-697.yaml | 2 + tests/fixtures/notebook/issue-697/note.md | 14 +++++ 9 files changed, 117 insertions(+), 13 deletions(-) create mode 100644 emanote/test/Emanote/Model/SDataSpec.hs create mode 100644 tests/fixtures/notebook/issue-697.yaml create mode 100644 tests/fixtures/notebook/issue-697/note.md diff --git a/docs/guide/yaml-config.md b/docs/guide/yaml-config.md index e9909634b..d8fa8d535 100644 --- a/docs/guide/yaml-config.md +++ b/docs/guide/yaml-config.md @@ -18,6 +18,16 @@ Notice how this page's sidebar colorscheme has [changed to green]{.greenery}? Vi >[!tip] Using in HTML templates > You can reference the YAML frontmatter config from [[html-template]]. See [here](https://github.com/srid/emanote/discussions/131#discussioncomment-1382189) for details. +## Cascade merge semantics + +When a child route's frontmatter or YAML overlaps with values from parent YAMLs, Emanote merges them along three rules: + +- **Objects** are deep-merged by key — the child overrides individual nested fields without touching siblings. +- **Arrays** concatenate (with deduplication) — `tags: [team-doc]` in `folder.yaml` plus `tags: [internal-note]` on a child note yields `[team-doc, internal-note]`. The same applies to other list-valued fields like `pandoc.filters`. +- **Scalars** right-win — the most-specific value (the leaf) overrides ancestors. + +The array rule is what makes a parent YAML's `tags` survive to its children even when those children declare their own. (Prior to [#697](https://github.com/srid/emanote/issues/697), arrays were aligned by index, which silently clobbered cascaded entries.) + ## Special properties - `page.image`: The image to use for the page. This is used for the [[ogp]] meta tag `og:image` meta tag. If not specified, the first image in the page is used. Relative URLs are automatically rewritten to absolute URLs if `page.siteUrl` is non-empty. diff --git a/emanote/CHANGELOG.md b/emanote/CHANGELOG.md index cfdfff16d..7a6750ef6 100644 --- a/emanote/CHANGELOG.md +++ b/emanote/CHANGELOG.md @@ -22,6 +22,7 @@ **Bug fixes** +- Cascade-declared `tags` no longer disappear from a child note that declares any of its own. The metadata-cascade merger previously used `aeson-extra`'s `lodashMerge`, which aligns arrays by index — `tags: [team-doc]` in `folder.yaml` plus `tags: [internal-note]` in `folder/note.md` produced `[internal-note]`, dropping the cascaded entry both from the per-page chip strip and from the `#352` global tag index. The merger now treats cascade arrays as extensible defaults: arrays concatenate (with deduplication), objects deep-merge by key, scalars right-win. The change generalises to other list-valued fields (e.g. `pandoc.filters`). `aeson-extra` is no longer a dependency (closes [#697](https://github.com/srid/emanote/issues/697)). - Wiki link custom titles now render HTML entities like ` ` the same way regular Markdown link labels do. Previously `[[note|Spivak (2014)]]` rendered the entity text literally as ` ` (closes [#441](https://github.com/srid/emanote/issues/441)). - Atom feed: a feed query that matches no notes no longer crashes the build; an empty-but-valid Atom document is emitted instead. Configuration errors (missing/invalid query block, missing `page.siteUrl`) still fail loudly ([#490](https://github.com/srid/emanote/issues/490), [#650](https://github.com/srid/emanote/pull/650)) - Markdown links to a static `.xml` asset (e.g. `[Test](./test.xml)`) now resolve to the file. Previously a `.xml` URL was always interpreted as the Atom feed of a same-named note, leaving asset links broken when no such feed-enabled note existed. The missing-link page now also tailors its "you may create…" hint to the URL extension instead of always suggesting `.md` / `.org` (closes [#547](https://github.com/srid/emanote/issues/547)) diff --git a/emanote/emanote.cabal b/emanote/emanote.cabal index ef4a8f267..29e9c5a11 100644 --- a/emanote/emanote.cabal +++ b/emanote/emanote.cabal @@ -94,7 +94,6 @@ common library-common -- TODO: We could use the ghcid flag trick in neuron.cabal to avoid rebuilds. build-depends: , aeson - , aeson-extra , aeson-optics , async , base >=4.14 && <5 @@ -153,6 +152,7 @@ common library-common , uri-encode , url-slug , uuid + , vector , warp , which , with-utf8 @@ -256,6 +256,7 @@ test-suite test other-modules: Emanote.Model.Link.RelSpec Emanote.Model.QuerySpec + Emanote.Model.SDataSpec Emanote.Model.TocSpec Emanote.Pandoc.BuiltinFiltersSpec Emanote.Pandoc.ExternalLinkSpec diff --git a/emanote/src/Emanote/Model/SData.hs b/emanote/src/Emanote/Model/SData.hs index 947f0fe7d..689df3d6e 100644 --- a/emanote/src/Emanote/Model/SData.hs +++ b/emanote/src/Emanote/Model/SData.hs @@ -4,11 +4,11 @@ module Emanote.Model.SData where import Data.Aeson qualified as Aeson -import Data.Aeson.Extra.Merge qualified as AesonMerge import Data.Aeson.KeyMap qualified as KM import Data.Data (Data) import Data.IxSet.Typed (Indexable (..), IxSet, ixGen, ixList) import Data.List.NonEmpty qualified as NE +import Data.Vector qualified as V import Data.Yaml qualified as Yaml import Emanote.Route qualified as R import Optics.TH (makeLenses) @@ -54,8 +54,21 @@ mergeAesons :: NonEmpty Aeson.Value -> Aeson.Value mergeAesons = last . NE.scanl1 mergeAeson +{- | Deep-merge two YAML-shaped Aeson values for the metadata cascade. + +Objects merge by key (recursively), arrays concatenate (then dedup), and +scalars right-win. The array case is the deliberate divergence from +@aeson-extra@'s @lodashMerge@: lodash aligns arrays by index, which for +list-valued fields like @tags@ silently clobbers cascade contributions +the moment a child note declares any of its own. Cascade defaults are +meant to extend, not align — see issue #697. +-} mergeAeson :: Aeson.Value -> Aeson.Value -> Aeson.Value -mergeAeson = AesonMerge.lodashMerge +mergeAeson (Aeson.Object a) (Aeson.Object b) = + Aeson.Object $ KM.unionWith mergeAeson a b +mergeAeson (Aeson.Array a) (Aeson.Array b) = + Aeson.Array $ V.fromList $ ordNub $ V.toList a <> V.toList b +mergeAeson _ b = b -- TODO: Use https://hackage.haskell.org/package/lens-aeson lookupAeson :: forall a. (Aeson.FromJSON a) => a -> NonEmpty Text -> Aeson.Value -> a diff --git a/emanote/test/Emanote/Model/SDataSpec.hs b/emanote/test/Emanote/Model/SDataSpec.hs new file mode 100644 index 000000000..f135c4dbb --- /dev/null +++ b/emanote/test/Emanote/Model/SDataSpec.hs @@ -0,0 +1,63 @@ +module Emanote.Model.SDataSpec where + +import Data.Aeson ((.=)) +import Data.Aeson qualified as Aeson +import Data.Aeson.Types qualified as Aeson +import Emanote.Model.SData qualified as SData +import Relude +import Test.Hspec + +obj :: [Aeson.Pair] -> Aeson.Value +obj = Aeson.object + +arr :: [Text] -> Aeson.Value +arr = Aeson.toJSON + +spec :: Spec +spec = do + describe "mergeAeson cascade semantics" $ do + it "right wins on scalar conflict" + $ SData.mergeAeson (obj ["name" .= ("old" :: Text)]) (obj ["name" .= ("new" :: Text)]) + `shouldBe` obj ["name" .= ("new" :: Text)] + it "deep-merges objects by key" + $ SData.mergeAeson + (obj ["template" .= obj ["theme" .= ("blue" :: Text), "name" .= ("old" :: Text)]]) + (obj ["template" .= obj ["theme" .= ("green" :: Text)]]) + `shouldBe` obj ["template" .= obj ["theme" .= ("green" :: Text), "name" .= ("old" :: Text)]] + it "concatenates arrays instead of aligning by index (issue #697)" + $ SData.mergeAeson + (obj ["tags" .= arr ["team-doc"]]) + (obj ["tags" .= arr ["internal-note"]]) + `shouldBe` obj ["tags" .= arr ["team-doc", "internal-note"]] + it "deduplicates repeated array elements after concatenation" + $ SData.mergeAeson + (obj ["tags" .= arr ["team-doc", "shared"]]) + (obj ["tags" .= arr ["shared", "internal-note"]]) + `shouldBe` obj ["tags" .= arr ["team-doc", "shared", "internal-note"]] + it "preserves single-side array values when only one side declares the key" + $ SData.mergeAeson + (obj ["tags" .= arr ["team-doc"]]) + (obj ["title" .= ("child" :: Text)]) + `shouldBe` obj ["tags" .= arr ["team-doc"], "title" .= ("child" :: Text)] + it "carries array semantics through nested objects (e.g. pandoc.filters)" + $ SData.mergeAeson + (obj ["pandoc" .= obj ["filters" .= arr ["site-wide.lua"]]]) + (obj ["pandoc" .= obj ["filters" .= arr ["page-local.lua"]]]) + `shouldBe` obj ["pandoc" .= obj ["filters" .= arr ["site-wide.lua", "page-local.lua"]]] + describe "mergeAesons folds left-to-right with right-most winning at scalars" $ do + it "later cascade entries override earlier scalar values" + $ SData.mergeAesons + ( obj ["template" .= obj ["theme" .= ("blue" :: Text)]] + :| [ obj ["template" .= obj ["theme" .= ("green" :: Text)]] + , obj ["template" .= obj ["theme" .= ("red" :: Text)]] + ] + ) + `shouldBe` obj ["template" .= obj ["theme" .= ("red" :: Text)]] + it "unions tags across the whole cascade" + $ SData.mergeAesons + ( obj ["tags" .= arr ["root"]] + :| [ obj ["tags" .= arr ["mid"]] + , obj ["tags" .= arr ["leaf"]] + ] + ) + `shouldBe` obj ["tags" .= arr ["root", "mid", "leaf"]] diff --git a/tests/features/smoke.feature b/tests/features/smoke.feature index 9b04f233d..956be0fb6 100644 --- a/tests/features/smoke.feature +++ b/tests/features/smoke.feature @@ -180,3 +180,13 @@ Feature: Smoke Scenario: Tag declared in sibling folder YAML appears on the root tag-index page (regression: #352) When I fetch "/-/tags.html" Then the response body contains "-/tags/issue-352-cascaded.html" + + Scenario: Cascaded YAML tag survives when the child note declares its own tags (regression: #697) + When I open "/issue-697/note.html" + Then the metadata tag chip with text "issue-697-cascaded" has href containing "-/tags/issue-697-cascaded.html" + And the metadata tag chip with text "issue-697-own" has href containing "-/tags/issue-697-own.html" + And the metadata tag chip with text "issue-697-inline" has href containing "-/tags/issue-697-inline.html" + + Scenario: Cascaded YAML tag still indexes a child note that declares its own tags (regression: #697) + When I fetch "/-/tags/issue-697-cascaded.html" + Then the response body contains "issue-697/note" diff --git a/tests/fixtures/notebook/issue-352/note.md b/tests/fixtures/notebook/issue-352/note.md index 472e7e090..a11cd86a0 100644 --- a/tests/fixtures/notebook/issue-352/note.md +++ b/tests/fixtures/notebook/issue-352/note.md @@ -1,13 +1,3 @@ - - # Cascaded-tag fixture This note declares no tags in its own frontmatter. The tag diff --git a/tests/fixtures/notebook/issue-697.yaml b/tests/fixtures/notebook/issue-697.yaml new file mode 100644 index 000000000..eef6ebec9 --- /dev/null +++ b/tests/fixtures/notebook/issue-697.yaml @@ -0,0 +1,2 @@ +tags: + - issue-697-cascaded diff --git a/tests/fixtures/notebook/issue-697/note.md b/tests/fixtures/notebook/issue-697/note.md new file mode 100644 index 000000000..c6e04fdc5 --- /dev/null +++ b/tests/fixtures/notebook/issue-697/note.md @@ -0,0 +1,14 @@ +--- +tags: + - issue-697-own +--- + +# Cascade-survives-own-tags fixture + +This note declares `issue-697-own` in its frontmatter, and an inline +#issue-697-inline tag in the body. The sibling `issue-697.yaml` +contributes `issue-697-cascaded` via the data cascade. + +All three must reach the page chip strip and the global tag index; +prior to #697 the cascaded tag was clobbered the moment the leaf +note declared any tags of its own. From 8ad010ec6190c188ab0c039dc00e39f85ba4f67a Mon Sep 17 00:00:00 2001 From: Sridhar Ratnakumar Date: Mon, 4 May 2026 12:09:56 -0400 Subject: [PATCH 2/5] refactor(lowy): clarify mergeAeson contract for cascade vs. non-cascade callers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Address three lowy findings on the just-landed mergeAeson rewrite: 1. Semantic overload at the boundary: mergeAeson is called from one true-cascade site (Meta.getEffectiveRouteMetaWith) and three non-cascade sites (Note.withAesonDefault, Note.overrideAesonText, View.Template.setErrorPageMeta). Renaming to mergeCascadeAeson would mislead the latter; instead, document that the contract is universal and that non-cascade callers happen to never traffic in arrays — they share the merger by design, not coincidence. 2. Stability note: per-key merge strategies are an item on the issue roadmap. The Haddock now flags the right boundary for that change (a new module that owns cascade folding) so future readers don't reach for a strategy parameter on this function. 3. Cascade-vs-non-cascade asymmetry: the contract section makes the shared dependence on object/scalar clauses explicit and lists every call site, so the invisible coupling lowy flagged is visible in the source. No code change beyond the Haddock block. --- emanote/src/Emanote/Model/SData.hs | 36 +++++++++++++++++++++++------- 1 file changed, 28 insertions(+), 8 deletions(-) diff --git a/emanote/src/Emanote/Model/SData.hs b/emanote/src/Emanote/Model/SData.hs index 689df3d6e..4fcdc4a54 100644 --- a/emanote/src/Emanote/Model/SData.hs +++ b/emanote/src/Emanote/Model/SData.hs @@ -54,14 +54,34 @@ mergeAesons :: NonEmpty Aeson.Value -> Aeson.Value mergeAesons = last . NE.scanl1 mergeAeson -{- | Deep-merge two YAML-shaped Aeson values for the metadata cascade. - -Objects merge by key (recursively), arrays concatenate (then dedup), and -scalars right-win. The array case is the deliberate divergence from -@aeson-extra@'s @lodashMerge@: lodash aligns arrays by index, which for -list-valued fields like @tags@ silently clobbers cascade contributions -the moment a child note declares any of its own. Cascade defaults are -meant to extend, not align — see issue #697. +{- | Deep-merge two YAML-shaped Aeson values. + +Contract — applies uniformly regardless of caller: + + * Objects merge by key, recursively. + * Arrays concatenate, then deduplicate (left order preserved). + * Scalars right-win. + +The array clause is the load-bearing one and the deliberate divergence +from @aeson-extra@'s @lodashMerge@: lodash aligns arrays by index, +which for list-valued fields like @tags@ silently clobbers cascade +contributions the moment a child note declares any of its own — see +issue #697. + +The function is general-purpose. The metadata cascade +('parseSDataCascading', 'Emanote.Model.Meta.getEffectiveRouteMetaWith') +is the case where every clause matters, but non-cascade callers +('Emanote.Model.Note.withAesonDefault', +'Emanote.Model.Note.overrideAesonText', +'Emanote.View.Template.setErrorPageMeta') merge values that contain no +arrays and so rely only on the object/scalar clauses; they share this +single merger by design rather than coincidence. + +Stability note: the contract is fixed across keys. If a future change +needs per-key strategies (e.g. \"replace, don't union, for field X\"), +the right boundary is a new module that owns cascade folding and +parameterises strategy — not a flag added here. Callers should keep +treating this function as the universal deep-merge primitive. -} mergeAeson :: Aeson.Value -> Aeson.Value -> Aeson.Value mergeAeson (Aeson.Object a) (Aeson.Object b) = From 5cb699f0fe1c0f72b3aa8d9acb3c01d48eaa7bbf Mon Sep 17 00:00:00 2001 From: Sridhar Ratnakumar Date: Mon, 4 May 2026 12:15:16 -0400 Subject: [PATCH 3/5] =?UTF-8?q?refactor(police):=20elegance=20=E2=80=94=20?= =?UTF-8?q?trim=20mergeAeson=20Haddock=20to=20the=20contract?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Code-police elegance pass flagged the post-lowy Haddock as disproportionate (28 prose lines for a 5-line function). Two of the three sections were over-specified: - The non-cascade caller enumeration named three call sites in a docstring that won't be updated when callers move; grep finds those call sites in seconds and the listing will rot. - The stability note prescribed a future module shape — speculative architecture advice that belongs in an issue or PR discussion, not in a function docstring. The Contract section + lodash-divergence paragraph already make the boundary visible: the merge applies uniformly to every caller, and the array clause is documented as the #697 fix. That's enough. --- emanote/src/Emanote/Model/SData.hs | 29 ++++++----------------------- 1 file changed, 6 insertions(+), 23 deletions(-) diff --git a/emanote/src/Emanote/Model/SData.hs b/emanote/src/Emanote/Model/SData.hs index 4fcdc4a54..befe89607 100644 --- a/emanote/src/Emanote/Model/SData.hs +++ b/emanote/src/Emanote/Model/SData.hs @@ -54,34 +54,17 @@ mergeAesons :: NonEmpty Aeson.Value -> Aeson.Value mergeAesons = last . NE.scanl1 mergeAeson -{- | Deep-merge two YAML-shaped Aeson values. - -Contract — applies uniformly regardless of caller: +{- | Deep-merge two YAML-shaped Aeson values. The contract applies +uniformly to every caller (cascade and non-cascade alike): * Objects merge by key, recursively. * Arrays concatenate, then deduplicate (left order preserved). * Scalars right-win. -The array clause is the load-bearing one and the deliberate divergence -from @aeson-extra@'s @lodashMerge@: lodash aligns arrays by index, -which for list-valued fields like @tags@ silently clobbers cascade -contributions the moment a child note declares any of its own — see -issue #697. - -The function is general-purpose. The metadata cascade -('parseSDataCascading', 'Emanote.Model.Meta.getEffectiveRouteMetaWith') -is the case where every clause matters, but non-cascade callers -('Emanote.Model.Note.withAesonDefault', -'Emanote.Model.Note.overrideAesonText', -'Emanote.View.Template.setErrorPageMeta') merge values that contain no -arrays and so rely only on the object/scalar clauses; they share this -single merger by design rather than coincidence. - -Stability note: the contract is fixed across keys. If a future change -needs per-key strategies (e.g. \"replace, don't union, for field X\"), -the right boundary is a new module that owns cascade folding and -parameterises strategy — not a flag added here. Callers should keep -treating this function as the universal deep-merge primitive. +The array clause is the deliberate divergence from @aeson-extra@'s +@lodashMerge@: lodash aligns arrays by index, which for list-valued +fields like @tags@ silently clobbers cascade contributions the moment +a child note declares any of its own — see issue #697. -} mergeAeson :: Aeson.Value -> Aeson.Value -> Aeson.Value mergeAeson (Aeson.Object a) (Aeson.Object b) = From 2e9dc07c0173ea41e51a7a535a22facf25cad1f8 Mon Sep 17 00:00:00 2001 From: Sridhar Ratnakumar Date: Mon, 4 May 2026 12:15:33 -0400 Subject: [PATCH 4/5] =?UTF-8?q?refactor(police):=20elegance=20=E2=80=94=20?= =?UTF-8?q?trim=20CHANGELOG=20bullet,=20link=20to=20docs/guide?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Code-police elegance pass: the bullet was restating the merge contract that docs/guide/yaml-config.md now owns. Trim to the bug-and-cause story and link out for the contract; the docs page is the canonical reference for "how cascade merges work." --- emanote/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/emanote/CHANGELOG.md b/emanote/CHANGELOG.md index 7a6750ef6..9330f90c3 100644 --- a/emanote/CHANGELOG.md +++ b/emanote/CHANGELOG.md @@ -22,7 +22,7 @@ **Bug fixes** -- Cascade-declared `tags` no longer disappear from a child note that declares any of its own. The metadata-cascade merger previously used `aeson-extra`'s `lodashMerge`, which aligns arrays by index — `tags: [team-doc]` in `folder.yaml` plus `tags: [internal-note]` in `folder/note.md` produced `[internal-note]`, dropping the cascaded entry both from the per-page chip strip and from the `#352` global tag index. The merger now treats cascade arrays as extensible defaults: arrays concatenate (with deduplication), objects deep-merge by key, scalars right-win. The change generalises to other list-valued fields (e.g. `pandoc.filters`). `aeson-extra` is no longer a dependency (closes [#697](https://github.com/srid/emanote/issues/697)). +- Cascade-declared `tags` no longer disappear from a child note that declares any of its own. The metadata-cascade merger previously inherited `aeson-extra`'s `lodashMerge`, which aligns arrays by index — `tags: [team-doc]` in `folder.yaml` plus `tags: [internal-note]` in `folder/note.md` produced `[internal-note]`, dropping the cascaded entry both from the per-page chip strip and from the `#352` global tag index. The merger now unions cascade arrays; see [yaml-config](docs/guide/yaml-config.md) for the full contract. `aeson-extra` is no longer a dependency (closes [#697](https://github.com/srid/emanote/issues/697)). - Wiki link custom titles now render HTML entities like ` ` the same way regular Markdown link labels do. Previously `[[note|Spivak (2014)]]` rendered the entity text literally as ` ` (closes [#441](https://github.com/srid/emanote/issues/441)). - Atom feed: a feed query that matches no notes no longer crashes the build; an empty-but-valid Atom document is emitted instead. Configuration errors (missing/invalid query block, missing `page.siteUrl`) still fail loudly ([#490](https://github.com/srid/emanote/issues/490), [#650](https://github.com/srid/emanote/pull/650)) - Markdown links to a static `.xml` asset (e.g. `[Test](./test.xml)`) now resolve to the file. Previously a `.xml` URL was always interpreted as the Atom feed of a same-named note, leaving asset links broken when no such feed-enabled note existed. The missing-link page now also tailors its "you may create…" hint to the URL extension instead of always suggesting `.md` / `.org` (closes [#547](https://github.com/srid/emanote/issues/547)) From edef4766d6a911fa49f00b2e380fc91037bd79f6 Mon Sep 17 00:00:00 2001 From: Sridhar Ratnakumar Date: Mon, 4 May 2026 13:02:34 -0400 Subject: [PATCH 5/5] docs(yaml-config): drop the #697 parenthetical from cascade merge section --- docs/guide/yaml-config.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guide/yaml-config.md b/docs/guide/yaml-config.md index d8fa8d535..e94999957 100644 --- a/docs/guide/yaml-config.md +++ b/docs/guide/yaml-config.md @@ -26,7 +26,7 @@ When a child route's frontmatter or YAML overlaps with values from parent YAMLs, - **Arrays** concatenate (with deduplication) — `tags: [team-doc]` in `folder.yaml` plus `tags: [internal-note]` on a child note yields `[team-doc, internal-note]`. The same applies to other list-valued fields like `pandoc.filters`. - **Scalars** right-win — the most-specific value (the leaf) overrides ancestors. -The array rule is what makes a parent YAML's `tags` survive to its children even when those children declare their own. (Prior to [#697](https://github.com/srid/emanote/issues/697), arrays were aligned by index, which silently clobbered cascaded entries.) +The array rule is what makes a parent YAML's `tags` survive to its children even when those children declare their own. ## Special properties