schemaorg_rs/profiles/google/
faqpage.rs1use crate::types::{SchemaNode, SchemaValue};
11use crate::validation::diagnostics::{DiagnosticCode, Severity, ValidationDiagnostic};
12use crate::validation::ValidationDiagnostic as VD;
13
14use super::common::{get_nested_nodes, has_non_empty_property, require_property};
15use crate::profiles::{NodeProfileResult, Profile, TypeEligibility};
16
17pub struct GoogleFaqPageProfile;
19
20impl Profile for GoogleFaqPageProfile {
21 fn name(&self) -> &'static str {
22 "google"
23 }
24
25 fn version(&self) -> &'static str {
26 "2026-04-01"
27 }
28
29 fn source_url(&self) -> &'static str {
30 "https://developers.google.com/search/docs/appearance/structured-data/faqpage"
31 }
32
33 fn supported_types(&self) -> &[&str] {
34 &["FAQPage"]
35 }
36
37 fn evaluate_node(&self, node: &SchemaNode, _vocab_diagnostics: &[VD]) -> NodeProfileResult {
38 let path = "FAQPage";
39 let mut diagnostics = Vec::new();
40 let mut required_missing = Vec::new();
41
42 diagnostics.push(ValidationDiagnostic {
44 path: path.to_string(),
45 severity: Severity::Info,
46 code: DiagnosticCode::EligibilityRestricted,
47 message: "FAQPage rich results eligibility is restricted to authoritative sites \
48 since 2024. Structural validation passed, but display depends on \
49 Google's site authority assessment."
50 .to_string(),
51 source_location: None,
52 });
53
54 if let Some(d) = require_property(node, "mainEntity", path) {
56 required_missing.push("mainEntity".to_string());
57 diagnostics.push(d);
58 }
59
60 let questions = get_nested_nodes(node, "mainEntity");
62 if questions.is_empty() && has_non_empty_property(node, "mainEntity") {
63 if let Some(values) = node.properties.get("mainEntity") {
66 let has_any_node = values.iter().any(|v| matches!(v, SchemaValue::Node(_)));
67 if !has_any_node {
68 diagnostics.push(ValidationDiagnostic {
69 path: format!("{path}.mainEntity"),
70 severity: Severity::Error,
71 code: DiagnosticCode::InvalidFieldValue,
72 message: "mainEntity must contain Question objects".to_string(),
73 source_location: None,
74 });
75 }
76 }
77 }
78
79 for (i, question) in questions.iter().enumerate() {
80 let q_path = if questions.len() > 1 {
81 format!("{path}.mainEntity[{i}]")
82 } else {
83 format!("{path}.mainEntity")
84 };
85
86 if let Some(d) = require_property(question, "name", &q_path) {
88 diagnostics.push(d);
89 }
90
91 if let Some(d) = require_property(question, "acceptedAnswer", &q_path) {
93 diagnostics.push(d);
94 }
95
96 for answer in get_nested_nodes(question, "acceptedAnswer") {
98 let a_path = format!("{q_path}.acceptedAnswer");
99 if let Some(d) = require_property(answer, "text", &a_path) {
100 diagnostics.push(d);
101 }
102 }
103 }
104
105 let eligible = required_missing.is_empty()
106 && !diagnostics.iter().any(|d| {
107 d.severity == Severity::Error && d.code != DiagnosticCode::EligibilityRestricted
108 });
109
110 NodeProfileResult {
111 type_eligibility: TypeEligibility {
112 schema_type: "FAQPage".to_string(),
113 eligible,
114 required_missing,
115 recommended_missing: Vec::new(),
116 field_diagnostics: diagnostics,
117 },
118 }
119 }
120}