@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 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
- Feed responses are edge-cached (`s-maxage` + `stale-while-revalidate`), so reads are fast
130
- and won't hammer your quota.
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;
@@ -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 };
@@ -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;
@@ -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 };
@@ -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.0",
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": {