@hollware/react 0.0.0-stage → 0.11.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/LICENSE +21 -0
- package/README.md +111 -2
- package/dist/index.d.mts +29 -0
- package/dist/index.mjs +26 -0
- package/package.json +54 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026-present Sergio Azócar and Hollware contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
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,6 +1,57 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hollware/react",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.11.0",
|
|
4
|
+
"description": "Read Hollware from any React app: server helpers, previews and a component that renders an entry's Comark document.",
|
|
5
|
+
"homepage": "https://hollware.com",
|
|
6
|
+
"bugs": {
|
|
7
|
+
"url": "https://github.com/hollware/hollware/issues"
|
|
8
|
+
},
|
|
9
|
+
"license": "MIT",
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "git+https://github.com/hollware/hollware.git",
|
|
13
|
+
"directory": "packages/react"
|
|
14
|
+
},
|
|
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": "0.11.0",
|
|
34
|
+
"@hollware/core": "0.11.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
|
+
}
|
|
6
57
|
}
|