package schema import "strings" // The levels of a finding. const ( LevelRequired = "required" LevelRecommended = "recommended" ) // Finding is one missing property of a node. type Finding struct { Level string `json:"level"` Property string `json:"property"` } // Report is a node with its findings. Rules is false for a type Google has // no rich-result rules for (FAQPage, CollectionPage, WebPage, Service …; // RESEARCH Pitfall 11): it is checked as valid schema.org only. type Report struct { ID string `json:"id"` Type string `json:"type"` Findings []Finding `json:"findings"` Rules bool `json:"rules"` } // rule is the required and recommended properties of a type (Google's // structured data documentation, cited in 33-RESEARCH.md). type rule struct { required []string recommended []string } var articleRule = rule{recommended: []string{"author", "datePublished", "dateModified", "headline", "image"}} var localRule = rule{required: []string{"name", "address"}, recommended: []string{"telephone", "url", "geo", "openingHoursSpecification", "priceRange", "image"}} // rules lists the types with Google rules; every other type has none. var rules = map[string]rule{ TypeArticle: articleRule, TypeBlogPosting: articleRule, TypeNewsArticle: articleRule, TypeBreadcrumbList: {required: []string{"itemListElement"}}, TypeVideoObject: {required: []string{"name", "thumbnailUrl", "uploadDate"}, recommended: []string{"description", "embedUrl"}}, TypeOrganization: {recommended: []string{"name", "url", "logo"}}, TypePerson: {required: []string{"name"}, recommended: []string{"url", "image"}}, TypeWebSite: {required: []string{"name", "url"}}, TypeProfilePage: {required: []string{"mainEntity"}}, } // localTypes are the LocalBusiness types (UI-SPEC C20), checked with the // LocalBusiness rules. var localTypes = map[string]bool{ "LocalBusiness": true, "ProfessionalService": true, "Store": true, "FoodEstablishment": true, "HealthAndBeautyBusiness": true, "MedicalBusiness": true, "LegalService": true, "HomeAndConstructionBusiness": true, "AutomotiveBusiness": true, "FinancialService": true, } // RulesFor returns the rule of typ and whether Google has rules for it. func RulesFor(typ string) (required, recommended []string, ok bool) { if localTypes[typ] { return localRule.required, localRule.recommended, true } r, ok := rules[typ] return r.required, r.recommended, ok } // NodeType is a node's first @type, "" when it has none. func NodeType(n map[string]any) string { switch t := n["@type"].(type) { case string: return t case []any: for _, v := range t { if s, ok := v.(string); ok { return s } } case []string: if len(t) > 0 { return t[0] } } return "" } // present reports whether a node property carries a value. func present(v any) bool { switch x := v.(type) { case nil: return false case string: return strings.TrimSpace(x) != "" case []any: return len(x) > 0 case map[string]any: return len(x) > 0 } return true } // ValidateNode checks one node against its type's rules. func ValidateNode(n map[string]any) Report { typ := NodeType(n) id, _ := n["@id"].(string) rep := Report{ID: id, Type: typ, Findings: []Finding{}} required, recommended, ok := RulesFor(typ) rep.Rules = ok for _, p := range required { if !present(n[p]) { rep.Findings = append(rep.Findings, Finding{Level: LevelRequired, Property: p}) } } if typ == TypeBreadcrumbList { if items, _ := n["itemListElement"].([]any); len(items) > 0 && len(items) < 2 { rep.Findings = append(rep.Findings, Finding{Level: LevelRequired, Property: "itemListElement"}) } } for _, p := range recommended { if !present(n[p]) { rep.Findings = append(rep.Findings, Finding{Level: LevelRecommended, Property: p}) } } return rep } // Nodes returns the node objects of a graph. func Nodes(graph map[string]any) []map[string]any { var out []map[string]any switch list := graph["@graph"].(type) { case []any: for _, raw := range list { if n, ok := raw.(map[string]any); ok { out = append(out, n) } } case []map[string]any: out = append(out, list...) } return out } // Validate checks every node of a graph (D-46): the admin preview, the // MCP tool and the publisher preview share it. func Validate(graph map[string]any) []Report { out := []Report{} for _, n := range Nodes(graph) { out = append(out, ValidateNode(n)) } return out }