diff --git a/dotcom-rendering/package.json b/dotcom-rendering/package.json
index 6ab042c52b6..6c9a5f52384 100644
--- a/dotcom-rendering/package.json
+++ b/dotcom-rendering/package.json
@@ -50,6 +50,7 @@
"@guardian/libs": "19.2.1",
"@guardian/ophan-tracker-js": "2.2.5",
"@guardian/react-crossword": "2.0.2",
+ "@guardian/react-crossword-next": "npm:@guardian/react-crossword@0.0.0-canary-20241209150926",
"@guardian/shimport": "1.0.2",
"@guardian/source": "8.0.0",
"@guardian/source-development-kitchen": "12.0.0",
diff --git a/dotcom-rendering/scripts/env/check-deps.js b/dotcom-rendering/scripts/env/check-deps.js
index c34c6ed0fec..2a9110c4195 100644
--- a/dotcom-rendering/scripts/env/check-deps.js
+++ b/dotcom-rendering/scripts/env/check-deps.js
@@ -8,10 +8,24 @@ if (pkg.devDependencies) {
process.exit(1);
}
-const mismatches = Object.entries(pkg.dependencies).filter(
- ([, version]) =>
- !semver.valid(version) && !version.startsWith('workspace:'),
-);
+/**
+ * We don't check packages that are not semver-compatible
+ * @type {RegExp[]}
+ */
+const exceptions = /** @type {const} */ ([
+ /npm:@guardian\/react-crossword@0.0.0-canary/,
+]);
+
+const mismatches = Object.entries(pkg.dependencies)
+ .filter(
+ ([, version]) =>
+ !exceptions.some((exception) => exception.test(version)),
+ )
+
+ .filter(
+ ([, version]) =>
+ !semver.valid(version) && !version.startsWith('workspace:'),
+ );
if (mismatches.length !== 0) {
warn('dotcom-rendering dependencies should be pinned.');
diff --git a/dotcom-rendering/src/components/Crossword.importable.tsx b/dotcom-rendering/src/components/Crossword.importable.tsx
new file mode 100644
index 00000000000..78cda1d207d
--- /dev/null
+++ b/dotcom-rendering/src/components/Crossword.importable.tsx
@@ -0,0 +1,6 @@
+import { Crossword as ReactCrossword } from '@guardian/react-crossword-next';
+import type { CrosswordProps } from '@guardian/react-crossword-next';
+
+export const Crossword = (data: CrosswordProps['data']) => (
+
+);
diff --git a/dotcom-rendering/src/lib/renderElement.tsx b/dotcom-rendering/src/lib/renderElement.tsx
index cd0241d8f26..f2923d63e87 100644
--- a/dotcom-rendering/src/lib/renderElement.tsx
+++ b/dotcom-rendering/src/lib/renderElement.tsx
@@ -9,6 +9,7 @@ import { CartoonComponent } from '../components/CartoonComponent';
import { ChartAtom } from '../components/ChartAtom.importable';
import { CodeBlockComponent } from '../components/CodeBlockComponent';
import { CommentBlockComponent } from '../components/CommentBlockComponent';
+import { Crossword } from '../components/Crossword.importable';
import { DividerBlockComponent } from '../components/DividerBlockComponent';
import { DocumentBlockComponent } from '../components/DocumentBlockComponent.importable';
import { EmailSignUpWrapper } from '../components/EmailSignUpWrapper';
@@ -841,6 +842,12 @@ export const renderElement = ({
case 'model.dotcomrendering.pageElements.DisclaimerBlockElement': {
return ;
}
+ case 'model.dotcomrendering.pageElements.CrosswordElement':
+ return (
+
+
+
+ );
case 'model.dotcomrendering.pageElements.AudioBlockElement':
case 'model.dotcomrendering.pageElements.ContentAtomBlockElement':
case 'model.dotcomrendering.pageElements.GenericAtomBlockElement':
diff --git a/dotcom-rendering/src/model/article-schema.json b/dotcom-rendering/src/model/article-schema.json
index f7bb836e913..e3f3d0027ea 100644
--- a/dotcom-rendering/src/model/article-schema.json
+++ b/dotcom-rendering/src/model/article-schema.json
@@ -119,6 +119,7 @@
}
},
"format": {
+ "description": "FEFormat is the stringified version of Format passed through from Frontend.\nIt gets converted to the `@guardian/libs` format on platform",
"type": "object",
"properties": {
"design": {
@@ -262,6 +263,7 @@
"$ref": "#/definitions/OnwardsSource"
},
"format": {
+ "description": "FEFormat is the stringified version of Format passed through from Frontend.\nIt gets converted to the `@guardian/libs` format on platform",
"type": "object",
"properties": {
"design": {
@@ -434,6 +436,104 @@
},
"isRightToLeftLang": {
"type": "boolean"
+ },
+ "crossword": {
+ "type": "object",
+ "properties": {
+ "creator": {
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string"
+ },
+ "webUrl": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "name",
+ "webUrl"
+ ]
+ },
+ "crosswordType": {
+ "enum": [
+ "cryptic",
+ "everyman",
+ "prize",
+ "quick",
+ "quick-cryptic",
+ "quiptic",
+ "special",
+ "speedy",
+ "weekend"
+ ],
+ "type": "string"
+ },
+ "date": {
+ "type": "number"
+ },
+ "dateSolutionAvailable": {
+ "type": "number"
+ },
+ "dimensions": {
+ "type": "object",
+ "properties": {
+ "rows": {
+ "type": "number"
+ },
+ "cols": {
+ "type": "number"
+ }
+ },
+ "required": [
+ "cols",
+ "rows"
+ ]
+ },
+ "entries": {
+ "type": "array",
+ "items": [
+ {
+ "$ref": "#/definitions/CAPIEntry"
+ }
+ ],
+ "minItems": 1,
+ "additionalItems": {
+ "$ref": "#/definitions/CAPIEntry"
+ }
+ },
+ "id": {
+ "type": "string"
+ },
+ "name": {
+ "type": "string"
+ },
+ "number": {
+ "type": "number"
+ },
+ "pdf": {
+ "type": "string"
+ },
+ "solutionAvailable": {
+ "type": "boolean"
+ },
+ "webPublicationDate": {
+ "type": "number"
+ },
+ "instructions": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "crosswordType",
+ "date",
+ "dimensions",
+ "entries",
+ "id",
+ "name",
+ "number",
+ "solutionAvailable"
+ ]
}
},
"required": [
@@ -762,6 +862,9 @@
},
{
"$ref": "#/definitions/WitnessTypeBlockElement"
+ },
+ {
+ "$ref": "#/definitions/CrosswordElement"
}
]
},
@@ -3976,6 +4079,241 @@
"witnessEmbedType"
]
},
+ "CrosswordElement": {
+ "type": "object",
+ "properties": {
+ "_type": {
+ "type": "string",
+ "const": "model.dotcomrendering.pageElements.CrosswordElement"
+ },
+ "crossword": {
+ "type": "object",
+ "properties": {
+ "creator": {
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string"
+ },
+ "webUrl": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "name",
+ "webUrl"
+ ]
+ },
+ "crosswordType": {
+ "enum": [
+ "cryptic",
+ "everyman",
+ "prize",
+ "quick",
+ "quick-cryptic",
+ "quiptic",
+ "special",
+ "speedy",
+ "weekend"
+ ],
+ "type": "string"
+ },
+ "date": {
+ "type": "number"
+ },
+ "dateSolutionAvailable": {
+ "type": "number"
+ },
+ "dimensions": {
+ "type": "object",
+ "properties": {
+ "rows": {
+ "type": "number"
+ },
+ "cols": {
+ "type": "number"
+ }
+ },
+ "required": [
+ "cols",
+ "rows"
+ ]
+ },
+ "entries": {
+ "type": "array",
+ "items": [
+ {
+ "$ref": "#/definitions/CAPIEntry"
+ }
+ ],
+ "minItems": 1,
+ "additionalItems": {
+ "$ref": "#/definitions/CAPIEntry"
+ }
+ },
+ "id": {
+ "type": "string"
+ },
+ "name": {
+ "type": "string"
+ },
+ "number": {
+ "type": "number"
+ },
+ "pdf": {
+ "type": "string"
+ },
+ "solutionAvailable": {
+ "type": "boolean"
+ },
+ "webPublicationDate": {
+ "type": "number"
+ },
+ "instructions": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "crosswordType",
+ "date",
+ "dimensions",
+ "entries",
+ "id",
+ "name",
+ "number",
+ "solutionAvailable"
+ ]
+ }
+ },
+ "required": [
+ "_type",
+ "crossword"
+ ]
+ },
+ "CAPIEntry": {
+ "allOf": [
+ {
+ "type": "object",
+ "properties": {
+ "id": {
+ "anyOf": [
+ {
+ "type": "string",
+ "pattern": "^[0-9]*-across$"
+ },
+ {
+ "type": "string",
+ "pattern": "^[0-9]*-down$"
+ }
+ ]
+ },
+ "group": {
+ "type": "array",
+ "items": [
+ {
+ "anyOf": [
+ {
+ "type": "string",
+ "pattern": "^[0-9]*-across$"
+ },
+ {
+ "type": "string",
+ "pattern": "^[0-9]*-down$"
+ }
+ ]
+ }
+ ],
+ "minItems": 1,
+ "additionalItems": {
+ "anyOf": [
+ {
+ "type": "string",
+ "pattern": "^[0-9]*-across$"
+ },
+ {
+ "type": "string",
+ "pattern": "^[0-9]*-down$"
+ }
+ ]
+ }
+ },
+ "number": {
+ "type": "number"
+ }
+ },
+ "required": [
+ "group",
+ "id",
+ "number"
+ ]
+ },
+ {
+ "type": "object",
+ "properties": {
+ "direction": {
+ "$ref": "#/definitions/Direction"
+ },
+ "position": {
+ "$ref": "#/definitions/Coords",
+ "description": "Coords of first cell"
+ },
+ "clue": {
+ "description": "The clue for the current entry",
+ "type": "string"
+ },
+ "humanNumber": {
+ "description": "The number for the clue",
+ "type": "string"
+ },
+ "solution": {
+ "description": "The solution to the entry's clue",
+ "type": "string"
+ },
+ "length": {
+ "description": "The length of the solution (we don't always have a solution)",
+ "type": "number"
+ },
+ "separatorLocations": {
+ "$ref": "#/definitions/Record",
+ "description": "Separators for multi-part solutions e.g.\n- ready,steady,go\n- tofu-eating"
+ }
+ },
+ "required": [
+ "clue",
+ "direction",
+ "humanNumber",
+ "length",
+ "position",
+ "separatorLocations"
+ ]
+ }
+ ]
+ },
+ "Direction": {
+ "enum": [
+ "across",
+ "down"
+ ],
+ "type": "string"
+ },
+ "Coords": {
+ "type": "object",
+ "properties": {
+ "x": {
+ "type": "number"
+ },
+ "y": {
+ "type": "number"
+ }
+ },
+ "required": [
+ "x",
+ "y"
+ ]
+ },
+ "Record": {
+ "type": "object"
+ },
"Block": {
"type": "object",
"properties": {
@@ -4139,6 +4477,7 @@
"type": "string"
},
"FEDesign": {
+ "description": "FEDesign is what frontend gives (originating in the capi scala client) us on the Format field\nhttps://github.com/guardian/content-api-scala-client/blob/master/client/src/main/scala/com.gu.contentapi.client/utils/format/Design.scala",
"enum": [
"AnalysisDesign",
"ArticleDesign",
@@ -4182,6 +4521,7 @@
"type": "string"
},
"FEDisplay": {
+ "description": "FEDisplay is the display information passed through from frontend (originating in the capi scala client) and dictates the display style of the content e.g. Immersive\nhttps://github.com/guardian/content-api-scala-client/blob/master/client/src/main/scala/com.gu.contentapi.client/utils/format/Display.scala",
"enum": [
"ImmersiveDisplay",
"NumberedListDisplay",
@@ -4202,6 +4542,7 @@
"type": "string"
},
"FELinkType": {
+ "description": "Data types for the API request bodies from clients that require transformation before internal use.\nWhere data types are coming from Frontend we try to use the 'FE' prefix.\nPrior to this we used 'CAPI' as a prefix which wasn't entirely accurate, and some data structures never received the prefix, meaning some are still missing it.",
"type": "object",
"properties": {
"url": {
@@ -4459,6 +4800,7 @@
"type": "object",
"properties": {
"format": {
+ "description": "FEFormat is the stringified version of Format passed through from Frontend.\nIt gets converted to the `@guardian/libs` format on platform",
"type": "object",
"properties": {
"design": {
diff --git a/dotcom-rendering/src/model/block-schema.json b/dotcom-rendering/src/model/block-schema.json
index ab6241ca5b4..6ef5a7178ab 100644
--- a/dotcom-rendering/src/model/block-schema.json
+++ b/dotcom-rendering/src/model/block-schema.json
@@ -349,6 +349,9 @@
},
{
"$ref": "#/definitions/WitnessTypeBlockElement"
+ },
+ {
+ "$ref": "#/definitions/CrosswordElement"
}
]
},
@@ -3563,6 +3566,241 @@
"witnessEmbedType"
]
},
+ "CrosswordElement": {
+ "type": "object",
+ "properties": {
+ "_type": {
+ "type": "string",
+ "const": "model.dotcomrendering.pageElements.CrosswordElement"
+ },
+ "crossword": {
+ "type": "object",
+ "properties": {
+ "creator": {
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string"
+ },
+ "webUrl": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "name",
+ "webUrl"
+ ]
+ },
+ "crosswordType": {
+ "enum": [
+ "cryptic",
+ "everyman",
+ "prize",
+ "quick",
+ "quick-cryptic",
+ "quiptic",
+ "special",
+ "speedy",
+ "weekend"
+ ],
+ "type": "string"
+ },
+ "date": {
+ "type": "number"
+ },
+ "dateSolutionAvailable": {
+ "type": "number"
+ },
+ "dimensions": {
+ "type": "object",
+ "properties": {
+ "rows": {
+ "type": "number"
+ },
+ "cols": {
+ "type": "number"
+ }
+ },
+ "required": [
+ "cols",
+ "rows"
+ ]
+ },
+ "entries": {
+ "type": "array",
+ "items": [
+ {
+ "$ref": "#/definitions/CAPIEntry"
+ }
+ ],
+ "minItems": 1,
+ "additionalItems": {
+ "$ref": "#/definitions/CAPIEntry"
+ }
+ },
+ "id": {
+ "type": "string"
+ },
+ "name": {
+ "type": "string"
+ },
+ "number": {
+ "type": "number"
+ },
+ "pdf": {
+ "type": "string"
+ },
+ "solutionAvailable": {
+ "type": "boolean"
+ },
+ "webPublicationDate": {
+ "type": "number"
+ },
+ "instructions": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "crosswordType",
+ "date",
+ "dimensions",
+ "entries",
+ "id",
+ "name",
+ "number",
+ "solutionAvailable"
+ ]
+ }
+ },
+ "required": [
+ "_type",
+ "crossword"
+ ]
+ },
+ "CAPIEntry": {
+ "allOf": [
+ {
+ "type": "object",
+ "properties": {
+ "id": {
+ "anyOf": [
+ {
+ "type": "string",
+ "pattern": "^[0-9]*-across$"
+ },
+ {
+ "type": "string",
+ "pattern": "^[0-9]*-down$"
+ }
+ ]
+ },
+ "group": {
+ "type": "array",
+ "items": [
+ {
+ "anyOf": [
+ {
+ "type": "string",
+ "pattern": "^[0-9]*-across$"
+ },
+ {
+ "type": "string",
+ "pattern": "^[0-9]*-down$"
+ }
+ ]
+ }
+ ],
+ "minItems": 1,
+ "additionalItems": {
+ "anyOf": [
+ {
+ "type": "string",
+ "pattern": "^[0-9]*-across$"
+ },
+ {
+ "type": "string",
+ "pattern": "^[0-9]*-down$"
+ }
+ ]
+ }
+ },
+ "number": {
+ "type": "number"
+ }
+ },
+ "required": [
+ "group",
+ "id",
+ "number"
+ ]
+ },
+ {
+ "type": "object",
+ "properties": {
+ "direction": {
+ "$ref": "#/definitions/Direction"
+ },
+ "position": {
+ "$ref": "#/definitions/Coords",
+ "description": "Coords of first cell"
+ },
+ "clue": {
+ "description": "The clue for the current entry",
+ "type": "string"
+ },
+ "humanNumber": {
+ "description": "The number for the clue",
+ "type": "string"
+ },
+ "solution": {
+ "description": "The solution to the entry's clue",
+ "type": "string"
+ },
+ "length": {
+ "description": "The length of the solution (we don't always have a solution)",
+ "type": "number"
+ },
+ "separatorLocations": {
+ "$ref": "#/definitions/Record",
+ "description": "Separators for multi-part solutions e.g.\n- ready,steady,go\n- tofu-eating"
+ }
+ },
+ "required": [
+ "clue",
+ "direction",
+ "humanNumber",
+ "length",
+ "position",
+ "separatorLocations"
+ ]
+ }
+ ]
+ },
+ "Direction": {
+ "enum": [
+ "across",
+ "down"
+ ],
+ "type": "string"
+ },
+ "Coords": {
+ "type": "object",
+ "properties": {
+ "x": {
+ "type": "number"
+ },
+ "y": {
+ "type": "number"
+ }
+ },
+ "required": [
+ "x",
+ "y"
+ ]
+ },
+ "Record": {
+ "type": "object"
+ },
"Attributes": {
"type": "object",
"properties": {
diff --git a/dotcom-rendering/src/model/front-schema.json b/dotcom-rendering/src/model/front-schema.json
index 1b08fa3f4b6..4abceb81996 100644
--- a/dotcom-rendering/src/model/front-schema.json
+++ b/dotcom-rendering/src/model/front-schema.json
@@ -617,6 +617,7 @@
]
},
"format": {
+ "description": "FEFormat is the stringified version of Format passed through from Frontend.\nIt gets converted to the `@guardian/libs` format on platform",
"type": "object",
"properties": {
"design": {
@@ -1090,6 +1091,7 @@
]
},
"format": {
+ "description": "FEFormat is the stringified version of Format passed through from Frontend.\nIt gets converted to the `@guardian/libs` format on platform",
"type": "object",
"properties": {
"design": {
@@ -1178,6 +1180,7 @@
]
},
"format": {
+ "description": "FEFormat is the stringified version of Format passed through from Frontend.\nIt gets converted to the `@guardian/libs` format on platform",
"type": "object",
"properties": {
"design": {
@@ -1365,6 +1368,7 @@
]
},
"format": {
+ "description": "FEFormat is the stringified version of Format passed through from Frontend.\nIt gets converted to the `@guardian/libs` format on platform",
"type": "object",
"properties": {
"design": {
@@ -1838,6 +1842,7 @@
]
},
"format": {
+ "description": "FEFormat is the stringified version of Format passed through from Frontend.\nIt gets converted to the `@guardian/libs` format on platform",
"type": "object",
"properties": {
"design": {
@@ -1926,6 +1931,7 @@
]
},
"format": {
+ "description": "FEFormat is the stringified version of Format passed through from Frontend.\nIt gets converted to the `@guardian/libs` format on platform",
"type": "object",
"properties": {
"design": {
@@ -2113,6 +2119,7 @@
]
},
"format": {
+ "description": "FEFormat is the stringified version of Format passed through from Frontend.\nIt gets converted to the `@guardian/libs` format on platform",
"type": "object",
"properties": {
"design": {
@@ -2586,6 +2593,7 @@
]
},
"format": {
+ "description": "FEFormat is the stringified version of Format passed through from Frontend.\nIt gets converted to the `@guardian/libs` format on platform",
"type": "object",
"properties": {
"design": {
@@ -2674,6 +2682,7 @@
]
},
"format": {
+ "description": "FEFormat is the stringified version of Format passed through from Frontend.\nIt gets converted to the `@guardian/libs` format on platform",
"type": "object",
"properties": {
"design": {
@@ -3036,6 +3045,7 @@
]
},
"FEDesign": {
+ "description": "FEDesign is what frontend gives (originating in the capi scala client) us on the Format field\nhttps://github.com/guardian/content-api-scala-client/blob/master/client/src/main/scala/com.gu.contentapi.client/utils/format/Design.scala",
"enum": [
"AnalysisDesign",
"ArticleDesign",
@@ -3079,6 +3089,7 @@
"type": "string"
},
"FEDisplay": {
+ "description": "FEDisplay is the display information passed through from frontend (originating in the capi scala client) and dictates the display style of the content e.g. Immersive\nhttps://github.com/guardian/content-api-scala-client/blob/master/client/src/main/scala/com.gu.contentapi.client/utils/format/Display.scala",
"enum": [
"ImmersiveDisplay",
"NumberedListDisplay",
@@ -3393,6 +3404,7 @@
]
},
"FELinkType": {
+ "description": "Data types for the API request bodies from clients that require transformation before internal use.\nWhere data types are coming from Frontend we try to use the 'FE' prefix.\nPrior to this we used 'CAPI' as a prefix which wasn't entirely accurate, and some data structures never received the prefix, meaning some are still missing it.",
"type": "object",
"properties": {
"url": {
@@ -3558,6 +3570,7 @@
"type": "object",
"properties": {
"format": {
+ "description": "FEFormat is the stringified version of Format passed through from Frontend.\nIt gets converted to the `@guardian/libs` format on platform",
"type": "object",
"properties": {
"design": {
diff --git a/dotcom-rendering/src/model/newsletter-page-schema.json b/dotcom-rendering/src/model/newsletter-page-schema.json
index 9ed29434550..bc4cc4df9e1 100644
--- a/dotcom-rendering/src/model/newsletter-page-schema.json
+++ b/dotcom-rendering/src/model/newsletter-page-schema.json
@@ -291,6 +291,7 @@
]
},
"FELinkType": {
+ "description": "Data types for the API request bodies from clients that require transformation before internal use.\nWhere data types are coming from Frontend we try to use the 'FE' prefix.\nPrior to this we used 'CAPI' as a prefix which wasn't entirely accurate, and some data structures never received the prefix, meaning some are still missing it.",
"type": "object",
"properties": {
"url": {
diff --git a/dotcom-rendering/src/model/tag-page-schema.json b/dotcom-rendering/src/model/tag-page-schema.json
index 610b9414df7..ea1a7dbdc9d 100644
--- a/dotcom-rendering/src/model/tag-page-schema.json
+++ b/dotcom-rendering/src/model/tag-page-schema.json
@@ -138,6 +138,7 @@
]
},
"format": {
+ "description": "FEFormat is the stringified version of Format passed through from Frontend.\nIt gets converted to the `@guardian/libs` format on platform",
"type": "object",
"properties": {
"design": {
@@ -611,6 +612,7 @@
]
},
"format": {
+ "description": "FEFormat is the stringified version of Format passed through from Frontend.\nIt gets converted to the `@guardian/libs` format on platform",
"type": "object",
"properties": {
"design": {
@@ -699,6 +701,7 @@
]
},
"format": {
+ "description": "FEFormat is the stringified version of Format passed through from Frontend.\nIt gets converted to the `@guardian/libs` format on platform",
"type": "object",
"properties": {
"design": {
@@ -1275,6 +1278,7 @@
],
"definitions": {
"FEDesign": {
+ "description": "FEDesign is what frontend gives (originating in the capi scala client) us on the Format field\nhttps://github.com/guardian/content-api-scala-client/blob/master/client/src/main/scala/com.gu.contentapi.client/utils/format/Design.scala",
"enum": [
"AnalysisDesign",
"ArticleDesign",
@@ -1318,6 +1322,7 @@
"type": "string"
},
"FEDisplay": {
+ "description": "FEDisplay is the display information passed through from frontend (originating in the capi scala client) and dictates the display style of the content e.g. Immersive\nhttps://github.com/guardian/content-api-scala-client/blob/master/client/src/main/scala/com.gu.contentapi.client/utils/format/Display.scala",
"enum": [
"ImmersiveDisplay",
"NumberedListDisplay",
@@ -1705,6 +1710,7 @@
]
},
"FELinkType": {
+ "description": "Data types for the API request bodies from clients that require transformation before internal use.\nWhere data types are coming from Frontend we try to use the 'FE' prefix.\nPrior to this we used 'CAPI' as a prefix which wasn't entirely accurate, and some data structures never received the prefix, meaning some are still missing it.",
"type": "object",
"properties": {
"url": {
diff --git a/dotcom-rendering/src/server/handler.article.web.ts b/dotcom-rendering/src/server/handler.article.web.ts
index f98084f570a..a29e03a54ae 100644
--- a/dotcom-rendering/src/server/handler.article.web.ts
+++ b/dotcom-rendering/src/server/handler.article.web.ts
@@ -3,7 +3,7 @@ import { Standard as ExampleArticle } from '../../fixtures/generated/fe-articles
import { decideFormat } from '../lib/articleFormat';
import { enhanceBlocks } from '../model/enhanceBlocks';
import { validateAsArticleType, validateAsBlock } from '../model/validate';
-import { enhanceArticleType } from '../types/article';
+import { enhanceArticleType, enhanceCrosswordArticle } from '../types/article';
import type { FEBlocksRequest } from '../types/frontend';
import { makePrefetchHeader } from './lib/header';
import { recordTypeAndPlatform } from './lib/logging-store';
@@ -13,7 +13,12 @@ export const handleArticle: RequestHandler = ({ body }, res) => {
recordTypeAndPlatform('article', 'web');
const frontendData = validateAsArticleType(body);
- const article = enhanceArticleType(frontendData, 'Web');
+ let article = enhanceArticleType(frontendData, 'Web');
+
+ if (article.frontendData.crossword) {
+ article = enhanceCrosswordArticle(article);
+ }
+
const { html, prefetchScripts } = renderHtml({
article,
});
diff --git a/dotcom-rendering/src/types/article.ts b/dotcom-rendering/src/types/article.ts
index e1875c1f123..dd44c820a0a 100644
--- a/dotcom-rendering/src/types/article.ts
+++ b/dotcom-rendering/src/types/article.ts
@@ -1,3 +1,4 @@
+import { isUndefined } from '@guardian/libs';
import { type ArticleFormat, decideFormat } from '../lib/articleFormat';
import type { ImageForAppsLightbox } from '../model/appsLightboxImages';
import { appsLightboxImages } from '../model/appsLightboxImages';
@@ -10,7 +11,7 @@ import {
type TableOfContentsItem,
} from '../model/enhanceTableOfContents';
import { enhancePinnedPost } from '../model/pinnedPost';
-import type { ImageForLightbox } from './content';
+import type { CrosswordElement, ImageForLightbox } from './content';
import type { FEArticleType } from './frontend';
import { type RenderingTarget } from './renderingTarget';
@@ -30,6 +31,40 @@ export type Article = {
frontendData: ArticleDeprecated;
};
+export const enhanceCrosswordArticle = (article: Article): Article => {
+ if (isUndefined(article.frontendData.crossword)) {
+ throw new TypeError('article does not contain a crossword');
+ }
+
+ const element: CrosswordElement = {
+ _type: 'model.dotcomrendering.pageElements.CrosswordElement' as const,
+ crossword: article.frontendData.crossword,
+ };
+
+ return {
+ ...article,
+ format: { ...article.format },
+ frontendData: {
+ ...article.frontendData,
+ blocks: [
+ {
+ id: article.frontendData.crossword.id,
+ elements: [element],
+ attributes: {
+ pinned: false,
+ keyEvent: false,
+ summary: false,
+ },
+ primaryDateLine:
+ article.frontendData.webPublicationDateDisplay,
+ secondaryDateLine:
+ article.frontendData.webPublicationSecondaryDateDisplay,
+ },
+ ],
+ },
+ };
+};
+
export const enhanceArticleType = (
data: FEArticleType,
renderingTarget: RenderingTarget,
diff --git a/dotcom-rendering/src/types/content.ts b/dotcom-rendering/src/types/content.ts
index d481eacf9c8..b8c92263795 100644
--- a/dotcom-rendering/src/types/content.ts
+++ b/dotcom-rendering/src/types/content.ts
@@ -1,3 +1,4 @@
+import { type CrosswordProps } from '@guardian/react-crossword-next';
import type { ArticleTheme } from '../lib/articleFormat';
export type StarRating = 0 | 1 | 2 | 3 | 4 | 5;
@@ -732,6 +733,12 @@ interface WitnessTypeBlockElement extends ThirdPartyEmbeddedContent {
| WitnessTypeDataVideo
| WitnessTypeDataText;
}
+
+export interface CrosswordElement {
+ _type: 'model.dotcomrendering.pageElements.CrosswordElement';
+ crossword: CrosswordProps['data'];
+}
+
export type FEElement =
| AdPlaceholderBlockElement
| AudioAtomBlockElement
@@ -791,7 +798,8 @@ export type FEElement =
| VideoYoutubeBlockElement
| VineBlockElement
| YoutubeBlockElement
- | WitnessTypeBlockElement;
+ | WitnessTypeBlockElement
+ | CrosswordElement;
// -------------------------------------
// Misc
diff --git a/dotcom-rendering/src/types/frontend.ts b/dotcom-rendering/src/types/frontend.ts
index 2f91fe527d7..ce15ae5681f 100644
--- a/dotcom-rendering/src/types/frontend.ts
+++ b/dotcom-rendering/src/types/frontend.ts
@@ -1,3 +1,4 @@
+import { type CrosswordProps } from '@guardian/react-crossword-next';
import type { SharedAdTargeting } from '../lib/ad-targeting';
import type { EditionId } from '../lib/edition';
import type { FEArticleBadgeType } from './badge';
@@ -126,6 +127,7 @@ export interface FEArticleType {
showTableOfContents: boolean;
lang?: string;
isRightToLeftLang?: boolean;
+ crossword?: CrosswordProps['data'];
}
type PageTypeType = {
@@ -160,10 +162,15 @@ export interface FEBlocksRequest {
keywordIds: string;
}
-// Themes are used for styling
-// RealPillars have pillar palette colours and have a `Pillar` type in Scala
-// FakePillars allow us to make modifications to style based on rules outside of the pillar of an article and have a `Special` type in Scala
-// https://github.com/guardian/content-api-scala-client/blob/master/client/src/main/scala/com.gu.contentapi.client/utils/format/Theme.scala
+/**
+ * Themes are used for styling.
+ *
+ * RealPillars have pillar palette colours and have a `Pillar` type in Scala.
+ *
+ * FakePillars allow us to make modifications to style based on rules outside of the pillar of an article and have a `Special` type in Scala.
+ *
+ * https://github.com/guardian/content-api-scala-client/blob/master/client/src/main/scala/com.gu.contentapi.client/utils/format/Theme.scala
+ */
type ThemePillar =
| 'NewsPillar'
| 'OpinionPillar'
@@ -174,9 +181,10 @@ type ThemePillar =
type ThemeSpecial = 'SpecialReportTheme' | 'Labs' | 'SpecialReportAltTheme';
export type FETheme = ThemePillar | ThemeSpecial;
-// FEDesign is what frontend gives (originating in the capi scala client) us on the Format field
-// https://github.com/guardian/content-api-scala-client/blob/master/client/src/main/scala/com.gu.contentapi.client/utils/format/Design.scala
-
+/**
+ * FEDesign is what frontend gives (originating in the capi scala client) us on the Format field
+ * https://github.com/guardian/content-api-scala-client/blob/master/client/src/main/scala/com.gu.contentapi.client/utils/format/Design.scala
+ */
export type FEDesign =
| 'ArticleDesign'
| 'PictureDesign'
@@ -202,29 +210,31 @@ export type FEDesign =
| 'FullPageInteractiveDesign'
| 'NewsletterSignupDesign'
| 'TimelineDesign'
- | 'ProfileDesign'; // FEDisplay is the display information passed through from frontend (originating in the capi scala client) and dictates the displaystyle of the content e.g. Immersive
-// https://github.com/guardian/content-api-scala-client/blob/master/client/src/main/scala/com.gu.contentapi.client/utils/format/Display.scala
+ | 'ProfileDesign';
+/** FEDisplay is the display information passed through from frontend (originating in the capi scala client) and dictates the display style of the content e.g. Immersive
+https://github.com/guardian/content-api-scala-client/blob/master/client/src/main/scala/com.gu.contentapi.client/utils/format/Display.scala */
export type FEDisplay =
| 'StandardDisplay'
| 'ImmersiveDisplay'
| 'ShowcaseDisplay'
| 'NumberedListDisplay';
-// FEFormat is the stringified version of Format passed through from Frontend.
-// It gets converted to the @guardian/libs format on platform
+/**
+ * FEFormat is the stringified version of Format passed through from Frontend.
+ * It gets converted to the `@guardian/libs` format on platform
+ */
export type FEFormat = {
design: FEDesign;
theme: FETheme;
display: FEDisplay;
};
-// Data types for the API request bodies from clients that require
-// transformation before internal use.
-// Where data types are coming from Frontend we try to use the 'FE' prefix.
-//
-// Prior to this we used 'CAPI' as a prefix which wasn't entirely accurate,
-// and some data structures never received the prefix, meaning some are still missing it.
+/**
+ * Data types for the API request bodies from clients that require transformation before internal use.
+ * Where data types are coming from Frontend we try to use the 'FE' prefix.
+ * Prior to this we used 'CAPI' as a prefix which wasn't entirely accurate, and some data structures never received the prefix, meaning some are still missing it.
+ */
export interface FELinkType {
url: string;
title: string;
@@ -251,8 +261,9 @@ export interface FENavType {
}
// Pillars are used for styling
-// RealPillars have pillar palette colours
-// FakePillars allow us to make modifications to style based on rules outside of the pillar of an article
+
+/** `RealPillars` have pillar palette colours */
type RealPillars = 'news' | 'opinion' | 'sport' | 'culture' | 'lifestyle';
+/** `FakePillars` allow us to make modifications to style based on rules outside of the pillar of an article */
type FakePillars = 'labs';
export type LegacyPillar = RealPillars | FakePillars;
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 1a72b4251eb..d7510f18932 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -364,6 +364,9 @@ importers:
'@guardian/react-crossword':
specifier: 2.0.2
version: 2.0.2
+ '@guardian/react-crossword-next':
+ specifier: npm:@guardian/react-crossword@0.0.0-canary-20241209150926
+ version: /@guardian/react-crossword@0.0.0-canary-20241209150926(@emotion/react@11.11.3)(@guardian/libs@19.2.1)(@guardian/source@8.0.0)(@types/react@18.3.1)(react-dom@18.3.1)(react@18.3.1)(typescript@5.5.3)
'@guardian/shimport':
specifier: 1.0.2
version: 1.0.2
@@ -4256,6 +4259,33 @@ packages:
tslib: 2.6.2
dev: false
+ /@guardian/react-crossword@0.0.0-canary-20241209150926(@emotion/react@11.11.3)(@guardian/libs@19.2.1)(@guardian/source@8.0.0)(@types/react@18.3.1)(react-dom@18.3.1)(react@18.3.1)(typescript@5.5.3):
+ resolution: {integrity: sha512-nJ9vi454SqMynQ0UDz+jBmO/l7YxgVIq6Gvfyy4p/b5cnBNsxh8n4OZtIexhJx/dOhKYsMSPw/5KU8YOnVhF9A==}
+ peerDependencies:
+ '@emotion/react': ^11.11.3
+ '@guardian/libs': ^19.1.0
+ '@guardian/source': ^8.0.0
+ '@types/react': ^18.2.79
+ react: ^18.2.0
+ typescript: ~5.5.2
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ typescript:
+ optional: true
+ dependencies:
+ '@emotion/react': 11.11.3(@types/react@18.3.1)(react@18.3.1)
+ '@guardian/libs': 19.2.1(tslib@2.6.2)(typescript@5.5.3)
+ '@guardian/source': 8.0.0(@emotion/react@11.11.3)(@types/react@18.3.1)(react@18.3.1)(tslib@2.6.2)(typescript@5.5.3)
+ '@types/react': 18.3.1
+ react: 18.3.1
+ tslib: 2.6.2
+ typescript: 5.5.3
+ use-local-storage-state: 19.5.0(react-dom@18.3.1)(react@18.3.1)
+ transitivePeerDependencies:
+ - react-dom
+ dev: false
+
/@guardian/react-crossword@2.0.2:
resolution: {integrity: sha512-pFvCpuUH+GKz12uUzW4+Lck/ZhDWvqLodr1UwXIE7qjJCz8V4NEfuiGZkkIpVoPh+dEHTkiDQ6Ks4653KdH01g==}
dependencies:
@@ -17691,6 +17721,17 @@ packages:
qs: 6.13.0
dev: false
+ /use-local-storage-state@19.5.0(react-dom@18.3.1)(react@18.3.1):
+ resolution: {integrity: sha512-sUJAyFvsmqMpBhdwaRr7GTKkkoxb6PWeNVvpBDrLuwQF1PpbJRKIbOYeLLeqJI7B3wdfFlLLCBbmOdopiSTBOw==}
+ engines: {node: '>=14'}
+ peerDependencies:
+ react: '>=18'
+ react-dom: '>=18'
+ dependencies:
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
+ dev: false
+
/util-deprecate@1.0.2:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
dev: false