/* catcrafts.net Copyright (C) 2026 Catcrafts The source code of this website is made available for viewing purposes only. No permission is granted to copy, modify, distribute, or create derivative works. */ // The compiled-in catalogue and the schema.org identity graph. Content is // code now; these assertions are the contract the shop pages rely on, // checked against the actual shipped data. import std; import Catcrafts.Shared; using namespace Catcrafts; namespace { int failures = 0; void Check(bool ok, std::string_view what, std::string_view got = {}) { if (ok) return; ++failures; std::println(std::cerr, "FAIL: {}{}{}", what, got.empty() ? "" : " got: ", got); } // ── the compiled-in catalogue ───────────────────────────────────────── void CatalogueContract() { using namespace Catcrafts::Money; const auto& products = Content::Products(); Check(products.size() == 1, "content: one product"); if (products.size() == 1) { const Product& pr = products[0]; Check(pr.slug == "fp6-pmos", "content: product slug"); // Coming-soon is the pre-launch state; launch flips it to // "available" and this check keeps passing either way. Check(pr.Buyable() || pr.ComingSoon(), "content: product is buyable or deliberately coming soon"); Check(pr.variants.size() == 3, "content: three colours"); // Cost-plus pricing, derived in code: supplier + €50, exactly. Check(pr.FindVariant("green") && pr.FindVariant("green")->priceInclMinor == 56330, "content: green = 513.30 supplier + 50 markup"); Check(pr.FindVariant("black") && pr.FindVariant("black")->priceInclMinor == 56930, "content: black = 519.30 supplier + 50 markup"); Check(pr.FindVariant("white") && pr.FindVariant("white")->priceInclMinor == 65488, "content: white = 604.88 supplier + 50 markup"); Check(pr.FindVariant("mauve") == nullptr, "content: unknown colour is null"); Check(pr.priceInclMinor == 56330, "content: from-price is the cheapest variant"); Check(pr.CheapestVariant() && pr.CheapestVariant()->slug == "green", "content: cheapest is green"); Check(pr.safetyNote.find("112") != std::string::npos && pr.safetyNote.find("not yet verified") != std::string::npos, "content: emergency-calling safety warning present and honest"); 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 variant Product per colour, each // with its ONE offer — built from the same integers the // checkout charges. { // The listing's shipping block is now carrier data, so the // render needs a table. US is priced here on purpose: the // carrier will happily quote it and the shop still must not // advertise it. const std::vector feedTable{ { "NL", { { 2000, 895 } } }, { "DE", { { 2000, 995 } } }, { "GB", { { 2000, 2450 } } }, { "US", { { 2000, 1794 } } }, }; auto pp = Views::RenderProduct(pr, Rates{}, feedTable); auto ld = Json::Parse(pp.meta.jsonLd); bool variantsOk = false; if (ld && ld->IsObject()) { 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") == "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 && pp.meta.jsonLd.find("\"productGroupID\"") != std::string::npos, "schema: variants carry shipping, returns, sku and group id"); // The published rates ARE the carrier's, at one unit's weight. Check(pp.meta.jsonLd.find("\"8.95\"") != std::string::npos && pp.meta.jsonLd.find("\"24.50\"") != std::string::npos, "schema: shipping rates come from the carrier table"); Check(pp.meta.jsonLd.find("\"17.94\"") == std::string::npos && pp.meta.jsonLd.find("\"US\"") == std::string::npos, "schema: a refused destination is never advertised, priced or not"); // No table: no shipping claim. The listing loses the merchant // block rather than inventing a rate — the whole point of // dropping the zone fallback. auto bare = Views::RenderProduct(pr, Rates{}); Check(bare.meta.jsonLd.find("OfferShippingDetails") == std::string::npos && bare.meta.jsonLd.find("MerchantReturnPolicy") == std::string::npos, "schema: with no carrier table the offer publishes no shipping"); Check(Json::Parse(bare.meta.jsonLd).has_value() && bare.meta.jsonLd.find("\"productGroupID\"") != std::string::npos, "schema: and the rest of the record still parses"); } } Check(!Content::Projects().empty(), "content: projects present"); Check(Content::LegalPages().size() == 3, "content: three legal pages"); Check(Content::AboutPage().sections.size() >= 3 && !Content::AboutPage().sections[0].body.empty() && Content::AboutPage().sections[0].body[0].find("Jorijn van der Graaf") != std::string::npos, "content: about page names the founder"); 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. void IdentityGraph() { constexpr std::string_view kOrgId = "https://catcrafts.net/#organization"; auto home = Views::RenderHome(Content::Projects(), std::span{}); 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"); } } // namespace int main() { CatalogueContract(); IdentityGraph(); if (failures != 0) { std::println(std::cerr, "{} check(s) failed", failures); return 1; } return 0; }