@cogenta/theme-docs 0.3.3 → 0.5.3

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.
Files changed (148) hide show
  1. package/dist/src/index.d.ts +2 -0
  2. package/dist/src/index.d.ts.map +1 -1
  3. package/dist/src/index.js +2 -0
  4. package/dist/src/index.js.map +1 -1
  5. package/dist/src/render/blocks/accordion.d.ts +8 -0
  6. package/dist/src/render/blocks/accordion.d.ts.map +1 -1
  7. package/dist/src/render/blocks/accordion.js +10 -10
  8. package/dist/src/render/blocks/accordion.js.map +1 -1
  9. package/dist/src/render/blocks/collection-list.d.ts +3 -11
  10. package/dist/src/render/blocks/collection-list.d.ts.map +1 -1
  11. package/dist/src/render/blocks/collection-list.js +69 -68
  12. package/dist/src/render/blocks/collection-list.js.map +1 -1
  13. package/dist/src/render/blocks/cta.d.ts +6 -0
  14. package/dist/src/render/blocks/cta.d.ts.map +1 -1
  15. package/dist/src/render/blocks/cta.js +9 -4
  16. package/dist/src/render/blocks/cta.js.map +1 -1
  17. package/dist/src/render/blocks/embed.d.ts.map +1 -1
  18. package/dist/src/render/blocks/embed.js +14 -13
  19. package/dist/src/render/blocks/embed.js.map +1 -1
  20. package/dist/src/render/blocks/faq.d.ts +7 -0
  21. package/dist/src/render/blocks/faq.d.ts.map +1 -1
  22. package/dist/src/render/blocks/faq.js +10 -10
  23. package/dist/src/render/blocks/faq.js.map +1 -1
  24. package/dist/src/render/blocks/feature-grid.d.ts.map +1 -1
  25. package/dist/src/render/blocks/feature-grid.js +19 -19
  26. package/dist/src/render/blocks/feature-grid.js.map +1 -1
  27. package/dist/src/render/blocks/gallery.d.ts +8 -4
  28. package/dist/src/render/blocks/gallery.d.ts.map +1 -1
  29. package/dist/src/render/blocks/gallery.js +19 -13
  30. package/dist/src/render/blocks/gallery.js.map +1 -1
  31. package/dist/src/render/blocks/hero.d.ts +12 -9
  32. package/dist/src/render/blocks/hero.d.ts.map +1 -1
  33. package/dist/src/render/blocks/hero.js +22 -20
  34. package/dist/src/render/blocks/hero.js.map +1 -1
  35. package/dist/src/render/blocks/logo-strip.d.ts +6 -0
  36. package/dist/src/render/blocks/logo-strip.d.ts.map +1 -1
  37. package/dist/src/render/blocks/logo-strip.js +6 -10
  38. package/dist/src/render/blocks/logo-strip.js.map +1 -1
  39. package/dist/src/render/blocks/logos.d.ts +10 -0
  40. package/dist/src/render/blocks/logos.d.ts.map +1 -1
  41. package/dist/src/render/blocks/logos.js +20 -17
  42. package/dist/src/render/blocks/logos.js.map +1 -1
  43. package/dist/src/render/blocks/media-figure.d.ts +0 -6
  44. package/dist/src/render/blocks/media-figure.d.ts.map +1 -1
  45. package/dist/src/render/blocks/media-figure.js +28 -16
  46. package/dist/src/render/blocks/media-figure.js.map +1 -1
  47. package/dist/src/render/blocks/pricing-table.d.ts.map +1 -1
  48. package/dist/src/render/blocks/pricing-table.js +16 -20
  49. package/dist/src/render/blocks/pricing-table.js.map +1 -1
  50. package/dist/src/render/blocks/prose.d.ts +13 -1
  51. package/dist/src/render/blocks/prose.d.ts.map +1 -1
  52. package/dist/src/render/blocks/prose.js +12 -31
  53. package/dist/src/render/blocks/prose.js.map +1 -1
  54. package/dist/src/render/blocks/quote.d.ts +5 -3
  55. package/dist/src/render/blocks/quote.d.ts.map +1 -1
  56. package/dist/src/render/blocks/quote.js +10 -14
  57. package/dist/src/render/blocks/quote.js.map +1 -1
  58. package/dist/src/render/blocks/stat-counter.d.ts +5 -0
  59. package/dist/src/render/blocks/stat-counter.d.ts.map +1 -1
  60. package/dist/src/render/blocks/stat-counter.js +6 -10
  61. package/dist/src/render/blocks/stat-counter.js.map +1 -1
  62. package/dist/src/render/blocks/stats.d.ts +10 -0
  63. package/dist/src/render/blocks/stats.d.ts.map +1 -1
  64. package/dist/src/render/blocks/stats.js +13 -8
  65. package/dist/src/render/blocks/stats.js.map +1 -1
  66. package/dist/src/render/blocks/testimonial.d.ts +4 -5
  67. package/dist/src/render/blocks/testimonial.d.ts.map +1 -1
  68. package/dist/src/render/blocks/testimonial.js +9 -13
  69. package/dist/src/render/blocks/testimonial.js.map +1 -1
  70. package/dist/src/render/chrome.d.ts +36 -1
  71. package/dist/src/render/chrome.d.ts.map +1 -1
  72. package/dist/src/render/chrome.js +122 -80
  73. package/dist/src/render/chrome.js.map +1 -1
  74. package/dist/src/render/doc-index.d.ts +25 -0
  75. package/dist/src/render/doc-index.d.ts.map +1 -0
  76. package/dist/src/render/doc-index.js +49 -0
  77. package/dist/src/render/doc-index.js.map +1 -0
  78. package/dist/src/render/doc-page.d.ts +6 -0
  79. package/dist/src/render/doc-page.d.ts.map +1 -0
  80. package/dist/src/render/doc-page.js +127 -0
  81. package/dist/src/render/doc-page.js.map +1 -0
  82. package/dist/src/render/layout.d.ts +23 -0
  83. package/dist/src/render/layout.d.ts.map +1 -0
  84. package/dist/src/render/layout.js +40 -0
  85. package/dist/src/render/layout.js.map +1 -0
  86. package/dist/src/render/render-block.d.ts +9 -6
  87. package/dist/src/render/render-block.d.ts.map +1 -1
  88. package/dist/src/render/render-block.js +47 -100
  89. package/dist/src/render/render-block.js.map +1 -1
  90. package/dist/src/render/rich-text.d.ts +26 -0
  91. package/dist/src/render/rich-text.d.ts.map +1 -0
  92. package/dist/src/render/rich-text.js +325 -0
  93. package/dist/src/render/rich-text.js.map +1 -0
  94. package/dist/src/render/search.d.ts +15 -0
  95. package/dist/src/render/search.d.ts.map +1 -0
  96. package/dist/src/render/search.js +31 -0
  97. package/dist/src/render/search.js.map +1 -0
  98. package/dist/src/render/strings.d.ts +51 -0
  99. package/dist/src/render/strings.d.ts.map +1 -0
  100. package/dist/src/render/strings.js +60 -0
  101. package/dist/src/render/strings.js.map +1 -0
  102. package/dist/src/render/term-archive.d.ts +5 -5
  103. package/dist/src/render/term-archive.d.ts.map +1 -1
  104. package/dist/src/render/term-archive.js +27 -16
  105. package/dist/src/render/term-archive.js.map +1 -1
  106. package/dist/src/render/widget-areas.d.ts +22 -0
  107. package/dist/src/render/widget-areas.d.ts.map +1 -0
  108. package/dist/src/render/widget-areas.js +29 -0
  109. package/dist/src/render/widget-areas.js.map +1 -0
  110. package/package.json +4 -4
  111. package/src/index.ts +2 -0
  112. package/src/render/blocks/accordion.ts +31 -42
  113. package/src/render/blocks/collection-list.ts +132 -123
  114. package/src/render/blocks/cta.ts +23 -18
  115. package/src/render/blocks/embed.ts +37 -28
  116. package/src/render/blocks/faq.ts +26 -37
  117. package/src/render/blocks/feature-grid.ts +34 -42
  118. package/src/render/blocks/gallery.ts +33 -29
  119. package/src/render/blocks/hero.ts +41 -45
  120. package/src/render/blocks/logo-strip.ts +21 -21
  121. package/src/render/blocks/logos.ts +32 -42
  122. package/src/render/blocks/media-figure.ts +47 -25
  123. package/src/render/blocks/pricing-table.ts +34 -41
  124. package/src/render/blocks/prose.ts +25 -40
  125. package/src/render/blocks/quote.ts +25 -19
  126. package/src/render/blocks/stat-counter.ts +27 -29
  127. package/src/render/blocks/stats.ts +35 -32
  128. package/src/render/blocks/testimonial.ts +18 -19
  129. package/src/render/chrome.ts +170 -83
  130. package/src/render/doc-index.ts +65 -0
  131. package/src/render/doc-page.ts +303 -0
  132. package/src/render/layout.ts +73 -0
  133. package/src/render/render-block.ts +67 -191
  134. package/src/render/rich-text.ts +446 -0
  135. package/src/render/search.ts +41 -0
  136. package/src/render/strings.ts +67 -0
  137. package/src/render/term-archive.ts +62 -38
  138. package/src/render/widget-areas.ts +33 -0
  139. package/src/styles/archive.css +21 -27
  140. package/src/styles/base.css +438 -838
  141. package/src/styles/blocks.css +658 -916
  142. package/src/styles/chrome.css +439 -0
  143. package/src/styles/docs.css +404 -0
  144. package/src/styles/rich.css +337 -0
  145. package/src/styles/theme.css +28 -23
  146. package/src/styles/tokens.css +209 -215
  147. package/src/styles/widgets.css +890 -0
  148. package/tokens.json +15 -15
@@ -0,0 +1,73 @@
1
+ import {
2
+ type Attributes,
3
+ blockHeadingTag,
4
+ type Child,
5
+ type HtmlElement,
6
+ h,
7
+ heading,
8
+ } from '@cogenta/theme-kit'
9
+
10
+ /**
11
+ * The frame every block of this theme renders into.
12
+ *
13
+ * The outer element carries the one section rhythm (`--cd-section`, the same
14
+ * for every block) and paints edge to edge when a variant asks for a band.
15
+ * The inner element is the twelve-column container, so a section title, a
16
+ * link, a figure and a caption all start on the same left edge. Inside a
17
+ * documentation page the same markup is laid out in the reading column
18
+ * instead (`docs.css`), so a block placed in an article needs nothing extra.
19
+ *
20
+ * `inner` is `figure` for a block whose semantics are a figure (a quotation
21
+ * and its attribution, an image and its caption): `<figcaption>` must be a
22
+ * direct child of its `<figure>`.
23
+ */
24
+ export function section(
25
+ outer: 'section' | 'div',
26
+ block: string,
27
+ className: string,
28
+ attrs: Attributes,
29
+ inner: 'div' | 'figure',
30
+ ...children: readonly Child[]
31
+ ): HtmlElement {
32
+ return h(
33
+ outer,
34
+ { class: `cd-section ${className}`, 'data-block': block, ...attrs },
35
+ h(inner, { class: `cd-container ${className}__inner` }, ...children),
36
+ )
37
+ }
38
+
39
+ /** A titled block's heading, from the first column, left-aligned with the content under it. */
40
+ export function sectionHead(blockName: string, title: string | undefined): HtmlElement | null {
41
+ if (title === undefined || title.trim() === '') return null
42
+ return h(
43
+ 'div',
44
+ { class: 'cd-head' },
45
+ heading(
46
+ blockHeadingTag(blockName) ?? 'h2',
47
+ { class: 'cd-head__title', 'data-field': 'title' },
48
+ title,
49
+ ),
50
+ )
51
+ }
52
+
53
+ /** A paragraph that renders only when the text is there: no empty `<p>` on the page. */
54
+ export function optionalText(
55
+ tag: string,
56
+ className: string,
57
+ value: string | undefined,
58
+ attrs: Attributes = {},
59
+ ): HtmlElement | null {
60
+ if (value === undefined || value.trim() === '') return null
61
+ return h(tag, { class: className, ...attrs }, value)
62
+ }
63
+
64
+ /** `Intl.DateTimeFormat`'s long date in the page's locale: "September 14, 2026". */
65
+ export function longDate(iso: string, locale: string): string {
66
+ try {
67
+ return new Intl.DateTimeFormat(locale, { dateStyle: 'long', timeZone: 'UTC' }).format(
68
+ new Date(iso),
69
+ )
70
+ } catch {
71
+ return iso.slice(0, 10)
72
+ }
73
+ }
@@ -1,15 +1,12 @@
1
1
  import { type BlockRegistry, VOCABULARY_NAMES, type VocabularyBlock } from '@cogenta/blocks'
2
2
  import {
3
- type ContentEntry,
4
- entryHref,
5
- entryTitle,
6
3
  type FetchedEntries,
7
4
  type HtmlElement,
8
5
  h,
9
6
  type PageContent,
10
7
  pageHasOwnHeading,
8
+ providedBlockNode,
11
9
  type RenderContext,
12
- renderEntryHeader,
13
10
  resolveBlockForRender,
14
11
  withBlockKey,
15
12
  withBlockVariant,
@@ -31,31 +28,56 @@ import { renderQuote } from './blocks/quote.js'
31
28
  import { renderStatCounter } from './blocks/stat-counter.js'
32
29
  import { renderStats } from './blocks/stats.js'
33
30
  import { renderTestimonial } from './blocks/testimonial.js'
31
+ import { isDocIndexBlock } from './doc-index.js'
32
+ import { renderDocPage } from './doc-page.js'
33
+ import { longDate } from './layout.js'
34
+ import { type HeadingAnchors, headingAnchors } from './rich-text.js'
34
35
 
35
36
  export type { FetchedEntries, PageContent }
36
37
 
37
- export function renderBlock(
38
+ function renderResolved(
38
39
  block: VocabularyBlock,
39
40
  ctx: RenderContext,
40
- entries: FetchedEntries = {},
41
- registry?: BlockRegistry,
41
+ entries: FetchedEntries,
42
+ registry: BlockRegistry | undefined,
43
+ anchors: HeadingAnchors,
42
44
  ): HtmlElement | null {
45
+ // A stored block is not always literally one of the shared vocabulary:
46
+ // resolving here turns an unimplemented theme-private block into its
47
+ // declared fallback instead of a silently blank slot.
43
48
  const resolved = resolveBlockForRender(block, VOCABULARY_NAMES, registry)
44
49
  if (resolved === null) return null
45
50
  const known = resolved as unknown as VocabularyBlock
46
- return withBlockVariant(renderKnownBlock(known, ctx, entries), known.variant)
51
+ // `variant` is envelope data every block carries identically, applied once
52
+ // here rather than by each of the seventeen renderers.
53
+ return withBlockVariant(renderKnownBlock(known, ctx, entries, anchors), known.variant)
54
+ }
55
+
56
+ export function renderBlock(
57
+ block: VocabularyBlock,
58
+ ctx: RenderContext,
59
+ entries: FetchedEntries = {},
60
+ registry?: BlockRegistry,
61
+ ): HtmlElement | null {
62
+ // A block a plugin provides arrives already rendered by the host, in a
63
+ // process of its own (contract D theme@1.7). Nothing else in this theme
64
+ // needs to know such blocks exist.
65
+ const provided = providedBlockNode(block, ctx)
66
+ if (provided !== null) return withBlockVariant(provided, block.variant)
67
+ return renderResolved(block, ctx, entries, registry, headingAnchors([block]))
47
68
  }
48
69
 
49
70
  function renderKnownBlock(
50
71
  known: VocabularyBlock,
51
72
  ctx: RenderContext,
52
73
  entries: FetchedEntries,
74
+ anchors: HeadingAnchors,
53
75
  ): HtmlElement | null {
54
76
  switch (known._type) {
55
77
  case 'hero':
56
78
  return renderHero(known, ctx)
57
79
  case 'prose':
58
- return renderProse(known, ctx)
80
+ return renderProse(known, ctx, anchors)
59
81
  case 'mediaFigure':
60
82
  return renderMediaFigure(known, ctx)
61
83
  case 'featureGrid':
@@ -87,6 +109,9 @@ function renderKnownBlock(
87
109
  case 'logoStrip':
88
110
  return renderLogoStrip(known, ctx)
89
111
  default: {
112
+ // Exhaustive over contract B: `known` is `never` here, so a block this
113
+ // package does not implement stops it compiling until it is. `null`
114
+ // rather than a throw: choosing a fallback is the render layer's job.
90
115
  const unreachable: never = known
91
116
  void unreachable
92
117
  return null
@@ -95,193 +120,43 @@ function renderKnownBlock(
95
120
  }
96
121
 
97
122
  /**
98
- * The name every doc page's own blueprint-seeded sidebar `collectionList`
99
- * targets — checked structurally (first block, this collection) rather than
100
- * by any flag on the block, because contract B carries no "this is chrome,
101
- * not content" bit and inventing one would be a contract change for a
102
- * single theme's layout choice.
103
- */
104
- const DOC_PAGE_COLLECTION = 'doc_page'
105
-
106
- function isDocSidebarBlock(block: VocabularyBlock): boolean {
107
- return block._type === 'collectionList' && block.collection === DOC_PAGE_COLLECTION
108
- }
109
-
110
- /** The sidebar entry whose own link resolves to the page being rendered — the "current page" the brief asks the sidebar to highlight and the breadcrumb to name. */
111
- function findCurrentEntry(
112
- entries: readonly ContentEntry[],
113
- ctx: RenderContext,
114
- ): ContentEntry | undefined {
115
- return entries.find((entry) => entryHref(entry, ctx) === ctx.url.pathname)
116
- }
117
-
118
- /**
119
- * The doc-page sidebar — grouped by each entry's own `section` field, in the
120
- * order the entries were fetched (the blueprint seeds them in `order`
121
- * within a section already; `collectionList.sort` cannot name `section`/
122
- * `order`, so an editor who reorders sections relies on the fetch order,
123
- * exactly like the "All guides" index on the home page does).
124
- */
125
- function renderSidebarPanel(
126
- entries: readonly ContentEntry[],
127
- ctx: RenderContext,
128
- currentHref: string,
129
- ): HtmlElement {
130
- const groups = new Map<string, ContentEntry[]>()
131
- for (const entry of entries) {
132
- const section =
133
- typeof entry.section === 'string' && entry.section !== ''
134
- ? entry.section
135
- : ctx.t('entry.untitled')
136
- const list = groups.get(section) ?? []
137
- list.push(entry)
138
- groups.set(section, list)
139
- }
140
-
141
- return h(
142
- 'nav',
143
- { class: 'cg-docs__nav-panel', 'aria-label': 'Documentation' },
144
- [...groups.entries()].map(([section, sectionEntries]) =>
145
- h(
146
- 'div',
147
- { class: 'cg-docs__nav-group' },
148
- h('p', { class: 'cg-docs__nav-heading' }, section),
149
- h(
150
- 'ul',
151
- { class: 'cg-docs__nav-items' },
152
- sectionEntries.map((entry) => {
153
- const href = entryHref(entry, ctx)
154
- const isCurrent = href === currentHref
155
- return h(
156
- 'li',
157
- {},
158
- h(
159
- 'a',
160
- {
161
- class: 'cg-docs__nav-link',
162
- href,
163
- 'aria-current': isCurrent ? 'page' : undefined,
164
- },
165
- entryTitle(entry, ctx),
166
- ),
167
- )
168
- }),
169
- ),
170
- ),
171
- ),
172
- )
173
- }
174
-
175
- /**
176
- * The sidebar, in two copies — the same zero-JS technique `chrome.ts` already
177
- * uses for the header's own mobile menu, and for the same reason: Chrome (and
178
- * every other engine implementing the current HTML rendering rules) hides a
179
- * closed `<details>`'s non-summary content through its own internal
180
- * `::details-content` box, not through the plain CSS `display: none` the spec
181
- * text describes — so a stylesheet rule that sets `display: block` on that
182
- * content, however specific, does not reliably bring it back while the
183
- * element itself stays closed. Verified live: a build with a single
184
- * `<details>` forced open only above the two-column breakpoint rendered an
185
- * empty sidebar column at 1280px in a real Chrome tab, despite every
186
- * computed style reporting `display: block`.
187
- *
188
- * The fix is the same one already proven for the header: a plain, always-
189
- * live `<nav>` for the desktop column (nothing to collapse, so nothing for
190
- * the browser to hide), and a **separate** `<details>` disclosure — a full
191
- * second copy of the same panel — for narrow viewports. Exactly one of the
192
- * two is ever `display: block` at a given viewport width (`base.css`), so a
193
- * screen reader is never offered two "Documentation" navigations at once,
194
- * matching the header's own accessibility guarantee.
195
- */
196
- function renderSidebar(
197
- entries: readonly ContentEntry[],
198
- ctx: RenderContext,
199
- currentHref: string,
200
- ): HtmlElement {
201
- return h('div', { class: 'cg-docs__nav-desktop' }, renderSidebarPanel(entries, ctx, currentHref))
202
- }
203
-
204
- function renderSidebarMobile(
205
- entries: readonly ContentEntry[],
206
- ctx: RenderContext,
207
- currentHref: string,
208
- ): HtmlElement {
209
- return h(
210
- 'details',
211
- { class: 'cg-docs__nav-mobile' },
212
- h('summary', { class: 'cg-docs__nav-toggle' }, 'On this site'),
213
- renderSidebarPanel(entries, ctx, currentHref),
214
- )
215
- }
216
-
217
- function renderBreadcrumb(section: string | undefined, title: string): HtmlElement {
218
- return h(
219
- 'nav',
220
- { class: 'cg-docs__breadcrumb', 'aria-label': 'Breadcrumb' },
221
- h(
222
- 'ol',
223
- {},
224
- section === undefined ? null : h('li', {}, section),
225
- h('li', { 'aria-current': 'page' }, title),
226
- ),
227
- )
228
- }
229
-
230
- /**
231
- * A doc page: two columns, CSS-only. The sidebar comes from the page's own
232
- * *first* block — a `collectionList` on `doc_page` the blueprint seeds on
233
- * every doc page for exactly this purpose — rendered as navigation rather
234
- * than as a card list, and dropped from the ordinary block stream so it
235
- * never also appears as content. The remaining blocks render in the content
236
- * column, unchanged.
123
+ * The opening of a page outside the documentation that has no hero: its
124
+ * title, its summary, and the date it was published when it has one.
237
125
  */
238
- function renderDocPage(
239
- page: PageContent,
240
- ctx: RenderContext,
241
- sidebarBlock: VocabularyBlock,
242
- entries: FetchedEntries,
243
- registry: BlockRegistry | undefined,
244
- ): HtmlElement {
245
- const sidebarEntries = entries[sidebarBlock._key] ?? []
246
- const currentHref = ctx.url.pathname
247
- const current = findCurrentEntry(sidebarEntries, ctx)
248
- const section = typeof current?.section === 'string' ? current.section : undefined
249
- const contentBlocks = page.blocks.slice(1)
250
- const hasOwnHeading = pageHasOwnHeading(contentBlocks)
251
- const entryHeader = renderEntryHeader(page, ctx)
252
-
126
+ function renderPageHead(page: PageContent, ctx: RenderContext): HtmlElement {
127
+ const published = page.entry?.publishedAt
253
128
  return h(
254
- 'main',
255
- { class: 'cg-main cg-docs', id: 'cg-main' },
256
- withBlockKey(
257
- h(
258
- 'div',
259
- { class: 'cg-docs__nav' },
260
- renderSidebar(sidebarEntries, ctx, currentHref),
261
- renderSidebarMobile(sidebarEntries, ctx, currentHref),
262
- ),
263
- sidebarBlock._key,
264
- ),
129
+ 'header',
130
+ { class: 'cd-page-head' },
265
131
  h(
266
132
  'div',
267
- { class: 'cg-docs__content' },
268
- renderBreadcrumb(section, page.title),
269
- entryHeader ?? (hasOwnHeading ? null : h('h1', { class: 'cg-docs__title' }, page.title)),
270
- contentBlocks.map((block) =>
271
- withBlockKey(renderBlock(block, ctx, entries, registry), block._key),
272
- ),
133
+ { class: 'cd-container cd-page-head__inner' },
134
+ h('h1', { class: 'cd-page-head__title' }, page.title),
135
+ page.entry?.excerpt === undefined
136
+ ? null
137
+ : h('p', { class: 'cd-page-head__lead' }, page.entry.excerpt),
138
+ published === undefined
139
+ ? null
140
+ : h(
141
+ 'p',
142
+ { class: 'cd-page-head__meta' },
143
+ h('time', { datetime: published }, longDate(published, ctx.locale)),
144
+ ),
273
145
  ),
274
146
  )
275
147
  }
276
148
 
277
149
  /**
278
- * `<main id="cg-main">` is mandatory: it is the skip-link's target, written
279
- * once by `@cogenta/cli`'s own render path.
150
+ * `<main id="cg-main">` is mandatory: it is the skip link's target, written
151
+ * by the host outside any theme's control.
152
+ *
153
+ * A page whose first block is the documentation's own index on `doc_page` is
154
+ * a documentation page (`doc-page.ts`); every other page is laid out on the
155
+ * twelve-column grid. Exactly one `h1` on every page: a hero carries it when
156
+ * there is one, the page head otherwise.
280
157
  *
281
- * `withBlockKey` stamps every rendered block with its contract-B `_key`, on
282
- * every page including a doc page's content column — the visual page
283
- * builder maps a clicked element back to the block that produced it, for
284
- * this theme exactly as for every other one.
158
+ * `withBlockKey` stamps every rendered block with its contract-B `_key`, so
159
+ * the visual page builder (L16) can map a clicked element back to its block.
285
160
  */
286
161
  export function renderPage(
287
162
  page: PageContent,
@@ -290,16 +165,17 @@ export function renderPage(
290
165
  registry?: BlockRegistry,
291
166
  ): HtmlElement {
292
167
  const first = page.blocks[0]
293
- if (first !== undefined && isDocSidebarBlock(first)) {
294
- return renderDocPage(page, ctx, first, entries, registry)
168
+ if (first !== undefined && isDocIndexBlock(first)) {
169
+ return renderDocPage(page, ctx, first, entries, registry, renderResolved)
295
170
  }
296
171
 
172
+ const anchors = headingAnchors(page.blocks)
297
173
  return h(
298
174
  'main',
299
- { class: 'cg-main', id: 'cg-main' },
300
- pageHasOwnHeading(page.blocks) ? null : h('h1', { class: 'cg-page__title' }, page.title),
175
+ { class: 'cg-main cd-main', id: 'cg-main' },
176
+ pageHasOwnHeading(page.blocks) ? null : renderPageHead(page, ctx),
301
177
  page.blocks.map((block) =>
302
- withBlockKey(renderBlock(block, ctx, entries, registry), block._key),
178
+ withBlockKey(renderResolved(block, ctx, entries, registry, anchors), block._key),
303
179
  ),
304
180
  )
305
181
  }