Skip to main content

Project Structure

Wunderframe applications follow a convention-based structure that organizes code, templates, and assets in a predictable way. This structure enables zero-configuration builds and clear separation of concerns.

Directory Overview

my-wunderframe-app/
├── app.lua # 🎯 Main application entry point
├── app.js # 📱 Frontend JavaScript entry
├── app.css # 🎨 Global styles and TailwindCSS
├── wunder.toml # ⚙️ Project configuration
├── package.json # 📦 Frontend dependencies
├── lib/ # 🧩 Application logic (Lua/LUAT)
│ ├── components/ # 🔄 Reusable LUAT components
│ │ ├── ui/ # 🎛️ UI components (buttons, cards, etc.)
│ │ ├── container/ # 📦 Layout containers (page, section)
│ │ ├── content/ # 📝 Content components (hero, gallery)
│ │ └── pages/ # 📄 Page-level components
│ ├── pages/ # 📃 Direct page templates
│ └── utils/ # 🛠️ Utility modules
│ ├── helper.lua # 🔧 Helper functions
│ ├── content_types.lua # 📋 Content type registry
│ └── registry.lua # 📚 Module registry system
└── src/ # 🌐 Frontend source files
├── directives/ # 🎪 Alpine.js directives
├── components/ # 🧱 Web components
└── helper.js # 🔧 Development utilities

Core Application Files

app.lua - Application Entry Point

The main Lua script that handles routing and rendering logic:

-- Main render function called for each request
local function render(pageContext, runtime)
local helpers = createContextHelpers(runtime)
helpers.createContext()

local currentNode = pageContext.current_node

-- Special handling for root path
if pageContext.full_path == "/" then
currentNode = get_node("stories/" .. pageContext.story_node.path .. "/site/homepage")
end

-- Set up template context
helpers.setContext("currentNode", currentNode)
helpers.setContext("pageContext", pageContext)

-- Route to appropriate component
local renderComponent = getMatchingContentType(currentNode.content_type)
return renderComponent(currentNode, runtime)
end

return { render = render }

app.js - Frontend Entry Point

Configures the client-side application with modern libraries:

import "@phosphor-icons/web/light";  // Icons
import Alpine from 'alpinejs'; // Reactive framework
import htmx from "htmx.org"; // Server interactions
import loopVideo from "./src/directives/loopvideo.js";

// Register custom Alpine directives
loopVideo(Alpine);
Alpine.start();

// Handle dynamic updates and development features
document.addEventListener('htmx:load', () => {
// Live editing integration
if(window.raisin) {
const editor = window.raisin.editor;
editor.onMessage("UPDATE", (newData) => {
htmx.ajax('POST', location.href, {
target: '#app',
swap: 'outerHTML',
values: { properties: JSON.stringify(newData.properties) }
});
});
}
initializeDev();
});

app.css - Global Styles

TailwindCSS configuration with custom theme and animations:

@import url('https://fonts.googleapis.com/css2?family=Inter...');
@import "tailwindcss";
@plugin "@tailwindcss/typography";

@custom-variant dark (&:where(.dark, .dark *));

@theme {
--font-sans: "Inter", "sans-serif";
--font-playful: "Sour Gummy", sans-serif;
}

/* Custom animations */
@keyframes fade-in-up {
0% { opacity: 0; transform: translateY(32px);}
100% { opacity: 1; transform: translateY(0);}
}
.animate-fade-in-up {
animation: fade-in-up 1s cubic-bezier(.23,1.01,.32,1) both;
}

Library Structure (lib/)

Components Directory

Organized by purpose and complexity:

lib/components/ui/

Basic UI building blocks:

ui/
├── AppBar.luat # Navigation header
├── Footer.luat # Site footer
├── Button.luat # Reusable button component
└── README.md # UI components documentation

lib/components/container/

Layout and structural components:

container/
├── Page.luat # Main page wrapper
└── Section.luat # Content sections

lib/components/content/

Content-specific components:

content/
├── Hero.luat # Hero/banner sections
├── Gallery.luat # Image galleries
└── Card.luat # Content cards

lib/components/pages/

Page-level components that combine other components:

pages/
├── Homepage.luat # Homepage template
└── StandardPage.luat # Standard content page

Utility Modules (lib/utils/)

helper.lua

Asset resolution and utility functions:

-- Resolves assets to CDN URLs with transformations
function resolveAsset(asset, opts)
if not asset or not asset.url then
return nil
end

local url = asset.url
local mime = asset.mime_type or ""
local cdn = "https://images.lowcodecms.com/cdn-cgi/image/"

if mime:find("^image/") then
local width = opts and opts.width or 1000
local quality = opts and opts.quality or 75
return cdn .. "width=" .. width .. ",quality=" .. quality .. "/" .. url
else
return "https://images.lowcodecms.com/" .. url
end
end

content_types.lua

Content type to component mapping:

local createModuleRegistry = require("lib/utils/registry")
local contentTypes = createModuleRegistry()

local HomePage = require("lib/components/pages/Homepage")
local StandardPage = require("lib/components/pages/StandardPage")

contentTypes.registerModule(HomePage)
contentTypes.registerModule(StandardPage)

return contentTypes.getModule

registry.lua

Module registration system:

local function createModuleRegistry()
local widgets = {}
local mapping = {}

local function registerModule(module)
table.insert(widgets, module)
mapping[module.type] = module
end

local function getModule(name)
local module = mapping[name]
return module and module.render or nil
end

return {
registerModule = registerModule,
getModule = getModule,
widgets = widgets,
mapping = mapping
}
end

Frontend Source (src/)

Alpine.js Directives (src/directives/)

Custom Alpine.js functionality:

// src/directives/loopvideo.js
export default function loopVideo(Alpine) {
Alpine.directive('loopvideo', (el, { expression }, { cleanup }) => {
const video = el;
if (video.tagName !== 'VIDEO') return;

video.muted = true;
video.loop = true;
video.playsInline = true;

const playVideo = async () => {
try {
await video.play();
} catch (error) {
console.log('Autoplay prevented:', error);
}
};

video.addEventListener('loadeddata', playVideo);
cleanup(() => video.removeEventListener('loadeddata', playVideo));
});
}

Development Helpers (src/helper.js)

Development mode utilities with WebSocket integration:

export const initializeDev = function() {
const body = document.querySelector('body');
if (body?.getAttribute('data-wunder-dev') === 'true') {
// WebSocket connection for live reloading
const ws = new WebSocket(`wss://${window.location.hostname}:${window.location.port}/ws`);

ws.onopen = () => {
ws.send(JSON.stringify({
action: 'subscribe_node',
data: { pattern: '**' }
}));
};

ws.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.event_type === 'NodeUpdated') {
// Handle file changes and reload appropriately
if (data.path?.endsWith('.js') || data.path?.endsWith('.lua')) {
window.location.reload();
} else if (data.path?.endsWith('.css')) {
// Hot reload CSS only
const styleTag = document.head.querySelector('link[data-wunder-style]');
if (styleTag) {
const url = new URL(styleTag.getAttribute("href"), window.location.origin);
url.searchParams.set('t', Date.now().toString());
styleTag.setAttribute('href', url.toString());
}
}
}
};
}
};

Configuration Files

wunder.toml

Project settings and enabled features:

name = "my-app"
target = "/"

[sync]
status = "unsynced"
workspaces = []

[frontend_tools]
enabled = ["tailwindcss", "typescript"]

package.json

Frontend dependencies:

{
"name": "my-app",
"type": "commonjs",
"main": "app.js",
"dependencies": {
"alpinejs": "^3.14.9",
"htmx.org": "^2.0.6",
"@phosphor-icons/web": "^2.1.2"
},
"devDependencies": {
"@tailwindcss/typography": "^0.5.16"
}
}

Best Practices

1. Component Organization

  • UI components: Generic, reusable elements
  • Container components: Layout and structure
  • Content components: Domain-specific elements
  • Page components: Full page compositions

2. Naming Conventions

  • Components: PascalCase (e.g., UserCard.luat)
  • Utilities: camelCase (e.g., helper.lua)
  • Directories: lowercase (e.g., components/, utils/)

3. Import Paths

Use consistent paths relative to project root:

local Helper = require("lib/utils/helper")
local Card = require("lib/components/ui/Card")

4. File Organization

Keep related files close together and maintain clear boundaries between server-side (Lua) and client-side (JavaScript) code.