@@ -32,6 +32,7 @@ import type { Provider } from "@metaobjectsdev/render";
3232import { GENERATED_HEADER } from "../constants.js" ;
3333import type { OutputLayout } from "../import-path.js" ;
3434import { docPageHref , docPageNode , type DocPageNode } from "../docs-paths.js" ;
35+ import { fieldAnchorSlug } from "./field-anchor.js" ;
3536import type { TemplateDocData , TemplateOutputPart } from "./template-doc-data.js" ;
3637import { buildEnrichedPayloadTree } from "./template-payload-tree.js" ;
3738import { annotateTemplate , type AnnotatePayloadField } from "./template-source-annotate.js" ;
@@ -174,6 +175,8 @@ export function buildTemplateDocData(
174175 const section = buildTemplateSourceSection ( {
175176 provider : opts . provider ,
176177 root,
178+ layout,
179+ template,
177180 payloadName,
178181 // Document: the single @textRef (unlabeled). Email: one labeled part each.
179182 refs :
@@ -202,6 +205,12 @@ interface SourceRefSpec {
202205interface BuildSectionArgs {
203206 provider : Provider ;
204207 root : MetaRoot ;
208+ /** Page-placement layout — threaded so field/partial hrefs route through the
209+ * SAME docPageHref the Payload cross-link uses (resolves under package layout). */
210+ layout : OutputLayout ;
211+ /** The `template.output` node whose page is being built — the FROM page for
212+ * every relative href on this section. */
213+ template : MetaData ;
205214 payloadName : string ;
206215 refs : SourceRefSpec [ ] ;
207216}
@@ -217,9 +226,23 @@ interface BuildSectionArgs {
217226 * resolved, so the caller omits the section entirely.
218227 */
219228function buildTemplateSourceSection ( args : BuildSectionArgs ) : string | undefined {
220- const { provider, root, payloadName, refs } = args ;
229+ const { provider, root, layout , template , payloadName, refs } = args ;
221230 const tree : AnnotatePayloadField [ ] = buildEnrichedPayloadTree ( root , payloadName ) ;
222- const resolvePartialHref = makePartialHrefResolver ( root ) ;
231+ const fromNode = docPageNode ( template ) ;
232+
233+ // Layout-aware field href: route through the SAME docPageHref the Payload
234+ // cross-link uses, so the link lands on the owner VO's REAL page in BOTH
235+ // layouts (flat → `./Owner.md`, package → `../<pkg>/Owner.md`). Resolve the
236+ // owner's package off the root (by short name); fall back to a package-less
237+ // node when it can't be resolved — mirroring the Payload link's fallback.
238+ const fieldHref = ( owner : string , name : string ) : string => {
239+ const ownerObj = root . findObject ( owner ) ;
240+ const toNode : DocPageNode =
241+ ownerObj !== undefined ? docPageNode ( ownerObj ) : { name : owner } ;
242+ return `${ docPageHref ( layout , fromNode , toNode ) } #${ fieldAnchorSlug ( name ) } ` ;
243+ } ;
244+
245+ const resolvePartialHref = makePartialHrefResolver ( root , layout , fromNode ) ;
223246 const isMultipart = refs . length > 1 || refs . some ( ( r ) => r . label !== undefined ) ;
224247
225248 const blocks : string [ ] = [ ] ;
@@ -229,6 +252,7 @@ function buildTemplateSourceSection(args: BuildSectionArgs): string | undefined
229252 const tokens = annotateTemplate ( source , tree , {
230253 ownerVoName : payloadName ,
231254 resolvePartialHref,
255+ fieldHref,
232256 } ) ;
233257 const parts : string [ ] = [ ] ;
234258 if ( isMultipart && spec . label !== undefined ) parts . push ( `### ${ spec . label } ` ) ;
@@ -244,15 +268,20 @@ function buildTemplateSourceSection(args: BuildSectionArgs): string | undefined
244268}
245269
246270/**
247- * A `{{>ref}}` partial-href resolver: returns `./<TemplateName>.md` when `ref`
248- * is a mustache source documented by SOME `template.output` node (it is one of
249- * that template's source refs — @textRef or an email part ref), else undefined
250- * (the partial is highlight-only). This makes a partial that inlines another
251- * documented template link to that template's page.
271+ * A `{{>ref}}` partial-href resolver: returns a relative href to the page of the
272+ * `template.output` node that documents `ref` (it is one of that template's
273+ * source refs — @textRef or an email part ref), else undefined (the partial is
274+ * highlight-only). The href routes through the SAME `docPageHref(layout, …)` the
275+ * field/Payload links use, so it resolves in BOTH layouts (flat → `./Name.md`,
276+ * package → a correct relative path like `../comms/OrderEmail.md`).
252277 */
253- function makePartialHrefResolver ( root : MetaRoot ) : ( ref : string ) => string | undefined {
254- // Map each documented source ref → the template node's short name.
255- const refToTemplate = new Map < string , string > ( ) ;
278+ function makePartialHrefResolver (
279+ root : MetaRoot ,
280+ layout : OutputLayout ,
281+ fromNode : DocPageNode ,
282+ ) : ( ref : string ) => string | undefined {
283+ // Map each documented source ref → the template node that documents it.
284+ const refToTemplate = new Map < string , MetaData > ( ) ;
256285 for ( const child of root . ownChildren ( ) ) {
257286 if ( child . type !== TYPE_TEMPLATE || child . subType !== TEMPLATE_SUBTYPE_OUTPUT ) continue ;
258287 for ( const attr of [
@@ -263,12 +292,12 @@ function makePartialHrefResolver(root: MetaRoot): (ref: string) => string | unde
263292 ] ) {
264293 const v = child . ownAttr ( attr ) ;
265294 if ( typeof v === "string" && v . length > 0 && ! refToTemplate . has ( v ) ) {
266- refToTemplate . set ( v , child . name ) ;
295+ refToTemplate . set ( v , child ) ;
267296 }
268297 }
269298 }
270299 return ( ref : string ) => {
271- const name = refToTemplate . get ( ref ) ;
272- return name !== undefined ? `./ ${ name } .md` : undefined ;
300+ const node = refToTemplate . get ( ref ) ;
301+ return node !== undefined ? docPageHref ( layout , fromNode , docPageNode ( node ) ) : undefined ;
273302 } ;
274303}
0 commit comments