diff --git a/.forgejo/workflows/deploy.yaml b/.forgejo/workflows/deploy.yaml index 1b278d1..1a4d160 100644 --- a/.forgejo/workflows/deploy.yaml +++ b/.forgejo/workflows/deploy.yaml @@ -37,7 +37,7 @@ jobs: nodejs \ clang lld libc++ \ wasi-libc wasi-libc++ wasi-libc++abi wasi-compiler-rt \ - git curl tar rsync zstd gzip jq openssl ffmpeg gnupg \ + git curl tar rsync zstd gzip jq openssl ffmpeg gnupg llvm \ cmake make perl # cmake/make/perl: the native server product pulls Crafter.Network, # which builds msquic (ExternalBuilder::CMake) with quictls — CMake's diff --git a/content/posts.json b/content/posts.json index aa44915..7b3a920 100644 --- a/content/posts.json +++ b/content/posts.json @@ -8,9 +8,11 @@ "excerpt": "While celebrating my previous post of working call audio @pvagner@fedi.ml left this excellent comment: And this reminded me something that i and many devs sadly often forget, if its works for me it doesn't mean it works for everyone. My philosophy is that linux should be as…", "media": [ { - "src": "https://files.catbox.moe/q0acuo.mp4", + "src": "/media/54d923869e19d71c.mp4", "kind": "video", - "poster": "https://ani.social/pictrs/image/5948f014-e9d4-4ace-bf6f-6f85e9b5f2fc.webp" + "poster": "/media/63acea1959c6400d.webp", + "w": 1080, + "h": 1080 } ], "score": 82, @@ -25,9 +27,11 @@ "excerpt": "At first, man thinks he controls the modem. But after the 20th reboot, man learns the modem controls him. — Epictetus So here it is! working VoLTE calls with audio on linux! What a rollercoaster it has been i'll tell you that. The frequency between my previous posts was only a…", "media": [ { - "src": "https://ani.social/pictrs/image/50e19f20-3667-4368-91ec-d787c515b9e7.mp4", + "src": "/media/8bc5b5a6565ba864.mp4", "kind": "video", - "poster": "https://ani.social/pictrs/image/00dfc344-88e7-46a1-973b-db7f3e58f679.webp" + "poster": "/media/7133dc70534bf47a.webp", + "w": 854, + "h": 480 } ], "score": 501, @@ -42,9 +46,11 @@ "excerpt": "My quest to 100% feature completeness continues! Today i bring before you... The working microphone! Now everyone is going to ask: Does this mean working calls? I asked the same thing, and with maybe a bit too much excitement i inserted my SIM card. But the answer is no, neither…", "media": [ { - "src": "https://ani.social/pictrs/image/752d0c86-666d-48dc-a0c5-aa2bba430afd.mp4", + "src": "/media/8e0ef8ba7e9769ec.mp4", "kind": "video", - "poster": "https://ani.social/pictrs/image/37bcfe3c-0c6b-4250-b432-7a71cf5de927.webp" + "poster": "/media/7f1d353c81fa55e6.webp", + "w": 854, + "h": 480 } ], "score": 296, @@ -59,9 +65,11 @@ "excerpt": "Well that was easy. Here i was thinking audio was going to be the boss fight... These were small easily identifiable kernel changes, not the entire investigations that GPS and NFC needed. Ill be working to getting calls fully working, but this is already a big step. And speaking…", "media": [ { - "src": "https://ani.social/pictrs/image/5963d0aa-5bff-4d46-bb1c-d3e1f6563918.mp4", + "src": "/media/f9d29d90b4c9c9b9.mp4", "kind": "video", - "poster": "https://ani.social/pictrs/image/25cca4db-40ab-4664-8a1d-6d93e3ffa249.webp" + "poster": "/media/07b79c6415f4c487.webp", + "w": 854, + "h": 480 } ], "score": 165, @@ -76,9 +84,11 @@ "excerpt": "No new features today, just wanted to make a short post. It is completely possible to do linux development on the go! Is it convenient? Probably not. Is a laptop better? Most likely. Is it funny? Yes. Also a double table is definitely better: photo of a linux phone with a…", "media": [ { - "src": "https://ani.social/pictrs/image/a318590f-6ab0-48ac-8a1d-81c37cda77f0.webp", + "src": "/media/1c75b19e98c7b6ef.webp", "kind": "image", - "poster": "" + "poster": "", + "w": 1920, + "h": 887 } ], "score": 264, @@ -93,9 +103,11 @@ "excerpt": "This is the continuation of my ongoing effort to get the FP6 fully feature complete with linux. I chose NFC cause i thought it would be relatively easy like GPS, well that assumption turned out to be wrong. But alot of time later and as you can see in the video now reading…", "media": [ { - "src": "https://ani.social/pictrs/image/fae177a4-73d2-43a7-9dd6-0228af1424f8.mp4", + "src": "/media/c8de4451ce9d5663.mp4", "kind": "video", - "poster": "https://ani.social/pictrs/image/883180d4-2994-44c9-b2a9-9ba5400b3e59.webp" + "poster": "/media/c6a1a1a41b2883e5.webp", + "w": 854, + "h": 480 } ], "score": 218, @@ -110,9 +122,11 @@ "excerpt": "I really want to turn this phone into my daily driver, so i started working on one of the features i would want in a daily driver, working GPS. This is the current component support table of postmarketos: FP6 feature support table i think audio is too complex for me, so i…", "media": [ { - "src": "https://ani.social/pictrs/image/c2561f4d-73ce-4e89-89bd-5ada7990ded9.webp", + "src": "/media/ce6475f10b3baeee.webp", "kind": "image", - "poster": "" + "poster": "", + "w": 887, + "h": 1920 } ], "score": 471, diff --git a/robots.txt b/robots.txt index 0d45eee..7395a6b 100644 --- a/robots.txt +++ b/robots.txt @@ -1,9 +1,7 @@ User-agent: * Allow: / -# The landing page for a submitted reservation. It also carries -# X-Robots-Tag: noindex — this is the belt to that braces, since a crawler that -# never fetches the URL cannot index it by accident. -Disallow: /reserved +# Order pages live at unguessable capability URLs and carry X-Robots-Tag: +# noindex themselves; there is no path prefix worth disallowing here. Sitemap: https://catcrafts.net/sitemap.xml diff --git a/server/implementations/main.cpp b/server/implementations/main.cpp index 061a7ec..f3e06da 100644 --- a/server/implementations/main.cpp +++ b/server/implementations/main.cpp @@ -421,9 +421,29 @@ void RunMoneySelfTest() { "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 offer per colour — the offers are + // built from the same integers the checkout charges. + { + auto pp = Views::RenderProduct(pr, Rates{}); + auto ld = Json::Parse(pp.meta.jsonLd); + bool offersOk = false; + if (ld && ld->IsObject()) { + if (const Json::Value* o = ld->Find("offers"); o && o->IsArray()) { + offersOk = o->array.size() == pr.variants.size(); + } + } + Check(ld && ld->IsObject() && ld->Str("@type") == "Product" && offersOk, + "schema: product JSON-LD parses, one offer per variant"); + } } 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"); } @@ -691,7 +711,7 @@ int main(int argc, char** argv) { // --routes: status + title for every route, for a quick smoke check. if (has("--routes")) { const Views::SiteContent content = LoadContent("content"); - for (std::string_view p : { "/", "/shop", "/shop/fp6-pmos", "/shop/nope", + for (std::string_view p : { "/", "/about", "/shop", "/shop/fp6-pmos", "/shop/nope", "/order/0123456789abcdef0123456789abcdef", "/order/not-a-token", "/legal/privacy", "/legal/imprint", diff --git a/shared/interfaces/Catcrafts.Shared-Content.cppm b/shared/interfaces/Catcrafts.Shared-Content.cppm index bed1785..4a9801c 100644 --- a/shared/interfaces/Catcrafts.Shared-Content.cppm +++ b/shared/interfaces/Catcrafts.Shared-Content.cppm @@ -154,6 +154,45 @@ export const std::vector& Demos() { return demos; } +// The about page: the deliberate weld between the person and the company. +// Search and AI answer engines had already connected Jorijn to catcrafts.net +// through the kernel patch trail, but with nothing first-party to quote they +// guessed — one AI overview attributed the domain to a different developer +// entirely, several redirected visitors to the Minecraft server on the +// singular domain. This page is the authoritative answer to "who is behind +// Catcrafts", for people and for machines. Same headed-sections shape as the +// legal pages, so it reuses their renderer's markup and CSS. +export const LegalPage& AboutPage() { + static const LegalPage page{ + .slug = "about", + .title = "About", + .updated = "2026-08-05", + .lede = "Catcrafts is a one-person company. This page is about the person, the mission, and the name.", + .sections = { + { "Who", + { + "Catcrafts is the registered one-person company (KVK 78437059) of Jorijn van der Graaf, a Dutch software engineer better known in the Linux mobile world as TheMightyCat.", + } }, + { "The mission", + { + "Accessible open-source software and hardware, for the benefit of everyone. Software you can read, hardware you can repair, and phones that answer to their owner rather than to an advertising department. A real alternative to big tech not a manifesto about one, but things you can download, flash and buy today.", + "Everything Catcrafts makes is published as open source. The shop is how the development gets funded: buying a phone here pays for the stack it runs.", + } }, + { "The work", + { + "imsd is the IMS/VoLTE implementation that makes phone calls work on mainline-Linux phones — written from scratch and tested on a live Dutch network. It is the reason a Fairphone 6 running postmarketOS can ring. Jorijn maintains the Fairphone 6 port of postmarketOS and upstreams the hardware support into the Linux kernel: sensor bindings, Qualcomm audio, the unglamorous patches that make a phone a phone.", + "The Crafter suite is the other half: a C++ build system with no DSL, a graphics engine that ray-traces natively on Vulkan and in the browser through WebGPU, a QUIC and HTTP/3 networking stack, and more. This website is built with all of it — the same C++ renders each page on the server and again in your browser as WebAssembly.", + "And ofcourse all the upstreaming work that comes along, Catcrafts will upstream everything it makes whenever possible.", + } }, + { "The name", + { + "Catcrafts is not the Minecraft server. That is catcraft.net — singular — a domain Jorijn registered in 2019 and let lapse, on which a rather successful Minecraft community then grew. He has been explaining the difference ever since, mostly to search engines. The cat in the logo speaks shell, not creeper.", + } }, + }, + }; + return page; +} + export const std::vector& LegalPages() { static const std::vector pages = { { @@ -210,7 +249,7 @@ export const std::vector& LegalPages() { } }, { "Business details", { - "Catcrafts, KVK 78437059, VAT NL003329281B38.", + "Catcrafts, owner Jorijn van der Graaf, KVK 78437059, VAT NL003329281B38.", } }, { "Security reports", { diff --git a/shared/interfaces/Catcrafts.Shared-Model.cppm b/shared/interfaces/Catcrafts.Shared-Model.cppm index a1a9dad..32b031a 100644 --- a/shared/interfaces/Catcrafts.Shared-Model.cppm +++ b/shared/interfaces/Catcrafts.Shared-Model.cppm @@ -108,6 +108,13 @@ export struct PageMeta { // prices are always in the markup, the HTML is identical for every // visitor, and nothing is detected server-side. bool geoPriceHint = false; + // schema.org JSON-LD for this page, already serialized. Emitted verbatim + // inside \n"; + } + return std::format( "\n" "\n\n" @@ -1258,7 +1425,7 @@ export std::string RenderDocument(const RenderedPage& page, page.meta.refreshSeconds) : std::string{}, page.meta.geoPriceHint ? kGeoPriceHintScript : std::string_view{}, - canonical.empty() ? std::string{} : canonical + "\n", + (canonical.empty() ? std::string{} : canonical + "\n") + headExtras, cssHref, bootScripts, nav.Str(), diff --git a/styles/styles.css b/styles/styles.css index d22ed44..22d016c 100644 --- a/styles/styles.css +++ b/styles/styles.css @@ -738,6 +738,13 @@ treatment it replaces, which read as the wrong category for the work. border-color: color-mix(in oklab, var(--danger) 40%, transparent); background: color-mix(in oklab, var(--danger) 10%, transparent); } + /* The one-line person-company link under the hero actions. Muted: it is a + fact, not a call to action. */ + .hero__byline { + color: var(--text-muted); + font-size: var(--step--1); + } + /* The coming-soon price list: the launch prices per colour, shown where the colour selector will be once orders open. */ .checkout__colors { diff --git a/tools/e2e.sh b/tools/e2e.sh index 05d8f45..92dcea3 100755 --- a/tools/e2e.sh +++ b/tools/e2e.sh @@ -111,7 +111,7 @@ header_has() { } echo "== status codes ==" -for p in / /shop /shop/fp6-pmos /projects /posts /demos /demos/raytracer \ +for p in / /about /shop /shop/fp6-pmos /projects /posts /demos /demos/raytracer \ /legal/privacy /legal/terms /legal/imprint /feed.xml /sitemap.xml /api/healthz; do status "$p" 200 done @@ -145,6 +145,11 @@ else bad "/demo Location header" "not /demos" fi +# Open shop or coming-soon? The pricing blob (data-cc) exists only on the real +# order form, so its presence is the probe. Used by the script-shape checks +# below and by the checkout/order/invoice gating further down. +if curl -s "$BASE/shop/fp6-pmos" | grep -q 'data-cc='; then SHOP_OPEN=1; else SHOP_OPEN=0; fi + echo "== the no-JavaScript guarantee ==" # The site must be complete without the wasm module. If these fail, the SSR # work has regressed and crawlers see an empty page again. @@ -160,16 +165,18 @@ for pg in /legal/privacy /legal/terms /legal/imprint /shop/fp6-pmos; do done # Shop pages are the one exception to script-free: they carry exactly ONE -# inline script — the timezone price hint. Pin its shape hard: inline only (no -# src=, so nothing external can ever ride in under this exception), no network -# APIs, and the page must remain complete without it — both prices in the -# markup regardless. +# EXECUTABLE inline script — the timezone price hint, whose tag is the bare +#