/* 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. */ export module Catcrafts:Views_impl; import :Views; import Crafter.Graphics; import Catcrafts.Shared; import std; using namespace Crafter; namespace Catcrafts { namespace { // Owning handle for the page chrome, ONLY when this module created it. // std::optional defers construction until InitializePage() runs — a // CreateInBody at static-init time would fire before main(). // // Left empty when the server already rendered the chrome: the handle is // owning, and destroying it would remove the server's markup from the // document. std::optional root; // True when the document arrived server-rendered. bool adoptedSsr = false; Views::SiteContent content; bool contentLoaded = false; Window* activeWindow = nullptr; } void SetActiveWindow(Window* window) { activeWindow = window; } void SetDocumentTitle(std::string_view title) { // Only meaningful on the client-rendered path. On an SSR'd page the // server already set a correct , and overwriting it with the // same string is pointless work. if (activeWindow && !adoptedSsr) activeWindow->SetTitle(title); } bool AdoptedSsr() { return adoptedSsr; } std::string ReadBundleFile(std::string_view name) { // cfg.files flattens to the bundle root (Crafter.Build copies by // filename, not by path), so "content/posts.json" is readable as // "posts.json" here. runtime.js has already fetched every files.json // entry into the VFS before _start, so this cannot block on the // network and cannot fail for a file that is in the manifest. std::ifstream in(std::string(name), std::ios::binary); if (!in) return {}; std::ostringstream buf; buf << in.rdbuf(); return buf.str(); } const Views::SiteContent& SiteData() { if (!contentLoaded) { contentLoaded = true; // Authored content is compiled into the module; only the // pipeline-generated files ride the VFS. content.projects = Content::Projects(); content.products = Content::Products(); content.legal = Content::LegalPages(); content.demos = Content::Demos(); content.posts = LoadPosts(ReadBundleFile("posts.json")); content.rates = LoadRates(ReadBundleFile("rates.json")); } return content; } void InitializePage() { // Two ways a page can arrive, and they need opposite handling. // // 1. Server-rendered (the normal case): #catcrafts-root already exists, // with the chrome and the route's content in it. Building a second // root here would duplicate the header and footer on screen, and // re-rendering <main> would replace correct markup with identical // markup — a wasted round of DOM work and a visible flash for // nothing. So: adopt, and touch nothing. // // 2. The static fallback shell: Caddy serves the wasm bundle's // index.html when the backend is down (see deploy/Caddyfile.example), // and that document has an empty <body>. Here the app IS the whole // site and has to build the chrome and render the route itself. Dom::HtmlElementPtr existing("catcrafts-root"); if (existing.ptr != 0) { adoptedSsr = true; return; } root.emplace(Dom::HtmlElement::CreateInBody("div", "catcrafts-root")); root->SetInnerHTML(std::format( R"(<header id="cc-header">{}</header>)" R"(<main id="main"></main>)" R"(<footer id="cc-footer">{}</footer>)", Views::RenderNav(RouteKind::Home).Str(), Views::RenderFooter().Str())); } }