/* 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. */ // Post pages: the routing, the loader's guard on what becomes a URL, and the // schema.org joins that keep every post attributed to the one Organization and // the one Person the rest of the site describes. 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); } } // namespace int main() { // ── routing ─────────────────────────────────────────────────────── Check(ParseRoute("/posts").kind == RouteKind::Posts, "route: /posts is the list"); Check(ParseRoute("/posts/hello-world").kind == RouteKind::Post, "route: /posts/"); Check(ParseRoute("/posts/hello-world").slug == "hello-world", "route: post slug captured"); Check(ParseRoute("/posts/hello-world/").kind == RouteKind::Post, "route: trailing slash normalised"); Check(ParseRoute("/posts/Hello").kind == RouteKind::NotFound, "route: uppercase slug is not a post URL"); Check(ParseRoute("/posts/../etc").kind == RouteKind::NotFound, "route: traversal never reaches a lookup"); Check(NavKindFor(RouteKind::Post) == RouteKind::Posts, "route: a post page highlights the Posts nav entry"); Check(NavKindFor(RouteKind::LegacyBlog) == RouteKind::Posts, "route: the retired /blog URL highlights it too"); Check(NavKindFor(RouteKind::Shop) == RouteKind::Shop, "route: a nav route is its own entry"); // ── the loader ──────────────────────────────────────────────────── { const auto posts = LoadPosts(R"([ {"title":"Good","slug":"good-post","permalink":"https://i.example/post/1", "body":"Hello.","published":"2026-01-01T00:00:00Z", "body_media":[{"src":"/media/a.webp","kind":"image","w":10,"h":20}]}, {"title":"Bad slug","slug":"NOT A SLUG","permalink":"https://i.example/post/2", "body":"Hello."}, {"title":"No body","slug":"no-body","permalink":"https://i.example/post/3"} ])"); Check(posts.size() == 3, "posts: all three load"); if (posts.size() == 3) { Check(posts[0].HasPage() && posts[0].slug == "good-post", "posts: valid slug kept"); Check(posts[0].body == "Hello.", "posts: body loaded"); Check(posts[0].bodyMedia.size() == 1 && posts[0].bodyMedia[0].width == 10, "posts: body media loaded with dimensions"); // A slug that could never match a route would render a "read more" // link to a 404 this site points at itself. Check(posts[1].slug.empty() && !posts[1].HasPage(), "posts: malformed slug is dropped, costing the page"); // A title and a link out is not a page worth minting a URL for. Check(!posts[2].HasPage(), "posts: no body means no page"); } Views::SiteContent content; content.posts = posts; Check(content.FindPost("good-post") != nullptr, "posts: found by slug"); Check(content.FindPost("no-body") == nullptr, "posts: a pageless post is not findable"); Check(content.FindPost("nope") == nullptr, "posts: unknown slug is not found"); // Which means the route 404s rather than rendering an empty article. Check(Views::RenderRoute(ParseRoute("/posts/no-body"), content).status == 404, "posts: a pageless slug is a real 404"); Check(Views::RenderRoute(ParseRoute("/posts/good-post"), content).status == 200, "posts: a real post renders"); } // ── the page ────────────────────────────────────────────────────── { Post p; p.title = "Working GPS!"; p.slug = "working-gps"; p.permalink = "https://lemmy.example/post/42"; p.community = "linuxphones@lemmy.example"; p.published = "2026-06-26T23:16:25Z"; p.excerpt = "A short summary."; p.body = "# How\n\nIt works."; PostMedia m; m.src = "/media/shot.webp"; m.kind = "image"; p.media.push_back(m); const auto page = Views::RenderPost(p); Check(page.meta.canonical == "/posts/working-gps", "post page: canonical is this site, not the instance"); Check(page.meta.ogType == "article", "post page: og:type is article"); Check(page.meta.ogImage == "/media/shot.webp", "post page: og:image from the post media"); Check(page.meta.description == p.excerpt, "post page: description is the excerpt"); Check(page.main.View().find("

How

") != std::string_view::npos, "post page: the body is rendered, not escaped away"); // The whole reason the body is hosted: the thread is still one click // away, and the reader is told where the discussion is. Check(page.main.View().find(p.permalink) != std::string_view::npos, "post page: still links the thread"); // The identity graph, same joins every other page makes. A typo'd @id // still renders and still validates — it just quietly splits this post // away from the entity the rest of the site describes. auto ld = Json::Parse(page.meta.jsonLd); Check(ld && ld->IsObject() && ld->Str("@type") == "BlogPosting", "post schema: parses as a BlogPosting"); if (ld && ld->IsObject()) { Check(ld->Str("url") == "https://catcrafts.net/posts/working-gps", "post schema: url is the on-site page"); Check(ld->Str("discussionUrl") == p.permalink, "post schema: the thread is the discussion, not the content"); Check(ld->Str("datePublished") == p.published, "post schema: publication date"); const Json::Value* author = ld->Find("author"); const Json::Value* publisher = ld->Find("publisher"); Check(author && author->Str("@id") == "https://catcrafts.net/about#person", "post schema: authored by the Person node on /about"); Check(publisher && publisher->Str("@id") == "https://catcrafts.net/#organization", "post schema: published by the Organization node"); } // A post with a page is advertised as one from the list and from home; // one without keeps pointing at the thread, because there is nothing // here to send the reader to. const std::array one{ p }; const auto list = Views::RenderPosts(one); Check(list.main.View().find(R"(href="/posts/working-gps")") != std::string_view::npos, "posts list: links the on-site page"); Check(list.main.View().find("Read the full post") != std::string_view::npos, "posts list: offers the full post"); // Inside the excerpt paragraph, trailing the text — not a row of its // own below the media, where it was the same offer made a screen // further down. Check(list.main.View().find( R"(A short summary. )" R"(Read the full post

)") != std::string_view::npos, "posts list: read-more trails the excerpt", list.main.View()); // With no excerpt there is no sentence to continue, so it falls back to // a row rather than vanishing with the paragraph that would have held it. Post unexcerpted = p; unexcerpted.excerpt.clear(); const std::array bare{ unexcerpted }; const auto listBare = Views::RenderPosts(bare); Check(listBare.main.View().find("Read the full post") != std::string_view::npos, "posts list: an excerptless post still offers the full post"); Post bodyless = p; bodyless.body.clear(); const std::array none{ bodyless }; const auto listNone = Views::RenderPosts(none); Check(listNone.main.View().find("Read the full post") == std::string_view::npos, "posts list: no read-more without a page to read"); Check(listNone.main.View().find(p.permalink) != std::string_view::npos, "posts list: a pageless post still links its thread"); } if (failures != 0) { std::println(std::cerr, "{} check(s) failed", failures); return 1; } return 0; }