create-june 0.0.1
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/README.md +26 -0
- package/bin.mjs +55 -0
- package/package.json +35 -0
- package/template/AGENTS.md +60 -0
- package/template/README.md +41 -0
- package/template/app/actions.ts +28 -0
- package/template/app/blog/[slug]/page.tsx +30 -0
- package/template/app/blog/page.tsx +30 -0
- package/template/app/layout.tsx +14 -0
- package/template/app/models/post.ts +35 -0
- package/template/app/not-found.tsx +12 -0
- package/template/app/page.tsx +18 -0
- package/template/app/posts/[slug]/error.tsx +15 -0
- package/template/app/posts/[slug]/page.tsx +23 -0
- package/template/app/posts/loading.tsx +5 -0
- package/template/app/posts/page.tsx +26 -0
- package/template/content/posts/2026-06-09-writing-with-agents.md +9 -0
- package/template/content/posts/2026-06-10-hello-content.md +17 -0
- package/template/db/migrations/0001_create_posts.sql +15 -0
- package/template/dev.ts +14 -0
- package/template/june.config.ts +22 -0
- package/template/package.json +11 -0
package/README.md
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# create-june
|
|
2
|
+
|
|
3
|
+
Scaffold a [June](https://june.build) app — **the agent-native React framework**.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npm create june my-app
|
|
7
|
+
# or: bun create june my-app
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
One `route()` definition serves every audience:
|
|
11
|
+
|
|
12
|
+
| humans | agents |
|
|
13
|
+
| --- | --- |
|
|
14
|
+
| streamed HTML, nested layouts, loading/error boundaries | the same routes as `.md` / `.json` |
|
|
15
|
+
| hover prerender + view transitions, zero client JS | `llms.txt`, `sitemap.xml`, MCP tools at `/mcp` |
|
|
16
|
+
|
|
17
|
+
The scaffold also ships an `AGENTS.md` (conventions for coding agents),
|
|
18
|
+
plain-SQL migrations (the data layer's source of truth), and markdown content
|
|
19
|
+
served to agents **verbatim** — the authored file, not a lossy conversion.
|
|
20
|
+
|
|
21
|
+
> ⚠ **Pre-release.** The `june` framework package is not on npm yet; this
|
|
22
|
+
> scaffold tracks the framework's development and currently runs inside the
|
|
23
|
+
> [june repository](https://github.com/junebuild/june). Watch
|
|
24
|
+
> [june.build](https://june.build) for the runnable release.
|
|
25
|
+
|
|
26
|
+
MIT
|
package/bin.mjs
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// create-june — scaffold a June app (the agent-native React framework).
|
|
3
|
+
// Non-interactive by design (CI/C3-friendly): `npm create june my-app` just works.
|
|
4
|
+
import { cpSync, existsSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { basename, join, resolve } from "node:path";
|
|
6
|
+
|
|
7
|
+
const args = process.argv.slice(2).filter((a) => !a.startsWith("-"));
|
|
8
|
+
const target = args[0] ?? "june-app";
|
|
9
|
+
const dest = resolve(process.cwd(), target);
|
|
10
|
+
const TEMPLATE = join(import.meta.dirname, "template");
|
|
11
|
+
|
|
12
|
+
if (existsSync(dest)) {
|
|
13
|
+
console.error(`✗ ${target} already exists — refusing to overwrite.`);
|
|
14
|
+
process.exit(1);
|
|
15
|
+
}
|
|
16
|
+
if (!existsSync(TEMPLATE)) {
|
|
17
|
+
console.error("✗ template missing from package — please file an issue.");
|
|
18
|
+
process.exit(1);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
cpSync(TEMPLATE, dest, { recursive: true });
|
|
22
|
+
|
|
23
|
+
// Stamp the app name into every scaffolded file that mentions it.
|
|
24
|
+
const name = basename(dest);
|
|
25
|
+
const stamp = (dir) => {
|
|
26
|
+
for (const e of readdirSync(dir, { withFileTypes: true })) {
|
|
27
|
+
const p = join(dir, e.name);
|
|
28
|
+
if (e.isDirectory()) stamp(p);
|
|
29
|
+
else {
|
|
30
|
+
const text = readFileSync(p, "utf8");
|
|
31
|
+
if (text.includes("__APP_NAME__")) {
|
|
32
|
+
writeFileSync(p, text.replaceAll("__APP_NAME__", name));
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
stamp(dest);
|
|
38
|
+
|
|
39
|
+
console.log(`
|
|
40
|
+
✓ Created ${name}/
|
|
41
|
+
|
|
42
|
+
June is the agent-native React framework: one route() definition serves
|
|
43
|
+
humans (streamed HTML) AND agents (.md / .json / llms.txt / MCP tools).
|
|
44
|
+
|
|
45
|
+
⚠ PRE-RELEASE: the \`june\` framework package is not on npm yet — this
|
|
46
|
+
scaffold shows the conventions and follows the framework's development.
|
|
47
|
+
Watch https://june.build and https://github.com/junebuild/june for the
|
|
48
|
+
runnable release. (The scaffold runs today inside the june repository.)
|
|
49
|
+
|
|
50
|
+
Explore:
|
|
51
|
+
${target}/app/ routes, layouts, boundaries
|
|
52
|
+
${target}/content/ markdown posts (served verbatim to agents as .md)
|
|
53
|
+
${target}/db/ plain-SQL migrations (the data layer's truth)
|
|
54
|
+
${target}/AGENTS.md conventions for coding agents
|
|
55
|
+
`);
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "create-june",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Scaffold a June app — the agent-native React framework. Every route serves humans (streamed HTML) and agents (markdown/JSON/MCP) from one definition.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"create-june": "./bin.mjs"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin.mjs",
|
|
11
|
+
"template"
|
|
12
|
+
],
|
|
13
|
+
"engines": {
|
|
14
|
+
"node": ">=20.11"
|
|
15
|
+
},
|
|
16
|
+
"scripts": {
|
|
17
|
+
"prepublishOnly": "node scripts/sync-template.mjs"
|
|
18
|
+
},
|
|
19
|
+
"keywords": [
|
|
20
|
+
"june",
|
|
21
|
+
"react",
|
|
22
|
+
"rsc",
|
|
23
|
+
"framework",
|
|
24
|
+
"scaffold",
|
|
25
|
+
"agent",
|
|
26
|
+
"mcp",
|
|
27
|
+
"dual-audience"
|
|
28
|
+
],
|
|
29
|
+
"homepage": "https://june.build",
|
|
30
|
+
"repository": {
|
|
31
|
+
"type": "git",
|
|
32
|
+
"url": "git+https://github.com/junebuild/june.git"
|
|
33
|
+
},
|
|
34
|
+
"license": "MIT"
|
|
35
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# __APP_NAME__ — guide for coding agents
|
|
2
|
+
|
|
3
|
+
You are working in a June app. June is dual-audience: routes serve humans (HTML)
|
|
4
|
+
and agents (JSON/markdown/manifests) from ONE definition. This file is the
|
|
5
|
+
agent-facing twin of README.md.
|
|
6
|
+
|
|
7
|
+
## Commands
|
|
8
|
+
- `bun run dev` — start the dev server (hot reload)
|
|
9
|
+
- From the framework repo root: `bun test src/june` (framework tests), `bun run typecheck`
|
|
10
|
+
|
|
11
|
+
## How to add things
|
|
12
|
+
|
|
13
|
+
**A page** — create `app/<path>/page.tsx` exporting a `route()`:
|
|
14
|
+
```tsx
|
|
15
|
+
import { route } from "june/route";
|
|
16
|
+
export default route({
|
|
17
|
+
async load(ctx) { return { /* data */ }; }, // ctx.params from [segments]
|
|
18
|
+
view: (data) => <main>…</main>, // HTML projection
|
|
19
|
+
json: (data) => data, // /path.json
|
|
20
|
+
md: (data) => "# …", // /path.md
|
|
21
|
+
});
|
|
22
|
+
```
|
|
23
|
+
Conventions per directory: `layout.tsx` (wraps), `loading.tsx` (Suspense
|
|
24
|
+
fallback for load()), `error.tsx` (recovery when load()/view() throws),
|
|
25
|
+
`not-found.tsx`, `[param]/`, `[...rest]/` (params.rest = "a/b/c"), `(group)/`
|
|
26
|
+
(invisible in URL), `_dir` (never routes).
|
|
27
|
+
|
|
28
|
+
**A schema change** — write a NEW file `db/migrations/000N_name.sql` (plain
|
|
29
|
+
SQLite SQL; DDL + backfill together; never edit an applied migration). It is
|
|
30
|
+
applied on next boot, in filename order. The SQL you write is exactly what runs
|
|
31
|
+
— there is no generator.
|
|
32
|
+
|
|
33
|
+
**A blog post / content** — drop a markdown file in `content/posts/`:
|
|
34
|
+
```md
|
|
35
|
+
---
|
|
36
|
+
title: My post
|
|
37
|
+
date: 2026-06-12
|
|
38
|
+
description: One line.
|
|
39
|
+
tags: [a, b]
|
|
40
|
+
---
|
|
41
|
+
Body in plain markdown.
|
|
42
|
+
```
|
|
43
|
+
It appears in `/blog` (list, from frontmatter) and `/blog/<filename>` (rendered).
|
|
44
|
+
The `.md` projection serves YOUR FILE verbatim — never edit rendered output;
|
|
45
|
+
edit the source file. Keep frontmatter flat (`key: value`, `[a, b]` lists only).
|
|
46
|
+
|
|
47
|
+
**An action/tool** — add a `defineAction({ id, description, input, run })` in
|
|
48
|
+
`app/actions.ts`. It becomes: a UI server action AND an MCP tool on `/mcp` AND
|
|
49
|
+
an entry in agent manifests. Write intent-shaped tools, not raw CRUD; return
|
|
50
|
+
high-signal shapes, not row dumps.
|
|
51
|
+
|
|
52
|
+
## Verify your work (oracles)
|
|
53
|
+
- `GET /llms.txt` lists every route; `GET <route>.json` shows the data the view uses.
|
|
54
|
+
- `POST /mcp` `{"method":"tools/list"}` shows your tools; `tools/call` executes one.
|
|
55
|
+
- Writes to a table auto-invalidate route caches tagged `table:<name>` — no manual wiring.
|
|
56
|
+
|
|
57
|
+
## Rules
|
|
58
|
+
- Don't bypass `route()` projections with ad-hoc endpoints — agents discover routes, not handlers.
|
|
59
|
+
- Don't edit applied migrations; add a new one.
|
|
60
|
+
- Keep `view` human-shaped and `json`/`md` agent-shaped; they share `load()` so they can't drift.
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# __APP_NAME__
|
|
2
|
+
|
|
3
|
+
A [June](../../README.md) app — dual-audience by default: every route serves
|
|
4
|
+
humans (streamed HTML) *and* agents (JSON / markdown / agent manifests) from one
|
|
5
|
+
definition.
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
bun run dev
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
| try | what you get |
|
|
12
|
+
| --- | --- |
|
|
13
|
+
| `/posts` | streamed HTML inside nested layouts (loading boundary while data loads) |
|
|
14
|
+
| `/posts.json` | the same route as JSON |
|
|
15
|
+
| `/posts/hello-june.md` | the same post as markdown |
|
|
16
|
+
| `/llms.txt` | the agent's map of this site (auto-derived) |
|
|
17
|
+
| `/mcp` | your server actions as MCP tools (`createPost`) |
|
|
18
|
+
|
|
19
|
+
## Structure
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
june.config.ts agent surface switches (everything defaults ON)
|
|
23
|
+
dev.ts boot: serve(app/) + register actions
|
|
24
|
+
db/
|
|
25
|
+
migrations/0001_*.sql SQL is the structural truth — applied in order on boot
|
|
26
|
+
app/
|
|
27
|
+
layout.tsx root layout (wraps every page)
|
|
28
|
+
page.tsx route(): view + md projections
|
|
29
|
+
not-found.tsx 404, rendered inside the layout chain
|
|
30
|
+
models/post.ts domain model (june/db sqlite) + migration runner
|
|
31
|
+
actions.ts server action == MCP tool (one definition)
|
|
32
|
+
posts/
|
|
33
|
+
loading.tsx Suspense fallback while load() runs
|
|
34
|
+
page.tsx route(): view + json + md, cached, tag-invalidated
|
|
35
|
+
[slug]/
|
|
36
|
+
page.tsx dynamic segment
|
|
37
|
+
error.tsx recovery UI when load() throws
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Conventions reference: `docs/app-router.md`. Data-layer philosophy (why
|
|
41
|
+
migrations are plain SQL): `docs/data-philosophy.md`.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// A server action that is ALSO an agent tool: the UI posts it, /mcp lists and
|
|
2
|
+
// executes it, the agent manifest advertises it. One definition, every surface.
|
|
3
|
+
import { defineAction } from "june/agent";
|
|
4
|
+
import { Posts } from "./models/post";
|
|
5
|
+
|
|
6
|
+
export const createPost = defineAction({
|
|
7
|
+
id: "createPost",
|
|
8
|
+
description: "Publish a new post with a unique slug, a title, and a markdown body.",
|
|
9
|
+
input: {
|
|
10
|
+
type: "object",
|
|
11
|
+
properties: {
|
|
12
|
+
slug: { type: "string", description: "URL slug, e.g. my-first-post" },
|
|
13
|
+
title: { type: "string", description: "Post title" },
|
|
14
|
+
body: { type: "string", description: "Markdown body" },
|
|
15
|
+
},
|
|
16
|
+
required: ["slug", "title"],
|
|
17
|
+
},
|
|
18
|
+
async run(input: { slug: string; title: string; body?: string }) {
|
|
19
|
+
// Writing `posts` auto-invalidates every cache entry tagged table:posts —
|
|
20
|
+
// the /posts list cache below drops without manual wiring.
|
|
21
|
+
return Posts.insert({
|
|
22
|
+
slug: input.slug,
|
|
23
|
+
title: input.title,
|
|
24
|
+
body: input.body ?? "",
|
|
25
|
+
published: 1,
|
|
26
|
+
});
|
|
27
|
+
},
|
|
28
|
+
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { route } from "june/route";
|
|
4
|
+
import { entry } from "june/content";
|
|
5
|
+
|
|
6
|
+
const POSTS = join(import.meta.dirname, "../../../content/posts");
|
|
7
|
+
|
|
8
|
+
export default route({
|
|
9
|
+
metadata: ({ post }) => ({
|
|
10
|
+
title: String(post.data.title ?? post.slug),
|
|
11
|
+
description: String(post.data.description ?? ""),
|
|
12
|
+
openGraph: { type: "article" },
|
|
13
|
+
}),
|
|
14
|
+
async load(ctx) {
|
|
15
|
+
const post = entry(POSTS, ctx.params.slug);
|
|
16
|
+
if (!post) throw new Error(`No post "${ctx.params.slug}"`);
|
|
17
|
+
return { post };
|
|
18
|
+
},
|
|
19
|
+
view: ({ post }) => (
|
|
20
|
+
<article>
|
|
21
|
+
<h1>{String(post.data.title)}</h1>
|
|
22
|
+
<p><small>{String(post.data.date)}</small></p>
|
|
23
|
+
<div dangerouslySetInnerHTML={{ __html: post.html }} />
|
|
24
|
+
<p><a href="/blog">← all posts</a></p>
|
|
25
|
+
</article>
|
|
26
|
+
),
|
|
27
|
+
// THE differentiator: the .md projection is the AUTHORED FILE, verbatim.
|
|
28
|
+
md: ({ post }) => post.original,
|
|
29
|
+
json: ({ post }) => ({ slug: post.slug, ...post.data, body: post.body }),
|
|
30
|
+
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { route } from "june/route";
|
|
4
|
+
import { collection } from "june/content";
|
|
5
|
+
|
|
6
|
+
const POSTS = join(import.meta.dirname, "../../content/posts");
|
|
7
|
+
|
|
8
|
+
export default route({
|
|
9
|
+
metadata: { title: "Blog", description: "Posts — rendered for humans, verbatim markdown for agents." },
|
|
10
|
+
load: () => ({ posts: collection(POSTS) }),
|
|
11
|
+
view: ({ posts }) => (
|
|
12
|
+
<main>
|
|
13
|
+
<h1>Blog</h1>
|
|
14
|
+
<ul>
|
|
15
|
+
{posts.map((p) => (
|
|
16
|
+
<li key={p.slug} style={{ marginBottom: 8 }}>
|
|
17
|
+
<a href={`/blog/${p.slug}`}>{String(p.data.title)}</a>
|
|
18
|
+
<br />
|
|
19
|
+
<small>{String(p.data.date)} — {String(p.data.description ?? "")}</small>
|
|
20
|
+
</li>
|
|
21
|
+
))}
|
|
22
|
+
</ul>
|
|
23
|
+
</main>
|
|
24
|
+
),
|
|
25
|
+
json: ({ posts }) => ({
|
|
26
|
+
posts: posts.map((p) => ({ slug: p.slug, ...p.data })),
|
|
27
|
+
}),
|
|
28
|
+
md: ({ posts }) =>
|
|
29
|
+
"# Blog\n\n" + posts.map((p) => `- [${p.data.title}](/blog/${p.slug}) — ${p.data.date}`).join("\n") + "\n",
|
|
30
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
|
|
3
|
+
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
|
4
|
+
return (
|
|
5
|
+
<div data-layout="root">
|
|
6
|
+
<nav style={{ display: "flex", gap: 16, padding: "14px 20px", borderBottom: "1px solid #e4e2da" }}>
|
|
7
|
+
<a href="/" style={{ fontWeight: 600 }}>__APP_NAME__</a>
|
|
8
|
+
<a href="/posts">Posts</a>
|
|
9
|
+
<a href="/llms.txt">llms.txt</a>
|
|
10
|
+
</nav>
|
|
11
|
+
{children}
|
|
12
|
+
</div>
|
|
13
|
+
);
|
|
14
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// Domain model — a model is a domain concern, not a route concern. Both
|
|
2
|
+
// /posts and /posts/[slug] import it.
|
|
3
|
+
import { readdirSync, readFileSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { sql, type Table } from "june/db";
|
|
6
|
+
|
|
7
|
+
export type Post = {
|
|
8
|
+
id: number;
|
|
9
|
+
slug: string;
|
|
10
|
+
title: string;
|
|
11
|
+
body: string;
|
|
12
|
+
published: number;
|
|
13
|
+
created_at: string;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const root = join(import.meta.dirname, "../..");
|
|
17
|
+
const db = sql.load(join(root, "db/app.db"));
|
|
18
|
+
|
|
19
|
+
// db/migrations/*.sql is the structural truth (docs/data-philosophy.md):
|
|
20
|
+
// applied in filename order, tracked in _migrations. No diff engine — the SQL
|
|
21
|
+
// you read is the SQL that ran.
|
|
22
|
+
const dir = join(root, "db/migrations");
|
|
23
|
+
db.db.run("CREATE TABLE IF NOT EXISTS _migrations (name TEXT PRIMARY KEY)");
|
|
24
|
+
for (const f of readdirSync(dir).filter((f) => f.endsWith(".sql")).sort()) {
|
|
25
|
+
const applied = db.db.query("SELECT 1 FROM _migrations WHERE name = ?").get(f);
|
|
26
|
+
if (applied) continue;
|
|
27
|
+
db.db.transaction(() => {
|
|
28
|
+
// exec(), not run(): migration files are MULTI-statement (DDL + seed DML);
|
|
29
|
+
// prepare()-based run executes only the first statement on node:sqlite.
|
|
30
|
+
db.db.exec(readFileSync(join(dir, f), "utf8"));
|
|
31
|
+
db.db.run("INSERT INTO _migrations (name) VALUES (?)", [f]);
|
|
32
|
+
})();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const Posts = db.posts as Table<Post>;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
|
|
3
|
+
export default function NotFound({ pathname }: { pathname: string }) {
|
|
4
|
+
return (
|
|
5
|
+
<main data-boundary="not-found">
|
|
6
|
+
<h1>Not found</h1>
|
|
7
|
+
<p>
|
|
8
|
+
Nothing lives at <code>{pathname}</code>. Try <a href="/posts">/posts</a>.
|
|
9
|
+
</p>
|
|
10
|
+
</main>
|
|
11
|
+
);
|
|
12
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { route } from "june/route";
|
|
3
|
+
|
|
4
|
+
export default route({
|
|
5
|
+
view: () => (
|
|
6
|
+
<main>
|
|
7
|
+
<h1>__APP_NAME__</h1>
|
|
8
|
+
<p>A June app. Humans get this page; agents get the same routes as data:</p>
|
|
9
|
+
<ul>
|
|
10
|
+
<li><code>/posts.json</code> — the list as JSON</li>
|
|
11
|
+
<li><code>/posts/hello-june.md</code> — a post as markdown</li>
|
|
12
|
+
<li><code>/llms.txt</code> · <code>/mcp</code> — discovery + tools</li>
|
|
13
|
+
</ul>
|
|
14
|
+
</main>
|
|
15
|
+
),
|
|
16
|
+
md: () =>
|
|
17
|
+
"# __APP_NAME__\n\nA June app. Routes speak HTML to humans and JSON/markdown/manifests to agents.\n",
|
|
18
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
|
|
3
|
+
export default function PostError({ error }: { error: unknown }) {
|
|
4
|
+
return (
|
|
5
|
+
<main data-boundary="error">
|
|
6
|
+
<h1>Could not load this post</h1>
|
|
7
|
+
<p>
|
|
8
|
+
<code>{String((error as Error)?.message ?? error)}</code>
|
|
9
|
+
</p>
|
|
10
|
+
<p>
|
|
11
|
+
<a href="/posts">Back to all posts</a>
|
|
12
|
+
</p>
|
|
13
|
+
</main>
|
|
14
|
+
);
|
|
15
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { route } from "june/route";
|
|
3
|
+
import { Posts } from "../../models/post";
|
|
4
|
+
|
|
5
|
+
export default route({
|
|
6
|
+
metadata: ({ post }) => ({ title: post.title, openGraph: { type: "article" } }),
|
|
7
|
+
async load(ctx) {
|
|
8
|
+
const post = await Posts.findBy({ slug: ctx.params.slug, published: 1 });
|
|
9
|
+
if (!post) throw new Error(`No post named "${ctx.params.slug}"`);
|
|
10
|
+
return { post };
|
|
11
|
+
},
|
|
12
|
+
view: ({ post }) => (
|
|
13
|
+
<article>
|
|
14
|
+
<h1>{post.title}</h1>
|
|
15
|
+
<p>{post.body}</p>
|
|
16
|
+
<p>
|
|
17
|
+
<small>{post.created_at}</small> · <a href="/posts">all posts</a>
|
|
18
|
+
</p>
|
|
19
|
+
</article>
|
|
20
|
+
),
|
|
21
|
+
json: ({ post }) => post,
|
|
22
|
+
md: ({ post }) => `# ${post.title}\n\n${post.body}\n`,
|
|
23
|
+
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { route } from "june/route";
|
|
3
|
+
import { Posts } from "../models/post";
|
|
4
|
+
|
|
5
|
+
export default route({
|
|
6
|
+
metadata: { title: "Posts", description: "All published posts." },
|
|
7
|
+
async load() {
|
|
8
|
+
return { posts: await Posts.where({ published: 1 }).all() };
|
|
9
|
+
},
|
|
10
|
+
view: ({ posts }) => (
|
|
11
|
+
<main>
|
|
12
|
+
<h1>Posts</h1>
|
|
13
|
+
<ul>
|
|
14
|
+
{posts.map((p) => (
|
|
15
|
+
<li key={p.id}>
|
|
16
|
+
<a href={`/posts/${p.slug}`}>{p.title}</a>
|
|
17
|
+
</li>
|
|
18
|
+
))}
|
|
19
|
+
</ul>
|
|
20
|
+
</main>
|
|
21
|
+
),
|
|
22
|
+
json: ({ posts }) => ({ posts: posts.map(({ id, slug, title }) => ({ id, slug, title })) }),
|
|
23
|
+
md: ({ posts }) => "# Posts\n\n" + posts.map((p) => `- [${p.title}](/posts/${p.slug})`).join("\n") + "\n",
|
|
24
|
+
// Cached until a write to `posts` invalidates the tag (e.g. the createPost tool).
|
|
25
|
+
cache: { ttl: 60, tags: ["table:posts"] },
|
|
26
|
+
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Writing with agents
|
|
3
|
+
date: 2026-06-09
|
|
4
|
+
description: Notes on a blog whose first reader is a crawler with taste.
|
|
5
|
+
tags: [agents]
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
Half the readers of this blog will be machines. June treats that as a feature:
|
|
9
|
+
every post advertises itself in `llms.txt` and serves clean markdown.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Content as files, audiences as projections
|
|
3
|
+
date: 2026-06-10
|
|
4
|
+
description: One markdown file. Humans get rendered HTML; agents get the file itself.
|
|
5
|
+
tags: [content, dual-audience]
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
This post is a plain markdown file in `content/posts/`.
|
|
9
|
+
|
|
10
|
+
- You are reading the **rendered HTML** projection.
|
|
11
|
+
- An agent fetching this URL with `.md` gets the **authored file, verbatim** —
|
|
12
|
+
frontmatter included. No lossy HTML-to-markdown conversion.
|
|
13
|
+
|
|
14
|
+
```ts
|
|
15
|
+
// the whole route:
|
|
16
|
+
const post = entry(POSTS, ctx.params.slug);
|
|
17
|
+
```
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
-- SQL migrations are the structural truth (see docs/data-philosophy.md).
|
|
2
|
+
-- DDL + seed (DML) co-located, applied in order, tracked in _migrations.
|
|
3
|
+
CREATE TABLE posts (
|
|
4
|
+
id INTEGER PRIMARY KEY,
|
|
5
|
+
slug TEXT NOT NULL UNIQUE,
|
|
6
|
+
title TEXT NOT NULL,
|
|
7
|
+
body TEXT NOT NULL DEFAULT '',
|
|
8
|
+
published INTEGER NOT NULL DEFAULT 0,
|
|
9
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
10
|
+
);
|
|
11
|
+
|
|
12
|
+
INSERT INTO posts (slug, title, body, published) VALUES
|
|
13
|
+
('hello-june', 'Hello, June', 'This post was created by db/migrations/0001 — plain SQL is the structural truth.', 1),
|
|
14
|
+
('dual-audience', 'Dual-audience by default', 'Every route here also speaks JSON, markdown, and agent manifests. Try /posts.json or /posts/hello-june.md.', 1),
|
|
15
|
+
('draft-notes', 'Draft notes', 'Unpublished drafts stay out of every list and projection.', 0);
|
package/template/dev.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { serve } from "june/server";
|
|
3
|
+
import config from "./june.config";
|
|
4
|
+
import "./app/actions"; // register actions (= agent tools) before the first request
|
|
5
|
+
|
|
6
|
+
const server = serve({
|
|
7
|
+
appDir: join(import.meta.dirname, "app"),
|
|
8
|
+
pageConvention: true,
|
|
9
|
+
agent: config.agent,
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
console.log(`__APP_NAME__ on http://localhost:${server.port}`);
|
|
13
|
+
console.log("human: / /posts /posts/hello-june");
|
|
14
|
+
console.log("agent: /posts.json /posts/hello-june.md /llms.txt /mcp");
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { defineJune } from "june/config";
|
|
2
|
+
|
|
3
|
+
// Dual-audience is ON by default — this file exists to turn things off, not on.
|
|
4
|
+
// agent.discovery llms.txt, sitemap.xml, api-catalog, Link header
|
|
5
|
+
// agent.mcp the /mcp endpoint (your actions as tools)
|
|
6
|
+
// agent.webmcp in-page tools for browser-resident agents
|
|
7
|
+
export default defineJune({
|
|
8
|
+
// Site-wide <head> defaults; per-route `metadata` merges over these.
|
|
9
|
+
site: { name: "__APP_NAME__", titleTemplate: "%s — __APP_NAME__" },
|
|
10
|
+
agent: { enabled: true, discovery: true, mcp: true, webmcp: true },
|
|
11
|
+
// Hover-intent speculation (these ARE the defaults — shown so you can see
|
|
12
|
+
// and change them). Agent surfaces (*.md/*.json/*.txt/*.xml//mcp) are always
|
|
13
|
+
// excluded. Heavy pages? prerender: "conservative" (mousedown) or false.
|
|
14
|
+
speculation: { prerender: "moderate", prefetch: "moderate", exclude: [] },
|
|
15
|
+
// Cross-document View Transitions: MPA navigations cross-fade, zero JS.
|
|
16
|
+
// (Also a default — visible here so you can turn it off.)
|
|
17
|
+
viewTransitions: true,
|
|
18
|
+
// Early Hints (RFC 8297): list critical assets to preload while the server
|
|
19
|
+
// renders, e.g. "</fonts/inter.woff2>; rel=preload; as=font; crossorigin".
|
|
20
|
+
// CF upgrades the Link header to a real 103; June's Node host emits 103 itself.
|
|
21
|
+
earlyHints: [],
|
|
22
|
+
});
|