@sandboxsonny/ace-kit 0.0.0-stage → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +25 -0
- package/LICENSE +21 -0
- package/README.md +79 -2
- package/astro/AceImage.astro +29 -0
- package/astro/AceMenu.astro +24 -0
- package/astro/AceMenuList.astro +25 -0
- package/astro/AceNotice.astro +19 -0
- package/astro/AcePreviewRefresh.astro +15 -0
- package/astro/AceRichText.astro +12 -0
- package/astro/AceTextArea.astro +14 -0
- package/dist/astro-integration.d.ts +20 -0
- package/dist/astro-integration.js +32 -0
- package/dist/astro-middleware.js +10 -0
- package/dist/astro.d.ts +139 -0
- package/dist/astro.js +24 -0
- package/dist/chunk-KEV3YAWN.js +66 -0
- package/dist/chunk-KLRCWAE4.js +147 -0
- package/dist/chunk-M5WR5TCR.js +16 -0
- package/dist/chunk-URLKW2YS.js +107 -0
- package/dist/chunk-XPZY5R5A.js +16 -0
- package/dist/chunk-YLO6IAXF.js +45 -0
- package/dist/chunk-YN7LJWGI.js +36 -0
- package/dist/cli.js +211 -0
- package/dist/index.d.ts +310 -0
- package/dist/index.js +6 -0
- package/dist/preview-script.js +6 -0
- package/dist/react-router-server.d.ts +225 -0
- package/dist/react-router-server.js +89 -0
- package/dist/react-router.d.ts +155 -0
- package/dist/react-router.js +55 -0
- package/dist/react.d.ts +88 -0
- package/dist/react.js +5 -0
- package/dist/vite.d.ts +17 -0
- package/dist/vite.js +27 -0
- package/docs/api-reference.md +105 -0
- package/docs/astro.md +93 -0
- package/docs/getting-started.md +62 -0
- package/docs/live-preview.md +15 -0
- package/docs/react-router.md +116 -0
- package/docs/react.md +81 -0
- package/docs/releasing.md +45 -0
- package/docs/rich-text.md +54 -0
- package/docs/typed-content.md +50 -0
- package/package.json +158 -4
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# React Router 7
|
|
2
|
+
|
|
3
|
+
Ace Kit has a React Router adapter, so a site only writes its design. It needs `react-router` 7 in framework mode with server rendering (`ssr: true`, the default), `@react-router/node`, `react-dom` and `vite`.
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
app/
|
|
7
|
+
routes.ts route("*", "routes/page.tsx") one route draws every page
|
|
8
|
+
entry.server.tsx one line
|
|
9
|
+
root.tsx createRootLoader + your layout and header
|
|
10
|
+
routes/page.tsx createPageLoader + your page
|
|
11
|
+
vite.config.ts plugins: [ace(), reactRouter()]
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Set up
|
|
15
|
+
|
|
16
|
+
**1. The Vite plugin** reads the settings from `.env` in development and makes sure the site uses one copy of React:
|
|
17
|
+
|
|
18
|
+
```ts
|
|
19
|
+
// vite.config.ts
|
|
20
|
+
import ace from "@sandboxsonny/ace-kit/vite";
|
|
21
|
+
import { reactRouter } from "@react-router/dev/vite";
|
|
22
|
+
import { defineConfig } from "vite";
|
|
23
|
+
|
|
24
|
+
export default defineConfig({ plugins: [ace(), reactRouter()] });
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
The settings are environment variables, read when the server starts, so one build runs anywhere. Put them in `.env` for development and in the host's environment for production:
|
|
28
|
+
|
|
29
|
+
| Variable | Default | Meaning |
|
|
30
|
+
| -------------------------- | ----------------------- | --------------------------------------------------------- |
|
|
31
|
+
| `ACE_API_URL` | `http://localhost:8080` | where the CMS Ace API lives |
|
|
32
|
+
| `ACE_SITE_DOMAIN` | `localhost` | the domain this site has in the CMS (the port is ignored) |
|
|
33
|
+
| `ACE_PREVIEW_ADMIN_ORIGIN` | `http://localhost:5173` | where the CMS admin app runs |
|
|
34
|
+
|
|
35
|
+
**2. The server entry**, which renders pages and lets the CMS's admin app (and only it) frame the site for [live preview](./live-preview.md):
|
|
36
|
+
|
|
37
|
+
```tsx
|
|
38
|
+
// app/entry.server.tsx
|
|
39
|
+
export { handleRequest as default, streamTimeout } from "@sandboxsonny/ace-kit/react-router/server";
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
It waits until a page is completely ready before answering (every page is fully loaded by its loader, and crawlers get whole pages). If you stream deferred data, write your own entry and call `addFramingPolicy(responseHeaders, origin)` in it.
|
|
43
|
+
|
|
44
|
+
**3. One route for every page:**
|
|
45
|
+
|
|
46
|
+
```ts
|
|
47
|
+
// app/routes.ts
|
|
48
|
+
import { type RouteConfig, route } from "@react-router/dev/routes";
|
|
49
|
+
|
|
50
|
+
export default [route("*", "routes/page.tsx")] satisfies RouteConfig;
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
```tsx
|
|
54
|
+
// app/routes/page.tsx
|
|
55
|
+
import { AceNotice, createMeta } from "@sandboxsonny/ace-kit/react-router";
|
|
56
|
+
import { createPageLoader } from "@sandboxsonny/ace-kit/react-router/server";
|
|
57
|
+
import type { Route } from "./+types/page";
|
|
58
|
+
|
|
59
|
+
export const loader = createPageLoader();
|
|
60
|
+
export { headers } from "@sandboxsonny/ace-kit/react-router/server";
|
|
61
|
+
export const meta = createMeta({ siteName: "My Site" });
|
|
62
|
+
|
|
63
|
+
export default function Page({ loaderData: page }: Route.ComponentProps) {
|
|
64
|
+
return page.kind === "entry" ? <h1>{page.entry.title}</h1> : <AceNotice page={page} />;
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
The loader reads the address (`/{content type}/{entry}`, and `/` is the `home` entry of `pages`), loads the entry and answers with the right status and headers: a missing page is a real 404, an unreachable CMS a 503, an expired preview a 403, `/pages/home` redirects to `/`, and a preview is never cached or indexed. `page.kind` is `entry`, `not-found`, `unavailable` or `preview-expired`. `createMeta` titles the page from the entry (followed by the site's name), adds its description, and asks search engines to ignore a preview. With [typed content](./typed-content.md), `createPageLoader<PageValues>()` gives `page.entry.values` its real shape.
|
|
69
|
+
|
|
70
|
+
**4. The root route** loads the header menu and keeps previews fresh:
|
|
71
|
+
|
|
72
|
+
```tsx
|
|
73
|
+
// app/root.tsx
|
|
74
|
+
import { useAceMenu, useAcePreview } from "@sandboxsonny/ace-kit/react-router";
|
|
75
|
+
import { createRootLoader } from "@sandboxsonny/ace-kit/react-router/server";
|
|
76
|
+
|
|
77
|
+
export const loader = createRootLoader({
|
|
78
|
+
menu: "main-menu", // the CMS menu with this handle
|
|
79
|
+
fallback: [{ label: "Home", to: "/" }], // if the site has none yet, or the CMS is down
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
export default function App() {
|
|
83
|
+
useAcePreview(); // in the CMS's preview frame, redraws the page when the editor saves a draft
|
|
84
|
+
const links = useAceMenu(); // [{ label, to, children? }]
|
|
85
|
+
return (/* your header, <Outlet />, your footer */);
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
`useAcePreview` and `useAceMenu` read what `createRootLoader` loaded, so call them in the root route (or anywhere below it). If your root loader loads more, spread ours into yours: `return { ...(await aceLoader(args)), other }`.
|
|
90
|
+
|
|
91
|
+
## Components
|
|
92
|
+
|
|
93
|
+
From `@sandboxsonny/ace-kit/react-router` (these are safe in the browser):
|
|
94
|
+
|
|
95
|
+
- **AceRichText** draws a rich text field. Links to pages of this site are router `Link`s, so navigating doesn't reload the page. It renders bare elements; style them from a wrapper.
|
|
96
|
+
- **AceTextArea** draws a Textarea field (plain text): `multiline` (the default) makes paragraphs and line breaks, `multiline={false}` is one line of text.
|
|
97
|
+
- **AceImage** is an `<img>` with `src`, `srcset`, `sizes`, size and alt text. Set `sizes` to how wide it is shown, and `lazy={false}` for the image at the top of the page.
|
|
98
|
+
- **AceNotice** is the heading and message for a page that isn't an entry. Give it a `className`.
|
|
99
|
+
|
|
100
|
+
## Reading content in your own loaders
|
|
101
|
+
|
|
102
|
+
From `@sandboxsonny/ace-kit/react-router/server`, with no client to create:
|
|
103
|
+
|
|
104
|
+
```ts
|
|
105
|
+
import { getEntries, getEntry, getMenuLinks } from "@sandboxsonny/ace-kit/react-router/server";
|
|
106
|
+
|
|
107
|
+
export async function loader() {
|
|
108
|
+
return { posts: await getEntries("posts", { limit: 10 }) };
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
`getAceClient()` is the client behind them. Import these only in loaders, `headers` and `entry.server.tsx`: React Router leaves them out of the browser bundle, which is what keeps the settings and the CMS address on the server.
|
|
113
|
+
|
|
114
|
+
## Without the adapter
|
|
115
|
+
|
|
116
|
+
Everything the adapter does is built from the framework-free [core](./getting-started.md): `loadPage`, `loadMenuLinks`, `previewTokenFrom`, `addFramingPolicy`, the [React components](./react.md) and `usePreviewRefresh`. The adapter is about 150 lines; read it if you need to do something it doesn't.
|
package/docs/react.md
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# React
|
|
2
|
+
|
|
3
|
+
Needs `react` 18 or newer. The components work with any React framework (React Router, Next.js, Remix, plain Vite). **On React Router 7, use the [React Router adapter](./react-router.md) instead of wiring this up yourself.** Fetch with the [client](./getting-started.md#the-client) on the server (a loader, a server component, `getServerSideProps`), then draw.
|
|
4
|
+
|
|
5
|
+
```tsx
|
|
6
|
+
import { AceImage, AceRichText } from "@sandboxsonny/ace-kit/react";
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## AceImage
|
|
10
|
+
|
|
11
|
+
```tsx
|
|
12
|
+
<AceImage image={entry.values.hero} sizes="(min-width: 64rem) 64rem, 100vw" lazy={false} className="hero" />
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
An `<img>` with `src`, `srcset`, `sizes`, `width`, `height` and `alt`. Any other `<img>` prop (`className`, `style`, `onLoad`...) is passed through. `lazy` defaults to `true`; turn it off for the image at the top of the page.
|
|
16
|
+
|
|
17
|
+
## AceRichText
|
|
18
|
+
|
|
19
|
+
```tsx
|
|
20
|
+
<div className="prose">
|
|
21
|
+
<AceRichText document={entry.values.body} />
|
|
22
|
+
</div>
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Draws a rich text field as React elements: no `dangerouslySetInnerHTML`, text is escaped by React, only the kinds of content the CMS allows are drawn, and link addresses are checked again. It renders bare elements, so style them from the wrapper.
|
|
26
|
+
|
|
27
|
+
Give it your router's link so navigation stays in the browser:
|
|
28
|
+
|
|
29
|
+
```tsx
|
|
30
|
+
import { Link } from "react-router";
|
|
31
|
+
|
|
32
|
+
<AceRichText document={body} link={({ href, children }) => <Link to={href}>{children}</Link>} />;
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
External links, `mailto:` and `tel:` links should stay plain `<a>`: have your `link` component check the address and fall back, if you mix them.
|
|
36
|
+
|
|
37
|
+
## AceTextArea
|
|
38
|
+
|
|
39
|
+
```tsx
|
|
40
|
+
<AceTextArea value={entry.values.summary} /> {/* paragraphs; a new line is a <br> */}
|
|
41
|
+
<p><AceTextArea value={entry.values.tagline} multiline={false} /></p> {/* one line, no element around it */}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Draws a Textarea field (plain text, not rich text). Text is escaped by React.
|
|
45
|
+
|
|
46
|
+
## Live preview
|
|
47
|
+
|
|
48
|
+
```tsx
|
|
49
|
+
import { usePreviewRefresh } from "@sandboxsonny/ace-kit/react";
|
|
50
|
+
import { useRevalidator } from "react-router";
|
|
51
|
+
|
|
52
|
+
function PreviewRefresh({ adminOrigin }: { adminOrigin: string | null }) {
|
|
53
|
+
const { revalidate } = useRevalidator();
|
|
54
|
+
const refresh = useCallback(() => revalidate(), [revalidate]);
|
|
55
|
+
usePreviewRefresh(adminOrigin, refresh);
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Pass `adminOrigin` only for a page shown with a preview token (`previewTokenFrom(url)` tells you). See [Live preview](./live-preview.md) for the rest: call `addFramingPolicy(headers, origin)` on every response (React Router: `entry.server.tsx`), and pass `page.headers` on so a preview is never cached.
|
|
61
|
+
|
|
62
|
+
## A route in React Router 7
|
|
63
|
+
|
|
64
|
+
```tsx
|
|
65
|
+
// app/routes.ts: one route draws every page
|
|
66
|
+
export default [route("*", "routes/page.tsx")] satisfies RouteConfig;
|
|
67
|
+
|
|
68
|
+
// app/routes/page.tsx
|
|
69
|
+
export async function loader({ request, params }: Route.LoaderArgs) {
|
|
70
|
+
const page = await loadPage(ace, { slug: params["*"], url: new URL(request.url) });
|
|
71
|
+
if (page.kind === "redirect") return redirect(page.location, { status: page.status, headers: page.headers });
|
|
72
|
+
return data(page, { status: page.status, headers: page.headers });
|
|
73
|
+
}
|
|
74
|
+
export const headers: Route.HeadersFunction = ({ loaderHeaders }) => loaderHeaders;
|
|
75
|
+
|
|
76
|
+
export default function Page({ loaderData: page }: Route.ComponentProps) {
|
|
77
|
+
return page.kind === "entry" ? <h1>{page.entry.title}</h1> : <p>Not here</p>;
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Returning `data(page, { status })` (rather than throwing) keeps a 404 or 503 inside your layout, with the header and footer.
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# Releasing
|
|
2
|
+
|
|
3
|
+
Versions and the changelog are managed with [Changesets](https://github.com/changesets/changesets), and releases are published to [npm](https://www.npmjs.com) by GitHub Actions. The package follows [semantic versioning](https://semver.org): patch for fixes, minor for backwards compatible additions, major for breaking changes. While the version is below 1.0.0, a minor release may contain breaking changes (say so in the changeset).
|
|
4
|
+
|
|
5
|
+
## Day to day
|
|
6
|
+
|
|
7
|
+
Every change that someone using the kit would notice gets a changeset:
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
npm run changeset
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
It asks for the size and a summary, and writes a small file in `.changeset/`. Commit it with the change. Nothing else is needed.
|
|
14
|
+
|
|
15
|
+
## Releasing
|
|
16
|
+
|
|
17
|
+
When changes are merged to `main`, the **Release** workflow opens (or updates) a pull request called "Version Packages". It bumps `package.json` and writes the summaries into `CHANGELOG.md`. Read it, then merge it: the workflow then publishes the new version to npm and tags it.
|
|
18
|
+
|
|
19
|
+
Each published version carries npm **provenance**: a public statement, signed by GitHub, that this exact package was built from this repository by this workflow. People can check it on the package's npm page (and with `npm audit signatures`).
|
|
20
|
+
|
|
21
|
+
## One-time setup
|
|
22
|
+
|
|
23
|
+
1. **Own the scope.** The package is `@sandboxsonny/ace-kit`; a scope on npm belongs to a user or an organisation of that name. [Create an npm account](https://www.npmjs.com/signup) called `sandboxsonny` (or an organisation with that name) and turn on two-factor authentication. If the name isn't available, pick another scope and follow "If the scope changes" below.
|
|
24
|
+
2. **A public repository,** with branch protection on `main` that requires the CI checks, so a release can only come from reviewed, passing code. Also turn on private vulnerability reporting (Settings → Security), which `SECURITY.md` points to.
|
|
25
|
+
3. **The first publish,** which needs a token because the package doesn't exist yet to attach trusted publishing to. Create a _granular access token_ on npm with read and write permission on the scope and a short expiry, store it in the repository as the secret `NPM_TOKEN`, and pass it to the release step as `NODE_AUTH_TOKEN`. (Or publish version 0.1.0 by hand: `npm login`, then `npm run release`.)
|
|
26
|
+
4. **Trusted publishing,** so no token is needed after that. On the package's page on npmjs.com → Settings → Trusted Publisher, add GitHub Actions with: the owner and repository exactly as GitHub spells them (**`sandboxSonny`** and **`ace-kit`**: the capitals matter), and the workflow file name **`release.yml`**. npm shows it as "not yet validated" until one publish goes through it, and drops it if that doesn't happen within 48 hours. Make the next release (even a patch with no code change) to validate it.
|
|
27
|
+
5. **Remove the token:** delete the `NPM_TOKEN` secret and revoke the token on npm, and make sure the workflow does not pass `NODE_AUTH_TOKEN` (npm would use it instead of the trusted route).
|
|
28
|
+
|
|
29
|
+
The workflow in this repository is already in the last state: no token, Node 24 with npm 11.5.1 or newer, and `id-token: write`. If a publish fails with a message about authentication, check that the repository name and workflow file name in the Trusted Publisher settings match exactly, and that the job really is running `release.yml` on `main`.
|
|
30
|
+
|
|
31
|
+
## What the checks protect
|
|
32
|
+
|
|
33
|
+
`npm run release` and CI run `npm run check`, which ends with `scripts/check-package.mjs`. It fails if a file the package points at is missing, if the command line tool has no `#!` line, if sources or tests would be published, or if `package.json` is not set up for publishing (scoped name, public access).
|
|
34
|
+
|
|
35
|
+
## Trying it before it is published
|
|
36
|
+
|
|
37
|
+
`npm run pack:local` builds the package and packs it into `sandboxsonny-ace-kit-<version>.tgz`, which is exactly what would be published. Install that file in a site (`npm install /path/to/sandboxsonny-ace-kit-0.1.0.tgz`). Prefer this to linking the folder (`file:../ace-kit`): a link hides problems a real install has (missing files, a wrong `exports` map), and Astro cannot find the preview script through an absolute link from outside the project.
|
|
38
|
+
|
|
39
|
+
## If the scope changes
|
|
40
|
+
|
|
41
|
+
The package name appears in `package.json`, in `src/helpers/package-name.ts` (generated types augment it; a test checks the two agree), in the docs, and in the integration code (`src/astro/integration.ts`). Search for `@sandboxsonny` and change it everywhere, then add a major changeset: consumers must change their imports.
|
|
42
|
+
|
|
43
|
+
## Publishing to GitHub Packages instead
|
|
44
|
+
|
|
45
|
+
Possible, with more friction for everyone who installs: GitHub's npm registry asks for a token even for public packages. Set `publishConfig.registry` to `https://npm.pkg.github.com`, `registry-url` in the workflow to the same, `NODE_AUTH_TOKEN` to `${{ secrets.GITHUB_TOKEN }}`, give the job `packages: write`, and tell users to add `@sandboxsonny:registry=https://npm.pkg.github.com` and a `read:packages` token to their `.npmrc`. (Provenance isn't available there.)
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# Rich text
|
|
2
|
+
|
|
3
|
+
A rich text field's value is a **document**: a small, safe JSON tree (no HTML in it).
|
|
4
|
+
|
|
5
|
+
```json
|
|
6
|
+
{
|
|
7
|
+
"type": "doc",
|
|
8
|
+
"content": [
|
|
9
|
+
{ "type": "heading", "attrs": { "level": 2 }, "content": [{ "type": "text", "text": "Hello" }] },
|
|
10
|
+
{
|
|
11
|
+
"type": "paragraph",
|
|
12
|
+
"content": [
|
|
13
|
+
{ "type": "text", "text": "Read " },
|
|
14
|
+
{ "type": "text", "text": "about us", "marks": [{ "type": "link", "attrs": { "href": "/pages/about" } }] }
|
|
15
|
+
]
|
|
16
|
+
},
|
|
17
|
+
{ "type": "image", "attrs": { "assetId": 12, "image": { "src": "...", "srcset": "...", "alt_text": "..." } } }
|
|
18
|
+
]
|
|
19
|
+
}
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
| Node | Notes |
|
|
23
|
+
| ------------------------------------- | ------------------------------------------------------------ |
|
|
24
|
+
| `paragraph`, `blockquote`, `listItem` | |
|
|
25
|
+
| `heading` | `attrs.level` 2, 3 or 4 |
|
|
26
|
+
| `bulletList`, `orderedList` | `orderedList` has `attrs.start` when it doesn't begin at 1 |
|
|
27
|
+
| `text` | `marks`: `bold`, `italic`, `link` |
|
|
28
|
+
| `hardBreak` | |
|
|
29
|
+
| `image` | `attrs.image` is the usual image object, resolved by the API |
|
|
30
|
+
|
|
31
|
+
Links arrive with their address already worked out: a link to an entry is `/{content type}/{entry}`. An image that was deleted is left out, and a link to a deleted entry keeps its words but loses the link.
|
|
32
|
+
|
|
33
|
+
## Drawing it
|
|
34
|
+
|
|
35
|
+
- **Astro:** `<AceRichText document={...} />` ([Astro](./astro.md)).
|
|
36
|
+
- **React:** `<AceRichText document={...} />` ([React](./react.md)).
|
|
37
|
+
- **Anything else:** `renderRichText(document)` returns an HTML string. Every piece of text and every attribute is escaped, so it is safe to put on a page as it is.
|
|
38
|
+
|
|
39
|
+
All three draw only what is in the table, check link addresses again (only paths, `https://`, `http://`, `mailto:` and `tel:` are links), and leave out anything they don't know, so a newer CMS can't break an older site.
|
|
40
|
+
|
|
41
|
+
## Plain text fields
|
|
42
|
+
|
|
43
|
+
A _Textarea_ field (not rich text) holds a plain string. `AceTextArea` draws it, and `multiline` says how:
|
|
44
|
+
|
|
45
|
+
```astro
|
|
46
|
+
<AceTextArea value={entry.values.summary} />{" "}
|
|
47
|
+
<!-- <p>…</p><p>…</p>: blank lines start paragraphs, a new line is a <br> -->
|
|
48
|
+
<p>
|
|
49
|
+
<AceTextArea value={entry.values.tagline} multiline={false} />
|
|
50
|
+
</p>
|
|
51
|
+
<!-- one line of text, with no element around it -->
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
It is the same component in Astro (`AceTextArea.astro`), React and React Router (`AceTextArea`). Text is escaped. For anything else, `renderTextArea(text, { multiline })` returns the same thing as an HTML string.
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Typed content
|
|
2
|
+
|
|
3
|
+
The kit can write TypeScript types for your site's content types, so `getEntry` and `getEntries` return the right shape and a change in the CMS shows up as a type error in your site.
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
npx ace-kit generate --api-url http://localhost:8080 --domain localhost
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
or with the settings in the environment:
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
ACE_API_URL=http://localhost:8080 ACE_SITE_DOMAIN=localhost npx ace-kit generate
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
It reads `GET /public/content-types` (no login: it describes field names and kinds, never entries) and writes `src/ace-kit.generated.d.ts` (change it with `--out`). Make sure the file is inside your `tsconfig` `include`. Commit it, and run the command again whenever content types change (a good moment is a CI step, or a `generate` script next to `dev`).
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
// ace-kit.generated.d.ts (generated)
|
|
19
|
+
declare module "@sandboxsonny/ace-kit" {
|
|
20
|
+
interface AceContentTypes {
|
|
21
|
+
pages: {
|
|
22
|
+
headline: string; // required fields are always there
|
|
23
|
+
body?: AceRichTextDocument; // others may be missing
|
|
24
|
+
hero?: AceImageData;
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Now the content type's handle is checked and its values are typed:
|
|
31
|
+
|
|
32
|
+
```ts
|
|
33
|
+
const about = await cms.getEntry("pages", "about"); // AceEntry<{ headline: string; body?: ...; hero?: ... }> | null
|
|
34
|
+
about?.values.headline; // string
|
|
35
|
+
cms.getEntry("pagez", "about"); // type error: no such content type
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
| Field | Type |
|
|
39
|
+
| -------------------------------- | --------------------------------------------------- |
|
|
40
|
+
| Text, Textarea | `string` |
|
|
41
|
+
| Number | `number` |
|
|
42
|
+
| Checkbox | `boolean` |
|
|
43
|
+
| Select | a union of its options, e.g. `"formal" \| "casual"` |
|
|
44
|
+
| Image | `AceImageData` |
|
|
45
|
+
| Rich text | `AceRichTextDocument` |
|
|
46
|
+
| a kind this version doesn't know | `unknown` |
|
|
47
|
+
|
|
48
|
+
A field is optional unless it is marked required in the CMS. Before you generate, handles are plain `string` and values are `Record<string, unknown>`, so nothing breaks.
|
|
49
|
+
|
|
50
|
+
For a content type you use in a component, name its values once: `type PageValues = AceContentTypes["pages"]`.
|
package/package.json
CHANGED
|
@@ -1,6 +1,160 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sandboxsonny/ace-kit",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Everything a website needs to use CMS Ace: a typed client, addressing, images, menus, rich text, live preview, and Astro and React adapters.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"cms",
|
|
7
|
+
"cms-ace",
|
|
8
|
+
"headless-cms",
|
|
9
|
+
"astro",
|
|
10
|
+
"react"
|
|
11
|
+
],
|
|
12
|
+
"license": "MIT",
|
|
13
|
+
"author": "Adams Development <sonny@adamsdevelopment.co.uk>",
|
|
14
|
+
"homepage": "https://github.com/sandboxSonny/ace-kit#readme",
|
|
15
|
+
"bugs": {
|
|
16
|
+
"url": "https://github.com/sandboxSonny/ace-kit/issues"
|
|
17
|
+
},
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "git+https://github.com/sandboxSonny/ace-kit.git"
|
|
21
|
+
},
|
|
22
|
+
"type": "module",
|
|
23
|
+
"sideEffects": [
|
|
24
|
+
"./dist/preview-script.js"
|
|
25
|
+
],
|
|
26
|
+
"engines": {
|
|
27
|
+
"node": ">=22.12.0"
|
|
28
|
+
},
|
|
29
|
+
"files": [
|
|
30
|
+
"dist",
|
|
31
|
+
"astro",
|
|
32
|
+
"docs",
|
|
33
|
+
"CHANGELOG.md",
|
|
34
|
+
"LICENSE"
|
|
35
|
+
],
|
|
36
|
+
"exports": {
|
|
37
|
+
".": {
|
|
38
|
+
"types": "./dist/index.d.ts",
|
|
39
|
+
"import": "./dist/index.js"
|
|
40
|
+
},
|
|
41
|
+
"./react": {
|
|
42
|
+
"types": "./dist/react.d.ts",
|
|
43
|
+
"import": "./dist/react.js"
|
|
44
|
+
},
|
|
45
|
+
"./react-router": {
|
|
46
|
+
"types": "./dist/react-router.d.ts",
|
|
47
|
+
"import": "./dist/react-router.js"
|
|
48
|
+
},
|
|
49
|
+
"./react-router/server": {
|
|
50
|
+
"types": "./dist/react-router-server.d.ts",
|
|
51
|
+
"import": "./dist/react-router-server.js"
|
|
52
|
+
},
|
|
53
|
+
"./astro": {
|
|
54
|
+
"types": "./dist/astro.d.ts",
|
|
55
|
+
"import": "./dist/astro.js"
|
|
56
|
+
},
|
|
57
|
+
"./astro/integration": {
|
|
58
|
+
"types": "./dist/astro-integration.d.ts",
|
|
59
|
+
"import": "./dist/astro-integration.js"
|
|
60
|
+
},
|
|
61
|
+
"./astro/middleware": {
|
|
62
|
+
"import": "./dist/astro-middleware.js"
|
|
63
|
+
},
|
|
64
|
+
"./astro/*.astro": "./astro/*.astro",
|
|
65
|
+
"./preview-script": {
|
|
66
|
+
"import": "./dist/preview-script.js"
|
|
67
|
+
},
|
|
68
|
+
"./package.json": "./package.json",
|
|
69
|
+
"./vite": {
|
|
70
|
+
"types": "./dist/vite.d.ts",
|
|
71
|
+
"import": "./dist/vite.js"
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
"bin": {
|
|
75
|
+
"ace-kit": "./dist/cli.js"
|
|
76
|
+
},
|
|
77
|
+
"publishConfig": {
|
|
78
|
+
"access": "public"
|
|
79
|
+
},
|
|
80
|
+
"scripts": {
|
|
81
|
+
"build": "tsup",
|
|
82
|
+
"dev": "tsup --watch",
|
|
83
|
+
"test": "vitest run",
|
|
84
|
+
"test:watch": "vitest",
|
|
85
|
+
"lint": "eslint .",
|
|
86
|
+
"lint:fix": "eslint . --fix",
|
|
87
|
+
"format": "prettier --write .",
|
|
88
|
+
"format:check": "prettier --check .",
|
|
89
|
+
"typecheck": "tsc --noEmit",
|
|
90
|
+
"check": "npm run format:check && npm run lint && npm run typecheck && npm test && npm run build && npm run check:package",
|
|
91
|
+
"check:package": "node scripts/check-package.mjs",
|
|
92
|
+
"pack:local": "npm run build && npm pack",
|
|
93
|
+
"storybook": "storybook dev -p 6006",
|
|
94
|
+
"build:storybook": "storybook build",
|
|
95
|
+
"changeset": "changeset",
|
|
96
|
+
"version": "changeset version && prettier --write CHANGELOG.md package.json",
|
|
97
|
+
"release": "npm run check && changeset publish"
|
|
98
|
+
},
|
|
99
|
+
"peerDependencies": {
|
|
100
|
+
"astro": ">=5",
|
|
101
|
+
"react": ">=18",
|
|
102
|
+
"react-router": ">=7",
|
|
103
|
+
"react-dom": ">=18",
|
|
104
|
+
"@react-router/node": ">=7",
|
|
105
|
+
"vite": ">=5"
|
|
106
|
+
},
|
|
107
|
+
"peerDependenciesMeta": {
|
|
108
|
+
"astro": {
|
|
109
|
+
"optional": true
|
|
110
|
+
},
|
|
111
|
+
"react": {
|
|
112
|
+
"optional": true
|
|
113
|
+
},
|
|
114
|
+
"react-router": {
|
|
115
|
+
"optional": true
|
|
116
|
+
},
|
|
117
|
+
"react-dom": {
|
|
118
|
+
"optional": true
|
|
119
|
+
},
|
|
120
|
+
"@react-router/node": {
|
|
121
|
+
"optional": true
|
|
122
|
+
},
|
|
123
|
+
"vite": {
|
|
124
|
+
"optional": true
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
"devDependencies": {
|
|
128
|
+
"@changesets/cli": "^3.0.3",
|
|
129
|
+
"@eslint/js": "^10.0.1",
|
|
130
|
+
"@react-router/node": "^7.18.4",
|
|
131
|
+
"@storybook/addon-docs": "^10.6.1",
|
|
132
|
+
"@storybook/react-vite": "^10.6.1",
|
|
133
|
+
"@testing-library/dom": "^10.4.2",
|
|
134
|
+
"@testing-library/react": "^16.3.3",
|
|
135
|
+
"@types/node": "^22.20.5",
|
|
136
|
+
"@types/react": "^19.3.0",
|
|
137
|
+
"@types/react-dom": "^19.3.0",
|
|
138
|
+
"astro": "^7.3.8",
|
|
139
|
+
"eslint": "^10.12.0",
|
|
140
|
+
"eslint-config-prettier": "^10.1.8",
|
|
141
|
+
"eslint-plugin-astro": "^1.7.0",
|
|
142
|
+
"eslint-plugin-react-hooks": "^7.1.1",
|
|
143
|
+
"globals": "^17.13.0",
|
|
144
|
+
"jsdom": "^29.1.1",
|
|
145
|
+
"prettier": "^3.9.10",
|
|
146
|
+
"prettier-plugin-astro": "^1.1.1",
|
|
147
|
+
"react": "^19.3.0",
|
|
148
|
+
"react-dom": "^19.3.0",
|
|
149
|
+
"react-router": "^7.18.4",
|
|
150
|
+
"storybook": "^10.6.1",
|
|
151
|
+
"tsup": "^8.5.1",
|
|
152
|
+
"typescript": "^6.0.3",
|
|
153
|
+
"typescript-eslint": "^8.71.1",
|
|
154
|
+
"vite": "^8.3.4",
|
|
155
|
+
"vitest": "^5.0.4"
|
|
156
|
+
},
|
|
157
|
+
"overrides": {
|
|
158
|
+
"esbuild": "^0.28.2"
|
|
159
|
+
}
|
|
160
|
+
}
|