Skip to content

Commit 1104a8e

Browse files
committed
Add support for canonical-url
The canonical url provides a way for users to either provide an explicit canonical url or to have Quarto automatically generate a canonical url. See discussion #3976 Fixes #4882
1 parent 8d0a8ef commit 1104a8e

File tree

11 files changed

+175
-21
lines changed

11 files changed

+175
-21
lines changed

news/changelog-1.4.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
- Add support for showing cross reference contents on hover (use `crossrefs-hover: false` to disable).
2020
- Add support for displaying `keywords` in HTML page title block, when present.
2121
- ([#3473](https://github.com/quarto-dev/quarto-cli/issues/3473)): Add support for `body-right` and `body-left` layouts for Website Table of Contents.
22+
- ([#4882](https://github.com/quarto-dev/quarto-cli/issues/4882)): Add support for `canonical-url`, which when provided will include a link tag with rel='canonical' which will use an explictly provided or automatically generated canonical url for the document.
2223
- ([#5189](https://github.com/quarto-dev/quarto-cli/issues/5189)): Ensure appendix shows even when `page-layout` is custom.
2324
- ([#5210](https://github.com/quarto-dev/quarto-cli/issues/5210)): Update to Bootstrap 5.2.2
2425
- ([#5393](https://github.com/quarto-dev/quarto-cli/issues/5393)): Properly set color of headings without using opacity.

src/config/constants.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ export const kNotebookPreserveCells = "notebook-preserve-cells";
5454
export const kClearCellOptions = "clear-cell-options";
5555
export const kDownloadUrl = "download-url";
5656
export const kLightbox = "lightbox";
57+
export const kCanonicalUrl = "canonical-url";
5758

5859
export const kMath = "math";
5960

@@ -223,6 +224,7 @@ export const kRenderDefaultsKeys = [
223224
kClearCellOptions,
224225
kHtmlTableProcessing,
225226
kValidateYaml,
227+
kCanonicalUrl,
226228
];
227229

228230
// language fields

src/config/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
kCalloutNoteCaption,
1818
kCalloutTipCaption,
1919
kCalloutWarningCaption,
20+
kCanonicalUrl,
2021
kCiteMethod,
2122
kCiteproc,
2223
kClearCellOptions,
@@ -477,6 +478,7 @@ export interface FormatRender {
477478
[kIpynbProduceSourceNotebook]?: boolean;
478479
[kHtmlTableProcessing]?: "none";
479480
[kValidateYaml]?: boolean;
481+
[kCanonicalUrl]?: boolean | string;
480482
}
481483

482484
export interface FormatExecute {

src/format/html/format-html-meta.ts

Lines changed: 47 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
/*
2-
* format-html-meta.ts
3-
*
4-
* Copyright (C) 2020-2022 Posit Software, PBC
5-
*
6-
*/
2+
* format-html-meta.ts
3+
*
4+
* Copyright (C) 2020-2022 Posit Software, PBC
5+
*/
76

87
import { kHtmlEmptyPostProcessResult } from "../../command/render/constants.ts";
98
import {
109
PandocInputTraits,
1110
RenderedFormat,
1211
} from "../../command/render/types.ts";
12+
import { kCanonicalUrl } from "../../config/constants.ts";
1313
import { Format, Metadata } from "../../config/types.ts";
1414
import { bibliographyCslJson } from "../../core/bibliography.ts";
1515
import {
@@ -23,8 +23,11 @@ import {
2323
import { Document } from "../../core/deno-dom.ts";
2424
import { encodeAttributeValue } from "../../core/html.ts";
2525
import { kWebsite } from "../../project/types/website/website-constants.ts";
26-
import { documentCSL } from "../../quarto-core/attribution/document.ts";
27-
import { writeMetaTag } from "./format-html-shared.ts";
26+
import {
27+
documentCSL,
28+
synthesizeCitationUrl,
29+
} from "../../quarto-core/attribution/document.ts";
30+
import { writeLinkTag, writeMetaTag } from "./format-html-shared.ts";
2831

2932
export const kGoogleScholar = "google-scholar";
3033

@@ -38,6 +41,18 @@ export function metadataPostProcessor(
3841
inputTraits: PandocInputTraits;
3942
renderedFormats: RenderedFormat[];
4043
}) => {
44+
// Generate a canonical tag if requested
45+
if (format.render[kCanonicalUrl]) {
46+
writeCanonicalUrl(
47+
doc,
48+
format.render[kCanonicalUrl],
49+
input,
50+
options.inputMetadata,
51+
format.pandoc["output-file"],
52+
offset,
53+
);
54+
}
55+
4156
if (googleScholarEnabled(format)) {
4257
const { csl, extras } = documentCSL(
4358
input,
@@ -269,3 +284,28 @@ function metadataWriter(metadata: MetaTagData[]) {
269284
};
270285
return write;
271286
}
287+
288+
function writeCanonicalUrl(
289+
doc: Document,
290+
url: string | boolean,
291+
input: string,
292+
inputMetadata: Metadata,
293+
outputFile?: string,
294+
offset?: string,
295+
) {
296+
if (typeof url === "string") {
297+
// Use the explicitly provided URL
298+
writeLinkTag("canonical", url, doc);
299+
} else if (url) {
300+
// Compute a canonical url and include that
301+
const canonicalUrl = synthesizeCitationUrl(
302+
input,
303+
inputMetadata,
304+
outputFile,
305+
offset,
306+
);
307+
if (canonicalUrl) {
308+
writeLinkTag("canonical", canonicalUrl, doc);
309+
}
310+
}
311+
}

src/format/html/format-html-shared.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -390,7 +390,7 @@ function prependHeading(
390390
classes: string[],
391391
) {
392392
const heading = doc.createElement("h" + level);
393-
if (typeof (title) == "string" && title !== "none") {
393+
if (typeof title == "string" && title !== "none") {
394394
heading.innerHTML = title;
395395
}
396396
if (classes) {
@@ -515,6 +515,20 @@ export function writeMetaTag(name: string, content: string, doc: Document) {
515515
doc.querySelector("head")?.appendChild(nl);
516516
}
517517

518+
export function writeLinkTag(rel: string, href: string, doc: Document) {
519+
// Meta tag
520+
const l = doc.createElement("LINK");
521+
l.setAttribute("rel", rel);
522+
l.setAttribute("href", href);
523+
524+
// New Line
525+
const nl = doc.createTextNode("\n");
526+
527+
// Insert the nodes
528+
doc.querySelector("head")?.appendChild(l);
529+
doc.querySelector("head")?.appendChild(nl);
530+
}
531+
518532
export function formatPageLayout(format: Format) {
519533
return format.metadata[kPageLayout] as string || kPageLayoutArticle;
520534
}

src/quarto-core/attribution/document.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -435,7 +435,7 @@ export function citationMeta(metadata: Metadata): Metadata {
435435
}
436436
}
437437

438-
function synthesizeCitationUrl(
438+
export function synthesizeCitationUrl(
439439
input: string,
440440
metadata: Metadata,
441441
outputFile?: string,

src/resources/editor/tools/vs-code.mjs

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15237,6 +15237,24 @@ var require_yaml_intelligence_resources = __commonJS({
1523715237
}
1523815238
},
1523915239
description: "Options for controlling the display and behavior of Notebook previews."
15240+
},
15241+
{
15242+
"canonical-url": null,
15243+
tags: {
15244+
formats: [
15245+
"$html-doc"
15246+
]
15247+
},
15248+
schema: {
15249+
anyOf: [
15250+
"boolean",
15251+
"string"
15252+
]
15253+
},
15254+
description: {
15255+
short: "Include a canonical link tag in website pages",
15256+
long: "Include a canonical link tag in website pages. You may pass either `true` to \nautomatically generate a canonical link, or pass a canonical url that you'd like\nto have placed in the `href` attribute of the tag.\n\nCanonical links can only be generated for websites with a known `site-url`.\n"
15257+
}
1524015258
}
1524115259
],
1524215260
"schema/document-listing.yml": [
@@ -22334,7 +22352,11 @@ var require_yaml_intelligence_resources = __commonJS({
2233422352
},
2233522353
"Disambiguating year suffix in author-date styles (e.g. \u201Ca\u201D in \u201CDoe,\n1999a\u201D).",
2233622354
"Manuscript configuration",
22337-
"internal-schema-hack"
22355+
"internal-schema-hack",
22356+
{
22357+
short: "Include a canonical link tag in website pages",
22358+
long: "Include a canonical link tag in website pages. You may pass either\n<code>true</code> to automatically generate a canonical link, or pass a\ncanonical url that you\u2019d like to have placed in the <code>href</code>\nattribute of the tag.\nCanonical links can only be generated for websites with a known\n<code>site-url</code>."
22359+
}
2233822360
],
2233922361
"schema/external-schemas.yml": [
2234022362
{
@@ -22558,12 +22580,12 @@ var require_yaml_intelligence_resources = __commonJS({
2255822580
mermaid: "%%"
2255922581
},
2256022582
"handlers/mermaid/schema.yml": {
22561-
_internalId: 180066,
22583+
_internalId: 180143,
2256222584
type: "object",
2256322585
description: "be an object",
2256422586
properties: {
2256522587
"mermaid-format": {
22566-
_internalId: 180058,
22588+
_internalId: 180135,
2256722589
type: "enum",
2256822590
enum: [
2256922591
"png",
@@ -22579,7 +22601,7 @@ var require_yaml_intelligence_resources = __commonJS({
2257922601
exhaustiveCompletions: true
2258022602
},
2258122603
theme: {
22582-
_internalId: 180065,
22604+
_internalId: 180142,
2258322605
type: "anyOf",
2258422606
anyOf: [
2258522607
{

src/resources/editor/tools/yaml/web-worker.js

Lines changed: 26 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/resources/editor/tools/yaml/yaml-intelligence-resources.json

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8209,6 +8209,24 @@
82098209
}
82108210
},
82118211
"description": "Options for controlling the display and behavior of Notebook previews."
8212+
},
8213+
{
8214+
"canonical-url": null,
8215+
"tags": {
8216+
"formats": [
8217+
"$html-doc"
8218+
]
8219+
},
8220+
"schema": {
8221+
"anyOf": [
8222+
"boolean",
8223+
"string"
8224+
]
8225+
},
8226+
"description": {
8227+
"short": "Include a canonical link tag in website pages",
8228+
"long": "Include a canonical link tag in website pages. You may pass either `true` to \nautomatically generate a canonical link, or pass a canonical url that you'd like\nto have placed in the `href` attribute of the tag.\n\nCanonical links can only be generated for websites with a known `site-url`.\n"
8229+
}
82128230
}
82138231
],
82148232
"schema/document-listing.yml": [
@@ -15306,7 +15324,11 @@
1530615324
},
1530715325
"Disambiguating year suffix in author-date styles (e.g.&nbsp;“a” in “Doe,\n1999a”).",
1530815326
"Manuscript configuration",
15309-
"internal-schema-hack"
15327+
"internal-schema-hack",
15328+
{
15329+
"short": "Include a canonical link tag in website pages",
15330+
"long": "Include a canonical link tag in website pages. You may pass either\n<code>true</code> to automatically generate a canonical link, or pass a\ncanonical url that you’d like to have placed in the <code>href</code>\nattribute of the tag.\nCanonical links can only be generated for websites with a known\n<code>site-url</code>."
15331+
}
1531015332
],
1531115333
"schema/external-schemas.yml": [
1531215334
{
@@ -15530,12 +15552,12 @@
1553015552
"mermaid": "%%"
1553115553
},
1553215554
"handlers/mermaid/schema.yml": {
15533-
"_internalId": 180066,
15555+
"_internalId": 180143,
1553415556
"type": "object",
1553515557
"description": "be an object",
1553615558
"properties": {
1553715559
"mermaid-format": {
15538-
"_internalId": 180058,
15560+
"_internalId": 180135,
1553915561
"type": "enum",
1554015562
"enum": [
1554115563
"png",
@@ -15551,7 +15573,7 @@
1555115573
"exhaustiveCompletions": true
1555215574
},
1555315575
"theme": {
15554-
"_internalId": 180065,
15576+
"_internalId": 180142,
1555515577
"type": "anyOf",
1555615578
"anyOf": [
1555715579
{

src/resources/schema/document-links.yml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,3 +141,19 @@
141141
boolean:
142142
description: "Whether to show a back button in the notebook preview."
143143
description: "Options for controlling the display and behavior of Notebook previews."
144+
145+
- canonical-url:
146+
tags:
147+
formats: [$html-doc]
148+
schema:
149+
anyOf:
150+
- boolean
151+
- string
152+
description:
153+
short: "Include a canonical link tag in website pages"
154+
long: |
155+
Include a canonical link tag in website pages. You may pass either `true` to
156+
automatically generate a canonical link, or pass a canonical url that you'd like
157+
to have placed in the `href` attribute of the tag.
158+
159+
Canonical links can only be generated for websites with a known `site-url`.

0 commit comments

Comments
 (0)