@quintype/seo 1.40.9 → 1.40.10

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,15 @@
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.40.10](https://github.com/quintype/quintype-node-seo/compare/v1.40.5...v1.40.10) (2022-07-29)
6
+
7
+
8
+ ### Bug Fixes
9
+
10
+ * Amphtml error ([#526](https://github.com/quintype/quintype-node-seo/issues/526)) ([24b6c61](https://github.com/quintype/quintype-node-seo/commit/24b6c616118f3db1a2cdda0d98d2827bf2a0d099))
11
+ * **embed-url:** use fallback when story elements are not there ([#530](https://github.com/quintype/quintype-node-seo/issues/530)) ([cf9ab06](https://github.com/quintype/quintype-node-seo/commit/cf9ab06d022a1b2d4c3d6843adcea32606712669))
12
+ * **video-structured-data:** use correct embed url ([#528](https://github.com/quintype/quintype-node-seo/issues/528)) ([abe449f](https://github.com/quintype/quintype-node-seo/commit/abe449fd7e75d45f9b9896444f3ef644ca2cf497))
13
+
5
14
  ### [1.40.9](https://github.com/quintype/quintype-node-seo/compare/v1.40.8...v1.40.9) (2022-07-12)
6
15
 
7
16
 
package/dist/index.cjs.js CHANGED
@@ -5,12 +5,12 @@ Object.defineProperty(exports, '__esModule', { value: true });
5
5
  var lodash = require('lodash');
6
6
  var React = require('react');
7
7
  var ReactDomServer = require('react-dom/server');
8
+ var get = require('lodash/get');
8
9
  var url = require('url');
9
10
  var dateFnsTz = require('date-fns-tz');
10
- var quintypeJs = require('quintype-js');
11
- var get = require('lodash/get');
12
11
  var isUndefined = require('lodash/isUndefined');
13
12
  var omitBy = require('lodash/omitBy');
13
+ var quintypeJs = require('quintype-js');
14
14
 
15
15
  function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
16
16
 
@@ -49,375 +49,185 @@ function isStoryPublic(story) {
49
49
  return story.access === undefined || story.access === null || story.access === 'public';
50
50
  }
51
51
 
52
- function buildTagsFromStory(config, story, url = {}, data = {}) {
53
- if (!story) return;
54
-
55
- function getStoryCardMetadata(cardId) {
56
- const { metadata = {} } = story.cards.find(card => card.id === cardId) || {};
57
- const urlWithCardId = `${config["sketches-host"]}/${story.slug}?cardId=${cardId}`;
52
+ function showAmpTag({ ampStoryPages = true }, pageType, story) {
53
+ if (!ampStoryPages || pageType !== 'story-page') {
54
+ return false;
55
+ }
58
56
 
59
- if (metadata && !lodash.isEmpty(metadata) && metadata["social-share"]) {
60
- return {
61
- title: metadata["social-share"].title || story.headline,
62
- description: metadata["social-share"].message || story.summary,
63
- ogUrl: urlWithCardId,
64
- ogTitle: metadata["social-share"].title || story.headline,
65
- ogDescription: metadata["social-share"].message || story.summary
66
- };
67
- }
68
- return metadata;
57
+ if (ampStoryPages === 'public' && !isStoryPublic(story)) {
58
+ return false;
69
59
  }
70
60
 
71
- const seo = story.seo || {};
61
+ return true;
62
+ }
72
63
 
73
- const storyUrl = story.url || `${config["sketches-host"]}/${story.slug}`;
64
+ const getDomain = (url, domainSlug) => {
65
+ const domain = domainSlug ? new URL(url).origin : '';
66
+ try {
67
+ return domain;
68
+ } catch (err) {
69
+ return "";
70
+ }
71
+ };
74
72
 
75
- const customSeo = lodash.get(data, ["data", "customSeo"], {});
76
- const authors = lodash.get(story, ["authors"], []).map(author => author.name);
77
- const title = customSeo.title || seo["meta-title"] || story.headline;
78
- const pageTitle = customSeo["page-title"] || seo["meta-title"] || story.headline;
79
- const description = customSeo.description || seo["meta-description"] || story.summary;
80
- const keywords = (customSeo.keywords || seo["meta-keywords"] || (story.tags || []).map(tag => tag.name)).join(",");
81
- const ogUrl = customSeo.ogUrl || lodash.get(seo, ["og", "url"]) || storyUrl;
82
- const getOgTitle = customSeo.ogTitle || lodash.get(story, ["alternative", "social", "default", "headline"], story.headline) || story.headline;
83
- const ogDescription = customSeo.ogDescription || story.summary;
84
- const storyMetaData = {
85
- title,
86
- "page-title": pageTitle,
87
- description,
88
- keywords,
89
- canonicalUrl: story["canonical-url"] || storyUrl,
90
- ogUrl,
91
- ogTitle: getOgTitle,
92
- ogDescription,
93
- storyUrl,
94
- author: authors
95
- };
73
+ /**
74
+ * StoryAmpTags adds the amphref to stories which support amp.
75
+ *
76
+ * @extends Generator
77
+ * @param {*} seoConfig
78
+ * @param {(boolean|"public")} seoConfig.ampStoryPages Should amp story pages be shown for all stories (true), not shown (false), or only be shown for public stories ("public"). Default: true
79
+ * @param {(boolean)} seoConfig.appendHostToAmpUrl If set to true, the url to be appended to the slug is computed based on the currentHostUrl and the domain slug, else the url is taken as the sketches host. Default: false
80
+ * @param {(boolean)} seoConfig.decodeAmpUrl If set to true, the storySlug that goes as the amp href url is decoded, else the storyslug is encoded. Default: false
81
+ * @param {...*} params See {@link Generator} for other Parameters
82
+ */
83
+ function StoryAmpTags(seoConfig, config, pageType, data = {}, opts) {
96
84
 
97
- if (url.query && url.query.cardId) {
98
- const storyCardMetadata = getStoryCardMetadata(url.query.cardId);
99
- return Object.assign({}, storyMetaData, storyCardMetadata); //TODO rewrite in spread syntax, add babel plugin
85
+ const story = get__default["default"](data, ["data", "story"], {});
86
+ const { currentHostUrl = '', domainSlug } = data;
87
+ // TODO: Remove this condition and always make absolute URL if that's better for AMP discoverability.
88
+ const ampUrlAppend = seoConfig.appendHostToAmpUrl ? getDomain(currentHostUrl, domainSlug) || config['sketches-host'] : '';
89
+ const storySlug = seoConfig.decodeAmpUrl ? decodeURIComponent(story.slug) : encodeURIComponent(story.slug);
90
+ const ampUrl = story["story-template"] === "visual-story" ? `${ampUrlAppend}/${storySlug}` : `${ampUrlAppend}/amp/story/${storySlug}`;
91
+
92
+ if (showAmpTag(seoConfig, pageType, story)) {
93
+ return [{
94
+ tag: 'link',
95
+ rel: 'amphtml',
96
+ href: ampUrl
97
+ }];
98
+ } else {
99
+ return [];
100
100
  }
101
+ }
101
102
 
102
- return storyMetaData;
103
+ /**
104
+ * AuthorTags adds the twitter:creator tag for story pages
105
+ *
106
+ * @extends Generator
107
+ * @param {*} seoConfig
108
+ * @param {...*} params See {@link Generator} for other Parameters
109
+ */
110
+ function AuthorTags(seoConfig, config, pageType, data, { url }) {
111
+ if (pageType != "story-page" || pageType != "story-page-amp") return [];
112
+
113
+ return [{
114
+ name: "twitter:creator",
115
+ content: lodash.get(data, ["data", "story", "author-name"])
116
+ }];
103
117
  }
104
118
 
105
- function buildTagsFromTopic(config, tag, url = {}, data) {
106
- if (lodash.isEmpty(tag)) return;
107
- const customSeo = lodash.get(data, ["data", "customSeo"], {});
108
- const tagName = customSeo.title || tag["meta-title"] || tag.name;
109
- const pageTitle = customSeo["page-title"] || tagName;
110
- const tagDescription = customSeo.description || tag["meta-description"];
111
- const description = `Read stories listed under on ${tag.name}`;
112
- const tagUrl = `${config["sketches-host"]}${url.pathname}`;
113
- const canonicalSlug = tag["canonical-slug"] || url.pathname;
114
- const canonicalUrl = `${config["sketches-host"]}${canonicalSlug}`;
115
- const ogTitle = customSeo.ogTitle || tagName;
116
- const ogDescription = customSeo.ogDescription || description;
117
- const topicMetaData = {
118
- title: tagName,
119
- "page-title": pageTitle,
120
- description: tagDescription || description,
121
- keywords: tagName,
122
- canonicalUrl,
123
- ogUrl: tagUrl,
124
- ogTitle,
125
- ogDescription
119
+ function getTitle$1(config) {
120
+ return config["publisher-settings"] ? config["publisher-settings"]["title"] : config["publisher-name"];
121
+ }
122
+
123
+ function generateStaticData(config) {
124
+ const title = getTitle$1(config);
125
+ const themeConfig = config["theme-attributes"] || {};
126
+ const publicIntegrations = get__default["default"](config, ['public-integrations'], {});
127
+ const staticData = {
128
+ "twitter:site": title,
129
+ "twitter:domain": config["sketches-host"],
130
+ "twitter:app:name:ipad": themeConfig["twitter_app_name_ipad"],
131
+ "twitter:app:name:googleplay": themeConfig["twitter_app_name_googleplay"],
132
+ "twitter:app:id:googleplay": themeConfig["twitter_app_id_googleplay"],
133
+ "twitter:app:name:iphone": themeConfig["twitter_app_name_iphone"],
134
+ "twitter:app:id:iphone": themeConfig["twitter_app_id_iphone"],
135
+ "apple-itunes-app": themeConfig["apple_itunes_app"],
136
+ "google-play-app": themeConfig["google_play_app"],
137
+ "fb:app_id": get__default["default"](publicIntegrations, ['facebook', 'app-id']) || get__default["default"](themeConfig, ["fb_app_id"]),
138
+ "fb:pages": themeConfig["fb_pages"],
139
+ "og:site_name": title
126
140
  };
127
141
 
128
- return topicMetaData;
142
+ return omitBy__default["default"](staticData, isUndefined__default["default"]);
129
143
  }
130
144
 
131
- function buildTagsFromAuthor(config, author, url = {}, data) {
132
- if (lodash.isEmpty(author)) return;
145
+ function generateImageObject(config = {}) {
146
+ const { "theme-attributes": themeConfig = {} } = config;
147
+ return {
148
+ "@context": "http://schema.org",
149
+ "@type": "ImageObject",
150
+ "author": config['publisher-name'],
151
+ "contentUrl": themeConfig.logo,
152
+ "url": themeConfig.logo,
153
+ "name": "logo",
154
+ "width": themeConfig.logo && getQueryParams(themeConfig.logo).width,
155
+ "height": themeConfig.logo && getQueryParams(themeConfig.logo).height
156
+ };
157
+ }
133
158
 
134
- const customSeo = lodash.get(data, ["data", "customSeo"], {});
135
- const title = customSeo.title || author.name;
136
- const pageTitle = customSeo["page-title"] || title;
137
- const description = customSeo.description || author.bio || `View all articles written by ${title}`;
138
- const ogTitle = customSeo.ogTitle || author.name;
139
- const authorUrl = `${config["sketches-host"]}${url.pathname}`;
140
- const ogDescription = customSeo.ogDescription || description;
159
+ function generateStructuredData(config = {}) {
160
+ const title = getTitle$1(config);
161
+ const { "theme-attributes": themeConfig, "social-links": socialLinks, "seo-metadata": seoMetadata = [] } = config;
162
+ const homePageSeo = seoMetadata.find(page => page["owner-type"] === "home") || {};
163
+ const { "page-title": pageTitle = "", description = "", keywords = "" } = get__default["default"](homePageSeo, ["data"], {});
164
+ if (!themeConfig || !themeConfig.logo) {
165
+ return {};
166
+ }
167
+ let enableStructuredDataForNewsArticle = themeConfig['structured_data_news_article'] || false;
168
+ if (config.hasOwnProperty('enableStructuredDataForNewsArticle') && typeof config.enableStructuredDataForNewsArticle !== "undefined") {
169
+ enableStructuredDataForNewsArticle = config.enableStructuredDataForNewsArticle;
170
+ }
141
171
 
142
172
  return {
143
- title,
144
- "page-title": pageTitle,
145
- description,
146
- keywords: `${title},${config["publisher-name"]}`,
147
- canonicalUrl: authorUrl,
148
- ogUrl: authorUrl,
149
- ogTitle,
150
- ogDescription
173
+ organization: {
174
+ name: title,
175
+ url: config["sketches-host"],
176
+ logo: generateImageObject(config),
177
+ sameAs: socialLinks ? Object.values(socialLinks) : []
178
+ },
179
+ enableNewsArticle: !!enableStructuredDataForNewsArticle,
180
+ storyUrlAsMainEntityUrl: !!enableStructuredDataForNewsArticle,
181
+ enableVideo: !themeConfig['structured_data_enable_video'],
182
+ enableLiveBlog: !themeConfig['structured_data_enable_live_blog'],
183
+ website: {
184
+ url: config["sketches-host"],
185
+ searchpath: "search?q={query}",
186
+ queryinput: "required name=query",
187
+ name: pageTitle || title,
188
+ headline: description,
189
+ keywords
190
+ }
151
191
  };
152
192
  }
153
193
 
154
- function buildCustomTags(customTags = {}, pageType = "") {
155
- const configObject = customTags[pageType];
156
- if (configObject) {
157
- return configObject;
194
+ function pickImageFromCard(story, cardId) {
195
+ const { metadata = {} } = story.cards.find(card => card.id === cardId) || {};
196
+ if (metadata && !lodash.isEmpty(metadata) && lodash.get(metadata, ["social-share", "image", "key"], false)) {
197
+ const alt = metadata["social-share"].image.attribution || metadata["social-share"].title || metadata["social-share"].message || getAttribution(story);
198
+ return {
199
+ image: new quintypeJs.FocusedImage(metadata["social-share"].image.key, metadata["social-share"].image.metadata || {}),
200
+ alt
201
+ };
158
202
  }
159
- return {};
160
203
  }
161
204
 
162
- function buildTagsFromStaticPage(config, page, url = {}, data) {
163
- const seoData = lodash.get(page, ["metadata", "seo"], {});
164
- const customSeo = lodash.get(data, ["data", "customSeo"], {});
165
- if (lodash.isEmpty(seoData) && lodash.isEmpty(customSeo)) return;
205
+ function getAttribution(story) {
206
+ return story["hero-image-attribution"] || story.summary || lodash.get(story, ["alternative", "home", "default", "headline"]) || story.headline;
207
+ }
166
208
 
167
- const { "meta-title": metaTitle, "meta-description": metaDescription, "meta-keywords": keywords } = seoData;
209
+ /**
210
+ * priority:
211
+ * 1. alternate social image
212
+ * 2. alternate hero image
213
+ * 3. hero image
214
+ * 4. "fallbackSocialImage" from seo config
215
+ * 5. logo_url from /api/v1/config > theme-attributes
216
+ * 5. logo from /api/v1/config > theme-attributes
217
+ * 6. undefined (meta tag won't get created)
218
+ */
219
+ function pickImageFromStory({ story, config, seoConfig }) {
220
+ function getAlt(type, key, fallback) {
221
+ return lodash.get(story, ["alternative", `${type}`, "default", "hero-image", `${key}`], fallback);
222
+ }
168
223
 
169
- const title = customSeo.title || metaTitle || page.title;
170
- const pageTitle = customSeo["page-title"] || title;
171
- const description = customSeo.description || metaDescription;
172
- const ogTitle = customSeo.ogTitle || title;
173
- const staticPageUrl = `${config["sketches-host"]}${url.pathname}`;
174
- const ogDescription = customSeo.ogDescription || description;
175
-
176
- return {
177
- title,
178
- "page-title": pageTitle,
179
- description,
180
- keywords: `${title},${config["publisher-name"]}`,
181
- canonicalUrl: staticPageUrl,
182
- ogUrl: staticPageUrl,
183
- ogTitle,
184
- ogDescription,
185
- keywords: customSeo.keywords || keywords
186
- };
187
- }
188
-
189
- // The findRelevantConfig method call has no ownerId for home page.
190
- // This causes the seoMetadata to be undefined.
191
- // So the default value for the ownerId is set to null.
192
-
193
- function getSeoData(config, pageType, data, url = {}, seoConfig = {}) {
194
- function findRelevantConfig(ownerType, ownerId = null) {
195
- const seoMetadata = config["seo-metadata"].find(page => page["owner-type"] === ownerType && page["owner-id"] === ownerId) || {};
196
- const { sections = [] } = config;
197
- const section = sections.find(section => ownerType == "section" && section.id === ownerId) || {};
198
- const customSeo = lodash.get(data, ["data", "customSeo"], {});
199
- if (seoMetadata.data || section.id || !lodash.isEmpty(customSeo)) {
200
- const result = Object.assign({}, {
201
- "page-title": customSeo["page-title"] || section.name,
202
- title: customSeo.title || section.name,
203
- canonicalUrl: customSeo["canonicalUrl"] || section["section-url"] || undefined
204
- }, seoMetadata.data);
205
-
206
- if (!result.description) {
207
- const homeSeoData = config["seo-metadata"].find(page => page["owner-type"] === "home") || {
208
- data: { description: "" }
209
- };
210
- result.description = customSeo.description || homeSeoData.data.description;
211
- }
212
-
213
- result.ogTitle = customSeo.ogTitle || result.title;
214
- result.ogDescription = customSeo.ogDescription || result.description;
215
- return result;
216
- }
217
- }
218
-
219
- if (seoConfig.customTags && seoConfig.customTags[pageType]) {
220
- return buildCustomTags(seoConfig.customTags, pageType);
221
- }
222
-
223
- function getShellSeoData(config) {
224
- const seoMetadata = config["seo-metadata"].find(meta => meta["owner-type"] === "home") || {};
225
- return Object.assign({}, seoMetadata.data, { canonicalUrl: SKIP_CANONICAL });
226
- }
227
-
228
- switch (pageType) {
229
- case "home-page":
230
- return findRelevantConfig("home");
231
- case "section-page":
232
- return findRelevantConfig("section", lodash.get(data, ["data", "section", "id"])) || getSeoDataFromCollection(config, data) || getSeoData(config, "home-page", data, url);
233
- case "collection-page":
234
- return getSeoDataFromCollection(config, data) || getSeoData(config, "home-page", data, url);
235
- case "tag-page":
236
- return buildTagsFromTopic(config, lodash.get(data, ["data", "tag"]), url, data) || getSeoData(config, "home-page", data, url);
237
- case "story-page":
238
- return buildTagsFromStory(config, lodash.get(data, ["data", "story"]), url, data) || getSeoData(config, "home-page", data, url);
239
- case "visual-story":
240
- return buildTagsFromStory(config, lodash.get(data, ["story"]), url, data) || getSeoData(config, "home-page", data, url);
241
- case "story-page-amp":
242
- return buildTagsFromStory(config, lodash.get(data, ["data", "story"]), url, data) || getSeoData(config, "home-page", data, url);
243
- case "author-page":
244
- return buildTagsFromAuthor(config, lodash.get(data, ["data", "author"], {}), url, data) || getSeoData(config, "home-page", data, url);
245
- case "static-page":
246
- return buildTagsFromStaticPage(config, lodash.get(data, ["data", "page"], {}), url, data) || getSeoData(config, "home-page", data, url);
247
- case "shell":
248
- return getShellSeoData(config);
249
- default:
250
- return getSeoData(config, "home-page", data, url);
251
- }
252
- }
253
-
254
- function getSeoDataFromCollection(config, data) {
255
- if (lodash.get(data, ["data", "collection", "name"])) {
256
- let { name, summary } = lodash.get(data, ["data", "collection"]);
257
- const customSeo = lodash.get(data, ["data", "customSeo"], {});
258
-
259
- if (!summary) {
260
- summary = (getSeoData(config, "home-page", data) || {}).description;
261
- }
262
- const title = customSeo.title || name;
263
- const pageTitle = customSeo["page-title"] || name;
264
- const ogTitle = customSeo.ogTitle || title;
265
- const description = customSeo.description || summary;
266
- const ogDescription = customSeo.ogDescription || summary;
267
- return {
268
- "page-title": pageTitle,
269
- title,
270
- ogTitle,
271
- description,
272
- ogDescription,
273
- canonicalUrl: SKIP_CANONICAL
274
- };
275
- }
276
- }
277
-
278
- const SKIP_CANONICAL = "__SKIP__CANONICAL__";
279
-
280
- /**
281
- * TextTags adds the majority of basic tags, such as
282
- * * Canonical URLs
283
- * * Title and Description
284
- * * Keywords
285
- *
286
- * If the current URL contains a cardId in the query parameters, then the title and description will come from *card["social-share"]*
287
- *
288
- * @extends Generator
289
- * @param {*} seoConfig
290
- * @param {boolean} seoConfig.enableOgTags Add og tags for Facebook
291
- * @param {boolean} seoConfig.enableTwitterCards Add twitter tags
292
- * @param {boolean} seoConfig.enableNews Add tags for Google News, like news_keywords
293
- * @param {Object} seoConfig.customTags Add tags for a custom page type. Usually looks like `{"custom-page": {"title": "value", "canonicalUrl": "value"}}`
294
- * @param {...*} params See {@link Generator} for other Parameters
295
- */
296
- function TextTags(seoConfig, config, pageType, data, { url }) {
297
- const seoData = getSeoData(config, pageType, data, url, seoConfig);
298
- const customSeo = lodash.get(data, ["data", "customSeo"], {});
299
-
300
- if (!seoData) return [];
301
-
302
- const currentUrl = `${config["sketches-host"]}${url.pathname}`;
303
-
304
- const basicTags = {
305
- description: customSeo.description || seoData.description,
306
- title: customSeo.title || seoData.title,
307
- keywords: customSeo.keywords || seoData.keywords
308
- };
309
-
310
- const ogUrl = customSeo.ogUrl || seoData.ogUrl || seoData.canonicalUrl || currentUrl;
311
- const ogTags = seoConfig.enableOgTags ? {
312
- "og:type": pageType === "story-page" || pageType === "story-page-amp" ? "article" : "website",
313
- "og:url": ogUrl === SKIP_CANONICAL ? undefined : ogUrl,
314
- "og:title": customSeo.ogTitle || seoData.ogTitle,
315
- "og:description": customSeo.ogDescription || seoData.ogDescription
316
- } : undefined;
317
-
318
- const twitterTags = seoConfig.enableTwitterCards ? {
319
- "twitter:card": "summary_large_image",
320
- "twitter:title": customSeo.twitterTitle || seoData.ogTitle,
321
- "twitter:description": customSeo.twitterDescription || seoData.ogDescription
322
- } : undefined;
323
-
324
- const allTags = Object.assign(basicTags, ogTags, twitterTags);
325
-
326
- const commonTags = [{ tag: "title", children: customSeo.title || data.title || seoData["page-title"] }];
327
-
328
- const canonical = seoData.canonicalUrl || currentUrl;
329
- if (canonical != SKIP_CANONICAL) {
330
- commonTags.push({ tag: "link", rel: "canonical", href: canonical });
331
- }
332
-
333
- if (pageType === "story-page" || pageType === "story-page-amp") {
334
- commonTags.push({ name: "author", content: seoData.author });
335
- }
336
-
337
- if ((pageType === "story-page" || pageType === "story-page-amp") && seoConfig.enableNews) {
338
- commonTags.push({ name: "news_keywords", content: seoData.keywords });
339
- if (lodash.get(data, ["data", "story", "seo", "meta-google-news-standout"])) commonTags.push({ tag: "link", rel: "standout", href: seoData.storyUrl || currentUrl });
340
- }
341
-
342
- return commonTags.concat(objectToTags(allTags));
343
- }
344
-
345
- function getTitle$1(seoConfig, config, pageType, data, params) {
346
- const customSeo = lodash.get(data, ["data", "customSeo"], {});
347
-
348
- if (lodash.get(data, ["title"])) return customSeo.title || lodash.get(data, ["title"]);
349
-
350
- if (lodash.get(data, ["data", "title"])) return customSeo.title || lodash.get(data, ["data", "title"]);
351
-
352
- const seoData = getSeoData(config, pageType, data, undefined, seoConfig) || {};
353
- return customSeo.title || seoData["page-title"];
354
- }
355
-
356
- /**
357
- * StaticTags puts whatever tags you've passed to it
358
- *
359
- * @extends Generator
360
- * @param {*} seoConfig
361
- * @param {Object} seoConfig.staticTags List of tags to be added. ex: `{"viewport": "width=device-width,initial-scale=1.0"}`
362
- * @param {...*} params See {@link Generator} for other Parameters
363
- */
364
- function StaticTags(seoConfig, config, pageType, data, { url }) {
365
- return objectToTags(seoConfig.staticTags || {});
366
- }
367
-
368
- /**
369
- * AuthorTags adds the twitter:creator tag for story pages
370
- *
371
- * @extends Generator
372
- * @param {*} seoConfig
373
- * @param {...*} params See {@link Generator} for other Parameters
374
- */
375
- function AuthorTags(seoConfig, config, pageType, data, { url }) {
376
- if (pageType != "story-page" || pageType != "story-page-amp") return [];
377
-
378
- return [{
379
- name: "twitter:creator",
380
- content: lodash.get(data, ["data", "story", "author-name"])
381
- }];
382
- }
383
-
384
- function pickImageFromCard(story, cardId) {
385
- const { metadata = {} } = story.cards.find(card => card.id === cardId) || {};
386
- if (metadata && !lodash.isEmpty(metadata) && lodash.get(metadata, ["social-share", "image", "key"], false)) {
387
- const alt = metadata["social-share"].image.attribution || metadata["social-share"].title || metadata["social-share"].message || getAttribution(story);
388
- return {
389
- image: new quintypeJs.FocusedImage(metadata["social-share"].image.key, metadata["social-share"].image.metadata || {}),
390
- alt
391
- };
392
- }
393
- }
394
-
395
- function getAttribution(story) {
396
- return story["hero-image-attribution"] || story.summary || lodash.get(story, ["alternative", "home", "default", "headline"]) || story.headline;
397
- }
398
-
399
- /**
400
- * priority:
401
- * 1. alternate social image
402
- * 2. alternate hero image
403
- * 3. hero image
404
- * 4. "fallbackSocialImage" from seo config
405
- * 5. logo_url from /api/v1/config > theme-attributes
406
- * 5. logo from /api/v1/config > theme-attributes
407
- * 6. undefined (meta tag won't get created)
408
- */
409
- function pickImageFromStory({ story, config, seoConfig }) {
410
- function getAlt(type, key, fallback) {
411
- return lodash.get(story, ["alternative", `${type}`, "default", "hero-image", `${key}`], fallback);
412
- }
413
-
414
- const alt = getAttribution(story);
415
- const fallbackSocialImage = lodash.get(seoConfig, ["fallbackSocialImage"]);
416
- const altHeroImg = getAlt("home", "hero-image-s3-key", null);
417
- const altSocialHeroImg = getAlt("social", "hero-image-s3-key", null);
418
- const storyHeroImage = lodash.get(story, ["hero-image-s3-key"]);
419
- const logo_url = lodash.get(config, ["theme-attributes", "logo_url"]);
420
- const logo = lodash.get(config, ["theme-attributes", "logo"]);
224
+ const alt = getAttribution(story);
225
+ const fallbackSocialImage = lodash.get(seoConfig, ["fallbackSocialImage"]);
226
+ const altHeroImg = getAlt("home", "hero-image-s3-key", null);
227
+ const altSocialHeroImg = getAlt("social", "hero-image-s3-key", null);
228
+ const storyHeroImage = lodash.get(story, ["hero-image-s3-key"]);
229
+ const logo_url = lodash.get(config, ["theme-attributes", "logo_url"]);
230
+ const logo = lodash.get(config, ["theme-attributes", "logo"]);
421
231
 
422
232
  if (altSocialHeroImg) {
423
233
  const metadata = getAlt("social", "hero-image-metadata", {});
@@ -523,6 +333,18 @@ function ImageTags(seoConfig, config, pageType, data, { url = {} }) {
523
333
  return tags;
524
334
  }
525
335
 
336
+ /**
337
+ * StaticTags puts whatever tags you've passed to it
338
+ *
339
+ * @extends Generator
340
+ * @param {*} seoConfig
341
+ * @param {Object} seoConfig.staticTags List of tags to be added. ex: `{"viewport": "width=device-width,initial-scale=1.0"}`
342
+ * @param {...*} params See {@link Generator} for other Parameters
343
+ */
344
+ function StaticTags(seoConfig, config, pageType, data, { url }) {
345
+ return objectToTags(seoConfig.staticTags || {});
346
+ }
347
+
526
348
  const getSchemaContext = { "@context": "http://schema.org" };
527
349
 
528
350
  function getSchemaType(type) {
@@ -1073,136 +895,314 @@ function StructuredDataTags({ structuredData = {} }, config, pageType, response
1073
895
  return tags;
1074
896
  }
1075
897
 
1076
- function showAmpTag({ ampStoryPages = true }, pageType, story) {
1077
- if (!ampStoryPages || pageType !== 'story-page') {
1078
- return false;
1079
- }
1080
-
1081
- if (ampStoryPages === 'public' && !isStoryPublic(story)) {
1082
- return false;
1083
- }
898
+ function buildTagsFromStory(config, story, url = {}, data = {}) {
899
+ if (!story) return;
1084
900
 
1085
- return true;
1086
- }
901
+ function getStoryCardMetadata(cardId) {
902
+ const { metadata = {} } = story.cards.find(card => card.id === cardId) || {};
903
+ const urlWithCardId = `${config["sketches-host"]}/${story.slug}?cardId=${cardId}`;
1087
904
 
1088
- const getDomain = (url, domainSlug) => {
1089
- const domain = domainSlug ? new URL(url).origin : '';
1090
- try {
1091
- return domain;
1092
- } catch (err) {
1093
- return "";
905
+ if (metadata && !lodash.isEmpty(metadata) && metadata["social-share"]) {
906
+ return {
907
+ title: metadata["social-share"].title || story.headline,
908
+ description: metadata["social-share"].message || story.summary,
909
+ ogUrl: urlWithCardId,
910
+ ogTitle: metadata["social-share"].title || story.headline,
911
+ ogDescription: metadata["social-share"].message || story.summary
912
+ };
913
+ }
914
+ return metadata;
1094
915
  }
1095
- };
1096
916
 
1097
- /**
1098
- * StoryAmpTags adds the amphref to stories which support amp.
1099
- *
1100
- * @extends Generator
1101
- * @param {*} seoConfig
1102
- * @param {(boolean|"public")} seoConfig.ampStoryPages Should amp story pages be shown for all stories (true), not shown (false), or only be shown for public stories ("public"). Default: true
1103
- * @param {(boolean)} seoConfig.appendHostToAmpUrl If set to true, the url to be appended to the slug is computed based on the currentHostUrl and the domain slug, else the url is taken as the sketches host. Default: false
1104
- * @param {(boolean)} seoConfig.decodeAmpUrl If set to true, the storySlug that goes as the amp href url is decoded, else the storyslug is encoded. Default: false
1105
- * @param {...*} params See {@link Generator} for other Parameters
1106
- */
1107
- function StoryAmpTags(seoConfig, config, pageType, data = {}, opts) {
917
+ const seo = story.seo || {};
1108
918
 
1109
- const story = get__default["default"](data, ["data", "story"], {});
1110
- const { currentHostUrl = '', domainSlug } = data;
1111
- // TODO: Remove this condition and always make absolute URL if that's better for AMP discoverability.
1112
- const ampUrlAppend = seoConfig.appendHostToAmpUrl ? getDomain(currentHostUrl, domainSlug) || config['sketches-host'] : '';
1113
- const storySlug = seoConfig.decodeAmpUrl ? decodeURIComponent(story.slug) : encodeURIComponent(story.slug);
1114
- const ampUrl = story["story-template"] === "visual-story" ? `${ampUrlAppend}/${storySlug}` : `${ampUrlAppend}/amp/story/${storySlug}`;
919
+ const storyUrl = story.url || `${config["sketches-host"]}/${story.slug}`;
1115
920
 
1116
- if (showAmpTag(seoConfig, pageType, story)) {
1117
- return [{
1118
- tag: 'link',
1119
- rel: 'amphtml',
1120
- href: ampUrl
1121
- }];
1122
- } else {
1123
- return [];
921
+ const customSeo = lodash.get(data, ["data", "customSeo"], {});
922
+ const authors = lodash.get(story, ["authors"], []).map(author => author.name);
923
+ const title = customSeo.title || seo["meta-title"] || story.headline;
924
+ const pageTitle = customSeo["page-title"] || seo["meta-title"] || story.headline;
925
+ const description = customSeo.description || seo["meta-description"] || story.summary;
926
+ const keywords = (customSeo.keywords || seo["meta-keywords"] || (story.tags || []).map(tag => tag.name)).join(",");
927
+ const ogUrl = customSeo.ogUrl || lodash.get(seo, ["og", "url"]) || storyUrl;
928
+ const getOgTitle = customSeo.ogTitle || lodash.get(story, ["alternative", "social", "default", "headline"], story.headline) || story.headline;
929
+ const ogDescription = customSeo.ogDescription || story.summary;
930
+ const storyMetaData = {
931
+ title,
932
+ "page-title": pageTitle,
933
+ description,
934
+ keywords,
935
+ canonicalUrl: story["canonical-url"] || storyUrl,
936
+ ogUrl,
937
+ ogTitle: getOgTitle,
938
+ ogDescription,
939
+ storyUrl,
940
+ author: authors
941
+ };
942
+
943
+ if (url.query && url.query.cardId) {
944
+ const storyCardMetadata = getStoryCardMetadata(url.query.cardId);
945
+ return Object.assign({}, storyMetaData, storyCardMetadata); //TODO rewrite in spread syntax, add babel plugin
1124
946
  }
947
+
948
+ return storyMetaData;
1125
949
  }
1126
950
 
1127
- function getTitle(config) {
1128
- return config["publisher-settings"] ? config["publisher-settings"]["title"] : config["publisher-name"];
951
+ function buildTagsFromTopic(config, tag, url = {}, data) {
952
+ if (lodash.isEmpty(tag)) return;
953
+ const customSeo = lodash.get(data, ["data", "customSeo"], {});
954
+ const tagName = customSeo.title || tag["meta-title"] || tag.name;
955
+ const pageTitle = customSeo["page-title"] || tagName;
956
+ const tagDescription = customSeo.description || tag["meta-description"];
957
+ const description = `Read stories listed under on ${tag.name}`;
958
+ const tagUrl = `${config["sketches-host"]}${url.pathname}`;
959
+ const canonicalSlug = tag["canonical-slug"] || url.pathname;
960
+ const canonicalUrl = `${config["sketches-host"]}${canonicalSlug}`;
961
+ const ogTitle = customSeo.ogTitle || tagName;
962
+ const ogDescription = customSeo.ogDescription || description;
963
+ const topicMetaData = {
964
+ title: tagName,
965
+ "page-title": pageTitle,
966
+ description: tagDescription || description,
967
+ keywords: tagName,
968
+ canonicalUrl,
969
+ ogUrl: tagUrl,
970
+ ogTitle,
971
+ ogDescription
972
+ };
973
+
974
+ return topicMetaData;
1129
975
  }
1130
976
 
1131
- function generateStaticData(config) {
1132
- const title = getTitle(config);
1133
- const themeConfig = config["theme-attributes"] || {};
1134
- const publicIntegrations = get__default["default"](config, ['public-integrations'], {});
1135
- const staticData = {
1136
- "twitter:site": title,
1137
- "twitter:domain": config["sketches-host"],
1138
- "twitter:app:name:ipad": themeConfig["twitter_app_name_ipad"],
1139
- "twitter:app:name:googleplay": themeConfig["twitter_app_name_googleplay"],
1140
- "twitter:app:id:googleplay": themeConfig["twitter_app_id_googleplay"],
1141
- "twitter:app:name:iphone": themeConfig["twitter_app_name_iphone"],
1142
- "twitter:app:id:iphone": themeConfig["twitter_app_id_iphone"],
1143
- "apple-itunes-app": themeConfig["apple_itunes_app"],
1144
- "google-play-app": themeConfig["google_play_app"],
1145
- "fb:app_id": get__default["default"](publicIntegrations, ['facebook', 'app-id']) || get__default["default"](themeConfig, ["fb_app_id"]),
1146
- "fb:pages": themeConfig["fb_pages"],
1147
- "og:site_name": title
977
+ function buildTagsFromAuthor(config, author, url = {}, data) {
978
+ if (lodash.isEmpty(author)) return;
979
+
980
+ const customSeo = lodash.get(data, ["data", "customSeo"], {});
981
+ const title = customSeo.title || author.name;
982
+ const pageTitle = customSeo["page-title"] || title;
983
+ const description = customSeo.description || author.bio || `View all articles written by ${title}`;
984
+ const ogTitle = customSeo.ogTitle || author.name;
985
+ const authorUrl = `${config["sketches-host"]}${url.pathname}`;
986
+ const ogDescription = customSeo.ogDescription || description;
987
+
988
+ return {
989
+ title,
990
+ "page-title": pageTitle,
991
+ description,
992
+ keywords: `${title},${config["publisher-name"]}`,
993
+ canonicalUrl: authorUrl,
994
+ ogUrl: authorUrl,
995
+ ogTitle,
996
+ ogDescription
1148
997
  };
998
+ }
1149
999
 
1150
- return omitBy__default["default"](staticData, isUndefined__default["default"]);
1000
+ function buildCustomTags(customTags = {}, pageType = "") {
1001
+ const configObject = customTags[pageType];
1002
+ if (configObject) {
1003
+ return configObject;
1004
+ }
1005
+ return {};
1151
1006
  }
1152
1007
 
1153
- function generateImageObject(config = {}) {
1154
- const { "theme-attributes": themeConfig = {} } = config;
1008
+ function buildTagsFromStaticPage(config, page, url = {}, data) {
1009
+ const seoData = lodash.get(page, ["metadata", "seo"], {});
1010
+ const customSeo = lodash.get(data, ["data", "customSeo"], {});
1011
+ if (lodash.isEmpty(seoData) && lodash.isEmpty(customSeo)) return;
1012
+
1013
+ const { "meta-title": metaTitle, "meta-description": metaDescription, "meta-keywords": keywords } = seoData;
1014
+
1015
+ const title = customSeo.title || metaTitle || page.title;
1016
+ const pageTitle = customSeo["page-title"] || title;
1017
+ const description = customSeo.description || metaDescription;
1018
+ const ogTitle = customSeo.ogTitle || title;
1019
+ const staticPageUrl = `${config["sketches-host"]}${url.pathname}`;
1020
+ const ogDescription = customSeo.ogDescription || description;
1021
+
1155
1022
  return {
1156
- "@context": "http://schema.org",
1157
- "@type": "ImageObject",
1158
- "author": config['publisher-name'],
1159
- "contentUrl": themeConfig.logo,
1160
- "url": themeConfig.logo,
1161
- "name": "logo",
1162
- "width": themeConfig.logo && getQueryParams(themeConfig.logo).width,
1163
- "height": themeConfig.logo && getQueryParams(themeConfig.logo).height
1023
+ title,
1024
+ "page-title": pageTitle,
1025
+ description,
1026
+ keywords: `${title},${config["publisher-name"]}`,
1027
+ canonicalUrl: staticPageUrl,
1028
+ ogUrl: staticPageUrl,
1029
+ ogTitle,
1030
+ ogDescription,
1031
+ keywords: customSeo.keywords || keywords
1164
1032
  };
1165
1033
  }
1166
1034
 
1167
- function generateStructuredData(config = {}) {
1168
- const title = getTitle(config);
1169
- const { "theme-attributes": themeConfig, "social-links": socialLinks, "seo-metadata": seoMetadata = [] } = config;
1170
- const homePageSeo = seoMetadata.find(page => page["owner-type"] === "home") || {};
1171
- const { "page-title": pageTitle = "", description = "", keywords = "" } = get__default["default"](homePageSeo, ["data"], {});
1172
- if (!themeConfig || !themeConfig.logo) {
1173
- return {};
1035
+ // The findRelevantConfig method call has no ownerId for home page.
1036
+ // This causes the seoMetadata to be undefined.
1037
+ // So the default value for the ownerId is set to null.
1038
+
1039
+ function getSeoData(config, pageType, data, url = {}, seoConfig = {}) {
1040
+ function findRelevantConfig(ownerType, ownerId = null) {
1041
+ const seoMetadata = config["seo-metadata"].find(page => page["owner-type"] === ownerType && page["owner-id"] === ownerId) || {};
1042
+ const { sections = [] } = config;
1043
+ const section = sections.find(section => ownerType == "section" && section.id === ownerId) || {};
1044
+ const customSeo = lodash.get(data, ["data", "customSeo"], {});
1045
+ if (seoMetadata.data || section.id || !lodash.isEmpty(customSeo)) {
1046
+ const result = Object.assign({}, {
1047
+ "page-title": customSeo["page-title"] || section.name,
1048
+ title: customSeo.title || section.name,
1049
+ canonicalUrl: customSeo["canonicalUrl"] || section["section-url"] || undefined
1050
+ }, seoMetadata.data);
1051
+
1052
+ if (!result.description) {
1053
+ const homeSeoData = config["seo-metadata"].find(page => page["owner-type"] === "home") || {
1054
+ data: { description: "" }
1055
+ };
1056
+ result.description = customSeo.description || homeSeoData.data.description;
1057
+ }
1058
+
1059
+ result.ogTitle = customSeo.ogTitle || result.title;
1060
+ result.ogDescription = customSeo.ogDescription || result.description;
1061
+ return result;
1062
+ }
1174
1063
  }
1175
- let enableStructuredDataForNewsArticle = themeConfig['structured_data_news_article'] || false;
1176
- if (config.hasOwnProperty('enableStructuredDataForNewsArticle') && typeof config.enableStructuredDataForNewsArticle !== "undefined") {
1177
- enableStructuredDataForNewsArticle = config.enableStructuredDataForNewsArticle;
1064
+
1065
+ if (seoConfig.customTags && seoConfig.customTags[pageType]) {
1066
+ return buildCustomTags(seoConfig.customTags, pageType);
1178
1067
  }
1179
1068
 
1180
- return {
1181
- organization: {
1182
- name: title,
1183
- url: config["sketches-host"],
1184
- logo: generateImageObject(config),
1185
- sameAs: socialLinks ? Object.values(socialLinks) : []
1186
- },
1187
- enableNewsArticle: !!enableStructuredDataForNewsArticle,
1188
- storyUrlAsMainEntityUrl: !!enableStructuredDataForNewsArticle,
1189
- enableVideo: !themeConfig['structured_data_enable_video'],
1190
- enableLiveBlog: !themeConfig['structured_data_enable_live_blog'],
1191
- website: {
1192
- url: config["sketches-host"],
1193
- searchpath: "search?q={query}",
1194
- queryinput: "required name=query",
1195
- name: pageTitle || title,
1196
- headline: description,
1197
- keywords
1069
+ function getShellSeoData(config) {
1070
+ const seoMetadata = config["seo-metadata"].find(meta => meta["owner-type"] === "home") || {};
1071
+ return Object.assign({}, seoMetadata.data, { canonicalUrl: SKIP_CANONICAL });
1072
+ }
1073
+
1074
+ switch (pageType) {
1075
+ case "home-page":
1076
+ return findRelevantConfig("home");
1077
+ case "section-page":
1078
+ return findRelevantConfig("section", lodash.get(data, ["data", "section", "id"])) || getSeoDataFromCollection(config, data) || getSeoData(config, "home-page", data, url);
1079
+ case "collection-page":
1080
+ return getSeoDataFromCollection(config, data) || getSeoData(config, "home-page", data, url);
1081
+ case "tag-page":
1082
+ return buildTagsFromTopic(config, lodash.get(data, ["data", "tag"]), url, data) || getSeoData(config, "home-page", data, url);
1083
+ case "story-page":
1084
+ return buildTagsFromStory(config, lodash.get(data, ["data", "story"]), url, data) || getSeoData(config, "home-page", data, url);
1085
+ case "visual-story":
1086
+ return buildTagsFromStory(config, lodash.get(data, ["story"]), url, data) || getSeoData(config, "home-page", data, url);
1087
+ case "story-page-amp":
1088
+ return buildTagsFromStory(config, lodash.get(data, ["data", "story"]), url, data) || getSeoData(config, "home-page", data, url);
1089
+ case "author-page":
1090
+ return buildTagsFromAuthor(config, lodash.get(data, ["data", "author"], {}), url, data) || getSeoData(config, "home-page", data, url);
1091
+ case "static-page":
1092
+ return buildTagsFromStaticPage(config, lodash.get(data, ["data", "page"], {}), url, data) || getSeoData(config, "home-page", data, url);
1093
+ case "shell":
1094
+ return getShellSeoData(config);
1095
+ default:
1096
+ return getSeoData(config, "home-page", data, url);
1097
+ }
1098
+ }
1099
+
1100
+ function getSeoDataFromCollection(config, data) {
1101
+ if (lodash.get(data, ["data", "collection", "name"])) {
1102
+ let { name, summary } = lodash.get(data, ["data", "collection"]);
1103
+ const customSeo = lodash.get(data, ["data", "customSeo"], {});
1104
+
1105
+ if (!summary) {
1106
+ summary = (getSeoData(config, "home-page", data) || {}).description;
1198
1107
  }
1108
+ const title = customSeo.title || name;
1109
+ const pageTitle = customSeo["page-title"] || name;
1110
+ const ogTitle = customSeo.ogTitle || title;
1111
+ const description = customSeo.description || summary;
1112
+ const ogDescription = customSeo.ogDescription || summary;
1113
+ return {
1114
+ "page-title": pageTitle,
1115
+ title,
1116
+ ogTitle,
1117
+ description,
1118
+ ogDescription,
1119
+ canonicalUrl: SKIP_CANONICAL
1120
+ };
1121
+ }
1122
+ }
1123
+
1124
+ const SKIP_CANONICAL = "__SKIP__CANONICAL__";
1125
+
1126
+ /**
1127
+ * TextTags adds the majority of basic tags, such as
1128
+ * * Canonical URLs
1129
+ * * Title and Description
1130
+ * * Keywords
1131
+ *
1132
+ * If the current URL contains a cardId in the query parameters, then the title and description will come from *card["social-share"]*
1133
+ *
1134
+ * @extends Generator
1135
+ * @param {*} seoConfig
1136
+ * @param {boolean} seoConfig.enableOgTags Add og tags for Facebook
1137
+ * @param {boolean} seoConfig.enableTwitterCards Add twitter tags
1138
+ * @param {boolean} seoConfig.enableNews Add tags for Google News, like news_keywords
1139
+ * @param {Object} seoConfig.customTags Add tags for a custom page type. Usually looks like `{"custom-page": {"title": "value", "canonicalUrl": "value"}}`
1140
+ * @param {...*} params See {@link Generator} for other Parameters
1141
+ */
1142
+ function TextTags(seoConfig, config, pageType, data, { url }) {
1143
+ const seoData = getSeoData(config, pageType, data, url, seoConfig);
1144
+ const customSeo = lodash.get(data, ["data", "customSeo"], {});
1145
+
1146
+ if (!seoData) return [];
1147
+
1148
+ const currentUrl = `${config["sketches-host"]}${url.pathname}`;
1149
+
1150
+ const basicTags = {
1151
+ description: customSeo.description || seoData.description,
1152
+ title: customSeo.title || seoData.title,
1153
+ keywords: customSeo.keywords || seoData.keywords
1199
1154
  };
1155
+
1156
+ const ogUrl = customSeo.ogUrl || seoData.ogUrl || seoData.canonicalUrl || currentUrl;
1157
+ const ogTags = seoConfig.enableOgTags ? {
1158
+ "og:type": pageType === "story-page" || pageType === "story-page-amp" ? "article" : "website",
1159
+ "og:url": ogUrl === SKIP_CANONICAL ? undefined : ogUrl,
1160
+ "og:title": customSeo.ogTitle || seoData.ogTitle,
1161
+ "og:description": customSeo.ogDescription || seoData.ogDescription
1162
+ } : undefined;
1163
+
1164
+ const twitterTags = seoConfig.enableTwitterCards ? {
1165
+ "twitter:card": "summary_large_image",
1166
+ "twitter:title": customSeo.twitterTitle || seoData.ogTitle,
1167
+ "twitter:description": customSeo.twitterDescription || seoData.ogDescription
1168
+ } : undefined;
1169
+
1170
+ const allTags = Object.assign(basicTags, ogTags, twitterTags);
1171
+
1172
+ const commonTags = [{ tag: "title", children: customSeo.title || data.title || seoData["page-title"] }];
1173
+
1174
+ const canonical = seoData.canonicalUrl || currentUrl;
1175
+ if (canonical != SKIP_CANONICAL) {
1176
+ commonTags.push({ tag: "link", rel: "canonical", href: canonical });
1177
+ }
1178
+
1179
+ if (pageType === "story-page" || pageType === "story-page-amp") {
1180
+ commonTags.push({ name: "author", content: seoData.author });
1181
+ }
1182
+
1183
+ if ((pageType === "story-page" || pageType === "story-page-amp") && seoConfig.enableNews) {
1184
+ commonTags.push({ name: "news_keywords", content: seoData.keywords });
1185
+ if (lodash.get(data, ["data", "story", "seo", "meta-google-news-standout"])) commonTags.push({ tag: "link", rel: "standout", href: seoData.storyUrl || currentUrl });
1186
+ }
1187
+
1188
+ return commonTags.concat(objectToTags(allTags));
1189
+ }
1190
+
1191
+ function getTitle(seoConfig, config, pageType, data, params) {
1192
+ const customSeo = lodash.get(data, ["data", "customSeo"], {});
1193
+
1194
+ if (lodash.get(data, ["title"])) return customSeo.title || lodash.get(data, ["title"]);
1195
+
1196
+ if (lodash.get(data, ["data", "title"])) return customSeo.title || lodash.get(data, ["data", "title"]);
1197
+
1198
+ const seoData = getSeoData(config, pageType, data, undefined, seoConfig) || {};
1199
+ return customSeo.title || seoData["page-title"];
1200
1200
  }
1201
1201
 
1202
1202
  function tagToKey(tag) {
1203
1203
  switch (tag.tag || "meta") {
1204
1204
  case "meta":
1205
- return `meta-${tag.name || "name"}-${tag.property || "property"}`;
1205
+ return `meta-${tag.name || tag.itemprop || "name"}-${tag.property || "property"}`;
1206
1206
  case "link":
1207
1207
  return `link-${tag.rel}`;
1208
1208
  case "title":
@@ -1293,7 +1293,7 @@ class SEO {
1293
1293
  }
1294
1294
 
1295
1295
  getTitle(config, pageType, data, params = {}) {
1296
- return getTitle$1(this.seoConfig, config, pageType, data);
1296
+ return getTitle(this.seoConfig, config, pageType, data);
1297
1297
  }
1298
1298
  }
1299
1299
 
@@ -0,0 +1 @@
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:!0});var lodash=require("lodash"),React=require("react"),ReactDomServer=require("react-dom/server"),get=require("lodash/get"),url=require("url"),dateFnsTz=require("date-fns-tz"),isUndefined=require("lodash/isUndefined"),omitBy=require("lodash/omitBy"),quintypeJs=require("quintype-js");function _interopDefaultLegacy(e){return e&&"object"==typeof e&&"default"in e?e:{default:e}}var React__default=_interopDefaultLegacy(React),ReactDomServer__default=_interopDefaultLegacy(ReactDomServer),get__default=_interopDefaultLegacy(get),isUndefined__default=_interopDefaultLegacy(isUndefined),omitBy__default=_interopDefaultLegacy(omitBy);function objectToTags(object){return lodash.entries(object).filter(([key,value])=>value).map(([key,value])=>({[getPropertyName(key)]:key,content:value}))}function getPropertyName(key){return key.startsWith("fb:")||key.startsWith("og:")?"property":"name"}function stripMillisecondsFromTime(date,timezone){const toReturn=date.toJSON();if(!toReturn)return toReturn;const zonedTime=timezone&&dateFnsTz.utcToZonedTime(date,timezone),formatZonedTime=zonedTime&&dateFnsTz.format(zonedTime,"yyyy-MM-dd'T'HH:mm:ssXXX",{timeZone:timezone}),formatToReturn=toReturn.split(".")[0]+"Z";return timezone?formatZonedTime:formatToReturn}function getQueryParams(url$1){const urlObj=new url.URL(url$1),search_params=new url.URLSearchParams(urlObj.search),getWidth=search_params.get("w")||"",getHeight=search_params.get("h")||"";return{width:getWidth,height:getHeight}}function isStoryPublic(story){return void 0===story.access||null===story.access||"public"===story.access}function showAmpTag({ampStoryPages:ampStoryPages=!0},pageType,story){return!(!ampStoryPages||"story-page"!==pageType)&&!("public"===ampStoryPages&&!isStoryPublic(story))}const getDomain=(url,domainSlug)=>{const domain=domainSlug?new URL(url).origin:"";try{return domain}catch(err){return""}};function StoryAmpTags(seoConfig,config,pageType,data={},opts){const story=get__default.default(data,["data","story"],{}),{currentHostUrl:currentHostUrl="",domainSlug:domainSlug}=data,ampUrlAppend=seoConfig.appendHostToAmpUrl?getDomain(currentHostUrl,domainSlug)||config["sketches-host"]:"",storySlug=seoConfig.decodeAmpUrl?decodeURIComponent(story.slug):encodeURIComponent(story.slug),ampUrl="visual-story"===story["story-template"]?`${ampUrlAppend}/${storySlug}`:`${ampUrlAppend}/amp/story/${storySlug}`;return showAmpTag(seoConfig,pageType,story)?[{tag:"link",rel:"amphtml",href:ampUrl}]:[]}function AuthorTags(seoConfig,config,pageType,data,{url:url}){return"story-page"!=pageType||"story-page-amp"!=pageType?[]:[{name:"twitter:creator",content:lodash.get(data,["data","story","author-name"])}]}function getTitle$1(config){return config["publisher-settings"]?config["publisher-settings"].title:config["publisher-name"]}function generateStaticData(config){const title=getTitle$1(config),themeConfig=config["theme-attributes"]||{},publicIntegrations=get__default.default(config,["public-integrations"],{}),staticData={"twitter:site":title,"twitter:domain":config["sketches-host"],"twitter:app:name:ipad":themeConfig.twitter_app_name_ipad,"twitter:app:name:googleplay":themeConfig.twitter_app_name_googleplay,"twitter:app:id:googleplay":themeConfig.twitter_app_id_googleplay,"twitter:app:name:iphone":themeConfig.twitter_app_name_iphone,"twitter:app:id:iphone":themeConfig.twitter_app_id_iphone,"apple-itunes-app":themeConfig.apple_itunes_app,"google-play-app":themeConfig.google_play_app,"fb:app_id":get__default.default(publicIntegrations,["facebook","app-id"])||get__default.default(themeConfig,["fb_app_id"]),"fb:pages":themeConfig.fb_pages,"og:site_name":title};return omitBy__default.default(staticData,isUndefined__default.default)}function generateImageObject(config={}){const{"theme-attributes":themeConfig={}}=config;return{"@context":"http://schema.org","@type":"ImageObject",author:config["publisher-name"],contentUrl:themeConfig.logo,url:themeConfig.logo,name:"logo",width:themeConfig.logo&&getQueryParams(themeConfig.logo).width,height:themeConfig.logo&&getQueryParams(themeConfig.logo).height}}function generateStructuredData(config={}){const title=getTitle$1(config),{"theme-attributes":themeConfig,"social-links":socialLinks,"seo-metadata":seoMetadata=[]}=config,homePageSeo=seoMetadata.find(page=>"home"===page["owner-type"])||{},{"page-title":pageTitle="",description:description="",keywords:keywords=""}=get__default.default(homePageSeo,["data"],{});if(!themeConfig||!themeConfig.logo)return{};let enableStructuredDataForNewsArticle=themeConfig.structured_data_news_article||!1;return config.hasOwnProperty("enableStructuredDataForNewsArticle")&&void 0!==config.enableStructuredDataForNewsArticle&&(enableStructuredDataForNewsArticle=config.enableStructuredDataForNewsArticle),{organization:{name:title,url:config["sketches-host"],logo:generateImageObject(config),sameAs:socialLinks?Object.values(socialLinks):[]},enableNewsArticle:!!enableStructuredDataForNewsArticle,storyUrlAsMainEntityUrl:!!enableStructuredDataForNewsArticle,enableVideo:!themeConfig.structured_data_enable_video,enableLiveBlog:!themeConfig.structured_data_enable_live_blog,website:{url:config["sketches-host"],searchpath:"search?q={query}",queryinput:"required name=query",name:pageTitle||title,headline:description,keywords:keywords}}}function pickImageFromCard(story,cardId){const{metadata:metadata={}}=story.cards.find(card=>card.id===cardId)||{};if(metadata&&!lodash.isEmpty(metadata)&&lodash.get(metadata,["social-share","image","key"],!1)){const alt=metadata["social-share"].image.attribution||metadata["social-share"].title||metadata["social-share"].message||getAttribution(story);return{image:new quintypeJs.FocusedImage(metadata["social-share"].image.key,metadata["social-share"].image.metadata||{}),alt:alt}}}function getAttribution(story){return story["hero-image-attribution"]||story.summary||lodash.get(story,["alternative","home","default","headline"])||story.headline}function pickImageFromStory({story:story,config:config,seoConfig:seoConfig}){function getAlt(type,key,fallback){return lodash.get(story,["alternative",`${type}`,"default","hero-image",`${key}`],fallback)}const alt=getAttribution(story),fallbackSocialImage=lodash.get(seoConfig,["fallbackSocialImage"]),altHeroImg=getAlt("home","hero-image-s3-key",null),altSocialHeroImg=getAlt("social","hero-image-s3-key",null),storyHeroImage=lodash.get(story,["hero-image-s3-key"]),logo_url=lodash.get(config,["theme-attributes","logo_url"]),logo=lodash.get(config,["theme-attributes","logo"]);if(altSocialHeroImg){const metadata=getAlt("social","hero-image-metadata",{});return{image:new quintypeJs.FocusedImage(altSocialHeroImg,metadata),alt:alt}}if(altHeroImg){const metadata=getAlt("home","hero-image-metadata",{});return{image:new quintypeJs.FocusedImage(altHeroImg,metadata),alt:alt}}if(storyHeroImage){const metadata=lodash.get(story,["hero-image-metadata"],{});return{image:new quintypeJs.FocusedImage(storyHeroImage,metadata),alt:alt}}return fallbackSocialImage?{image:fallbackSocialImage,alt:alt,includesHost:!0}:logo_url?{image:logo_url,alt:alt,includesHost:!0}:logo?{image:logo,alt:alt,includesHost:!0}:{image:void 0,alt:void 0}}function pickImageFromCollection(collection){const coverImage=lodash.get(collection,["metadata","cover-image"])||{};if(!coverImage["cover-image-s3-key"])return{};const alt=collection.summary||collection.name||null;return{image:new quintypeJs.FocusedImage(coverImage["cover-image-s3-key"],coverImage["cover-image-metadata"]||{}),alt:alt}}function pickImage({pageType:pageType,config:config,seoConfig:seoConfig,data:data,url:url}){if("story-page"===pageType&&url.query&&url.query.cardId){const story=lodash.get(data,["data","story"])||{};return pickImageFromCard(story,url.query.cardId)||pickImageFromStory({story:story,seoConfig:seoConfig,config:config})}if("visual-story"===pageType&&url.query&&url.query.cardId){const story=lodash.get(data,["story"])||{};return pickImageFromCard(story,url.query.cardId)||pickImageFromStory({story:story,seoConfig:seoConfig,config:config})}if("story-page"===pageType||"story-page-amp"===pageType){const story=lodash.get(data,["data","story"])||{};return pickImageFromStory({story:story,seoConfig:seoConfig,config:config})}if("visual-story"===pageType){const story=lodash.get(data,["story"])||{};return pickImageFromStory({story:story,seoConfig:seoConfig,config:config})}return lodash.get(data,["data","collection"])?pickImageFromCollection(lodash.get(data,["data","collection"])):{image:void 0,alt:void 0}}function ImageTags(seoConfig,config,pageType,data,{url:url={}}){const{image:image,alt:alt,includesHost:includesHost=!1}=pickImage({pageType:pageType,data:data,url:url,seoConfig:seoConfig,config:config});if(!image)return[];const tags=[];return seoConfig.enableTwitterCards&&(tags.push({name:"twitter:image",content:includesHost?image:`https://${config["cdn-image"]}/${image.path([16,9],{w:1200,auto:"format,compress",ogImage:!0,enlarge:!0})}`}),alt&&tags.push({property:"twitter:image:alt",content:alt})),seoConfig.enableOgTags&&(tags.push({property:"og:image",content:includesHost?image:`https://${config["cdn-image"]}/${image.path([40,21],{w:1200,auto:"format,compress",ogImage:!0,enlarge:!0})}`}),tags.push({property:"og:image:width",content:1200}),lodash.get(image,["metadata","focus-point"])&&tags.push({property:"og:image:height",content:630}),alt&&tags.push({property:"og:image:alt",content:alt})),tags}function StaticTags(seoConfig,config,pageType,data,{url:url}){return objectToTags(seoConfig.staticTags||{})}const getSchemaContext={"@context":"http://schema.org"};function getSchemaType(type){return{"@type":type}}function getSchemaPerson(name,url=""){return Object.assign({},getSchemaType("Person"),{givenName:name,name:name},url&&{url:url})}function getSchemaFooter({cssSelector:cssSelector}){return cssSelector?Object.assign({},getSchemaContext,getSchemaType("WPFooter"),{cssSelector:cssSelector}):{}}function getSchemaHeader({cssSelector:cssSelector}){return cssSelector?Object.assign({},getSchemaContext,getSchemaType("WPHeader"),{cssSelector:cssSelector}):{}}function getSchemaBlogPosting(card={},author={},headline="",image="",structuredData={},story={},timezone){const{website:{url:url=""}={}}=structuredData,orgUrl=lodash.get(structuredData,["organization","url"],"");return Object.assign({},getSchemaType("BlogPosting"),getSchemaMainEntityOfPage(`${url}/${story.slug}`),getSchemaPublisher(structuredData.organization,orgUrl),{dateModified:stripMillisecondsFromTime(new Date(card["card-updated-at"]),timezone),dateCreated:stripMillisecondsFromTime(new Date(card["card-added-at"]),timezone),datePublished:stripMillisecondsFromTime(new Date(card["card-updated-at"]),timezone),author:author,headline:headline,image:image})}function getSchemaPublisher(organization,orgUrl){const id={id:orgUrl};return{publisher:Object.assign({},getSchemaType("Organization"),getSchemaContext,organization,id)}}function getSchemaMainEntityOfPage(id){return{mainEntityOfPage:Object.assign({},getSchemaType("WebPage"),{"@id":id})}}function getSchemaMovieReview(movieObject={}){const movieCreatedAt=lodash.get(movieObject,["created-at"],null),actors=lodash.get(movieObject,["actors"],[]).map(actor=>getSchemaPerson(actor.name)),directors=lodash.get(movieObject,["directors"],[]).map(director=>getSchemaPerson(director.name)),producers=lodash.get(movieObject,["producers"],[]).map(producer=>getSchemaPerson(producer.name));return{actors:actors,directors:directors,name:lodash.get(movieObject,["name"],""),sameAs:lodash.get(movieObject,["sameAs"],""),description:lodash.get(movieObject,["meta-description"],""),producer:producers,image:lodash.get(movieObject,["photo","0","url"],""),dateCreated:movieCreatedAt?new Date(movieCreatedAt).toISOString():""}}function getSchemaWebsite(website={}){const{url:url,searchpath:searchpath,name:name,headline:headline,keywords:keywords,queryinput:queryinput}=website;return Object.assign({},getSchemaContext,getSchemaType("WebSite"),{url:url,interactivityType:"mixed",name:name,headline:headline,keywords:keywords,copyrightHolder:Object.assign({},getSchemaType("Organization"),{name:name}),potentialAction:{"@type":"SearchAction",target:`${url}/${searchpath}`,"query-input":queryinput}},getSchemaMainEntityOfPage(url))}function getSchemaListItem(position=0,name="",url=""){return Object.assign({},getSchemaType("ListItem"),{position:position,name:name,item:url})}function getSchemaBreadcrumbList(breadcrumbsDataList){const itemListElement=breadcrumbsDataList.map(({name:name="",url:url=""},index)=>getSchemaListItem(index+1,name,url));return Object.assign({},getSchemaContext,getSchemaType("BreadcrumbList"),{itemListElement:itemListElement})}function getMovieEntityTags(movieJson){return getSchemaMovieReview(movieJson)}function generateTagsForEntity(entity,ldJson){if(entity.type&&"movie"===entity.type.toLowerCase())return ldJson("Movie",getMovieEntityTags(entity))}function getLdJsonFields(type,fields){return Object.assign({},fields,getSchemaType(type),getSchemaContext)}function ldJson(type,fields){const json=JSON.stringify(getLdJsonFields(type,fields)).replace(/</g,"&lt;").replace(/>/g,"&gt;");return{tag:"script",type:"application/ld+json",dangerouslySetInnerHTML:{__html:json}}}function imageUrl(publisherConfig,s3Key,width,height){const imageSrc=/^https?.*/.test(publisherConfig["cdn-image"])?publisherConfig["cdn-image"]:`https://${publisherConfig["cdn-image"]}`;return`${imageSrc}/${s3Key}?w=${width=width||""}&h=${height=height||""}&auto=format%2Ccompress&fit=max`}function generateCommonData(structuredData={},story={},publisherConfig={},timezone){const storyUrl=story.url||`${publisherConfig["sketches-host"]}/${story.slug}`,orgUrl=get__default.default(structuredData,["organization","url"],""),mainEntityUrl=Object.keys(story).length>0&&structuredData.storyUrlAsMainEntityUrl?storyUrl:get__default.default(structuredData,["organization","url"],""),imageWidth=1200,imageHeight=675;return Object.assign({},{headline:story.headline,image:[imageUrl(publisherConfig,story["hero-image-s3-key"],1200,675)],url:`${publisherConfig["sketches-host"]}/${story.slug}`,datePublished:stripMillisecondsFromTime(new Date(story["first-published-at"]),timezone)},getSchemaMainEntityOfPage(mainEntityUrl),getSchemaPublisher(structuredData.organization,orgUrl))}function authorData(authors=[],authorSchema=[],publisherConfig={}){return authorSchema.length>0?authorSchema.map(author=>getSchemaPerson(author.name,author.url)):authors.map(author=>{const authorUrl=author.slug?`${publisherConfig["sketches-host"]}/author/${author.slug}`:null;return getSchemaPerson(author.name,authorUrl)})}function getTextElementsOfCards(story){if(story&&story.cards)return story.cards.reduce((acc,currentCard)=>acc.concat(currentCard["story-elements"].filter(element=>"text"===element.type)),[])}function getPlainText(text=""){return text.replace(/<[^>]+>/g,"")}function getCompleteText(story,stripHtmlFromArticleBody){const textArray=[];getTextElementsOfCards(story).forEach(item=>{const textContent=stripHtmlFromArticleBody?getPlainText(item.text):item.text;textArray.push(textContent)});const completeCardText=textArray.join(".");return completeCardText}function articleSectionObj(story){if("video"!==story["story-template"])return{articleSection:get__default.default(story,["sections","0","display-name"],"")}}function generateArticleData(structuredData={},story={},publisherConfig={},timezone){const metaKeywords=story.seo&&story.seo["meta-keywords"]||[],authors=story.authors&&0!==story.authors.length?story.authors:[{name:story["author-name"]||""}],storyKeysPresence=Object.keys(story).length>0,imageWidth=1200,imageHeight=675,storyAccessType=storyAccess(story.access),authorSchema=structuredData.authorSchema&&structuredData.authorSchema(story)||[];return Object.assign({},generateCommonData(structuredData,story,publisherConfig,timezone),{author:authorData(authors,authorSchema,publisherConfig),keywords:metaKeywords.join(","),thumbnailUrl:imageUrl(publisherConfig,story["hero-image-s3-key"],1200,675),articleBody:storyKeysPresence&&getCompleteText(story,structuredData.stripHtmlFromArticleBody)||"",dateCreated:stripMillisecondsFromTime(new Date(story["first-published-at"]),timezone),dateModified:stripMillisecondsFromTime(new Date(story["last-published-at"]),timezone),name:storyKeysPresence&&story.headline||"",image:generateArticleImageData(story["hero-image-s3-key"],publisherConfig),isAccessibleForFree:storyAccessType,isPartOf:generateIsPartOfDataForArticle(story,publisherConfig)},articleSectionObj(story))}function generateArticleImageData(image,publisherConfig={}){const imageWidth=1200,imageHeight=675,articleImage=imageUrl(publisherConfig,image,1200,675);return Object.assign({},{"@type":"ImageObject",url:articleImage},getQueryParams(articleImage))}function storyAccess(access){return null===access||"public"===access||"login"===access||"subscription"!==access&&void 0}function generateIsPartOfDataForArticle(story={},publisherConfig={}){return Object.assign({},{"@type":"WebPage",url:`${publisherConfig["sketches-host"]}/${story.slug}`,primaryImageOfPage:generateArticleImageData(story["hero-image-s3-key"],publisherConfig)})}function generateIsPartOfDataForNewsArticle(story={},publisherConfig={},pageType="",structuredData={}){const publisherName=publisherConfig["publisher-name"],productId=`${publisherConfig["publisher-name"]}${structuredData.isShowcaseProduct?".com:showcase":".com:basic"}`,isPartOfData=generateIsPartOfDataForArticle(story,publisherConfig);return structuredData.isSubscriptionsEnabled?Object.assign(isPartOfData,{"@type":["WebPage","CreativeWork","Product"],name:publisherName,productID:productId}):"story-page-amp"===pageType&&structuredData.isAmpSubscriptionsEnabled?Object.assign({},{"@type":["WebPage","CreativeWork","Product"],name:publisherName,productID:productId,url:`${publisherConfig["sketches-host"]}/${story.slug}`,primaryImageOfPage:generateArticleImageData(story["hero-image-s3-key"],publisherConfig)}):isPartOfData}function generateHasPartData(storyAccess){return storyAccess?{}:{hasPart:[{"@type":"WebPageElement",isAccessibleForFree:storyAccess,cssSelector:".paywall"}]}}function generateNewsArticleData(structuredData={},story={},publisherConfig={},pageType=""){const{alternative:alternative={}}=story.alternative||{},storyAccessType=storyAccess(story.access);return Object.assign({},{alternativeHeadline:alternative.home&&alternative.home.default?alternative.home.default.headline:"",description:story.summary,isAccessibleForFree:storyAccessType,isPartOf:generateIsPartOfDataForNewsArticle(story,publisherConfig,pageType,structuredData)},generateHasPartData(storyAccessType))}function findStoryElementField(card,type,field,defaultValue){const elements=card["story-elements"].filter(e=>e.type==type||e.subtype==type);return elements.length>0?elements[0][field]:defaultValue}function generateLiveBlogPostingData(structuredData={},story={},publisherConfig={},timezone){const imageWidth=1200,imageHeight=675,authorSchema=structuredData.authorSchema&&structuredData.authorSchema(story)||[];return{headline:story.headline,description:story.summary||story.headline,author:story["author-name"],coverageEndTime:stripMillisecondsFromTime(new Date(story["last-published-at"]),timezone),coverageStartTime:stripMillisecondsFromTime(new Date(story["first-published-at"]),timezone),dateModified:stripMillisecondsFromTime(new Date(story["last-published-at"]),timezone),liveBlogUpdate:story.cards.map(card=>getSchemaBlogPosting(card,authorData(story.authors,authorSchema,publisherConfig),findStoryElementField(card,"title","text",story.headline),imageUrl(publisherConfig,findStoryElementField(card,"image","image-s3-key",story["hero-image-s3-key"]),1200,675),structuredData,story,timezone))}}function getEmbedUrl(cards){let embedUrl="";return cards.find(card=>{const storyElements=get__default.default(card,["story-elements"],[]);return storyElements.find(elem=>!!elem["embed-url"]&&(embedUrl=elem["embed-url"],!0))}),embedUrl}function generateVideoArticleData(structuredData={},story={},publisherConfig={},timezone){const metaKeywords=story.seo&&story.seo["meta-keywords"]||[],storyCards=get__default.default(story,["cards"],[]),embedUrl=getEmbedUrl(storyCards),socialShareMsg=get__default.default(story,["summary"],""),metaDescription=get__default.default(story,["seo","meta-description"],""),subHeadline=get__default.default(story,["subheadline"],""),headline=get__default.default(story,["headline"],""),imageWidth=1200,imageHeight=675,authorSchema=structuredData.authorSchema&&structuredData.authorSchema(story)||[];return Object.assign({},generateCommonData(structuredData,story,publisherConfig,timezone),{author:authorData(story.authors,authorSchema,publisherConfig),keywords:metaKeywords.join(","),dateCreated:stripMillisecondsFromTime(new Date(story["first-published-at"]),timezone),dateModified:stripMillisecondsFromTime(new Date(story["last-published-at"]),timezone),description:socialShareMsg||metaDescription||subHeadline||headline,name:story.headline,thumbnailUrl:[imageUrl(publisherConfig,story["hero-image-s3-key"],1200,675)],uploadDate:stripMillisecondsFromTime(new Date(story["last-published-at"]),timezone),embedUrl:embedUrl})}function generateWebSiteData(structuredData={},story={},publisherConfig={}){return getSchemaWebsite(structuredData.website)}function generateBreadcrumbListData(pageType="",publisherConfig={},data={}){const{"sketches-host":domain="",sections:sections=[]}=publisherConfig;let breadcrumbsDataList=[{name:"Home",url:domain}];function addCrumb(crumbsDataList=[],currentSection={}){if(!currentSection["parent-id"])return crumbsDataList;const parentSection=sections.find(section=>section.id===currentSection["parent-id"]);if(!parentSection)return crumbsDataList;const{"section-url":url="",name:name=""}=parentSection;return crumbsDataList.unshift({url:url,name:name}),addCrumb(crumbsDataList,parentSection)}function getSectionPageCrumbs(section={}){const{"section-url":url="",name:name=""}=section,crumbsDataList=[{url:url,name:name}];return addCrumb(crumbsDataList,section)}function getStoryPageCrumbs({headline:headline="",url:url="",sections:[storySection]}={}){let sectionCrumbsDataList=[];return storySection&&(sectionCrumbsDataList=getSectionPageCrumbs(storySection)),sectionCrumbsDataList.push({name:headline,url:url}),sectionCrumbsDataList}function getBreadCrumbs(breadcrumb={}){const crumbsDataList=[{url:breadcrumb.url,name:breadcrumb.name}];return addCrumb(crumbsDataList)}if(data.breadcrumbs&&"name"in data.breadcrumbs&&"url"in data.breadcrumbs)return breadcrumbsDataList=breadcrumbsDataList.concat(getBreadCrumbs(data.breadcrumbs)),getSchemaBreadcrumbList(breadcrumbsDataList);switch(pageType){case"section-page":breadcrumbsDataList=breadcrumbsDataList.concat(getSectionPageCrumbs(data.section));break;case"story-page":case"story-page-amp":breadcrumbsDataList=breadcrumbsDataList.concat(getStoryPageCrumbs(data.story))}return getSchemaBreadcrumbList(breadcrumbsDataList)}function StructuredDataTags({structuredData:structuredData={}},config,pageType,response={},{url:url}){const tags=[],{story:story={},timezone:timezone=null}=response.data||{},entities=get__default.default(response,["data","linkedEntities"],null)||[],{config:publisherConfig={}}=response;publisherConfig["publisher-settings"];const isStructuredDataEmpty=0===Object.keys(structuredData).length,enableBreadcrumbList=get__default.default(structuredData,["enableBreadcrumbList"],!0),structuredDataTags=get__default.default(structuredData,["structuredDataTags"],[]);let articleData={};if(isStructuredDataEmpty||(articleData=generateArticleData(structuredData,story,publisherConfig,timezone),structuredDataTags.map(type=>{pageType===type&&(tags.push(ldJson("Organization",structuredData.organization)),structuredData.website&&tags.push(ldJson("Website",Object.assign({},generateWebSiteData(structuredData,story,publisherConfig)))))})),isStructuredDataEmpty||"home-page"!==pageType||(tags.push(ldJson("Organization",structuredData.organization)),structuredData.website&&tags.push(ldJson("Website",Object.assign({},generateWebSiteData(structuredData,story,publisherConfig))))),!isStructuredDataEmpty&&enableBreadcrumbList&&tags.push(ldJson("BreadcrumbList",generateBreadcrumbListData(pageType,publisherConfig,response.data))),!isStructuredDataEmpty&&"story-page"===pageType){const newsArticleTags=generateNewsArticleTags();newsArticleTags?tags.push(storyTags(),newsArticleTags):tags.push(storyTags())}if(!isStructuredDataEmpty&&"story-page-amp"===pageType){const newsArticleTags=generateNewsArticleTags();newsArticleTags?tags.push(storyTags(),newsArticleTags):tags.push(storyTags())}if(!isStructuredDataEmpty&&structuredData.header&&tags.push(ldJson("WPHeader",getSchemaHeader(structuredData.header))),!isStructuredDataEmpty&&structuredData.footer&&tags.push(ldJson("WPFooter",getSchemaFooter(structuredData.footer))),entities.length>0)for(let entity of entities){const entityTags=generateTagsForEntity(entity,ldJson);entityTags&&tags.push(entityTags)}function generateNewsArticleTags(){if(structuredData.enableNewsArticle)return ldJson("NewsArticle",Object.assign({},articleData,generateNewsArticleData(structuredData,story,publisherConfig,pageType)))}function storyTags(){return structuredData.enableLiveBlog&&"live-blog"===story["story-template"]?ldJson("LiveBlogPosting",Object.assign({},generateLiveBlogPostingData(structuredData,story,publisherConfig,timezone))):structuredData.enableVideo&&"video"===story["story-template"]?ldJson("VideoObject",generateVideoArticleData(structuredData,story,publisherConfig,timezone)):"withoutArticleSchema"!==structuredData.enableNewsArticle?ldJson("Article",articleData):{}}return tags}function buildTagsFromStory(config,story,url={},data={}){if(!story)return;function getStoryCardMetadata(cardId){const{metadata:metadata={}}=story.cards.find(card=>card.id===cardId)||{},urlWithCardId=`${config["sketches-host"]}/${story.slug}?cardId=${cardId}`;return metadata&&!lodash.isEmpty(metadata)&&metadata["social-share"]?{title:metadata["social-share"].title||story.headline,description:metadata["social-share"].message||story.summary,ogUrl:urlWithCardId,ogTitle:metadata["social-share"].title||story.headline,ogDescription:metadata["social-share"].message||story.summary}:metadata}const seo=story.seo||{},storyUrl=story.url||`${config["sketches-host"]}/${story.slug}`,customSeo=lodash.get(data,["data","customSeo"],{}),authors=lodash.get(story,["authors"],[]).map(author=>author.name),title=customSeo.title||seo["meta-title"]||story.headline,pageTitle=customSeo["page-title"]||seo["meta-title"]||story.headline,description=customSeo.description||seo["meta-description"]||story.summary,keywords=(customSeo.keywords||seo["meta-keywords"]||(story.tags||[]).map(tag=>tag.name)).join(","),ogUrl=customSeo.ogUrl||lodash.get(seo,["og","url"])||storyUrl,getOgTitle=customSeo.ogTitle||lodash.get(story,["alternative","social","default","headline"],story.headline)||story.headline,ogDescription=customSeo.ogDescription||story.summary,storyMetaData={title:title,"page-title":pageTitle,description:description,keywords:keywords,canonicalUrl:story["canonical-url"]||storyUrl,ogUrl:ogUrl,ogTitle:getOgTitle,ogDescription:ogDescription,storyUrl:storyUrl,author:authors};if(url.query&&url.query.cardId){const storyCardMetadata=getStoryCardMetadata(url.query.cardId);return Object.assign({},storyMetaData,storyCardMetadata)}return storyMetaData}function buildTagsFromTopic(config,tag,url={},data){if(lodash.isEmpty(tag))return;const customSeo=lodash.get(data,["data","customSeo"],{}),tagName=customSeo.title||tag["meta-title"]||tag.name,pageTitle=customSeo["page-title"]||tagName,tagDescription=customSeo.description||tag["meta-description"],description=`Read stories listed under on ${tag.name}`,tagUrl=`${config["sketches-host"]}${url.pathname}`,canonicalSlug=tag["canonical-slug"]||url.pathname,canonicalUrl=`${config["sketches-host"]}${canonicalSlug}`,ogTitle=customSeo.ogTitle||tagName,ogDescription=customSeo.ogDescription||description,topicMetaData={title:tagName,"page-title":pageTitle,description:tagDescription||description,keywords:tagName,canonicalUrl:canonicalUrl,ogUrl:tagUrl,ogTitle:ogTitle,ogDescription:ogDescription};return topicMetaData}function buildTagsFromAuthor(config,author,url={},data){if(lodash.isEmpty(author))return;const customSeo=lodash.get(data,["data","customSeo"],{}),title=customSeo.title||author.name,pageTitle=customSeo["page-title"]||title,description=customSeo.description||author.bio||`View all articles written by ${title}`,ogTitle=customSeo.ogTitle||author.name,authorUrl=`${config["sketches-host"]}${url.pathname}`,ogDescription=customSeo.ogDescription||description;return{title:title,"page-title":pageTitle,description:description,keywords:`${title},${config["publisher-name"]}`,canonicalUrl:authorUrl,ogUrl:authorUrl,ogTitle:ogTitle,ogDescription:ogDescription}}function buildCustomTags(customTags={},pageType=""){const configObject=customTags[pageType];return configObject||{}}function buildTagsFromStaticPage(config,page,url={},data){const seoData=lodash.get(page,["metadata","seo"],{}),customSeo=lodash.get(data,["data","customSeo"],{});if(lodash.isEmpty(seoData)&&lodash.isEmpty(customSeo))return;const{"meta-title":metaTitle,"meta-description":metaDescription,"meta-keywords":keywords}=seoData,title=customSeo.title||metaTitle||page.title,pageTitle=customSeo["page-title"]||title,description=customSeo.description||metaDescription,ogTitle=customSeo.ogTitle||title,staticPageUrl=`${config["sketches-host"]}${url.pathname}`,ogDescription=customSeo.ogDescription||description;return{title:title,"page-title":pageTitle,description:description,keywords:`${title},${config["publisher-name"]}`,canonicalUrl:staticPageUrl,ogUrl:staticPageUrl,ogTitle:ogTitle,ogDescription:ogDescription,keywords:customSeo.keywords||keywords}}function getSeoData(config,pageType,data,url={},seoConfig={}){function findRelevantConfig(ownerType,ownerId=null){const seoMetadata=config["seo-metadata"].find(page=>page["owner-type"]===ownerType&&page["owner-id"]===ownerId)||{},{sections:sections=[]}=config,section=sections.find(section=>"section"==ownerType&&section.id===ownerId)||{},customSeo=lodash.get(data,["data","customSeo"],{});if(seoMetadata.data||section.id||!lodash.isEmpty(customSeo)){const result=Object.assign({},{"page-title":customSeo["page-title"]||section.name,title:customSeo.title||section.name,canonicalUrl:customSeo.canonicalUrl||section["section-url"]||void 0},seoMetadata.data);if(!result.description){const homeSeoData=config["seo-metadata"].find(page=>"home"===page["owner-type"])||{data:{description:""}};result.description=customSeo.description||homeSeoData.data.description}return result.ogTitle=customSeo.ogTitle||result.title,result.ogDescription=customSeo.ogDescription||result.description,result}}if(seoConfig.customTags&&seoConfig.customTags[pageType])return buildCustomTags(seoConfig.customTags,pageType);function getShellSeoData(config){const seoMetadata=config["seo-metadata"].find(meta=>"home"===meta["owner-type"])||{};return Object.assign({},seoMetadata.data,{canonicalUrl:SKIP_CANONICAL})}switch(pageType){case"home-page":return findRelevantConfig("home");case"section-page":return findRelevantConfig("section",lodash.get(data,["data","section","id"]))||getSeoDataFromCollection(config,data)||getSeoData(config,"home-page",data,url);case"collection-page":return getSeoDataFromCollection(config,data)||getSeoData(config,"home-page",data,url);case"tag-page":return buildTagsFromTopic(config,lodash.get(data,["data","tag"]),url,data)||getSeoData(config,"home-page",data,url);case"story-page":return buildTagsFromStory(config,lodash.get(data,["data","story"]),url,data)||getSeoData(config,"home-page",data,url);case"visual-story":return buildTagsFromStory(config,lodash.get(data,["story"]),url,data)||getSeoData(config,"home-page",data,url);case"story-page-amp":return buildTagsFromStory(config,lodash.get(data,["data","story"]),url,data)||getSeoData(config,"home-page",data,url);case"author-page":return buildTagsFromAuthor(config,lodash.get(data,["data","author"],{}),url,data)||getSeoData(config,"home-page",data,url);case"static-page":return buildTagsFromStaticPage(config,lodash.get(data,["data","page"],{}),url,data)||getSeoData(config,"home-page",data,url);case"shell":return getShellSeoData(config);default:return getSeoData(config,"home-page",data,url)}}function getSeoDataFromCollection(config,data){if(lodash.get(data,["data","collection","name"])){let{name:name,summary:summary}=lodash.get(data,["data","collection"]);const customSeo=lodash.get(data,["data","customSeo"],{});summary||(summary=(getSeoData(config,"home-page",data)||{}).description);const title=customSeo.title||name,pageTitle=customSeo["page-title"]||name,ogTitle=customSeo.ogTitle||title,description=customSeo.description||summary,ogDescription=customSeo.ogDescription||summary;return{"page-title":pageTitle,title:title,ogTitle:ogTitle,description:description,ogDescription:ogDescription,canonicalUrl:SKIP_CANONICAL}}}const SKIP_CANONICAL="__SKIP__CANONICAL__";function TextTags(seoConfig,config,pageType,data,{url:url}){const seoData=getSeoData(config,pageType,data,url,seoConfig),customSeo=lodash.get(data,["data","customSeo"],{});if(!seoData)return[];const currentUrl=`${config["sketches-host"]}${url.pathname}`,basicTags={description:customSeo.description||seoData.description,title:customSeo.title||seoData.title,keywords:customSeo.keywords||seoData.keywords},ogUrl=customSeo.ogUrl||seoData.ogUrl||seoData.canonicalUrl||currentUrl,ogTags=seoConfig.enableOgTags?{"og:type":"story-page"===pageType||"story-page-amp"===pageType?"article":"website","og:url":ogUrl===SKIP_CANONICAL?void 0:ogUrl,"og:title":customSeo.ogTitle||seoData.ogTitle,"og:description":customSeo.ogDescription||seoData.ogDescription}:void 0,twitterTags=seoConfig.enableTwitterCards?{"twitter:card":"summary_large_image","twitter:title":customSeo.twitterTitle||seoData.ogTitle,"twitter:description":customSeo.twitterDescription||seoData.ogDescription}:void 0,allTags=Object.assign(basicTags,ogTags,twitterTags),commonTags=[{tag:"title",children:customSeo.title||data.title||seoData["page-title"]}],canonical=seoData.canonicalUrl||currentUrl;return canonical!=SKIP_CANONICAL&&commonTags.push({tag:"link",rel:"canonical",href:canonical}),"story-page"!==pageType&&"story-page-amp"!==pageType||commonTags.push({name:"author",content:seoData.author}),"story-page"!==pageType&&"story-page-amp"!==pageType||!seoConfig.enableNews||(commonTags.push({name:"news_keywords",content:seoData.keywords}),lodash.get(data,["data","story","seo","meta-google-news-standout"])&&commonTags.push({tag:"link",rel:"standout",href:seoData.storyUrl||currentUrl})),commonTags.concat(objectToTags(allTags))}function getTitle(seoConfig,config,pageType,data,params){const customSeo=lodash.get(data,["data","customSeo"],{});if(lodash.get(data,["title"]))return customSeo.title||lodash.get(data,["title"]);if(lodash.get(data,["data","title"]))return customSeo.title||lodash.get(data,["data","title"]);const seoData=getSeoData(config,pageType,data,void 0,seoConfig)||{};return customSeo.title||seoData["page-title"]}function tagToKey(tag){switch(tag.tag||"meta"){case"meta":return`meta-${tag.name||tag.itemprop||"name"}-${tag.property||"property"}`;case"link":return`link-${tag.rel}`;case"title":return"title";default:return Math.random().toString()}}class MetaTagList{constructor(tags){this.tags=tags}toString(){const uniqueTags=lodash.uniqBy(this.tags.reverse(),tagToKey).reverse();return ReactDomServer__default.default.renderToStaticMarkup(uniqueTags.map(tag=>React__default.default.createElement(tag.tag||"meta",lodash.omit(tag,"tag"))))}addTag(){return new MetaTagList(this.tags.concat([].slice.call(arguments)))}}class SEO{constructor(seoConfig={}){this.seoConfig=seoConfig,this.generators=(seoConfig.generators||[TextTags,ImageTags,AuthorTags,StaticTags,StructuredDataTags,StoryAmpTags]).concat(seoConfig.extraGenerators||[])}getMetaTags(config,pageType,data,params={}){return pageType=lodash.get(this.seoConfig,["pageTypeAliases",pageType],pageType),new MetaTagList(lodash.flatMap(this.generators,generator=>generator(this.seoConfig,config,pageType,data,params)))}getTitle(config,pageType,data,params={}){return getTitle(this.seoConfig,config,pageType,data)}}exports.AuthorTags=AuthorTags,exports.ImageTags=ImageTags,exports.MetaTagList=MetaTagList,exports.SEO=SEO,exports.StaticTags=StaticTags,exports.StoryAmpTags=StoryAmpTags,exports.StructuredDataTags=StructuredDataTags,exports.TextTags=TextTags,exports.generateStaticData=generateStaticData,exports.generateStructuredData=generateStructuredData;
package/index.js CHANGED
@@ -1,20 +1,20 @@
1
- import {omit, flatMap, get, uniqBy} from "lodash";
1
+ import { flatMap, get, omit, uniqBy } from "lodash";
2
2
  import React from "react";
3
- import ReactDomServer from"react-dom/server";
3
+ import ReactDomServer from "react-dom/server";
4
+ import { StoryAmpTags } from './src/amp-tags.js';
5
+ import { AuthorTags } from './src/author-tags.js';
6
+ import { generateStaticData, generateStructuredData } from './src/generate-common-seo';
7
+ import { ImageTags } from './src/image-tags.js';
8
+ import { StaticTags } from './src/static-tags.js';
9
+ import { StructuredDataTags } from './src/structured-data/structured-data-tags.js';
10
+ import { getTitle, TextTags } from './src/text-tags.js';
4
11
 
5
- import {TextTags, getTitle} from './src/text-tags.js';
6
- import {StaticTags} from './src/static-tags.js';
7
- import {AuthorTags} from './src/author-tags.js';
8
- import {ImageTags} from './src/image-tags.js';
9
- import {StructuredDataTags} from './src/structured-data/structured-data-tags.js';
10
- import {StoryAmpTags} from './src/amp-tags.js';
11
- import {generateStaticData, generateStructuredData} from './src/generate-common-seo';
12
12
 
13
- export {TextTags, StaticTags, AuthorTags, ImageTags, StructuredDataTags, StoryAmpTags, generateStaticData, generateStructuredData};
13
+ export { TextTags, StaticTags, AuthorTags, ImageTags, StructuredDataTags, StoryAmpTags, generateStaticData, generateStructuredData };
14
14
 
15
15
  function tagToKey(tag) {
16
- switch(tag.tag || "meta") {
17
- case "meta": return `meta-${tag.name || "name"}-${tag.property || "property"}`;
16
+ switch (tag.tag || "meta") {
17
+ case "meta": return `meta-${tag.name || tag.itemprop || "name"}-${tag.property || "property"}`;
18
18
  case "link": return `link-${tag.rel}`;
19
19
  case "title": return `title`;
20
20
  default: return Math.random().toString();
@@ -28,7 +28,7 @@ export class MetaTagList {
28
28
 
29
29
  toString() {
30
30
  const uniqueTags = uniqBy(this.tags.reverse(), tagToKey).reverse();
31
- return ReactDomServer.renderToStaticMarkup(uniqueTags.map(tag => React.createElement(tag.tag || "meta", omit(tag, "tag"))))
31
+ return ReactDomServer.renderToStaticMarkup(uniqueTags.map(tag => React.createElement(tag.tag || "meta", omit(tag, "tag"))));
32
32
  }
33
33
 
34
34
  addTag() {
@@ -102,6 +102,6 @@ export class SEO {
102
102
  }
103
103
 
104
104
  getTitle(config, pageType, data, params = {}) {
105
- return getTitle(this.seoConfig, config, pageType, data, params)
105
+ return getTitle(this.seoConfig, config, pageType, data, params);
106
106
  }
107
107
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quintype/seo",
3
- "version": "1.40.9",
3
+ "version": "1.40.10",
4
4
  "description": "SEO Modules for Quintype",
5
5
  "main": "dist/index.cjs.js",
6
6
  "repository": "https://github.com/quintype/quintype-node-seo",