Migrate to crafter-build project.cpp and restructure views

- Replace project.json + run.sh with a C++ project.cpp build descriptor
  (crafter-build), targeting wasm32-wasip1 with the Crafter.Graphics dep
- Add Catcrafts-Views implementation; drop Catcrafts-Component
- Add catcrafts-head.js (sets <title>/<link> tags at runtime)
- Update Blog/Root/main modules, README, and favicon

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jorijn van der Graaf 2026-07-18 23:27:41 +02:00
commit 13a12c2a2c
15 changed files with 227 additions and 126 deletions

View file

@ -1,6 +1,6 @@
# catcrafts.net # catcrafts.net
This is the source code for catcrafts.net, a website built entirely in C++ using the Crafter.CppDOM library. This is the source code for catcrafts.net, a website built entirely in C++ using the Crafter.Graphics library.
```bash ```bash
crafter-build build executable crafter-build build executable

24
catcrafts-head.js Normal file
View file

@ -0,0 +1,24 @@
// Head-element setup for catcrafts.net. The Crafter.Graphics Dom partition
// only exposes element creation under <body>, so anything that has to live in
// <head> (title, stylesheet link, favicon link) gets injected from this
// loader. EnableWasiBrowserRuntime auto-picks up every *.js in cfg.files and
// emits a <script type="module"> tag for it ahead of runtime.js, so this
// runs before the wasm module starts and styles are in place by first paint.
document.title = "Catcrafts.net";
const styles = document.createElement("link");
styles.rel = "stylesheet";
styles.href = "styles.css";
document.head.appendChild(styles);
const favicon = document.createElement("link");
favicon.rel = "icon";
favicon.type = "image/svg+xml";
favicon.href = "favicon.svg";
document.head.appendChild(favicon);
const viewport = document.createElement("meta");
viewport.name = "viewport";
viewport.content = "width=device-width, initial-scale=1.0";
document.head.appendChild(viewport);

View file

@ -1,6 +1,6 @@
<!-- save this as emoji-favicon.svg --> <!-- save this as emoji-favicon.svg -->
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64"> <svg xmlns="http://www.w3.org/2000/svg" width="128" height="128">
<text x="50%" y="50%" font-size="48" text-anchor="middle" dominant-baseline="central"> <text x="50%" y="50%" font-size="110" text-anchor="middle" dominant-baseline="central">
🐱 🐱
</text> </text>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 219 B

After

Width:  |  Height:  |  Size: 222 B

Before After
Before After

View file

@ -1,8 +1,8 @@
/* /*
catcrafts.net catcrafts.net
Copyright (C) 2025 Catcrafts Copyright (C) 2026 Catcrafts
The source code of this website is made available for viewing purposes only. 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. No permission is granted to copy, modify, distribute, or create derivative works.
*/ */
@ -10,22 +10,24 @@ export module Catcrafts:Blog_impl;
import :Blog; import :Blog;
import :Root; import :Root;
import :Views; import :Views;
import Crafter.Graphics;
import std; import std;
using namespace Crafter::CppDOMBindings; using namespace Crafter;
export namespace Catcrafts { export namespace Catcrafts {
std::vector<HtmlElementView> blogButtons; // Persistent storage for the per-post card click handlers. HtmlElementPtr
// unregisters its listeners on destruction, so the elements have to
// outlive Render(); clear() at the top of RenderBlog() detaches the
// previous render's handlers before we attach the new ones.
std::vector<Dom::HtmlElementPtr> blogButtons;
void RenderBlog() { void RenderBlog() {
blogButtons.clear(); blogButtons.clear();
std::string html = ""; std::string html = "";
for(const BlogPost& post : posts) { for(const BlogPost& post : posts) {
std::cout << "test" << std::endl;
// For preview, we'll limit the content to first 200 characters
std::string previewContent = post.content; std::string previewContent = post.content;
if(previewContent.length() > 200) { if(previewContent.length() > 200) {
// Find the last space before 200 characters to avoid cutting words
std::size_t lastSpace = previewContent.find_last_of(' ', 200); std::size_t lastSpace = previewContent.find_last_of(' ', 200);
if(lastSpace != std::string::npos) { if(lastSpace != std::string::npos) {
previewContent = previewContent.substr(0, lastSpace) + "..."; previewContent = previewContent.substr(0, lastSpace) + "...";
@ -47,24 +49,21 @@ export namespace Catcrafts {
</div> </div>
</div>)", post.slug, post.name, post.date, previewContent); </div>)", post.slug, post.name, post.date, previewContent);
} }
main.SetInnerHTML(std::format(R"(<div class="blog-posts">{}</div>)", html)); MainContent().SetInnerHTML(std::format(R"(<div class="blog-posts">{}</div>)", html));
for(const BlogPost& post : posts) { for(const BlogPost& post : posts) {
// Add click listener to the entire post card Dom::HtmlElementPtr& cardView = blogButtons.emplace_back(std::format("blog-post-{}", post.slug));
HtmlElementView& cardView = blogButtons.emplace_back(std::format("blog-post-{}", post.slug)); cardView.AddClickListener([slug = post.slug](Dom::MouseEvent) {
cardView.AddClickListener([slug = post.slug](Crafter::MouseEvent e) { Router::PushState("{}", "", std::format("/blog/{}", slug));
PushState("{}", "", std::format("/blog/{}", slug));
RenderRoot(std::format("/blog/{}", slug)); RenderRoot(std::format("/blog/{}", slug));
}); });
} }
} }
void RenderBlogPost(const std::string_view slug) { void RenderBlogPost(const std::string_view slug) {
std::cout << "render "<< std::endl;
for(const BlogPost& post : posts) { for(const BlogPost& post : posts) {
if(post.slug == slug) { if(post.slug == slug) {
main.SetInnerHTML(std::format(R"( MainContent().SetInnerHTML(std::format(R"(
<div class="blog-post-page"> <div class="blog-post-page">
<div class="post-header"> <div class="post-header">
<h1 class="post-title">{}</h1> <h1 class="post-title">{}</h1>
@ -78,6 +77,6 @@ export namespace Catcrafts {
} }
} }
main.SetInnerHTML("<h1>Post Not Found</h1><p>The requested blog post could not be found.</p>"); MainContent().SetInnerHTML("<h1>Post Not Found</h1><p>The requested blog post could not be found.</p>");
} }
} }

View file

@ -1,8 +1,8 @@
/* /*
catcrafts.net catcrafts.net
Copyright (C) 2025 Catcrafts Copyright (C) 2026 Catcrafts
The source code of this website is made available for viewing purposes only. 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. No permission is granted to copy, modify, distribute, or create derivative works.
*/ */
@ -10,29 +10,27 @@ export module Catcrafts:Root_impl;
import :Root; import :Root;
import :Views; import :Views;
import :Blog; import :Blog;
import Crafter.Graphics;
import std; import std;
using namespace Crafter;
namespace Catcrafts { namespace Catcrafts {
void RenderRoot(const std::string_view route) { void RenderRoot(const std::string_view route) {
std::string currentRoute = std::string(route); std::string currentRoute = std::string(route);
if(currentRoute == "/blog" || currentRoute == "/") { if(currentRoute == "/blog" || currentRoute == "/") {
RenderBlog(); RenderBlog();
} else if(currentRoute.rfind("/blog/", 0) == 0) { } else if(currentRoute.rfind("/blog/", 0) == 0) {
// Handle individual blog post routes like /blog/0, /blog/1, etc.
std::size_t pos = currentRoute.find_last_of('/'); std::size_t pos = currentRoute.find_last_of('/');
std::cout << pos << std::endl;
std::cout << std::string::npos << std::endl;
std::cout << (pos != std::string::npos) << std::endl;
if(pos != std::string::npos) { if(pos != std::string::npos) {
std::string postSlug = currentRoute.substr(pos + 1); std::string postSlug = currentRoute.substr(pos + 1);
std::cout << postSlug << std::endl;
RenderBlogPost(postSlug); RenderBlogPost(postSlug);
} else { } else {
main.SetInnerHTML("<h1>Post Not Found</h1><p>The requested blog post could not be found.</p>"); MainContent().SetInnerHTML("<h1>Post Not Found</h1><p>The requested blog post could not be found.</p>");
} }
} else { } else {
RenderBlog(); //default route RenderBlog();
} }
} }
} }

View file

@ -0,0 +1,51 @@
/*
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 std;
using namespace Crafter;
namespace Catcrafts {
namespace {
// Owning handle for the page-chrome root. `std::optional` lets us
// defer construction until InitializePage() runs (the CreateInBody
// call would otherwise fire at static-init time, before main()).
std::optional<Dom::HtmlElement> root;
}
void InitializePage() {
root.emplace(Dom::HtmlElement::CreateInBody("div", "catcrafts-root"));
root->SetInnerHTML(R"(
<header>
<div class="nav-container">
<a href="/" class="logo">🐱 Catcrafts</a>
<nav>
<ul>
<li><a id="blog-nav-button" style="cursor: pointer;" class="active">Blog</a></li>
<li><a href="https://forgejo.catcrafts.net/Catcrafts/">Forgejo</a></li>
</ul>
</nav>
</div>
</header>
<main id="main"></main>
<footer>
<div class="footer-content">
<div class="footer-links">
Powered by Crafter.Graphics, Running near native with WASM!
<a href="https://forgejo.catcrafts.net/Catcrafts/catcrafts.net">View source</a>
</div>
<p>&copy; 2026 Catcrafts®. All rights reserved. Crafter® and Catcrafts® are registered trademarks with the EUIPO</p>
</div>
</footer>)");
}
}

View file

@ -1,30 +1,40 @@
/* /*
catcrafts.net catcrafts.net
Copyright (C) 2025 Catcrafts Copyright (C) 2026 Catcrafts
The source code of this website is made available for viewing purposes only. 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. No permission is granted to copy, modify, distribute, or create derivative works.
*/ */
import Catcrafts; import Catcrafts;
import Crafter.CppDOM; import Crafter.Graphics;
import std; import std;
using namespace Catcrafts; using namespace Catcrafts;
using namespace Crafter; using namespace Crafter;
using namespace Crafter::CppDOMBindings;
HtmlElementView* blogButton;
int main() { int main() {
AddPopStateListener([]() { Device::Initialize();
RenderRoot(GetPathNameString()); // Window in CRAFTER_GRAPHICS_WINDOW_DOM mode is a page-level event hub
// plus the rAF driver; even without subscribing to its events we need
// it so StartSync() can keep the wasm module alive past main() (the
// bridge stashes a raw pointer, so the storage has to outlive main).
static Window window(0, 0, "Catcrafts");
InitializePage();
Router::AddPopStateListener([]{
RenderRoot(Router::GetPath());
}); });
blogButton = new HtmlElementView("blog-nav-button"); static Dom::HtmlElementPtr blogButton("blog-nav-button");
blogButton->AddClickListener([](Crafter::MouseEvent e) { blogButton.AddClickListener([](Dom::MouseEvent) {
PushState("{}", "", "/blog"); Router::PushState("{}", "", "/blog");
RenderRoot("/blog"); RenderRoot("/blog");
}); });
RenderRoot(GetPathNameString()); RenderRoot(Router::GetPath());
}
window.StartUpdate();
window.StartSync();
return 0;
}

View file

@ -1,13 +1,13 @@
/* /*
catcrafts.net catcrafts.net
Copyright (C) 2025 Catcrafts Copyright (C) 2026 Catcrafts
The source code of this website is made available for viewing purposes only. 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. No permission is granted to copy, modify, distribute, or create derivative works.
*/ */
export module Catcrafts:Blog; export module Catcrafts:Blog;
import Crafter.CppDOM; import Crafter.Graphics;
import std; import std;
using namespace Crafter; using namespace Crafter;
@ -22,8 +22,8 @@ export namespace Catcrafts {
{ {
"In WASM, Exit doesn't mean done.", "In WASM, Exit doesn't mean done.",
"in-wasm-exit-doesnt-mean-done", "in-wasm-exit-doesnt-mean-done",
"2025-11-14", "2026-11-14",
R"(So if anyone looked at the source code for this website pefore this post you would have seen that every is allocated with new, for example this very blog was defined as <code>std::vector<BlogPost> posts = new std::vector<BlogPost>{...</code><br><br> R"(So if anyone looked at the source code for this website pefore this post you would have seen that everything was allocated with new, for example this very blog was defined as <code>std::vector&lt;BlogPost&gt; posts = new std::vector&lt;BlogPost&gt;{...};</code><br><br>
Reason for this was that everything became corrupted when callbacking from JS, not knowing sure as to why this was the fastest solution, but after debugging the problem became clear.<br><br> Reason for this was that everything became corrupted when callbacking from JS, not knowing sure as to why this was the fastest solution, but after debugging the problem became clear.<br><br>
@ -31,7 +31,7 @@ export namespace Catcrafts {
This caused all the memory corruption errors since everything was destructed.<br><br> This caused all the memory corruption errors since everything was destructed.<br><br>
Luckily this is a very simple fix of adding <code>-fno-c++-static-destructors</code>, and things like this are is also the reason i use clang instead of gcc.<br><br> Luckily this is a very simple fix of adding <code>-fno-c++-static-destructors</code>, and the ease of things like this is also one of the reasons i use clang instead of gcc.<br><br>
So now all examples and this site have been updated to use normal variables again.<br><br> So now all examples and this site have been updated to use normal variables again.<br><br>
@ -40,12 +40,12 @@ export namespace Catcrafts {
{ {
"Hello World!", "Hello World!",
"hello-world", "hello-world",
"2025-11-12", "2026-11-12",
R"(Welcome to catcrafts.net!<br><br> R"(Welcome to catcrafts.net!<br><br>
Here we believe optimization is everything and C++ is a gift from god.<br> Here we believe optimization is everything and C++ is a gift from god.<br>
This blog will mostly be dedicated to random tidbits i come across while working on my Crafter series of libraries.<br><br> This blog will mostly be dedicated to random tidbits i come across while working on my Crafter series of libraries.<br><br>
Like this website which is fully written in C++ using the Crafter.CppDOM library.<br> Like this website which is fully written in C++ using the Crafter.Graphics library.<br>
And source available too!<br> And source available too!<br>
<a href="https://forgejo.catcrafts.net/Catcrafts/catcrafts.net">https://forgejo.catcrafts.net/Catcrafts/catcrafts.net<a>)" <a href="https://forgejo.catcrafts.net/Catcrafts/catcrafts.net">https://forgejo.catcrafts.net/Catcrafts/catcrafts.net<a>)"
} }

View file

@ -1,16 +0,0 @@
/*
catcrafts.net
Copyright (C) 2025 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:Component;
using namespace Crafter;
export namespace Catcrafts {
class Component {
std::string render();
};
}

View file

@ -1,13 +1,13 @@
/* /*
catcrafts.net catcrafts.net
Copyright (C) 2025 Catcrafts Copyright (C) 2026 Catcrafts
The source code of this website is made available for viewing purposes only. 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. No permission is granted to copy, modify, distribute, or create derivative works.
*/ */
export module Catcrafts:Root; export module Catcrafts:Root;
import Crafter.CppDOM; import Crafter.Graphics;
import std; import std;
using namespace Crafter; using namespace Crafter;

View file

@ -1,48 +1,27 @@
/* /*
catcrafts.net catcrafts.net
Copyright (C) 2025 Catcrafts Copyright (C) 2026 Catcrafts
The source code of this website is made available for viewing purposes only. 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. No permission is granted to copy, modify, distribute, or create derivative works.
*/ */
export module Catcrafts:Views; export module Catcrafts:Views;
import Crafter.CppDOM; import Crafter.Graphics;
import std;
using namespace Crafter; using namespace Crafter;
export namespace Catcrafts { export namespace Catcrafts {
HtmlElementPtr body("body", R"( // Builds the persistent page chrome (header / <main id="main"> / footer)
<header> // under <body>. Owns the root element for the page's lifetime so its
<div class="nav-container"> // children don't get yanked out of the DOM. Must be called from main()
<a href="/" class="logo">🐱 Catcrafts</a> // — the Crafter.Graphics Dom bridge is only safe to use after wasm
<nav> // instantiation hands control to user code.
<ul> void InitializePage();
<li><a id="blog-nav-button" style="cursor: pointer;" class="active">Blog</a></li>
<li><a href="https://forgejo.catcrafts.net/Catcrafts/">Forgejo</a></li>
</ul>
</nav>
</div>
</header>
<main id="main"></main> // Per-render scratch ref to the <main> content container created by
// InitializePage(). Looking it up fresh on every call costs one
<footer> // document.getElementById; in return we don't have to thread a long-lived
<div class="footer-content"> // reference across module boundaries.
<div class="footer-links"> inline Dom::HtmlElementPtr MainContent() { return Dom::HtmlElementPtr("main"); }
Powered by Crafter.CppDOM, Running near native with WASM!
<a href="https://forgejo.catcrafts.net/Catcrafts/catcrafts.net">View source</a>
</div>
<p>&copy; 2025 Catcrafts®. All rights reserved. Crafter® and Catcrafts® are registered trademarks with the EUIPO</p>
</div>
</footer>)");
HtmlElementPtr head("head", R"(
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Catcrafts.net</title>
<link rel="stylesheet" href="/styles.css">
<link rel="icon" href="/favicon.svg" type="image/svg+xml">
)");
HtmlElementPtr main("main");
} }

View file

@ -1,6 +1,6 @@
/* /*
catcrafts.net catcrafts.net
Copyright (C) 2025 Catcrafts Copyright (C) 2026 Catcrafts
The source code of this website is made available for viewing purposes only. 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. No permission is granted to copy, modify, distribute, or create derivative works.

75
project.cpp Normal file
View file

@ -0,0 +1,75 @@
/*
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.
*/
import std;
import Crafter.Build;
namespace fs = std::filesystem;
using namespace Crafter;
extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> args) {
std::vector<std::string> depArgs(args.begin(), args.end());
depArgs.push_back("--target=wasm32-wasip1");
// --local resolves the Crafter.Graphics dep from a sibling working tree
// instead of fetching it from forgejo. Mirrors Crafter.Graphics's own
// project.cpp convention so edits across the two repos pick up without
// commit-and-pull.
bool useLocal = false;
for (std::string_view a : args) {
if (a == "--local") { useLocal = true; break; }
}
if (useLocal && std::find(depArgs.begin(), depArgs.end(), std::string("--local")) == depArgs.end()) {
depArgs.push_back("--local");
}
Configuration* graphics = useLocal
? LocalProject({
.projectFile = "../Crafter/Crafter.Graphics/project.cpp",
.args = depArgs,
})
: GitProject({
.source = { .url = "https://forgejo.catcrafts.net/Catcrafts/Crafter.Graphics.git" },
.args = depArgs,
});
Configuration cfg;
cfg.path = "./";
cfg.name = "Catcrafts.Net";
cfg.outputName = "catcrafts";
cfg.type = ConfigurationType::Executable;
cfg.target = "wasm32-wasip1";
ApplyStandardArgs(cfg, args);
cfg.dependencies = { graphics };
std::array<fs::path, 4> ifaces = {
"interfaces/Catcrafts",
"interfaces/Catcrafts-Views",
"interfaces/Catcrafts-Blog",
"interfaces/Catcrafts-Root",
};
std::array<fs::path, 4> impls = {
"implementations/main",
"implementations/Catcrafts-Blog",
"implementations/Catcrafts-Root",
"implementations/Catcrafts-Views",
};
cfg.GetInterfacesAndImplementations(ifaces, impls);
cfg.files.emplace_back(fs::path("styles/styles.css"));
cfg.files.emplace_back(fs::path("robots.txt"));
cfg.files.emplace_back(fs::path("sitemap.xml"));
cfg.files.emplace_back(fs::path("favicon.svg"));
// Loaded as a <script type="module"> before runtime.js by
// EnableWasiBrowserRuntime — sets <title>/<link> tags since the
// Dom partition has no head-element access.
cfg.files.emplace_back(fs::path("catcrafts-head.js"));
EnableWasiBrowserRuntime(cfg);
return cfg;
}

View file

@ -1,18 +0,0 @@
{
"name": "main",
"configurations": [
{
"name": "executable",
"interfaces": ["interfaces/Catcrafts", "interfaces/Catcrafts-Views", "interfaces/Catcrafts-Blog", "interfaces/Catcrafts-Root"],
"implementations": ["implementations/main", "implementations/Catcrafts-Blog", "implementations/Catcrafts-Root"],
"target": "wasm32-wasi",
"additional_files": ["styles/styles.css", "robots.txt", "sitemap.xml", "favicon.svg"],
"dependencies": [
{
"path":"https://forgejo.catcrafts.net/Catcrafts/Crafter.CppDOM.git",
"configuration":"lib"
}
]
}
]
}

1
run.sh
View file

@ -1 +0,0 @@
caddy file-server --listen :8080 --root bin/executable