create-fff-app 0.1.10 → 0.1.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,25 +1,9 @@
1
- -- fff-stack starter schema
2
- -- Deploy with: wrangler d1 execute fff-stack-db --file=schema.sql
3
-
4
- CREATE TABLE IF NOT EXISTS users (
5
- id TEXT PRIMARY KEY,
6
- name TEXT NOT NULL,
7
- created_at TEXT NOT NULL DEFAULT (datetime('now'))
8
- );
9
-
10
- CREATE INDEX IF NOT EXISTS idx_users_created_at ON users(created_at);
11
-
12
- CREATE TABLE IF NOT EXISTS posts (
13
- id TEXT PRIMARY KEY,
14
- author_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
15
- title TEXT NOT NULL,
16
- slug TEXT NOT NULL UNIQUE,
17
- body TEXT NOT NULL,
18
- published_at TEXT,
19
- created_at TEXT NOT NULL DEFAULT (datetime('now')),
20
- updated_at TEXT NOT NULL DEFAULT (datetime('now'))
21
- );
22
-
23
- CREATE INDEX IF NOT EXISTS idx_posts_slug ON posts(slug);
24
- CREATE INDEX IF NOT EXISTS idx_posts_author_id ON posts(author_id);
25
- CREATE INDEX IF NOT EXISTS idx_posts_created_at ON posts(created_at);
1
+ -- Reference schema (not applied automatically)
2
+ -- Use migrations/ for versioned changes: bun run migrate
3
+
4
+ -- Example:
5
+ -- CREATE TABLE IF NOT EXISTS items (
6
+ -- id TEXT PRIMARY KEY,
7
+ -- name TEXT NOT NULL,
8
+ -- created_at TEXT NOT NULL DEFAULT (datetime('now'))
9
+ -- );
@@ -18,7 +18,7 @@
18
18
  </ItemGroup>
19
19
 
20
20
  <ItemGroup>
21
- <PackageReference Include="FffStack.Client" Version="0.1.0" />
21
+ <PackageReference Include="FffStack.Client" Version="0.1.1" />
22
22
  </ItemGroup>
23
23
 
24
24
  </Project>
@@ -6,18 +6,6 @@
6
6
  </PropertyGroup>
7
7
 
8
8
  <ItemGroup>
9
- <!-- Domain — pure types, no side effects -->
10
- <Compile Include="Domain/Employees/Employee.fs" />
11
-
12
- <!-- Workflows — business logic (may use Infrastructure) -->
13
- <Compile Include="Workflows/Employees/EmployeeWorkflows.fs" />
14
-
15
- <!-- Endpoints — HTTP layer (routes + page templates) -->
16
- <Compile Include="Endpoints/Employees/Routes.fs" />
17
- <Compile Include="Endpoints/Pages/Templates.fs" />
18
- <Compile Include="Endpoints/Pages/Routes.fs" />
19
-
20
- <!-- Entry point -->
21
9
  <Compile Include="Worker.fs" />
22
10
  </ItemGroup>
23
11
 
@@ -1,44 +1,14 @@
1
1
  module App.Server.Worker
2
2
 
3
3
  open FffStack.Server
4
- open Fable.Core
5
4
 
6
5
  let router = Router()
7
6
 
8
- // ── API routes (JSON) ─────────────────────────────────────────────────────────
9
- Endpoints.Employees.Routes.register router
10
-
11
- // ── Page routes (HTML, MPA) ───────────────────────────────────────────────────
12
- Endpoints.Pages.Routes.register router
13
-
14
- // ── Error pages ───────────────────────────────────────────────────────────────
15
-
16
7
  router.OnNotFound(fun _ctx ->
17
- promise {
18
- let body = """
19
- <section class="card">
20
- <div class="card-label">404</div>
21
- <h2>Page not found</h2>
22
- <p class="text-muted" style="margin-top:0.5rem">The page you were looking for doesn't exist.</p>
23
- <a href="/" class="btn btn-primary" style="display:inline-block;margin-top:1.25rem">← Back to Employees</a>
24
- </section>"""
25
- return Response.html (Endpoints.Pages.Templates.shell "Not Found" "" body)
26
- }) |> ignore
8
+ promise { return Response.notFound "Not found" }) |> ignore
27
9
 
28
10
  router.OnError(fun _ctx ex ->
29
- let msg = ex.Message
30
- let body =
31
- sprintf """
32
- <section class="card">
33
- <div class="card-label">500</div>
34
- <h2>Something went wrong</h2>
35
- <p class="text-muted" style="margin-top:0.5rem">%s</p>
36
- <a href="/" class="btn btn-secondary" style="display:inline-block;margin-top:1.25rem">← Back to Employees</a>
37
- </section>"""
38
- msg
39
- promise {
40
- return Response.html (Endpoints.Pages.Templates.shell "Error" "" body)
41
- }) |> ignore
11
+ promise { return Response.html (sprintf "<h1>500</h1><p>%s</p>" ex.Message) }) |> ignore
42
12
 
43
13
  // ── Cloudflare Workers entry point ────────────────────────────────────────────
44
14
 
@@ -1,23 +1,21 @@
1
- name = "fff-stack-app"
2
- main = "dist/server/Worker.js"
1
+ name = "fff-stack-app"
2
+ main = "dist/server/Worker.js"
3
3
  compatibility_date = "2024-09-23"
4
4
 
5
- # Client assets served from Cloudflare Pages / Workers Static Assets
5
+ # Client assets (CSS, JS, images)
6
6
  [assets]
7
7
  directory = "./dist/client"
8
8
  binding = "ASSETS"
9
9
 
10
- # Cloudflare D1 (SQLite)
11
- [[d1_databases]]
12
- binding = "DB"
13
- database_name = "fff-stack-db"
14
- database_id = "YOUR_D1_DATABASE_ID" # replace after: wrangler d1 create fff-stack-db
10
+ # ── Add bindings below as you need them ───────────────────────────────────────
15
11
 
16
- # Cloudflare R2 object storage
17
- [[r2_buckets]]
18
- binding = "BUCKET"
19
- bucket_name = "fff-stack-bucket" # replace with your bucket name
20
- # preview_bucket_name = "fff-stack-bucket-dev" # optional local dev bucket
12
+ # Cloudflare D1 (SQLite) — uncomment after: wrangler d1 create <name>
13
+ # [[d1_databases]]
14
+ # binding = "DB"
15
+ # database_name = "fff-stack-app-db"
16
+ # database_id = "YOUR_D1_DATABASE_ID"
21
17
 
22
- [build]
23
- command = "bun run build:wrangler"
18
+ # Cloudflare R2 (object storage) — uncomment after: wrangler r2 bucket create <name>
19
+ # [[r2_buckets]]
20
+ # binding = "BUCKET"
21
+ # bucket_name = "fff-stack-app-bucket"
@@ -1,16 +0,0 @@
1
- -- Employee directory table
2
- -- Applied automatically by: bun run migrate
3
-
4
- CREATE TABLE IF NOT EXISTS employees (
5
- id TEXT PRIMARY KEY,
6
- name TEXT NOT NULL,
7
- email TEXT NOT NULL UNIQUE,
8
- department TEXT NOT NULL,
9
- role TEXT NOT NULL,
10
- status TEXT NOT NULL DEFAULT 'active',
11
- joined_at TEXT NOT NULL DEFAULT (date('now'))
12
- );
13
-
14
- CREATE INDEX IF NOT EXISTS idx_employees_name ON employees(name);
15
- CREATE INDEX IF NOT EXISTS idx_employees_department ON employees(department);
16
- CREATE INDEX IF NOT EXISTS idx_employees_status ON employees(status);
@@ -1,34 +0,0 @@
1
- namespace App.Server.Domain.Employees
2
-
3
- // ── Value objects ──────────────────────────────────────────────────────────────
4
-
5
- type EmployeeName = private EmployeeName of string
6
-
7
- module EmployeeName =
8
- let create (raw: string) =
9
- let t = raw.Trim()
10
- if t.Length < 2 then Error "Name must be at least 2 characters"
11
- elif t.Length > 100 then Error "Name must be at most 100 characters"
12
- else Ok (EmployeeName t)
13
- let value (EmployeeName n) = n
14
-
15
- type WorkEmail = private WorkEmail of string
16
-
17
- module WorkEmail =
18
- let create (raw: string) =
19
- let t = raw.Trim().ToLowerInvariant()
20
- if not (t.Contains "@") || t.Length < 5 then Error "Invalid email address"
21
- else Ok (WorkEmail t)
22
- let value (WorkEmail e) = e
23
-
24
- // ── Entity ─────────────────────────────────────────────────────────────────────
25
-
26
- type Employee = {
27
- id: string
28
- name: string
29
- email: string
30
- department: string
31
- role: string
32
- status: string // "active" | "inactive"
33
- joinedAt: string
34
- }
@@ -1,35 +0,0 @@
1
- module App.Server.Endpoints.Employees.Routes
2
-
3
- open FffStack.Server
4
- open Fable.Core.JsInterop
5
- open App.Server.Workflows.Employees
6
-
7
- let register (router: Router) : unit =
8
- router
9
-
10
- .Get("/api/employees", fun ctx ->
11
- promise {
12
- let db = Env.requireD1 ctx.Env "DB"
13
- let! employees = getAll db
14
- return Response.json (employees |> List.toArray)
15
- })
16
-
17
- .Post("/api/employees", fun ctx ->
18
- promise {
19
- let db = Env.requireD1 ctx.Env "DB"
20
- let! body = ctx.Request.json<{| name: string; email: string; department: string; role: string; status: string |}>()
21
- match! create db body.name body.email body.department body.role body.status with
22
- | Error e -> return Response.badRequest e
23
- | Ok emp -> return Response.created emp
24
- })
25
-
26
- .Delete("/api/employees/:id", fun ctx ->
27
- promise {
28
- let id = ctx.Params.["id"]
29
- let db = Env.requireD1 ctx.Env "DB"
30
- let! ok = delete db id
31
- if ok then return Response.noContent ()
32
- else return Response.notFound (sprintf "Employee '%s' not found" id)
33
- })
34
-
35
- |> ignore
@@ -1,66 +0,0 @@
1
- <section class="card">
2
- <div class="card-label">About</div>
3
- <h2>fff-stack</h2>
4
- <p class="about-lead">
5
- A fullstack F# framework for <strong>Cloudflare Workers</strong>,
6
- D1, and R2. Write your entire application — client, server, and
7
- templates — in F#. Zero JavaScript by hand.
8
- </p>
9
-
10
- <h3>Architecture</h3>
11
- <ul class="about-list">
12
- <li>
13
- <strong>FJSX</strong> — Svelte-style templates compiled to
14
- optimised DOM operations. No virtual DOM.
15
- </li>
16
- <li>
17
- <strong>F# Client Runtime</strong> — class-based components
18
- with <code>OnMount</code> / <code>OnUpdate</code> /
19
- <code>OnDispose</code> lifecycle hooks.
20
- </li>
21
- <li>
22
- <strong>F# Server Runtime</strong> — chainable Router,
23
- D1/R2 bindings, Validation DSL, and Middleware pipeline.
24
- </li>
25
- <li>
26
- <strong>Islands Architecture</strong> — the server renders
27
- full HTML; client JavaScript only hydrates interactive islands.
28
- </li>
29
- </ul>
30
-
31
- <h3>Quick Start</h3>
32
- <pre class="code-block"><code>bunx create-fff-app my-app
33
- cd my-app &amp;&amp; bun run demo # dev server (no .NET needed)
34
- bun dev # full stack with wrangler
35
- wrangler deploy # ship to Cloudflare</code></pre>
36
- </section>
37
-
38
- <section class="card">
39
- <div class="card-label">Stack</div>
40
- <div class="stack-grid">
41
- <div class="stack-item">
42
- <span class="stack-name">Language</span>
43
- <span class="stack-value">F# (Fable → JS)</span>
44
- </div>
45
- <div class="stack-item">
46
- <span class="stack-name">Runtime</span>
47
- <span class="stack-value">Cloudflare Workers</span>
48
- </div>
49
- <div class="stack-item">
50
- <span class="stack-name">Database</span>
51
- <span class="stack-value">Cloudflare D1 (SQLite)</span>
52
- </div>
53
- <div class="stack-item">
54
- <span class="stack-name">Storage</span>
55
- <span class="stack-value">Cloudflare R2</span>
56
- </div>
57
- <div class="stack-item">
58
- <span class="stack-name">Templates</span>
59
- <span class="stack-value">FJSX (custom compiler)</span>
60
- </div>
61
- <div class="stack-item">
62
- <span class="stack-name">Dev Tool</span>
63
- <span class="stack-value">Bun</span>
64
- </div>
65
- </div>
66
- </section>
@@ -1,66 +0,0 @@
1
- <section class="card">
2
- <div class="list-header">
3
- <h1>Edit Employee</h1>
4
- <a href="/" class="btn btn-ghost btn-sm">← Back to list</a>
5
- </div>
6
-
7
- <form method="POST" action="/employees/update">
8
- <input type="hidden" name="id" value={this.id} />
9
- <div class="form-row">
10
- <div class="form-group">
11
- <label for="name">Full Name</label>
12
- <input type="text" id="name" name="name" value={this.name} required />
13
- </div>
14
- <div class="form-group">
15
- <label for="email">Work Email</label>
16
- <input type="email" id="email" name="email" value={this.email} required />
17
- </div>
18
- </div>
19
- <div class="form-row">
20
- <div class="form-group">
21
- <label for="department">Department</label>
22
- <select id="department" name="department" required>
23
- {#each this.departments as dept}
24
- {#if dept.isSelected}
25
- <option value={dept.value} selected="selected">{dept.value}</option>
26
- {:else}
27
- <option value={dept.value}>{dept.value}</option>
28
- {/if}
29
- {/each}
30
- </select>
31
- </div>
32
- <div class="form-group">
33
- <label for="role">Job Title</label>
34
- <input type="text" id="role" name="role" value={this.role} required />
35
- </div>
36
- </div>
37
- <div class="form-group">
38
- <label>Status</label>
39
- <div class="radio-group">
40
- {#if this.statusIsActive}
41
- <label class="form-checkbox">
42
- <input type="radio" name="status" value="active" checked />
43
- Active
44
- </label>
45
- <label class="form-checkbox">
46
- <input type="radio" name="status" value="inactive" />
47
- Inactive
48
- </label>
49
- {:else}
50
- <label class="form-checkbox">
51
- <input type="radio" name="status" value="active" />
52
- Active
53
- </label>
54
- <label class="form-checkbox">
55
- <input type="radio" name="status" value="inactive" checked />
56
- Inactive
57
- </label>
58
- {/if}
59
- </div>
60
- </div>
61
- <div class="form-actions">
62
- <button type="submit" class="btn btn-primary">Save Changes</button>
63
- <a href="/" class="btn btn-secondary">Cancel</a>
64
- </div>
65
- </form>
66
- </section>
@@ -1,54 +0,0 @@
1
- <section class="card">
2
- <div class="list-header">
3
- <h1>Employees</h1>
4
- <a href="/employees/new" class="btn btn-primary">Add Employee</a>
5
- </div>
6
-
7
- <div class="stat-bar">
8
- <span class="badge badge-neutral">{this.total} Total</span>
9
- <span class="badge badge-success">{this.active} Active</span>
10
- </div>
11
-
12
- {#if this.isEmpty}
13
- <p class="empty-state">No employees yet. Add the first one using the button above.</p>
14
- {:else}
15
- <table class="data-table">
16
- <thead>
17
- <tr>
18
- <th>Name</th>
19
- <th>Email</th>
20
- <th>Department</th>
21
- <th>Job Title</th>
22
- <th>Status</th>
23
- <th>Joined</th>
24
- <th></th>
25
- </tr>
26
- </thead>
27
- <tbody>
28
- {#each this.employees as emp}
29
- <tr>
30
- <td class="font-medium">{emp.name}</td>
31
- <td class="text-muted">{emp.email}</td>
32
- <td>{emp.department}</td>
33
- <td>{emp.role}</td>
34
- <td>
35
- {#if emp.isActive}
36
- <span class="badge badge-success">Active</span>
37
- {:else}
38
- <span class="badge badge-neutral">Inactive</span>
39
- {/if}
40
- </td>
41
- <td class="text-muted text-mono">{emp.joinedAt}</td>
42
- <td class="actions">
43
- <a href={emp.editUrl} class="btn btn-ghost btn-sm">Edit</a>
44
- <form method="POST" action="/employees/delete">
45
- <input type="hidden" name="id" value={emp.id} />
46
- <button type="submit" class="btn btn-ghost btn-sm" style="color:var(--fff-danger)">Remove</button>
47
- </form>
48
- </td>
49
- </tr>
50
- {/each}
51
- </tbody>
52
- </table>
53
- {/if}
54
- </section>
@@ -1,55 +0,0 @@
1
- <section class="card">
2
- <div class="list-header">
3
- <h1>Add Employee</h1>
4
- <a href="/" class="btn btn-ghost btn-sm">← Back to list</a>
5
- </div>
6
-
7
- <form method="POST" action="/employees">
8
- <div class="form-row">
9
- <div class="form-group">
10
- <label for="name">Full Name</label>
11
- <input type="text" id="name" name="name" placeholder="Jane Smith" required />
12
- </div>
13
- <div class="form-group">
14
- <label for="email">Work Email</label>
15
- <input type="email" id="email" name="email" placeholder="jane@company.com" required />
16
- </div>
17
- </div>
18
- <div class="form-row">
19
- <div class="form-group">
20
- <label for="department">Department</label>
21
- <select id="department" name="department" required>
22
- <option value="">Select department…</option>
23
- <option value="Engineering">Engineering</option>
24
- <option value="Design">Design</option>
25
- <option value="Sales">Sales</option>
26
- <option value="Marketing">Marketing</option>
27
- <option value="Operations">Operations</option>
28
- <option value="HR">HR</option>
29
- <option value="Finance">Finance</option>
30
- </select>
31
- </div>
32
- <div class="form-group">
33
- <label for="role">Job Title</label>
34
- <input type="text" id="role" name="role" placeholder="Software Engineer" required />
35
- </div>
36
- </div>
37
- <div class="form-group">
38
- <label>Status</label>
39
- <div class="radio-group">
40
- <label class="form-checkbox">
41
- <input type="radio" name="status" value="active" checked />
42
- Active
43
- </label>
44
- <label class="form-checkbox">
45
- <input type="radio" name="status" value="inactive" />
46
- Inactive
47
- </label>
48
- </div>
49
- </div>
50
- <div class="form-actions">
51
- <button type="submit" class="btn btn-primary">Add Employee</button>
52
- <a href="/" class="btn btn-secondary">Cancel</a>
53
- </div>
54
- </form>
55
- </section>
@@ -1,40 +0,0 @@
1
- <!DOCTYPE html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="UTF-8"/>
5
- <meta name="viewport" content="width=device-width, initial-scale=1"/>
6
- <title>{this.title} — Staff</title>
7
- <link rel="stylesheet" href="/style.css"/>
8
- </head>
9
- <body>
10
- <header class="site-header">
11
- <div class="container">
12
- <a href="/" class="logo">FFF<span>·</span>Stack</a>
13
- <nav class="site-nav">
14
- {#if this.homeActive}
15
- <a href="/" class="active">Employees</a>
16
- {:else}
17
- <a href="/">Employees</a>
18
- {/if}
19
- {#if this.aboutActive}
20
- <a href="/about" class="active">About</a>
21
- {:else}
22
- <a href="/about">About</a>
23
- {/if}
24
- </nav>
25
- </div>
26
- </header>
27
-
28
- <main>
29
- {@this.body}
30
- </main>
31
-
32
- <footer class="site-footer">
33
- <div class="container">
34
- <small>Built with <strong>fff-stack</strong> · F# · FJSX · Cloudflare Workers</small>
35
- </div>
36
- </footer>
37
-
38
- <script type="module" src="/app.js"></script>
39
- </body>
40
- </html>
@@ -1,118 +0,0 @@
1
- module App.Server.Endpoints.Pages.Routes
2
-
3
- open FffStack.Server
4
- open Fable.Core
5
- open Fable.Core.JsInterop
6
- open App.Server.Domain.Employees
7
- open App.Server.Workflows.Employees
8
-
9
- let private deptChoices (current: string) =
10
- [ "Engineering"; "Design"; "Sales"; "Marketing"; "Operations"; "HR"; "Finance" ]
11
- |> List.map (fun d -> createObj [ "value" ==> d; "isSelected" ==> (d = current) ])
12
- |> List.toArray
13
-
14
- let private employeeRow (emp: Employee) =
15
- createObj [
16
- "id" ==> emp.id
17
- "name" ==> emp.name
18
- "email" ==> emp.email
19
- "department" ==> emp.department
20
- "role" ==> emp.role
21
- "status" ==> emp.status
22
- "joinedAt" ==> emp.joinedAt
23
- "isActive" ==> (emp.status = "active")
24
- "editUrl" ==> (sprintf "/employees/%s/edit" emp.id)
25
- ]
26
-
27
- let register (router: Router) : unit =
28
- router
29
-
30
- // GET / — employee list
31
- .Get("/", fun ctx ->
32
- promise {
33
- let db = Env.requireD1 ctx.Env "DB"
34
- let! employees = getAll db
35
- let total = employees.Length
36
- let active = employees |> List.filter (fun e -> e.status = "active") |> List.length
37
- let flash = Flash.banner ctx.Request
38
- let body = Templates.renderEmployeeList (createObj [
39
- "employees" ==> (employees |> List.map employeeRow |> List.toArray)
40
- "isEmpty" ==> (total = 0)
41
- "total" ==> total
42
- "active" ==> active
43
- ])
44
- return Response.html (Templates.shell "Employees" "/" (flash + body))
45
- })
46
-
47
- // GET /employees/new — add employee form
48
- .Get("/employees/new", fun _ctx ->
49
- promise {
50
- return Response.html (Templates.shell "Add Employee" "/" (Templates.renderEmployeeNew (createObj [])))
51
- })
52
-
53
- // POST /employees — create employee (PRG)
54
- .Post("/employees", fun ctx ->
55
- promise {
56
- let db = Env.requireD1 ctx.Env "DB"
57
- let! form = Form.parseForm ctx.Request
58
- let get f = Form.field f form |> Result.defaultValue ""
59
- match! create db (get "name") (get "email") (get "department") (get "role") (get "status") with
60
- | Error e -> return Flash.redirectWith "/" "error" e
61
- | Ok emp ->
62
- return Flash.redirectWith "/" "success"
63
- (sprintf "%s has been added to the directory." emp.name)
64
- })
65
-
66
- // GET /employees/:id/edit — edit employee form
67
- .Get("/employees/:id/edit", fun ctx ->
68
- promise {
69
- let id = ctx.Params.["id"]
70
- let db = Env.requireD1 ctx.Env "DB"
71
- match! getById db id with
72
- | None -> return Response.notFound (sprintf "Employee '%s' not found" id)
73
- | Some emp ->
74
- let body = Templates.renderEmployeeEdit (createObj [
75
- "id" ==> emp.id
76
- "name" ==> emp.name
77
- "email" ==> emp.email
78
- "role" ==> emp.role
79
- "status" ==> emp.status
80
- "statusIsActive" ==> (emp.status = "active")
81
- "departments" ==> deptChoices emp.department
82
- ])
83
- return Response.html (Templates.shell "Edit Employee" "/" body)
84
- })
85
-
86
- // POST /employees/update — update employee (PRG)
87
- .Post("/employees/update", fun ctx ->
88
- promise {
89
- let db = Env.requireD1 ctx.Env "DB"
90
- let! form = Form.parseForm ctx.Request
91
- let get f = Form.field f form |> Result.defaultValue ""
92
- match! update db (get "id") (get "name") (get "email") (get "department") (get "role") (get "status") with
93
- | Error e -> return Flash.redirectWith "/" "error" e
94
- | Ok emp ->
95
- return Flash.redirectWith "/" "success"
96
- (sprintf "%s has been updated." emp.name)
97
- })
98
-
99
- // POST /employees/delete — delete employee (PRG)
100
- .Post("/employees/delete", fun ctx ->
101
- promise {
102
- let db = Env.requireD1 ctx.Env "DB"
103
- let! form = Form.parseForm ctx.Request
104
- match Form.field "id" form with
105
- | Error _ -> return Flash.redirectWith "/" "error" "Missing employee ID"
106
- | Ok id ->
107
- let! ok = delete db id
108
- if ok then return Flash.redirectWith "/" "success" "Employee removed."
109
- else return Flash.redirectWith "/" "error" "Employee not found."
110
- })
111
-
112
- // GET /about — tech stack info
113
- .Get("/about", fun _ctx ->
114
- promise {
115
- return Response.html (Templates.shell "About" "/about" (Templates.renderAbout()))
116
- })
117
-
118
- |> ignore