@lvlz/sdk 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 +130 -0
- package/package.json +43 -0
package/README.md
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# @lvlz/sdk
|
|
2
|
+
|
|
3
|
+
Fetch and render your [LVLZ.ai](https://lvlz.ai) published posts on **any** site —
|
|
4
|
+
Next.js, React, Vite, or plain HTML. LVLZ generates SEO/GEO-optimized blog posts; this
|
|
5
|
+
SDK puts them on your domain so they boost _your_ search and AI-answer visibility.
|
|
6
|
+
|
|
7
|
+
> **For SEO/GEO, render on the server.** AI crawlers (GPTBot, ClaudeBot, PerplexityBot)
|
|
8
|
+
> usually don't run JavaScript, and Google's JS rendering is unreliable. The Next.js
|
|
9
|
+
> recipe below server-renders your posts — that's what actually moves the needle. The
|
|
10
|
+
> `<script>` embed is a convenience for sites without a build step.
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npm install @lvlz/sdk
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Get a **publishable key** (`lvlz_pub_…`) from LVLZ → Settings → Integrations → Website SDK.
|
|
19
|
+
It's read-only and brand-scoped, so it's safe to ship in client code.
|
|
20
|
+
|
|
21
|
+
## Core client
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
import { createClient } from '@lvlz/sdk';
|
|
25
|
+
|
|
26
|
+
const lvlz = createClient({ apiKey: 'lvlz_pub_…' });
|
|
27
|
+
|
|
28
|
+
const { posts } = await lvlz.getPosts({ limit: 20 });
|
|
29
|
+
const post = await lvlz.getPost('my-post-slug'); // null if not found
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Next.js (recommended — SSR + ISR + metadata)
|
|
33
|
+
|
|
34
|
+
`app/blog/page.tsx`
|
|
35
|
+
|
|
36
|
+
```tsx
|
|
37
|
+
import { createClient } from '@lvlz/sdk';
|
|
38
|
+
import { LvlzPostList } from '@lvlz/sdk/react';
|
|
39
|
+
|
|
40
|
+
const lvlz = createClient({ apiKey: process.env.LVLZ_KEY! });
|
|
41
|
+
export const revalidate = 600; // re-check for new posts every 10 minutes
|
|
42
|
+
|
|
43
|
+
export default async function Blog() {
|
|
44
|
+
const { posts } = await lvlz.getPosts();
|
|
45
|
+
return <LvlzPostList posts={posts} />;
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
`app/blog/[slug]/page.tsx`
|
|
50
|
+
|
|
51
|
+
```tsx
|
|
52
|
+
import { notFound } from 'next/navigation';
|
|
53
|
+
import type { Metadata } from 'next';
|
|
54
|
+
import { createClient } from '@lvlz/sdk';
|
|
55
|
+
import { LvlzArticle } from '@lvlz/sdk/react';
|
|
56
|
+
|
|
57
|
+
const lvlz = createClient({ apiKey: process.env.LVLZ_KEY! });
|
|
58
|
+
export const revalidate = 600;
|
|
59
|
+
|
|
60
|
+
export async function generateStaticParams() {
|
|
61
|
+
const { posts } = await lvlz.getPosts({ limit: 100 });
|
|
62
|
+
return posts.map((p) => ({ slug: p.slug }));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export async function generateMetadata(
|
|
66
|
+
{ params }: { params: Promise<{ slug: string }> },
|
|
67
|
+
): Promise<Metadata> {
|
|
68
|
+
const { slug } = await params;
|
|
69
|
+
const post = await lvlz.getPost(slug);
|
|
70
|
+
if (!post) return {};
|
|
71
|
+
return {
|
|
72
|
+
title: post.title,
|
|
73
|
+
description: post.description ?? post.excerpt ?? undefined,
|
|
74
|
+
keywords: post.keywords,
|
|
75
|
+
openGraph: { title: post.title, description: post.excerpt ?? undefined, type: 'article' },
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export default async function PostPage({ params }: { params: Promise<{ slug: string }> }) {
|
|
80
|
+
const { slug } = await params;
|
|
81
|
+
const post = await lvlz.getPost(slug);
|
|
82
|
+
if (!post) notFound();
|
|
83
|
+
return <LvlzArticle post={post} className="prose mx-auto" />;
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
`LvlzArticle` also injects a `BlogPosting` JSON-LD `<script>` for richer SEO/GEO.
|
|
88
|
+
|
|
89
|
+
## React / Vite (client-side)
|
|
90
|
+
|
|
91
|
+
```tsx
|
|
92
|
+
import { useEffect, useState } from 'react';
|
|
93
|
+
import { createClient, type PublicPost } from '@lvlz/sdk';
|
|
94
|
+
import { LvlzPostList } from '@lvlz/sdk/react';
|
|
95
|
+
|
|
96
|
+
const lvlz = createClient({ apiKey: import.meta.env.VITE_LVLZ_KEY });
|
|
97
|
+
|
|
98
|
+
export function Blog() {
|
|
99
|
+
const [posts, setPosts] = useState<PublicPost[]>([]);
|
|
100
|
+
useEffect(() => {
|
|
101
|
+
lvlz.getPosts().then((r) => setPosts(r.posts));
|
|
102
|
+
}, []);
|
|
103
|
+
return <LvlzPostList posts={posts} />;
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## Plain HTML (no build step)
|
|
108
|
+
|
|
109
|
+
```html
|
|
110
|
+
<div id="lvlz-blog"></div>
|
|
111
|
+
<script
|
|
112
|
+
src="https://app.lvlz.ai/sdk.js"
|
|
113
|
+
data-key="lvlz_pub_…"
|
|
114
|
+
data-target="#lvlz-blog"
|
|
115
|
+
></script>
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
The embed lists your posts and renders a single post when the URL has `?lvlz_slug=…`.
|
|
119
|
+
Add `data-slug="my-post-slug"` to render one specific post.
|
|
120
|
+
|
|
121
|
+
## API
|
|
122
|
+
|
|
123
|
+
| Method | Returns |
|
|
124
|
+
| --- | --- |
|
|
125
|
+
| `createClient({ apiKey, baseUrl?, fetchOptions? })` | `LvlzClient` |
|
|
126
|
+
| `client.getPosts({ limit?, offset? })` | `{ posts, total, limit, offset }` |
|
|
127
|
+
| `client.getPost(slug)` | `PublicPost \| null` |
|
|
128
|
+
|
|
129
|
+
Feed responses are edge-cached (`s-maxage` + `stale-while-revalidate`), so reads are fast
|
|
130
|
+
and won't hammer your quota.
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@lvlz/sdk",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Fetch and render your LVLZ.ai published posts on any site (Next.js, React, Vite, plain HTML).",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "./dist/index.cjs",
|
|
8
|
+
"module": "./dist/index.js",
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"import": "./dist/index.js",
|
|
14
|
+
"require": "./dist/index.cjs"
|
|
15
|
+
},
|
|
16
|
+
"./react": {
|
|
17
|
+
"types": "./dist/react.d.ts",
|
|
18
|
+
"import": "./dist/react.js",
|
|
19
|
+
"require": "./dist/react.cjs"
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"files": ["dist"],
|
|
23
|
+
"sideEffects": false,
|
|
24
|
+
"scripts": {
|
|
25
|
+
"build": "tsup",
|
|
26
|
+
"dev": "tsup --watch"
|
|
27
|
+
},
|
|
28
|
+
"keywords": ["seo", "geo", "blog", "headless", "cms", "lvlz"],
|
|
29
|
+
"peerDependencies": {
|
|
30
|
+
"react": ">=18"
|
|
31
|
+
},
|
|
32
|
+
"peerDependenciesMeta": {
|
|
33
|
+
"react": {
|
|
34
|
+
"optional": true
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"@types/react": "^19",
|
|
39
|
+
"react": "^19",
|
|
40
|
+
"tsup": "^8.0.0",
|
|
41
|
+
"typescript": "^5"
|
|
42
|
+
}
|
|
43
|
+
}
|