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.