@viglet/create-shio-app 2026.3.2

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.
@@ -0,0 +1,108 @@
1
+ # {{projectName}}
2
+
3
+ A [Next.js](https://nextjs.org) (App Router) site powered by **Shio CMS** via the
4
+ Content Delivery API — headless, TypeScript-first, **zero Java**.
5
+
6
+ ## Getting started
7
+
8
+ 1. Set your connection details in `.env.local`:
9
+
10
+ ```bash
11
+ SHIO_URL=https://your-shio-instance
12
+ SHIO_SITE_ID=<your site id>
13
+ SHIO_CDA_TOKEN=<your CDA token>
14
+ ```
15
+
16
+ 2. Run the dev server:
17
+
18
+ ```bash
19
+ npm run dev
20
+ ```
21
+
22
+ 3. Open [http://localhost:3000](http://localhost:3000).
23
+
24
+ ## How it works
25
+
26
+ - [`lib/shio.ts`](lib/shio.ts) creates a shared [`@viglet/shio-client`](https://www.npmjs.com/package/@viglet/shio-client) instance from the environment.
27
+ - [`app/page.tsx`](app/page.tsx) lists the site's published posts (`shio.query`).
28
+ - [`app/[...slug]/page.tsx`](app/[...slug]/page.tsx) resolves any path to a post via `shio.getPostByUrl(siteId, "/the/path")` — friendly-URL routing straight from the CDA, with `generateStaticParams` (SSG) + `revalidate` (ISR).
29
+
30
+ All content comes from the Shio CDA (`/api/v2/cda/**`). You never touch the Java backend — just edit React/TypeScript.
31
+
32
+ ## Rendering modes
33
+
34
+ This starter ships all three Next.js rendering modes against the CDA. Pick per
35
+ route — they compose freely.
36
+
37
+ ### SSG — static at build time
38
+
39
+ `generateStaticParams` in [`app/[...slug]/page.tsx`](app/[...slug]/page.tsx)
40
+ pre-renders every published post's friendly URL at `next build`. Fastest
41
+ possible response; ideal for content that changes rarely.
42
+
43
+ ### ISR — incremental static regeneration
44
+
45
+ `export const revalidate = 60` caches a rendered page and re-generates it at most
46
+ once per minute on the next request. Pages not covered by `generateStaticParams`
47
+ are rendered on-demand and then cached the same way. Tune the window per route.
48
+
49
+ ### On-demand revalidation — instant updates
50
+
51
+ Instead of waiting out the ISR window, Shio can push the moment content is
52
+ published. [`app/api/revalidate/route.ts`](app/api/revalidate/route.ts) receives
53
+ a signed webhook and calls `revalidatePath` so the affected page re-renders on
54
+ its next hit — staleness drops from minutes to seconds.
55
+
56
+ 1. Set `SHIO_WEBHOOK_SECRET` in `.env.local`.
57
+ 2. In Shio, add a publish webhook for this site pointing at
58
+ `https://your-site/api/revalidate` with the same secret (HMAC-SHA256).
59
+
60
+ The webhook body is `{ event, siteId, postId, url, tags? }`, signed as
61
+ `x-shio-signature: sha256=<hex>`. See the full contract in
62
+ [docs/recipes/nextjs-rendering.md](https://github.com/openviglet/shio/blob/main/docs/recipes/nextjs-rendering.md).
63
+
64
+ ### SSR draft preview
65
+
66
+ Editors can preview unpublished content before it goes live, using a
67
+ PREVIEW-env CDA token (which serves draft-preferred content):
68
+
69
+ 1. Set `SHIO_PREVIEW_TOKEN` (a PREVIEW-env token) and `SHIO_PREVIEW_SECRET`.
70
+ 2. Open `/api/preview?secret=<SHIO_PREVIEW_SECRET>&url=/the/path` — this turns on
71
+ Next.js Draft Mode and the page renders dynamically (SSR) through the preview
72
+ client. A banner with an "Exit preview" link appears.
73
+
74
+ When Draft Mode is off (the default for visitors), everything is plain SSG/ISR.
75
+
76
+ ## Customizing
77
+
78
+ The post's fields arrive as `post.attrs` (a JSON object keyed by your post-type
79
+ attribute names). The catch-all page renders a `content`/`body`/`text`/`html`
80
+ field if present, and otherwise dumps `attrs` so you can see the shape. Adapt the
81
+ rendering to your own post types.
82
+
83
+ ## Typed content (`post.attrs.*` autocomplete)
84
+
85
+ `post.attrs` is `Record<string, unknown>` by default. Generate TypeScript
86
+ declarations from your live post-types so you get autocomplete and type-checking:
87
+
88
+ ```bash
89
+ npm run shio:types # reads SHIO_URL / SHIO_CDA_TOKEN, writes lib/shio-types.d.ts
90
+ ```
91
+
92
+ This emits one `Post` interface per post-type plus a `ShioPostTypes` name→type
93
+ map. Cast a fetched post to its generated type to unlock typed `attrs`:
94
+
95
+ ```ts
96
+ import type { ArticlePost } from "@/lib/shio-types";
97
+
98
+ const post = (await client.getPostByUrl(SITE_ID, url)) as ArticlePost | null;
99
+ post?.attrs.title; // string — autocompleted
100
+ ```
101
+
102
+ Re-run after changing a post-type. The output is auto-generated — don't edit it
103
+ by hand.
104
+
105
+ ## Learn more
106
+
107
+ - [Shio CMS](https://github.com/openviglet/shio)
108
+ - [Next.js Docs](https://nextjs.org/docs)
@@ -0,0 +1,110 @@
1
+ import { notFound } from "next/navigation";
2
+ import { draftMode } from "next/headers";
3
+ import type { Metadata } from "next";
4
+ import { getShioClient, shio, SITE_ID, slugToUrl } from "@/lib/shio";
5
+
6
+ // ISR: pages not pre-rendered at build are rendered on-demand and cached for
7
+ // 60s. For instant updates, wire publish webhooks to `/api/revalidate` (SH9) —
8
+ // then this fixed window is just a fallback. In Draft Mode the route renders
9
+ // dynamically (SSR) and this value is ignored.
10
+ export const revalidate = 60;
11
+
12
+ type Params = { slug?: string[] };
13
+
14
+ /**
15
+ * Pre-render every published post's friendly URL at build time (SSG). Pages not
16
+ * listed here are still rendered on-demand and cached (ISR via `revalidate`).
17
+ */
18
+ export async function generateStaticParams(): Promise<Params[]> {
19
+ if (!SITE_ID) return [];
20
+ const listing = await shio.query({ siteId: SITE_ID, size: 500 });
21
+ return listing.posts
22
+ .filter((p) => p.furl)
23
+ .map((p) => ({ slug: p.furl!.replace(/^\//, "").split("/") }));
24
+ }
25
+
26
+ export async function generateMetadata({
27
+ params,
28
+ }: {
29
+ params: Promise<Params>;
30
+ }): Promise<Metadata> {
31
+ const { slug } = await params;
32
+ if (!SITE_ID) return {};
33
+ const client = await getShioClient();
34
+ const post = await client.getPostByUrl(SITE_ID, slugToUrl(slug));
35
+ if (!post) return {};
36
+ return {
37
+ title: post.title ?? undefined,
38
+ description: post.summary ?? undefined,
39
+ };
40
+ }
41
+
42
+ export default async function CatchAllPage({
43
+ params,
44
+ }: {
45
+ params: Promise<Params>;
46
+ }) {
47
+ const { slug } = await params;
48
+ if (!SITE_ID) notFound();
49
+
50
+ // PREVIEW client when Draft Mode is on (SSR draft preview), PROD otherwise.
51
+ const { isEnabled: isDraft } = await draftMode();
52
+ const client = await getShioClient();
53
+
54
+ const post = await client.getPostByUrl(SITE_ID, slugToUrl(slug));
55
+ if (!post) notFound();
56
+
57
+ // `attrs` is the post-type field map (e.g. content, body, image...). This
58
+ // starter renders a `content`/`body`/`text` HTML field if present; adapt to
59
+ // your own post types.
60
+ const html = pickHtml(post.attrs);
61
+
62
+ return (
63
+ <article>
64
+ {isDraft && <DraftBanner />}
65
+ <h1>{post.title}</h1>
66
+ {post.summary && <p style={{ color: "#666" }}>{post.summary}</p>}
67
+ {html ? (
68
+ <div dangerouslySetInnerHTML={{ __html: html }} />
69
+ ) : (
70
+ <pre
71
+ style={{
72
+ background: "#f6f6f6",
73
+ padding: "1rem",
74
+ borderRadius: 8,
75
+ overflow: "auto",
76
+ }}
77
+ >
78
+ {JSON.stringify(post.attrs, null, 2)}
79
+ </pre>
80
+ )}
81
+ </article>
82
+ );
83
+ }
84
+
85
+ function pickHtml(attrs: Record<string, unknown>): string | null {
86
+ for (const key of ["content", "body", "text", "html"]) {
87
+ const value = attrs[key];
88
+ if (typeof value === "string" && value.trim()) return value;
89
+ }
90
+ return null;
91
+ }
92
+
93
+ /** Shown while Draft Mode is active; links to disable it. */
94
+ function DraftBanner() {
95
+ return (
96
+ <div
97
+ style={{
98
+ background: "#fff7ed",
99
+ border: "1px solid #fdba74",
100
+ borderRadius: 8,
101
+ padding: "0.5rem 0.75rem",
102
+ marginBottom: "1rem",
103
+ fontSize: "0.85rem",
104
+ }}
105
+ >
106
+ Draft preview is on.{" "}
107
+ <a href="/api/preview?disable=1">Exit preview</a>
108
+ </div>
109
+ );
110
+ }
@@ -0,0 +1,65 @@
1
+ import { cookies, draftMode } from "next/headers";
2
+ import { NextResponse } from "next/server";
3
+ import { PREVIEW_SECRET, PREVIEW_TOKEN_COOKIE } from "@/lib/shio";
4
+
5
+ /**
6
+ * Draft-preview entry point (Shio roadmap SH9 + SH13, riding on SH5 PREVIEW-env keys).
7
+ *
8
+ * Two ways in:
9
+ * - Console-issued, short-lived token (SH13):
10
+ * `/api/preview?token=<minted>&url=/the/path`
11
+ * Shio's console mints an expiring PREVIEW-env CDA token and hands the editor
12
+ * this link. The token is stashed in an httpOnly cookie and used per-request
13
+ * by `getShioClient()`; it stops working on its own when it expires server-side.
14
+ * - Static shared secret (SH9):
15
+ * `/api/preview?secret=<SHIO_PREVIEW_SECRET>&url=/the/path`
16
+ * Falls back to the long-lived `SHIO_PREVIEW_TOKEN` server client.
17
+ *
18
+ * Disable: `/api/preview?disable=1`
19
+ *
20
+ * Enabling turns on Next.js Draft Mode (a cookie); subsequent requests render
21
+ * dynamically (SSR) through the PREVIEW CDA client, which prefers draft rows
22
+ * over published ones.
23
+ */
24
+ export async function GET(request: Request): Promise<NextResponse> {
25
+ const { searchParams } = new URL(request.url);
26
+ const draft = await draftMode();
27
+ const cookieStore = await cookies();
28
+
29
+ if (searchParams.get("disable")) {
30
+ draft.disable();
31
+ cookieStore.delete(PREVIEW_TOKEN_COOKIE);
32
+ return NextResponse.redirect(new URL(searchParams.get("url") ?? "/", request.url));
33
+ }
34
+
35
+ // Redirect target — relative paths only, to avoid open redirects.
36
+ const target = searchParams.get("url") ?? "/";
37
+ const safeTarget = target.startsWith("/") ? target : "/";
38
+
39
+ const token = searchParams.get("token");
40
+ if (token) {
41
+ // SH13: trust the console-minted token; the backend enforces its expiry and
42
+ // single-site scope. Stash it httpOnly so getShioClient() reads draft-preferred.
43
+ draft.enable();
44
+ cookieStore.set(PREVIEW_TOKEN_COOKIE, token, {
45
+ httpOnly: true,
46
+ sameSite: "lax",
47
+ path: "/",
48
+ maxAge: 60 * 60, // 1h ceiling; the token itself may expire sooner server-side
49
+ });
50
+ return NextResponse.redirect(new URL(safeTarget, request.url));
51
+ }
52
+
53
+ if (!PREVIEW_SECRET) {
54
+ return NextResponse.json(
55
+ { message: "SHIO_PREVIEW_SECRET is not configured" },
56
+ { status: 500 }
57
+ );
58
+ }
59
+ if (searchParams.get("secret") !== PREVIEW_SECRET) {
60
+ return NextResponse.json({ message: "Invalid preview secret" }, { status: 401 });
61
+ }
62
+
63
+ draft.enable();
64
+ return NextResponse.redirect(new URL(safeTarget, request.url));
65
+ }
@@ -0,0 +1,96 @@
1
+ import { createHmac, timingSafeEqual } from "node:crypto";
2
+ import { revalidatePath, revalidateTag } from "next/cache";
3
+ import { NextResponse } from "next/server";
4
+ import { WEBHOOK_SECRET } from "@/lib/shio";
5
+
6
+ // Webhook receiver — runs on the Node runtime (needs `node:crypto`).
7
+ export const runtime = "nodejs";
8
+
9
+ /**
10
+ * On-demand revalidation endpoint (Shio roadmap SH9).
11
+ *
12
+ * Shio's publish pipeline (SH12) POSTs here on publish / unpublish / delete. The
13
+ * body is HMAC-SHA256-signed with `SHIO_WEBHOOK_SECRET`; we verify it, then ask
14
+ * Next.js to drop the affected page(s) from the ISR cache so the next request
15
+ * re-renders fresh content — turning minutes-of-staleness ISR into seconds.
16
+ *
17
+ * Expected payload (the contract SH12 emits — see docs/recipes/nextjs-rendering.md):
18
+ * {
19
+ * "event": "publish" | "unpublish" | "delete",
20
+ * "siteId": "<site id>",
21
+ * "postId": "<post id>",
22
+ * "url": "/the/friendly-url", // optional but preferred
23
+ * "tags": ["optional", "cache", "tags"]
24
+ * }
25
+ *
26
+ * Signature header: `x-shio-signature: sha256=<hex hmac of the raw body>`.
27
+ */
28
+ export async function POST(request: Request): Promise<NextResponse> {
29
+ if (!WEBHOOK_SECRET) {
30
+ return NextResponse.json(
31
+ { revalidated: false, message: "SHIO_WEBHOOK_SECRET is not configured" },
32
+ { status: 500 }
33
+ );
34
+ }
35
+
36
+ const raw = await request.text();
37
+ const signature = request.headers.get("x-shio-signature");
38
+ if (!verifySignature(raw, signature, WEBHOOK_SECRET)) {
39
+ return NextResponse.json(
40
+ { revalidated: false, message: "Invalid signature" },
41
+ { status: 401 }
42
+ );
43
+ }
44
+
45
+ let payload: WebhookPayload;
46
+ try {
47
+ payload = JSON.parse(raw) as WebhookPayload;
48
+ } catch {
49
+ return NextResponse.json(
50
+ { revalidated: false, message: "Invalid JSON body" },
51
+ { status: 400 }
52
+ );
53
+ }
54
+
55
+ const revalidated: string[] = [];
56
+
57
+ // Drop the affected page itself plus the home/listing page.
58
+ if (payload.url) {
59
+ revalidatePath(payload.url);
60
+ revalidated.push(`path:${payload.url}`);
61
+ }
62
+ revalidatePath("/");
63
+ revalidated.push("path:/");
64
+
65
+ // Honour any explicit cache tags (pair with `fetch(..., { next: { tags } })`).
66
+ for (const tag of payload.tags ?? []) {
67
+ revalidateTag(tag);
68
+ revalidated.push(`tag:${tag}`);
69
+ }
70
+
71
+ return NextResponse.json({ revalidated: true, event: payload.event, dropped: revalidated });
72
+ }
73
+
74
+ type WebhookPayload = {
75
+ event?: string;
76
+ siteId?: string;
77
+ postId?: string;
78
+ url?: string;
79
+ tags?: string[];
80
+ };
81
+
82
+ /** Constant-time verification of `sha256=<hex>` against HMAC-SHA256(raw, secret). */
83
+ function verifySignature(
84
+ raw: string,
85
+ header: string | null,
86
+ secret: string
87
+ ): boolean {
88
+ if (!header) return false;
89
+ const provided = header.startsWith("sha256=") ? header.slice(7) : header;
90
+ const expected = createHmac("sha256", secret).update(raw).digest("hex");
91
+ // timingSafeEqual throws if lengths differ — guard first.
92
+ const a = Buffer.from(provided, "hex");
93
+ const b = Buffer.from(expected, "hex");
94
+ if (a.length !== b.length || a.length === 0) return false;
95
+ return timingSafeEqual(a, b);
96
+ }
@@ -0,0 +1,25 @@
1
+ import type { ReactNode } from "react";
2
+
3
+ export const metadata = {
4
+ title: "Shio site",
5
+ description: "Powered by Shio CMS — headless, JavaScript-first.",
6
+ };
7
+
8
+ export default function RootLayout({ children }: { children: ReactNode }) {
9
+ return (
10
+ <html lang="en">
11
+ <body
12
+ style={{
13
+ fontFamily: "system-ui, sans-serif",
14
+ margin: 0,
15
+ lineHeight: 1.6,
16
+ color: "#1a1a1a",
17
+ }}
18
+ >
19
+ <main style={{ maxWidth: 720, margin: "0 auto", padding: "2rem 1rem" }}>
20
+ {children}
21
+ </main>
22
+ </body>
23
+ </html>
24
+ );
25
+ }
@@ -0,0 +1,72 @@
1
+ import Link from "next/link";
2
+ import { getShioClient, SITE_ID } from "@/lib/shio";
3
+
4
+ // ISR: re-fetch from the CDA at most once per minute. Pair with publish
5
+ // webhooks + on-demand revalidation (`/api/revalidate`) for instant updates
6
+ // (see Shio roadmap SH9/SH12). In Draft Mode this renders dynamically (SSR).
7
+ export const revalidate = 60;
8
+
9
+ export default async function HomePage() {
10
+ if (!SITE_ID) {
11
+ return <SetupHint />;
12
+ }
13
+
14
+ const shio = await getShioClient();
15
+ const site = await shio.getSite(SITE_ID);
16
+ const listing = await shio.query({ siteId: SITE_ID, size: 50 });
17
+
18
+ return (
19
+ <>
20
+ <header style={{ marginBottom: "2rem" }}>
21
+ <h1 style={{ marginBottom: 4 }}>{site?.name ?? "Shio site"}</h1>
22
+ {site?.description && (
23
+ <p style={{ color: "#666", marginTop: 0 }}>{site.description}</p>
24
+ )}
25
+ </header>
26
+
27
+ <h2 style={{ fontSize: "1rem", textTransform: "uppercase", color: "#888" }}>
28
+ Published content
29
+ </h2>
30
+
31
+ {listing.posts.length === 0 ? (
32
+ <p style={{ color: "#666" }}>
33
+ No published posts yet. Publish content in Shio, then refresh.
34
+ </p>
35
+ ) : (
36
+ <ul style={{ listStyle: "none", padding: 0 }}>
37
+ {listing.posts.map((post) => (
38
+ <li key={post.id} style={{ padding: "0.75rem 0", borderBottom: "1px solid #eee" }}>
39
+ {post.furl ? (
40
+ <Link href={post.furl}>{post.title ?? post.furl}</Link>
41
+ ) : (
42
+ <span>{post.title ?? post.id}</span>
43
+ )}
44
+ {post.summary && (
45
+ <p style={{ margin: "4px 0 0", color: "#666", fontSize: "0.9rem" }}>
46
+ {post.summary}
47
+ </p>
48
+ )}
49
+ </li>
50
+ ))}
51
+ </ul>
52
+ )}
53
+ </>
54
+ );
55
+ }
56
+
57
+ function SetupHint() {
58
+ return (
59
+ <>
60
+ <h1>Welcome to your Shio site 👋</h1>
61
+ <p>
62
+ Set <code>SHIO_SITE_ID</code> (and <code>SHIO_URL</code> /{" "}
63
+ <code>SHIO_CDA_TOKEN</code>) in <code>.env.local</code>, then restart{" "}
64
+ <code>npm run dev</code>.
65
+ </p>
66
+ <p style={{ color: "#666" }}>
67
+ This starter talks to the Shio Content Delivery API through{" "}
68
+ <code>@viglet/shio-client</code> — no Java, just TypeScript.
69
+ </p>
70
+ </>
71
+ );
72
+ }
@@ -0,0 +1,26 @@
1
+ # dependencies
2
+ /node_modules
3
+
4
+ # next.js
5
+ /.next/
6
+ /out/
7
+
8
+ # production
9
+ /build
10
+
11
+ # misc
12
+ .DS_Store
13
+ *.pem
14
+
15
+ # debug
16
+ npm-debug.log*
17
+ yarn-debug.log*
18
+ yarn-error.log*
19
+
20
+ # env files (keep .env.example, ignore real secrets)
21
+ .env*.local
22
+ .env
23
+
24
+ # typescript
25
+ *.tsbuildinfo
26
+ next-env.d.ts
@@ -0,0 +1,70 @@
1
+ import { cookies, draftMode } from "next/headers";
2
+ import { ShioClient } from "@viglet/shio-client";
3
+
4
+ const SHIO_BASE_URL = process.env.SHIO_URL ?? "http://localhost:2710";
5
+
6
+ /** httpOnly cookie holding a console-minted, short-lived preview token (SH13). */
7
+ export const PREVIEW_TOKEN_COOKIE = "shio-preview-token";
8
+
9
+ /** A draft-preferred client bound to a specific (short-lived) preview token. */
10
+ function previewClientFor(token: string): ShioClient {
11
+ return new ShioClient({ baseUrl: SHIO_BASE_URL, apiToken: token });
12
+ }
13
+
14
+ /**
15
+ * Shared Shio Content Delivery API clients.
16
+ *
17
+ * Reads connection details from the environment (see `.env.local`). Safe to
18
+ * import from any Server Component, Route Handler, or `generateStaticParams`.
19
+ *
20
+ * Two clients are exposed:
21
+ * - `shio` — a PROD-env CDA key; serves PUBLISHED content (SSG/ISR).
22
+ * - `shioPreview` — a PREVIEW-env CDA key; serves draft-preferred content (SSR
23
+ * draft preview, see `app/api/preview`). `null` unless
24
+ * `SHIO_PREVIEW_TOKEN` is set.
25
+ *
26
+ * Use {@link getShioClient} to pick the right one for the current request — it
27
+ * honours Next.js Draft Mode automatically.
28
+ */
29
+ export const shio = new ShioClient({
30
+ baseUrl: process.env.SHIO_URL ?? "http://localhost:2710",
31
+ apiToken: process.env.SHIO_CDA_TOKEN,
32
+ });
33
+
34
+ /** The PREVIEW-env client (draft-preferred reads), or `null` if not configured. */
35
+ export const shioPreview: ShioClient | null = process.env.SHIO_PREVIEW_TOKEN
36
+ ? new ShioClient({
37
+ baseUrl: process.env.SHIO_URL ?? "http://localhost:2710",
38
+ apiToken: process.env.SHIO_PREVIEW_TOKEN,
39
+ })
40
+ : null;
41
+
42
+ /**
43
+ * Pick the CDA client for the current request. Under Next.js Draft Mode, prefer
44
+ * a console-minted short-lived preview token (SH13) from the request cookie;
45
+ * otherwise fall back to the long-lived PREVIEW server client (SH9). With Draft
46
+ * Mode off, always the PROD client. Call from Server Components / Route Handlers.
47
+ */
48
+ export async function getShioClient(): Promise<ShioClient> {
49
+ const { isEnabled } = await draftMode();
50
+ if (isEnabled) {
51
+ const token = (await cookies()).get(PREVIEW_TOKEN_COOKIE)?.value;
52
+ if (token) return previewClientFor(token);
53
+ if (shioPreview) return shioPreview;
54
+ }
55
+ return shio;
56
+ }
57
+
58
+ /** The site this front-end renders. */
59
+ export const SITE_ID = process.env.SHIO_SITE_ID ?? "";
60
+
61
+ /** Shared secret the publish webhook signs its body with (HMAC-SHA256, SH12). */
62
+ export const WEBHOOK_SECRET = process.env.SHIO_WEBHOOK_SECRET ?? "";
63
+
64
+ /** Secret that gates the draft-preview entry point (`app/api/preview`). */
65
+ export const PREVIEW_SECRET = process.env.SHIO_PREVIEW_SECRET ?? "";
66
+
67
+ /** Normalize Next's `slug[]` into a CDA friendly-URL path (always leading slash). */
68
+ export function slugToUrl(slug?: string[]): string {
69
+ return "/" + (slug ?? []).join("/");
70
+ }
@@ -0,0 +1,5 @@
1
+ /// <reference types="next" />
2
+ /// <reference types="next/image-types/global" />
3
+
4
+ // NOTE: This file should not be edited
5
+ // see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
@@ -0,0 +1,12 @@
1
+ /** @type {import('next').NextConfig} */
2
+ const nextConfig = {
3
+ // Allow <Image> to load media served from the Shio instance.
4
+ images: {
5
+ remotePatterns: [
6
+ { protocol: "http", hostname: "localhost" },
7
+ // Add your Shio host here, e.g. { protocol: "https", hostname: "cms.example.com" }
8
+ ],
9
+ },
10
+ };
11
+
12
+ export default nextConfig;
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "{{projectName}}",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "next dev",
8
+ "build": "next build",
9
+ "start": "next start",
10
+ "lint": "next lint",
11
+ "shio:types": "shio-types --url ${SHIO_URL:-http://localhost:2710} --token $SHIO_CDA_TOKEN --out lib/shio-types.d.ts"
12
+ },
13
+ "dependencies": {
14
+ "@viglet/shio-client": "^2026.2.1",
15
+ "next": "^15.0.0",
16
+ "react": "^19.0.0",
17
+ "react-dom": "^19.0.0"
18
+ },
19
+ "devDependencies": {
20
+ "@types/node": "^22.0.0",
21
+ "@types/react": "^19.0.0",
22
+ "@types/react-dom": "^19.0.0",
23
+ "typescript": "^5.6.0"
24
+ }
25
+ }
@@ -0,0 +1,21 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "lib": ["dom", "dom.iterable", "ES2022"],
5
+ "allowJs": true,
6
+ "skipLibCheck": true,
7
+ "strict": true,
8
+ "noEmit": true,
9
+ "esModuleInterop": true,
10
+ "module": "esnext",
11
+ "moduleResolution": "bundler",
12
+ "resolveJsonModule": true,
13
+ "isolatedModules": true,
14
+ "jsx": "preserve",
15
+ "incremental": true,
16
+ "plugins": [{ "name": "next" }],
17
+ "paths": { "@/*": ["./*"] }
18
+ },
19
+ "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
20
+ "exclude": ["node_modules"]
21
+ }