@lvlz/sdk 0.1.0 → 0.1.2
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 +24 -2
- package/dist/index.cjs +62 -0
- package/dist/index.d.cts +49 -0
- package/dist/index.d.ts +49 -0
- package/dist/index.js +59 -0
- package/dist/react.cjs +78 -0
- package/dist/react.d.cts +33 -0
- package/dist/react.d.ts +33 -0
- package/dist/react.js +74 -0
- package/package.json +5 -2
package/README.md
CHANGED
|
@@ -126,5 +126,27 @@ Add `data-slug="my-post-slug"` to render one specific post.
|
|
|
126
126
|
| `client.getPosts({ limit?, offset? })` | `{ posts, total, limit, offset }` |
|
|
127
127
|
| `client.getPost(slug)` | `PublicPost \| null` |
|
|
128
128
|
|
|
129
|
-
|
|
130
|
-
|
|
129
|
+
## Caching
|
|
130
|
+
|
|
131
|
+
The feed is edge-cached (`s-maxage=300, stale-while-revalidate=86400`), so reads are fast and
|
|
132
|
+
won't hammer your quota. Two things to know:
|
|
133
|
+
|
|
134
|
+
- **Empty responses are never cached** — as soon as your first post is published it shows up
|
|
135
|
+
(no stale empty feed).
|
|
136
|
+
- `getPosts()` always sends `limit`/`offset`, so every call hits one canonical, cacheable URL.
|
|
137
|
+
|
|
138
|
+
Control the **framework-level** cache via `fetchOptions`:
|
|
139
|
+
|
|
140
|
+
```ts
|
|
141
|
+
// Next.js — ISR: re-fetch at most every 10 minutes (pairs with `export const revalidate`)
|
|
142
|
+
const lvlz = createClient({
|
|
143
|
+
apiKey: process.env.LVLZ_KEY!,
|
|
144
|
+
fetchOptions: { next: { revalidate: 600 } },
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
// Always fresh (e.g. a preview/dev environment)
|
|
148
|
+
const lvlz = createClient({ apiKey: '…', fetchOptions: { cache: 'no-store' } });
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
New posts appear within the `s-maxage` window (≤5 min) of your `revalidate` interval.
|
|
152
|
+
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
var DEFAULT_BASE = "https://app.lvlz.ai";
|
|
5
|
+
var LvlzError = class extends Error {
|
|
6
|
+
constructor(message, status) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.name = "LvlzError";
|
|
9
|
+
this.status = status;
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
function createClient(options) {
|
|
13
|
+
const base = (options.baseUrl ?? DEFAULT_BASE).replace(/\/+$/, "");
|
|
14
|
+
const key = options.apiKey.trim();
|
|
15
|
+
async function request(path, params, init) {
|
|
16
|
+
const url = new URL(`${base}/api/sdk/v1${path}`);
|
|
17
|
+
url.searchParams.set("key", key);
|
|
18
|
+
for (const [k, v] of Object.entries(params)) {
|
|
19
|
+
if (v !== void 0) url.searchParams.set(k, String(v));
|
|
20
|
+
}
|
|
21
|
+
const res = await fetch(url.toString(), {
|
|
22
|
+
credentials: "omit",
|
|
23
|
+
...options.fetchOptions,
|
|
24
|
+
...init
|
|
25
|
+
});
|
|
26
|
+
if (!res.ok) {
|
|
27
|
+
let message = `Request failed (${res.status})`;
|
|
28
|
+
try {
|
|
29
|
+
const body = await res.json();
|
|
30
|
+
if (body?.error) message = body.error;
|
|
31
|
+
} catch {
|
|
32
|
+
}
|
|
33
|
+
throw new LvlzError(message, res.status);
|
|
34
|
+
}
|
|
35
|
+
return res.json();
|
|
36
|
+
}
|
|
37
|
+
return {
|
|
38
|
+
getPosts(opts = {}, init) {
|
|
39
|
+
return request(
|
|
40
|
+
"/posts",
|
|
41
|
+
{ limit: opts.limit ?? 20, offset: opts.offset ?? 0 },
|
|
42
|
+
init
|
|
43
|
+
);
|
|
44
|
+
},
|
|
45
|
+
async getPost(slug, init) {
|
|
46
|
+
try {
|
|
47
|
+
const { post } = await request(
|
|
48
|
+
`/posts/${encodeURIComponent(slug)}`,
|
|
49
|
+
{},
|
|
50
|
+
init
|
|
51
|
+
);
|
|
52
|
+
return post;
|
|
53
|
+
} catch (e) {
|
|
54
|
+
if (e instanceof LvlzError && e.status === 404) return null;
|
|
55
|
+
throw e;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
exports.LvlzError = LvlzError;
|
|
62
|
+
exports.createClient = createClient;
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
interface PublicPost {
|
|
2
|
+
id: string;
|
|
3
|
+
slug: string;
|
|
4
|
+
title: string;
|
|
5
|
+
excerpt: string | null;
|
|
6
|
+
description: string | null;
|
|
7
|
+
/** Clean semantic HTML, ready to render. */
|
|
8
|
+
html: string;
|
|
9
|
+
keywords: string[];
|
|
10
|
+
wordCount: number;
|
|
11
|
+
publishedAt: string | null;
|
|
12
|
+
updatedAt: string;
|
|
13
|
+
canonicalUrl?: string | null;
|
|
14
|
+
coverImage?: string | null;
|
|
15
|
+
/** BlogPosting JSON-LD string — inject in a <script type="application/ld+json">. */
|
|
16
|
+
jsonLd: string;
|
|
17
|
+
}
|
|
18
|
+
interface PublicPostList {
|
|
19
|
+
posts: PublicPost[];
|
|
20
|
+
total: number;
|
|
21
|
+
limit: number;
|
|
22
|
+
offset: number;
|
|
23
|
+
}
|
|
24
|
+
interface LvlzClientOptions {
|
|
25
|
+
/** Your publishable key (lvlz_pub_…). Safe to ship in client code. */
|
|
26
|
+
apiKey: string;
|
|
27
|
+
/** Defaults to https://app.lvlz.ai */
|
|
28
|
+
baseUrl?: string;
|
|
29
|
+
/**
|
|
30
|
+
* Extra fetch options merged into every request — e.g. Next.js caching:
|
|
31
|
+
* `{ next: { revalidate: 600 } }`.
|
|
32
|
+
*/
|
|
33
|
+
fetchOptions?: RequestInit;
|
|
34
|
+
}
|
|
35
|
+
interface GetPostsOptions {
|
|
36
|
+
limit?: number;
|
|
37
|
+
offset?: number;
|
|
38
|
+
}
|
|
39
|
+
declare class LvlzError extends Error {
|
|
40
|
+
status: number;
|
|
41
|
+
constructor(message: string, status: number);
|
|
42
|
+
}
|
|
43
|
+
interface LvlzClient {
|
|
44
|
+
getPosts(options?: GetPostsOptions, init?: RequestInit): Promise<PublicPostList>;
|
|
45
|
+
getPost(slug: string, init?: RequestInit): Promise<PublicPost | null>;
|
|
46
|
+
}
|
|
47
|
+
declare function createClient(options: LvlzClientOptions): LvlzClient;
|
|
48
|
+
|
|
49
|
+
export { type GetPostsOptions, type LvlzClient, type LvlzClientOptions, LvlzError, type PublicPost, type PublicPostList, createClient };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
interface PublicPost {
|
|
2
|
+
id: string;
|
|
3
|
+
slug: string;
|
|
4
|
+
title: string;
|
|
5
|
+
excerpt: string | null;
|
|
6
|
+
description: string | null;
|
|
7
|
+
/** Clean semantic HTML, ready to render. */
|
|
8
|
+
html: string;
|
|
9
|
+
keywords: string[];
|
|
10
|
+
wordCount: number;
|
|
11
|
+
publishedAt: string | null;
|
|
12
|
+
updatedAt: string;
|
|
13
|
+
canonicalUrl?: string | null;
|
|
14
|
+
coverImage?: string | null;
|
|
15
|
+
/** BlogPosting JSON-LD string — inject in a <script type="application/ld+json">. */
|
|
16
|
+
jsonLd: string;
|
|
17
|
+
}
|
|
18
|
+
interface PublicPostList {
|
|
19
|
+
posts: PublicPost[];
|
|
20
|
+
total: number;
|
|
21
|
+
limit: number;
|
|
22
|
+
offset: number;
|
|
23
|
+
}
|
|
24
|
+
interface LvlzClientOptions {
|
|
25
|
+
/** Your publishable key (lvlz_pub_…). Safe to ship in client code. */
|
|
26
|
+
apiKey: string;
|
|
27
|
+
/** Defaults to https://app.lvlz.ai */
|
|
28
|
+
baseUrl?: string;
|
|
29
|
+
/**
|
|
30
|
+
* Extra fetch options merged into every request — e.g. Next.js caching:
|
|
31
|
+
* `{ next: { revalidate: 600 } }`.
|
|
32
|
+
*/
|
|
33
|
+
fetchOptions?: RequestInit;
|
|
34
|
+
}
|
|
35
|
+
interface GetPostsOptions {
|
|
36
|
+
limit?: number;
|
|
37
|
+
offset?: number;
|
|
38
|
+
}
|
|
39
|
+
declare class LvlzError extends Error {
|
|
40
|
+
status: number;
|
|
41
|
+
constructor(message: string, status: number);
|
|
42
|
+
}
|
|
43
|
+
interface LvlzClient {
|
|
44
|
+
getPosts(options?: GetPostsOptions, init?: RequestInit): Promise<PublicPostList>;
|
|
45
|
+
getPost(slug: string, init?: RequestInit): Promise<PublicPost | null>;
|
|
46
|
+
}
|
|
47
|
+
declare function createClient(options: LvlzClientOptions): LvlzClient;
|
|
48
|
+
|
|
49
|
+
export { type GetPostsOptions, type LvlzClient, type LvlzClientOptions, LvlzError, type PublicPost, type PublicPostList, createClient };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
var DEFAULT_BASE = "https://app.lvlz.ai";
|
|
3
|
+
var LvlzError = class extends Error {
|
|
4
|
+
constructor(message, status) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.name = "LvlzError";
|
|
7
|
+
this.status = status;
|
|
8
|
+
}
|
|
9
|
+
};
|
|
10
|
+
function createClient(options) {
|
|
11
|
+
const base = (options.baseUrl ?? DEFAULT_BASE).replace(/\/+$/, "");
|
|
12
|
+
const key = options.apiKey.trim();
|
|
13
|
+
async function request(path, params, init) {
|
|
14
|
+
const url = new URL(`${base}/api/sdk/v1${path}`);
|
|
15
|
+
url.searchParams.set("key", key);
|
|
16
|
+
for (const [k, v] of Object.entries(params)) {
|
|
17
|
+
if (v !== void 0) url.searchParams.set(k, String(v));
|
|
18
|
+
}
|
|
19
|
+
const res = await fetch(url.toString(), {
|
|
20
|
+
credentials: "omit",
|
|
21
|
+
...options.fetchOptions,
|
|
22
|
+
...init
|
|
23
|
+
});
|
|
24
|
+
if (!res.ok) {
|
|
25
|
+
let message = `Request failed (${res.status})`;
|
|
26
|
+
try {
|
|
27
|
+
const body = await res.json();
|
|
28
|
+
if (body?.error) message = body.error;
|
|
29
|
+
} catch {
|
|
30
|
+
}
|
|
31
|
+
throw new LvlzError(message, res.status);
|
|
32
|
+
}
|
|
33
|
+
return res.json();
|
|
34
|
+
}
|
|
35
|
+
return {
|
|
36
|
+
getPosts(opts = {}, init) {
|
|
37
|
+
return request(
|
|
38
|
+
"/posts",
|
|
39
|
+
{ limit: opts.limit ?? 20, offset: opts.offset ?? 0 },
|
|
40
|
+
init
|
|
41
|
+
);
|
|
42
|
+
},
|
|
43
|
+
async getPost(slug, init) {
|
|
44
|
+
try {
|
|
45
|
+
const { post } = await request(
|
|
46
|
+
`/posts/${encodeURIComponent(slug)}`,
|
|
47
|
+
{},
|
|
48
|
+
init
|
|
49
|
+
);
|
|
50
|
+
return post;
|
|
51
|
+
} catch (e) {
|
|
52
|
+
if (e instanceof LvlzError && e.status === 404) return null;
|
|
53
|
+
throw e;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export { LvlzError, createClient };
|
package/dist/react.cjs
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var jsxRuntime = require('react/jsx-runtime');
|
|
4
|
+
|
|
5
|
+
// src/react.tsx
|
|
6
|
+
var STYLE_TAG_ID = "lvlz-sdk-styles";
|
|
7
|
+
var CSS = `
|
|
8
|
+
.lvlz-article{max-width:768px;margin:0 auto;color:inherit}
|
|
9
|
+
.lvlz-cover{width:100%;aspect-ratio:16/9;border-radius:16px;overflow:hidden;margin:0 0 2rem;background:#f3f3f5}
|
|
10
|
+
.lvlz-cover img{width:100%;height:100%;object-fit:cover;display:block}
|
|
11
|
+
.lvlz-title{font-size:2.25rem;font-weight:780;line-height:1.15;margin:0 0 1.25rem;letter-spacing:-.02em}
|
|
12
|
+
.lvlz-prose{line-height:1.75;font-size:1.05rem}
|
|
13
|
+
.lvlz-prose h2{font-size:1.6rem;font-weight:720;margin:2.2rem 0 .8rem;line-height:1.25;letter-spacing:-.01em}
|
|
14
|
+
.lvlz-prose h3{font-size:1.25rem;font-weight:680;margin:1.6rem 0 .6rem}
|
|
15
|
+
.lvlz-prose p{margin:0 0 1.1rem}
|
|
16
|
+
.lvlz-prose a{color:#7c3aed;text-decoration:underline;text-underline-offset:2px}
|
|
17
|
+
.lvlz-prose ul,.lvlz-prose ol{margin:0 0 1.1rem 1.25rem}
|
|
18
|
+
.lvlz-prose li{margin:.35rem 0}
|
|
19
|
+
.lvlz-prose blockquote{border-left:3px solid #7c3aed;padding-left:1rem;margin:1.4rem 0;color:#555;font-style:italic}
|
|
20
|
+
.lvlz-prose img{width:100%;height:auto;border-radius:12px;margin:1.6rem 0;display:block}
|
|
21
|
+
.lvlz-list{display:grid;gap:1.5rem;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));list-style:none;padding:0;margin:0}
|
|
22
|
+
.lvlz-card{overflow:hidden;border-radius:14px;border:1px solid rgba(0,0,0,.08);background:#fff;transition:transform .15s ease,box-shadow .15s ease}
|
|
23
|
+
.lvlz-card:hover{transform:translateY(-2px);box-shadow:0 10px 30px rgba(0,0,0,.08)}
|
|
24
|
+
.lvlz-card a{text-decoration:none;color:inherit;display:flex;flex-direction:column;height:100%}
|
|
25
|
+
.lvlz-card-thumb{aspect-ratio:16/9;overflow:hidden;background:#f3f3f5}
|
|
26
|
+
.lvlz-card-thumb img{width:100%;height:100%;object-fit:cover;display:block;transition:transform .3s ease}
|
|
27
|
+
.lvlz-card:hover .lvlz-card-thumb img{transform:scale(1.04)}
|
|
28
|
+
.lvlz-card-body{padding:1rem 1.1rem 1.2rem}
|
|
29
|
+
.lvlz-card-title{font-size:1.05rem;font-weight:680;margin:0 0 .4rem;line-height:1.3}
|
|
30
|
+
.lvlz-card-excerpt{font-size:.9rem;color:#666;margin:0;line-height:1.5}
|
|
31
|
+
@media (prefers-color-scheme:dark){
|
|
32
|
+
.lvlz-card{background:#15151a;border-color:rgba(255,255,255,.1)}
|
|
33
|
+
.lvlz-card-excerpt{color:#a1a1aa}
|
|
34
|
+
.lvlz-prose blockquote{color:#a1a1aa}
|
|
35
|
+
}
|
|
36
|
+
`;
|
|
37
|
+
function LvlzStyles() {
|
|
38
|
+
return /* @__PURE__ */ jsxRuntime.jsx("style", { id: STYLE_TAG_ID, dangerouslySetInnerHTML: { __html: CSS } });
|
|
39
|
+
}
|
|
40
|
+
function LvlzArticle({
|
|
41
|
+
post,
|
|
42
|
+
className,
|
|
43
|
+
includeJsonLd = true,
|
|
44
|
+
showCover = true,
|
|
45
|
+
showTitle = true,
|
|
46
|
+
styled = true
|
|
47
|
+
}) {
|
|
48
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("article", { className: ["lvlz-article", className].filter(Boolean).join(" "), children: [
|
|
49
|
+
styled ? /* @__PURE__ */ jsxRuntime.jsx(LvlzStyles, {}) : null,
|
|
50
|
+
includeJsonLd && post.jsonLd ? /* @__PURE__ */ jsxRuntime.jsx("script", { type: "application/ld+json", dangerouslySetInnerHTML: { __html: post.jsonLd } }) : null,
|
|
51
|
+
showCover && post.coverImage ? /* @__PURE__ */ jsxRuntime.jsx("figure", { className: "lvlz-cover", children: /* @__PURE__ */ jsxRuntime.jsx("img", { src: post.coverImage, alt: post.title, loading: "eager" }) }) : null,
|
|
52
|
+
showTitle ? /* @__PURE__ */ jsxRuntime.jsx("h1", { className: "lvlz-title", children: post.title }) : null,
|
|
53
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "lvlz-prose", dangerouslySetInnerHTML: { __html: post.html } })
|
|
54
|
+
] });
|
|
55
|
+
}
|
|
56
|
+
function LvlzPostList({
|
|
57
|
+
posts,
|
|
58
|
+
className,
|
|
59
|
+
hrefFor,
|
|
60
|
+
renderItem,
|
|
61
|
+
styled = true
|
|
62
|
+
}) {
|
|
63
|
+
const href = hrefFor ?? ((p) => `/blog/${p.slug}`);
|
|
64
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("ul", { className: ["lvlz-list", className].filter(Boolean).join(" "), children: [
|
|
65
|
+
styled ? /* @__PURE__ */ jsxRuntime.jsx(LvlzStyles, {}) : null,
|
|
66
|
+
posts.map((post) => /* @__PURE__ */ jsxRuntime.jsx("li", { className: "lvlz-card", children: renderItem ? renderItem(post) : /* @__PURE__ */ jsxRuntime.jsxs("a", { href: href(post), children: [
|
|
67
|
+
post.coverImage ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "lvlz-card-thumb", children: /* @__PURE__ */ jsxRuntime.jsx("img", { src: post.coverImage, alt: post.title, loading: "lazy" }) }) : null,
|
|
68
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "lvlz-card-body", children: [
|
|
69
|
+
/* @__PURE__ */ jsxRuntime.jsx("h3", { className: "lvlz-card-title", children: post.title }),
|
|
70
|
+
post.excerpt ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "lvlz-card-excerpt", children: post.excerpt }) : null
|
|
71
|
+
] })
|
|
72
|
+
] }) }, post.id))
|
|
73
|
+
] });
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
exports.LvlzArticle = LvlzArticle;
|
|
77
|
+
exports.LvlzPostList = LvlzPostList;
|
|
78
|
+
exports.LvlzStyles = LvlzStyles;
|
package/dist/react.d.cts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { PublicPost } from './index.cjs';
|
|
3
|
+
|
|
4
|
+
/** Inject the default LVLZ styles once. Safe to render multiple times. */
|
|
5
|
+
declare function LvlzStyles(): React.JSX.Element;
|
|
6
|
+
interface LvlzArticleProps {
|
|
7
|
+
post: PublicPost;
|
|
8
|
+
className?: string;
|
|
9
|
+
/** Render the BlogPosting JSON-LD <script>. Default true. */
|
|
10
|
+
includeJsonLd?: boolean;
|
|
11
|
+
/** Render the cover image hero. Default true. */
|
|
12
|
+
showCover?: boolean;
|
|
13
|
+
/** Render the post title as an <h1>. Default true. */
|
|
14
|
+
showTitle?: boolean;
|
|
15
|
+
/** Inject default styling. Set false to bring your own CSS. Default true. */
|
|
16
|
+
styled?: boolean;
|
|
17
|
+
}
|
|
18
|
+
/** A full, polished article: cover hero + title + HTML body (inline images included). */
|
|
19
|
+
declare function LvlzArticle({ post, className, includeJsonLd, showCover, showTitle, styled, }: LvlzArticleProps): React.JSX.Element;
|
|
20
|
+
interface LvlzPostListProps {
|
|
21
|
+
posts: PublicPost[];
|
|
22
|
+
className?: string;
|
|
23
|
+
/** Build the href for a post. Default: `/blog/{slug}`. */
|
|
24
|
+
hrefFor?: (post: PublicPost) => string;
|
|
25
|
+
/** Custom item renderer; overrides the default card. */
|
|
26
|
+
renderItem?: (post: PublicPost) => React.ReactNode;
|
|
27
|
+
/** Inject default styling. Default true. */
|
|
28
|
+
styled?: boolean;
|
|
29
|
+
}
|
|
30
|
+
/** A responsive grid of post cards with cover thumbnails. */
|
|
31
|
+
declare function LvlzPostList({ posts, className, hrefFor, renderItem, styled, }: LvlzPostListProps): React.JSX.Element;
|
|
32
|
+
|
|
33
|
+
export { LvlzArticle, type LvlzArticleProps, LvlzPostList, type LvlzPostListProps, LvlzStyles };
|
package/dist/react.d.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { PublicPost } from './index.js';
|
|
3
|
+
|
|
4
|
+
/** Inject the default LVLZ styles once. Safe to render multiple times. */
|
|
5
|
+
declare function LvlzStyles(): React.JSX.Element;
|
|
6
|
+
interface LvlzArticleProps {
|
|
7
|
+
post: PublicPost;
|
|
8
|
+
className?: string;
|
|
9
|
+
/** Render the BlogPosting JSON-LD <script>. Default true. */
|
|
10
|
+
includeJsonLd?: boolean;
|
|
11
|
+
/** Render the cover image hero. Default true. */
|
|
12
|
+
showCover?: boolean;
|
|
13
|
+
/** Render the post title as an <h1>. Default true. */
|
|
14
|
+
showTitle?: boolean;
|
|
15
|
+
/** Inject default styling. Set false to bring your own CSS. Default true. */
|
|
16
|
+
styled?: boolean;
|
|
17
|
+
}
|
|
18
|
+
/** A full, polished article: cover hero + title + HTML body (inline images included). */
|
|
19
|
+
declare function LvlzArticle({ post, className, includeJsonLd, showCover, showTitle, styled, }: LvlzArticleProps): React.JSX.Element;
|
|
20
|
+
interface LvlzPostListProps {
|
|
21
|
+
posts: PublicPost[];
|
|
22
|
+
className?: string;
|
|
23
|
+
/** Build the href for a post. Default: `/blog/{slug}`. */
|
|
24
|
+
hrefFor?: (post: PublicPost) => string;
|
|
25
|
+
/** Custom item renderer; overrides the default card. */
|
|
26
|
+
renderItem?: (post: PublicPost) => React.ReactNode;
|
|
27
|
+
/** Inject default styling. Default true. */
|
|
28
|
+
styled?: boolean;
|
|
29
|
+
}
|
|
30
|
+
/** A responsive grid of post cards with cover thumbnails. */
|
|
31
|
+
declare function LvlzPostList({ posts, className, hrefFor, renderItem, styled, }: LvlzPostListProps): React.JSX.Element;
|
|
32
|
+
|
|
33
|
+
export { LvlzArticle, type LvlzArticleProps, LvlzPostList, type LvlzPostListProps, LvlzStyles };
|
package/dist/react.js
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { jsx, jsxs } from 'react/jsx-runtime';
|
|
2
|
+
|
|
3
|
+
// src/react.tsx
|
|
4
|
+
var STYLE_TAG_ID = "lvlz-sdk-styles";
|
|
5
|
+
var CSS = `
|
|
6
|
+
.lvlz-article{max-width:768px;margin:0 auto;color:inherit}
|
|
7
|
+
.lvlz-cover{width:100%;aspect-ratio:16/9;border-radius:16px;overflow:hidden;margin:0 0 2rem;background:#f3f3f5}
|
|
8
|
+
.lvlz-cover img{width:100%;height:100%;object-fit:cover;display:block}
|
|
9
|
+
.lvlz-title{font-size:2.25rem;font-weight:780;line-height:1.15;margin:0 0 1.25rem;letter-spacing:-.02em}
|
|
10
|
+
.lvlz-prose{line-height:1.75;font-size:1.05rem}
|
|
11
|
+
.lvlz-prose h2{font-size:1.6rem;font-weight:720;margin:2.2rem 0 .8rem;line-height:1.25;letter-spacing:-.01em}
|
|
12
|
+
.lvlz-prose h3{font-size:1.25rem;font-weight:680;margin:1.6rem 0 .6rem}
|
|
13
|
+
.lvlz-prose p{margin:0 0 1.1rem}
|
|
14
|
+
.lvlz-prose a{color:#7c3aed;text-decoration:underline;text-underline-offset:2px}
|
|
15
|
+
.lvlz-prose ul,.lvlz-prose ol{margin:0 0 1.1rem 1.25rem}
|
|
16
|
+
.lvlz-prose li{margin:.35rem 0}
|
|
17
|
+
.lvlz-prose blockquote{border-left:3px solid #7c3aed;padding-left:1rem;margin:1.4rem 0;color:#555;font-style:italic}
|
|
18
|
+
.lvlz-prose img{width:100%;height:auto;border-radius:12px;margin:1.6rem 0;display:block}
|
|
19
|
+
.lvlz-list{display:grid;gap:1.5rem;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));list-style:none;padding:0;margin:0}
|
|
20
|
+
.lvlz-card{overflow:hidden;border-radius:14px;border:1px solid rgba(0,0,0,.08);background:#fff;transition:transform .15s ease,box-shadow .15s ease}
|
|
21
|
+
.lvlz-card:hover{transform:translateY(-2px);box-shadow:0 10px 30px rgba(0,0,0,.08)}
|
|
22
|
+
.lvlz-card a{text-decoration:none;color:inherit;display:flex;flex-direction:column;height:100%}
|
|
23
|
+
.lvlz-card-thumb{aspect-ratio:16/9;overflow:hidden;background:#f3f3f5}
|
|
24
|
+
.lvlz-card-thumb img{width:100%;height:100%;object-fit:cover;display:block;transition:transform .3s ease}
|
|
25
|
+
.lvlz-card:hover .lvlz-card-thumb img{transform:scale(1.04)}
|
|
26
|
+
.lvlz-card-body{padding:1rem 1.1rem 1.2rem}
|
|
27
|
+
.lvlz-card-title{font-size:1.05rem;font-weight:680;margin:0 0 .4rem;line-height:1.3}
|
|
28
|
+
.lvlz-card-excerpt{font-size:.9rem;color:#666;margin:0;line-height:1.5}
|
|
29
|
+
@media (prefers-color-scheme:dark){
|
|
30
|
+
.lvlz-card{background:#15151a;border-color:rgba(255,255,255,.1)}
|
|
31
|
+
.lvlz-card-excerpt{color:#a1a1aa}
|
|
32
|
+
.lvlz-prose blockquote{color:#a1a1aa}
|
|
33
|
+
}
|
|
34
|
+
`;
|
|
35
|
+
function LvlzStyles() {
|
|
36
|
+
return /* @__PURE__ */ jsx("style", { id: STYLE_TAG_ID, dangerouslySetInnerHTML: { __html: CSS } });
|
|
37
|
+
}
|
|
38
|
+
function LvlzArticle({
|
|
39
|
+
post,
|
|
40
|
+
className,
|
|
41
|
+
includeJsonLd = true,
|
|
42
|
+
showCover = true,
|
|
43
|
+
showTitle = true,
|
|
44
|
+
styled = true
|
|
45
|
+
}) {
|
|
46
|
+
return /* @__PURE__ */ jsxs("article", { className: ["lvlz-article", className].filter(Boolean).join(" "), children: [
|
|
47
|
+
styled ? /* @__PURE__ */ jsx(LvlzStyles, {}) : null,
|
|
48
|
+
includeJsonLd && post.jsonLd ? /* @__PURE__ */ jsx("script", { type: "application/ld+json", dangerouslySetInnerHTML: { __html: post.jsonLd } }) : null,
|
|
49
|
+
showCover && post.coverImage ? /* @__PURE__ */ jsx("figure", { className: "lvlz-cover", children: /* @__PURE__ */ jsx("img", { src: post.coverImage, alt: post.title, loading: "eager" }) }) : null,
|
|
50
|
+
showTitle ? /* @__PURE__ */ jsx("h1", { className: "lvlz-title", children: post.title }) : null,
|
|
51
|
+
/* @__PURE__ */ jsx("div", { className: "lvlz-prose", dangerouslySetInnerHTML: { __html: post.html } })
|
|
52
|
+
] });
|
|
53
|
+
}
|
|
54
|
+
function LvlzPostList({
|
|
55
|
+
posts,
|
|
56
|
+
className,
|
|
57
|
+
hrefFor,
|
|
58
|
+
renderItem,
|
|
59
|
+
styled = true
|
|
60
|
+
}) {
|
|
61
|
+
const href = hrefFor ?? ((p) => `/blog/${p.slug}`);
|
|
62
|
+
return /* @__PURE__ */ jsxs("ul", { className: ["lvlz-list", className].filter(Boolean).join(" "), children: [
|
|
63
|
+
styled ? /* @__PURE__ */ jsx(LvlzStyles, {}) : null,
|
|
64
|
+
posts.map((post) => /* @__PURE__ */ jsx("li", { className: "lvlz-card", children: renderItem ? renderItem(post) : /* @__PURE__ */ jsxs("a", { href: href(post), children: [
|
|
65
|
+
post.coverImage ? /* @__PURE__ */ jsx("div", { className: "lvlz-card-thumb", children: /* @__PURE__ */ jsx("img", { src: post.coverImage, alt: post.title, loading: "lazy" }) }) : null,
|
|
66
|
+
/* @__PURE__ */ jsxs("div", { className: "lvlz-card-body", children: [
|
|
67
|
+
/* @__PURE__ */ jsx("h3", { className: "lvlz-card-title", children: post.title }),
|
|
68
|
+
post.excerpt ? /* @__PURE__ */ jsx("p", { className: "lvlz-card-excerpt", children: post.excerpt }) : null
|
|
69
|
+
] })
|
|
70
|
+
] }) }, post.id))
|
|
71
|
+
] });
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export { LvlzArticle, LvlzPostList, LvlzStyles };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lvlz/sdk",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Fetch and render your LVLZ.ai published posts on any site (Next.js, React, Vite, plain HTML).",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -23,7 +23,10 @@
|
|
|
23
23
|
"sideEffects": false,
|
|
24
24
|
"scripts": {
|
|
25
25
|
"build": "tsup",
|
|
26
|
-
"dev": "tsup --watch"
|
|
26
|
+
"dev": "tsup --watch",
|
|
27
|
+
"prepublishOnly": "npm run build",
|
|
28
|
+
"prepack": "npm run build",
|
|
29
|
+
"verify-pack": "npm pack --dry-run 2>&1 | grep -q 'dist/index.js' || (echo 'ERROR: dist/index.js missing from package — run npm run build' && exit 1)"
|
|
27
30
|
},
|
|
28
31
|
"keywords": ["seo", "geo", "blog", "headless", "cms", "lvlz"],
|
|
29
32
|
"peerDependencies": {
|