@@ -38,14 +38,16 @@ YAML scalar.
3838The trap is to reach for frontmatter for everything
3939because it's structured. A 60-character ` tagline ` in
4040frontmatter and the same 60 characters in a
41- ` ## Tagline ` body section produce identical JSON;
42- the body version is shorter to edit, diffs cleanly
43- when wrapped, and is lintable as Markdown.
41+ ` ## Tagline ` body section project the same string;
42+ only the key moves, from ` frontmatter.tagline ` to
43+ ` tagline.text ` . The body version is shorter to edit,
44+ diffs cleanly when wrapped, and is lintable as
45+ Markdown.
4446
4547## Worked example
4648
47- A product-copy file with a tagline, a lead, and one
48- per-surface description.
49+ A product-copy file at ` docs/copy/product.md ` with a
50+ tagline, a lead, and one per-surface description.
4951
5052### Frontmatter-heavy (the trap)
5153
@@ -101,33 +103,58 @@ Inline diagnostics, fix-on-save, and instant
101103navigation for Markdown in VS Code.
102104```
103105
104- With a matching schema in ` .mdsmith.yml ` :
106+ With a matching schema and kind assignment in
107+ ` .mdsmith.yml ` :
105108
106109``` yaml
107110kinds :
108111 product-copy :
109112 schema :
110113 sections :
111114 - heading : { regex: '^Tagline$' }
115+ content :
116+ - { kind: paragraph }
112117 - heading : { regex: '^Lead$' }
118+ content :
119+ - { kind: paragraph }
113120 - heading : { regex: '^VS Code$' }
114121 bind : vscode-description
122+ content :
123+ - { kind: paragraph }
124+ kind-assignment :
125+ - glob : ["docs/copy/product.md"]
126+ kinds : [product-copy]
115127` ` `
116128
117- ` mdsmith extract product-copy --format json` emits
118- the same shape both encodings would produce :
129+ Each ` content:` entry declares the paragraph its
130+ section projects. A section without one projects as
131+ an empty object — the schema, not the body, decides
132+ what `extract` emits.
133+
134+ ` mdsmith extract product-copy --format json docs/copy/product.md`
135+ emits :
119136
120137` ` ` json
121138{
122- "frontmatter": { "title": "Product copy" },
123- "tagline": { "text": "Mark down your ideas; smith them into shipping docs." },
124- "lead": { "text": "A lint-and-fix tool that keeps your Markdown consistent across every surface — READMEs, docs site, editor extensions." },
125- "vscode-description": { "text": "Inline diagnostics, fix-on-save, and instant navigation for Markdown in VS Code." }
139+ "frontmatter": {
140+ "title": "Product copy"
141+ },
142+ "lead": {
143+ "text": "A lint-and-fix tool that keeps your Markdown consistent across every surface — READMEs, docs site, editor extensions."
144+ },
145+ "tagline": {
146+ "text": "Mark down your ideas; smith them into shipping docs."
147+ },
148+ "vscode-description": {
149+ "text": "Inline diagnostics, fix-on-save, and instant navigation for Markdown in VS Code."
150+ }
126151}
127152` ` `
128153
129- The body version costs nothing at the projection
130- layer and is the editable artifact.
154+ Keys come out sorted, not in document order. The
155+ consumer reads the same strings the frontmatter
156+ version held, and the body version is the editable
157+ artifact.
131158
132159# # Projecting inline structure
133160
@@ -165,21 +192,36 @@ kinds:
165192 - { kind: paragraph, projection: inline, required: true }
166193` ` `
167194
168- ` mdsmith extract product-copy --format json` emits the
169- headline as a span list — text, then the level-1
170- emphasis span with its own `children`, then the
171- trailing text :
195+ ` mdsmith extract product-copy --format json docs/copy/product.md `
196+ emits the headline as a span list: text, then the
197+ level-1 emphasis span with its own `children`, then
198+ the trailing text :
172199
173200` ` ` json
174201{
175- "frontmatter": { "title": "Product copy" },
202+ "frontmatter": {
203+ "title": "Product copy"
204+ },
176205 "headline": {
177206 "inline": [
178- { "span": "text", "value": "Mark" },
179- { "span": "emphasis", "level": 1, "children": [
180- { "span": "text", "value": "down" }
181- ]},
182- { "span": "text", "value": ", smithed." }
207+ {
208+ "span": "text",
209+ "value": "Mark"
210+ },
211+ {
212+ "children": [
213+ {
214+ "span": "text",
215+ "value": "down"
216+ }
217+ ],
218+ "level": 1,
219+ "span": "emphasis"
220+ },
221+ {
222+ "span": "text",
223+ "value": ", smithed."
224+ }
183225 ]
184226 }
185227}
@@ -194,9 +236,11 @@ recursive mode switch:
194236` ` ` json
195237"inline": [
196238 { "span": "text", "value": "run " },
197- { "span": "strong", "level": 2, "children": [
198- { "span": "code", "value": "mdsmith fix" }
199- ]},
239+ {
240+ "children": [{ "span": "code", "value": "mdsmith fix" }],
241+ "level": 2,
242+ "span": "strong"
243+ },
200244 { "span": "text", "value": " daily" }
201245]
202246` ` `
@@ -237,6 +281,81 @@ than one line, and anything that benefits from
237281Markdown formatting (code, emphasis, links) all
238282belong in the body.
239283
284+ # # Frontmatter `title` and the H1
285+
286+ The worked example carries the same string twice :
287+ `title : Product copy` in frontmatter and
288+ ` # Product copy` as the H1. Nothing checks the two
289+ against each other by default, so they can drift
290+ apart edit by edit.
291+
292+ The test from the previous section decides it. When
293+ no catalog row, site template, or release script
294+ reads `frontmatter.title`, delete the field; the H1
295+ alone is the title. When a tool does read the
296+ field, keep it and let MDS020 enforce the match.
297+
298+ Enforcement needs a file-based schema. An inline
299+ ` schema:` starts matching at H2 — the H1 belongs to
300+ [first-line-heading][mds004] — so the kind switches
301+ to a `proto.md` whose first row is the `{title}`
302+ placeholder :
303+
304+ ` ` ` markdown
305+ # {title}
306+
307+ ## ...
308+ ` ` `
309+
310+ ` ` ` yaml
311+ kinds:
312+ product-copy:
313+ rules:
314+ required-structure:
315+ schema: copy-proto.md
316+ ` ` `
317+
318+ The `{title}` row requires the frontmatter field
319+ and checks the H1 text against its value. A drifted
320+ H1 fails `mdsmith check` :
321+
322+ ` ` ` text
323+ docs/copy/product.md:4:1 MDS020 heading does not match frontmatter: expected "Product copy" (from title), got "Product page copy"
324+ ` ` `
325+
326+ The synced H1 also becomes data. `mdsmith extract`
327+ projects the H1 scope under a `title` key, with the
328+ captured heading text inside :
329+
330+ ` ` ` json
331+ {
332+ "frontmatter": {
333+ "title": "Product copy"
334+ },
335+ "title": {
336+ "title": "Product copy"
337+ }
338+ }
339+ ` ` `
340+
341+ Weigh two limits before switching. Every schema
342+ source on a file must declare the same root level,
343+ so an H1-rooted `proto.md` cannot compose with an
344+ H2-rooted inline schema on the same file. And a
345+ ` proto.md` declares heading rows only, not
346+ ` content:` entries, so the worked example's
347+ paragraph projections (`tagline.text`, …) drop out
348+ of the tree.
349+
350+ When the kind's main job is extraction, keep the
351+ inline schema and delete the frontmatter field
352+ instead. mdsmith cannot project the H1 text without
353+ a frontmatter field behind it : a `{title}` row with
354+ no `title` field matches any heading, and `extract`
355+ skips wildcard scopes.
356+
357+ [mds004] : ../../internal/rules/MDS004-first-line-heading/README.md
358+
240359# # `bind:` patterns
241360
242361` bind:` renames the JSON key that a heading or
@@ -281,13 +400,14 @@ embed reads the tagline directly:
281400file: docs/copy/product.md
282401extract: tagline.text
283402?>
284- Mark down your ideas; smith them into shipping
285- docs.
403+ Mark down your ideas; smith them into shipping docs.
286404<?/include?>
287405` ` `
288406
289- The directive runs the included file through the
290- same projection rules `mdsmith extract` produces,
407+ The spliced text lands on one line : a `text`
408+ projection joins a soft-wrapped paragraph with
409+ spaces. The directive runs the included file through
410+ the same projection rules `mdsmith extract` uses,
291411walks the dotted path, and splices the leaf. There
292412is no intermediate "fragment" file to keep in sync —
293413the README reads the source of truth on every lint.
0 commit comments