mintiljs 0.1.0 → 0.1.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 CHANGED
@@ -5,8 +5,9 @@
5
5
  MintilJS is a full-stack framework built on **Bun**, **Hono**, and **React 19**. Drop files in `pages/` and they become SSR routes. Add `api/` files for JSON endpoints. Throw in `islands/` for partial hydration. All with zero configuration.
6
6
 
7
7
  ```sh
8
- bun create mintiljs my-app
8
+ bun create mintil my-app
9
9
  cd my-app
10
+ bun install
10
11
  bun run mintil dev
11
12
  ```
12
13
 
@@ -28,6 +29,15 @@ bun run mintil dev
28
29
 
29
30
  ## Install
30
31
 
32
+ ```sh
33
+ bun create mintil my-app
34
+ cd my-app
35
+ bun install
36
+ bun run mintil dev
37
+ ```
38
+
39
+ Or add to an existing project:
40
+
31
41
  ```sh
32
42
  bun add mintiljs
33
43
  ```
@@ -113,8 +123,9 @@ import type { MintilConfig } from "mintiljs";
113
123
 
114
124
  export default {
115
125
  port: 3456,
116
- host: false, // true = bind to 0.0.0.0
117
- mode: "development", // "development" | "production"
126
+ host: false, // true = bind to 0.0.0.0
127
+ mode: "development", // "development" | "production"
128
+ assetsPath: "/assets", // prefix for public/ files; "/" to serve at root
118
129
  plugins: [],
119
130
  } satisfies MintilConfig;
120
131
  ```
@@ -123,7 +134,7 @@ export default {
123
134
 
124
135
  | Command | Description |
125
136
  |---|---|
126
- | `mintil init <name>` | Scaffold a new project |
137
+ | `mintil init <name>` | Scaffold a new project (use `.` for current dir) |
127
138
  | `mintil dev` | Dev mode with auto-reload |
128
139
  | `mintil start` | Production mode |
129
140
  | `mintil g page <n>` | Scaffold a page |
@@ -135,16 +146,49 @@ export default {
135
146
  ### Auth (`mintiljs/auth`)
136
147
 
137
148
  ```ts
149
+ // api/login.ts
138
150
  import { createAuthMiddleware, InMemorySessionStore } from "mintiljs/auth";
139
151
 
140
- const auth = createAuthMiddleware({
141
- jwt: { secret: process.env.JWT_SECRET!, expiresIn: "1h" },
142
- session: { store: new InMemorySessionStore(), maxAge: 86400, cookieName: "session", cookiePath: "/" },
152
+ const JWT_SECRET = process.env.JWT_SECRET || "dev-secret";
153
+ const store = new InMemorySessionStore();
154
+
155
+ export const auth = createAuthMiddleware({
156
+ jwt: { secret: JWT_SECRET, expiresIn: "1h" },
157
+ session: { store, maxAge: 86400, cookieName: "session", cookiePath: "/" },
143
158
  });
144
159
 
145
- const token = await auth.signJWT({ sub: userId });
146
- const payload = await auth.verifyJWT(token);
147
- app.get("/api/admin", auth.requireAuth, handler);
160
+ // POST /api/login sign JWT and return it
161
+ import type { ApiMethodHandler } from "mintiljs";
162
+
163
+ export const POST: ApiMethodHandler = async (request, response) => {
164
+ const { username, password } = await request.json();
165
+ if (username !== "admin" || password !== "123456") {
166
+ return response.json({ error: "Invalid credentials" }, 401);
167
+ }
168
+ const token = await auth.signJWT({ sub: username, userId: username, role: "admin" });
169
+ return response.json({ token });
170
+ };
171
+ ```
172
+
173
+ ```ts
174
+ // api/admin/middleware.ts — protect all /api/admin/* routes
175
+ import { auth } from "../login";
176
+
177
+ export default auth.requireAuth;
178
+ ```
179
+
180
+ ```ts
181
+ // api/me.ts — use the verified token
182
+ import type { ApiMethodHandler } from "mintiljs";
183
+ import { auth } from "./login";
184
+
185
+ export const GET: ApiMethodHandler = async (request, response) => {
186
+ const token = request.header("Authorization")?.slice(7);
187
+ const payload = token ? await auth.verifyJWT(token) : null;
188
+ return payload
189
+ ? response.json({ user: { id: payload.userId, name: payload.sub } })
190
+ : response.json({ error: "Unauthorized" }, 401);
191
+ };
148
192
  ```
149
193
 
150
194
  ### i18n (`mintiljs/i18n`)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mintiljs",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Minimal SSR web framework for Bun — React pages rendered on the server, zero client JS unless you ask for it",
5
5
  "type": "module",
6
6
  "module": "index.ts",
package/src/cli/cli.ts CHANGED
@@ -38,13 +38,17 @@ function showHelp() {
38
38
  console.log(`Usage: mintil <command>
39
39
 
40
40
  Commands:
41
- init <name> Create a new Mintil project in the <name> directory
42
- dev Run the project in development mode (forces mode: development)
41
+ init <name> Create a new Mintil project (use "." for current dir)
42
+ dev Run the project in development mode
43
43
  start Run the project (uses config mode or defaults to production)
44
44
  generate <t> <n> Scaffold a page, api, component, or island (alias: g)
45
45
  help Show this help message
46
46
 
47
- Run \`mintil generate help\` for scaffold type options.
47
+ Quick start:
48
+ bun create mintil my-app
49
+ cd my-app
50
+ bun install
51
+ bun run mintil dev
48
52
  `);
49
53
  }
50
54
 
@@ -54,15 +58,18 @@ async function initProject(name?: string) {
54
58
  process.exit(1);
55
59
  }
56
60
 
57
- const target = path.resolve(process.cwd(), name);
61
+ const useCwd = name === ".";
62
+ const target = useCwd ? process.cwd() : path.resolve(process.cwd(), name);
58
63
  const template = path.resolve(import.meta.dir, "..", "..", "templates", "default");
59
64
 
60
65
  try {
61
66
  await fs.access(target);
62
- console.error(`Directory already exists: ${target}`);
63
- process.exit(1);
67
+ if (!useCwd) {
68
+ console.error(`Directory already exists: ${target}`);
69
+ process.exit(1);
70
+ }
64
71
  } catch {
65
- // expected: directory does not exist
72
+ // expected: directory does not exist (unless useCwd)
66
73
  }
67
74
 
68
75
  await fs.cp(template, target, { recursive: true });
@@ -71,14 +78,16 @@ async function initProject(name?: string) {
71
78
  const pkgPath = path.join(target, "package.json");
72
79
  try {
73
80
  const pkg = JSON.parse(await fs.readFile(pkgPath, "utf-8")) as Record<string, unknown>;
74
- pkg.name = name;
81
+ pkg.name = path.basename(target);
75
82
  await fs.writeFile(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
76
83
  } catch {
77
84
  // ignore if no package.json
78
85
  }
79
86
 
80
87
  console.log(`Created Mintil project at ${target}`);
81
- console.log(`\nNext steps:\n cd ${name}\n bun install\n mintil dev`);
88
+ console.log(`\nNext steps:`);
89
+ if (!useCwd) console.log(` cd ${name}`);
90
+ console.log(` bun install\n mintil dev`);
82
91
  }
83
92
 
84
93
  async function runProject(command: "dev" | "start") {
@@ -5,12 +5,15 @@ export function resolveConfig(root: string, userConfig?: MintilConfig): Resolved
5
5
  const envPort = process.env.PORT ? Number(process.env.PORT) : undefined;
6
6
  const envMode = process.env.NODE_ENV === "production" ? "production" : "development";
7
7
 
8
+ const assetsPath = userConfig?.assetsPath ?? "/";
9
+
8
10
  return {
9
11
  root,
10
12
  port: userConfig?.port ?? envPort ?? 3456,
11
13
  hosts: userConfig?.hosts ?? [],
12
14
  mode: userConfig?.mode ?? (envMode as ResolvedConfig["mode"]),
13
15
  host: userConfig?.host ?? false,
16
+ assetsPath: assetsPath.startsWith("/") ? assetsPath : `/${assetsPath}`,
14
17
  plugins: userConfig?.plugins ?? [],
15
18
  };
16
19
  }
@@ -2,7 +2,7 @@ import path from "node:path";
2
2
  import React from "react";
3
3
  import type { Hono } from "hono";
4
4
  import type { Context } from "hono";
5
- import { renderPage } from "../render/ssr.tsx";
5
+ import { renderPage, renderClientShell } from "../render/ssr.tsx";
6
6
  import { tryStreamPage } from "../render/stream.tsx";
7
7
  import { resetIslands, getUsedIslands, getIslandDef } from "../render/island.tsx";
8
8
  import {
@@ -125,7 +125,7 @@ export async function registerPageRoutes(
125
125
 
126
126
  let clientBundle: string | null = null;
127
127
  let clientBundleName: string | null = null;
128
- if (page.useClient && frameworkJs) {
128
+ if (page.useClient && !page.useServer && frameworkJs) {
129
129
  const js = await buildClientBundle(page.file, page.route, { mode: resolved.mode });
130
130
  if (js) {
131
131
  const bundleName = routeToBundleName(page.route);
@@ -152,7 +152,7 @@ export async function registerPageRoutes(
152
152
  const props = pagePropsFromContext(c);
153
153
 
154
154
  let extraProps: Record<string, any> | undefined;
155
- if (page.getServerSideProps) {
155
+ if (page.getServerSideProps && !page.useServer) {
156
156
  try {
157
157
  const result = await page.getServerSideProps({
158
158
  params: props.params,
@@ -180,9 +180,27 @@ export async function registerPageRoutes(
180
180
  extraProps,
181
181
  };
182
182
 
183
+ if (page.useClient && clientBundle) {
184
+ // useClient pages skip SSR of the React tree (hooks like useState
185
+ // don't work server-side). Render just the layout shell with an
186
+ // empty root div — the client bundle hydrates the full component.
187
+ const html = await renderClientShell(pageOpts);
188
+ return c.html(html);
189
+ }
190
+
191
+ if (page.useServer) {
192
+ // useServer pages are async-first — skip island detection,
193
+ // skip getServerSideProps (component fetches its own data),
194
+ // go straight to streaming (supports Suspense/async components).
195
+ const streamResponse = await tryStreamPage(page.component, pageOpts);
196
+ if (streamResponse) return streamResponse;
197
+ // Fallback to sync render (will show error boundary if async)
198
+ const html = await renderPage(page.component, pageOpts);
199
+ return c.html(html);
200
+ }
201
+
183
202
  // Quick sync pass to detect used islands
184
203
  resetIslands();
185
- const React = await import("react");
186
204
  const { renderToString } = await import("react-dom/server");
187
205
  const quickElement = buildQuickElement(page.component, pageOpts, layout);
188
206
  try { renderToString(quickElement); } catch {}
@@ -34,19 +34,24 @@ export async function createMintilApp(
34
34
  return c.text(css, 200, { "Content-Type": "text/css" });
35
35
  });
36
36
 
37
- app.get("/assets/*", async (c) => {
38
- const subpath = c.req.path.replace(/^\/assets\//, "");
37
+ // Serve static files from public/ works as a pass-through middleware so
38
+ // page/API routes still resolve when the file doesn't exist.
39
+ const prefix = resolved.assetsPath === "/" ? "" : resolved.assetsPath.replace(/\/+$/, "");
40
+ app.use("*", async (c, next) => {
41
+ if (c.req.method !== "GET") return next();
42
+ const reqPath = c.req.path;
43
+ // Skip internal routes
44
+ if (reqPath.startsWith("/_mintil") || reqPath === "/styles.css") return next();
45
+ // If a prefix is configured, only handle paths under it
46
+ if (prefix && !reqPath.startsWith(prefix)) return next();
47
+ const subpath = prefix ? reqPath.slice(prefix.length).replace(/^\//, "") : reqPath.replace(/^\//, "");
39
48
  const filePath = path.join(resolved.root, "public", subpath);
40
- if (!filePath.startsWith(path.join(resolved.root, "public"))) {
41
- return c.notFound();
42
- }
49
+ if (!filePath.startsWith(path.join(resolved.root, "public"))) return next();
43
50
  try {
44
51
  const file = Bun.file(filePath);
45
- if (!(await file.exists())) return c.notFound();
46
- return new Response(file);
47
- } catch {
48
- return c.notFound();
49
- }
52
+ if (await file.exists()) return new Response(file);
53
+ } catch { /* fall through */ }
54
+ return next();
50
55
  });
51
56
 
52
57
  // Auto-register i18n API if i18n/messages directory exists
@@ -129,3 +134,7 @@ export async function start(config?: MintilConfig): Promise<ReturnType<typeof Bu
129
134
  export function resetStarted() {
130
135
  started = false;
131
136
  }
137
+
138
+ function escapeRegex(str: string): string {
139
+ return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
140
+ }
@@ -196,3 +196,42 @@ function buildInnerPage(Page: PageComponent, pageProps: Record<string, any>) {
196
196
  React.createElement(Page, pageProps),
197
197
  );
198
198
  }
199
+
200
+ function buildEmptyShell() {
201
+ return React.createElement("div", { id: "__mintil-root" });
202
+ }
203
+
204
+ /**
205
+ * Render a shell HTML for `useClient` pages — wraps the layout around an
206
+ * empty root div, then lets the client bundle hydrate the full component.
207
+ * Avoids SSR errors from hooks (useState, useEffect, etc.) that require
208
+ * a client-side React dispatcher.
209
+ */
210
+ export async function renderClientShell(
211
+ opts: RenderOptions,
212
+ ): Promise<string> {
213
+ const pageProps = { params: opts.params, searchParams: opts.searchParams, ...opts.extraProps };
214
+
215
+ const layout = opts.layout ?? (await loadLayout(opts.root, opts.nonce));
216
+ const css = await processCss(opts.root, opts.mode);
217
+
218
+ let content: string;
219
+ try {
220
+ const pageElement = layout
221
+ ? React.createElement(layout, { children: buildEmptyShell() })
222
+ : buildEmptyShell();
223
+ content = renderToString(pageElement);
224
+ } catch (err) {
225
+ const error = err instanceof Error ? err : new Error(String(err));
226
+ console.error(`[mintil] client-shell render error for ${opts.route}:`, error.message);
227
+ const fallback = renderToString(
228
+ layout
229
+ ? React.createElement(layout, { children: ErrorFallback({ error }) })
230
+ : ErrorFallback({ error }),
231
+ );
232
+ content = fallback;
233
+ }
234
+
235
+ const extra = buildExtraScripts(opts, pageProps);
236
+ return buildHtmlShell(content, extra, css, opts.route);
237
+ }
@@ -10,6 +10,7 @@ export interface PageRoute {
10
10
  mod: any;
11
11
  component: PageComponent;
12
12
  useClient: boolean;
13
+ useServer: boolean;
13
14
  getServerSideProps?: GetServerSideProps;
14
15
  }
15
16
 
@@ -36,8 +37,9 @@ export async function collectPageRoutes(root: string, nonce?: string): Promise<P
36
37
  const component = mod.default ?? mod.Page ?? null;
37
38
  if (typeof component !== "function") continue;
38
39
  const useClient = mod.useClient === true;
40
+ const useServer = mod.useServer === true;
39
41
  const getServerSideProps = typeof mod.getServerSideProps === "function" ? mod.getServerSideProps.bind(null) : undefined;
40
- routes.push({ file, route, mod, component, useClient, getServerSideProps });
42
+ routes.push({ file, route, mod, component, useClient, useServer, getServerSideProps });
41
43
  }
42
44
 
43
45
  routes.sort((a, b) => sortRoute(a.route, b.route));
@@ -18,6 +18,8 @@ export interface MintilConfig {
18
18
  mode?: "development" | "production";
19
19
  /** Whether to bind to all interfaces (`0.0.0.0`). Defaults to `false` (loopback only). */
20
20
  host?: boolean;
21
+ /** URL path prefix for static assets from `public/`. Defaults to `/assets`. Set to `"/"` to serve at root. */
22
+ assetsPath?: string;
21
23
  /** Plugins to register. Each plugin's `setup` receives the Hono app and resolved config. */
22
24
  plugins?: import("./plugin.ts").MintilPlugin[];
23
25
  }
package/src/types/page.ts CHANGED
@@ -17,8 +17,11 @@ export interface PageProps {
17
17
  /**
18
18
  * A page component. Must be the default export of a file inside `pages/`.
19
19
  * Receives {@link PageProps} (plus any extra props from `getServerSideProps`).
20
+ *
21
+ * When `useServer` is `true`, the component can be `async` and fetch data directly
22
+ * without `getServerSideProps`. React 19's streaming SSR handles Suspense natively.
20
23
  */
21
- export type PageComponent = (props: any) => import("react").JSX.Element;
24
+ export type PageComponent = (props: any) => import("react").JSX.Element | Promise<import("react").JSX.Element>;
22
25
 
23
26
  /**
24
27
  * Context object passed to `getServerSideProps`.
@@ -0,0 +1,10 @@
1
+ import type { ApiHandler } from "mintiljs";
2
+
3
+ const counter: ApiHandler = (c) => {
4
+ const count = parseInt(c.req.query("count") ?? "0", 10);
5
+ const action = c.req.query("action") ?? "inc";
6
+ const next = action === "inc" ? count + 1 : Math.max(0, count - 1);
7
+ return c.json({ count: next });
8
+ };
9
+
10
+ export default counter;
@@ -0,0 +1,8 @@
1
+ import type { MintilMiddleware } from "mintiljs";
2
+
3
+ const middleware: MintilMiddleware = async (c, next) => {
4
+ c.res.headers.set("X-API-Time", Date.now().toString());
5
+ await next();
6
+ };
7
+
8
+ export default middleware;
@@ -14,6 +14,12 @@ export default function BaseLayout({ children }: { children: React.ReactNode })
14
14
  <nav className="mx-auto flex max-w-4xl items-center gap-6 px-4 py-4">
15
15
  <a href="/" className="text-sm font-medium text-blue-600 hover:text-blue-800">Home</a>
16
16
  <a href="/about" className="text-sm font-medium text-blue-600 hover:text-blue-800">About</a>
17
+ <a href="/counter" className="text-sm font-medium text-blue-600 hover:text-blue-800">Counter</a>
18
+ <a href="/ssr-counter" className="text-sm font-medium text-blue-600 hover:text-blue-800">SSR</a>
19
+ <a href="/island-demo" className="text-sm font-medium text-blue-600 hover:text-blue-800">Islands</a>
20
+ <a href="/server-data" className="text-sm font-medium text-blue-600 hover:text-blue-800">Server</a>
21
+ <a href="/blog" className="text-sm font-medium text-blue-600 hover:text-blue-800">Blog</a>
22
+ <a href="/i18n-test" className="text-sm font-medium text-blue-600 hover:text-blue-800">i18n</a>
17
23
  </nav>
18
24
  </header>
19
25
  <main className="mx-auto w-full max-w-4xl flex-1 px-4 py-10">{children}</main>
@@ -0,0 +1,12 @@
1
+ {
2
+ "greeting": "Hello",
3
+ "nav.home": "Home",
4
+ "nav.about": "About",
5
+ "nav.counter": "Counter",
6
+ "nav.ssr": "SSR Counter",
7
+ "nav.i18n": "i18n",
8
+ "nav.blog": "Blog",
9
+ "welcome": "Welcome to {site}!",
10
+ "farewell": "Goodbye",
11
+ "builtWith": "Built with MintilJs"
12
+ }
@@ -0,0 +1,34 @@
1
+ import React from "react";
2
+
3
+ interface CounterProps {
4
+ initial?: number;
5
+ }
6
+
7
+ export default function Counter({ initial = 0 }: CounterProps) {
8
+ const [count, setCount] = React.useState(initial);
9
+ return (
10
+ <div className="flex items-center gap-4">
11
+ <button
12
+ onClick={() => setCount((c) => c - 1)}
13
+ className="rounded-lg bg-gray-200 px-4 py-2 text-lg font-semibold hover:bg-gray-300"
14
+ >
15
+ -1
16
+ </button>
17
+ <span className="min-w-[3rem] text-center text-2xl font-mono font-bold tabular-nums">
18
+ {count}
19
+ </span>
20
+ <button
21
+ onClick={() => setCount((c) => c + 1)}
22
+ className="rounded-lg bg-blue-600 px-4 py-2 text-lg font-semibold text-white hover:bg-blue-700"
23
+ >
24
+ +1
25
+ </button>
26
+ <button
27
+ onClick={() => setCount(0)}
28
+ className="text-sm text-gray-500 underline hover:text-gray-700"
29
+ >
30
+ reset
31
+ </button>
32
+ </div>
33
+ );
34
+ }
@@ -0,0 +1,8 @@
1
+ import type { MintilMiddleware } from "mintiljs";
2
+
3
+ const middleware: MintilMiddleware = async (c, next) => {
4
+ console.log(`[middleware] ${c.req.method} ${c.req.path}`);
5
+ await next();
6
+ };
7
+
8
+ export default middleware;
@@ -1,6 +1,5 @@
1
1
  {
2
2
  "name": "my-mintil-app",
3
- "module": "index.ts",
4
3
  "type": "module",
5
4
  "scripts": {
6
5
  "dev": "mintil dev",
@@ -9,9 +8,11 @@
9
8
  "peerDependencies": {
10
9
  "typescript": "^5"
11
10
  },
12
- "dependencies": {
13
- "mintiljs": "*",
11
+ "optionalDependencies": {
14
12
  "postcss": "^8",
15
13
  "@tailwindcss/postcss": "^4"
14
+ },
15
+ "dependencies": {
16
+ "mintiljs": "*"
16
17
  }
17
18
  }
@@ -8,6 +8,18 @@ export default function AboutPage() {
8
8
  MintilJs is a minimal SSR framework that runs on Bun. Pages are rendered with React on the server
9
9
  and streamed as HTML — no client-side JavaScript required.
10
10
  </p>
11
+ <div className="mt-8 space-y-4">
12
+ <h2 className="text-xl font-semibold">Features</h2>
13
+ <ul className="list-disc pl-6 space-y-2 text-gray-600">
14
+ <li>File-based routing — <code className="rounded bg-gray-100 px-1.5 py-0.5 text-sm font-mono text-blue-700">pages/</code> for SSR, <code className="rounded bg-gray-100 px-1.5 py-0.5 text-sm font-mono text-blue-700">api/</code> for JSON</li>
15
+ <li>Zero JS in browser unless you opt in with <code className="rounded bg-gray-100 px-1.5 py-0.5 text-sm font-mono text-blue-700">useClient</code></li>
16
+ <li>Islands — independent interactive components <code className="rounded bg-gray-100 px-1.5 py-0.5 text-sm font-mono text-blue-700">islands/</code></li>
17
+ <li>Internationalization via <code className="rounded bg-gray-100 px-1.5 py-0.5 text-sm font-mono text-blue-700">mintiljs/i18n</code></li>
18
+ <li>Auth module via <code className="rounded bg-gray-100 px-1.5 py-0.5 text-sm font-mono text-blue-700">mintiljs/auth</code></li>
19
+ <li>Hierarchical middleware — global, API, scoped</li>
20
+ <li>Tailwind v4 via PostCSS</li>
21
+ </ul>
22
+ </div>
11
23
  </section>
12
24
  );
13
25
  }
@@ -0,0 +1,28 @@
1
+ import React from "react";
2
+
3
+ const posts = [
4
+ { slug: "hello-world", title: "Hello World" },
5
+ { slug: "getting-started", title: "Getting Started with MintilJS" },
6
+ { slug: "ssr-vs-csr", title: "SSR vs CSR" },
7
+ ];
8
+
9
+ export default function BlogIndex() {
10
+ return (
11
+ <div>
12
+ <h1 className="mb-6 text-3xl font-bold tracking-tight">Blog</h1>
13
+ <ul className="space-y-4">
14
+ {posts.map((post) => (
15
+ <li key={post.slug}>
16
+ <a
17
+ href={`/blog/${post.slug}`}
18
+ className="block rounded-lg border border-gray-200 bg-white p-4 transition hover:border-blue-300 hover:shadow-sm"
19
+ >
20
+ <h2 className="text-lg font-semibold text-blue-600">{post.title}</h2>
21
+ <p className="mt-1 text-sm text-gray-500">/{post.slug}</p>
22
+ </a>
23
+ </li>
24
+ ))}
25
+ </ul>
26
+ </div>
27
+ );
28
+ }
@@ -0,0 +1,12 @@
1
+ import React from "react";
2
+
3
+ export default function BlogLayout({ children }: { children: React.ReactNode }) {
4
+ return (
5
+ <div className="mx-auto max-w-2xl">
6
+ <nav className="mb-8 flex items-center gap-4 text-sm text-gray-500">
7
+ <a href="/blog" className="font-medium text-blue-600 hover:text-blue-800">All Posts</a>
8
+ </nav>
9
+ {children}
10
+ </div>
11
+ );
12
+ }
@@ -0,0 +1,26 @@
1
+ import React from "react";
2
+ import { getMessages, t, format } from "mintiljs/i18n";
3
+ import type { GetServerSideProps } from "mintiljs";
4
+
5
+ export const getServerSideProps: GetServerSideProps = async (ctx) => {
6
+ const locale = (ctx.searchParams?.lang as string) || "en";
7
+ const messages = await getMessages(locale);
8
+ return { props: { locale, messages } };
9
+ };
10
+
11
+ export default function I18nPage({ locale, messages }: { locale: string; messages: Record<string, string> }) {
12
+ return (
13
+ <div>
14
+ <h1 className="mb-6 text-3xl font-bold tracking-tight">i18n Example</h1>
15
+ <p className="mb-2 text-gray-600">Locale: <code className="rounded bg-gray-100 px-1.5 py-0.5 text-sm font-mono text-blue-700">{locale}</code></p>
16
+ <div className="mt-6 space-y-2 rounded-lg border border-gray-200 bg-white p-6">
17
+ <p><strong>greeting:</strong> {t(messages, "greeting")}</p>
18
+ <p><strong>nav.home:</strong> {t(messages, "nav.home")}</p>
19
+ <p><strong>welcome:</strong> {t(messages, "welcome", { site: "MintilJS" })}</p>
20
+ <p><strong>format() example:</strong> {format("Welcome to {site}, {user}!", { site: "MintilJS", user: "dev" })}</p>
21
+ <p className="text-sm text-gray-500">Total messages: {Object.keys(messages).length}</p>
22
+ </div>
23
+ <p className="mt-4 text-sm text-gray-400">Try: <code>?lang=pt-BR</code></p>
24
+ </div>
25
+ );
26
+ }
@@ -1,6 +1,16 @@
1
1
  import React from "react";
2
2
  import Card from "@app/components/card";
3
3
 
4
+ const features = [
5
+ { href: "/about", title: "About", desc: "Learn more about MintilJS" },
6
+ { href: "/counter", title: "Counter (useClient)", desc: "Interactive page with client-side JavaScript" },
7
+ { href: "/ssr-counter", title: "SSR Counter", desc: "Server-rendered form-based interaction, zero JS" },
8
+ { href: "/island-demo", title: "Islands", desc: "Hydratable interactive components" },
9
+ { href: "/server-data", title: "Server (useServer)", desc: "Async component fetching data directly — zero JS" },
10
+ { href: "/blog", title: "Blog", desc: "Dynamic routes with (:slug).tsx" },
11
+ { href: "/i18n-test", title: "i18n", desc: "Internationalization with getMessages and t()" },
12
+ ];
13
+
4
14
  export default function HomePage() {
5
15
  return (
6
16
  <div>
@@ -9,6 +19,18 @@ export default function HomePage() {
9
19
  <Card title="Server rendered">
10
20
  This component was imported via <code className="rounded bg-gray-100 px-1.5 py-0.5 text-sm font-mono text-blue-700">@app/components/card</code>.
11
21
  </Card>
22
+ <div className="mt-8 grid gap-4 sm:grid-cols-2">
23
+ {features.map((f) => (
24
+ <a
25
+ key={f.href}
26
+ href={f.href}
27
+ className="rounded-lg border border-gray-200 bg-white p-4 transition hover:border-blue-300 hover:shadow-sm"
28
+ >
29
+ <h3 className="font-semibold text-blue-600">{f.title}</h3>
30
+ <p className="mt-1 text-sm text-gray-500">{f.desc}</p>
31
+ </a>
32
+ ))}
33
+ </div>
12
34
  </div>
13
35
  );
14
36
  }
@@ -0,0 +1,14 @@
1
+ import React from "react";
2
+ import { Island } from "mintiljs";
3
+
4
+ export default function IslandDemo() {
5
+ return (
6
+ <div>
7
+ <h1 className="mb-6 text-3xl font-bold tracking-tight">Island Demo</h1>
8
+ <p className="mb-4 text-gray-600">This counter is an Island — it hydrates independently on the client.</p>
9
+ <div className="rounded-lg border border-gray-200 bg-white p-6">
10
+ <Island name="Counter" props={{ initial: 42 }} />
11
+ </div>
12
+ </div>
13
+ );
14
+ }
@@ -0,0 +1,36 @@
1
+ import React from "react";
2
+
3
+ export const useServer = true;
4
+
5
+ async function fetchPosts() {
6
+ // Simula fetching de dados — podia ser DB, FS, API externa
7
+ return [
8
+ { id: 1, title: "MintilJS is fast" },
9
+ { id: 2, title: "SSR sem JavaScript" },
10
+ { id: 3, title: "React Server Components sem compilador" },
11
+ ];
12
+ }
13
+
14
+ export default async function ServerDataPage() {
15
+ const posts = await fetchPosts();
16
+
17
+ return (
18
+ <div>
19
+ <h1 className="mb-6 text-3xl font-bold tracking-tight">Server Data (useServer)</h1>
20
+ <p className="mb-4 text-gray-600">
21
+ Esta página usa <code className="rounded bg-gray-100 px-1.5 py-0.5 text-sm font-mono text-blue-700">useServer = true</code>.
22
+ O componente é <code className="rounded bg-gray-100 px-1.5 py-0.5 text-sm font-mono text-blue-700">async</code> e faz fetch
23
+ diretamente no servidor — sem <code className="rounded bg-gray-100 px-1.5 py-0.5 text-sm font-mono text-blue-700">getServerSideProps</code>.
24
+ Zero JavaScript enviado ao cliente.
25
+ </p>
26
+ <div className="space-y-3">
27
+ {posts.map((post) => (
28
+ <div key={post.id} className="rounded-lg border border-gray-200 bg-white p-4">
29
+ <h2 className="font-semibold text-gray-900">{post.title}</h2>
30
+ <p className="text-sm text-gray-500">Post #{post.id}</p>
31
+ </div>
32
+ ))}
33
+ </div>
34
+ </div>
35
+ );
36
+ }
@@ -0,0 +1,19 @@
1
+ import React from "react";
2
+
3
+ export default function SsrCounterPage() {
4
+ return (
5
+ <div>
6
+ <h1 className="mb-6 text-3xl font-bold tracking-tight">SSR Counter</h1>
7
+ <p className="mb-4 text-gray-600">This counter uses a form POST — no client JavaScript needed.</p>
8
+ <form method="POST" action="/api/counter" className="flex items-center gap-4">
9
+ <span className="text-2xl font-mono font-bold tabular-nums">0</span>
10
+ <button type="submit" name="action" value="inc" className="rounded-lg bg-blue-600 px-4 py-2 text-white hover:bg-blue-700">
11
+ +1
12
+ </button>
13
+ <button type="submit" name="action" value="dec" className="rounded-lg bg-gray-200 px-4 py-2 hover:bg-gray-300">
14
+ -1
15
+ </button>
16
+ </form>
17
+ </div>
18
+ );
19
+ }
@@ -1 +1 @@
1
- This is a static asset served under /assets/readme.txt.
1
+ This is a static asset served from the public/ directory.