@qewordly/react 0.1.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 +61 -0
- package/dist/client.d.ts +139 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +172 -0
- package/dist/components.d.ts +35 -0
- package/dist/components.d.ts.map +1 -0
- package/dist/components.js +22 -0
- package/dist/helpers.d.ts +16 -0
- package/dist/helpers.d.ts.map +1 -0
- package/dist/helpers.js +26 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +18 -0
- package/package.json +34 -0
package/README.md
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# @qewordly/react
|
|
2
|
+
|
|
3
|
+
Headless-first blog SDK. Data functions run on the server (RSC, SSG, Route Handlers); components render to static HTML with zero client JS.
|
|
4
|
+
|
|
5
|
+
## 5-minute integration (Next.js App Router)
|
|
6
|
+
|
|
7
|
+
```tsx
|
|
8
|
+
// lib/qw.ts
|
|
9
|
+
import { createQewordlyClient } from "@qewordly/react";
|
|
10
|
+
|
|
11
|
+
export const qw = createQewordlyClient({
|
|
12
|
+
baseUrl: process.env.QEWORLDLY_API_URL!, // e.g. https://api.qewordly.com
|
|
13
|
+
apiKey: process.env.QEWORLDLY_API_KEY!, // qw_live_… — server env only
|
|
14
|
+
});
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
```tsx
|
|
18
|
+
// app/blog/[slug]/page.tsx
|
|
19
|
+
import { QewordlyPost, QewordlyJsonLd, getPostJsonLd, getPostMetadata } from "@qewordly/react";
|
|
20
|
+
import { qw } from "@/lib/qw";
|
|
21
|
+
import { notFound } from "next/navigation";
|
|
22
|
+
|
|
23
|
+
export async function generateMetadata({ params }: { params: Promise<{ slug: string }> }) {
|
|
24
|
+
const post = await qw.getPost((await params).slug);
|
|
25
|
+
if (!post) return {};
|
|
26
|
+
const meta = getPostMetadata(post);
|
|
27
|
+
return { title: meta.title, description: meta.description };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export default async function PostPage({ params }: { params: Promise<{ slug: string }> }) {
|
|
31
|
+
const post = await qw.getPost((await params).slug, { revalidate: 60 });
|
|
32
|
+
if (!post) notFound();
|
|
33
|
+
return (
|
|
34
|
+
<>
|
|
35
|
+
<QewordlyPost post={post} />
|
|
36
|
+
<QewordlyJsonLd data={getPostJsonLd(post, { url: `https://yoursite.com/blog/${post.slug}` })} />
|
|
37
|
+
</>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
```tsx
|
|
43
|
+
// app/sitemap.ts
|
|
44
|
+
import { qw } from "@/lib/qw";
|
|
45
|
+
|
|
46
|
+
export default async function sitemap() {
|
|
47
|
+
const entries = await qw.getSitemapEntries();
|
|
48
|
+
return entries.map((e) => ({
|
|
49
|
+
url: `https://yoursite.com/blog/${e.slug}`,
|
|
50
|
+
lastModified: new Date(e.updatedAt),
|
|
51
|
+
}));
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Rules
|
|
56
|
+
|
|
57
|
+
- **Keys stay server-side.** Never prefix the key with `NEXT_PUBLIC_`, never fetch from the browser. There are no client hooks in this package — that is deliberate, not missing.
|
|
58
|
+
- **Theme:** `qw.getTheme()` + `<QewordlyStyles theme={theme} />` injects `--qw-*` variables. Components accept `className`; bring your own CSS.
|
|
59
|
+
- **Preview:** issue a token (`POST /workspace/preview-token`), enable Next draft mode, pass `previewToken` to any read. Drafts resolve; archived never does.
|
|
60
|
+
- **Caching:** pass `revalidate` / `tags` to map onto Next fetch semantics. Publish webhooks (US-094) will call your `revalidateTag` endpoint for instant updates.
|
|
61
|
+
- **Eject anytime:** every component is a thin wrapper — `qw.getPosts()` returns plain data you can render however you like.
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Headless Qewordly client (US-090). Framework-free data functions that run
|
|
3
|
+
* anywhere `fetch` exists — React Server Components, Route Handlers, SSG —
|
|
4
|
+
* with zero client JS required to render. Keys stay server-side: there are
|
|
5
|
+
* deliberately NO client hooks in this package (browser fetching would leak
|
|
6
|
+
* the key); see README.
|
|
7
|
+
*
|
|
8
|
+
* Wire shapes mirror the API JSON (dates are ISO strings, not Date objects)
|
|
9
|
+
* rather than reusing @repo/types rows, so the types never lie about what
|
|
10
|
+
* crossed the network.
|
|
11
|
+
*/
|
|
12
|
+
export interface TaxonomyRef {
|
|
13
|
+
id: string;
|
|
14
|
+
name: string;
|
|
15
|
+
slug: string;
|
|
16
|
+
}
|
|
17
|
+
export interface PublicPost {
|
|
18
|
+
id: string;
|
|
19
|
+
title: string;
|
|
20
|
+
slug: string;
|
|
21
|
+
content: string;
|
|
22
|
+
excerpt: string | null;
|
|
23
|
+
status: string;
|
|
24
|
+
metaTitle: string | null;
|
|
25
|
+
metaDesc: string | null;
|
|
26
|
+
coverImage: string | null;
|
|
27
|
+
publishedAt: string | null;
|
|
28
|
+
createdAt: string;
|
|
29
|
+
updatedAt: string;
|
|
30
|
+
categories: TaxonomyRef[];
|
|
31
|
+
tags: TaxonomyRef[];
|
|
32
|
+
}
|
|
33
|
+
export interface PageMeta {
|
|
34
|
+
page: number;
|
|
35
|
+
limit: number;
|
|
36
|
+
total: number;
|
|
37
|
+
}
|
|
38
|
+
export interface ListResult<T> {
|
|
39
|
+
data: T[];
|
|
40
|
+
meta: PageMeta;
|
|
41
|
+
}
|
|
42
|
+
export interface SitemapEntry {
|
|
43
|
+
slug: string;
|
|
44
|
+
updatedAt: string;
|
|
45
|
+
}
|
|
46
|
+
export interface SdkTheme {
|
|
47
|
+
primaryColor: string;
|
|
48
|
+
backgroundColor: string;
|
|
49
|
+
textColor: string;
|
|
50
|
+
fontFamily: string;
|
|
51
|
+
logoUrl: string | null;
|
|
52
|
+
}
|
|
53
|
+
export declare class QewordlyError extends Error {
|
|
54
|
+
readonly status: number;
|
|
55
|
+
readonly code: string;
|
|
56
|
+
constructor(message: string, status?: number, code?: string);
|
|
57
|
+
}
|
|
58
|
+
export interface NextFetchOptions {
|
|
59
|
+
/** Maps to Next.js fetch `next.revalidate`. Omitted = framework default. */
|
|
60
|
+
revalidate?: number | false;
|
|
61
|
+
/** Maps to Next.js fetch `next.tags`. */
|
|
62
|
+
tags?: string[];
|
|
63
|
+
}
|
|
64
|
+
export interface ClientConfig {
|
|
65
|
+
/** API origin, e.g. "https://api.qewordly.com". Trailing slash tolerated. */
|
|
66
|
+
baseUrl: string;
|
|
67
|
+
/** `qw_live_…` key. Server-side only — never ship to the browser. */
|
|
68
|
+
apiKey: string;
|
|
69
|
+
/** Injectable for tests and edge runtimes. Defaults to global fetch. */
|
|
70
|
+
fetch?: typeof fetch;
|
|
71
|
+
}
|
|
72
|
+
export interface ListOptions extends NextFetchOptions {
|
|
73
|
+
page?: number;
|
|
74
|
+
limit?: number;
|
|
75
|
+
category?: string;
|
|
76
|
+
tag?: string;
|
|
77
|
+
/** Short-lived draft token from POST /workspace/preview-token. */
|
|
78
|
+
previewToken?: string;
|
|
79
|
+
}
|
|
80
|
+
export interface ReadOptions extends NextFetchOptions {
|
|
81
|
+
previewToken?: string;
|
|
82
|
+
}
|
|
83
|
+
export declare function createQewordlyClient(config: ClientConfig): {
|
|
84
|
+
getPosts(opts?: ListOptions): Promise<ListResult<PublicPost>>;
|
|
85
|
+
/** Null when the slug has no visible post (draft without preview, unknown slug). Other failures throw. */
|
|
86
|
+
getPost(slug: string, opts?: ReadOptions): Promise<PublicPost | null>;
|
|
87
|
+
searchPosts(q: string, opts?: ListOptions): Promise<ListResult<PublicPost>>;
|
|
88
|
+
getCategories(opts?: ReadOptions): Promise<TaxonomyRef[]>;
|
|
89
|
+
getTags(opts?: ReadOptions): Promise<TaxonomyRef[]>;
|
|
90
|
+
getSitemapEntries(opts?: ReadOptions): Promise<SitemapEntry[]>;
|
|
91
|
+
getTheme(opts?: ReadOptions): Promise<SdkTheme>;
|
|
92
|
+
/**
|
|
93
|
+
* Resolve an image path from the API to an absolute URL. Uploads live on
|
|
94
|
+
* the API origin (`/uploads/…`), so relative paths resolve against this
|
|
95
|
+
* client's baseUrl; absolute URLs pass through untouched. Null stays null.
|
|
96
|
+
*/
|
|
97
|
+
getImageUrl(path: string | null): string | null;
|
|
98
|
+
};
|
|
99
|
+
export type QewordlyClient = ReturnType<typeof createQewordlyClient>;
|
|
100
|
+
export interface PostMetadata {
|
|
101
|
+
title: string;
|
|
102
|
+
description: string;
|
|
103
|
+
image: string | null;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Resolve an image path from the API to an absolute URL. Uploads live on the
|
|
107
|
+
* API origin (`/uploads/…`), so relative paths resolve against the given base
|
|
108
|
+
* (the client's baseUrl, or `imageBaseUrl`); absolute URLs pass through
|
|
109
|
+
* untouched. Null/blank stays null. Single contract shared by getPostMetadata,
|
|
110
|
+
* getPostJsonLd, and the React components.
|
|
111
|
+
*/
|
|
112
|
+
export declare function resolveImageUrl(path: string | null, imageBaseUrl?: string): string | null;
|
|
113
|
+
/**
|
|
114
|
+
* Sanity-style fallback chains: metaTitle → title → ""; metaDesc → excerpt
|
|
115
|
+
* → "". Title is never null — callers can spread this straight into
|
|
116
|
+
* generateMetadata without conditionals.
|
|
117
|
+
*/
|
|
118
|
+
export declare function getPostMetadata(post: Pick<PublicPost, "title" | "metaTitle" | "metaDesc" | "excerpt" | "coverImage">, opts?: {
|
|
119
|
+
imageBaseUrl?: string;
|
|
120
|
+
}): PostMetadata;
|
|
121
|
+
export interface ArticleJsonLd {
|
|
122
|
+
"@context": "https://schema.org";
|
|
123
|
+
"@type": "Article";
|
|
124
|
+
headline: string;
|
|
125
|
+
description?: string;
|
|
126
|
+
image?: string;
|
|
127
|
+
datePublished?: string;
|
|
128
|
+
dateModified: string;
|
|
129
|
+
url?: string;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Article structured data with only defined fields emitted. Feeds the GEO
|
|
133
|
+
* "citable content" story — the SDK renders content AI engines can quote.
|
|
134
|
+
*/
|
|
135
|
+
export declare function getPostJsonLd(post: Pick<PublicPost, "title" | "metaDesc" | "excerpt" | "coverImage" | "publishedAt" | "updatedAt">, opts?: {
|
|
136
|
+
url?: string;
|
|
137
|
+
imageBaseUrl?: string;
|
|
138
|
+
}): ArticleJsonLd;
|
|
139
|
+
//# sourceMappingURL=client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Headless Qewordly client (US-090). Framework-free data functions that run
|
|
4
|
+
* anywhere `fetch` exists — React Server Components, Route Handlers, SSG —
|
|
5
|
+
* with zero client JS required to render. Keys stay server-side: there are
|
|
6
|
+
* deliberately NO client hooks in this package (browser fetching would leak
|
|
7
|
+
* the key); see README.
|
|
8
|
+
*
|
|
9
|
+
* Wire shapes mirror the API JSON (dates are ISO strings, not Date objects)
|
|
10
|
+
* rather than reusing @repo/types rows, so the types never lie about what
|
|
11
|
+
* crossed the network.
|
|
12
|
+
*/
|
|
13
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
|
+
exports.QewordlyError = void 0;
|
|
15
|
+
exports.createQewordlyClient = createQewordlyClient;
|
|
16
|
+
exports.resolveImageUrl = resolveImageUrl;
|
|
17
|
+
exports.getPostMetadata = getPostMetadata;
|
|
18
|
+
exports.getPostJsonLd = getPostJsonLd;
|
|
19
|
+
// ─── Errors ──────────────────────────────────────────────────────────────────
|
|
20
|
+
class QewordlyError extends Error {
|
|
21
|
+
status;
|
|
22
|
+
code;
|
|
23
|
+
constructor(message, status = 0, code = "UNKNOWN") {
|
|
24
|
+
super(message);
|
|
25
|
+
this.name = "QewordlyError";
|
|
26
|
+
this.status = status;
|
|
27
|
+
this.code = code;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
exports.QewordlyError = QewordlyError;
|
|
31
|
+
function createQewordlyClient(config) {
|
|
32
|
+
const baseUrl = config.baseUrl.replace(/\/$/, "");
|
|
33
|
+
const fetchFn = (config.fetch ?? globalThis.fetch);
|
|
34
|
+
if (!config.apiKey)
|
|
35
|
+
throw new QewordlyError("apiKey is required.", 0, "MISSING_API_KEY");
|
|
36
|
+
async function doFetch(url, opts) {
|
|
37
|
+
const init = {
|
|
38
|
+
headers: { "X-API-Key": config.apiKey, Accept: "application/json" },
|
|
39
|
+
};
|
|
40
|
+
if (opts.revalidate !== undefined || opts.tags !== undefined) {
|
|
41
|
+
init.next = {};
|
|
42
|
+
if (opts.revalidate !== undefined)
|
|
43
|
+
init.next.revalidate = opts.revalidate;
|
|
44
|
+
if (opts.tags !== undefined)
|
|
45
|
+
init.next.tags = opts.tags;
|
|
46
|
+
}
|
|
47
|
+
let res;
|
|
48
|
+
try {
|
|
49
|
+
res = await fetchFn(url, init);
|
|
50
|
+
}
|
|
51
|
+
catch (err) {
|
|
52
|
+
throw new QewordlyError(`Could not reach the Qewordly API: ${err instanceof Error ? err.message : String(err)}`, 0, "FETCH_FAILED");
|
|
53
|
+
}
|
|
54
|
+
if (!res.ok) {
|
|
55
|
+
const body = (await res.json().catch(() => null));
|
|
56
|
+
throw new QewordlyError(body?.error ?? `Request failed with status ${res.status}.`, res.status, body?.code ?? "REQUEST_FAILED");
|
|
57
|
+
}
|
|
58
|
+
return res;
|
|
59
|
+
}
|
|
60
|
+
function buildUrl(path, params) {
|
|
61
|
+
const qs = new URLSearchParams();
|
|
62
|
+
for (const [k, v] of Object.entries(params)) {
|
|
63
|
+
if (v !== undefined)
|
|
64
|
+
qs.set(k, String(v));
|
|
65
|
+
}
|
|
66
|
+
return `${baseUrl}/v1/public${path}${qs.size > 0 ? `?${qs}` : ""}`;
|
|
67
|
+
}
|
|
68
|
+
async function request(path, params, opts) {
|
|
69
|
+
const res = await doFetch(buildUrl(path, params), opts);
|
|
70
|
+
const json = (await res.json());
|
|
71
|
+
return json.data;
|
|
72
|
+
}
|
|
73
|
+
async function requestList(path, params, opts) {
|
|
74
|
+
const res = await doFetch(buildUrl(path, params), opts);
|
|
75
|
+
return (await res.json());
|
|
76
|
+
}
|
|
77
|
+
const previewParam = (token) => (token ? { preview: token } : {});
|
|
78
|
+
return {
|
|
79
|
+
getPosts(opts = {}) {
|
|
80
|
+
const { previewToken, revalidate, tags, ...rest } = opts;
|
|
81
|
+
return requestList("/posts", { ...rest, ...previewParam(previewToken) }, { revalidate, tags });
|
|
82
|
+
},
|
|
83
|
+
/** Null when the slug has no visible post (draft without preview, unknown slug). Other failures throw. */
|
|
84
|
+
async getPost(slug, opts = {}) {
|
|
85
|
+
try {
|
|
86
|
+
return await request(`/posts/${encodeURIComponent(slug)}`, { ...previewParam(opts.previewToken) }, opts);
|
|
87
|
+
}
|
|
88
|
+
catch (err) {
|
|
89
|
+
if (err instanceof QewordlyError && err.status === 404)
|
|
90
|
+
return null;
|
|
91
|
+
throw err;
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
searchPosts(q, opts = {}) {
|
|
95
|
+
const { previewToken, revalidate, tags, ...rest } = opts;
|
|
96
|
+
return requestList("/search", { q, ...rest, ...previewParam(previewToken) }, { revalidate, tags });
|
|
97
|
+
},
|
|
98
|
+
getCategories(opts = {}) {
|
|
99
|
+
return request("/categories", { ...previewParam(opts.previewToken) }, opts);
|
|
100
|
+
},
|
|
101
|
+
getTags(opts = {}) {
|
|
102
|
+
return request("/tags", { ...previewParam(opts.previewToken) }, opts);
|
|
103
|
+
},
|
|
104
|
+
getSitemapEntries(opts = {}) {
|
|
105
|
+
return request("/sitemap", { ...previewParam(opts.previewToken) }, opts);
|
|
106
|
+
},
|
|
107
|
+
getTheme(opts = {}) {
|
|
108
|
+
return request("/theme", { ...previewParam(opts.previewToken) }, opts);
|
|
109
|
+
},
|
|
110
|
+
/**
|
|
111
|
+
* Resolve an image path from the API to an absolute URL. Uploads live on
|
|
112
|
+
* the API origin (`/uploads/…`), so relative paths resolve against this
|
|
113
|
+
* client's baseUrl; absolute URLs pass through untouched. Null stays null.
|
|
114
|
+
*/
|
|
115
|
+
getImageUrl(path) {
|
|
116
|
+
if (!path)
|
|
117
|
+
return null;
|
|
118
|
+
if (/^https?:\/\//i.test(path))
|
|
119
|
+
return path;
|
|
120
|
+
return `${baseUrl}${path.startsWith("/") ? "" : "/"}${path}`;
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Resolve an image path from the API to an absolute URL. Uploads live on the
|
|
126
|
+
* API origin (`/uploads/…`), so relative paths resolve against the given base
|
|
127
|
+
* (the client's baseUrl, or `imageBaseUrl`); absolute URLs pass through
|
|
128
|
+
* untouched. Null/blank stays null. Single contract shared by getPostMetadata,
|
|
129
|
+
* getPostJsonLd, and the React components.
|
|
130
|
+
*/
|
|
131
|
+
function resolveImageUrl(path, imageBaseUrl) {
|
|
132
|
+
const trimmed = path?.trim() || null;
|
|
133
|
+
if (trimmed === null)
|
|
134
|
+
return null;
|
|
135
|
+
if (/^https?:\/\//i.test(trimmed))
|
|
136
|
+
return trimmed;
|
|
137
|
+
const base = (imageBaseUrl ?? "").replace(/\/$/, "");
|
|
138
|
+
return base !== "" ? `${base}${trimmed.startsWith("/") ? "" : "/"}${trimmed}` : trimmed;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Sanity-style fallback chains: metaTitle → title → ""; metaDesc → excerpt
|
|
142
|
+
* → "". Title is never null — callers can spread this straight into
|
|
143
|
+
* generateMetadata without conditionals.
|
|
144
|
+
*/
|
|
145
|
+
function getPostMetadata(post, opts = {}) {
|
|
146
|
+
const title = post.metaTitle?.trim() || post.title || "";
|
|
147
|
+
const description = post.metaDesc?.trim() || post.excerpt?.trim() || "";
|
|
148
|
+
const image = resolveImageUrl(post.coverImage, opts.imageBaseUrl);
|
|
149
|
+
return { title, description, image };
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Article structured data with only defined fields emitted. Feeds the GEO
|
|
153
|
+
* "citable content" story — the SDK renders content AI engines can quote.
|
|
154
|
+
*/
|
|
155
|
+
function getPostJsonLd(post, opts = {}) {
|
|
156
|
+
const description = post.metaDesc?.trim() || post.excerpt?.trim() || undefined;
|
|
157
|
+
// Relative API paths ("/uploads/…") would be broken image URLs in structured
|
|
158
|
+
// data — resolve against the API origin like getPostMetadata does.
|
|
159
|
+
const image = resolveImageUrl(post.coverImage, opts.imageBaseUrl);
|
|
160
|
+
// Conditional spreads keep key order deterministic (headline → optionals →
|
|
161
|
+
// dateModified → url) regardless of which fields are present.
|
|
162
|
+
return {
|
|
163
|
+
"@context": "https://schema.org",
|
|
164
|
+
"@type": "Article",
|
|
165
|
+
headline: post.title,
|
|
166
|
+
...(description !== undefined ? { description } : {}),
|
|
167
|
+
...(image ? { image } : {}),
|
|
168
|
+
...(post.publishedAt ? { datePublished: post.publishedAt } : {}),
|
|
169
|
+
dateModified: post.updatedAt,
|
|
170
|
+
...(opts.url ? { url: opts.url } : {}),
|
|
171
|
+
};
|
|
172
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Presentational components (US-090). Server Components by default — no
|
|
3
|
+
* "use client", no hooks, no browser APIs — so they render to static HTML
|
|
4
|
+
* with zero client JS. Every component is a thin wrapper over plain data:
|
|
5
|
+
* pass props from the headless client and keep your own design via
|
|
6
|
+
* className. No CSS is shipped; theme variables come from QewordlyStyles.
|
|
7
|
+
*/
|
|
8
|
+
import type { ReactNode } from "react";
|
|
9
|
+
import type { PublicPost, SdkTheme } from "./client";
|
|
10
|
+
import type { ArticleJsonLd } from "./client";
|
|
11
|
+
export declare function QewordlyStyles({ theme }: {
|
|
12
|
+
theme: SdkTheme;
|
|
13
|
+
}): import("react").JSX.Element;
|
|
14
|
+
export declare function QewordlyJsonLd({ data }: {
|
|
15
|
+
data: ArticleJsonLd;
|
|
16
|
+
}): import("react").JSX.Element;
|
|
17
|
+
export interface QewordlyPostProps {
|
|
18
|
+
post: PublicPost;
|
|
19
|
+
className?: string;
|
|
20
|
+
/**
|
|
21
|
+
* API origin used to resolve relative coverImage paths (`/uploads/…`),
|
|
22
|
+
* same contract as the client's getImageUrl. Absolute URLs pass through;
|
|
23
|
+
* omit it and relative paths render unchanged (previous behavior).
|
|
24
|
+
*/
|
|
25
|
+
imageBaseUrl?: string;
|
|
26
|
+
}
|
|
27
|
+
export declare function QewordlyPost({ post, className, imageBaseUrl }: QewordlyPostProps): import("react").JSX.Element;
|
|
28
|
+
export interface QewordlyBlogProps {
|
|
29
|
+
posts: PublicPost[];
|
|
30
|
+
className?: string;
|
|
31
|
+
/** Host-owned item rendering (links, cards, dates). Default is title + excerpt, deliberately link-free: routes belong to the host. */
|
|
32
|
+
renderPost?: (post: PublicPost) => ReactNode;
|
|
33
|
+
}
|
|
34
|
+
export declare function QewordlyBlog({ posts, className, renderPost }: QewordlyBlogProps): import("react").JSX.Element;
|
|
35
|
+
//# sourceMappingURL=components.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"components.d.ts","sourceRoot":"","sources":["../src/components.tsx"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AACvC,OAAO,KAAK,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAC;AAGrD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAE9C,wBAAgB,cAAc,CAAC,EAAE,KAAK,EAAE,EAAE;IAAE,KAAK,EAAE,QAAQ,CAAA;CAAE,+BAE5D;AAED,wBAAgB,cAAc,CAAC,EAAE,IAAI,EAAE,EAAE;IAAE,IAAI,EAAE,aAAa,CAAA;CAAE,+BAE/D;AAED,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,UAAU,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;OAIG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,wBAAgB,YAAY,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,YAAY,EAAE,EAAE,iBAAiB,+BAWhF;AAED,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,UAAU,EAAE,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,sIAAsI;IACtI,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,UAAU,KAAK,SAAS,CAAC;CAC9C;AAED,wBAAgB,YAAY,CAAC,EAAE,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,EAAE,iBAAiB,+BAe/E"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.QewordlyStyles = QewordlyStyles;
|
|
4
|
+
exports.QewordlyJsonLd = QewordlyJsonLd;
|
|
5
|
+
exports.QewordlyPost = QewordlyPost;
|
|
6
|
+
exports.QewordlyBlog = QewordlyBlog;
|
|
7
|
+
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
8
|
+
const client_1 = require("./client");
|
|
9
|
+
const helpers_1 = require("./helpers");
|
|
10
|
+
function QewordlyStyles({ theme }) {
|
|
11
|
+
return (0, jsx_runtime_1.jsx)("style", { children: (0, helpers_1.themeToCssText)(theme) });
|
|
12
|
+
}
|
|
13
|
+
function QewordlyJsonLd({ data }) {
|
|
14
|
+
return (0, jsx_runtime_1.jsx)("script", { type: "application/ld+json", children: JSON.stringify(data) });
|
|
15
|
+
}
|
|
16
|
+
function QewordlyPost({ post, className, imageBaseUrl }) {
|
|
17
|
+
const cover = (0, client_1.resolveImageUrl)(post.coverImage, imageBaseUrl);
|
|
18
|
+
return ((0, jsx_runtime_1.jsxs)("article", { className: className, children: [(0, jsx_runtime_1.jsx)("h1", { children: post.title }), cover && (0, jsx_runtime_1.jsx)("img", { src: cover, alt: post.title }), (0, jsx_runtime_1.jsx)("div", { dangerouslySetInnerHTML: { __html: post.content } })] }));
|
|
19
|
+
}
|
|
20
|
+
function QewordlyBlog({ posts, className, renderPost }) {
|
|
21
|
+
return ((0, jsx_runtime_1.jsx)("div", { className: className, children: posts.map((post) => renderPost ? ((0, jsx_runtime_1.jsx)("div", { children: renderPost(post) }, post.id)) : ((0, jsx_runtime_1.jsxs)("article", { children: [(0, jsx_runtime_1.jsx)("h2", { children: post.title }), post.excerpt && (0, jsx_runtime_1.jsx)("p", { children: post.excerpt })] }, post.id))) }));
|
|
22
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Theme → CSS variable helpers (US-090/US-091). Pure and framework-free.
|
|
3
|
+
* Trust boundary: values originate from the tenant's own dashboard, so no
|
|
4
|
+
* escaping is applied — never feed untrusted input through these.
|
|
5
|
+
*/
|
|
6
|
+
import type { SdkTheme } from "./client";
|
|
7
|
+
export declare const THEME_VARS: {
|
|
8
|
+
readonly primary: "--qw-primary";
|
|
9
|
+
readonly background: "--qw-background";
|
|
10
|
+
readonly text: "--qw-text";
|
|
11
|
+
readonly font: "--qw-font";
|
|
12
|
+
};
|
|
13
|
+
export declare function themeToCssVars(theme: SdkTheme): Record<string, string>;
|
|
14
|
+
/** `:root{--qw-primary:…;…}` block for QewordlyStyles. */
|
|
15
|
+
export declare function themeToCssText(theme: SdkTheme): string;
|
|
16
|
+
//# sourceMappingURL=helpers.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"helpers.d.ts","sourceRoot":"","sources":["../src/helpers.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAC;AAEzC,eAAO,MAAM,UAAU;;;;;CAKb,CAAC;AAEX,wBAAgB,cAAc,CAAC,KAAK,EAAE,QAAQ,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAOtE;AAED,0DAA0D;AAC1D,wBAAgB,cAAc,CAAC,KAAK,EAAE,QAAQ,GAAG,MAAM,CAKtD"}
|
package/dist/helpers.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.THEME_VARS = void 0;
|
|
4
|
+
exports.themeToCssVars = themeToCssVars;
|
|
5
|
+
exports.themeToCssText = themeToCssText;
|
|
6
|
+
exports.THEME_VARS = {
|
|
7
|
+
primary: "--qw-primary",
|
|
8
|
+
background: "--qw-background",
|
|
9
|
+
text: "--qw-text",
|
|
10
|
+
font: "--qw-font",
|
|
11
|
+
};
|
|
12
|
+
function themeToCssVars(theme) {
|
|
13
|
+
return {
|
|
14
|
+
[exports.THEME_VARS.primary]: theme.primaryColor,
|
|
15
|
+
[exports.THEME_VARS.background]: theme.backgroundColor,
|
|
16
|
+
[exports.THEME_VARS.text]: theme.textColor,
|
|
17
|
+
[exports.THEME_VARS.font]: theme.fontFamily,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
/** `:root{--qw-primary:…;…}` block for QewordlyStyles. */
|
|
21
|
+
function themeToCssText(theme) {
|
|
22
|
+
const vars = themeToCssVars(theme);
|
|
23
|
+
return `:root{${Object.entries(vars)
|
|
24
|
+
.map(([k, v]) => `${k}:${v}`)
|
|
25
|
+
.join(";")}}`;
|
|
26
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { createQewordlyClient, QewordlyError, getPostMetadata, getPostJsonLd, resolveImageUrl, } from "./client";
|
|
2
|
+
export type { QewordlyClient, ClientConfig, ListOptions, ReadOptions, NextFetchOptions, PublicPost, TaxonomyRef, PageMeta, ListResult, SitemapEntry, SdkTheme, PostMetadata, ArticleJsonLd, } from "./client";
|
|
3
|
+
export { themeToCssVars, themeToCssText, THEME_VARS } from "./helpers";
|
|
4
|
+
export { QewordlyStyles, QewordlyJsonLd, QewordlyPost, QewordlyBlog } from "./components";
|
|
5
|
+
export type { QewordlyPostProps, QewordlyBlogProps } from "./components";
|
|
6
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,oBAAoB,EACpB,aAAa,EACb,eAAe,EACf,aAAa,EACb,eAAe,GAChB,MAAM,UAAU,CAAC;AAClB,YAAY,EACV,cAAc,EACd,YAAY,EACZ,WAAW,EACX,WAAW,EACX,gBAAgB,EAChB,UAAU,EACV,WAAW,EACX,QAAQ,EACR,UAAU,EACV,YAAY,EACZ,QAAQ,EACR,YAAY,EACZ,aAAa,GACd,MAAM,UAAU,CAAC;AAClB,OAAO,EAAE,cAAc,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AACvE,OAAO,EAAE,cAAc,EAAE,cAAc,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAC1F,YAAY,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.QewordlyBlog = exports.QewordlyPost = exports.QewordlyJsonLd = exports.QewordlyStyles = exports.THEME_VARS = exports.themeToCssText = exports.themeToCssVars = exports.resolveImageUrl = exports.getPostJsonLd = exports.getPostMetadata = exports.QewordlyError = exports.createQewordlyClient = void 0;
|
|
4
|
+
var client_1 = require("./client");
|
|
5
|
+
Object.defineProperty(exports, "createQewordlyClient", { enumerable: true, get: function () { return client_1.createQewordlyClient; } });
|
|
6
|
+
Object.defineProperty(exports, "QewordlyError", { enumerable: true, get: function () { return client_1.QewordlyError; } });
|
|
7
|
+
Object.defineProperty(exports, "getPostMetadata", { enumerable: true, get: function () { return client_1.getPostMetadata; } });
|
|
8
|
+
Object.defineProperty(exports, "getPostJsonLd", { enumerable: true, get: function () { return client_1.getPostJsonLd; } });
|
|
9
|
+
Object.defineProperty(exports, "resolveImageUrl", { enumerable: true, get: function () { return client_1.resolveImageUrl; } });
|
|
10
|
+
var helpers_1 = require("./helpers");
|
|
11
|
+
Object.defineProperty(exports, "themeToCssVars", { enumerable: true, get: function () { return helpers_1.themeToCssVars; } });
|
|
12
|
+
Object.defineProperty(exports, "themeToCssText", { enumerable: true, get: function () { return helpers_1.themeToCssText; } });
|
|
13
|
+
Object.defineProperty(exports, "THEME_VARS", { enumerable: true, get: function () { return helpers_1.THEME_VARS; } });
|
|
14
|
+
var components_1 = require("./components");
|
|
15
|
+
Object.defineProperty(exports, "QewordlyStyles", { enumerable: true, get: function () { return components_1.QewordlyStyles; } });
|
|
16
|
+
Object.defineProperty(exports, "QewordlyJsonLd", { enumerable: true, get: function () { return components_1.QewordlyJsonLd; } });
|
|
17
|
+
Object.defineProperty(exports, "QewordlyPost", { enumerable: true, get: function () { return components_1.QewordlyPost; } });
|
|
18
|
+
Object.defineProperty(exports, "QewordlyBlog", { enumerable: true, get: function () { return components_1.QewordlyBlog; } });
|
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@qewordly/react",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"description": "Headless-first React SDK for Qewordly blogs. Server Components supported, zero client JS required.",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"default": "./dist/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist",
|
|
16
|
+
"README.md"
|
|
17
|
+
],
|
|
18
|
+
"scripts": {
|
|
19
|
+
"build": "tsc --project tsconfig.build.json",
|
|
20
|
+
"check-types": "tsc --noEmit",
|
|
21
|
+
"prepublishOnly": "npm run build"
|
|
22
|
+
},
|
|
23
|
+
"peerDependencies": {
|
|
24
|
+
"react": ">=18"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"@repo/typescript-config": "*",
|
|
28
|
+
"@types/react": "19.2.18",
|
|
29
|
+
"@types/react-dom": "19.2.5",
|
|
30
|
+
"react": "19.2.8",
|
|
31
|
+
"react-dom": "19.2.8",
|
|
32
|
+
"typescript": "7.0.2"
|
|
33
|
+
}
|
|
34
|
+
}
|