@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 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"] === "recipe") {
1035
- const recipeTags = generateRecipePageSchema(story);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quintype/seo",
3
- "version": "1.48.0-recipe-schema.0",
3
+ "version": "1.48.0",
4
4
  "description": "SEO Modules for Quintype",
5
5
  "main": "dist/index.cjs.js",
6
6
  "repository": "https://github.com/quintype/quintype-node-seo",
@@ -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"] === "recipe") {
527
- const recipeTags = generateRecipePageSchema(story);
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
+ }