@quintype/seo 1.51.0 → 1.52.2
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,12 @@
|
|
|
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.52.2](https://github.com/quintype/quintype-node-seo/compare/v1.52.1...v1.52.2) (2026-06-24)
|
|
6
|
+
|
|
7
|
+
### [1.52.1](https://github.com/quintype/quintype-node-seo/compare/v1.52.0...v1.52.1) (2026-06-24)
|
|
8
|
+
|
|
9
|
+
## [1.52.0](https://github.com/quintype/quintype-node-seo/compare/v1.51.0...v1.52.0) (2026-06-24)
|
|
10
|
+
|
|
5
11
|
## [1.51.0](https://github.com/quintype/quintype-node-seo/compare/v1.50.3...v1.51.0) (2026-06-03)
|
|
6
12
|
|
|
7
13
|
|
package/dist/index.cjs.js
CHANGED
|
@@ -809,6 +809,46 @@ function getCompleteTextOfOneCard(storyElements, stripHtmlFromArticleBody) {
|
|
|
809
809
|
return getTextElementsOfOneCard(storyElements).map(item => stripHtmlFromArticleBody ? getPlainText(item.text) : item.text).join(".");
|
|
810
810
|
}
|
|
811
811
|
|
|
812
|
+
function getFaqQuestionItem(question = "", answer = "") {
|
|
813
|
+
const name = getPlainText(question).trim();
|
|
814
|
+
const text = getPlainText(answer).trim();
|
|
815
|
+
|
|
816
|
+
if (!name || !text) return null;
|
|
817
|
+
|
|
818
|
+
return {
|
|
819
|
+
"@type": "Question",
|
|
820
|
+
name,
|
|
821
|
+
acceptedAnswer: {
|
|
822
|
+
"@type": "Answer",
|
|
823
|
+
text
|
|
824
|
+
}
|
|
825
|
+
};
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
function getFaqMainEntity(story = {}) {
|
|
829
|
+
const cards = get__default["default"](story, ["cards"], []);
|
|
830
|
+
const faqItems = [];
|
|
831
|
+
|
|
832
|
+
cards.forEach(card => {
|
|
833
|
+
const storyElements = get__default["default"](card, ["story-elements"], []);
|
|
834
|
+
|
|
835
|
+
storyElements.forEach(element => {
|
|
836
|
+
if (element.type !== "text") return;
|
|
837
|
+
if (element.subtype !== "q-and-a") return;
|
|
838
|
+
|
|
839
|
+
const metadataQuestion = get__default["default"](element, ["metadata", "question"], "");
|
|
840
|
+
const metadataAnswer = get__default["default"](element, ["metadata", "answer"], "");
|
|
841
|
+
|
|
842
|
+
if (metadataQuestion && metadataAnswer) {
|
|
843
|
+
const metadataFaqItem = getFaqQuestionItem(metadataQuestion, metadataAnswer);
|
|
844
|
+
if (metadataFaqItem) faqItems.push(metadataFaqItem);
|
|
845
|
+
}
|
|
846
|
+
});
|
|
847
|
+
});
|
|
848
|
+
|
|
849
|
+
return faqItems;
|
|
850
|
+
}
|
|
851
|
+
|
|
812
852
|
function generateLiveBlogPostingData(structuredData = {}, story = {}, publisherConfig = {}, timezone) {
|
|
813
853
|
const imageWidth = 1200;
|
|
814
854
|
const imageHeight = 675;
|
|
@@ -1486,6 +1526,20 @@ function StructuredDataTags({ structuredData = {} }, config, pageType, response
|
|
|
1486
1526
|
return products.length ? products : null;
|
|
1487
1527
|
}
|
|
1488
1528
|
|
|
1529
|
+
function generateFaqTags() {
|
|
1530
|
+
const mainEntity = getFaqMainEntity(story);
|
|
1531
|
+
if (!mainEntity.length) return null;
|
|
1532
|
+
|
|
1533
|
+
return ldJson("FAQPage", { mainEntity });
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1536
|
+
if (!isStructuredDataEmpty && (pageType === "story-page" || pageType === "story-page-amp")) {
|
|
1537
|
+
const faqTags = generateFaqTags();
|
|
1538
|
+
if (faqTags) {
|
|
1539
|
+
tags.push(faqTags);
|
|
1540
|
+
}
|
|
1541
|
+
}
|
|
1542
|
+
|
|
1489
1543
|
// All Pages have: Publisher, Site
|
|
1490
1544
|
// Story Page have : Article/NewsArticle/LiveBlog/Review as appropriate
|
|
1491
1545
|
return tags;
|
package/package.json
CHANGED
|
@@ -290,6 +290,46 @@ function getCompleteTextOfOneCard(storyElements, stripHtmlFromArticleBody) {
|
|
|
290
290
|
.join(".");
|
|
291
291
|
}
|
|
292
292
|
|
|
293
|
+
function getFaqQuestionItem(question = "", answer = "") {
|
|
294
|
+
const name = getPlainText(question).trim();
|
|
295
|
+
const text = getPlainText(answer).trim();
|
|
296
|
+
|
|
297
|
+
if (!name || !text) return null;
|
|
298
|
+
|
|
299
|
+
return {
|
|
300
|
+
"@type": "Question",
|
|
301
|
+
name,
|
|
302
|
+
acceptedAnswer: {
|
|
303
|
+
"@type": "Answer",
|
|
304
|
+
text,
|
|
305
|
+
},
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function getFaqMainEntity(story = {}) {
|
|
310
|
+
const cards = get(story, ["cards"], []);
|
|
311
|
+
const faqItems = [];
|
|
312
|
+
|
|
313
|
+
cards.forEach((card) => {
|
|
314
|
+
const storyElements = get(card, ["story-elements"], []);
|
|
315
|
+
|
|
316
|
+
storyElements.forEach((element) => {
|
|
317
|
+
if (element.type !== "text") return;
|
|
318
|
+
if (element.subtype !== "q-and-a") return;
|
|
319
|
+
|
|
320
|
+
const metadataQuestion = get(element, ["metadata", "question"], "");
|
|
321
|
+
const metadataAnswer = get(element, ["metadata", "answer"], "");
|
|
322
|
+
|
|
323
|
+
if (metadataQuestion && metadataAnswer) {
|
|
324
|
+
const metadataFaqItem = getFaqQuestionItem(metadataQuestion, metadataAnswer);
|
|
325
|
+
if (metadataFaqItem) faqItems.push(metadataFaqItem);
|
|
326
|
+
}
|
|
327
|
+
});
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
return faqItems;
|
|
331
|
+
}
|
|
332
|
+
|
|
293
333
|
function generateLiveBlogPostingData(structuredData = {}, story = {}, publisherConfig = {}, timezone) {
|
|
294
334
|
const imageWidth = 1200;
|
|
295
335
|
const imageHeight = 675;
|
|
@@ -1050,6 +1090,20 @@ export function StructuredDataTags({ structuredData = {} }, config, pageType, re
|
|
|
1050
1090
|
return products.length ? products : null;
|
|
1051
1091
|
}
|
|
1052
1092
|
|
|
1093
|
+
function generateFaqTags() {
|
|
1094
|
+
const mainEntity = getFaqMainEntity(story);
|
|
1095
|
+
if (!mainEntity.length) return null;
|
|
1096
|
+
|
|
1097
|
+
return ldJson("FAQPage", { mainEntity });
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
if (!isStructuredDataEmpty && (pageType === "story-page" || pageType === "story-page-amp")) {
|
|
1101
|
+
const faqTags = generateFaqTags();
|
|
1102
|
+
if (faqTags) {
|
|
1103
|
+
tags.push(faqTags);
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1053
1107
|
// All Pages have: Publisher, Site
|
|
1054
1108
|
// Story Page have : Article/NewsArticle/LiveBlog/Review as appropriate
|
|
1055
1109
|
return tags;
|
|
@@ -662,6 +662,88 @@ describe("StructuredDataTags", function () {
|
|
|
662
662
|
});
|
|
663
663
|
});
|
|
664
664
|
|
|
665
|
+
describe("with FAQ schema data", function () {
|
|
666
|
+
it("adds FAQPage schema for q-and-a subtype when both question and answer are in metadata", function () {
|
|
667
|
+
const cards = [
|
|
668
|
+
{
|
|
669
|
+
"story-elements": [
|
|
670
|
+
{
|
|
671
|
+
type: "text",
|
|
672
|
+
subtype: "q-and-a",
|
|
673
|
+
metadata: {
|
|
674
|
+
question: "What is FAQ Schema?",
|
|
675
|
+
answer: "FAQ Schema is structured data that helps search engines understand FAQ content.",
|
|
676
|
+
},
|
|
677
|
+
},
|
|
678
|
+
{
|
|
679
|
+
type: "text",
|
|
680
|
+
subtype: "q-and-a",
|
|
681
|
+
metadata: {
|
|
682
|
+
question: "Is FAQ markup mandatory?",
|
|
683
|
+
answer: "No, it is optional.",
|
|
684
|
+
},
|
|
685
|
+
},
|
|
686
|
+
],
|
|
687
|
+
},
|
|
688
|
+
];
|
|
689
|
+
|
|
690
|
+
const string = getSeoMetadata(
|
|
691
|
+
getSeoConfig({ newsArticle: false }),
|
|
692
|
+
{},
|
|
693
|
+
"story-page",
|
|
694
|
+
sampleStoryData(null, cards, sampleAuthorsData()),
|
|
695
|
+
{ url: url.parse("/") }
|
|
696
|
+
);
|
|
697
|
+
const ampPageString = getSeoMetadata(
|
|
698
|
+
getSeoConfig({ newsArticle: false }),
|
|
699
|
+
{},
|
|
700
|
+
"story-page-amp",
|
|
701
|
+
sampleStoryData(null, cards, sampleAuthorsData()),
|
|
702
|
+
{ url: url.parse("/") }
|
|
703
|
+
);
|
|
704
|
+
|
|
705
|
+
assertContains('"@type":"FAQPage"', string);
|
|
706
|
+
assertContains('"name":"What is FAQ Schema?"', string);
|
|
707
|
+
assertContains('"acceptedAnswer":{"@type":"Answer","text":"FAQ Schema is structured data that helps search engines understand FAQ content."}', string);
|
|
708
|
+
assertContains('"name":"Is FAQ markup mandatory?"', string);
|
|
709
|
+
assertContains('"text":"No, it is optional."', string);
|
|
710
|
+
assertContains('"@type":"FAQPage"', ampPageString);
|
|
711
|
+
});
|
|
712
|
+
|
|
713
|
+
it("does not add FAQPage schema when q-and-a metadata is missing question or answer", function () {
|
|
714
|
+
const cards = [
|
|
715
|
+
{
|
|
716
|
+
"story-elements": [
|
|
717
|
+
{
|
|
718
|
+
type: "text",
|
|
719
|
+
subtype: "q-and-a",
|
|
720
|
+
metadata: {
|
|
721
|
+
question: "What is FAQ Schema?",
|
|
722
|
+
},
|
|
723
|
+
},
|
|
724
|
+
{
|
|
725
|
+
type: "text",
|
|
726
|
+
subtype: "q-and-a",
|
|
727
|
+
metadata: {
|
|
728
|
+
answer: "Only answer, no question.",
|
|
729
|
+
},
|
|
730
|
+
},
|
|
731
|
+
],
|
|
732
|
+
},
|
|
733
|
+
];
|
|
734
|
+
|
|
735
|
+
const string = getSeoMetadata(
|
|
736
|
+
getSeoConfig({ newsArticle: false }),
|
|
737
|
+
{},
|
|
738
|
+
"story-page",
|
|
739
|
+
sampleStoryData(null, cards, sampleAuthorsData()),
|
|
740
|
+
{ url: url.parse("/") }
|
|
741
|
+
);
|
|
742
|
+
|
|
743
|
+
assertDoesNotContains('"@type":"FAQPage"', string);
|
|
744
|
+
});
|
|
745
|
+
});
|
|
746
|
+
|
|
665
747
|
describe("articleBody content", function () {
|
|
666
748
|
const cards = [
|
|
667
749
|
{
|