@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 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
- # Temporary Holding Version
1
+ # @hollware/react
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
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.
@@ -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.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
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
  }