@vexcms/next 0.1.0-alpha.12 → 0.1.0-alpha.14
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/dist/NextAdminLayout.js +3 -20
- package/dist/NextAdminLayout.js.map +1 -1
- package/dist/NextAdminPage.js +2 -137
- package/dist/NextAdminPage.js.map +1 -1
- package/dist/cache/createVexRevalidateRoute.d.ts +88 -0
- package/dist/cache/createVexRevalidateRoute.d.ts.map +1 -0
- package/dist/cache/createVexServerClient.d.ts +31 -0
- package/dist/cache/createVexServerClient.d.ts.map +1 -0
- package/dist/cache/index.d.ts +5 -0
- package/dist/cache/index.d.ts.map +1 -0
- package/dist/cache/index.js +9 -0
- package/dist/cache/index.js.map +1 -0
- package/dist/cache/types.d.ts +36 -0
- package/dist/cache/types.d.ts.map +1 -0
- package/dist/chunk-5AGM3PEX.js +121 -0
- package/dist/chunk-5AGM3PEX.js.map +1 -0
- package/dist/chunk-KIRSFMPI.js +78 -0
- package/dist/chunk-KIRSFMPI.js.map +1 -0
- package/dist/chunk-TR3Z3P6Q.js +143 -0
- package/dist/chunk-TR3Z3P6Q.js.map +1 -0
- package/dist/chunk-YCJNIS6O.js +25 -0
- package/dist/chunk-YCJNIS6O.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +48 -0
- package/dist/index.js.map +1 -0
- package/dist/seo/createVexRobots.d.ts +22 -0
- package/dist/seo/createVexRobots.d.ts.map +1 -0
- package/dist/seo/createVexSitemap.d.ts +41 -0
- package/dist/seo/createVexSitemap.d.ts.map +1 -0
- package/dist/seo/index.d.ts +5 -0
- package/dist/seo/index.d.ts.map +1 -0
- package/dist/seo/index.js +13 -0
- package/dist/seo/index.js.map +1 -0
- package/dist/seo/vexMetadata.d.ts +30 -0
- package/dist/seo/vexMetadata.d.ts.map +1 -0
- package/dist/seo/vexStaticParams.d.ts +25 -0
- package/dist/seo/vexStaticParams.d.ts.map +1 -0
- package/dist/testing/index.d.ts +2 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +3 -0
- package/dist/testing/index.js.map +1 -0
- package/package.json +24 -4
package/dist/NextAdminLayout.js
CHANGED
|
@@ -1,23 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
import { cookies } from "next/headers";
|
|
5
|
-
import { jsx } from "react/jsx-runtime";
|
|
6
|
-
async function NextAdminLayout(props) {
|
|
7
|
-
const clientConfig = sanitizeConfigForClient(props.config);
|
|
8
|
-
const cookieStore = await cookies();
|
|
9
|
-
const sidebarOpen = String(cookieStore.get("sidebar_state")?.value) === "true";
|
|
10
|
-
return /* @__PURE__ */ jsx(
|
|
11
|
-
NextAdminLayoutClient,
|
|
12
|
-
{
|
|
13
|
-
config: clientConfig,
|
|
14
|
-
user: props.user,
|
|
15
|
-
organization: props.organization,
|
|
16
|
-
sidebarOpen,
|
|
17
|
-
children: props.children
|
|
18
|
-
}
|
|
19
|
-
);
|
|
20
|
-
}
|
|
1
|
+
import {
|
|
2
|
+
NextAdminLayout
|
|
3
|
+
} from "./chunk-YCJNIS6O.js";
|
|
21
4
|
export {
|
|
22
5
|
NextAdminLayout
|
|
23
6
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":[
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
package/dist/NextAdminPage.js
CHANGED
|
@@ -1,141 +1,6 @@
|
|
|
1
|
-
// src/NextAdminPage.tsx
|
|
2
|
-
import { fetchQuery } from "convex/nextjs";
|
|
3
|
-
import { vexConvexApi } from "@vexcms/core";
|
|
4
1
|
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
CollectionEditView,
|
|
8
|
-
MediaCollectionEditView,
|
|
9
|
-
MediaCollectionListView,
|
|
10
|
-
GlobalsListView,
|
|
11
|
-
GlobalEditView
|
|
12
|
-
} from "@vexcms/react";
|
|
13
|
-
import { sanitizeConfigForClient } from "@vexcms/core";
|
|
14
|
-
import { jsx, jsxs } from "react/jsx-runtime";
|
|
15
|
-
async function NextAdminPage(props) {
|
|
16
|
-
const { path = [] } = await props.params;
|
|
17
|
-
const [collectionSlug, documentId] = path;
|
|
18
|
-
const clientConfig = sanitizeConfigForClient(props.config);
|
|
19
|
-
if (!collectionSlug) {
|
|
20
|
-
return /* @__PURE__ */ jsx(DashboardView, { config: clientConfig });
|
|
21
|
-
}
|
|
22
|
-
if (collectionSlug === "globals") {
|
|
23
|
-
if (!documentId) {
|
|
24
|
-
return /* @__PURE__ */ jsx(GlobalsListView, { config: clientConfig });
|
|
25
|
-
}
|
|
26
|
-
const globalConfig = clientConfig.globals.find((g) => g.slug === documentId);
|
|
27
|
-
if (!globalConfig) {
|
|
28
|
-
return /* @__PURE__ */ jsxs("div", { children: [
|
|
29
|
-
/* @__PURE__ */ jsxs("p", { className: "text-muted-foreground p-6", children: [
|
|
30
|
-
'Global "',
|
|
31
|
-
documentId,
|
|
32
|
-
'" not found.'
|
|
33
|
-
] }),
|
|
34
|
-
/* @__PURE__ */ jsx("p", { children: "TODO: add not found view" })
|
|
35
|
-
] });
|
|
36
|
-
}
|
|
37
|
-
const global = await fetchQuery(
|
|
38
|
-
vexConvexApi.globals.get,
|
|
39
|
-
{ slug: globalConfig.slug },
|
|
40
|
-
props.token ? { token: props.token } : void 0
|
|
41
|
-
);
|
|
42
|
-
return /* @__PURE__ */ jsx(GlobalEditView, { global: globalConfig, initialData: global });
|
|
43
|
-
}
|
|
44
|
-
const collection = clientConfig.collections.find((c) => c.slug === collectionSlug);
|
|
45
|
-
const mediaCollection = clientConfig.mediaCollections.find((mc) => mc.slug === collectionSlug);
|
|
46
|
-
if (!collection && !mediaCollection) {
|
|
47
|
-
return /* @__PURE__ */ jsxs("div", { children: [
|
|
48
|
-
/* @__PURE__ */ jsxs("p", { className: "text-muted-foreground p-6", children: [
|
|
49
|
-
'Collection "',
|
|
50
|
-
collectionSlug,
|
|
51
|
-
'" not found.'
|
|
52
|
-
] }),
|
|
53
|
-
/* @__PURE__ */ jsx("p", { children: "TODO: add not found view" })
|
|
54
|
-
] });
|
|
55
|
-
}
|
|
56
|
-
if (mediaCollection && documentId) {
|
|
57
|
-
const initialData2 = await fetchQuery(
|
|
58
|
-
vexConvexApi.get,
|
|
59
|
-
{
|
|
60
|
-
id: documentId,
|
|
61
|
-
collection: mediaCollection.slug
|
|
62
|
-
},
|
|
63
|
-
props.token ? { token: props.token } : void 0
|
|
64
|
-
);
|
|
65
|
-
return /* @__PURE__ */ jsx(
|
|
66
|
-
MediaCollectionEditView,
|
|
67
|
-
{
|
|
68
|
-
collection: mediaCollection,
|
|
69
|
-
documentId,
|
|
70
|
-
initialData: initialData2
|
|
71
|
-
}
|
|
72
|
-
);
|
|
73
|
-
}
|
|
74
|
-
if (mediaCollection) {
|
|
75
|
-
const numItems2 = Math.max(
|
|
76
|
-
mediaCollection.admin.table.serverPageSize,
|
|
77
|
-
mediaCollection.admin.table.defaultPageSize
|
|
78
|
-
);
|
|
79
|
-
const initialData2 = await fetchQuery(
|
|
80
|
-
vexConvexApi.find,
|
|
81
|
-
{
|
|
82
|
-
collection: collectionSlug,
|
|
83
|
-
paginationOpts: {
|
|
84
|
-
numItems: numItems2,
|
|
85
|
-
totalDocs: true,
|
|
86
|
-
cursor: null
|
|
87
|
-
}
|
|
88
|
-
},
|
|
89
|
-
props.token ? { token: props.token } : void 0
|
|
90
|
-
);
|
|
91
|
-
return /* @__PURE__ */ jsx(
|
|
92
|
-
MediaCollectionListView,
|
|
93
|
-
{
|
|
94
|
-
collection: mediaCollection,
|
|
95
|
-
initialData: initialData2
|
|
96
|
-
}
|
|
97
|
-
);
|
|
98
|
-
}
|
|
99
|
-
if (collection && documentId) {
|
|
100
|
-
const initialData2 = await fetchQuery(
|
|
101
|
-
vexConvexApi.get,
|
|
102
|
-
{
|
|
103
|
-
id: documentId,
|
|
104
|
-
collection: collection.slug
|
|
105
|
-
},
|
|
106
|
-
props.token ? { token: props.token } : void 0
|
|
107
|
-
);
|
|
108
|
-
return /* @__PURE__ */ jsx(
|
|
109
|
-
CollectionEditView,
|
|
110
|
-
{
|
|
111
|
-
collection,
|
|
112
|
-
documentId,
|
|
113
|
-
initialData: initialData2
|
|
114
|
-
}
|
|
115
|
-
);
|
|
116
|
-
}
|
|
117
|
-
if (!collection) {
|
|
118
|
-
throw new Error("invalid collection slug");
|
|
119
|
-
}
|
|
120
|
-
const numItems = Math.max(
|
|
121
|
-
collection.admin.table.serverPageSize,
|
|
122
|
-
collection.admin.table.defaultPageSize
|
|
123
|
-
);
|
|
124
|
-
const initialData = await fetchQuery(
|
|
125
|
-
vexConvexApi.findPaginated,
|
|
126
|
-
{
|
|
127
|
-
collection: collectionSlug,
|
|
128
|
-
depth: 1,
|
|
129
|
-
paginationOpts: {
|
|
130
|
-
cursor: null,
|
|
131
|
-
numItems,
|
|
132
|
-
totalDocs: true
|
|
133
|
-
}
|
|
134
|
-
},
|
|
135
|
-
props.token ? { token: props.token } : void 0
|
|
136
|
-
);
|
|
137
|
-
return /* @__PURE__ */ jsx(CollectionListView, { collection, initialData });
|
|
138
|
-
}
|
|
2
|
+
NextAdminPage
|
|
3
|
+
} from "./chunk-TR3Z3P6Q.js";
|
|
139
4
|
export {
|
|
140
5
|
NextAdminPage
|
|
141
6
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":[
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import type { CollectionSlug, VexConfig, VexDocument, VexRevalidateResponse } from "@vexcms/core";
|
|
2
|
+
import type { NextRequest } from "next/server";
|
|
3
|
+
import { NextResponse } from "next/server";
|
|
4
|
+
/** Props for {@link createVexRevalidateRoute}. */
|
|
5
|
+
export interface CreateVexRevalidateRouteProps {
|
|
6
|
+
/**
|
|
7
|
+
* The resolved VexCMS config. Supplies `access` for the write-permission
|
|
8
|
+
* check and `routes.map` for target resolution.
|
|
9
|
+
*/
|
|
10
|
+
config: VexConfig;
|
|
11
|
+
/**
|
|
12
|
+
* Resolves the authenticated user and active organization for the current
|
|
13
|
+
* request. Wire to `() => fetchAuthQuery(api.auth.api.getUserOrg, {})` — the
|
|
14
|
+
* same call the admin panel route already makes.
|
|
15
|
+
*/
|
|
16
|
+
getAuth: () => Promise<{
|
|
17
|
+
organization?: Record<string, unknown>;
|
|
18
|
+
user: null | Record<string, unknown>;
|
|
19
|
+
}>;
|
|
20
|
+
/**
|
|
21
|
+
* Reads the caller's session token for the current request. Wire to the host
|
|
22
|
+
* app's `getToken` export (`convexBetterAuthNextJs`'s output, re-exported
|
|
23
|
+
* from e.g. `~/auth/server`). A falsy return means "no session".
|
|
24
|
+
*/
|
|
25
|
+
getToken: () => Promise<null | string | undefined>;
|
|
26
|
+
/**
|
|
27
|
+
* Lists every document in a collection, for the collection-wide purge
|
|
28
|
+
* (`{ collection, all: true }`). Wire to a read through the app's own
|
|
29
|
+
* `publishedSlugs`-style query — the factory cannot know an app's generated
|
|
30
|
+
* Convex `api`, which is why this is injected like `getToken`/`getAuth`.
|
|
31
|
+
*
|
|
32
|
+
* Omit to leave the collection-wide branch unwired: it then purges nothing
|
|
33
|
+
* and reports that in `errors`, rather than silently succeeding.
|
|
34
|
+
*/
|
|
35
|
+
listCollection?: (collection: CollectionSlug) => Promise<Partial<VexDocument>[]>;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Creates a Next.js route handler that purges the cached paths affected by a
|
|
39
|
+
* document write. The admin panel calls this via `useVexMutation` after every
|
|
40
|
+
* successful create/update/delete, same-origin, as a relative fetch — no URL
|
|
41
|
+
* config, no shared secret.
|
|
42
|
+
*
|
|
43
|
+
* **Session-authorized, not secret-authorized.** The caller is a signed-in
|
|
44
|
+
* admin, so this reuses `getToken`/`getAuth` exactly like the admin panel route
|
|
45
|
+
* does, rather than introducing an env var or a shared secret. There is no
|
|
46
|
+
* API-key fallback: a caller with no browser session cannot reach this route as
|
|
47
|
+
* designed.
|
|
48
|
+
*
|
|
49
|
+
* **Path-based, not tag-based.** `revalidatePath` is the only purge mechanism.
|
|
50
|
+
* `cacheComponents: true` — required for `cacheTag`/`revalidateTag` — is
|
|
51
|
+
* incompatible with the `dynamic`/`runtime` segment configs the auth and admin
|
|
52
|
+
* routes require (measured: 5 files fail to build with it enabled), so
|
|
53
|
+
* `revalidateTag` is not an option in this codebase. Do not "upgrade" this to
|
|
54
|
+
* tag-based revalidation without first removing that segment-config dependency
|
|
55
|
+
* everywhere.
|
|
56
|
+
*
|
|
57
|
+
* **Never fails the caller past authorization.** A missing session returns 401,
|
|
58
|
+
* a session without write permission 403, and an oversized `changes` batch 413
|
|
59
|
+
* — all three reject before any map work runs. Past that point the handler
|
|
60
|
+
* never throws or returns 5xx: an unconfigured map, a throwing map, or an
|
|
61
|
+
* individual `revalidatePath` failure all resolve to 200 with the failures
|
|
62
|
+
* reported in the body. A failed purge is a stale page; failing the endpoint
|
|
63
|
+
* would only teach the fire-and-forget caller to retry pointlessly.
|
|
64
|
+
*
|
|
65
|
+
* @param props - Config and the host app's auth accessors.
|
|
66
|
+
* @returns `{ POST }` — mount directly as the route module's named export.
|
|
67
|
+
*
|
|
68
|
+
* @example
|
|
69
|
+
* ```ts
|
|
70
|
+
* // app/api/vex/revalidate/route.ts
|
|
71
|
+
* import { api } from "@convex/_generated/api";
|
|
72
|
+
* import { createVexRevalidateRoute } from "@vexcms/next/cache";
|
|
73
|
+
* import { fetchAuthQuery, getToken } from "~/auth/server";
|
|
74
|
+
* import config from "~/vex.config";
|
|
75
|
+
*
|
|
76
|
+
* export const { POST } = createVexRevalidateRoute({
|
|
77
|
+
* config,
|
|
78
|
+
* getToken,
|
|
79
|
+
* getAuth: () => fetchAuthQuery(api.auth.api.getUserOrg, {}),
|
|
80
|
+
* });
|
|
81
|
+
* ```
|
|
82
|
+
*/
|
|
83
|
+
export declare function createVexRevalidateRoute(props: CreateVexRevalidateRouteProps): {
|
|
84
|
+
POST: (request: NextRequest) => Promise<NextResponse<{
|
|
85
|
+
error: string;
|
|
86
|
+
} | VexRevalidateResponse>>;
|
|
87
|
+
};
|
|
88
|
+
//# sourceMappingURL=createVexRevalidateRoute.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"createVexRevalidateRoute.d.ts","sourceRoot":"","sources":["../../src/cache/createVexRevalidateRoute.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,cAAc,EAEd,SAAS,EACT,WAAW,EAIX,qBAAqB,EACtB,MAAM,cAAc,CAAC;AAGtB,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC/C,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE3C,kDAAkD;AAClD,MAAM,WAAW,6BAA6B;IAC5C;;;OAGG;IACH,MAAM,EAAE,SAAS,CAAC;IAClB;;;;OAIG;IACH,OAAO,EAAE,MAAM,OAAO,CAAC;QACrB,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;QACvC,IAAI,EAAE,IAAI,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;KACtC,CAAC,CAAC;IACH;;;;OAIG;IACH,QAAQ,EAAE,MAAM,OAAO,CAAC,IAAI,GAAG,MAAM,GAAG,SAAS,CAAC,CAAC;IACnD;;;;;;;;OAQG;IACH,cAAc,CAAC,EAAE,CAAC,UAAU,EAAE,cAAc,KAAK,OAAO,CAAC,OAAO,CAAC,WAAW,CAAC,EAAE,CAAC,CAAC;CAClF;AAoBD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6CG;AACH,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,6BAA6B,GAAG;IAC9E,IAAI,EAAE,CAAC,OAAO,EAAE,WAAW,KAAK,OAAO,CAAC,YAAY,CAAC;QAAE,KAAK,EAAE,MAAM,CAAA;KAAE,GAAG,qBAAqB,CAAC,CAAC,CAAC;CAClG,CA8GA"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { VexServerClientOptions, VexServerClient } from "./types";
|
|
2
|
+
/**
|
|
3
|
+
* Creates a server-only Convex read client safe to use in prerenderable
|
|
4
|
+
* Next.js routes.
|
|
5
|
+
*
|
|
6
|
+
* Wraps `ConvexHttpClient` from `convex/browser` instead of `convex/nextjs`'s
|
|
7
|
+
* `fetchQuery`, which hard-codes `client.setFetchOptions({ cache: "no-store" })`
|
|
8
|
+
* — the reason every route calling it builds dynamic (`ƒ`). This client never
|
|
9
|
+
* sets that option, so a route calling only `query()` can be prerendered and
|
|
10
|
+
* governed by the route's own `export const revalidate`.
|
|
11
|
+
*
|
|
12
|
+
* Every `query()` call is deduped via `React.cache` for the lifetime of one
|
|
13
|
+
* request: two identical `(query, args)` calls — e.g. `generateMetadata` and
|
|
14
|
+
* its page both reading the same document — resolve from a single Convex round
|
|
15
|
+
* trip instead of two.
|
|
16
|
+
*
|
|
17
|
+
* `React.cache` is instantiated fresh inside every `createVexServerClient()`
|
|
18
|
+
* call, so that dedupe only holds across calls sharing the SAME client
|
|
19
|
+
* instance — two clients built in two different modules cannot share a round
|
|
20
|
+
* trip even when they read identical `(query, args)`. Each app therefore
|
|
21
|
+
* creates exactly one instance, exported from its own `src/lib/vex.ts`, and
|
|
22
|
+
* every route or component imports that shared `vex` rather than calling
|
|
23
|
+
* `createVexServerClient()` itself.
|
|
24
|
+
*
|
|
25
|
+
* @param props - Client configuration.
|
|
26
|
+
* @returns A {@link VexServerClient} whose `query` method is deduped per request.
|
|
27
|
+
* @throws {Error} When no `url` is given and `process.env.NEXT_PUBLIC_CONVEX_URL`
|
|
28
|
+
* is unset.
|
|
29
|
+
*/
|
|
30
|
+
export declare function createVexServerClient(props?: VexServerClientOptions): VexServerClient;
|
|
31
|
+
//# sourceMappingURL=createVexServerClient.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"createVexServerClient.d.ts","sourceRoot":"","sources":["../../src/cache/createVexServerClient.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,sBAAsB,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAEvE;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,GAAE,sBAA2B,GAAG,eAAe,CA2BzF"}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { createVexRevalidateRoute } from "./createVexRevalidateRoute";
|
|
2
|
+
export type { CreateVexRevalidateRouteProps } from "./createVexRevalidateRoute";
|
|
3
|
+
export { createVexServerClient } from "./createVexServerClient";
|
|
4
|
+
export type { VexServerClientOptions, VexServerClient, } from "./types";
|
|
5
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/cache/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,wBAAwB,EAAE,MAAM,4BAA4B,CAAC;AACtE,YAAY,EAAE,6BAA6B,EAAE,MAAM,4BAA4B,CAAC;AAChF,OAAO,EAAE,qBAAqB,EAAE,MAAM,yBAAyB,CAAC;AAOhE,YAAY,EACV,sBAAsB,EACtB,eAAe,GAChB,MAAM,SAAS,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { FunctionReference, FunctionReturnType, OptionalRestArgs } from "convex/server";
|
|
2
|
+
/**
|
|
3
|
+
* Construction options for {@link createVexServerClient}.
|
|
4
|
+
*
|
|
5
|
+
* Nothing here configures caching — the client's only cache behaviour is that
|
|
6
|
+
* it never sets `cache: "no-store"` and dedupes per request via `React.cache`.
|
|
7
|
+
* Cache *policy* lives where Next requires it: `export const revalidate` in
|
|
8
|
+
* each route.
|
|
9
|
+
*/
|
|
10
|
+
export interface VexServerClientOptions {
|
|
11
|
+
/**
|
|
12
|
+
* Skip Convex's deployment URL format check — for self-hosted backends whose
|
|
13
|
+
* URL does not match `https://<name>.convex.cloud`.
|
|
14
|
+
*/
|
|
15
|
+
skipConvexDeploymentUrlCheck?: boolean;
|
|
16
|
+
/** Convex deployment URL. Defaults to `process.env.NEXT_PUBLIC_CONVEX_URL`. */
|
|
17
|
+
url?: string;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Server-only Convex read client returned by {@link createVexServerClient}.
|
|
21
|
+
*
|
|
22
|
+
* Exposes only `query` — this client is for prerenderable read paths, and it
|
|
23
|
+
* never sets `cache: "no-store"`, unlike `convex/nextjs`'s `fetchQuery`.
|
|
24
|
+
*/
|
|
25
|
+
export interface VexServerClient {
|
|
26
|
+
/**
|
|
27
|
+
* Runs a Convex query, deduped via `React.cache` for the lifetime of one
|
|
28
|
+
* request when called again with the same function reference and args.
|
|
29
|
+
*
|
|
30
|
+
* @param query - The Convex query function reference to call.
|
|
31
|
+
* @param args - Arguments for `query`, per Convex's `OptionalRestArgs`.
|
|
32
|
+
* @returns The query's resolved return value.
|
|
33
|
+
*/
|
|
34
|
+
query<Query extends FunctionReference<"query">>(query: Query, ...args: OptionalRestArgs<Query>): Promise<FunctionReturnType<Query>>;
|
|
35
|
+
}
|
|
36
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/cache/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAE7F;;;;;;;GAOG;AACH,MAAM,WAAW,sBAAsB;IACrC;;;OAGG;IACH,4BAA4B,CAAC,EAAE,OAAO,CAAC;IACvC,+EAA+E;IAC/E,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED;;;;;GAKG;AACH,MAAM,WAAW,eAAe;IAC9B;;;;;;;OAOG;IACH,KAAK,CAAC,KAAK,SAAS,iBAAiB,CAAC,OAAO,CAAC,EAC5C,KAAK,EAAE,KAAK,EACZ,GAAG,IAAI,EAAE,gBAAgB,CAAC,KAAK,CAAC,GAC/B,OAAO,CAAC,kBAAkB,CAAC,KAAK,CAAC,CAAC,CAAC;CACvC"}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
// src/cache/createVexRevalidateRoute.ts
|
|
2
|
+
import { hasPermission, resolveTargets, VEX_REVALIDATE_BATCH_SIZE } from "@vexcms/core";
|
|
3
|
+
import { revalidatePath } from "next/cache";
|
|
4
|
+
import { NextResponse } from "next/server";
|
|
5
|
+
function toCrudAction(operation) {
|
|
6
|
+
if (operation === "remove") return "delete";
|
|
7
|
+
if (operation === "upsert") return "update";
|
|
8
|
+
return operation;
|
|
9
|
+
}
|
|
10
|
+
function createVexRevalidateRoute(props) {
|
|
11
|
+
return {
|
|
12
|
+
async POST(request) {
|
|
13
|
+
const token = await props.getToken();
|
|
14
|
+
if (token === null || token === void 0 || token === "") {
|
|
15
|
+
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
|
16
|
+
}
|
|
17
|
+
let body;
|
|
18
|
+
try {
|
|
19
|
+
body = await request.json();
|
|
20
|
+
} catch {
|
|
21
|
+
return NextResponse.json({ error: "Bad Request" }, { status: 400 });
|
|
22
|
+
}
|
|
23
|
+
const { organization, user } = await props.getAuth();
|
|
24
|
+
const operation = "all" in body ? "update" : body.operation;
|
|
25
|
+
const action = toCrudAction(operation);
|
|
26
|
+
const allowed = hasPermission({
|
|
27
|
+
access: props.config.access,
|
|
28
|
+
action,
|
|
29
|
+
organization,
|
|
30
|
+
resource: body.collection,
|
|
31
|
+
user
|
|
32
|
+
});
|
|
33
|
+
if (!allowed) {
|
|
34
|
+
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
|
|
35
|
+
}
|
|
36
|
+
const errors = [];
|
|
37
|
+
let changes;
|
|
38
|
+
if ("all" in body) {
|
|
39
|
+
if (props.listCollection === void 0) {
|
|
40
|
+
errors.push(
|
|
41
|
+
new Error(
|
|
42
|
+
"createVexRevalidateRoute: a collection-wide purge was requested but `listCollection` is not wired"
|
|
43
|
+
)
|
|
44
|
+
);
|
|
45
|
+
changes = [];
|
|
46
|
+
} else {
|
|
47
|
+
try {
|
|
48
|
+
const docs = await props.listCollection(body.collection);
|
|
49
|
+
changes = docs.map((doc) => ({ after: doc }));
|
|
50
|
+
} catch (error) {
|
|
51
|
+
errors.push(error);
|
|
52
|
+
changes = [];
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
} else {
|
|
56
|
+
changes = body.changes;
|
|
57
|
+
if (changes.length > VEX_REVALIDATE_BATCH_SIZE) {
|
|
58
|
+
return NextResponse.json(
|
|
59
|
+
{
|
|
60
|
+
error: `Cannot revalidate more than ${VEX_REVALIDATE_BATCH_SIZE} changes in a single request`
|
|
61
|
+
},
|
|
62
|
+
{ status: 413 }
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
const routesConfig = props.config.routes;
|
|
67
|
+
if (routesConfig === void 0) {
|
|
68
|
+
return NextResponse.json({ errors, revalidated: [] }, { status: 200 });
|
|
69
|
+
}
|
|
70
|
+
const pool = [];
|
|
71
|
+
for (const change of changes) {
|
|
72
|
+
const target = resolveTargets({
|
|
73
|
+
after: change.after,
|
|
74
|
+
before: change.before,
|
|
75
|
+
collection: body.collection,
|
|
76
|
+
map: routesConfig.map,
|
|
77
|
+
operation: action
|
|
78
|
+
});
|
|
79
|
+
pool.push(...target.paths);
|
|
80
|
+
errors.push(...target.errors);
|
|
81
|
+
}
|
|
82
|
+
const revalidated = [];
|
|
83
|
+
for (const path of new Set(pool)) {
|
|
84
|
+
try {
|
|
85
|
+
revalidatePath(path);
|
|
86
|
+
revalidated.push(path);
|
|
87
|
+
} catch (error) {
|
|
88
|
+
errors.push(error);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return NextResponse.json({ errors, revalidated }, { status: 200 });
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// src/cache/createVexServerClient.ts
|
|
97
|
+
import { ConvexHttpClient } from "convex/browser";
|
|
98
|
+
import { cache } from "react";
|
|
99
|
+
function createVexServerClient(props = {}) {
|
|
100
|
+
const url = props.url ?? process.env.NEXT_PUBLIC_CONVEX_URL;
|
|
101
|
+
if (url === void 0 || url === "") {
|
|
102
|
+
throw new Error(
|
|
103
|
+
"createVexServerClient: no Convex deployment URL. Pass `url`, or set NEXT_PUBLIC_CONVEX_URL."
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
const client = new ConvexHttpClient(url, {
|
|
107
|
+
skipConvexDeploymentUrlCheck: props.skipConvexDeploymentUrlCheck
|
|
108
|
+
});
|
|
109
|
+
const cachedQuery = cache(
|
|
110
|
+
async (query, args) => client.query(query, args)
|
|
111
|
+
);
|
|
112
|
+
return {
|
|
113
|
+
query: async (query, ...args) => cachedQuery(query, args[0] ?? {})
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export {
|
|
118
|
+
createVexRevalidateRoute,
|
|
119
|
+
createVexServerClient
|
|
120
|
+
};
|
|
121
|
+
//# sourceMappingURL=chunk-5AGM3PEX.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/cache/createVexRevalidateRoute.ts","../src/cache/createVexServerClient.ts"],"sourcesContent":["import type {\n CollectionSlug,\n CrudWriteAction,\n VexConfig,\n VexDocument,\n VexMutationOperation,\n VexRevalidateChange,\n VexRevalidateRequest,\n VexRevalidateResponse,\n} from \"@vexcms/core\";\nimport { hasPermission, resolveTargets, VEX_REVALIDATE_BATCH_SIZE } from \"@vexcms/core\";\nimport { revalidatePath } from \"next/cache\";\nimport type { NextRequest } from \"next/server\";\nimport { NextResponse } from \"next/server\";\n\n/** Props for {@link createVexRevalidateRoute}. */\nexport interface CreateVexRevalidateRouteProps {\n /**\n * The resolved VexCMS config. Supplies `access` for the write-permission\n * check and `routes.map` for target resolution.\n */\n config: VexConfig;\n /**\n * Resolves the authenticated user and active organization for the current\n * request. Wire to `() => fetchAuthQuery(api.auth.api.getUserOrg, {})` — the\n * same call the admin panel route already makes.\n */\n getAuth: () => Promise<{\n organization?: Record<string, unknown>;\n user: null | Record<string, unknown>;\n }>;\n /**\n * Reads the caller's session token for the current request. Wire to the host\n * app's `getToken` export (`convexBetterAuthNextJs`'s output, re-exported\n * from e.g. `~/auth/server`). A falsy return means \"no session\".\n */\n getToken: () => Promise<null | string | undefined>;\n /**\n * Lists every document in a collection, for the collection-wide purge\n * (`{ collection, all: true }`). Wire to a read through the app's own\n * `publishedSlugs`-style query — the factory cannot know an app's generated\n * Convex `api`, which is why this is injected like `getToken`/`getAuth`.\n *\n * Omit to leave the collection-wide branch unwired: it then purges nothing\n * and reports that in `errors`, rather than silently succeeding.\n */\n listCollection?: (collection: CollectionSlug) => Promise<Partial<VexDocument>[]>;\n}\n\n/**\n * Maps the wire verb to the CRUD action `hasPermission` and `resolveTargets`\n * accept.\n *\n * `VexMutationOperation` is named for the `vexConvexApi` function that produced\n * the write (`vexConvexApi.remove`), while `hasPermission` only accepts\n * `CRUD_ACTIONS`. Mapping happens exactly once per request so the same value\n * drives both the permission check and target resolution.\n *\n * @param operation - The wire operation from the request body.\n * @returns The equivalent CRUD action.\n */\nfunction toCrudAction(operation: VexMutationOperation): CrudWriteAction {\n if (operation === \"remove\") return \"delete\";\n if (operation === \"upsert\") return \"update\";\n return operation;\n}\n\n/**\n * Creates a Next.js route handler that purges the cached paths affected by a\n * document write. The admin panel calls this via `useVexMutation` after every\n * successful create/update/delete, same-origin, as a relative fetch — no URL\n * config, no shared secret.\n *\n * **Session-authorized, not secret-authorized.** The caller is a signed-in\n * admin, so this reuses `getToken`/`getAuth` exactly like the admin panel route\n * does, rather than introducing an env var or a shared secret. There is no\n * API-key fallback: a caller with no browser session cannot reach this route as\n * designed.\n *\n * **Path-based, not tag-based.** `revalidatePath` is the only purge mechanism.\n * `cacheComponents: true` — required for `cacheTag`/`revalidateTag` — is\n * incompatible with the `dynamic`/`runtime` segment configs the auth and admin\n * routes require (measured: 5 files fail to build with it enabled), so\n * `revalidateTag` is not an option in this codebase. Do not \"upgrade\" this to\n * tag-based revalidation without first removing that segment-config dependency\n * everywhere.\n *\n * **Never fails the caller past authorization.** A missing session returns 401,\n * a session without write permission 403, and an oversized `changes` batch 413\n * — all three reject before any map work runs. Past that point the handler\n * never throws or returns 5xx: an unconfigured map, a throwing map, or an\n * individual `revalidatePath` failure all resolve to 200 with the failures\n * reported in the body. A failed purge is a stale page; failing the endpoint\n * would only teach the fire-and-forget caller to retry pointlessly.\n *\n * @param props - Config and the host app's auth accessors.\n * @returns `{ POST }` — mount directly as the route module's named export.\n *\n * @example\n * ```ts\n * // app/api/vex/revalidate/route.ts\n * import { api } from \"@convex/_generated/api\";\n * import { createVexRevalidateRoute } from \"@vexcms/next/cache\";\n * import { fetchAuthQuery, getToken } from \"~/auth/server\";\n * import config from \"~/vex.config\";\n *\n * export const { POST } = createVexRevalidateRoute({\n * config,\n * getToken,\n * getAuth: () => fetchAuthQuery(api.auth.api.getUserOrg, {}),\n * });\n * ```\n */\nexport function createVexRevalidateRoute(props: CreateVexRevalidateRouteProps): {\n POST: (request: NextRequest) => Promise<NextResponse<{ error: string } | VexRevalidateResponse>>;\n} {\n return {\n async POST(\n request: NextRequest,\n ): Promise<NextResponse<{ error: string } | VexRevalidateResponse>> {\n const token = await props.getToken();\n if (token === null || token === undefined || token === \"\") {\n return NextResponse.json({ error: \"Unauthorized\" }, { status: 401 });\n }\n\n let body: VexRevalidateRequest;\n try {\n body = (await request.json()) as VexRevalidateRequest;\n } catch {\n return NextResponse.json({ error: \"Bad Request\" }, { status: 400 });\n }\n\n const { organization, user } = await props.getAuth();\n\n // One mapping, used for BOTH the permission check and target resolution.\n const operation: VexMutationOperation = \"all\" in body ? \"update\" : body.operation;\n const action = toCrudAction(operation);\n\n const allowed = hasPermission({\n access: props.config.access,\n action,\n organization,\n resource: body.collection,\n user,\n });\n if (!allowed) {\n return NextResponse.json({ error: \"Forbidden\" }, { status: 403 });\n }\n\n const errors: unknown[] = [];\n let changes: VexRevalidateChange[];\n\n if (\"all\" in body) {\n // Server-derived, so no client-supplied batch to bound. Each document\n // is mapped as its current state.\n if (props.listCollection === undefined) {\n errors.push(\n new Error(\n \"createVexRevalidateRoute: a collection-wide purge was requested but `listCollection` is not wired\",\n ),\n );\n changes = [];\n } else {\n try {\n const docs = await props.listCollection(body.collection);\n changes = docs.map((doc) => ({ after: doc }));\n } catch (error) {\n // An unreachable deployment must not 500 a purge request.\n errors.push(error);\n changes = [];\n }\n }\n } else {\n changes = body.changes;\n if (changes.length > VEX_REVALIDATE_BATCH_SIZE) {\n return NextResponse.json(\n {\n error: `Cannot revalidate more than ${VEX_REVALIDATE_BATCH_SIZE} changes in a single request`,\n },\n { status: 413 },\n );\n }\n }\n\n const routesConfig = props.config.routes;\n if (routesConfig === undefined) {\n // A project with no route map still gets a working, harmless\n // endpoint rather than a 500. Any error already accumulated (e.g. an\n // unwired collection-wide purge) is still reported.\n return NextResponse.json({ errors, revalidated: [] }, { status: 200 });\n }\n\n const pool: string[] = [];\n\n for (const change of changes) {\n // A throwing map is contained inside `resolveTargets` — its failure\n // lands in that call's `errors` and never propagates here.\n const target = resolveTargets({\n after: change.after,\n before: change.before,\n collection: body.collection,\n map: routesConfig.map,\n operation: action,\n });\n pool.push(...target.paths);\n errors.push(...target.errors);\n }\n\n // Dedupe across the whole batch, preserving first-seen order — the same\n // contract `resolveTargets` applies to one document's before/after.\n const revalidated: string[] = [];\n for (const path of new Set(pool)) {\n try {\n revalidatePath(path);\n revalidated.push(path);\n } catch (error) {\n // `revalidatePath` is a Next API called here, so it is not covered by\n // `resolveTargets`' containment.\n errors.push(error);\n }\n }\n\n return NextResponse.json({ errors, revalidated }, { status: 200 });\n },\n };\n}\n","import type { FunctionReference, OptionalRestArgs } from \"convex/server\";\n\nimport { ConvexHttpClient } from \"convex/browser\";\nimport { cache } from \"react\";\n\nimport type { VexServerClientOptions, VexServerClient } from \"./types\";\n\n/**\n * Creates a server-only Convex read client safe to use in prerenderable\n * Next.js routes.\n *\n * Wraps `ConvexHttpClient` from `convex/browser` instead of `convex/nextjs`'s\n * `fetchQuery`, which hard-codes `client.setFetchOptions({ cache: \"no-store\" })`\n * — the reason every route calling it builds dynamic (`ƒ`). This client never\n * sets that option, so a route calling only `query()` can be prerendered and\n * governed by the route's own `export const revalidate`.\n *\n * Every `query()` call is deduped via `React.cache` for the lifetime of one\n * request: two identical `(query, args)` calls — e.g. `generateMetadata` and\n * its page both reading the same document — resolve from a single Convex round\n * trip instead of two.\n *\n * `React.cache` is instantiated fresh inside every `createVexServerClient()`\n * call, so that dedupe only holds across calls sharing the SAME client\n * instance — two clients built in two different modules cannot share a round\n * trip even when they read identical `(query, args)`. Each app therefore\n * creates exactly one instance, exported from its own `src/lib/vex.ts`, and\n * every route or component imports that shared `vex` rather than calling\n * `createVexServerClient()` itself.\n *\n * @param props - Client configuration.\n * @returns A {@link VexServerClient} whose `query` method is deduped per request.\n * @throws {Error} When no `url` is given and `process.env.NEXT_PUBLIC_CONVEX_URL`\n * is unset.\n */\nexport function createVexServerClient(props: VexServerClientOptions = {}): VexServerClient {\n const url = props.url ?? process.env.NEXT_PUBLIC_CONVEX_URL;\n if (url === undefined || url === \"\") {\n throw new Error(\n \"createVexServerClient: no Convex deployment URL. Pass `url`, or set NEXT_PUBLIC_CONVEX_URL.\",\n );\n }\n\n const client = new ConvexHttpClient(url, {\n skipConvexDeploymentUrlCheck: props.skipConvexDeploymentUrlCheck,\n });\n\n // Deliberately NOT `client.setFetchOptions({ cache: \"no-store\" })` — that is\n // exactly what `convex/nextjs`'s `fetchQuery` does and what forces every\n // consuming route dynamic.\n\n // `React.cache` keys on the argument list, so the entry is (query reference,\n // args) — never the reference alone. Args are normalised to a single value so\n // `query(fn)` and `query(fn, {})` share one entry.\n const cachedQuery = cache(\n async (query: FunctionReference<\"query\">, args: unknown): Promise<unknown> =>\n client.query(query, args as OptionalRestArgs<FunctionReference<\"query\">>[0]),\n );\n\n return {\n query: async (query, ...args) => cachedQuery(query, args[0] ?? {}),\n };\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// src/seo/createVexRobots.ts
|
|
2
|
+
function createVexRobots(props) {
|
|
3
|
+
return function robots() {
|
|
4
|
+
const disallow = props.disallow ?? [];
|
|
5
|
+
return {
|
|
6
|
+
rules: {
|
|
7
|
+
allow: "/",
|
|
8
|
+
userAgent: "*",
|
|
9
|
+
...disallow.length === 0 ? {} : { disallow }
|
|
10
|
+
},
|
|
11
|
+
sitemap: `${props.siteUrl}/sitemap.xml`
|
|
12
|
+
};
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// src/seo/createVexSitemap.ts
|
|
17
|
+
function createVexSitemap(props) {
|
|
18
|
+
return async function sitemap() {
|
|
19
|
+
try {
|
|
20
|
+
const items = await props.client.query(props.query, props.args ?? {});
|
|
21
|
+
if (!Array.isArray(items)) {
|
|
22
|
+
return [];
|
|
23
|
+
}
|
|
24
|
+
const { getUpdatedAt } = props;
|
|
25
|
+
return items.map((item) => {
|
|
26
|
+
const updatedAt = getUpdatedAt?.(item);
|
|
27
|
+
return {
|
|
28
|
+
url: props.toUrl(props.getSlug(item)),
|
|
29
|
+
...typeof updatedAt === "number" && Number.isFinite(updatedAt) ? { lastModified: new Date(updatedAt) } : {}
|
|
30
|
+
};
|
|
31
|
+
});
|
|
32
|
+
} catch {
|
|
33
|
+
return [];
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// src/seo/vexMetadata.ts
|
|
39
|
+
function vexMetadata(props) {
|
|
40
|
+
return {
|
|
41
|
+
alternates: { canonical: props.path },
|
|
42
|
+
description: props.description,
|
|
43
|
+
metadataBase: new URL(props.siteUrl),
|
|
44
|
+
// `openGraph` is ALWAYS emitted. Gating the whole block on an image is the
|
|
45
|
+
// defect Step 1 fixed in `apps/www/src/lib/metadata.ts`: a page with no
|
|
46
|
+
// uploaded image published no `og:title` and no `og:description` at all.
|
|
47
|
+
// Only `images` is conditional.
|
|
48
|
+
openGraph: {
|
|
49
|
+
description: props.description,
|
|
50
|
+
title: props.title,
|
|
51
|
+
...props.imageUrl === void 0 || props.imageUrl === "" ? {} : { images: [{ url: props.imageUrl }] }
|
|
52
|
+
},
|
|
53
|
+
title: props.title
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// src/seo/vexStaticParams.ts
|
|
58
|
+
async function vexStaticParams(props) {
|
|
59
|
+
try {
|
|
60
|
+
const items = await props.client.query(props.query, props.args ?? {});
|
|
61
|
+
if (!Array.isArray(items)) {
|
|
62
|
+
return [];
|
|
63
|
+
}
|
|
64
|
+
return items.map((item) => ({
|
|
65
|
+
[props.paramName]: props.getSlug(item)
|
|
66
|
+
}));
|
|
67
|
+
} catch {
|
|
68
|
+
return [];
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export {
|
|
73
|
+
createVexRobots,
|
|
74
|
+
createVexSitemap,
|
|
75
|
+
vexMetadata,
|
|
76
|
+
vexStaticParams
|
|
77
|
+
};
|
|
78
|
+
//# sourceMappingURL=chunk-KIRSFMPI.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/seo/createVexRobots.ts","../src/seo/createVexSitemap.ts","../src/seo/vexMetadata.ts","../src/seo/vexStaticParams.ts"],"sourcesContent":["import type { MetadataRoute } from \"next\";\n\n/**\n * Creates a Next.js `robots.ts` default export that allows crawlers across the\n * site and points them at the generated sitemap.\n *\n * @param props - Robots configuration.\n * @returns A `robots()` function suitable as `app/robots.ts`'s default export.\n */\nexport function createVexRobots(props: {\n /**\n * Paths to disallow. Every vexcms template passes `[\"/admin\", \"/api\"]` — the\n * admin panel is authenticated and neither belongs in an index. Omitted\n * entirely when absent or empty, rather than emitted as an empty array.\n */\n disallow?: string[];\n /**\n * The site's absolute origin, no trailing slash (e.g. `https://example.com`).\n * The sitemap URL is derived as `${siteUrl}/sitemap.xml`.\n */\n siteUrl: string;\n}): () => MetadataRoute.Robots {\n return function robots(): MetadataRoute.Robots {\n const disallow = props.disallow ?? [];\n return {\n rules: {\n allow: \"/\",\n userAgent: \"*\",\n ...(disallow.length === 0 ? {} : { disallow }),\n },\n sitemap: `${props.siteUrl}/sitemap.xml`,\n };\n };\n}\n","import type { FunctionReference, FunctionReturnType, OptionalRestArgs } from \"convex/server\";\nimport type { MetadataRoute } from \"next\";\n\nimport type { VexServerClient } from \"../cache/types\";\n\ntype ArrayItem<T> = T extends (infer Item)[] ? Item : never;\n\n/**\n * Creates a Next.js `sitemap.ts` default export from a Convex published-slugs\n * query (`packages/core/src/api/publishedSlugs/server.ts`).\n *\n * Tolerates an unreachable Convex deployment (P-020): a read failure returns\n * `[]` — an empty sitemap — rather than failing the build.\n *\n * @param props - Sitemap configuration.\n * @returns A `sitemap()` function suitable as `app/sitemap.ts`'s default export.\n */\nexport function createVexSitemap<Query extends FunctionReference<\"query\">>(props: {\n args?: OptionalRestArgs<Query>[0];\n client: VexServerClient;\n getSlug: (item: ArrayItem<FunctionReturnType<Query>>) => string;\n /**\n * Optional last-modified extractor. Omit it when the collection carries no\n * modification timestamp — Convex's `_creationTime` is set at insert and\n * never moves, so reporting it as `lastModified` would be a lie.\n * `lastModified` is optional in the sitemap protocol; an absent value is\n * correct, a wrong one costs crawl budget.\n *\n * May return `undefined` for an individual item, and that is the common\n * case: `defineCollection` injects `updatedAt` on every content collection,\n * but only a write through `create`/`update` populates it — a seeded or\n * imported row has none. Those entries are emitted with no `lastModified`\n * rather than an `Invalid Date`.\n */\n getUpdatedAt?: (item: ArrayItem<FunctionReturnType<Query>>) => number | undefined;\n /**\n * Maps one item's slug to its absolute public URL — the route mapper, so URL\n * shape (e.g. `/blog/[slug]` vs `/[slug]`) is the caller's choice.\n */\n toUrl: (slug: string) => string;\n query: Query;\n}): () => Promise<MetadataRoute.Sitemap> {\n return async function sitemap(): Promise<MetadataRoute.Sitemap> {\n try {\n const items = await props.client.query(props.query, props.args ?? {});\n if (!Array.isArray(items)) {\n return [];\n }\n const { getUpdatedAt } = props;\n // `Array.isArray` widens a generic return to `any[]`, so the element\n // type is restated here rather than asserted on `items`.\n return items.map((item: ArrayItem<FunctionReturnType<Query>>) => {\n // Resolved per item, not per call: a collection can carry `updatedAt`\n // while an individual seeded or imported row has never been written\n // through the API. `new Date(undefined)` is an `Invalid Date`, which\n // serializes to a `<lastmod>` no crawler can parse, so the tag is\n // dropped for that entry instead.\n const updatedAt = getUpdatedAt?.(item);\n return {\n url: props.toUrl(props.getSlug(item)),\n ...(typeof updatedAt === \"number\" && Number.isFinite(updatedAt)\n ? { lastModified: new Date(updatedAt) }\n : {}),\n };\n });\n } catch {\n // Unreachable deployment, or any other read failure.\n return [];\n }\n };\n}\n","import type { Metadata } from \"next\";\n\n/**\n * Builds a Next.js `Metadata` object with unconditional title/description, a\n * conditional OG image, `metadataBase`, and a canonical link.\n *\n * Generalizes what Step 1 fixed by hand in `apps/www/src/lib/metadata.ts`:\n * title and description are set regardless of whether an OG image resolved —\n * only the `openGraph.images` entry is conditional on `props.imageUrl`. The\n * original gated the whole `openGraph` block on the image, so a page without\n * one published no OG title or description at all.\n *\n * This FORMATS metadata; it does not read Convex. Resolving which title and\n * description win is the caller's job, because only the caller knows the\n * page/site precedence rules.\n *\n * @param props - Metadata inputs.\n * @returns A `Metadata` object for a page's `generateMetadata` or static\n * `metadata` export.\n */\nexport function vexMetadata(props: {\n description?: string;\n /** Absolute OG image URL. Omit to emit `openGraph` without an image. */\n imageUrl?: string;\n /** The page's path (e.g. `/about`), used for the canonical link. */\n path: string;\n /** The site's absolute origin, used for `metadataBase`. */\n siteUrl: string;\n title: string;\n}): Metadata {\n return {\n alternates: { canonical: props.path },\n description: props.description,\n metadataBase: new URL(props.siteUrl),\n // `openGraph` is ALWAYS emitted. Gating the whole block on an image is the\n // defect Step 1 fixed in `apps/www/src/lib/metadata.ts`: a page with no\n // uploaded image published no `og:title` and no `og:description` at all.\n // Only `images` is conditional.\n openGraph: {\n description: props.description,\n title: props.title,\n ...(props.imageUrl === undefined || props.imageUrl === \"\"\n ? {}\n : { images: [{ url: props.imageUrl }] }),\n },\n title: props.title,\n };\n}\n","import type { FunctionReference, FunctionReturnType, OptionalRestArgs } from \"convex/server\";\n\nimport type { VexServerClient } from \"../cache/types\";\n\ntype ArrayItem<T> = T extends (infer Item)[] ? Item : never;\n\n/**\n * Builds Next.js `generateStaticParams` output for a slug-driven route from a\n * Convex query.\n *\n * Returns `[]` when the read fails — including an unreachable Convex\n * deployment, which every CI build hits because it supplies placeholder env\n * rather than skipping validation (P-020). A route with no static params still\n * renders correctly on demand; it is simply not prerendered until the next\n * build.\n *\n * @param props - Static params configuration.\n * @returns `{ [paramName]: string }[]`, or `[]` if the read fails.\n */\nexport async function vexStaticParams<Query extends FunctionReference<\"query\">>(props: {\n args?: OptionalRestArgs<Query>[0];\n client: VexServerClient;\n getSlug: (item: ArrayItem<FunctionReturnType<Query>>) => string;\n paramName: string;\n query: Query;\n}): Promise<Record<string, string>[]> {\n try {\n const items = await props.client.query(props.query, props.args ?? {});\n if (!Array.isArray(items)) {\n return [];\n }\n // `Array.isArray` widens a generic return to `any[]`, so the element type\n // is restated here rather than asserted on `items`.\n return items.map((item: ArrayItem<FunctionReturnType<Query>>) => ({\n [props.paramName]: props.getSlug(item),\n }));\n } catch {\n // Unreachable deployment, or any other read failure. An empty list means\n // \"prerender nothing\", which is correct — the route still renders on\n // demand — and never fails the build.\n return [];\n }\n}\n"],"mappings":";AASO,SAAS,gBAAgB,OAYD;AAC7B,SAAO,SAAS,SAA+B;AAC7C,UAAM,WAAW,MAAM,YAAY,CAAC;AACpC,WAAO;AAAA,MACL,OAAO;AAAA,QACL,OAAO;AAAA,QACP,WAAW;AAAA,QACX,GAAI,SAAS,WAAW,IAAI,CAAC,IAAI,EAAE,SAAS;AAAA,MAC9C;AAAA,MACA,SAAS,GAAG,MAAM,OAAO;AAAA,IAC3B;AAAA,EACF;AACF;;;AChBO,SAAS,iBAA2D,OAwBlC;AACvC,SAAO,eAAe,UAA0C;AAC9D,QAAI;AACF,YAAM,QAAQ,MAAM,MAAM,OAAO,MAAM,MAAM,OAAO,MAAM,QAAQ,CAAC,CAAC;AACpE,UAAI,CAAC,MAAM,QAAQ,KAAK,GAAG;AACzB,eAAO,CAAC;AAAA,MACV;AACA,YAAM,EAAE,aAAa,IAAI;AAGzB,aAAO,MAAM,IAAI,CAAC,SAA+C;AAM/D,cAAM,YAAY,eAAe,IAAI;AACrC,eAAO;AAAA,UACL,KAAK,MAAM,MAAM,MAAM,QAAQ,IAAI,CAAC;AAAA,UACpC,GAAI,OAAO,cAAc,YAAY,OAAO,SAAS,SAAS,IAC1D,EAAE,cAAc,IAAI,KAAK,SAAS,EAAE,IACpC,CAAC;AAAA,QACP;AAAA,MACF,CAAC;AAAA,IACH,QAAQ;AAEN,aAAO,CAAC;AAAA,IACV;AAAA,EACF;AACF;;;AClDO,SAAS,YAAY,OASf;AACX,SAAO;AAAA,IACL,YAAY,EAAE,WAAW,MAAM,KAAK;AAAA,IACpC,aAAa,MAAM;AAAA,IACnB,cAAc,IAAI,IAAI,MAAM,OAAO;AAAA;AAAA;AAAA;AAAA;AAAA,IAKnC,WAAW;AAAA,MACT,aAAa,MAAM;AAAA,MACnB,OAAO,MAAM;AAAA,MACb,GAAI,MAAM,aAAa,UAAa,MAAM,aAAa,KACnD,CAAC,IACD,EAAE,QAAQ,CAAC,EAAE,KAAK,MAAM,SAAS,CAAC,EAAE;AAAA,IAC1C;AAAA,IACA,OAAO,MAAM;AAAA,EACf;AACF;;;AC5BA,eAAsB,gBAA0D,OAM1C;AACpC,MAAI;AACF,UAAM,QAAQ,MAAM,MAAM,OAAO,MAAM,MAAM,OAAO,MAAM,QAAQ,CAAC,CAAC;AACpE,QAAI,CAAC,MAAM,QAAQ,KAAK,GAAG;AACzB,aAAO,CAAC;AAAA,IACV;AAGA,WAAO,MAAM,IAAI,CAAC,UAAgD;AAAA,MAChE,CAAC,MAAM,SAAS,GAAG,MAAM,QAAQ,IAAI;AAAA,IACvC,EAAE;AAAA,EACJ,QAAQ;AAIN,WAAO,CAAC;AAAA,EACV;AACF;","names":[]}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
// src/NextAdminPage.tsx
|
|
2
|
+
import { fetchQuery } from "convex/nextjs";
|
|
3
|
+
import { vexConvexApi } from "@vexcms/core";
|
|
4
|
+
import {
|
|
5
|
+
DashboardView,
|
|
6
|
+
CollectionListView,
|
|
7
|
+
CollectionEditView,
|
|
8
|
+
MediaCollectionEditView,
|
|
9
|
+
MediaCollectionListView,
|
|
10
|
+
GlobalsListView,
|
|
11
|
+
GlobalEditView
|
|
12
|
+
} from "@vexcms/react";
|
|
13
|
+
import { sanitizeConfigForClient } from "@vexcms/core";
|
|
14
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
15
|
+
async function NextAdminPage(props) {
|
|
16
|
+
const { path = [] } = await props.params;
|
|
17
|
+
const [collectionSlug, documentId] = path;
|
|
18
|
+
const clientConfig = sanitizeConfigForClient(props.config);
|
|
19
|
+
if (!collectionSlug) {
|
|
20
|
+
return /* @__PURE__ */ jsx(DashboardView, { config: clientConfig });
|
|
21
|
+
}
|
|
22
|
+
if (collectionSlug === "globals") {
|
|
23
|
+
if (!documentId) {
|
|
24
|
+
return /* @__PURE__ */ jsx(GlobalsListView, { config: clientConfig });
|
|
25
|
+
}
|
|
26
|
+
const globalConfig = clientConfig.globals.find((g) => g.slug === documentId);
|
|
27
|
+
if (!globalConfig) {
|
|
28
|
+
return /* @__PURE__ */ jsxs("div", { children: [
|
|
29
|
+
/* @__PURE__ */ jsxs("p", { className: "text-muted-foreground p-6", children: [
|
|
30
|
+
'Global "',
|
|
31
|
+
documentId,
|
|
32
|
+
'" not found.'
|
|
33
|
+
] }),
|
|
34
|
+
/* @__PURE__ */ jsx("p", { children: "TODO: add not found view" })
|
|
35
|
+
] });
|
|
36
|
+
}
|
|
37
|
+
const global = await fetchQuery(
|
|
38
|
+
vexConvexApi.globals.get,
|
|
39
|
+
{ slug: globalConfig.slug },
|
|
40
|
+
props.token ? { token: props.token } : void 0
|
|
41
|
+
);
|
|
42
|
+
return /* @__PURE__ */ jsx(GlobalEditView, { global: globalConfig, initialData: global });
|
|
43
|
+
}
|
|
44
|
+
const collection = clientConfig.collections.find((c) => c.slug === collectionSlug);
|
|
45
|
+
const mediaCollection = clientConfig.mediaCollections.find((mc) => mc.slug === collectionSlug);
|
|
46
|
+
if (!collection && !mediaCollection) {
|
|
47
|
+
return /* @__PURE__ */ jsxs("div", { children: [
|
|
48
|
+
/* @__PURE__ */ jsxs("p", { className: "text-muted-foreground p-6", children: [
|
|
49
|
+
'Collection "',
|
|
50
|
+
collectionSlug,
|
|
51
|
+
'" not found.'
|
|
52
|
+
] }),
|
|
53
|
+
/* @__PURE__ */ jsx("p", { children: "TODO: add not found view" })
|
|
54
|
+
] });
|
|
55
|
+
}
|
|
56
|
+
if (mediaCollection && documentId) {
|
|
57
|
+
const initialData2 = await fetchQuery(
|
|
58
|
+
vexConvexApi.get,
|
|
59
|
+
{
|
|
60
|
+
id: documentId,
|
|
61
|
+
collection: mediaCollection.slug
|
|
62
|
+
},
|
|
63
|
+
props.token ? { token: props.token } : void 0
|
|
64
|
+
);
|
|
65
|
+
return /* @__PURE__ */ jsx(
|
|
66
|
+
MediaCollectionEditView,
|
|
67
|
+
{
|
|
68
|
+
collection: mediaCollection,
|
|
69
|
+
documentId,
|
|
70
|
+
initialData: initialData2
|
|
71
|
+
}
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
if (mediaCollection) {
|
|
75
|
+
const numItems2 = Math.max(
|
|
76
|
+
mediaCollection.admin.table.serverPageSize,
|
|
77
|
+
mediaCollection.admin.table.defaultPageSize
|
|
78
|
+
);
|
|
79
|
+
const initialData2 = await fetchQuery(
|
|
80
|
+
vexConvexApi.find,
|
|
81
|
+
{
|
|
82
|
+
collection: collectionSlug,
|
|
83
|
+
paginationOpts: {
|
|
84
|
+
numItems: numItems2,
|
|
85
|
+
totalDocs: true,
|
|
86
|
+
cursor: null
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
props.token ? { token: props.token } : void 0
|
|
90
|
+
);
|
|
91
|
+
return /* @__PURE__ */ jsx(
|
|
92
|
+
MediaCollectionListView,
|
|
93
|
+
{
|
|
94
|
+
collection: mediaCollection,
|
|
95
|
+
initialData: initialData2
|
|
96
|
+
}
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
if (collection && documentId) {
|
|
100
|
+
const initialData2 = await fetchQuery(
|
|
101
|
+
vexConvexApi.get,
|
|
102
|
+
{
|
|
103
|
+
id: documentId,
|
|
104
|
+
collection: collection.slug
|
|
105
|
+
},
|
|
106
|
+
props.token ? { token: props.token } : void 0
|
|
107
|
+
);
|
|
108
|
+
return /* @__PURE__ */ jsx(
|
|
109
|
+
CollectionEditView,
|
|
110
|
+
{
|
|
111
|
+
collection,
|
|
112
|
+
documentId,
|
|
113
|
+
initialData: initialData2
|
|
114
|
+
}
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
if (!collection) {
|
|
118
|
+
throw new Error("invalid collection slug");
|
|
119
|
+
}
|
|
120
|
+
const numItems = Math.max(
|
|
121
|
+
collection.admin.table.serverPageSize,
|
|
122
|
+
collection.admin.table.defaultPageSize
|
|
123
|
+
);
|
|
124
|
+
const initialData = await fetchQuery(
|
|
125
|
+
vexConvexApi.findPaginated,
|
|
126
|
+
{
|
|
127
|
+
collection: collectionSlug,
|
|
128
|
+
depth: 1,
|
|
129
|
+
paginationOpts: {
|
|
130
|
+
cursor: null,
|
|
131
|
+
numItems,
|
|
132
|
+
totalDocs: true
|
|
133
|
+
}
|
|
134
|
+
},
|
|
135
|
+
props.token ? { token: props.token } : void 0
|
|
136
|
+
);
|
|
137
|
+
return /* @__PURE__ */ jsx(CollectionListView, { collection, initialData });
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export {
|
|
141
|
+
NextAdminPage
|
|
142
|
+
};
|
|
143
|
+
//# sourceMappingURL=chunk-TR3Z3P6Q.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/NextAdminPage.tsx"],"sourcesContent":["import { fetchQuery } from \"convex/nextjs\";\nimport { vexConvexApi } from \"@vexcms/core\";\nimport type { CollectionSlug, PaginationResult, VexConfig, VexMediaDocument } from \"@vexcms/core\";\nimport {\n DashboardView,\n CollectionListView,\n CollectionEditView,\n MediaCollectionEditView,\n MediaCollectionListView,\n GlobalsListView,\n GlobalEditView,\n} from \"@vexcms/react\";\nimport { sanitizeConfigForClient } from \"@vexcms/core\";\n\n/**\n * VexCMS admin page server component for Next.js.\n *\n * An `async` server component that routes by the `[[...slug]]` catch-all\n * params, prefetches Convex data via `fetchQuery`, and renders the correct\n * view component. Does **not** include a layout wrapper — `VexAdminLayout`\n * in `app/admin/layout.tsx` owns the persistent shell.\n *\n * **Route mapping:**\n * | `path` array | View |\n * |---|---|\n * | `[]` or undefined | `DashboardView` |\n * | `[collectionSlug]` | `CollectionListView` with preloaded docs |\n * | `[collectionSlug, \"new\"]` | `CollectionEditView` (empty form) |\n * | `[collectionSlug, documentId]` | `CollectionEditView` with preloaded doc |\n *\n * @param props - Component props\n * @param props.config - The resolved VexCMS configuration from `vex.config.ts`\n * @param props.params - Next.js 15 async params `{ path?: string[] }`\n * @returns The appropriate admin view for the current URL path.\n *\n * @example\n * ```tsx\n * // app/admin/[[...slug]]/page.tsx\n * import { VexAdminPage } from \"@vexcms/next\";\n * import config from \"../../../../vex.config\";\n *\n * export default function AdminPage({\n * params,\n * }: {\n * params: Promise<{ path?: string[] }>;\n * }) {\n * return <VexAdminPage config={config} params={params} />;\n * }\n * ```\n */\nexport async function NextAdminPage(props: {\n config: VexConfig;\n params: Promise<{ path?: string[] }>;\n token?: string;\n}) {\n const { path = [] } = await props.params;\n const [collectionSlug, documentId] = path;\n\n // Sanitize config for client components (strips storageAdapters, recursively sanitizes mediaCollections)\n const clientConfig = sanitizeConfigForClient(props.config);\n\n if (!collectionSlug) {\n return <DashboardView config={clientConfig} />;\n }\n\n if (collectionSlug === \"globals\") {\n if (!documentId) {\n return <GlobalsListView config={clientConfig} />;\n }\n // Validate the route param against the registered globals first — the\n // found config's `slug` carries the narrowed GlobalSlug type, so no cast\n // of the raw URL segment is needed downstream.\n const globalConfig = clientConfig.globals.find((g) => g.slug === documentId);\n if (!globalConfig) {\n return (\n <div>\n <p className=\"text-muted-foreground p-6\">Global "{documentId}" not found.</p>\n <p>TODO: add not found view</p>\n </div>\n );\n }\n const global = await fetchQuery(\n vexConvexApi.globals.get,\n { slug: globalConfig.slug },\n props.token ? { token: props.token } : undefined,\n );\n return <GlobalEditView global={globalConfig} initialData={global} />;\n }\n\n const collection = clientConfig.collections.find((c) => c.slug === collectionSlug);\n const mediaCollection = clientConfig.mediaCollections.find((mc) => mc.slug === collectionSlug);\n\n if (!collection && !mediaCollection) {\n return (\n <div>\n <p className=\"text-muted-foreground p-6\">\n Collection "{collectionSlug}" not found.\n </p>\n <p>TODO: add not found view</p>\n </div>\n );\n }\n\n if (mediaCollection && documentId) {\n const initialData = await fetchQuery(\n vexConvexApi.get,\n {\n id: documentId,\n collection: mediaCollection.slug,\n },\n props.token ? { token: props.token } : undefined,\n );\n return (\n <MediaCollectionEditView\n collection={mediaCollection}\n documentId={documentId}\n initialData={initialData as VexMediaDocument | null}\n />\n );\n }\n\n if (mediaCollection) {\n const numItems = Math.max(\n mediaCollection.admin.table.serverPageSize,\n mediaCollection.admin.table.defaultPageSize,\n );\n const initialData = await fetchQuery(\n vexConvexApi.find,\n {\n collection: collectionSlug as CollectionSlug,\n paginationOpts: {\n numItems,\n totalDocs: true,\n cursor: null,\n },\n },\n props.token ? { token: props.token } : undefined,\n );\n return (\n <MediaCollectionListView\n collection={mediaCollection}\n initialData={initialData as PaginationResult<VexMediaDocument>}\n />\n );\n }\n\n if (collection && documentId) {\n const initialData = await fetchQuery(\n vexConvexApi.get,\n {\n id: documentId,\n collection: collection.slug,\n },\n props.token ? { token: props.token } : undefined,\n );\n return (\n <CollectionEditView\n collection={collection}\n documentId={documentId}\n initialData={initialData}\n />\n );\n }\n\n if (!collection) {\n throw new Error(\"invalid collection slug\");\n }\n\n const numItems = Math.max(\n collection.admin.table.serverPageSize,\n collection.admin.table.defaultPageSize,\n );\n const initialData = await fetchQuery(\n vexConvexApi.findPaginated,\n {\n collection: collectionSlug as CollectionSlug,\n depth: 1,\n paginationOpts: {\n cursor: null,\n numItems,\n totalDocs: true,\n },\n },\n props.token ? { token: props.token } : undefined,\n );\n return <CollectionListView collection={collection} initialData={initialData} />;\n}\n"],"mappings":"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","names":["initialData","numItems"]}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// src/NextAdminLayout.tsx
|
|
2
|
+
import { sanitizeConfigForClient } from "@vexcms/core";
|
|
3
|
+
import { NextAdminLayoutClient } from "./NextAdminLayoutClient";
|
|
4
|
+
import { cookies } from "next/headers";
|
|
5
|
+
import { jsx } from "react/jsx-runtime";
|
|
6
|
+
async function NextAdminLayout(props) {
|
|
7
|
+
const clientConfig = sanitizeConfigForClient(props.config);
|
|
8
|
+
const cookieStore = await cookies();
|
|
9
|
+
const sidebarOpen = String(cookieStore.get("sidebar_state")?.value) === "true";
|
|
10
|
+
return /* @__PURE__ */ jsx(
|
|
11
|
+
NextAdminLayoutClient,
|
|
12
|
+
{
|
|
13
|
+
config: clientConfig,
|
|
14
|
+
user: props.user,
|
|
15
|
+
organization: props.organization,
|
|
16
|
+
sidebarOpen,
|
|
17
|
+
children: props.children
|
|
18
|
+
}
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export {
|
|
23
|
+
NextAdminLayout
|
|
24
|
+
};
|
|
25
|
+
//# sourceMappingURL=chunk-YCJNIS6O.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/NextAdminLayout.tsx"],"sourcesContent":["import type { ReactNode } from \"react\";\nimport type { VexConfig } from \"@vexcms/core\";\nimport { sanitizeConfigForClient } from \"@vexcms/core\";\nimport type { AdminUser } from \"@vexcms/react\";\nimport { NextAdminLayoutClient } from \"./NextAdminLayoutClient\";\nimport { cookies } from \"next/headers\";\n\n/**\n * Next.js admin layout shell for VexCMS.\n *\n * A **server component** that sits on the server→client boundary for the admin\n * panel. It sanitizes the resolved `vex.config` with `sanitizeConfigForClient`\n * — stripping storage adapter class instances, functions, and React components\n * — *before* the config is handed to the client leaf `NextAdminLayoutClient`.\n *\n * This placement is the whole point: React serializes props when a server\n * component renders a client component, so the sanitize **must** happen on the\n * server side of that edge. Sanitizing inside the client component would run\n * too late — the raw config would already have failed to serialize.\n *\n * The client leaf handles everything that needs the browser (`usePathname`,\n * `NuqsAdapter`, `NextLink`/`NextImage`); this wrapper does no client work.\n *\n * Render it from your admin `layout.tsx`. Because this is a server component,\n * your `layout.tsx` may itself be an `async` server component (e.g. to run auth\n * checks) and pass the **raw** `vex.config` straight through — no sanitize\n * needed on the application side.\n *\n * @param props - Layout props\n * @param props.config - The resolved VexCMS config from `vex.config.ts` (raw)\n * @param props.children - The page content from `[[...slug]]/page.tsx`\n * @param props.user - Current user for the admin shell\n * @returns The rendered admin shell, wrapping `children` in the sanitized-config\n * client boundary ({@link NextAdminLayoutClient}).\n * @example\n * ```tsx\n * // app/admin/layout.tsx\n * import { NextAdminLayout } from \"@vexcms/next/client\";\n * import config from \"~/vex.config\";\n * import { getCurrentUser } from \"~/auth/serverUtils\";\n *\n * export default async function AdminLayout({ children }: { children: ReactNode }) {\n * const user = await getCurrentUser();\n * return (\n * <NextAdminLayout config={config} user={user ?? undefined}>\n * {children}\n * </NextAdminLayout>\n * );\n * }\n * ```\n */\nexport async function NextAdminLayout(props: {\n config: VexConfig;\n children: ReactNode;\n user?: AdminUser;\n organization?: Record<string, unknown>;\n}) {\n // Sanitize on the SERVER side of the server→client boundary, before the\n // config is serialized as a prop into the client leaf. This removes the\n // storage adapter class instances that would otherwise crash RSC\n // serialization (\"Classes or null prototypes are not supported\").\n const clientConfig = sanitizeConfigForClient(props.config);\n const cookieStore = await cookies();\n const sidebarOpen = String(cookieStore.get(\"sidebar_state\")?.value) === \"true\";\n\n return (\n <NextAdminLayoutClient\n config={clientConfig}\n user={props.user}\n organization={props.organization}\n sidebarOpen={sidebarOpen}\n >\n {props.children}\n </NextAdminLayoutClient>\n );\n}\n"],"mappings":";AAEA,SAAS,+BAA+B;AAExC,SAAS,6BAA6B;AACtC,SAAS,eAAe;AA6DpB;AAfJ,eAAsB,gBAAgB,OAKnC;AAKD,QAAM,eAAe,wBAAwB,MAAM,MAAM;AACzD,QAAM,cAAc,MAAM,QAAQ;AAClC,QAAM,cAAc,OAAO,YAAY,IAAI,eAAe,GAAG,KAAK,MAAM;AAExE,SACE;AAAA,IAAC;AAAA;AAAA,MACC,QAAQ;AAAA,MACR,MAAM,MAAM;AAAA,MACZ,cAAc,MAAM;AAAA,MACpB;AAAA,MAEC,gBAAM;AAAA;AAAA,EACT;AAEJ;","names":[]}
|
package/dist/index.d.ts
CHANGED
|
@@ -4,4 +4,13 @@ export { relationship, defineCollection } from "@vexcms/react";
|
|
|
4
4
|
export { text, number, select, date, url, checkbox, defineConfig, } from "@vexcms/react";
|
|
5
5
|
export type { AdminUser, RelationshipFieldInput, RelationshipField, AdminCollectionConfigInput, CollectionConfigInput, CollectionConfig, TextField, NumberField, SelectField, DateField, UrlField, CheckboxField, AdminField, CollectionSlug, VexDocument, VexConfig, VexConfigInput, RelationshipPreviewProps, } from "@vexcms/react";
|
|
6
6
|
export type { RelationshipKeysOf, TextKeysOf, SortableKeysOf, RelationshipTargetOf, Populated, PopulateShape, } from "@vexcms/core";
|
|
7
|
+
export { createVexServerClient } from "./cache/createVexServerClient";
|
|
8
|
+
export type { VexServerClientOptions, VexServerClient } from "./cache/types";
|
|
9
|
+
export { createVexRobots } from "./seo/createVexRobots";
|
|
10
|
+
export { createVexSitemap } from "./seo/createVexSitemap";
|
|
11
|
+
export { vexMetadata } from "./seo/vexMetadata";
|
|
12
|
+
export { vexStaticParams } from "./seo/vexStaticParams";
|
|
13
|
+
export { createVexRevalidateRoute } from "./cache/createVexRevalidateRoute";
|
|
14
|
+
export type { CreateVexRevalidateRouteProps } from "./cache/createVexRevalidateRoute";
|
|
15
|
+
export type {} from "./cache/types";
|
|
7
16
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAyBA,cAAc,iBAAiB,CAAC;AAChC,cAAc,mBAAmB,CAAC;AAGlC,OAAO,EAAE,YAAY,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAG/D,OAAO,EACL,IAAI,EACJ,MAAM,EACN,MAAM,EACN,IAAI,EACJ,GAAG,EACH,QAAQ,EACR,YAAY,GACb,MAAM,eAAe,CAAC;AAMvB,YAAY,EACV,SAAS,EAET,sBAAsB,EACtB,iBAAiB,EACjB,0BAA0B,EAC1B,qBAAqB,EACrB,gBAAgB,EAEhB,SAAS,EACT,WAAW,EACX,WAAW,EACX,SAAS,EACT,QAAQ,EACR,aAAa,EAEb,UAAU,EACV,cAAc,EACd,WAAW,EACX,SAAS,EACT,cAAc,EACd,wBAAwB,GACzB,MAAM,eAAe,CAAC;AAYvB,YAAY,EACV,kBAAkB,EAClB,UAAU,EACV,cAAc,EACd,oBAAoB,EACpB,SAAS,EACT,aAAa,GACd,MAAM,cAAc,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAyBA,cAAc,iBAAiB,CAAC;AAChC,cAAc,mBAAmB,CAAC;AAGlC,OAAO,EAAE,YAAY,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAG/D,OAAO,EACL,IAAI,EACJ,MAAM,EACN,MAAM,EACN,IAAI,EACJ,GAAG,EACH,QAAQ,EACR,YAAY,GACb,MAAM,eAAe,CAAC;AAMvB,YAAY,EACV,SAAS,EAET,sBAAsB,EACtB,iBAAiB,EACjB,0BAA0B,EAC1B,qBAAqB,EACrB,gBAAgB,EAEhB,SAAS,EACT,WAAW,EACX,WAAW,EACX,SAAS,EACT,QAAQ,EACR,aAAa,EAEb,UAAU,EACV,cAAc,EACd,WAAW,EACX,SAAS,EACT,cAAc,EACd,wBAAwB,GACzB,MAAM,eAAe,CAAC;AAYvB,YAAY,EACV,kBAAkB,EAClB,UAAU,EACV,cAAc,EACd,oBAAoB,EACpB,SAAS,EACT,aAAa,GACd,MAAM,cAAc,CAAC;AAKtB,OAAO,EAAE,qBAAqB,EAAE,MAAM,+BAA+B,CAAC;AACtE,YAAY,EAAE,sBAAsB,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAC7E,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AACxD,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC1D,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AACxD,OAAO,EAAE,wBAAwB,EAAE,MAAM,kCAAkC,CAAC;AAC5E,YAAY,EAAE,6BAA6B,EAAE,MAAM,kCAAkC,CAAC;AACtF,YAAY,EACX,MAAM,eAAe,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import {
|
|
2
|
+
NextAdminPage
|
|
3
|
+
} from "./chunk-TR3Z3P6Q.js";
|
|
4
|
+
import {
|
|
5
|
+
NextAdminLayout
|
|
6
|
+
} from "./chunk-YCJNIS6O.js";
|
|
7
|
+
import {
|
|
8
|
+
createVexRevalidateRoute,
|
|
9
|
+
createVexServerClient
|
|
10
|
+
} from "./chunk-5AGM3PEX.js";
|
|
11
|
+
import {
|
|
12
|
+
createVexRobots,
|
|
13
|
+
createVexSitemap,
|
|
14
|
+
vexMetadata,
|
|
15
|
+
vexStaticParams
|
|
16
|
+
} from "./chunk-KIRSFMPI.js";
|
|
17
|
+
|
|
18
|
+
// src/index.ts
|
|
19
|
+
import { relationship, defineCollection } from "@vexcms/react";
|
|
20
|
+
import {
|
|
21
|
+
text,
|
|
22
|
+
number,
|
|
23
|
+
select,
|
|
24
|
+
date,
|
|
25
|
+
url,
|
|
26
|
+
checkbox,
|
|
27
|
+
defineConfig
|
|
28
|
+
} from "@vexcms/react";
|
|
29
|
+
export {
|
|
30
|
+
NextAdminLayout,
|
|
31
|
+
NextAdminPage,
|
|
32
|
+
checkbox,
|
|
33
|
+
createVexRevalidateRoute,
|
|
34
|
+
createVexRobots,
|
|
35
|
+
createVexServerClient,
|
|
36
|
+
createVexSitemap,
|
|
37
|
+
date,
|
|
38
|
+
defineCollection,
|
|
39
|
+
defineConfig,
|
|
40
|
+
number,
|
|
41
|
+
relationship,
|
|
42
|
+
select,
|
|
43
|
+
text,
|
|
44
|
+
url,
|
|
45
|
+
vexMetadata,
|
|
46
|
+
vexStaticParams
|
|
47
|
+
};
|
|
48
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["// @vexcms/next v0.1.0-alpha.1\n//\n// Two distinct export surfaces:\n//\n// 1. Server / client component entries (sub-path imports — required because\n// \"use client\" can't be mixed with async server components in one bundle):\n// import { NextAdminPage } from \"@vexcms/next/server\" ← async server component\n// import { NextAdminLayout } from \"@vexcms/next/client\" ← \"use client\" component\n//\n// 2. Config / type re-exports (everything below). Re-exports `@vexcms/react`'s\n// HKT-bound config functions and types transitively so Next consumers can\n// import every config function from a single package:\n//\n// import {\n// defineConfig,\n// defineCollection,\n// relationship,\n// text, number, select, date, url, checkbox,\n// } from \"@vexcms/next\";\n//\n// `admin.components.preview` slots remain typed as React `ComponentType<P>`\n// via `ReactHKT` because @vexcms/react bound it before re-exporting. See\n// spec 22 Decision 15 for the architectural rationale.\n\n// Server / client surface\nexport * from \"./NextAdminPage\";\nexport * from \"./NextAdminLayout\";\n\n// Config functions — HKT-bound wrappers from @vexcms/react\nexport { relationship, defineCollection } from \"@vexcms/react\";\n\n// Field config functions without component slots (pure pass-through)\nexport {\n text,\n number,\n select,\n date,\n url,\n checkbox,\n defineConfig,\n} from \"@vexcms/react\";\n\n// Public types — HKT-bound where applicable.\n// `*FieldInput` core config types intentionally not re-exported (they collide\n// with React `*FieldInput` component names; users get them via inference\n// through the config functions).\nexport type {\n AdminUser,\n // HKT-bound (admin.components.preview = React ComponentType)\n RelationshipFieldInput,\n RelationshipField,\n AdminCollectionConfigInput,\n CollectionConfigInput,\n CollectionConfig,\n // Resolved field types\n TextField,\n NumberField,\n SelectField,\n DateField,\n UrlField,\n CheckboxField,\n // Shared types\n AdminField,\n CollectionSlug,\n VexDocument,\n VexConfig,\n VexConfigInput,\n RelationshipPreviewProps,\n} from \"@vexcms/react\";\n\n// API functions are NOT re-exported from this barrel because @vexcms/next's\n// root index.ts is imported by server components (NextAdminPage). Pulling\n// @vexcms/core/client here would transitively import @convex-dev/react-query\n// which uses createContext — crashing RSC.\n//\n// Import directly from the correct environment-specific path instead:\n//\n// import { find, get, search } from \"@vexcms/core/client\"; ← React components\n// import { find, get, search } from \"@vexcms/core/server\"; ← Convex handlers\n\nexport type {\n RelationshipKeysOf,\n TextKeysOf,\n SortableKeysOf,\n RelationshipTargetOf,\n Populated,\n PopulateShape,\n} from \"@vexcms/core\";\n\n// Cache / SEO surface — server-only, no client context, so these are safe in\n// this RSC-imported root barrel. Also reachable via the \"@vexcms/next/cache\"\n// and \"@vexcms/next/seo\" subpaths.\nexport { createVexServerClient } from \"./cache/createVexServerClient\";\nexport type { VexServerClientOptions, VexServerClient } from \"./cache/types\";\nexport { createVexRobots } from \"./seo/createVexRobots\";\nexport { createVexSitemap } from \"./seo/createVexSitemap\";\nexport { vexMetadata } from \"./seo/vexMetadata\";\nexport { vexStaticParams } from \"./seo/vexStaticParams\";\nexport { createVexRevalidateRoute } from \"./cache/createVexRevalidateRoute\";\nexport type { CreateVexRevalidateRouteProps } from \"./cache/createVexRevalidateRoute\";\nexport type {\n} from \"./cache/types\";\n"],"mappings":";;;;;;;;;;;;;;;;;;AA6BA,SAAS,cAAc,wBAAwB;AAG/C;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;","names":[]}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { MetadataRoute } from "next";
|
|
2
|
+
/**
|
|
3
|
+
* Creates a Next.js `robots.ts` default export that allows crawlers across the
|
|
4
|
+
* site and points them at the generated sitemap.
|
|
5
|
+
*
|
|
6
|
+
* @param props - Robots configuration.
|
|
7
|
+
* @returns A `robots()` function suitable as `app/robots.ts`'s default export.
|
|
8
|
+
*/
|
|
9
|
+
export declare function createVexRobots(props: {
|
|
10
|
+
/**
|
|
11
|
+
* Paths to disallow. Every vexcms template passes `["/admin", "/api"]` — the
|
|
12
|
+
* admin panel is authenticated and neither belongs in an index. Omitted
|
|
13
|
+
* entirely when absent or empty, rather than emitted as an empty array.
|
|
14
|
+
*/
|
|
15
|
+
disallow?: string[];
|
|
16
|
+
/**
|
|
17
|
+
* The site's absolute origin, no trailing slash (e.g. `https://example.com`).
|
|
18
|
+
* The sitemap URL is derived as `${siteUrl}/sitemap.xml`.
|
|
19
|
+
*/
|
|
20
|
+
siteUrl: string;
|
|
21
|
+
}): () => MetadataRoute.Robots;
|
|
22
|
+
//# sourceMappingURL=createVexRobots.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"createVexRobots.d.ts","sourceRoot":"","sources":["../../src/seo/createVexRobots.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,MAAM,CAAC;AAE1C;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE;IACrC;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB;;;OAGG;IACH,OAAO,EAAE,MAAM,CAAC;CACjB,GAAG,MAAM,aAAa,CAAC,MAAM,CAY7B"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { FunctionReference, FunctionReturnType, OptionalRestArgs } from "convex/server";
|
|
2
|
+
import type { MetadataRoute } from "next";
|
|
3
|
+
import type { VexServerClient } from "../cache/types";
|
|
4
|
+
type ArrayItem<T> = T extends (infer Item)[] ? Item : never;
|
|
5
|
+
/**
|
|
6
|
+
* Creates a Next.js `sitemap.ts` default export from a Convex published-slugs
|
|
7
|
+
* query (`packages/core/src/api/publishedSlugs/server.ts`).
|
|
8
|
+
*
|
|
9
|
+
* Tolerates an unreachable Convex deployment (P-020): a read failure returns
|
|
10
|
+
* `[]` — an empty sitemap — rather than failing the build.
|
|
11
|
+
*
|
|
12
|
+
* @param props - Sitemap configuration.
|
|
13
|
+
* @returns A `sitemap()` function suitable as `app/sitemap.ts`'s default export.
|
|
14
|
+
*/
|
|
15
|
+
export declare function createVexSitemap<Query extends FunctionReference<"query">>(props: {
|
|
16
|
+
args?: OptionalRestArgs<Query>[0];
|
|
17
|
+
client: VexServerClient;
|
|
18
|
+
getSlug: (item: ArrayItem<FunctionReturnType<Query>>) => string;
|
|
19
|
+
/**
|
|
20
|
+
* Optional last-modified extractor. Omit it when the collection carries no
|
|
21
|
+
* modification timestamp — Convex's `_creationTime` is set at insert and
|
|
22
|
+
* never moves, so reporting it as `lastModified` would be a lie.
|
|
23
|
+
* `lastModified` is optional in the sitemap protocol; an absent value is
|
|
24
|
+
* correct, a wrong one costs crawl budget.
|
|
25
|
+
*
|
|
26
|
+
* May return `undefined` for an individual item, and that is the common
|
|
27
|
+
* case: `defineCollection` injects `updatedAt` on every content collection,
|
|
28
|
+
* but only a write through `create`/`update` populates it — a seeded or
|
|
29
|
+
* imported row has none. Those entries are emitted with no `lastModified`
|
|
30
|
+
* rather than an `Invalid Date`.
|
|
31
|
+
*/
|
|
32
|
+
getUpdatedAt?: (item: ArrayItem<FunctionReturnType<Query>>) => number | undefined;
|
|
33
|
+
/**
|
|
34
|
+
* Maps one item's slug to its absolute public URL — the route mapper, so URL
|
|
35
|
+
* shape (e.g. `/blog/[slug]` vs `/[slug]`) is the caller's choice.
|
|
36
|
+
*/
|
|
37
|
+
toUrl: (slug: string) => string;
|
|
38
|
+
query: Query;
|
|
39
|
+
}): () => Promise<MetadataRoute.Sitemap>;
|
|
40
|
+
export {};
|
|
41
|
+
//# sourceMappingURL=createVexSitemap.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"createVexSitemap.d.ts","sourceRoot":"","sources":["../../src/seo/createVexSitemap.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAC7F,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,MAAM,CAAC;AAE1C,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAEtD,KAAK,SAAS,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,MAAM,IAAI,CAAC,EAAE,GAAG,IAAI,GAAG,KAAK,CAAC;AAE5D;;;;;;;;;GASG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,SAAS,iBAAiB,CAAC,OAAO,CAAC,EAAE,KAAK,EAAE;IAChF,IAAI,CAAC,EAAE,gBAAgB,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;IAClC,MAAM,EAAE,eAAe,CAAC;IACxB,OAAO,EAAE,CAAC,IAAI,EAAE,SAAS,CAAC,kBAAkB,CAAC,KAAK,CAAC,CAAC,KAAK,MAAM,CAAC;IAChE;;;;;;;;;;;;OAYG;IACH,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,SAAS,CAAC,kBAAkB,CAAC,KAAK,CAAC,CAAC,KAAK,MAAM,GAAG,SAAS,CAAC;IAClF;;;OAGG;IACH,KAAK,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;IAChC,KAAK,EAAE,KAAK,CAAC;CACd,GAAG,MAAM,OAAO,CAAC,aAAa,CAAC,OAAO,CAAC,CA6BvC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/seo/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AACtD,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAC5C,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { Metadata } from "next";
|
|
2
|
+
/**
|
|
3
|
+
* Builds a Next.js `Metadata` object with unconditional title/description, a
|
|
4
|
+
* conditional OG image, `metadataBase`, and a canonical link.
|
|
5
|
+
*
|
|
6
|
+
* Generalizes what Step 1 fixed by hand in `apps/www/src/lib/metadata.ts`:
|
|
7
|
+
* title and description are set regardless of whether an OG image resolved —
|
|
8
|
+
* only the `openGraph.images` entry is conditional on `props.imageUrl`. The
|
|
9
|
+
* original gated the whole `openGraph` block on the image, so a page without
|
|
10
|
+
* one published no OG title or description at all.
|
|
11
|
+
*
|
|
12
|
+
* This FORMATS metadata; it does not read Convex. Resolving which title and
|
|
13
|
+
* description win is the caller's job, because only the caller knows the
|
|
14
|
+
* page/site precedence rules.
|
|
15
|
+
*
|
|
16
|
+
* @param props - Metadata inputs.
|
|
17
|
+
* @returns A `Metadata` object for a page's `generateMetadata` or static
|
|
18
|
+
* `metadata` export.
|
|
19
|
+
*/
|
|
20
|
+
export declare function vexMetadata(props: {
|
|
21
|
+
description?: string;
|
|
22
|
+
/** Absolute OG image URL. Omit to emit `openGraph` without an image. */
|
|
23
|
+
imageUrl?: string;
|
|
24
|
+
/** The page's path (e.g. `/about`), used for the canonical link. */
|
|
25
|
+
path: string;
|
|
26
|
+
/** The site's absolute origin, used for `metadataBase`. */
|
|
27
|
+
siteUrl: string;
|
|
28
|
+
title: string;
|
|
29
|
+
}): Metadata;
|
|
30
|
+
//# sourceMappingURL=vexMetadata.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"vexMetadata.d.ts","sourceRoot":"","sources":["../../src/seo/vexMetadata.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,MAAM,CAAC;AAErC;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE;IACjC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,wEAAwE;IACxE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,oEAAoE;IACpE,IAAI,EAAE,MAAM,CAAC;IACb,2DAA2D;IAC3D,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;CACf,GAAG,QAAQ,CAkBX"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { FunctionReference, FunctionReturnType, OptionalRestArgs } from "convex/server";
|
|
2
|
+
import type { VexServerClient } from "../cache/types";
|
|
3
|
+
type ArrayItem<T> = T extends (infer Item)[] ? Item : never;
|
|
4
|
+
/**
|
|
5
|
+
* Builds Next.js `generateStaticParams` output for a slug-driven route from a
|
|
6
|
+
* Convex query.
|
|
7
|
+
*
|
|
8
|
+
* Returns `[]` when the read fails — including an unreachable Convex
|
|
9
|
+
* deployment, which every CI build hits because it supplies placeholder env
|
|
10
|
+
* rather than skipping validation (P-020). A route with no static params still
|
|
11
|
+
* renders correctly on demand; it is simply not prerendered until the next
|
|
12
|
+
* build.
|
|
13
|
+
*
|
|
14
|
+
* @param props - Static params configuration.
|
|
15
|
+
* @returns `{ [paramName]: string }[]`, or `[]` if the read fails.
|
|
16
|
+
*/
|
|
17
|
+
export declare function vexStaticParams<Query extends FunctionReference<"query">>(props: {
|
|
18
|
+
args?: OptionalRestArgs<Query>[0];
|
|
19
|
+
client: VexServerClient;
|
|
20
|
+
getSlug: (item: ArrayItem<FunctionReturnType<Query>>) => string;
|
|
21
|
+
paramName: string;
|
|
22
|
+
query: Query;
|
|
23
|
+
}): Promise<Record<string, string>[]>;
|
|
24
|
+
export {};
|
|
25
|
+
//# sourceMappingURL=vexStaticParams.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"vexStaticParams.d.ts","sourceRoot":"","sources":["../../src/seo/vexStaticParams.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAE7F,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAEtD,KAAK,SAAS,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,MAAM,IAAI,CAAC,EAAE,GAAG,IAAI,GAAG,KAAK,CAAC;AAE5D;;;;;;;;;;;;GAYG;AACH,wBAAsB,eAAe,CAAC,KAAK,SAAS,iBAAiB,CAAC,OAAO,CAAC,EAAE,KAAK,EAAE;IACrF,IAAI,CAAC,EAAE,gBAAgB,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;IAClC,MAAM,EAAE,eAAe,CAAC;IACxB,OAAO,EAAE,CAAC,IAAI,EAAE,SAAS,CAAC,kBAAkB,CAAC,KAAK,CAAC,CAAC,KAAK,MAAM,CAAC;IAChE,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,KAAK,CAAC;CACd,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,CAAC,CAiBpC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/testing/index.ts"],"names":[],"mappings":"AAKA,cAAc,uBAAuB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/testing/index.ts"],"sourcesContent":["// Re-exports the full public surface of `@vexcms/react/testing` so Next.js\n// consumers can import the entire VexCMS test kit from `@vexcms/next`\n// without also depending on `@vexcms/react` directly. A wildcard re-export\n// (rather than an enumerated list) keeps this file in sync automatically as\n// `@vexcms/react/testing`'s own surface grows.\nexport * from \"@vexcms/react/testing\";\n"],"mappings":";AAKA,cAAc;","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vexcms/next",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.14",
|
|
4
4
|
"description": "Next.js adapter and admin routes for VexCMS",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cms",
|
|
@@ -43,6 +43,21 @@
|
|
|
43
43
|
"import": "./dist/NextAdminLayout.js",
|
|
44
44
|
"types": "./dist/NextAdminLayout.d.ts"
|
|
45
45
|
},
|
|
46
|
+
"./cache": {
|
|
47
|
+
"source": "./src/cache/index.ts",
|
|
48
|
+
"import": "./dist/cache/index.js",
|
|
49
|
+
"types": "./dist/cache/index.d.ts"
|
|
50
|
+
},
|
|
51
|
+
"./seo": {
|
|
52
|
+
"source": "./src/seo/index.ts",
|
|
53
|
+
"import": "./dist/seo/index.js",
|
|
54
|
+
"types": "./dist/seo/index.d.ts"
|
|
55
|
+
},
|
|
56
|
+
"./testing": {
|
|
57
|
+
"source": "./src/testing/index.ts",
|
|
58
|
+
"import": "./dist/testing/index.js",
|
|
59
|
+
"types": "./dist/testing/index.d.ts"
|
|
60
|
+
},
|
|
46
61
|
"./styles": "./styles.css"
|
|
47
62
|
},
|
|
48
63
|
"publishConfig": {
|
|
@@ -51,8 +66,8 @@
|
|
|
51
66
|
},
|
|
52
67
|
"dependencies": {
|
|
53
68
|
"lucide-react": "0.577.0",
|
|
54
|
-
"@vexcms/core": "0.1.0-alpha.
|
|
55
|
-
"@vexcms/react": "0.1.0-alpha.
|
|
69
|
+
"@vexcms/core": "0.1.0-alpha.14",
|
|
70
|
+
"@vexcms/react": "0.1.0-alpha.14"
|
|
56
71
|
},
|
|
57
72
|
"devDependencies": {
|
|
58
73
|
"@convex-dev/react-query": "0.1.0",
|
|
@@ -62,6 +77,7 @@
|
|
|
62
77
|
"@types/node": "20.19.43",
|
|
63
78
|
"@types/react": "19.2.17",
|
|
64
79
|
"@types/react-dom": "19.2.3",
|
|
80
|
+
"@vitest/coverage-v8": "4.1.10",
|
|
65
81
|
"convex": "1.44.0",
|
|
66
82
|
"next": "16.3.3",
|
|
67
83
|
"nuqs": "2.9.0",
|
|
@@ -70,6 +86,7 @@
|
|
|
70
86
|
"tailwindcss": "4.3.2",
|
|
71
87
|
"tsup": "8.5.1",
|
|
72
88
|
"typescript": "6.0.3",
|
|
89
|
+
"vitest": "4.1.10",
|
|
73
90
|
"zod": "4.4.3",
|
|
74
91
|
"@vexcms/tsconfig": "0.0.0"
|
|
75
92
|
},
|
|
@@ -89,6 +106,9 @@
|
|
|
89
106
|
"build": "tsup && tsc -p tsconfig.build.json --emitDeclarationOnly",
|
|
90
107
|
"dev": "tsup --watch --no-clean",
|
|
91
108
|
"clean": "rm -rf dist",
|
|
92
|
-
"typecheck": "tsc --noEmit -p tsconfig.check.json"
|
|
109
|
+
"typecheck": "tsc --noEmit -p tsconfig.check.json",
|
|
110
|
+
"test": "vitest run",
|
|
111
|
+
"coverage": "vitest run --coverage",
|
|
112
|
+
"test:watch": "vitest"
|
|
93
113
|
}
|
|
94
114
|
}
|