create-apexjs 0.5.1 → 0.5.3
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.
- package/package.json +1 -1
- package/templates/default/AGENTS.md +85 -0
- package/templates/features/auth/pages/account.alpine +54 -0
- package/templates/features/auth/server/api/login.ts +16 -0
- package/templates/features/auth/server/api/logout.ts +11 -0
- package/templates/features/auth/server/api/whoami.ts +11 -0
- package/templates/features/auth/server/auth.ts +10 -0
- package/templates/features/data/db/index.ts +28 -0
- package/templates/features/data/models/Message.ts +21 -0
- package/templates/features/data/pages/guestbook.alpine +56 -0
- package/templates/features/data/server/api/messages.ts +8 -0
- package/templates/features/i18n/locales/en.json +5 -0
- package/templates/features/i18n/locales/fr.json +5 -0
- package/templates/features/i18n/pages/hello.alpine +24 -0
package/package.json
CHANGED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# AGENTS.md — building this app with an AI agent
|
|
2
|
+
|
|
3
|
+
This is an **Apex JS** app. Apex is a full-stack, AI-native meta-framework for
|
|
4
|
+
Alpine.js: file-based routing, SSR, islands, and **every typed API route is also
|
|
5
|
+
an MCP tool**. This file tells you (the agent) how to work here effectively.
|
|
6
|
+
|
|
7
|
+
## Golden rules
|
|
8
|
+
1. **Prefer generators over hand-writing.** Run `apex make …` / `apex extend …` /
|
|
9
|
+
`apex add …` to create files — they produce correct, conventional, typed code.
|
|
10
|
+
Hand-writing boilerplate is where you make mistakes.
|
|
11
|
+
2. **Everything has one place.** Follow the structure below; don't invent folders.
|
|
12
|
+
3. **Security is the framework's job, not yours.** Use `auth`/`can`/`scope` — never
|
|
13
|
+
hand-roll auth. Defaults are fail-closed.
|
|
14
|
+
4. **Verify your work.** Add tests with `createTestApp` and run `apex test`.
|
|
15
|
+
5. **TypeScript is strict.** If it compiles, it's probably right; lean on the types.
|
|
16
|
+
|
|
17
|
+
## Do things with the CLI (this is the fast path)
|
|
18
|
+
```bash
|
|
19
|
+
apex make page dashboard # pages/dashboard.alpine (a route → /dashboard)
|
|
20
|
+
apex make component Card # components/Card.alpine (<Card /> in templates)
|
|
21
|
+
apex make model Post title:string body:string # models/Post.ts → migration + REST + MCP CRUD
|
|
22
|
+
apex make api webhooks # server/api/webhooks.ts (defineApexRoute; also an MCP tool)
|
|
23
|
+
apex make service Billing # services/BillingService.ts
|
|
24
|
+
apex extend auth # add sealed-cookie sessions + login/logout + /account
|
|
25
|
+
apex extend data # add a DB-backed model + /guestbook demo (Supabase-ready)
|
|
26
|
+
apex extend i18n # add locales/*.json + /fr routing
|
|
27
|
+
apex add button card modal # copy themeable UI components in
|
|
28
|
+
apex migrate # apply DB migrations
|
|
29
|
+
apex build --preset vercel # build + Vercel config (also: netlify, docker); then deploy
|
|
30
|
+
apex test # run Vitest
|
|
31
|
+
```
|
|
32
|
+
`apex make <kind> …` kinds: `page component api service store layout middleware test model migration auth`.
|
|
33
|
+
|
|
34
|
+
## Project structure
|
|
35
|
+
```
|
|
36
|
+
pages/**/*.alpine Routes. File path → URL. [param] = dynamic. index.alpine → parent path.
|
|
37
|
+
layouts/*.alpine Shared shells; default.alpine wraps every page (<slot/>).
|
|
38
|
+
components/*.alpine Reusable <PascalCase/> components (props via attributes).
|
|
39
|
+
server/api/*.ts Typed routes (defineApexRoute) / model resources → REST + MCP.
|
|
40
|
+
server/auth.ts Identity (sessionAuth) → ctx.user everywhere. (from `apex extend auth`)
|
|
41
|
+
models/*.ts defineModel → schema + migration + table + REST/MCP CRUD.
|
|
42
|
+
db/ createDb + migrations. Uses DATABASE_URL (Postgres) or in-memory libSQL.
|
|
43
|
+
locales/*.json i18n catalogs. (from `apex extend i18n`)
|
|
44
|
+
services/*.ts Business logic (classes). Keep routes thin; delegate here.
|
|
45
|
+
stores/*.ts Global SSR-safe state ($store.x).
|
|
46
|
+
composables/*.ts Reusable client logic (useX) for <script client>.
|
|
47
|
+
shared/*.ts Types shared by server + client.
|
|
48
|
+
tests/*.test.ts Vitest. createTestApp boots the whole app in-process.
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## The `.alpine` single-file component
|
|
52
|
+
```alpine
|
|
53
|
+
<script server lang="ts">
|
|
54
|
+
// Runs on the server. loader() data becomes the page's x-data (real HTML first).
|
|
55
|
+
export function loader() { return { title: 'Hello' } }
|
|
56
|
+
export function head() { return { title: 'Hello' } } // SEO
|
|
57
|
+
</script>
|
|
58
|
+
<script client lang="ts">
|
|
59
|
+
// Optional client-only logic; import composables here.
|
|
60
|
+
</script>
|
|
61
|
+
<template x-data>
|
|
62
|
+
<h1 x-text="title"></h1>
|
|
63
|
+
<Counter start="0" client:load /> <!-- island: client:load | client:visible | client:idle -->
|
|
64
|
+
</template>
|
|
65
|
+
<style scoped> h1 { color: var(--color-primary); } </style>
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## APIs you'll use (import from `@apex-stack/core`, `/server`, `/testing`; data from `@apex-stack/data`)
|
|
69
|
+
- **Route** → `defineApexRoute({ method, input: { …zod }, mcp: true, auth: true, can, handler })`. `mcp: true` makes it an AI-callable tool at `/mcp`.
|
|
70
|
+
- **Model** → `defineModel('posts', { fields: {…}, use: [timestamps(), owned()] })` → `.resource(handle)` mounts REST + MCP CRUD.
|
|
71
|
+
- **Auth** → `server/auth.ts` default-exports `sessionAuth({ password })`; gate routes with `auth: true` / `can`. In loaders, the user is `locals.user`.
|
|
72
|
+
- **Config** → `apex.config.ts`: `defineConfig({ runtimeConfig: { …, public: {…} }, i18n: {…} })`. Read with `useRuntimeConfig()`.
|
|
73
|
+
- **Test** → `createTestApp({ root })` → `app.get/post(path, body?, { user })`, `app.mcp.listTools()`.
|
|
74
|
+
|
|
75
|
+
The full stability contract of public APIs is in the framework's `API.md` — 🟢 Stable ones won't break; 🟡 Experimental ones might.
|
|
76
|
+
|
|
77
|
+
## Deploy
|
|
78
|
+
`apex build --preset vercel|netlify|docker`. Set `DATABASE_URL` (Supabase/Neon/Postgres or Turso) + `APEX_SESSION_PASSWORD` (≥32 chars) in the host env. See the deploy docs.
|
|
79
|
+
|
|
80
|
+
## Drive Apex as MCP tools (optional, powerful)
|
|
81
|
+
Apex ships an MCP server for its own CLI — run `apex mcp-server` (stdio). Point your
|
|
82
|
+
agent host at it to get `apex_make`, `apex_extend`, `apex_add`, `apex_build`, and
|
|
83
|
+
`apex_list` as structured tools, so you scaffold by calling tools instead of shelling out.
|
|
84
|
+
|
|
85
|
+
Docs: https://apexjs.site/docs/
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
<script server lang="ts">
|
|
2
|
+
// The user resolved by server/auth.ts is seeded into locals — so the page
|
|
3
|
+
// renders the signed-in state on the SERVER, no flash of "logged out".
|
|
4
|
+
export function loader({ locals }: { locals: Record<string, unknown> }) {
|
|
5
|
+
return { user: (locals.user as { name: string } | undefined) ?? null }
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function head() {
|
|
9
|
+
return { title: 'Account — apex-showcase' }
|
|
10
|
+
}
|
|
11
|
+
</script>
|
|
12
|
+
|
|
13
|
+
<template x-data="{
|
|
14
|
+
name: '',
|
|
15
|
+
async login() {
|
|
16
|
+
if (!this.name.trim()) return
|
|
17
|
+
await fetch('/api/login', {
|
|
18
|
+
method: 'POST',
|
|
19
|
+
headers: { 'content-type': 'application/json' },
|
|
20
|
+
body: JSON.stringify({ name: this.name }),
|
|
21
|
+
})
|
|
22
|
+
location.reload()
|
|
23
|
+
},
|
|
24
|
+
async logout() {
|
|
25
|
+
await fetch('/api/logout', { method: 'POST' })
|
|
26
|
+
location.reload()
|
|
27
|
+
},
|
|
28
|
+
}">
|
|
29
|
+
<section class="py-8">
|
|
30
|
+
<h1 class="font-title text-3xl font-extrabold tracking-tight text-on-surface-strong dark:text-on-surface-dark-strong">Account</h1>
|
|
31
|
+
<p class="mt-2 max-w-2xl text-on-surface/80 dark:text-on-surface-dark/80">
|
|
32
|
+
Sealed-cookie sessions via <code>server/auth.ts</code>. The signed-in name below is
|
|
33
|
+
rendered on the server from <code>locals.user</code>, and the gated route
|
|
34
|
+
<code>/api/whoami</code> returns 401 until you log in.
|
|
35
|
+
</p>
|
|
36
|
+
|
|
37
|
+
<div class="mt-6 max-w-md rounded-radius border border-outline p-5 dark:border-outline-dark">
|
|
38
|
+
<template x-if="user">
|
|
39
|
+
<div class="flex flex-col items-start gap-3">
|
|
40
|
+
<p class="text-on-surface-strong dark:text-on-surface-dark-strong">Signed in as <strong x-text="user.name"></strong>.</p>
|
|
41
|
+
<button type="button" @click="logout()" class="rounded-radius border border-outline px-4 py-2 text-sm font-medium transition hover:opacity-75 dark:border-outline-dark">Log out</button>
|
|
42
|
+
</div>
|
|
43
|
+
</template>
|
|
44
|
+
|
|
45
|
+
<template x-if="!user">
|
|
46
|
+
<form class="flex flex-col items-start gap-3" @submit.prevent="login()">
|
|
47
|
+
<p class="text-on-surface/80 dark:text-on-surface-dark/80">You are not signed in.</p>
|
|
48
|
+
<input x-model="name" placeholder="Your name" class="w-full rounded-radius border border-outline bg-transparent px-3 py-2 dark:border-outline-dark" />
|
|
49
|
+
<button type="submit" class="inline-flex items-center justify-center gap-2 rounded-radius border border-primary bg-primary px-4 py-2 text-sm font-medium text-on-primary transition hover:opacity-75 dark:border-primary-dark dark:bg-primary-dark dark:text-on-primary-dark">Log in</button>
|
|
50
|
+
</form>
|
|
51
|
+
</template>
|
|
52
|
+
</div>
|
|
53
|
+
</section>
|
|
54
|
+
</template>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { defineApexRoute, useRuntimeConfig } from '@apex-stack/core'
|
|
2
|
+
import { login } from '@apex-stack/core/server'
|
|
3
|
+
import { z } from 'zod'
|
|
4
|
+
|
|
5
|
+
// Demo login: accept a name and seal it into the session. A real app would
|
|
6
|
+
// verify a password / OAuth code here first, then call login() with the result.
|
|
7
|
+
export default defineApexRoute({
|
|
8
|
+
method: 'POST',
|
|
9
|
+
description: 'Log in (demo: name only) and start a session',
|
|
10
|
+
input: { name: z.string().min(1) },
|
|
11
|
+
handler: async ({ input, event }) => {
|
|
12
|
+
const user = { id: input.name.toLowerCase().replace(/\s+/g, '-'), name: input.name }
|
|
13
|
+
await login(event, { user }, { password: String(useRuntimeConfig().sessionPassword) })
|
|
14
|
+
return { user }
|
|
15
|
+
},
|
|
16
|
+
})
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { defineApexRoute, useRuntimeConfig } from '@apex-stack/core'
|
|
2
|
+
import { logout } from '@apex-stack/core/server'
|
|
3
|
+
|
|
4
|
+
export default defineApexRoute({
|
|
5
|
+
method: 'POST',
|
|
6
|
+
description: 'Clear the session cookie',
|
|
7
|
+
handler: async ({ event }) => {
|
|
8
|
+
await logout(event, { password: String(useRuntimeConfig().sessionPassword) })
|
|
9
|
+
return { ok: true }
|
|
10
|
+
},
|
|
11
|
+
})
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { defineApexRoute } from '@apex-stack/core'
|
|
2
|
+
|
|
3
|
+
// A gated route: `auth: true` → 401 for anonymous callers, and it's hidden from
|
|
4
|
+
// the MCP tool list for users who can't reach it. ctx.user is the session identity.
|
|
5
|
+
export default defineApexRoute({
|
|
6
|
+
method: 'GET',
|
|
7
|
+
description: 'Return the currently signed-in user',
|
|
8
|
+
auth: true,
|
|
9
|
+
mcp: true,
|
|
10
|
+
handler: ({ user }) => ({ user }),
|
|
11
|
+
})
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { useRuntimeConfig } from '@apex-stack/core'
|
|
2
|
+
import { sessionAuth } from '@apex-stack/core/server'
|
|
3
|
+
|
|
4
|
+
// Identity for the whole app. sessionAuth resolves the caller from the sealed
|
|
5
|
+
// (encrypted + signed, HttpOnly) session cookie and injects the result as
|
|
6
|
+
// ctx.user into every loader, API route and MCP tool. Anonymous → user = null.
|
|
7
|
+
// Swap this file for a JWT/OAuth adapter without touching any route.
|
|
8
|
+
export default sessionAuth({
|
|
9
|
+
password: String(useRuntimeConfig().sessionPassword),
|
|
10
|
+
})
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { createDb } from '@apex-stack/data'
|
|
2
|
+
import { Message } from '../models/Message.js'
|
|
3
|
+
|
|
4
|
+
// Use a hosted Postgres (Supabase / Neon / any Postgres) when DATABASE_URL is
|
|
5
|
+
// set — otherwise an in-memory libSQL for zero-setup local dev. createDb
|
|
6
|
+
// auto-tunes Postgres for a transaction pooler (e.g. Supabase's, on port 6543):
|
|
7
|
+
// it disables prepared statements and enables SSL. Requires the `postgres` driver
|
|
8
|
+
// (already a dependency of this app).
|
|
9
|
+
const url = process.env.DATABASE_URL
|
|
10
|
+
export const handle = url
|
|
11
|
+
? await createDb({ driver: 'postgres', url })
|
|
12
|
+
: await createDb({ driver: 'libsql', url: ':memory:' })
|
|
13
|
+
|
|
14
|
+
// Create the schema — dialect-aware (SQLite vs Postgres) and idempotent, straight
|
|
15
|
+
// from the model. For a long-lived Postgres you'd normally run `apex migrate`
|
|
16
|
+
// once and drop this; keeping it makes the showcase zero-setup on either backend.
|
|
17
|
+
await handle.exec(Message.migrationSql(handle.dialect))
|
|
18
|
+
|
|
19
|
+
// Seed a couple of rows if the table is empty.
|
|
20
|
+
const now = handle.dialect === 'postgres' ? 'now()' : "datetime('now')"
|
|
21
|
+
const seeded = await handle.query('SELECT COUNT(*) AS n FROM messages')
|
|
22
|
+
if (Number(seeded[0]?.n ?? 0) === 0) {
|
|
23
|
+
await handle.exec(
|
|
24
|
+
`INSERT INTO messages (author, body, created_at, updated_at) VALUES
|
|
25
|
+
('Ada Lovelace', 'Apex feels like HTML with superpowers.', ${now}, ${now}),
|
|
26
|
+
('Alan Turing', 'One route — and my AI assistant can call it too.', ${now}, ${now})`,
|
|
27
|
+
)
|
|
28
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { defineModel, timestamps } from '@apex-stack/data'
|
|
2
|
+
|
|
3
|
+
// A model is Apex's single source of truth for a piece of data. This ONE
|
|
4
|
+
// definition derives, with no extra wiring:
|
|
5
|
+
// • a typed Zod schema for insert/update validation
|
|
6
|
+
// • the SQL migration in db/migrations
|
|
7
|
+
// • a Drizzle table (per dialect: sqlite/libSQL/postgres)
|
|
8
|
+
// • a full REST + MCP resource — GET/POST /api/messages, tools
|
|
9
|
+
// messages_list / messages_get / messages_create / …
|
|
10
|
+
//
|
|
11
|
+
// Behaviors are composable, reusable lifecycle add-ons. `timestamps()` stamps
|
|
12
|
+
// created_at on insert + updated_at on every write and keeps them out of the
|
|
13
|
+
// client-writable insert shape. Others ship in @apex-stack/data: owned() (per-user
|
|
14
|
+
// ownership + auth gating), softDeletes(), auditable(), observable().
|
|
15
|
+
export const Message = defineModel('messages', {
|
|
16
|
+
fields: {
|
|
17
|
+
author: { type: 'string', notNull: true },
|
|
18
|
+
body: { type: 'string', notNull: true },
|
|
19
|
+
},
|
|
20
|
+
use: [timestamps()],
|
|
21
|
+
})
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
<script server lang="ts">
|
|
2
|
+
import { handle } from '../db/index.js'
|
|
3
|
+
|
|
4
|
+
// Runs on the server: reads straight from the database, so the message list is
|
|
5
|
+
// real, indexable HTML before any JS loads. The data comes from the SAME model
|
|
6
|
+
// (models/Message.ts) that powers /api/messages and the messages_* MCP tools.
|
|
7
|
+
export async function loader() {
|
|
8
|
+
const messages = await handle.query(
|
|
9
|
+
'SELECT id, author, body, created_at FROM messages ORDER BY id DESC LIMIT 20',
|
|
10
|
+
)
|
|
11
|
+
return { messages }
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function head() {
|
|
15
|
+
return { title: 'Guestbook — apex-showcase' }
|
|
16
|
+
}
|
|
17
|
+
</script>
|
|
18
|
+
|
|
19
|
+
<template x-data="{
|
|
20
|
+
async submit(e) {
|
|
21
|
+
const f = new FormData(e.target)
|
|
22
|
+
await fetch('/api/messages', {
|
|
23
|
+
method: 'POST',
|
|
24
|
+
headers: { 'content-type': 'application/json' },
|
|
25
|
+
body: JSON.stringify({ author: f.get('author') || 'Anonymous', body: f.get('body') }),
|
|
26
|
+
})
|
|
27
|
+
location.reload()
|
|
28
|
+
}
|
|
29
|
+
}">
|
|
30
|
+
<section class="py-8">
|
|
31
|
+
<h1 class="font-title text-3xl font-extrabold tracking-tight text-on-surface-strong dark:text-on-surface-dark-strong">Guestbook</h1>
|
|
32
|
+
<p class="mt-2 max-w-2xl text-on-surface/80 dark:text-on-surface-dark/80">
|
|
33
|
+
A database-backed model in action. The list is server-rendered from
|
|
34
|
+
<code>models/Message.ts</code>; the form posts to <code>/api/messages</code>
|
|
35
|
+
— the very same route your AI can call as the <code>messages_create</code> MCP tool.
|
|
36
|
+
</p>
|
|
37
|
+
|
|
38
|
+
<ul class="mt-6 divide-y divide-outline dark:divide-outline-dark">
|
|
39
|
+
<template x-for="m in messages" :key="m.id">
|
|
40
|
+
<li class="flex gap-3 py-3">
|
|
41
|
+
<span class="grid size-9 flex-none place-items-center rounded-full bg-primary font-bold text-sm text-on-primary dark:bg-primary-dark dark:text-on-primary-dark" x-text="(m.author || '?').slice(0,1).toUpperCase()"></span>
|
|
42
|
+
<div>
|
|
43
|
+
<p class="text-on-surface-strong dark:text-on-surface-dark-strong" x-text="m.body"></p>
|
|
44
|
+
<p class="text-sm text-on-surface/70 dark:text-on-surface-dark/70"><strong x-text="m.author"></strong> · <span x-text="m.created_at"></span></p>
|
|
45
|
+
</div>
|
|
46
|
+
</li>
|
|
47
|
+
</template>
|
|
48
|
+
</ul>
|
|
49
|
+
|
|
50
|
+
<form class="mt-6 grid max-w-lg gap-3 rounded-radius border border-outline p-4 dark:border-outline-dark" @submit.prevent="submit($event)">
|
|
51
|
+
<input name="author" placeholder="Your name" class="rounded-radius border border-outline bg-transparent px-3 py-2 dark:border-outline-dark" />
|
|
52
|
+
<textarea name="body" placeholder="Say something…" required maxlength="280" class="rounded-radius border border-outline bg-transparent px-3 py-2 dark:border-outline-dark"></textarea>
|
|
53
|
+
<div><button type="submit" class="inline-flex items-center justify-center gap-2 rounded-radius border border-primary bg-primary px-4 py-2 text-sm font-medium text-on-primary transition hover:opacity-75 dark:border-primary-dark dark:bg-primary-dark dark:text-on-primary-dark">Sign the guestbook</button></div>
|
|
54
|
+
</form>
|
|
55
|
+
</section>
|
|
56
|
+
</template>
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { handle } from '../../db/index.js'
|
|
2
|
+
import { Message } from '../../models/Message.js'
|
|
3
|
+
|
|
4
|
+
// The model becomes a live REST + MCP resource with zero extra wiring:
|
|
5
|
+
// GET /api/messages · MCP tool messages_list
|
|
6
|
+
// GET /api/messages/:id · MCP tool messages_get
|
|
7
|
+
// POST /api/messages · MCP tool messages_create (+ update / delete)
|
|
8
|
+
export default Message.resource(handle)
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
{
|
|
2
|
+
"greeting": "Hello from Apex!",
|
|
3
|
+
"intro": "This paragraph was translated on the server, before any JavaScript ran. Switch to /fr/hello for French — the /<locale> path prefix (or the Accept-Language header) selects the catalog.",
|
|
4
|
+
"nav": { "home": "Home", "guestbook": "Guestbook", "account": "Account", "hello": "i18n" }
|
|
5
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
{
|
|
2
|
+
"greeting": "Bonjour depuis Apex !",
|
|
3
|
+
"intro": "Ce paragraphe a été traduit sur le serveur, avant l’exécution du moindre JavaScript. Passez à /hello pour l’anglais — le préfixe /<locale> (ou l’en-tête Accept-Language) choisit le catalogue.",
|
|
4
|
+
"nav": { "home": "Accueil", "guestbook": "Livre d’or", "account": "Compte", "hello": "i18n" }
|
|
5
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
<script server lang="ts">
|
|
2
|
+
// locals.t and locals.locale are injected by the i18n runtime once i18n is
|
|
3
|
+
// declared in apex.config.ts. Translation happens on the server, so the HTML
|
|
4
|
+
// arrives already localized — great for SEO.
|
|
5
|
+
export function loader({ locals }: { locals: Record<string, unknown> }) {
|
|
6
|
+
const t = locals.t as (k: string, p?: Record<string, unknown>) => string
|
|
7
|
+
return { greeting: t('greeting'), intro: t('intro'), locale: locals.locale as string }
|
|
8
|
+
}
|
|
9
|
+
export function head({ data }: { data: { greeting: string } }) {
|
|
10
|
+
return { title: data.greeting }
|
|
11
|
+
}
|
|
12
|
+
</script>
|
|
13
|
+
|
|
14
|
+
<template x-data>
|
|
15
|
+
<section class="py-8">
|
|
16
|
+
<p class="text-sm text-on-surface/70 dark:text-on-surface-dark/70">active locale: <strong x-text="locale"></strong></p>
|
|
17
|
+
<h1 class="mt-2 font-title text-3xl font-extrabold tracking-tight text-on-surface-strong dark:text-on-surface-dark-strong" x-text="greeting"></h1>
|
|
18
|
+
<p class="mt-3 max-w-2xl text-on-surface/80 dark:text-on-surface-dark/80" x-text="intro"></p>
|
|
19
|
+
<div class="mt-5 flex gap-3">
|
|
20
|
+
<a href="/hello" class="text-primary hover:opacity-75 dark:text-primary-dark">English</a>
|
|
21
|
+
<a href="/fr/hello" class="text-primary hover:opacity-75 dark:text-primary-dark">Français</a>
|
|
22
|
+
</div>
|
|
23
|
+
</section>
|
|
24
|
+
</template>
|