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
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
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 -->
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64">
<text x="50%" y="50%" font-size="48" text-anchor="middle" dominant-baseline="central">
<svg xmlns="http://www.w3.org/2000/svg" width="128" height="128">
<text x="50%" y="50%" font-size="110" text-anchor="middle" dominant-baseline="central">
🐱
</text>
</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
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.
*/
@ -10,22 +10,24 @@ export module Catcrafts:Blog_impl;
import :Blog;
import :Root;
import :Views;
import Crafter.Graphics;
import std;
using namespace Crafter::CppDOMBindings;
using namespace Crafter;
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() {
blogButtons.clear();
std::string html = "";
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;
if(previewContent.length() > 200) {
// Find the last space before 200 characters to avoid cutting words
std::size_t lastSpace = previewContent.find_last_of(' ', 200);
if(lastSpace != std::string::npos) {
previewContent = previewContent.substr(0, lastSpace) + "...";
@ -47,24 +49,21 @@ export namespace Catcrafts {
</div>
</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) {
// Add click listener to the entire post card
HtmlElementView& cardView = blogButtons.emplace_back(std::format("blog-post-{}", post.slug));
cardView.AddClickListener([slug = post.slug](Crafter::MouseEvent e) {
PushState("{}", "", std::format("/blog/{}", slug));
Dom::HtmlElementPtr& cardView = blogButtons.emplace_back(std::format("blog-post-{}", post.slug));
cardView.AddClickListener([slug = post.slug](Dom::MouseEvent) {
Router::PushState("{}", "", std::format("/blog/{}", slug));
RenderRoot(std::format("/blog/{}", slug));
});
}
}
void RenderBlogPost(const std::string_view slug) {
std::cout << "render "<< std::endl;
for(const BlogPost& post : posts) {
if(post.slug == slug) {
main.SetInnerHTML(std::format(R"(
MainContent().SetInnerHTML(std::format(R"(
<div class="blog-post-page">
<div class="post-header">
<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
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.
*/
@ -10,29 +10,27 @@ export module Catcrafts:Root_impl;
import :Root;
import :Views;
import :Blog;
import Crafter.Graphics;
import std;
using namespace Crafter;
namespace Catcrafts {
void RenderRoot(const std::string_view route) {
std::string currentRoute = std::string(route);
if(currentRoute == "/blog" || currentRoute == "/") {
RenderBlog();
} 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::cout << pos << std::endl;
std::cout << std::string::npos << std::endl;
std::cout << (pos != std::string::npos) << std::endl;
if(pos != std::string::npos) {
std::string postSlug = currentRoute.substr(pos + 1);
std::cout << postSlug << std::endl;
RenderBlogPost(postSlug);
} 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 {
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
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.
*/
import Catcrafts;
import Crafter.CppDOM;
import Crafter.Graphics;
import std;
using namespace Catcrafts;
using namespace Crafter;
using namespace Crafter::CppDOMBindings;
HtmlElementView* blogButton;
int main() {
AddPopStateListener([]() {
RenderRoot(GetPathNameString());
Device::Initialize();
// 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");
blogButton->AddClickListener([](Crafter::MouseEvent e) {
PushState("{}", "", "/blog");
static Dom::HtmlElementPtr blogButton("blog-nav-button");
blogButton.AddClickListener([](Dom::MouseEvent) {
Router::PushState("{}", "", "/blog");
RenderRoot("/blog");
});
RenderRoot(GetPathNameString());
}
RenderRoot(Router::GetPath());
window.StartUpdate();
window.StartSync();
return 0;
}

View file

@ -1,13 +1,13 @@
/*
catcrafts.net
Copyright (C) 2025 Catcrafts
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:Blog;
import Crafter.CppDOM;
import Crafter.Graphics;
import std;
using namespace Crafter;
@ -22,8 +22,8 @@ export namespace Catcrafts {
{
"In WASM, Exit doesn't mean done.",
"in-wasm-exit-doesnt-mean-done",
"2025-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>
"2026-11-14",
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>
@ -31,7 +31,7 @@ export namespace Catcrafts {
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>
@ -40,12 +40,12 @@ export namespace Catcrafts {
{
"Hello World!",
"hello-world",
"2025-11-12",
"2026-11-12",
R"(Welcome to catcrafts.net!<br><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>
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>
<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
Copyright (C) 2025 Catcrafts
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:Root;
import Crafter.CppDOM;
import Crafter.Graphics;
import std;
using namespace Crafter;

View file

@ -1,48 +1,27 @@
/*
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.
*/
export module Catcrafts:Views;
import Crafter.CppDOM;
import Crafter.Graphics;
import std;
using namespace Crafter;
export namespace Catcrafts {
HtmlElementPtr body("body", 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>
// Builds the persistent page chrome (header / <main id="main"> / footer)
// under <body>. Owns the root element for the page's lifetime so its
// children don't get yanked out of the DOM. Must be called from main()
// — the Crafter.Graphics Dom bridge is only safe to use after wasm
// instantiation hands control to user code.
void InitializePage();
<main id="main"></main>
<footer>
<div class="footer-content">
<div class="footer-links">
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");
// Per-render scratch ref to the <main> content container created by
// InitializePage(). Looking it up fresh on every call costs one
// document.getElementById; in return we don't have to thread a long-lived
// reference across module boundaries.
inline Dom::HtmlElementPtr MainContent() { return Dom::HtmlElementPtr("main"); }
}

View file

@ -1,6 +1,6 @@
/*
catcrafts.net
Copyright (C) 2025 Catcrafts
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.

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