@intelex-ia/sdk 0.6.0 → 0.7.0-rc.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 +406 -310
- package/dist/client.d.ts +25 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +31 -2
- package/dist/client.js.map +1 -1
- package/dist/react.d.ts +243 -11
- package/dist/react.d.ts.map +1 -1
- package/dist/react.js +393 -13
- package/dist/react.js.map +1 -1
- package/dist/types.d.ts +53 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -1,310 +1,406 @@
|
|
|
1
|
-
# `@intelex-ia/sdk`
|
|
2
|
-
|
|
3
|
-
SDK pra consumir a **Content API IntelexIA** em sites headless (Next.js, Astro etc.). Multiplataforma Fase 2b.
|
|
4
|
-
|
|
5
|
-
> **O que você ganha:** `getArticles` / `getArticleBySlug` tipados, `mergePosts()` pra unir conteúdo antigo + novo do painel, `<ArticleJsonLd>` pra injetar schema JSON-LD, e `articleToMetadata` pra plugar no `generateMetadata` do Next.js App Router. Zero dependências (usa `fetch` global).
|
|
6
|
-
|
|
7
|
-
## Instalação rápida (recomendado)
|
|
8
|
-
|
|
9
|
-
Se você está começando agora, use o CLI — ele detecta a estrutura do seu projeto e configura tudo automaticamente:
|
|
10
|
-
|
|
11
|
-
```bash
|
|
12
|
-
npx @intelex-ia/cli init
|
|
13
|
-
```
|
|
14
|
-
|
|
15
|
-
Cria os arquivos no lugar certo (App Router ou Pages Router, com ou sem `src/`), configura `.env.local`, gera um `INTELEXIA.md` completo na raiz e instala o SDK pra você.
|
|
16
|
-
|
|
17
|
-
## Instalação manual
|
|
18
|
-
|
|
19
|
-
```bash
|
|
20
|
-
npm install @intelex-ia/sdk
|
|
21
|
-
# ou pnpm add @intelex-ia/sdk / yarn add @intelex-ia/sdk
|
|
22
|
-
```
|
|
23
|
-
|
|
24
|
-
## Configuração
|
|
25
|
-
|
|
26
|
-
1. No painel da IntelexIA, **cadastre o site como "Headless (código)"** (vai pedir domain + URL e secret de revalidate).
|
|
27
|
-
2. Abra o site na lista → seção **"Tokens de API (Content API)"** → **Gerar token**. Você recebe `itx_…` (mostrado uma única vez).
|
|
28
|
-
3. Coloque em `.env.local`:
|
|
29
|
-
|
|
30
|
-
```bash
|
|
31
|
-
INTELEXIA_API_TOKEN=itx_seu_token_aqui
|
|
32
|
-
INTELEXIA_API_URL=https://api.innovarticles.intelexia.com.br # opcional, é o default
|
|
33
|
-
INTELEXIA_REVALIDATE_SECRET=cole-o-mesmo-segredo-do-painel
|
|
34
|
-
```
|
|
35
|
-
|
|
36
|
-
## Três modos de integração
|
|
37
|
-
|
|
38
|
-
Cada cliente tem uma situação diferente. Escolha o modo que faz sentido:
|
|
39
|
-
|
|
40
|
-
| Modo | Quando usar | O que muda no `/blog` |
|
|
41
|
-
|---|---|---|
|
|
42
|
-
| **🔀 Additive (recomendado)** | Cliente já tem posts antigos e quer **adicionar** os do painel sem perder os antigos | `mergePosts()` une os dois feeds, dedupa por slug, ordena por data |
|
|
43
|
-
| **🔁 Replace** | Cliente migrou tudo pro painel — só os novos contam | Substitui `getAllPosts()` por `client.getArticles()` |
|
|
44
|
-
| **🆕 Isolated** | Cliente quer manter `/blog` antigo intacto e criar rota nova só pra IA | Cria `/blog/ia` separado, blog antigo zero alterado |
|
|
45
|
-
|
|
46
|
-
---
|
|
47
|
-
|
|
48
|
-
### Modo 1: Additive (cliente existente — caso mais comum)
|
|
49
|
-
|
|
50
|
-
```tsx
|
|
51
|
-
// app/blog/page.tsx
|
|
52
|
-
import { intelexia } from "@/lib/intelexia";
|
|
53
|
-
import { mergePosts } from "@intelex-ia/sdk";
|
|
54
|
-
import { posts as localPosts } from "@/lib/posts"; // seu array atual
|
|
55
|
-
import Link from "next/link";
|
|
56
|
-
|
|
57
|
-
export default async function BlogIndex() {
|
|
58
|
-
// 1) Busca os artigos do painel
|
|
59
|
-
const { items: remote } = await intelexia.getArticles({ limit: 50 });
|
|
60
|
-
|
|
61
|
-
// 2) Mistura com os seus posts antigos
|
|
62
|
-
const all = mergePosts({
|
|
63
|
-
local: localPosts,
|
|
64
|
-
remote,
|
|
65
|
-
mapLocal: (p) => ({
|
|
66
|
-
slug: p.slug,
|
|
67
|
-
title: p.title,
|
|
68
|
-
excerpt: p.excerpt,
|
|
69
|
-
publishedAt: p.date,
|
|
70
|
-
featuredImageUrl: null,
|
|
71
|
-
category: p.category,
|
|
72
|
-
}),
|
|
73
|
-
// Opcional — defaults sensatos:
|
|
74
|
-
// dedupe: "remote-wins" (painel substitui local se slug colide)
|
|
75
|
-
// sortBy: "date-desc" (mais recente primeiro)
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
return (
|
|
79
|
-
<ul>
|
|
80
|
-
{all.map((p) => (
|
|
81
|
-
<li key={p.slug}>
|
|
82
|
-
<Link href={`/blog/${p.slug}`}>{p.title}</Link>
|
|
83
|
-
<time>{new Date(p.publishedAt).toLocaleDateString("pt-BR")}</time>
|
|
84
|
-
{p.source === "remote" && <span className="badge-ia">✨ IA</span>}
|
|
85
|
-
</li>
|
|
86
|
-
))}
|
|
87
|
-
</ul>
|
|
88
|
-
);
|
|
89
|
-
}
|
|
90
|
-
```
|
|
91
|
-
|
|
92
|
-
Pro post individual, tenta o painel primeiro, cai pro local se 404:
|
|
93
|
-
|
|
94
|
-
```tsx
|
|
95
|
-
// app/blog/[slug]/page.tsx
|
|
96
|
-
import { intelexia } from "@/lib/intelexia";
|
|
97
|
-
import { IntelexiaApiError } from "@intelex-ia/sdk";
|
|
98
|
-
import { ArticleJsonLd, articleToMetadata } from "@intelex-ia/sdk/react";
|
|
99
|
-
import { getPostBySlug } from "@/lib/posts";
|
|
100
|
-
import { notFound } from "next/navigation";
|
|
101
|
-
|
|
102
|
-
export default async function PostPage({ params }: { params: Promise<{ slug: string }> }) {
|
|
103
|
-
const { slug } = await params;
|
|
104
|
-
|
|
105
|
-
// 1) Tenta painel
|
|
106
|
-
try {
|
|
107
|
-
const { article } = await intelexia.getArticleBySlug(slug);
|
|
108
|
-
return (
|
|
109
|
-
<article>
|
|
110
|
-
<ArticleJsonLd article={article} />
|
|
111
|
-
<h1>{article.title}</h1>
|
|
112
|
-
<
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
import {
|
|
157
|
-
import {
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
<
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
if (
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
|
244
|
-
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
### `client.
|
|
251
|
-
|
|
252
|
-
Resposta: `{ ok, mode, lifecycle,
|
|
253
|
-
|
|
254
|
-
### `
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
|
261
|
-
|
|
|
262
|
-
| `
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
1
|
+
# `@intelex-ia/sdk`
|
|
2
|
+
|
|
3
|
+
SDK pra consumir a **Content API IntelexIA** em sites headless (Next.js, Astro etc.). Multiplataforma Fase 2b.
|
|
4
|
+
|
|
5
|
+
> **O que você ganha:** `getArticles` / `getArticleBySlug` tipados, `mergePosts()` pra unir conteúdo antigo + novo do painel, `<ArticleJsonLd>` pra injetar schema JSON-LD, e `articleToMetadata` pra plugar no `generateMetadata` do Next.js App Router. Zero dependências (usa `fetch` global).
|
|
6
|
+
|
|
7
|
+
## Instalação rápida (recomendado)
|
|
8
|
+
|
|
9
|
+
Se você está começando agora, use o CLI — ele detecta a estrutura do seu projeto e configura tudo automaticamente:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npx @intelex-ia/cli init
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Cria os arquivos no lugar certo (App Router ou Pages Router, com ou sem `src/`), configura `.env.local`, gera um `INTELEXIA.md` completo na raiz e instala o SDK pra você.
|
|
16
|
+
|
|
17
|
+
## Instalação manual
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm install @intelex-ia/sdk
|
|
21
|
+
# ou pnpm add @intelex-ia/sdk / yarn add @intelex-ia/sdk
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Configuração
|
|
25
|
+
|
|
26
|
+
1. No painel da IntelexIA, **cadastre o site como "Headless (código)"** (vai pedir domain + URL e secret de revalidate).
|
|
27
|
+
2. Abra o site na lista → seção **"Tokens de API (Content API)"** → **Gerar token**. Você recebe `itx_…` (mostrado uma única vez).
|
|
28
|
+
3. Coloque em `.env.local`:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
INTELEXIA_API_TOKEN=itx_seu_token_aqui
|
|
32
|
+
INTELEXIA_API_URL=https://api.innovarticles.intelexia.com.br # opcional, é o default
|
|
33
|
+
INTELEXIA_REVALIDATE_SECRET=cole-o-mesmo-segredo-do-painel
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Três modos de integração
|
|
37
|
+
|
|
38
|
+
Cada cliente tem uma situação diferente. Escolha o modo que faz sentido:
|
|
39
|
+
|
|
40
|
+
| Modo | Quando usar | O que muda no `/blog` |
|
|
41
|
+
|---|---|---|
|
|
42
|
+
| **🔀 Additive (recomendado)** | Cliente já tem posts antigos e quer **adicionar** os do painel sem perder os antigos | `mergePosts()` une os dois feeds, dedupa por slug, ordena por data |
|
|
43
|
+
| **🔁 Replace** | Cliente migrou tudo pro painel — só os novos contam | Substitui `getAllPosts()` por `client.getArticles()` |
|
|
44
|
+
| **🆕 Isolated** | Cliente quer manter `/blog` antigo intacto e criar rota nova só pra IA | Cria `/blog/ia` separado, blog antigo zero alterado |
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
### Modo 1: Additive (cliente existente — caso mais comum)
|
|
49
|
+
|
|
50
|
+
```tsx
|
|
51
|
+
// app/blog/page.tsx
|
|
52
|
+
import { intelexia } from "@/lib/intelexia";
|
|
53
|
+
import { mergePosts } from "@intelex-ia/sdk";
|
|
54
|
+
import { posts as localPosts } from "@/lib/posts"; // seu array atual
|
|
55
|
+
import Link from "next/link";
|
|
56
|
+
|
|
57
|
+
export default async function BlogIndex() {
|
|
58
|
+
// 1) Busca os artigos do painel
|
|
59
|
+
const { items: remote } = await intelexia.getArticles({ limit: 50 });
|
|
60
|
+
|
|
61
|
+
// 2) Mistura com os seus posts antigos
|
|
62
|
+
const all = mergePosts({
|
|
63
|
+
local: localPosts,
|
|
64
|
+
remote,
|
|
65
|
+
mapLocal: (p) => ({
|
|
66
|
+
slug: p.slug,
|
|
67
|
+
title: p.title,
|
|
68
|
+
excerpt: p.excerpt,
|
|
69
|
+
publishedAt: p.date,
|
|
70
|
+
featuredImageUrl: null,
|
|
71
|
+
category: p.category,
|
|
72
|
+
}),
|
|
73
|
+
// Opcional — defaults sensatos:
|
|
74
|
+
// dedupe: "remote-wins" (painel substitui local se slug colide)
|
|
75
|
+
// sortBy: "date-desc" (mais recente primeiro)
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
return (
|
|
79
|
+
<ul>
|
|
80
|
+
{all.map((p) => (
|
|
81
|
+
<li key={p.slug}>
|
|
82
|
+
<Link href={`/blog/${p.slug}`}>{p.title}</Link>
|
|
83
|
+
<time>{new Date(p.publishedAt).toLocaleDateString("pt-BR")}</time>
|
|
84
|
+
{p.source === "remote" && <span className="badge-ia">✨ IA</span>}
|
|
85
|
+
</li>
|
|
86
|
+
))}
|
|
87
|
+
</ul>
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Pro post individual, tenta o painel primeiro, cai pro local se 404:
|
|
93
|
+
|
|
94
|
+
```tsx
|
|
95
|
+
// app/blog/[slug]/page.tsx
|
|
96
|
+
import { intelexia } from "@/lib/intelexia";
|
|
97
|
+
import { IntelexiaApiError } from "@intelex-ia/sdk";
|
|
98
|
+
import { ArticleJsonLd, ArticleBody, AuthorBar, AuthorSignature, articleToMetadata } from "@intelex-ia/sdk/react";
|
|
99
|
+
import { getPostBySlug } from "@/lib/posts";
|
|
100
|
+
import { notFound } from "next/navigation";
|
|
101
|
+
|
|
102
|
+
export default async function PostPage({ params }: { params: Promise<{ slug: string }> }) {
|
|
103
|
+
const { slug } = await params;
|
|
104
|
+
|
|
105
|
+
// 1) Tenta painel
|
|
106
|
+
try {
|
|
107
|
+
const { article } = await intelexia.getArticleBySlug(slug);
|
|
108
|
+
return (
|
|
109
|
+
<article>
|
|
110
|
+
<ArticleJsonLd article={article} />
|
|
111
|
+
<h1>{article.title}</h1>
|
|
112
|
+
<AuthorBar article={article} empresa="Acme" />
|
|
113
|
+
<ArticleBody article={article} />
|
|
114
|
+
<AuthorSignature article={article} empresa="Acme" />
|
|
115
|
+
</article>
|
|
116
|
+
);
|
|
117
|
+
} catch (err) {
|
|
118
|
+
if (!(err instanceof IntelexiaApiError) || err.status !== 404) throw err;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// 2) Fallback: post antigo local
|
|
122
|
+
const local = getPostBySlug(slug);
|
|
123
|
+
if (!local) notFound();
|
|
124
|
+
return (
|
|
125
|
+
<article>
|
|
126
|
+
<h1>{local.title}</h1>
|
|
127
|
+
{/* renderiza o local do jeito que você já fazia */}
|
|
128
|
+
</article>
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### Modo 2: Replace (migração completa)
|
|
134
|
+
|
|
135
|
+
```tsx
|
|
136
|
+
// app/blog/page.tsx
|
|
137
|
+
import { intelexia } from "@/lib/intelexia";
|
|
138
|
+
|
|
139
|
+
export default async function BlogIndex() {
|
|
140
|
+
const { items } = await intelexia.getArticles({ limit: 20 });
|
|
141
|
+
return (
|
|
142
|
+
<ul>
|
|
143
|
+
{items.map((a) => (
|
|
144
|
+
<li key={a.id}>
|
|
145
|
+
<Link href={`/blog/${a.slug}`}>{a.title}</Link>
|
|
146
|
+
</li>
|
|
147
|
+
))}
|
|
148
|
+
</ul>
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Pro post individual, sem fallback:
|
|
154
|
+
|
|
155
|
+
```tsx
|
|
156
|
+
import { intelexia } from "@/lib/intelexia";
|
|
157
|
+
import { IntelexiaApiError } from "@intelex-ia/sdk";
|
|
158
|
+
import { ArticleJsonLd, ArticleBody, AuthorBar, AuthorSignature, articleToMetadata } from "@intelex-ia/sdk/react";
|
|
159
|
+
import { notFound } from "next/navigation";
|
|
160
|
+
|
|
161
|
+
async function fetchArticle(slug: string) {
|
|
162
|
+
try {
|
|
163
|
+
return await intelexia.getArticleBySlug(slug);
|
|
164
|
+
} catch (err) {
|
|
165
|
+
if (err instanceof IntelexiaApiError && err.status === 404) notFound();
|
|
166
|
+
throw err;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export async function generateMetadata({ params }: { params: Promise<{ slug: string }> }) {
|
|
171
|
+
const { slug } = await params;
|
|
172
|
+
const { article } = await fetchArticle(slug);
|
|
173
|
+
return articleToMetadata(article);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export default async function PostPage({ params }: { params: Promise<{ slug: string }> }) {
|
|
177
|
+
const { slug } = await params;
|
|
178
|
+
const { article } = await fetchArticle(slug);
|
|
179
|
+
return (
|
|
180
|
+
<article>
|
|
181
|
+
<ArticleJsonLd article={article} />
|
|
182
|
+
<h1>{article.title}</h1>
|
|
183
|
+
<AuthorBar article={article} empresa="Acme" />
|
|
184
|
+
<ArticleBody article={article} />
|
|
185
|
+
<AuthorSignature article={article} empresa="Acme" />
|
|
186
|
+
</article>
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
### Modo 3: Isolated (rota nova, blog antigo intacto)
|
|
192
|
+
|
|
193
|
+
Cria `app/blog/ia/page.tsx` e `app/blog/ia/[slug]/page.tsx` com o mesmo código do modo Replace, mas em rota separada (`/blog/ia` em vez de `/blog`). Zero alteração no blog existente.
|
|
194
|
+
|
|
195
|
+
---
|
|
196
|
+
|
|
197
|
+
## Cliente — `lib/intelexia.ts`
|
|
198
|
+
|
|
199
|
+
```ts
|
|
200
|
+
import { createClient } from "@intelex-ia/sdk";
|
|
201
|
+
|
|
202
|
+
export const intelexia = createClient({
|
|
203
|
+
token: process.env.INTELEXIA_API_TOKEN!,
|
|
204
|
+
// ISR: revalida a cada 60s; o webhook revalidate invalida sob demanda
|
|
205
|
+
init: { next: { revalidate: 60, tags: ["intelexia-articles"] } },
|
|
206
|
+
});
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
## Webhook de revalidate — `app/api/revalidate/route.ts`
|
|
210
|
+
|
|
211
|
+
Quando um artigo publica/edita no painel, nosso worker faz `POST` no seu endpoint. Implemente assim:
|
|
212
|
+
|
|
213
|
+
```ts
|
|
214
|
+
import { NextResponse } from "next/server";
|
|
215
|
+
import { revalidatePath, revalidateTag } from "next/cache";
|
|
216
|
+
|
|
217
|
+
export async function POST(req: Request) {
|
|
218
|
+
const headerSecret = req.headers.get("x-revalidate-secret");
|
|
219
|
+
const body = (await req.json().catch(() => ({}))) as {
|
|
220
|
+
secret?: string;
|
|
221
|
+
slug?: string | null;
|
|
222
|
+
domain?: string;
|
|
223
|
+
};
|
|
224
|
+
const got = headerSecret ?? body.secret ?? "";
|
|
225
|
+
if (!process.env.INTELEXIA_REVALIDATE_SECRET || got !== process.env.INTELEXIA_REVALIDATE_SECRET) {
|
|
226
|
+
return NextResponse.json({ ok: false, error: "unauthorized" }, { status: 401 });
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (body.slug) revalidatePath(`/blog/${body.slug}`);
|
|
230
|
+
revalidatePath("/blog");
|
|
231
|
+
revalidateTag("intelexia-articles");
|
|
232
|
+
|
|
233
|
+
return NextResponse.json({ ok: true, revalidated: { slug: body.slug ?? null } });
|
|
234
|
+
}
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
> O worker envia o secret tanto no header `x-revalidate-secret` quanto no body, tem timeout de 15s e é **fire-and-forget** (se seu endpoint falhar, o artigo continua publicado e a ISR atualiza pelo `revalidate: 60`).
|
|
238
|
+
|
|
239
|
+
## API
|
|
240
|
+
|
|
241
|
+
### `createClient(config)`
|
|
242
|
+
|
|
243
|
+
| Opção | Tipo | Default |
|
|
244
|
+
| -------- | -------------------- | ------------------------------------------------- |
|
|
245
|
+
| `token` | `string` (obrig.) | — |
|
|
246
|
+
| `apiUrl` | `string` | `https://api.innovarticles.intelexia.com.br` |
|
|
247
|
+
| `fetch` | `typeof fetch` | `globalThis.fetch` |
|
|
248
|
+
| `init` | `RequestInit` | `undefined` (passe `{ next: { revalidate: N } }`) |
|
|
249
|
+
|
|
250
|
+
### `client.getArticles({ limit?, offset?, init? })`
|
|
251
|
+
|
|
252
|
+
Resposta: `{ ok, mode, lifecycle, count, items: ArticleListItem[] }`.
|
|
253
|
+
|
|
254
|
+
### `client.getArticleBySlug(slug, { init? })`
|
|
255
|
+
|
|
256
|
+
Resposta: `{ ok, mode, lifecycle, article: Article }`. **404 → joga `IntelexiaApiError`** (use `try/catch` + `notFound()`).
|
|
257
|
+
|
|
258
|
+
### `mergePosts({ local, remote, mapLocal, dedupe?, sortBy? })` 🆕 0.2.0
|
|
259
|
+
|
|
260
|
+
| Opção | Tipo | Default |
|
|
261
|
+
| ---------- | ------------------------------------------------- | ---------------- |
|
|
262
|
+
| `local` | `readonly TLocal[]` (qualquer shape) | — |
|
|
263
|
+
| `remote` | `readonly ArticleListItem[]` | — |
|
|
264
|
+
| `mapLocal` | `(post: TLocal) => Omit<UnifiedPost,"source"> \| null` | — |
|
|
265
|
+
| `dedupe` | `"remote-wins" \| "local-wins" \| "throw"` | `"remote-wins"` |
|
|
266
|
+
| `sortBy` | `"date-desc" \| "date-asc" \| "none"` | `"date-desc"` |
|
|
267
|
+
|
|
268
|
+
Retorna `UnifiedPost[]` com `source: "local" | "remote"`. `mapLocal` retornando `null` filtra o post (útil pra esconder rascunhos). Slugs duplicados resolvidos pela estratégia `dedupe`. Veja exemplo no Modo Additive acima.
|
|
269
|
+
|
|
270
|
+
### `mode: "full" | "plain"`
|
|
271
|
+
|
|
272
|
+
- **`full`** (sites `active`/`paused`): inclui schema JSON-LD, metaTitle/Description, keyword(s), interlinking interno no `contentHtml`.
|
|
273
|
+
- **`plain`** (site `cancelled`): só conteúdo cru — schema/meta/keywords vêm `null`, interlinking removido. O `ArticleJsonLd` e o `articleToMetadata` lidam graciosamente (omitem o que não tem).
|
|
274
|
+
|
|
275
|
+
A decisão é **server-side e não-negociável** — o cliente não escolhe o modo.
|
|
276
|
+
|
|
277
|
+
### Helpers `/react`
|
|
278
|
+
|
|
279
|
+
- `<ArticleJsonLd article={article} />`: renderiza `<script type="application/ld+json">` com o `schemaJson`. No-op se ausente.
|
|
280
|
+
- `inteligenciaDoArtigo(article)`: a camada de SEO do artigo, venha **aninhada** em `article.intelligence` (o que a Content API manda em `full`) ou na raiz (payload antigo). É a regra única que o `ArticleJsonLd`, o `articleToMetadata` e o `autoriaDoArtigo` usam.
|
|
281
|
+
|
|
282
|
+
> 🚨 **Se você lê a inteligência à mão, leia pelo helper.** Até a 0.6.0 o
|
|
283
|
+
> `ArticleJsonLd` e o `articleToMetadata` liam `article.schemaJson` e
|
|
284
|
+
> `article.metaDescription` da RAIZ, onde a Content API não entrega nada no modo
|
|
285
|
+
> `full`. O efeito, medido no ar em 26/10/2026: página com **zero** blocos
|
|
286
|
+
> `application/ld+json` e **sem** meta description. Nada quebrava e nada
|
|
287
|
+
> aparecia no console. Desde a 0.7.0 os três helpers leem pelo
|
|
288
|
+
> `inteligenciaDoArtigo()`, com queda para a raiz.
|
|
289
|
+
- `articleToMetadata(article)`: objeto compatível com o `Metadata` do Next.js App Router (title, description, keywords, openGraph, twitter, customMetaTags como `other`).
|
|
290
|
+
- `<ArticleBody article={article} />`: o corpo do artigo, vestido. **Use este em vez de injetar o `contentHtml` numa div.**
|
|
291
|
+
- `<AuthorBar article={article} empresa="Acme" />`: a barra de autoria, logo abaixo do `<h1>`.
|
|
292
|
+
- `<AuthorSignature article={article} empresa="Acme" />`: a assinatura do fim do artigo, com o selo de apoio de IA.
|
|
293
|
+
|
|
294
|
+
#### Por que não injetar o `contentHtml` numa div
|
|
295
|
+
|
|
296
|
+
O corpo chega como HTML semântico **pelado**: `<h2>`, `<ul>`, `<table>`, sem
|
|
297
|
+
classe. No WordPress funciona, porque o tema veste por tag. Num site Tailwind
|
|
298
|
+
não: o `@tailwind base` zera `list-style`, zera tamanho e peso dos seis títulos
|
|
299
|
+
e deixa a tabela sem borda. Medido no preflight 3.4 real, sem folha por cima, o
|
|
300
|
+
`<h2>` sai a 16px peso 400 e a lista sem marcador. Isso não é "sem estilo", é
|
|
301
|
+
pior que sem estilo, e foi o que manteve o blog de um cliente como parede de
|
|
302
|
+
texto por semanas.
|
|
303
|
+
|
|
304
|
+
O `<ArticleBody>` faz três coisas que ninguém precisa repetir:
|
|
305
|
+
|
|
306
|
+
1. injeta a folha estrutural, escopada na classe `ia-artigo`, que ele mesmo põe
|
|
307
|
+
na div. O que você passar em `className` entra **ao lado** do gancho, nunca
|
|
308
|
+
no lugar dele;
|
|
309
|
+
2. devolve o que o preflight tira, com especificidade de 0,0,1 nas regras que
|
|
310
|
+
disputam com o reset e 0,0,0 no resto. Regra sua escrita por classe
|
|
311
|
+
(`.meu-blog h2`) ganha da nossa, sempre;
|
|
312
|
+
3. remove a assinatura que o motor embute no `contentHtml`, para ela não sair
|
|
313
|
+
duas vezes quando a página usa `<AuthorSignature>`. Por isso os dois andam
|
|
314
|
+
juntos: `<ArticleBody>` sozinho deixa a página sem autor;
|
|
315
|
+
4. embrulha cada `<table>` num `<div class="tabela-rolante">`. O navegador
|
|
316
|
+
ignora `overflow-x` em caixa de tabela, então sem o contêiner a tabela larga
|
|
317
|
+
faz a **página inteira** rolar de lado no celular: medido a 375px, 494
|
|
318
|
+
contra 375. O embrulho é idempotente e só sai com a folha, porque é ela que
|
|
319
|
+
traz a regra do contêiner.
|
|
320
|
+
|
|
321
|
+
`comFolha={false}` dispensa a folha. É obrigatório em **Tailwind v4 cujo corpo
|
|
322
|
+
já é vestido pelo `@tailwindcss/typography` instalado**, ou por utilitário no
|
|
323
|
+
wrapper: ali o site emite tudo dentro de `@layer`, camada é decidida antes de
|
|
324
|
+
especificidade, e a nossa folha, que fica fora de camada, ganharia de quem fez
|
|
325
|
+
certo. Em Tailwind v3 não existe esse caso.
|
|
326
|
+
|
|
327
|
+
A folha também pode ser importada solta, em `CSS_CORPO_DO_ARTIGO`, com a classe
|
|
328
|
+
em `CLASSE_DO_CORPO`.
|
|
329
|
+
|
|
330
|
+
#### A folha lê a marca do site, sem configuração
|
|
331
|
+
|
|
332
|
+
As cores e a fonte de título saem de variável, e a variável LÊ o token que o
|
|
333
|
+
site já tem, com queda para o que a folha fazia antes:
|
|
334
|
+
|
|
335
|
+
| Variável da folha | Token do site | Queda |
|
|
336
|
+
|---|---|---|
|
|
337
|
+
| `--ia-cor-titulo` | `--primary` | `inherit` |
|
|
338
|
+
| `--ia-cor-destaque` | `--accent` | `currentColor` |
|
|
339
|
+
| `--ia-cor-borda` | `--border` | `currentColor` |
|
|
340
|
+
| `--ia-fonte-titulo` | nenhum | `inherit` |
|
|
341
|
+
|
|
342
|
+
Site que declara o token como **trio de HSL** (`--primary: 222 47% 11%`, o
|
|
343
|
+
jeito do shadcn) cai na queda e fica igual a antes, nunca pior. Medido nos três
|
|
344
|
+
casos em 26/10/2026, com o preflight 3.4.19 real: sem token, o `h2` herda a cor
|
|
345
|
+
do corpo; com `#003366`, sai `#003366`; com o trio, herda de novo, e a borda da
|
|
346
|
+
tabela segue em 1px.
|
|
347
|
+
|
|
348
|
+
Para discordar, redeclare a variável na sua folha, sem tocar no pacote:
|
|
349
|
+
|
|
350
|
+
```css
|
|
351
|
+
.meu-blog .ia-artigo { --ia-cor-titulo: #003366; --ia-fonte-titulo: "Fraunces", serif; }
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
A FAQ em `<details>` sai em acordeão, com o marcador do navegador escondido,
|
|
355
|
+
triângulo próprio e giro no estado aberto.
|
|
356
|
+
|
|
357
|
+
#### Site preso numa versão antiga do SDK
|
|
358
|
+
|
|
359
|
+
Se o site ainda não pode subir para a 0.7.0, `folhaParaSite()` devolve a MESMA
|
|
360
|
+
folha reescrita para o CSS do próprio site, numa classe dele:
|
|
361
|
+
|
|
362
|
+
```ts
|
|
363
|
+
import { folhaParaSite } from "@intelex-ia/sdk/react";
|
|
364
|
+
console.log(folhaParaSite([".meu-artigo"])); // cole no globals.css
|
|
365
|
+
console.log(folhaParaSite([".a", ".b"])); // duas classes de uma vez
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
O seletor sai por classe, `0,1,1`, então ganha do reset de tipo em qualquer
|
|
369
|
+
versão do Tailwind, com ou sem camada. E quando o site migrar para o
|
|
370
|
+
`<ArticleBody>` nada conflita: `0,1,1` continua ganhando do `0,0,1` do SDK, ou
|
|
371
|
+
seja a folha do site continua mandando no que ela já decidia.
|
|
372
|
+
|
|
373
|
+
## Erros
|
|
374
|
+
|
|
375
|
+
```ts
|
|
376
|
+
import { IntelexiaApiError, MergeConflictError } from "@intelex-ia/sdk";
|
|
377
|
+
|
|
378
|
+
try {
|
|
379
|
+
await intelexia.getArticleBySlug("post-inexistente");
|
|
380
|
+
} catch (err) {
|
|
381
|
+
if (err instanceof IntelexiaApiError) {
|
|
382
|
+
console.error(err.status, err.message); // 404, "Artigo não encontrado."
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
try {
|
|
387
|
+
mergePosts({ local, remote, mapLocal, dedupe: "throw" });
|
|
388
|
+
} catch (err) {
|
|
389
|
+
if (err instanceof MergeConflictError) {
|
|
390
|
+
console.error(`Conflito de slug: ${err.slug}`);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
## Compatibilidade
|
|
396
|
+
|
|
397
|
+
- **Node** 18+ (fetch nativo), Edge Runtime, Browser.
|
|
398
|
+
- **React** 18+ (peer dep opcional — só pros helpers `/react`).
|
|
399
|
+
- **TypeScript:** tipos publicados no `dist`.
|
|
400
|
+
- **Frameworks:** Next.js (App Router e Pages Router), Astro, Remix, SvelteKit, Vite, qualquer JS.
|
|
401
|
+
|
|
402
|
+
## Changelog
|
|
403
|
+
|
|
404
|
+
- **0.7.0**: adiciona `<ArticleBody>`, `<AuthorBar>`, `<AuthorSignature>`, `CSS_CORPO_DO_ARTIGO`, `CLASSE_DO_CORPO`, `envolverTabelas` e `folhaParaSite`. O corpo do artigo passa a sair vestido, sem depender de o site lembrar de uma folha. A data da barra passa a sair no fuso de Brasília, e não no do servidor.
|
|
405
|
+
- **0.2.0** — adiciona `mergePosts()` + tipo `UnifiedPost` + `MergeConflictError` (3 modos de integração).
|
|
406
|
+
- **0.1.0** — release inicial: `createClient`, `getArticles`, `getArticleBySlug`, `<ArticleJsonLd>`, `articleToMetadata`.
|