@hollware/react 0.0.0 → 1.0.0-rc.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +110 -1
- package/dist/index.d.mts +29 -0
- package/dist/index.mjs +26 -0
- package/package.json +48 -4
package/README.md
CHANGED
|
@@ -1,3 +1,112 @@
|
|
|
1
1
|
# @hollware/react
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Read [Hollware](https://hollware.com) from any React app: server helpers that turn a `Request` into
|
|
4
|
+
a published entry (and into a draft, for the visitor of a preview link), and `<HollwareContent>`,
|
|
5
|
+
which renders an entry's Comark document with your own React components. It knows no framework: it
|
|
6
|
+
works the same in a React Router loader, a Next.js Server Component or a TanStack Start server
|
|
7
|
+
function, and it imports nothing of Cloudflare.
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
npm install @hollware/react
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
On Cloudflare, bind the Hollware Worker in the site's `wrangler.jsonc`:
|
|
14
|
+
|
|
15
|
+
```jsonc
|
|
16
|
+
"services": [{ "binding": "HOLLWARE", "service": "my-hollware", "entrypoint": "Hollware" }]
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## React Router
|
|
20
|
+
|
|
21
|
+
```tsx
|
|
22
|
+
// app/hollware.server.ts: the `.server` suffix keeps the binding out of the browser's bundle
|
|
23
|
+
import { env } from 'cloudflare:workers'
|
|
24
|
+
import { createHollware, hollwareBinding } from '@hollware/react'
|
|
25
|
+
export const hollware = createHollware({
|
|
26
|
+
site: 'my-site',
|
|
27
|
+
reader: () => hollwareBinding(env.HOLLWARE),
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
// app/root.tsx: the link of a preview is exchanged for a cookie, with a 303
|
|
31
|
+
export const middleware = [
|
|
32
|
+
({ request }) => {
|
|
33
|
+
const r = previewRedirect(request)
|
|
34
|
+
if (r) throw r
|
|
35
|
+
},
|
|
36
|
+
]
|
|
37
|
+
export const headers = ({ errorHeaders }: HeadersArgs) => errorHeaders
|
|
38
|
+
|
|
39
|
+
// app/routes/post.tsx
|
|
40
|
+
export async function loader({ request, params }: LoaderFunctionArgs) {
|
|
41
|
+
try {
|
|
42
|
+
const answer = await hollware.getEntry('blog', 'en', params.slug!, { request })
|
|
43
|
+
if (!answer) throw data('Not found', { status: 404, headers: previewHeaders(request) })
|
|
44
|
+
return data(
|
|
45
|
+
{ entry: answer.entry },
|
|
46
|
+
{ headers: answer.preview ? previewHeaders(request) : undefined },
|
|
47
|
+
)
|
|
48
|
+
} catch (error) {
|
|
49
|
+
// a refused token: 404, and the cookie is cleared
|
|
50
|
+
const headers = previewHeaders(request, error)
|
|
51
|
+
throw headers.has('set-cookie') ? data('Not found', { status: 404, headers }) : error
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
export default function Post() {
|
|
55
|
+
const { entry } = useLoaderData<typeof loader>()
|
|
56
|
+
return <HollwareContent entry={entry} components={{ Callout }} />
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
`examples/react-router-blog` is the whole site, and `apps/playground/test/harness/react-router-site.test.ts`
|
|
61
|
+
builds it and reads the playground.
|
|
62
|
+
|
|
63
|
+
## Next.js
|
|
64
|
+
|
|
65
|
+
```tsx
|
|
66
|
+
// middleware.ts: export const middleware = (req: NextRequest) => previewRedirect(req) ?? NextResponse.next()
|
|
67
|
+
// app/blog/[slug]/page.tsx, a Server Component: the cookie makes the page dynamic, so it is not kept
|
|
68
|
+
const hollware = createHollware({ site: 'my-site', url: HOLLWARE_URL, token: HOLLWARE_TOKEN })
|
|
69
|
+
const preview = (await cookies()).get(PREVIEW_COOKIE)?.value
|
|
70
|
+
const answer = await hollware.getEntry('blog', 'en', (await params).slug, { preview })
|
|
71
|
+
return answer ? <HollwareContent entry={answer.entry} components={{ Callout }} /> : notFound()
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## The server helpers
|
|
75
|
+
|
|
76
|
+
| What | Does |
|
|
77
|
+
| ------------------------------------------- | ---------------------------------------------------------------------------------------------------- |
|
|
78
|
+
| `createHollware({ site, reader })` | The read side of a site. `reader` is a service binding, or a function that gives it |
|
|
79
|
+
| `createHollware({ site, url, token })` | The same over HTTP, with a read token (`hollware token create --read`) |
|
|
80
|
+
| `getEntry(collection, locale, slug, opts?)` | `{ entry, preview }`, or `undefined` when there is none; `opts.request` reads the preview cookie |
|
|
81
|
+
| `listEntries(collection, opts?)` | `{ entries, nextCursor }` of published entries, newest first (`locale`, `tag`, `limit`, `cursor`) |
|
|
82
|
+
| `getTranslations(collection, group)` | The `{ locale, slug }` of each entry of a translation group |
|
|
83
|
+
| `hollwareBinding(env.HOLLWARE)` | The service binding as a reader, with a clear error when it is missing |
|
|
84
|
+
| `previewRedirect(request)` | The `303` that exchanges a preview link for a cookie, or `null` |
|
|
85
|
+
| `previewHeaders(request, error?)` | `Cache-Control: no-store` for a visitor with a token, and the `Set-Cookie` that clears a refused one |
|
|
86
|
+
| `previewCookieClear(error, request)` | That `Set-Cookie`, or `null` when `error` is no refusal of the token |
|
|
87
|
+
| `<HollwareContent entry components />` | Renders the entry's document on the server and hydrates it in the browser |
|
|
88
|
+
|
|
89
|
+
An entry is what a reader sees: its frontmatter, tags, dates and Comark `document`. Its verdict, its
|
|
90
|
+
revision and its last publication stay on the server, because a framework that renders on the server
|
|
91
|
+
sends a loader's data to the browser.
|
|
92
|
+
|
|
93
|
+
## Components
|
|
94
|
+
|
|
95
|
+
`<HollwareContent>` renders the Comark document Hollware stored when the entry was written, with
|
|
96
|
+
`@comark/react`. `components` maps the names of the components in the entry (`::callout` is
|
|
97
|
+
`Callout`) to your own; an element with no mapping renders as the plain tag. The page needs no
|
|
98
|
+
Markdown parser: the document is already parsed.
|
|
99
|
+
|
|
100
|
+
## Previews
|
|
101
|
+
|
|
102
|
+
"Preview on the site" in the admin opens the entry's page with `?hollware-preview=<token>`: a signed
|
|
103
|
+
link for one entry, valid for one hour. `previewRedirect` answers it with a `303` to the same URL
|
|
104
|
+
without the parameter and the token in the `hollware_preview` cookie (`HttpOnly`, `SameSite=Lax`,
|
|
105
|
+
an hour, `Secure` but on `http://localhost`). `getEntry({ request })` then reads the latest revision
|
|
106
|
+
of that entry, a draft included, and the answer has `preview: true`: the page must not be kept, so
|
|
107
|
+
send it `previewHeaders(request)`. A token that is expired or not valid is an error from `getEntry`,
|
|
108
|
+
never the published version; `previewHeaders(request, error)` clears the cookie for it. A token for
|
|
109
|
+
another entry is answered as no entry.
|
|
110
|
+
|
|
111
|
+
There is no hook for the browser: it would need a read token there and CORS on Hollware, and
|
|
112
|
+
published content is already in the page the server sent.
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { EntryAnswer, EntryPage, GetEntryOptions, Hollware, HollwareOptions, HollwareReader, ListEntriesOptions, PREVIEW_COOKIE, PageDocument, PageEntry, PageSummary, ReaderOptions, Translation, createHollware, hollwareBinding, isPreviewRequest, previewCookieClear, previewHeaders, previewRedirect } from "@hollware/client";
|
|
2
|
+
import { MarkdownDocumentProps } from "@comark/react/components/MarkdownDocument";
|
|
3
|
+
import { ReactElement } from "react";
|
|
4
|
+
//#region src/content.d.ts
|
|
5
|
+
/** What `<HollwareContent>` renders: an entry as `getEntry` gives it. */
|
|
6
|
+
interface HollwareContentProps {
|
|
7
|
+
/** The entry, with its `document`: nothing renders without one. */
|
|
8
|
+
readonly entry: {
|
|
9
|
+
readonly document?: unknown;
|
|
10
|
+
} | null | undefined;
|
|
11
|
+
/**
|
|
12
|
+
* Maps the names of the components in the entry (`::callout` is `Callout`) to the site's own
|
|
13
|
+
* React components. An element with no mapping renders as the plain tag.
|
|
14
|
+
*/
|
|
15
|
+
readonly components?: NonNullable<MarkdownDocumentProps['components']>;
|
|
16
|
+
/** A class for the wrapper `div`, next to Comark's own `comark-content`. */
|
|
17
|
+
readonly className?: string;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* The body of an entry: the Comark document Hollware stored when it was written, rendered with
|
|
21
|
+
* `@comark/react`. It is a plain component, so it renders on the server and hydrates in the
|
|
22
|
+
* browser from the same document. Pass `components` to render Hollware's components as your own.
|
|
23
|
+
*/
|
|
24
|
+
export declare function HollwareContent({ entry, components, className }: HollwareContentProps): ReactElement | null;
|
|
25
|
+
//#endregion
|
|
26
|
+
//#region src/index.d.ts
|
|
27
|
+
export declare const name = "@hollware/react";
|
|
28
|
+
//#endregion
|
|
29
|
+
export { type EntryAnswer, type EntryPage, type GetEntryOptions, type Hollware, type HollwareContentProps, type HollwareOptions, type HollwareReader, type ListEntriesOptions, PREVIEW_COOKIE, type PageDocument, type PageEntry, type PageSummary, type ReaderOptions, type Translation, createHollware, hollwareBinding, isPreviewRequest, previewCookieClear, previewHeaders, previewRedirect };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { PREVIEW_COOKIE, createHollware, hollwareBinding, isPreviewRequest, previewCookieClear, previewHeaders, previewRedirect } from "@hollware/client";
|
|
2
|
+
import { MarkdownDocument } from "@comark/react/components/MarkdownDocument";
|
|
3
|
+
import { jsx } from "react/jsx-runtime";
|
|
4
|
+
//#region src/content.tsx
|
|
5
|
+
function isDocument(value) {
|
|
6
|
+
return typeof value === "object" && value !== null && Array.isArray(Reflect.get(value, "nodes"));
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* The body of an entry: the Comark document Hollware stored when it was written, rendered with
|
|
10
|
+
* `@comark/react`. It is a plain component, so it renders on the server and hydrates in the
|
|
11
|
+
* browser from the same document. Pass `components` to render Hollware's components as your own.
|
|
12
|
+
*/
|
|
13
|
+
function HollwareContent({ entry, components, className }) {
|
|
14
|
+
const document = entry?.document;
|
|
15
|
+
if (!isDocument(document)) return null;
|
|
16
|
+
return /* @__PURE__ */ jsx(MarkdownDocument, {
|
|
17
|
+
value: document,
|
|
18
|
+
components,
|
|
19
|
+
className
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
//#endregion
|
|
23
|
+
//#region src/index.ts
|
|
24
|
+
const name = "@hollware/react";
|
|
25
|
+
//#endregion
|
|
26
|
+
export { HollwareContent, PREVIEW_COOKIE, createHollware, hollwareBinding, isPreviewRequest, name, previewCookieClear, previewHeaders, previewRedirect };
|
package/package.json
CHANGED
|
@@ -1,13 +1,57 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hollware/react",
|
|
3
|
-
"version": "0.0.0",
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "1.0.0-rc.0",
|
|
4
|
+
"description": "Read Hollware from any React app: server helpers, previews and a component that renders an entry's Comark document.",
|
|
5
5
|
"homepage": "https://hollware.com",
|
|
6
|
+
"bugs": {
|
|
7
|
+
"url": "https://github.com/hollware/hollware/issues"
|
|
8
|
+
},
|
|
6
9
|
"license": "MIT",
|
|
7
10
|
"repository": {
|
|
8
11
|
"type": "git",
|
|
9
12
|
"url": "git+https://github.com/hollware/hollware.git",
|
|
10
13
|
"directory": "packages/react"
|
|
11
14
|
},
|
|
12
|
-
"
|
|
13
|
-
|
|
15
|
+
"files": [
|
|
16
|
+
"dist"
|
|
17
|
+
],
|
|
18
|
+
"type": "module",
|
|
19
|
+
"sideEffects": false,
|
|
20
|
+
"exports": {
|
|
21
|
+
".": {
|
|
22
|
+
"types": "./dist/index.d.mts",
|
|
23
|
+
"default": "./dist/index.mjs"
|
|
24
|
+
},
|
|
25
|
+
"./package.json": "./package.json"
|
|
26
|
+
},
|
|
27
|
+
"publishConfig": {
|
|
28
|
+
"access": "public",
|
|
29
|
+
"provenance": true
|
|
30
|
+
},
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"@comark/react": "0.7.0",
|
|
33
|
+
"@hollware/client": "1.0.0-rc.0",
|
|
34
|
+
"@hollware/core": "1.0.0-rc.0"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@types/node": "24.19.2",
|
|
38
|
+
"@types/react": "19.3.0",
|
|
39
|
+
"@types/react-dom": "19.3.0",
|
|
40
|
+
"publint": "0.3.25",
|
|
41
|
+
"react": "19.3.1",
|
|
42
|
+
"react-dom": "19.3.1",
|
|
43
|
+
"typescript": "7.0.2",
|
|
44
|
+
"vite-plus": "1.2.0"
|
|
45
|
+
},
|
|
46
|
+
"peerDependencies": {
|
|
47
|
+
"react": "^19.0.0"
|
|
48
|
+
},
|
|
49
|
+
"engines": {
|
|
50
|
+
"node": ">=24.11.0"
|
|
51
|
+
},
|
|
52
|
+
"scripts": {
|
|
53
|
+
"build": "vp pack",
|
|
54
|
+
"test": "vp test",
|
|
55
|
+
"publint": "publint --strict"
|
|
56
|
+
}
|
|
57
|
+
}
|