Skip to content

Commit afa8006

Browse files
fix: resolve shortcodes in website.title/description for llms.txt (#14239)
The metadata markdown pipeline in website-meta.ts now writes resolved (shortcode-expanded) site title and description back into format.metadata during per-page rendering. These values flow through ProjectOutputFile.format to the post-render phase, where updateLlmsTxt reads them from outputFiles[0].format.metadata. Also adds a smoke test with an env shortcode in website.title that verifies llms.txt contains the resolved value. Fixes #14237
1 parent 0d4dfe5 commit afa8006

File tree

6 files changed

+101
-8
lines changed

6 files changed

+101
-8
lines changed

src/project/types/website/website-llms.ts

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,16 +24,24 @@ import {
2424
websiteTitle,
2525
} from "./website-config.ts";
2626
import { inputFileHref } from "./website-shared.ts";
27-
import { isDraftVisible, isProjectDraft, projectDraftMode } from "./website-utils.ts";
27+
import {
28+
isDraftVisible,
29+
isProjectDraft,
30+
projectDraftMode,
31+
} from "./website-utils.ts";
2832
import { resolveInputTargetForOutputFile } from "../../project-index.ts";
29-
import { Format } from "../../../config/types.ts";
33+
import { Format, Metadata } from "../../../config/types.ts";
34+
import { kWebsite } from "./website-constants.ts";
3035

3136
/**
3237
* Compute the output HTML file path from the source file.
3338
* Uses inputFileHref to convert the relative source path to an HTML href,
3439
* then joins with the output directory.
3540
*/
36-
function computeOutputFilePath(source: string, project: ProjectContext): string {
41+
function computeOutputFilePath(
42+
source: string,
43+
project: ProjectContext,
44+
): string {
3745
const outputDir = projectOutputDir(project);
3846
const sourceRelative = relative(project.dir, source);
3947
// inputFileHref returns "/path/to/file.html" - strip leading / and join with output dir
@@ -176,7 +184,10 @@ ${main.innerHTML}
176184
* Restores original code text (with annotation markers) and converts
177185
* the annotation definition list to an ordered list.
178186
*/
179-
function preprocessAnnotatedCodeBlocks(doc: Document, container: Element): void {
187+
function preprocessAnnotatedCodeBlocks(
188+
doc: Document,
189+
container: Element,
190+
): void {
180191
// Restore original code text in annotated code blocks.
181192
// The llms-code-annotations.lua filter saves the original text
182193
// (before code-annotation.lua strips markers) as a data attribute.
@@ -301,8 +312,17 @@ export async function updateLlmsTxt(
301312
return;
302313
}
303314

304-
const siteTitle = websiteTitle(context.config) || "Untitled";
305-
const siteDesc = websiteDescription(context.config) || "";
315+
// Read resolved site title/description from the first output file's format
316+
// metadata. The metadata markdown pipeline in website-meta.ts writes resolved
317+
// values (with shortcodes expanded) back into format.metadata during per-page
318+
// rendering, and these flow through to ProjectOutputFile.format.
319+
const firstFileMeta = outputFiles.length > 0
320+
? outputFiles[0].format.metadata[kWebsite] as Metadata | undefined
321+
: undefined;
322+
const siteTitle = (firstFileMeta?.title as string) ||
323+
websiteTitle(context.config) || "Untitled";
324+
const siteDesc = (firstFileMeta?.description as string) ||
325+
websiteDescription(context.config) || "";
306326
const baseUrl = websiteBaseurl(context.config);
307327
const draftMode = projectDraftMode(context);
308328

@@ -338,7 +358,9 @@ export async function updateLlmsTxt(
338358
// Extract title from the format metadata or use filename
339359
const title = (file.format.metadata?.title as string) ||
340360
basename(file.file, ".html");
341-
const relativePath = pathWithForwardSlashes(relative(outputDir, llmsPath));
361+
const relativePath = pathWithForwardSlashes(
362+
relative(outputDir, llmsPath),
363+
);
342364
const filePath = baseUrl
343365
? (baseUrl.endsWith("/") ? baseUrl : baseUrl + "/") + relativePath
344366
: relativePath;

src/project/types/website/website-meta.ts

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -431,6 +431,8 @@ const kTwitterDesc = "quarto-twittercarddesc";
431431
const kOgTitle = "quarto-ogcardtitle";
432432
const kOgDesc = "quarto-ogcardddesc";
433433
const kMetaSideNameId = "quarto-metasitename";
434+
const kMetaSiteDescId = "quarto-metasitedesc";
435+
434436
function metaMarkdownPipeline(format: Format, extras: FormatExtras) {
435437
const resolvedTitle = computePageTitle(format);
436438

@@ -504,7 +506,13 @@ function metaMarkdownPipeline(format: Format, extras: FormatExtras) {
504506
processRendered(rendered: Record<string, Element>, doc: Document) {
505507
const renderedEl = rendered[kMetaSideNameId];
506508
if (renderedEl) {
507-
// Update the document title
509+
// Write resolved title back into format.metadata so it flows
510+
// through to ProjectOutputFile.format for post-render consumers
511+
const siteMeta = format.metadata[kWebsite] as Metadata;
512+
if (siteMeta) {
513+
siteMeta[kTitle] = renderedEl.innerText;
514+
}
515+
// Update the og:site_name meta tag
508516
const el = doc.querySelector(
509517
`meta[property="og:site_name"]`,
510518
);
@@ -555,9 +563,32 @@ function metaMarkdownPipeline(format: Format, extras: FormatExtras) {
555563
},
556564
};
557565

566+
const siteDescriptionHandler = {
567+
getUnrendered() {
568+
const siteMeta = format.metadata[kWebsite] as Metadata;
569+
if (siteMeta && siteMeta[kDescription]) {
570+
return {
571+
inlines: { [kMetaSiteDescId]: siteMeta[kDescription] as string },
572+
};
573+
}
574+
},
575+
processRendered(rendered: Record<string, Element>) {
576+
const renderedEl = rendered[kMetaSiteDescId];
577+
if (renderedEl) {
578+
// Write resolved description back into format.metadata so it flows
579+
// through to ProjectOutputFile.format for post-render consumers
580+
const siteMeta = format.metadata[kWebsite] as Metadata;
581+
if (siteMeta) {
582+
siteMeta[kDescription] = renderedEl.innerText;
583+
}
584+
}
585+
},
586+
};
587+
558588
return createMarkdownPipeline("quarto-meta-markdown", [
559589
titleMetaHandler,
560590
siteTitleMetaHandler,
561591
descriptionMetaHandler,
592+
siteDescriptionHandler,
562593
]);
563594
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
/.quarto/
2+
*.html
3+
*.llms.md
4+
llms.txt
5+
search.json
6+
site_libs/
7+
*_files/
8+
9+
**/*.quarto_ipynb
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
LLMS_TEST_VAR="Resolved"
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
project:
2+
type: website
3+
output-dir: .
4+
5+
website:
6+
title: "Site Title {{< env LLMS_TEST_VAR >}}"
7+
llms-txt: true
8+
navbar:
9+
left:
10+
- href: index.qmd
11+
text: Home
12+
13+
format:
14+
html:
15+
theme: cosmo
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
title: "Home"
3+
_quarto:
4+
render-project: true
5+
tests:
6+
html:
7+
ensureLlmsTxtExists: true
8+
ensureLlmsTxtRegexMatches:
9+
- ["^# Site Title Resolved"]
10+
- ["\\{\\{< env"]
11+
---
12+
13+
## Test Content
14+
15+
This tests that shortcodes in website.title are resolved in llms.txt.

0 commit comments

Comments
 (0)