@quintype/seo 1.48.0-recipe-schema.0 → 1.48.0
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.
- package/CHANGELOG.md +7 -0
- package/dist/index.cjs.js +61 -48
- package/package.json +1 -1
- package/src/structured-data/schema.js +0 -27
- package/src/structured-data/structured-data-tags.js +46 -35
- package/src/utils.js +16 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
|
4
4
|
|
|
5
|
+
## [1.48.0](https://github.com/quintype/quintype-node-seo/compare/v1.47.0...v1.48.0) (2024-12-11)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
|
|
10
|
+
* **media-gallery:** add media gallery schema to visual stories ([#565](https://github.com/quintype/quintype-node-seo/issues/565)) ([34ded03](https://github.com/quintype/quintype-node-seo/commit/34ded036225af150d8085c6d108b5c8bbacece73))
|
|
11
|
+
|
|
5
12
|
## [1.47.0](https://github.com/quintype/quintype-node-seo/compare/v1.46.5...v1.47.0) (2024-10-30)
|
|
6
13
|
|
|
7
14
|
|
package/dist/index.cjs.js
CHANGED
|
@@ -57,6 +57,22 @@ function getWatermarkImage(story, cdnSrc, cdnURL) {
|
|
|
57
57
|
return watermarkImageS3Key;
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
+
function getAllowedCards(card) {
|
|
61
|
+
const storyElementWhitelist = ["text", "title", "image", "video"];
|
|
62
|
+
const validCards = card["story-elements"].some(el => {
|
|
63
|
+
if (el.type === "jsembed") {
|
|
64
|
+
const videoUrl = el && atob(`${el["embed-js"]}`);
|
|
65
|
+
const formatWhitelist = ["mp4", "webm", "ogv"];
|
|
66
|
+
const isValidVideoUrl = formatWhitelist.some(format => {
|
|
67
|
+
return videoUrl && videoUrl.endsWith(format);
|
|
68
|
+
});
|
|
69
|
+
return isValidVideoUrl;
|
|
70
|
+
}
|
|
71
|
+
return storyElementWhitelist.includes(el.type);
|
|
72
|
+
});
|
|
73
|
+
return validCards;
|
|
74
|
+
}
|
|
75
|
+
|
|
60
76
|
function showAmpTag({ ampStoryPages = true }, pageType, story) {
|
|
61
77
|
if (!ampStoryPages || pageType !== "story-page") {
|
|
62
78
|
return false;
|
|
@@ -550,33 +566,6 @@ function generateAuthorPageSchema(publisherConfig, data, url) {
|
|
|
550
566
|
};
|
|
551
567
|
}
|
|
552
568
|
|
|
553
|
-
function generateRecipePageSchema(story) {
|
|
554
|
-
const { headline, url, "author-name": authorName, description, metadata } = story;
|
|
555
|
-
const servings = lodash.get(metadata, ["servings"], "");
|
|
556
|
-
const ingredients = lodash.get(metadata, ["ingredients"], "");
|
|
557
|
-
const recipeAttributes = lodash.get(metadata, ["story-attributes"], "");
|
|
558
|
-
const cuisine = lodash.get(recipeAttributes, ["cuisine", "0"], "");
|
|
559
|
-
const preperationtime = lodash.get(recipeAttributes, ["preparationtime", "0"], "");
|
|
560
|
-
const cookingtime = lodash.get(recipeAttributes, ["cookingtime", "0"], "");
|
|
561
|
-
|
|
562
|
-
return {
|
|
563
|
-
"@context": "https://schema.org/",
|
|
564
|
-
"@type": "Recipe",
|
|
565
|
-
name: headline,
|
|
566
|
-
url: url,
|
|
567
|
-
author: {
|
|
568
|
-
"@type": "Person",
|
|
569
|
-
name: authorName
|
|
570
|
-
},
|
|
571
|
-
description: description,
|
|
572
|
-
recipeIngredient: ingredients,
|
|
573
|
-
prepTime: preperationtime,
|
|
574
|
-
cookTime: cookingtime,
|
|
575
|
-
recipeCuisine: cuisine,
|
|
576
|
-
recipeYield: servings
|
|
577
|
-
};
|
|
578
|
-
}
|
|
579
|
-
|
|
580
569
|
function getMovieEntityTags(movieJson) {
|
|
581
570
|
return getSchemaMovieReview(movieJson);
|
|
582
571
|
}
|
|
@@ -987,6 +976,7 @@ function StructuredDataTags({ structuredData = {} }, config, pageType, response
|
|
|
987
976
|
const isStructuredDataEmpty = Object.keys(structuredData).length === 0;
|
|
988
977
|
const enableBreadcrumbList = get__default["default"](structuredData, ["enableBreadcrumbList"], true);
|
|
989
978
|
const structuredDataTags = get__default["default"](structuredData, ["structuredDataTags"], []);
|
|
979
|
+
|
|
990
980
|
let articleData = {};
|
|
991
981
|
|
|
992
982
|
if (!isStructuredDataEmpty) {
|
|
@@ -1015,32 +1005,13 @@ function StructuredDataTags({ structuredData = {} }, config, pageType, response
|
|
|
1015
1005
|
if (!isStructuredDataEmpty && pageType === "story-page") {
|
|
1016
1006
|
const newsArticleTags = generateNewsArticleTags();
|
|
1017
1007
|
newsArticleTags ? tags.push(storyTags(), newsArticleTags) : tags.push(storyTags());
|
|
1018
|
-
if (story["story-template"] === "recipe") {
|
|
1019
|
-
const recipeTags = generateRecipePageSchema(story);
|
|
1020
|
-
recipeTags.image = Object.assign({
|
|
1021
|
-
"@type": "ImageObject"
|
|
1022
|
-
}, generateArticleImageData(story["hero-image-s3-key"], publisherConfig));
|
|
1023
|
-
recipeTags.video = Object.assign({
|
|
1024
|
-
"@type": "VideoObject"
|
|
1025
|
-
}, generateVideoArticleData(structuredData, story, publisherConfig, timezone));
|
|
1026
|
-
|
|
1027
|
-
tags.push(ldJson("Recipe", recipeTags));
|
|
1028
|
-
}
|
|
1029
1008
|
}
|
|
1030
1009
|
|
|
1031
1010
|
if (!isStructuredDataEmpty && pageType === "story-page-amp") {
|
|
1032
1011
|
const newsArticleTags = generateNewsArticleTags();
|
|
1033
1012
|
newsArticleTags ? tags.push(storyTags(), newsArticleTags) : tags.push(storyTags());
|
|
1034
|
-
if (story["story-template"] === "
|
|
1035
|
-
|
|
1036
|
-
recipeTags.image = Object.assign({
|
|
1037
|
-
"@type": "ImageObject"
|
|
1038
|
-
}, generateArticleImageData(story["hero-image-s3-key"], publisherConfig));
|
|
1039
|
-
recipeTags.video = Object.assign({
|
|
1040
|
-
"@type": "VideoObject"
|
|
1041
|
-
}, generateVideoArticleData(structuredData, story, publisherConfig, timezone));
|
|
1042
|
-
|
|
1043
|
-
tags.push(ldJson("Recipe", recipeTags));
|
|
1013
|
+
if (story["story-template"] === "visual-story") {
|
|
1014
|
+
tags.push(visualStorySchema());
|
|
1044
1015
|
}
|
|
1045
1016
|
}
|
|
1046
1017
|
|
|
@@ -1084,6 +1055,48 @@ function StructuredDataTags({ structuredData = {} }, config, pageType, response
|
|
|
1084
1055
|
return {};
|
|
1085
1056
|
}
|
|
1086
1057
|
|
|
1058
|
+
function visualStorySchema() {
|
|
1059
|
+
if (!story || !publisherConfig) return null;
|
|
1060
|
+
const storyCards = get__default["default"](story, ["cards"], []).filter(card => getAllowedCards(card));
|
|
1061
|
+
const galleryItems = storyCards.map(card => {
|
|
1062
|
+
const imageElement = card["story-elements"].find(el => el.type === "image");
|
|
1063
|
+
if (!imageElement) return; // for now schema is added only for images
|
|
1064
|
+
const titleElement = card["story-elements"].find(el => el.type === "title");
|
|
1065
|
+
const textElements = card["story-elements"].filter(el => el.type === "text" && el.subtype !== "cta");
|
|
1066
|
+
const description = textElements.reduce((acc, current) => `${acc}. ${current.text}`, "");
|
|
1067
|
+
return {
|
|
1068
|
+
"@type": "ImageObject",
|
|
1069
|
+
image: imageUrl(publisherConfig, imageElement["image-s3-key"]),
|
|
1070
|
+
name: get__default["default"](titleElement, ["text"]),
|
|
1071
|
+
contentUrl: story.url,
|
|
1072
|
+
description: description,
|
|
1073
|
+
caption: get__default["default"](imageElement, ["title"])
|
|
1074
|
+
};
|
|
1075
|
+
});
|
|
1076
|
+
const heroImgSrc = story["hero-image-s3-key"];
|
|
1077
|
+
if (heroImgSrc) {
|
|
1078
|
+
galleryItems.unshift({
|
|
1079
|
+
"@type": "ImageObject",
|
|
1080
|
+
image: imageUrl(publisherConfig, heroImgSrc),
|
|
1081
|
+
name: get__default["default"](story, ["headline"]),
|
|
1082
|
+
contentUrl: story.url,
|
|
1083
|
+
description: get__default["default"](story, ["subheadline"]),
|
|
1084
|
+
caption: get__default["default"](story, ["hero-image-caption"])
|
|
1085
|
+
});
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
const metaDescription = get__default["default"](story, ["seo", "meta-description"], "");
|
|
1089
|
+
const subHeadline = get__default["default"](story, ["subheadline"], "");
|
|
1090
|
+
const headline = get__default["default"](story, ["headline"], "");
|
|
1091
|
+
const schema = Object.assign({}, getSchemaMainEntityOfPage(story.url), {
|
|
1092
|
+
name: story.headline || "Media Gallery",
|
|
1093
|
+
description: metaDescription || subHeadline || headline,
|
|
1094
|
+
// author: authorData(story.authors, [], publisherConfig),
|
|
1095
|
+
hasPart: { "@type": "ImageGallery", associatedMedia: galleryItems }
|
|
1096
|
+
});
|
|
1097
|
+
return ldJson("MediaGallery", schema);
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1087
1100
|
// All Pages have: Publisher, Site
|
|
1088
1101
|
// Story Page have : Article/NewsArticle/LiveBlog/Review as appropriate
|
|
1089
1102
|
return tags;
|
package/package.json
CHANGED
|
@@ -141,30 +141,3 @@ export function generateAuthorPageSchema(publisherConfig, data, url) {
|
|
|
141
141
|
},
|
|
142
142
|
};
|
|
143
143
|
}
|
|
144
|
-
|
|
145
|
-
export function generateRecipePageSchema(story) {
|
|
146
|
-
const { headline, url, "author-name": authorName, description, metadata } = story;
|
|
147
|
-
const servings = get(metadata, ["servings"], "");
|
|
148
|
-
const ingredients = get(metadata, ["ingredients"], "");
|
|
149
|
-
const recipeAttributes = get(metadata, ["story-attributes"], "");
|
|
150
|
-
const cuisine = get(recipeAttributes, ["cuisine", "0"], "");
|
|
151
|
-
const preperationtime = get(recipeAttributes, ["preparationtime", "0"], "");
|
|
152
|
-
const cookingtime = get(recipeAttributes, ["cookingtime", "0"], "");
|
|
153
|
-
|
|
154
|
-
return {
|
|
155
|
-
"@context": "https://schema.org/",
|
|
156
|
-
"@type": "Recipe",
|
|
157
|
-
name: headline,
|
|
158
|
-
url: url,
|
|
159
|
-
author: {
|
|
160
|
-
"@type": "Person",
|
|
161
|
-
name: authorName,
|
|
162
|
-
},
|
|
163
|
-
description: description,
|
|
164
|
-
recipeIngredient: ingredients,
|
|
165
|
-
prepTime: preperationtime,
|
|
166
|
-
cookTime: cookingtime,
|
|
167
|
-
recipeCuisine: cuisine,
|
|
168
|
-
recipeYield: servings,
|
|
169
|
-
};
|
|
170
|
-
}
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import get from "lodash/get";
|
|
2
|
-
import { getQueryParams, stripMillisecondsFromTime } from "../utils";
|
|
2
|
+
import { getAllowedCards, getQueryParams, stripMillisecondsFromTime } from "../utils";
|
|
3
3
|
import { generateTagsForEntity } from "./entity";
|
|
4
4
|
import {
|
|
5
5
|
generateAuthorPageSchema,
|
|
6
|
-
generateRecipePageSchema,
|
|
7
6
|
getSchemaBlogPosting,
|
|
8
7
|
getSchemaBreadcrumbList,
|
|
9
8
|
getSchemaContext,
|
|
@@ -473,6 +472,7 @@ export function StructuredDataTags({ structuredData = {} }, config, pageType, re
|
|
|
473
472
|
const isStructuredDataEmpty = Object.keys(structuredData).length === 0;
|
|
474
473
|
const enableBreadcrumbList = get(structuredData, ["enableBreadcrumbList"], true);
|
|
475
474
|
const structuredDataTags = get(structuredData, ["structuredDataTags"], []);
|
|
475
|
+
|
|
476
476
|
let articleData = {};
|
|
477
477
|
|
|
478
478
|
if (!isStructuredDataEmpty) {
|
|
@@ -501,44 +501,13 @@ export function StructuredDataTags({ structuredData = {} }, config, pageType, re
|
|
|
501
501
|
if (!isStructuredDataEmpty && pageType === "story-page") {
|
|
502
502
|
const newsArticleTags = generateNewsArticleTags();
|
|
503
503
|
newsArticleTags ? tags.push(storyTags(), newsArticleTags) : tags.push(storyTags());
|
|
504
|
-
if (story["story-template"] === "recipe") {
|
|
505
|
-
const recipeTags = generateRecipePageSchema(story);
|
|
506
|
-
recipeTags.image = Object.assign(
|
|
507
|
-
{
|
|
508
|
-
"@type": "ImageObject",
|
|
509
|
-
},
|
|
510
|
-
generateArticleImageData(story["hero-image-s3-key"], publisherConfig)
|
|
511
|
-
);
|
|
512
|
-
recipeTags.video = Object.assign(
|
|
513
|
-
{
|
|
514
|
-
"@type": "VideoObject",
|
|
515
|
-
},
|
|
516
|
-
generateVideoArticleData(structuredData, story, publisherConfig, timezone)
|
|
517
|
-
);
|
|
518
|
-
|
|
519
|
-
tags.push(ldJson("Recipe", recipeTags));
|
|
520
|
-
}
|
|
521
504
|
}
|
|
522
505
|
|
|
523
506
|
if (!isStructuredDataEmpty && pageType === "story-page-amp") {
|
|
524
507
|
const newsArticleTags = generateNewsArticleTags();
|
|
525
508
|
newsArticleTags ? tags.push(storyTags(), newsArticleTags) : tags.push(storyTags());
|
|
526
|
-
if (story["story-template"] === "
|
|
527
|
-
|
|
528
|
-
recipeTags.image = Object.assign(
|
|
529
|
-
{
|
|
530
|
-
"@type": "ImageObject",
|
|
531
|
-
},
|
|
532
|
-
generateArticleImageData(story["hero-image-s3-key"], publisherConfig)
|
|
533
|
-
);
|
|
534
|
-
recipeTags.video = Object.assign(
|
|
535
|
-
{
|
|
536
|
-
"@type": "VideoObject",
|
|
537
|
-
},
|
|
538
|
-
generateVideoArticleData(structuredData, story, publisherConfig, timezone)
|
|
539
|
-
);
|
|
540
|
-
|
|
541
|
-
tags.push(ldJson("Recipe", recipeTags));
|
|
509
|
+
if (story["story-template"] === "visual-story") {
|
|
510
|
+
tags.push(visualStorySchema());
|
|
542
511
|
}
|
|
543
512
|
}
|
|
544
513
|
|
|
@@ -588,6 +557,48 @@ export function StructuredDataTags({ structuredData = {} }, config, pageType, re
|
|
|
588
557
|
return {};
|
|
589
558
|
}
|
|
590
559
|
|
|
560
|
+
function visualStorySchema() {
|
|
561
|
+
if (!story || !publisherConfig) return null;
|
|
562
|
+
const storyCards = get(story, ["cards"], []).filter((card) => getAllowedCards(card));
|
|
563
|
+
const galleryItems = storyCards.map((card) => {
|
|
564
|
+
const imageElement = card["story-elements"].find((el) => el.type === "image");
|
|
565
|
+
if (!imageElement) return; // for now schema is added only for images
|
|
566
|
+
const titleElement = card["story-elements"].find((el) => el.type === "title");
|
|
567
|
+
const textElements = card["story-elements"].filter((el) => el.type === "text" && el.subtype !== "cta");
|
|
568
|
+
const description = textElements.reduce((acc, current) => `${acc}. ${current.text}`, "");
|
|
569
|
+
return {
|
|
570
|
+
"@type": "ImageObject",
|
|
571
|
+
image: imageUrl(publisherConfig, imageElement["image-s3-key"]),
|
|
572
|
+
name: get(titleElement, ["text"]),
|
|
573
|
+
contentUrl: story.url,
|
|
574
|
+
description: description,
|
|
575
|
+
caption: get(imageElement, ["title"]),
|
|
576
|
+
};
|
|
577
|
+
});
|
|
578
|
+
const heroImgSrc = story["hero-image-s3-key"];
|
|
579
|
+
if (heroImgSrc) {
|
|
580
|
+
galleryItems.unshift({
|
|
581
|
+
"@type": "ImageObject",
|
|
582
|
+
image: imageUrl(publisherConfig, heroImgSrc),
|
|
583
|
+
name: get(story, ["headline"]),
|
|
584
|
+
contentUrl: story.url,
|
|
585
|
+
description: get(story, ["subheadline"]),
|
|
586
|
+
caption: get(story, ["hero-image-caption"]),
|
|
587
|
+
});
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
const metaDescription = get(story, ["seo", "meta-description"], "");
|
|
591
|
+
const subHeadline = get(story, ["subheadline"], "");
|
|
592
|
+
const headline = get(story, ["headline"], "");
|
|
593
|
+
const schema = Object.assign({}, getSchemaMainEntityOfPage(story.url), {
|
|
594
|
+
name: story.headline || "Media Gallery",
|
|
595
|
+
description: metaDescription || subHeadline || headline,
|
|
596
|
+
// author: authorData(story.authors, [], publisherConfig),
|
|
597
|
+
hasPart: { "@type": "ImageGallery", associatedMedia: galleryItems },
|
|
598
|
+
});
|
|
599
|
+
return ldJson("MediaGallery", schema);
|
|
600
|
+
}
|
|
601
|
+
|
|
591
602
|
// All Pages have: Publisher, Site
|
|
592
603
|
// Story Page have : Article/NewsArticle/LiveBlog/Review as appropriate
|
|
593
604
|
return tags;
|
package/src/utils.js
CHANGED
|
@@ -40,3 +40,19 @@ export function getWatermarkImage(story, cdnSrc, cdnURL) {
|
|
|
40
40
|
}
|
|
41
41
|
return watermarkImageS3Key;
|
|
42
42
|
}
|
|
43
|
+
|
|
44
|
+
export function getAllowedCards(card) {
|
|
45
|
+
const storyElementWhitelist = ["text", "title", "image", "video"];
|
|
46
|
+
const validCards = card["story-elements"].some((el) => {
|
|
47
|
+
if (el.type === "jsembed") {
|
|
48
|
+
const videoUrl = el && atob(`${el["embed-js"]}`);
|
|
49
|
+
const formatWhitelist = ["mp4", "webm", "ogv"];
|
|
50
|
+
const isValidVideoUrl = formatWhitelist.some((format) => {
|
|
51
|
+
return videoUrl && videoUrl.endsWith(format);
|
|
52
|
+
});
|
|
53
|
+
return isValidVideoUrl;
|
|
54
|
+
}
|
|
55
|
+
return storyElementWhitelist.includes(el.type);
|
|
56
|
+
});
|
|
57
|
+
return validCards;
|
|
58
|
+
}
|