create-nextly-app 0.0.2-alpha.7 → 0.0.2-alpha.70
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 +36 -9
- package/dist/cli.cjs +3047 -32460
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.d.cts +1 -1
- package/dist/cli.d.ts +1 -1
- package/dist/cli.mjs +3048 -3602
- package/dist/cli.mjs.map +1 -1
- package/dist/create-nextly-5wiGMVI6.mjs +52550 -0
- package/dist/create-nextly-5wiGMVI6.mjs.map +1 -0
- package/dist/create-nextly-BbKGDrEC.cjs +52628 -0
- package/dist/create-nextly-BbKGDrEC.cjs.map +1 -0
- package/dist/index.cjs +3 -24861
- package/dist/index.d.cts +40 -34
- package/dist/index.d.ts +40 -34
- package/dist/index.mjs +1 -6
- package/package.json +28 -19
- package/skills/nextly-authentication/SKILL.md +187 -0
- package/skills/nextly-collections/SKILL.md +215 -0
- package/skills/nextly-deploy/SKILL.md +248 -0
- package/skills/nextly-install/SKILL.md +121 -0
- package/templates/base/.env.example +21 -6
- package/templates/base/AGENTS.md.template +226 -0
- package/templates/base/CLAUDE.md.template +1 -0
- package/templates/base/gitignore +42 -0
- package/templates/base/next.config.ts +11 -17
- package/templates/base/src/app/admin/[[...params]]/page.tsx +3 -0
- package/templates/base/src/app/admin/api/media/[[...path]]/route.ts +26 -0
- package/templates/base/src/app/globals.css +15 -2
- package/templates/base/src/app/layout.tsx +32 -18
- package/templates/base/src/lib/site-url.ts +55 -0
- package/templates/base/src/types/generated/nextly-types.ts +6 -4
- package/templates/base/src/types/generated/plugin-admin-components.generated.ts +6 -0
- package/templates/base/src/types/generated/plugin-admin-imports.generated.ts +32 -0
- package/templates/base/tsconfig.json +8 -1
- package/templates/blank/src/{components → app/(frontend)/components}/ThemeToggle.tsx +55 -24
- package/templates/blank/src/app/(frontend)/page.tsx +2 -2
- package/templates/blank/src/app/globals.css +37 -1
- package/templates/blank/src/app/layout.tsx +28 -18
- package/templates/blank/src/instrumentation.ts +32 -0
- package/templates/plugin/AGENTS.md.template +125 -0
- package/templates/plugin/CLAUDE.md.template +1 -0
- package/templates/plugin/README.md +48 -0
- package/templates/plugin/dev/.env.example +11 -0
- package/templates/plugin/dev/next.config.ts +43 -0
- package/templates/plugin/dev/nextly.config.ts +17 -0
- package/templates/plugin/dev/src/app/admin/[[...params]]/page.tsx +24 -0
- package/templates/plugin/dev/src/app/admin/api/[[...params]]/route.ts +19 -0
- package/templates/plugin/dev/src/app/layout.tsx +13 -0
- package/templates/plugin/dev/src/instrumentation.ts +25 -0
- package/templates/plugin/dev/src/seed-dev-user.ts +51 -0
- package/templates/plugin/dev/src/types/generated/plugin-admin-components.generated.ts +8 -0
- package/templates/plugin/dev/src/types/generated/plugin-admin-imports.generated.ts +37 -0
- package/templates/plugin/dev/tsconfig.json +22 -0
- package/templates/plugin/eslint.config.mjs +19 -0
- package/templates/plugin/gitignore +13 -0
- package/templates/plugin/src/admin/SettingsPage.tsx +57 -0
- package/templates/plugin/src/admin/index.ts +24 -0
- package/templates/plugin/src/collections/example.ts +13 -0
- package/templates/plugin/src/index.ts +6 -0
- package/templates/plugin/src/plugin.test.ts +35 -0
- package/templates/plugin/src/plugin.ts +48 -0
- package/templates/plugin/src/types.ts +17 -0
- package/templates/plugin/template.json +14 -0
- package/templates/plugin/tsconfig.json +18 -0
- package/templates/plugin/tsdown.config.ts +30 -0
- package/templates/plugin/vitest.config.ts +27 -0
- package/dist/chunk-RPGFJSDP.mjs +0 -28835
- package/dist/chunk-RPGFJSDP.mjs.map +0 -1
- package/dist/index.cjs.map +0 -1
- package/dist/index.mjs.map +0 -1
- package/templates/blank/src/access/README.md +0 -22
- package/templates/blank/src/collections/README.md +0 -22
- package/templates/blank/src/lib/README.md +0 -15
- package/templates/blank/src/singles/README.md +0 -19
- /package/templates/blank/src/{components → app/(frontend)/components}/README.md +0 -0
|
@@ -1,21 +1,15 @@
|
|
|
1
1
|
import type { NextConfig } from "next";
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
"better-sqlite3",
|
|
15
|
-
"bcryptjs",
|
|
16
|
-
"sharp",
|
|
17
|
-
"esbuild",
|
|
18
|
-
],
|
|
19
|
-
};
|
|
3
|
+
/**
|
|
4
|
+
* Minimal base `next.config.ts` for bundled templates.
|
|
5
|
+
*
|
|
6
|
+
* This file is intentionally left minimal (no adapter whitelist). The
|
|
7
|
+
* scaffolder will generate or patch a database-specific `next.config.ts`
|
|
8
|
+
* that includes only the selected adapter and driver in
|
|
9
|
+
* `serverExternalPackages`. Keeping the base file minimal avoids
|
|
10
|
+
* advertising adapters the user didn't choose and prevents misleading
|
|
11
|
+
* generated projects.
|
|
12
|
+
*/
|
|
13
|
+
const nextConfig: NextConfig = {};
|
|
20
14
|
|
|
21
15
|
export default nextConfig;
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import "@nextlyhq/admin/style.css";
|
|
4
|
+
// Registers every plugin admin component the app's config declares. Generated
|
|
5
|
+
// by `nextly generate:types`; run it again after adding or changing a plugin.
|
|
6
|
+
import "../../../types/generated/plugin-admin-imports.generated";
|
|
4
7
|
import { RootLayout, QueryProvider, ErrorBoundary } from "@nextlyhq/admin";
|
|
5
8
|
|
|
6
9
|
export default function AdminPage() {
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Admin Media API — permission-gated management surface.
|
|
3
|
+
*
|
|
4
|
+
* The admin's session cookie is scoped to `/admin`, so it reaches here but not
|
|
5
|
+
* the public `/api/media`. Every operation is checked against a media
|
|
6
|
+
* permission (read/create/update/delete-media) and the acting identity is
|
|
7
|
+
* taken from the session/API key, never a request field. This is the only
|
|
8
|
+
* place media can be uploaded, edited, moved or deleted.
|
|
9
|
+
*
|
|
10
|
+
* Supported (all gated):
|
|
11
|
+
* - GET/POST/PATCH/DELETE /admin/api/media/* (files, folders, bulk delete)
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { createMediaHandlers } from "nextly/api/media-handlers";
|
|
15
|
+
|
|
16
|
+
import config from "@nextly-config";
|
|
17
|
+
|
|
18
|
+
const handlers = createMediaHandlers({
|
|
19
|
+
config,
|
|
20
|
+
requireAuth: true,
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
export const GET = handlers.GET;
|
|
24
|
+
export const POST = handlers.POST;
|
|
25
|
+
export const PATCH = handlers.PATCH;
|
|
26
|
+
export const DELETE = handlers.DELETE;
|
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
@import "tailwindcss";
|
|
2
2
|
|
|
3
|
+
/* The families the imported font stylesheets declare, bound to the variables the rules
|
|
4
|
+
below read. Named here rather than by `next/font`, which used to inject them: the faces
|
|
5
|
+
now arrive as ordinary stylesheets, so the binding has to be written down somewhere, and
|
|
6
|
+
the name is load-bearing — the admin theme reads `--font-geist`. Quoted family names, so
|
|
7
|
+
the two-word values stay one identifier. */
|
|
8
|
+
:root {
|
|
9
|
+
--font-geist: "Geist Variable";
|
|
10
|
+
--font-geist-mono: "Geist Mono Variable";
|
|
11
|
+
}
|
|
12
|
+
|
|
3
13
|
:root {
|
|
4
14
|
--background: #ffffff;
|
|
5
15
|
--foreground: #171717;
|
|
@@ -8,8 +18,11 @@
|
|
|
8
18
|
@theme inline {
|
|
9
19
|
--color-background: var(--background);
|
|
10
20
|
--color-foreground: var(--foreground);
|
|
11
|
-
|
|
12
|
-
|
|
21
|
+
/* Fallback arguments, not a trailing family list: an unresolved var() with no
|
|
22
|
+
fallback invalidates the whole declaration rather than falling through to
|
|
23
|
+
what comes after the closing paren. */
|
|
24
|
+
--font-sans: var(--font-geist, ui-sans-serif), system-ui, sans-serif;
|
|
25
|
+
--font-mono: var(--font-geist-mono, ui-monospace), monospace;
|
|
13
26
|
}
|
|
14
27
|
|
|
15
28
|
@media (prefers-color-scheme: dark) {
|
|
@@ -1,16 +1,32 @@
|
|
|
1
1
|
import type { Metadata } from "next";
|
|
2
|
-
import
|
|
2
|
+
import "@fontsource-variable/geist";
|
|
3
|
+
import "@fontsource-variable/geist-mono";
|
|
3
4
|
import "./globals.css";
|
|
5
|
+
import { SITE_URL } from "@/lib/site-url";
|
|
4
6
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
7
|
+
/**
|
|
8
|
+
* The faces are imported as STYLESHEETS from packages in `node_modules`, rather
|
|
9
|
+
* than fetched from fonts.googleapis.com by `next/font/google` while
|
|
10
|
+
* `next build` runs — which made every build depend on reaching a third party
|
|
11
|
+
* and fail behind a proxy, on a locked-down runner, or offline.
|
|
12
|
+
*
|
|
13
|
+
* A bare package import rather than `next/font/local` pointing INTO
|
|
14
|
+
* `node_modules`: a literal path asserts where the package physically lives,
|
|
15
|
+
* and that assertion is false under Yarn's Plug'n'Play linker (no
|
|
16
|
+
* `node_modules` at all), under npm/Yarn workspace hoisting (the package moves
|
|
17
|
+
* to the workspace root), and under pnpm's own symlinked store. An import asks
|
|
18
|
+
* the resolver instead, which is correct under every layout.
|
|
19
|
+
*
|
|
20
|
+
* The trade is `next/font`'s metric-adjusted fallback, so text can shift
|
|
21
|
+
* slightly as the face arrives. The families are bound to this app's `--font-*`
|
|
22
|
+
* variables in `globals.css`, which is what every rule downstream reads. *
|
|
23
|
+
* Importing the package root ships every subset the face offers — Latin, Latin
|
|
24
|
+
* Extended, Cyrillic, Vietnamese — but each `@font-face` carries a
|
|
25
|
+
* `unicode-range`, so a browser downloads only the subsets the page actually
|
|
26
|
+
* uses. The deployed bundle is larger than a single hand-picked file; what a
|
|
27
|
+
* reader fetches is not, and a page in Cyrillic now gets its face instead of a
|
|
28
|
+
* fallback.
|
|
29
|
+
*/
|
|
14
30
|
|
|
15
31
|
/**
|
|
16
32
|
* `metadataBase` tells Next.js how to resolve relative URLs in OpenGraph
|
|
@@ -19,9 +35,7 @@ const geistMono = Geist_Mono({
|
|
|
19
35
|
* `https://yourblog.com`). The localhost fallback keeps dev working.
|
|
20
36
|
*/
|
|
21
37
|
export const metadata: Metadata = {
|
|
22
|
-
metadataBase: new URL(
|
|
23
|
-
process.env.NEXT_PUBLIC_SITE_URL ?? "http://localhost:3000"
|
|
24
|
-
),
|
|
38
|
+
metadataBase: new URL(SITE_URL),
|
|
25
39
|
title: { default: "Nextly", template: "%s — Nextly" },
|
|
26
40
|
description: "A site built with Nextly.",
|
|
27
41
|
};
|
|
@@ -32,12 +46,12 @@ export default function RootLayout({
|
|
|
32
46
|
children: React.ReactNode;
|
|
33
47
|
}>) {
|
|
34
48
|
return (
|
|
49
|
+
// The font variables belong on <html>: the admin theme declares --font-sans
|
|
50
|
+
// in a non-inline @theme, which emits it into :root and resolves the
|
|
51
|
+
// reference THERE. A variable exposed lower down, on <body>, is invisible to
|
|
52
|
+
// that declaration however it is spelled.
|
|
35
53
|
<html lang="en" suppressHydrationWarning>
|
|
36
|
-
<body
|
|
37
|
-
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
|
38
|
-
>
|
|
39
|
-
{children}
|
|
40
|
-
</body>
|
|
54
|
+
<body className="antialiased">{children}</body>
|
|
41
55
|
</html>
|
|
42
56
|
);
|
|
43
57
|
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The origin this site's absolute URLs are built from.
|
|
3
|
+
*
|
|
4
|
+
* Absolute URLs are needed in places that leave the browser's context —
|
|
5
|
+
* `metadataBase`, canonical links, Open Graph tags, the sitemap, RSS and
|
|
6
|
+
* JSON-LD. A relative path is meaningless in all of them, so something has to
|
|
7
|
+
* name the origin.
|
|
8
|
+
*
|
|
9
|
+
* ## Why two variables, in this order
|
|
10
|
+
*
|
|
11
|
+
* `NEXT_PUBLIC_SITE_URL` is where READERS find the site. `NEXT_PUBLIC_APP_URL`
|
|
12
|
+
* is where the Nextly app itself lives — the admin, the API, the origin its
|
|
13
|
+
* emails and preview links point at. They are usually the same host and are
|
|
14
|
+
* not the same question: a headless setup serves its public pages from one
|
|
15
|
+
* origin and runs the admin on another.
|
|
16
|
+
*
|
|
17
|
+
* So the public one wins when it is set, and the app's own origin is used when
|
|
18
|
+
* it is not. That second step matters more than it looks: the generated `.env`
|
|
19
|
+
* sets `NEXT_PUBLIC_APP_URL`, so without it a project that configured the only
|
|
20
|
+
* URL it was given would still publish `localhost` canonicals and OG tags to
|
|
21
|
+
* production — silently, because nothing about a wrong absolute URL fails at
|
|
22
|
+
* build time.
|
|
23
|
+
*
|
|
24
|
+
* ## The development fallback reads the port it is actually served on
|
|
25
|
+
*
|
|
26
|
+
* Not a hardcoded 3000. Next assigns `process.env.PORT` to the port it ends up
|
|
27
|
+
* listening on — after its own "port in use, using available port instead"
|
|
28
|
+
* fallback — so this follows `next dev -p 4000`, and follows Next onto a
|
|
29
|
+
* different port when 3000 is taken. A literal cannot do either, and the
|
|
30
|
+
* failure is quiet: pages render while every absolute URL points somewhere
|
|
31
|
+
* nothing is listening.
|
|
32
|
+
*
|
|
33
|
+
* `PORT` is a server-side value and is not inlined into client bundles, so a
|
|
34
|
+
* client component importing this reads the plain default. That is the same
|
|
35
|
+
* answer it would have had anyway, and client code should prefer
|
|
36
|
+
* `window.location.origin`, which is exact.
|
|
37
|
+
*/
|
|
38
|
+
const DEV_PORT = process.env.PORT ?? "3000";
|
|
39
|
+
|
|
40
|
+
export const SITE_URL =
|
|
41
|
+
process.env.NEXT_PUBLIC_SITE_URL ??
|
|
42
|
+
process.env.NEXT_PUBLIC_APP_URL ??
|
|
43
|
+
`http://localhost:${DEV_PORT}`;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Join a path onto {@link SITE_URL} without duplicating slashes.
|
|
47
|
+
*
|
|
48
|
+
* @param path - A root-relative path; a leading slash is optional
|
|
49
|
+
* @returns The absolute URL
|
|
50
|
+
*/
|
|
51
|
+
export function absoluteUrl(path = "/"): string {
|
|
52
|
+
const base = SITE_URL.replace(/\/$/, "");
|
|
53
|
+
const p = path.startsWith("/") ? path : `/${path}`;
|
|
54
|
+
return `${base}${p}`;
|
|
55
|
+
}
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Nextly Generated Types
|
|
3
3
|
*
|
|
4
|
-
* This file
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* This file is generated by `nextly generate:types`, which this project runs
|
|
5
|
+
* as its `types:generate` script. Run it after every change to the schema in
|
|
6
|
+
* nextly.config.ts. It reads only that file, so collections built in the
|
|
7
|
+
* Schema Builder get types once they are moved into code
|
|
8
|
+
* (`nextly db:sync --promote <slug>`).
|
|
7
9
|
*
|
|
8
10
|
* Do not edit this file manually - it will be overwritten.
|
|
9
11
|
*/
|
|
10
12
|
|
|
11
|
-
// Types
|
|
13
|
+
// Types are generated here by `nextly generate:types`.
|
|
12
14
|
export {};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/* AUTO-GENERATED by `nextly generate:types` — DO NOT EDIT.
|
|
2
|
+
* The component paths `plugin-admin-imports.generated.ts` registers, as
|
|
3
|
+
* plain strings, so server code can read what the admin's map holds
|
|
4
|
+
* without importing the components. Regenerated with it. */
|
|
5
|
+
|
|
6
|
+
export const registeredComponentPaths = [] as const;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/* AUTO-GENERATED by `nextly generate:types` — DO NOT EDIT.
|
|
2
|
+
* Statically registers the plugin admin components the admin resolves by
|
|
3
|
+
* string (pages, settings, views, slots, field-type editors) so they load
|
|
4
|
+
* without manual host imports. */
|
|
5
|
+
|
|
6
|
+
import { registeredComponentPaths as listed } from "./plugin-admin-components.generated";
|
|
7
|
+
|
|
8
|
+
const registered = {};
|
|
9
|
+
|
|
10
|
+
type Registered = keyof typeof registered;
|
|
11
|
+
type Listed = (typeof listed)[number];
|
|
12
|
+
/** `true` only while this map registers exactly the paths its list names. */
|
|
13
|
+
type Agrees = [Registered] extends [Listed]
|
|
14
|
+
? [Listed] extends [Registered]
|
|
15
|
+
? true
|
|
16
|
+
: "component map and its list disagree: run npx nextly generate:types"
|
|
17
|
+
: "component map and its list disagree: run npx nextly generate:types";
|
|
18
|
+
export const componentMapAgreesWithList: Agrees = true;
|
|
19
|
+
|
|
20
|
+
if (
|
|
21
|
+
JSON.stringify(Object.keys(registered).sort()) !==
|
|
22
|
+
JSON.stringify([...listed].sort())
|
|
23
|
+
) {
|
|
24
|
+
throw Object.assign(
|
|
25
|
+
new Error(
|
|
26
|
+
"The admin's component map and its list disagree: run npx nextly generate:types."
|
|
27
|
+
),
|
|
28
|
+
{ code: "NEXTLY_ADMIN_COMPONENT_MAP_MISMATCH" }
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export const registeredComponentPaths = listed;
|
|
@@ -20,7 +20,14 @@
|
|
|
20
20
|
],
|
|
21
21
|
"paths": {
|
|
22
22
|
"@/*": ["./src/*"],
|
|
23
|
-
"
|
|
23
|
+
"@/components/*": ["./src/app/(frontend)/components/*"],
|
|
24
|
+
"@nextly-config": ["./nextly.config.ts"],
|
|
25
|
+
// Pagefind's browser bundle is generated into public/ at build time, so it
|
|
26
|
+
// is imported by URL and cannot be resolved on disk while type-checking.
|
|
27
|
+
// A `declare module` cannot describe it either: TypeScript treats a rooted
|
|
28
|
+
// specifier as relative and rejects the declaration (TS2436). Templates
|
|
29
|
+
// without a search page simply never import it, leaving this mapping inert.
|
|
30
|
+
"/pagefind/pagefind.js": ["./src/types/pagefind.d.ts"]
|
|
24
31
|
}
|
|
25
32
|
},
|
|
26
33
|
"include": [
|
|
@@ -1,36 +1,67 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import { useEffect,
|
|
3
|
+
import { useEffect, useSyncExternalStore } from "react";
|
|
4
4
|
|
|
5
5
|
type Theme = "light" | "dark" | "system";
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
useEffect(() => {
|
|
11
|
-
const savedTheme = localStorage.getItem("theme") as Theme | null;
|
|
12
|
-
if (savedTheme) {
|
|
13
|
-
setTheme(savedTheme);
|
|
14
|
-
applyTheme(savedTheme);
|
|
15
|
-
} else {
|
|
16
|
-
applyTheme("system");
|
|
17
|
-
}
|
|
18
|
-
}, []);
|
|
7
|
+
const STORAGE_KEY = "theme";
|
|
8
|
+
const THEMES: readonly Theme[] = ["light", "dark", "system"];
|
|
9
|
+
const DARK_QUERY = "(prefers-color-scheme: dark)";
|
|
19
10
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
11
|
+
/**
|
|
12
|
+
* Everything showing the theme in this tab. The browser's `storage` event
|
|
13
|
+
* only reaches other tabs, so a change made here is announced here by hand.
|
|
14
|
+
*/
|
|
15
|
+
const listeners = new Set<() => void>();
|
|
25
16
|
|
|
26
|
-
|
|
17
|
+
function subscribe(listener: () => void) {
|
|
18
|
+
listeners.add(listener);
|
|
19
|
+
window.addEventListener("storage", listener);
|
|
20
|
+
return () => {
|
|
21
|
+
listeners.delete(listener);
|
|
22
|
+
window.removeEventListener("storage", listener);
|
|
27
23
|
};
|
|
24
|
+
}
|
|
28
25
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
26
|
+
/** The saved choice, or "system" when there is none or it is not one of the three. */
|
|
27
|
+
function readTheme(): Theme {
|
|
28
|
+
const saved = localStorage.getItem(STORAGE_KEY);
|
|
29
|
+
return THEMES.find(theme => theme === saved) ?? "system";
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function saveTheme(theme: Theme) {
|
|
33
|
+
localStorage.setItem(STORAGE_KEY, theme);
|
|
34
|
+
for (const listener of listeners) listener();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function applyTheme(theme: Theme) {
|
|
38
|
+
const isDark =
|
|
39
|
+
theme === "dark" ||
|
|
40
|
+
(theme === "system" && window.matchMedia(DARK_QUERY).matches);
|
|
41
|
+
document.documentElement.classList.toggle("dark", isDark);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function ThemeToggle() {
|
|
45
|
+
// Read from storage rather than copied into state by an effect. The server
|
|
46
|
+
// renders "system"; after hydration React swaps in the saved choice.
|
|
47
|
+
const theme = useSyncExternalStore<Theme>(
|
|
48
|
+
subscribe,
|
|
49
|
+
readTheme,
|
|
50
|
+
() => "system"
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
// Keep the page's colours in step with the choice, and with the system's
|
|
54
|
+
// setting for as long as "system" is the choice.
|
|
55
|
+
useEffect(() => {
|
|
56
|
+
applyTheme(theme);
|
|
57
|
+
if (theme !== "system") return;
|
|
58
|
+
const query = window.matchMedia(DARK_QUERY);
|
|
59
|
+
const follow = () => applyTheme("system");
|
|
60
|
+
query.addEventListener("change", follow);
|
|
61
|
+
return () => query.removeEventListener("change", follow);
|
|
62
|
+
}, [theme]);
|
|
63
|
+
|
|
64
|
+
const handleThemeChange = (newTheme: Theme) => saveTheme(newTheme);
|
|
34
65
|
|
|
35
66
|
return (
|
|
36
67
|
<div className="fixed top-6 right-6 flex items-center border border-foreground/10 bg-background p-1 z-50">
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import Link from "next/link";
|
|
2
2
|
|
|
3
3
|
import { ThemeToggle } from "@/components/ThemeToggle";
|
|
4
|
+
import { SITE_URL } from "@/lib/site-url";
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Blank-template landing page (public route via the (frontend) route group).
|
|
@@ -26,8 +27,7 @@ interface SetupStatus {
|
|
|
26
27
|
|
|
27
28
|
async function fetchSetupStatus(): Promise<SetupStatus | null> {
|
|
28
29
|
try {
|
|
29
|
-
const
|
|
30
|
-
const res = await fetch(`${baseUrl}/api/auth/setup-status`, {
|
|
30
|
+
const res = await fetch(`${SITE_URL}/api/auth/setup-status`, {
|
|
31
31
|
cache: "no-store",
|
|
32
32
|
});
|
|
33
33
|
if (!res.ok) return null;
|
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
@import "tailwindcss";
|
|
2
2
|
|
|
3
|
+
/* The families the imported font stylesheets declare, bound to the variables the rules
|
|
4
|
+
below read. Named here rather than by `next/font`, which used to inject them: the faces
|
|
5
|
+
now arrive as ordinary stylesheets, so the binding has to be written down somewhere, and
|
|
6
|
+
the name is load-bearing — the admin theme reads `--font-geist`. Quoted family names, so
|
|
7
|
+
the two-word values stay one identifier. */
|
|
8
|
+
:root {
|
|
9
|
+
/* `--font-geist` and `--font-geist-mono` are the names the ADMIN theme reads
|
|
10
|
+
(`--font-sans: var(--font-geist, Geist), ...` in a non-inline `@theme`, so
|
|
11
|
+
it resolves at `:root`). A template that loads a different face still has to
|
|
12
|
+
expose it under these names or the admin renders in a system fallback while
|
|
13
|
+
the app's own pages render correctly. */
|
|
14
|
+
--font-geist: "Inter Variable";
|
|
15
|
+
--font-geist-mono: "JetBrains Mono Variable";
|
|
16
|
+
/* This template's own display face, read by `.font-display` below. NOT named
|
|
17
|
+
`--font-mono` for its mono face: that is the theme's OWN token, declared at
|
|
18
|
+
`:root`, and redefining it here replaces the theme's whole fallback chain
|
|
19
|
+
rather than feeding it. */
|
|
20
|
+
--font-display: "Inter Variable";
|
|
21
|
+
}
|
|
22
|
+
|
|
3
23
|
/**
|
|
4
24
|
* Blank-template global styles.
|
|
5
25
|
*
|
|
@@ -28,6 +48,18 @@
|
|
|
28
48
|
--color-foreground: #f9fafb;
|
|
29
49
|
}
|
|
30
50
|
|
|
51
|
+
/* `@theme inline` on purpose, and it is the whole point of this block. An inline theme
|
|
52
|
+
SUBSTITUTES the value where the utility is used; a plain `@theme` emits the variable into
|
|
53
|
+
`:root`, which is where `@nextlyhq/admin` declares its own `--font-mono` — redefining it there
|
|
54
|
+
would replace the admin's entire fallback chain with this one family.
|
|
55
|
+
|
|
56
|
+
Needed because the frontend routes do not load the admin stylesheet, so nothing else binds the
|
|
57
|
+
`font-mono` utility this template uses on its badge and file-path text; without it they fall
|
|
58
|
+
back to Tailwind's default mono rather than the face the template loads. */
|
|
59
|
+
@theme inline {
|
|
60
|
+
--font-mono: var(--font-geist-mono, ui-monospace), monospace;
|
|
61
|
+
}
|
|
62
|
+
|
|
31
63
|
@theme {
|
|
32
64
|
--color-background: var(--color-background);
|
|
33
65
|
--color-foreground: var(--color-foreground);
|
|
@@ -67,7 +99,11 @@ body {
|
|
|
67
99
|
}
|
|
68
100
|
|
|
69
101
|
.font-display {
|
|
70
|
-
|
|
102
|
+
/* The generic families are the var()'s fallback argument rather than a list
|
|
103
|
+
after it: an unresolved var() with no fallback invalidates the whole
|
|
104
|
+
declaration, so the element would inherit its parent's face instead of
|
|
105
|
+
falling through to these. */
|
|
106
|
+
font-family: var(--font-display, ui-sans-serif), system-ui, sans-serif;
|
|
71
107
|
}
|
|
72
108
|
|
|
73
109
|
.btn-primary {
|
|
@@ -1,19 +1,33 @@
|
|
|
1
1
|
import type { Metadata } from "next";
|
|
2
|
-
import
|
|
2
|
+
import "@fontsource-variable/inter";
|
|
3
|
+
import "@fontsource-variable/jetbrains-mono";
|
|
3
4
|
|
|
4
5
|
import "./globals.css";
|
|
6
|
+
import { SITE_URL } from "@/lib/site-url";
|
|
5
7
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
8
|
+
/**
|
|
9
|
+
* The faces are imported as STYLESHEETS from packages in `node_modules`, rather
|
|
10
|
+
* than fetched from fonts.googleapis.com by `next/font/google` while
|
|
11
|
+
* `next build` runs — which made every build depend on reaching a third party
|
|
12
|
+
* and fail behind a proxy, on a locked-down runner, or offline.
|
|
13
|
+
*
|
|
14
|
+
* A bare package import rather than `next/font/local` pointing INTO
|
|
15
|
+
* `node_modules`: a literal path asserts where the package physically lives,
|
|
16
|
+
* and that assertion is false under Yarn's Plug'n'Play linker (no
|
|
17
|
+
* `node_modules` at all), under npm/Yarn workspace hoisting (the package moves
|
|
18
|
+
* to the workspace root), and under pnpm's own symlinked store. An import asks
|
|
19
|
+
* the resolver instead, which is correct under every layout.
|
|
20
|
+
*
|
|
21
|
+
* The trade is `next/font`'s metric-adjusted fallback, so text can shift
|
|
22
|
+
* slightly as the face arrives. The families are bound to this app's `--font-*`
|
|
23
|
+
* variables in `globals.css`, which is what every rule downstream reads. *
|
|
24
|
+
* Importing the package root ships every subset the face offers — Latin, Latin
|
|
25
|
+
* Extended, Cyrillic, Vietnamese — but each `@font-face` carries a
|
|
26
|
+
* `unicode-range`, so a browser downloads only the subsets the page actually
|
|
27
|
+
* uses. The deployed bundle is larger than a single hand-picked file; what a
|
|
28
|
+
* reader fetches is not, and a page in Cyrillic now gets its face instead of a
|
|
29
|
+
* fallback.
|
|
30
|
+
*/
|
|
17
31
|
|
|
18
32
|
/**
|
|
19
33
|
* Blank-template root layout.
|
|
@@ -24,9 +38,7 @@ const mono = JetBrains_Mono({
|
|
|
24
38
|
* blank-template pages can use relative OG image URLs without breaking.
|
|
25
39
|
*/
|
|
26
40
|
export const metadata: Metadata = {
|
|
27
|
-
metadataBase: new URL(
|
|
28
|
-
process.env.NEXT_PUBLIC_SITE_URL ?? "http://localhost:3000"
|
|
29
|
-
),
|
|
41
|
+
metadataBase: new URL(SITE_URL),
|
|
30
42
|
title: { default: "Nextly", template: "%s — Nextly" },
|
|
31
43
|
description: "A Nextly project.",
|
|
32
44
|
};
|
|
@@ -38,9 +50,7 @@ export default function RootLayout({
|
|
|
38
50
|
}>) {
|
|
39
51
|
return (
|
|
40
52
|
<html lang="en" suppressHydrationWarning>
|
|
41
|
-
<body className={
|
|
42
|
-
{children}
|
|
43
|
-
</body>
|
|
53
|
+
<body className="antialiased">{children}</body>
|
|
44
54
|
</html>
|
|
45
55
|
);
|
|
46
56
|
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Starts Nextly when the server starts, before it serves a request.
|
|
3
|
+
*
|
|
4
|
+
* Next.js runs `register()` once per server instance and finishes it before
|
|
5
|
+
* the instance handles traffic, in every serverless function as well as the
|
|
6
|
+
* admin's. Without it, Nextly starts on the first request that happens to
|
|
7
|
+
* need it, so a route that cannot start it alone, such as `/api/health`,
|
|
8
|
+
* answers as if the server could not serve until some other request has.
|
|
9
|
+
*
|
|
10
|
+
* It also registers Nextly's Next.js cache adapter here, so a Direct API write
|
|
11
|
+
* from any function flushes the cache tags it touches.
|
|
12
|
+
*
|
|
13
|
+
* Guarded to the Node.js runtime and imported dynamically inside the guard:
|
|
14
|
+
* Nextly needs Node-only packages that the Edge runtime cannot bundle. Next.js
|
|
15
|
+
* skips `register()` during `next build`, so this never opens a database
|
|
16
|
+
* connection at build time.
|
|
17
|
+
*/
|
|
18
|
+
export async function register(): Promise<void> {
|
|
19
|
+
if (process.env.NEXT_RUNTIME !== "nodejs") return;
|
|
20
|
+
|
|
21
|
+
const { registerNextCacheRevalidator } = await import("nextly/runtime");
|
|
22
|
+
registerNextCacheRevalidator();
|
|
23
|
+
|
|
24
|
+
const { createRegister } = await import("nextly");
|
|
25
|
+
const { default: config } = await import("../nextly.config");
|
|
26
|
+
// Next.js writes its Cache Components setting into this file as it builds
|
|
27
|
+
// it. Nextly, loaded from node_modules, cannot read that for itself, and
|
|
28
|
+
// warns when Cache Components is on, which it does not support yet.
|
|
29
|
+
await createRegister(config, {
|
|
30
|
+
cacheComponents: process.env.__NEXT_CACHE_COMPONENTS,
|
|
31
|
+
})();
|
|
32
|
+
}
|