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,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;
}