parent
f2d20bb409
commit
fca089c20d
5 changed files with 298 additions and 70 deletions
|
|
@ -422,25 +422,34 @@ void RunMoneySelfTest() {
|
|||
Check(pr.warranty.find("TODO") == std::string::npos && pr.warranty.size() > 100,
|
||||
"content: warranty is written, not a placeholder");
|
||||
// The product page's schema.org record must parse with our own
|
||||
// JSON parser and carry one offer per colour — the offers are
|
||||
// built from the same integers the checkout charges.
|
||||
// JSON parser and carry one variant Product per colour, each
|
||||
// with its ONE offer — built from the same integers the
|
||||
// checkout charges.
|
||||
{
|
||||
auto pp = Views::RenderProduct(pr, Rates{});
|
||||
auto ld = Json::Parse(pp.meta.jsonLd);
|
||||
bool offersOk = false;
|
||||
bool variantsOk = false;
|
||||
if (ld && ld->IsObject()) {
|
||||
if (const Json::Value* o = ld->Find("offers"); o && o->IsArray()) {
|
||||
offersOk = o->array.size() == pr.variants.size();
|
||||
if (const Json::Value* v = ld->Find("hasVariant");
|
||||
v && v->IsArray() && v->array.size() == pr.variants.size()) {
|
||||
variantsOk = true;
|
||||
for (const Json::Value& node : v->array) {
|
||||
const Json::Value* o = node.Find("offers");
|
||||
variantsOk = variantsOk && node.Str("@type") == "Product"
|
||||
&& o && o->IsObject();
|
||||
}
|
||||
}
|
||||
}
|
||||
Check(ld && ld->IsObject() && ld->Str("@type") == "Product" && offersOk,
|
||||
"schema: product JSON-LD parses, one offer per variant");
|
||||
// Merchant-grade fields: shipping, returns, brand, sku — what
|
||||
// Merchant Center's website-crawl feed reads at launch.
|
||||
Check(ld && ld->IsObject() && ld->Str("@type") == "ProductGroup" && variantsOk,
|
||||
"schema: product JSON-LD parses, one variant per colour");
|
||||
// Merchant-grade fields: shipping, returns, sku, group id —
|
||||
// what Merchant Center's website-crawl feed reads at launch
|
||||
// (productGroupID is its item_group_id).
|
||||
Check(pp.meta.jsonLd.find("OfferShippingDetails") != std::string::npos
|
||||
&& pp.meta.jsonLd.find("MerchantReturnPolicy") != std::string::npos
|
||||
&& pp.meta.jsonLd.find("\"sku\"") != std::string::npos,
|
||||
"schema: offers carry shipping, returns and sku");
|
||||
&& pp.meta.jsonLd.find("\"sku\"") != std::string::npos
|
||||
&& pp.meta.jsonLd.find("\"productGroupID\"") != std::string::npos,
|
||||
"schema: variants carry shipping, returns, sku and group id");
|
||||
}
|
||||
}
|
||||
Check(!Content::Projects().empty(), "content: projects present");
|
||||
|
|
@ -453,6 +462,86 @@ void RunMoneySelfTest() {
|
|||
Check(!Content::Demos().empty(), "content: demos present");
|
||||
}
|
||||
|
||||
// ── the identity graph ────────────────────────────────────────────
|
||||
// "Catcrafts" is two common words with no space, so it competes with a
|
||||
// decade of kids' craft blogs, Etsy and a Minecraft server on the
|
||||
// singular domain. The way out is not prose: it is one registered entity
|
||||
// that every page points at by @id. Those joins are worth asserting
|
||||
// because breaking one is silent — a typo'd @id still renders, still
|
||||
// validates as JSON-LD, and still splits the graph back into three
|
||||
// same-named strangers, which is the exact failure this markup exists to
|
||||
// prevent.
|
||||
{
|
||||
constexpr std::string_view kOrgId = "https://catcrafts.net/#organization";
|
||||
|
||||
auto home = Views::RenderHome(Content::Projects(), std::span<const Post>{});
|
||||
auto ld = Json::Parse(home.meta.jsonLd);
|
||||
const Json::Value* org = nullptr;
|
||||
const Json::Value* site = nullptr;
|
||||
if (ld && ld->IsObject()) {
|
||||
if (const Json::Value* g = ld->Find("@graph"); g && g->IsArray()) {
|
||||
for (const Json::Value& node : g->array) {
|
||||
if (node.Str("@type") == "Organization") org = &node;
|
||||
if (node.Str("@type") == "WebSite") site = &node;
|
||||
}
|
||||
}
|
||||
}
|
||||
Check(org && site, "schema: home graph parses, carries Organization and WebSite");
|
||||
|
||||
if (org && site) {
|
||||
Check(org->Str("@id") == kOrgId, "schema: organization node is identified");
|
||||
Check(site->Str("@id") == "https://catcrafts.net/#website",
|
||||
"schema: website node is identified");
|
||||
// The join that makes two nodes one entity rather than two.
|
||||
const Json::Value* publisher = site->Find("publisher");
|
||||
Check(publisher && publisher->IsObject() && publisher->Str("@id") == kOrgId,
|
||||
"schema: website is published by the organization node");
|
||||
// The navigational query "catcrafts" is answered from the site
|
||||
// entity, so the spellings people type belong on it.
|
||||
const Json::Value* alt = site->Find("alternateName");
|
||||
Check(alt && alt->IsArray() && !alt->array.empty(),
|
||||
"schema: website carries the spellings people type");
|
||||
|
||||
// Registry numbers, typed. These are the part no name-twin can
|
||||
// produce — each one is checkable against a public register,
|
||||
// which also means a wrong value is worse than no value.
|
||||
bool kvk = false, vat = false, eori = false;
|
||||
if (const Json::Value* ids = org->Find("identifier"); ids && ids->IsArray()) {
|
||||
for (const Json::Value& id : ids->array) {
|
||||
if (id.Str("propertyID") == "KVK") kvk = id.Str("value") == "78437059";
|
||||
if (id.Str("propertyID") == "VAT") vat = id.Str("value") == "NL003329281B38";
|
||||
if (id.Str("propertyID") == "EORI") eori = id.Str("value") == "NL1900095326";
|
||||
}
|
||||
}
|
||||
Check(kvk && vat && eori, "schema: KVK, VAT and EORI present and exact");
|
||||
|
||||
// The name-twin guard. "Cat Crafts" with a space is the generic
|
||||
// craft phrase owned by everyone else; claiming it as an alternate
|
||||
// name argues for merging this entity into the corpus it needs to
|
||||
// stay distinct from.
|
||||
Check(home.meta.jsonLd.find("Cat Crafts") == std::string::npos,
|
||||
"schema: the spaced generic is not claimed as a brand name");
|
||||
}
|
||||
|
||||
// Cross-page joins: both must name the SAME @id the home page defines.
|
||||
auto about = Views::RenderAbout(Content::AboutPage());
|
||||
Check(about.meta.jsonLd.find(kOrgId) != std::string::npos,
|
||||
"schema: about joins the founder to the organization node");
|
||||
if (!Content::Products().empty()) {
|
||||
auto pp = Views::RenderProduct(Content::Products()[0], Rates{});
|
||||
Check(pp.meta.jsonLd.find(kOrgId) != std::string::npos,
|
||||
"schema: offers are sold by the organization node");
|
||||
}
|
||||
|
||||
// The person join, same mechanism in the other direction: home's
|
||||
// founder and about's mainEntity must name one Person node, or "who
|
||||
// founded Catcrafts" splits into two same-named strangers too.
|
||||
constexpr std::string_view kPersonId = "https://catcrafts.net/about#person";
|
||||
Check(home.meta.jsonLd.find(kPersonId) != std::string::npos
|
||||
&& about.meta.jsonLd.find(kPersonId) != std::string::npos,
|
||||
"schema: founder and about name one Person node");
|
||||
}
|
||||
|
||||
// ── the Sendcloud response parser ─────────────────────────────────
|
||||
{
|
||||
const auto table = Server::ParseSendcloudMethods(R"({"shipping_methods":[
|
||||
|
|
|
|||
Loading…
Reference in a new issue