readysite / docs / website.md
15.2 KB
website.md

Website: AI-Native CMS

An AI-native content management system where AI assistance is a first-class feature. Status: Work in Progress

Target Audience

ReadySite targets technology enthusiasts who want to build with AI - a WordPress-inspired platform for the AI era. While the long-term goal is accessibility for non-technical users, the current focus is on users who are comfortable with:

  • Basic HTML/CSS knowledge (for page content)
  • API key configuration (for AI providers)
  • Technical concepts like "collections" and "endpoints"

This is explicitly not targeting drag-and-drop users (like Squarespace/Wix) today, though a visual editor is on the roadmap.

Overview

The website is a CMS built on ReadySite's framework layers. It demonstrates how to build a full application with:

  • AI chat with tool execution
  • Hierarchical pages with templates
  • Dynamic collections with typed schemas
  • Mutation tracking for undo/redo

Architecture

website/
├── main.go              # Bootstrap, seed data, register controllers
├── Dockerfile           # Multi-stage build
├── controllers/         # 9 controllers (2,330 LOC)
│   ├── auth.go          # JWT authentication
│   ├── setup.go         # First-run wizard
│   ├── dashboard.go     # Admin home
│   ├── pages.go         # Page CRUD
│   ├── collections.go   # Collection + document CRUD
│   ├── endpoints.go     # Serverless endpoints (WIP)
│   ├── chat.go          # AI conversation + streaming
│   ├── workspace.go     # Main admin workspace
│   └── site.go          # Public page rendering
├── models/              # Data entities
│   ├── db.go            # Database setup
│   ├── user.go          # Users with roles
│   ├── page.go          # Hierarchical pages
│   ├── collection.go    # Collections + documents
│   ├── conversation.go  # Chat + messages + mutations
│   ├── endpoint.go      # Serverless endpoints
│   └── settings.go      # Key-value settings
├── internal/
│   ├── access/          # Role constants, permission checking, ACL
│   ├── assist/          # AI conversation helpers
│   ├── render/          # Template functions for pages
│   ├── runtime/         # Endpoint execution (Lua sandbox)
│   ├── schema/          # Collection field types + validation
│   └── tools/           # AI tool definitions + executor
└── views/               # 28 templates (2,627 LOC)

Key Features

1. AI Chat with Tool Execution

The AI can create, update, and delete content through defined tools:

// 15 tools defined in internal/tools/
tools.Register("create_page", ...)
tools.Register("update_page", ...)
tools.Register("delete_page", ...)
tools.Register("create_collection", ...)
tools.Register("create_document", ...)
// ... etc

Chat streams responses via SSE, executing tools in real-time:

<div hx-ext="sse" sse-connect="/admin/chat/{{.ID}}/stream" sse-swap="content">
    <!-- AI responses stream here -->
</div>

Creating Custom AI Tools

To add a new AI tool, follow these three steps:

Step 1: Define the tool schema (in internal/tools/)

Create a file for your tool or add to an existing file:

// internal/tools/notifications.go
package tools

import "github.com/readysite/readysite/pkg/assistant"

var sendNotificationTool = assistant.NewTool("send_notification", "Send a notification to users").
    String("title", "Notification title", true).     // required
    String("message", "Notification body", true).    // required
    String("user_id", "Target user ID (optional, all users if empty)", false).
    Build()

// Arguments struct for parsing
type SendNotificationArgs struct {
    Title   string `json:"title"`
    Message string `json:"message"`
    UserID  string `json:"user_id"`
}

Available parameter types:

  • String(name, description, required) - Text parameter
  • Int(name, description, required) - Integer parameter
  • Number(name, description, required) - Float parameter
  • Bool(name, description, required) - Boolean parameter
  • Object(name, description, required) - JSON object parameter
  • Array(name, description, required) - JSON array parameter

Step 2: Register the tool (in internal/tools/tools.go)

Add your tool to the All() function:

func All() []assistant.Tool {
    return []assistant.Tool{
        // ... existing tools ...
        sendNotificationTool,  // Add your tool
    }
}

Step 3: Implement the executor (in internal/tools/executor.go)

Add a case to the Execute switch and implement the handler:

func (e *Executor) Execute(call assistant.ToolCall) (string, error) {
    switch call.Name {
    // ... existing cases ...
    case "send_notification":
        return e.sendNotification(call)
    }
}

func (e *Executor) sendNotification(call assistant.ToolCall) (string, error) {
    var args SendNotificationArgs
    if err := call.ParseArguments(&args); err != nil {
        return "", fmt.Errorf("invalid arguments: %w", err)
    }

    // Implement your logic here
    count, err := notifications.Send(args.Title, args.Message, args.UserID)
    if err != nil {
        return "", fmt.Errorf("failed to send notification: %w", err)
    }

    // Record mutation for undo support (optional)
    e.recordMutation(assist.ActionCreate, "notification", notificationID, nil, notification)

    // Return JSON result
    return jsonResult(map[string]any{
        "sent_to": count,
        "message": fmt.Sprintf("Sent notification to %d users", count),
    })
}

Helper functions:

  • call.ParseArguments(&args) - Parse JSON arguments into struct
  • e.recordMutation(action, entityType, entityID, before, after) - Record for undo
  • jsonResult(data) - Format success response as JSON

2. Mutation Tracking

Every AI action is recorded for undo/redo:

type Mutation struct {
    MessageID   string  // Which message triggered this
    Action      string  // create, update, delete
    EntityType  string  // page, collection, document
    EntityID    string
    BeforeState string  // JSON snapshot before
    AfterState  string  // JSON snapshot after
    Undone      bool
}

3. Dynamic Collections

Collections have typed schemas:

schema := []schema.Field{
    {Name: "title", Type: schema.Text, Required: true},
    {Name: "publishedAt", Type: schema.Date},
    {Name: "featured", Type: schema.Bool},
    {Name: "tags", Type: schema.JSON},
}

Field types: Text, Number, Bool, Date, Email, URL, Select, Relation, File, JSON

4. Serverless Endpoints (Lua)

Endpoints execute Lua code in a sandboxed environment:

-- Access request context
local method = request.method
local path = request.path
local name = request.query.name
local body = request.body

-- Return response
return {
    status = 200,
    headers = { ["Content-Type"] = "application/json" },
    body = json_encode({ message = "Hello, " .. name })
}

Available globals:

Global Purpose
request.method HTTP method (GET, POST, etc.)
request.path Request path
request.query Query parameters table
request.headers Request headers table
request.body Request body string
json_encode(value) Encode Lua value to JSON
json_decode(string) Decode JSON to Lua table

Constraints:

  • 5 second execution timeout
  • Sandboxed: only base, string, table, math libraries
  • No file system or network access

5. Schema Validation

Documents are validated against their collection's schema:

// Schema definition
schema := []schema.Field{
    {Name: "title", Type: schema.Text, Required: true, Options: map[string]any{"maxLength": 100}},
    {Name: "price", Type: schema.Number, Options: map[string]any{"min": 0}},
    {Name: "email", Type: schema.Email},
    {Name: "status", Type: schema.Select, Options: map[string]any{"values": []string{"draft", "published"}}},
}

// Validation
err := schema.ValidateDocument(collection, data)
if verrs, ok := err.(schema.ValidationErrors); ok {
    for _, e := range verrs {
        fmt.Printf("%s: %s\n", e.Field, e.Message)
    }
}

6. Template Functions for Pages

Pages can embed dynamic content:

<!-- In a page's HTML -->
<h1>{{site_name}}</h1>

{{range $post := documents "blog"}}
    <article>
        <h2>{{$post.GetString "title"}}</h2>
        <p>{{$post.GetString "excerpt"}}</p>
    </article>
{{else}}
    <p>No posts yet.</p>
{{end}}

Available functions:

Function Purpose
documents "collection" Get all documents in collection
document "collection" "id" Get single document
collection "name" Get collection metadata
page "id" Get page by ID
pages Get all pages
published_pages Get published pages only
site_name Site name from settings
site_description Site description from settings

Data Models

User

type User struct {
    database.Model
    Email        string  // Unique
    PasswordHash string
    Name         string
    AvatarURL    string
    Role         string  // admin, user, viewer
    Verified     bool
}

Page

type Page struct {
    database.Model
    ParentID    string  // For hierarchy
    Title       string
    Description string
    HTML        string  // Full page content
    Position    int     // Sort order
    Published   bool
}

// Methods
page.Path()      // Returns full URL path
page.Parent()    // Returns parent page
page.Children()  // Returns child pages
page.Siblings()  // Returns pages at same level

Collection + Document

type Collection struct {
    database.Model
    Name        string
    Description string
    Schema      string  // JSON field definitions
    System      bool    // Protected from deletion
}

type Document struct {
    database.Model
    CollectionID string
    Data         string  // JSON data
}

// Document methods
doc.GetString("field")
doc.GetInt("field")
doc.GetBool("field")
doc.GetTime("field")
doc.Set("field", value)

Conversation + Message

type Conversation struct {
    database.Model
    UserID  string
    Title   string
    Context string  // JSON: current page/collection context
}

type Message struct {
    database.Model
    ConversationID string
    Role           string  // user, assistant, system
    Content        string
    ToolCalls      string  // JSON: AI tool calls
    Status         string  // pending, streaming, complete
}

Controllers

Auth (/signin, /signout)

JWT-based authentication with bcrypt password hashing.

// Guard functions for routes
auth.RequireAuth   // Redirects to /signin
auth.RequireAdmin  // Returns 403 if not admin

Setup (/setup/*)

First-run wizard:

  1. Create admin account
  2. Configure AI provider (provider, API key, model)
  3. Set site name and description

Pages (/admin/pages/*)

Full CRUD with hierarchy support:

  • Create/edit pages with HTML content
  • Drag-drop reordering
  • Publish/unpublish toggle
  • Parent-child relationships

Collections (/admin/collections/*)

Dynamic content types:

  • Define schema with typed fields
  • CRUD for documents within collections
  • Field validation (WIP)

Chat (/admin/chat/*)

AI conversation interface:

  • Create new conversations
  • Stream AI responses via SSE
  • Execute tool calls
  • Track mutations for undo

Site (/*)

Public page rendering:

  • Catch-all route for published pages
  • Template function execution
  • 404 handling

Roadmap

Based on customer experience review, the following phases are planned:

Phase 1: Version Publishing System (Tasks 82-84)

Replace the boolean Published field with a version-based system:

  • Add Status field to PageContent (draft/published)
  • Pages derive published state from whether any version is published
  • UI shows version status with publish/unpublish per version

Phase 2: WYSIWYG Editor (Tasks 85-88)

Add visual editing for non-technical users:

  • Add "Editor" tab between Preview and Code tabs
  • React island using Tiptap (modern ProseMirror wrapper)
  • Toolbar with formatting (bold, italic, headings, lists, links)
  • HTML import/export synced with Code tab

Phase 3: Blog Example (Tasks 89-96)

Create a complete working example out of the box:

  • Blog listing page using template functions
  • Blog view page with dynamic routing via query params
  • Blog edit page with HTMX forms
  • New post creation form
  • All using existing collections API (not Lua - see note below)

Phase 4: OAuth for AI Providers (Future)

Research Complete (Tasks 97-98): Neither OpenAI nor Anthropic offer OAuth for third-party apps to access their APIs on behalf of users.

  • OpenAI: Their Apps SDK uses OAuth 2.1 but ChatGPT is the client calling YOUR services via MCP - not the reverse
  • Anthropic: Actively blocked third-party apps from using Claude OAuth tokens in January 2026

Conclusion: OAuth for AI providers is not viable. Continue with API key approach, provide tutorial content to help users get started.

Known Limitations

Lua Endpoints Have No Database Access

The Lua runtime (internal/runtime/lua.go) only provides request/response handling:

  • request object (method, path, query, headers, body)
  • json_encode / json_decode helpers
  • 5-second execution timeout

There are no database helpers. Lua endpoints cannot create, read, update, or delete documents. For CRUD operations, use:

  1. The collections controller API (/admin/collections/{id}/documents)
  2. HTMX forms in pages that POST to the collections API

A future task (93) may add optional database helpers to Lua.

Template Functions Don't Have Request Access

The render functions in internal/render/ don't receive *http.Request. To access query parameters in page templates, the request must be passed through the template context (Task 94).

Implementation Status

Complete

  • User authentication (JWT + bcrypt)
  • Setup wizard
  • Page CRUD with hierarchy
  • Page versioning (content history, restore, diff view)
  • Collection CRUD with schemas
  • Document CRUD with schema validation
  • Endpoint CRUD with Lua execution
  • AI chat with SSE streaming
  • Tool execution framework (21 tools)
  • Mutation tracking
  • ACL system (rules, middleware, enforcement)
  • Admin UI (DaisyUI + HTMX)
  • Public page rendering with ACL checks
  • File upload with validation
  • Rate limiting
  • Audit logging
  • Health endpoint

Partial

  • AI integration (framework ready, needs provider testing)
  • Undo/redo (mutations tracked, UI not wired)

Not Started (Roadmap Items)

  • Version publishing system (Phase 1)
  • WYSIWYG editor (Phase 2)
  • Blog example seed data (Phase 3)
  • Full-text search
  • Backup/restore
  • Python/Node endpoint runtimes

Environment Variables

Variable Required Description
PORT No Server port (default: 5000)
ENV No Set to "production" for prod mode
AUTH_SECRET Yes JWT signing secret

Running Locally

cd website
go run .
# Open http://localhost:5000
# Complete setup wizard on first run

Deployment

# From project root
go run ./cmd/launch --new website

The Dockerfile uses multi-stage build:

  • Builder: golang:1.24-bookworm (CGO for libsql)
  • Runtime: debian:bookworm-slim
  • Data directory: /data/ (mount as volume)
← Back