@intelex-ia/sdk 0.1.0 → 0.3.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 +310 -207
- package/dist/autoria.test.d.ts +2 -0
- package/dist/autoria.test.d.ts.map +1 -0
- package/dist/autoria.test.js +85 -0
- package/dist/autoria.test.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/merge.d.ts +86 -0
- package/dist/merge.d.ts.map +1 -0
- package/dist/merge.js +84 -0
- package/dist/merge.js.map +1 -0
- package/dist/merge.test.d.ts +2 -0
- package/dist/merge.test.d.ts.map +1 -0
- package/dist/merge.test.js +224 -0
- package/dist/merge.test.js.map +1 -0
- package/dist/react.d.ts +58 -0
- package/dist/react.d.ts.map +1 -1
- package/dist/react.js +97 -1
- package/dist/react.js.map +1 -1
- package/package.json +64 -64
package/README.md
CHANGED
|
@@ -1,207 +1,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,
|
|
6
|
-
|
|
7
|
-
## Instalação
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
```
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
##
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
<
|
|
111
|
-
|
|
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
|
-
|
|
157
|
-
|
|
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
|
-
|
|
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
|
+
<div dangerouslySetInnerHTML={{ __html: article.contentHtml ?? "" }} />
|
|
113
|
+
</article>
|
|
114
|
+
);
|
|
115
|
+
} catch (err) {
|
|
116
|
+
if (!(err instanceof IntelexiaApiError) || err.status !== 404) throw err;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// 2) Fallback: post antigo local
|
|
120
|
+
const local = getPostBySlug(slug);
|
|
121
|
+
if (!local) notFound();
|
|
122
|
+
return (
|
|
123
|
+
<article>
|
|
124
|
+
<h1>{local.title}</h1>
|
|
125
|
+
{/* renderiza o local do jeito que você já fazia */}
|
|
126
|
+
</article>
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### Modo 2: Replace (migração completa)
|
|
132
|
+
|
|
133
|
+
```tsx
|
|
134
|
+
// app/blog/page.tsx
|
|
135
|
+
import { intelexia } from "@/lib/intelexia";
|
|
136
|
+
|
|
137
|
+
export default async function BlogIndex() {
|
|
138
|
+
const { items } = await intelexia.getArticles({ limit: 20 });
|
|
139
|
+
return (
|
|
140
|
+
<ul>
|
|
141
|
+
{items.map((a) => (
|
|
142
|
+
<li key={a.id}>
|
|
143
|
+
<Link href={`/blog/${a.slug}`}>{a.title}</Link>
|
|
144
|
+
</li>
|
|
145
|
+
))}
|
|
146
|
+
</ul>
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
Pro post individual, sem fallback:
|
|
152
|
+
|
|
153
|
+
```tsx
|
|
154
|
+
import { intelexia } from "@/lib/intelexia";
|
|
155
|
+
import { IntelexiaApiError } from "@intelex-ia/sdk";
|
|
156
|
+
import { ArticleJsonLd, articleToMetadata } from "@intelex-ia/sdk/react";
|
|
157
|
+
import { notFound } from "next/navigation";
|
|
158
|
+
|
|
159
|
+
async function fetchArticle(slug: string) {
|
|
160
|
+
try {
|
|
161
|
+
return await intelexia.getArticleBySlug(slug);
|
|
162
|
+
} catch (err) {
|
|
163
|
+
if (err instanceof IntelexiaApiError && err.status === 404) notFound();
|
|
164
|
+
throw err;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export async function generateMetadata({ params }: { params: Promise<{ slug: string }> }) {
|
|
169
|
+
const { slug } = await params;
|
|
170
|
+
const { article } = await fetchArticle(slug);
|
|
171
|
+
return articleToMetadata(article);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export default async function PostPage({ params }: { params: Promise<{ slug: string }> }) {
|
|
175
|
+
const { slug } = await params;
|
|
176
|
+
const { article } = await fetchArticle(slug);
|
|
177
|
+
return (
|
|
178
|
+
<article>
|
|
179
|
+
<ArticleJsonLd article={article} />
|
|
180
|
+
<h1>{article.title}</h1>
|
|
181
|
+
<div dangerouslySetInnerHTML={{ __html: article.contentHtml ?? "" }} />
|
|
182
|
+
</article>
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
### Modo 3: Isolated (rota nova, blog antigo intacto)
|
|
188
|
+
|
|
189
|
+
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.
|
|
190
|
+
|
|
191
|
+
---
|
|
192
|
+
|
|
193
|
+
## Cliente — `lib/intelexia.ts`
|
|
194
|
+
|
|
195
|
+
```ts
|
|
196
|
+
import { createClient } from "@intelex-ia/sdk";
|
|
197
|
+
|
|
198
|
+
export const intelexia = createClient({
|
|
199
|
+
token: process.env.INTELEXIA_API_TOKEN!,
|
|
200
|
+
// ISR: revalida a cada 60s; o webhook revalidate invalida sob demanda
|
|
201
|
+
init: { next: { revalidate: 60, tags: ["intelexia-articles"] } },
|
|
202
|
+
});
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
## Webhook de revalidate — `app/api/revalidate/route.ts`
|
|
206
|
+
|
|
207
|
+
Quando um artigo publica/edita no painel, nosso worker faz `POST` no seu endpoint. Implemente assim:
|
|
208
|
+
|
|
209
|
+
```ts
|
|
210
|
+
import { NextResponse } from "next/server";
|
|
211
|
+
import { revalidatePath, revalidateTag } from "next/cache";
|
|
212
|
+
|
|
213
|
+
export async function POST(req: Request) {
|
|
214
|
+
const headerSecret = req.headers.get("x-revalidate-secret");
|
|
215
|
+
const body = (await req.json().catch(() => ({}))) as {
|
|
216
|
+
secret?: string;
|
|
217
|
+
slug?: string | null;
|
|
218
|
+
domain?: string;
|
|
219
|
+
};
|
|
220
|
+
const got = headerSecret ?? body.secret ?? "";
|
|
221
|
+
if (!process.env.INTELEXIA_REVALIDATE_SECRET || got !== process.env.INTELEXIA_REVALIDATE_SECRET) {
|
|
222
|
+
return NextResponse.json({ ok: false, error: "unauthorized" }, { status: 401 });
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (body.slug) revalidatePath(`/blog/${body.slug}`);
|
|
226
|
+
revalidatePath("/blog");
|
|
227
|
+
revalidateTag("intelexia-articles");
|
|
228
|
+
|
|
229
|
+
return NextResponse.json({ ok: true, revalidated: { slug: body.slug ?? null } });
|
|
230
|
+
}
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
> 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`).
|
|
234
|
+
|
|
235
|
+
## API
|
|
236
|
+
|
|
237
|
+
### `createClient(config)`
|
|
238
|
+
|
|
239
|
+
| Opção | Tipo | Default |
|
|
240
|
+
| -------- | -------------------- | ------------------------------------------------- |
|
|
241
|
+
| `token` | `string` (obrig.) | — |
|
|
242
|
+
| `apiUrl` | `string` | `https://api.innovarticles.intelexia.com.br` |
|
|
243
|
+
| `fetch` | `typeof fetch` | `globalThis.fetch` |
|
|
244
|
+
| `init` | `RequestInit` | `undefined` (passe `{ next: { revalidate: N } }`) |
|
|
245
|
+
|
|
246
|
+
### `client.getArticles({ limit?, offset?, init? })`
|
|
247
|
+
|
|
248
|
+
Resposta: `{ ok, mode, lifecycle, count, items: ArticleListItem[] }`.
|
|
249
|
+
|
|
250
|
+
### `client.getArticleBySlug(slug, { init? })`
|
|
251
|
+
|
|
252
|
+
Resposta: `{ ok, mode, lifecycle, article: Article }`. **404 → joga `IntelexiaApiError`** (use `try/catch` + `notFound()`).
|
|
253
|
+
|
|
254
|
+
### `mergePosts({ local, remote, mapLocal, dedupe?, sortBy? })` 🆕 0.2.0
|
|
255
|
+
|
|
256
|
+
| Opção | Tipo | Default |
|
|
257
|
+
| ---------- | ------------------------------------------------- | ---------------- |
|
|
258
|
+
| `local` | `readonly TLocal[]` (qualquer shape) | — |
|
|
259
|
+
| `remote` | `readonly ArticleListItem[]` | — |
|
|
260
|
+
| `mapLocal` | `(post: TLocal) => Omit<UnifiedPost,"source"> \| null` | — |
|
|
261
|
+
| `dedupe` | `"remote-wins" \| "local-wins" \| "throw"` | `"remote-wins"` |
|
|
262
|
+
| `sortBy` | `"date-desc" \| "date-asc" \| "none"` | `"date-desc"` |
|
|
263
|
+
|
|
264
|
+
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.
|
|
265
|
+
|
|
266
|
+
### `mode: "full" | "plain"`
|
|
267
|
+
|
|
268
|
+
- **`full`** (sites `active`/`paused`): inclui schema JSON-LD, metaTitle/Description, keyword(s), interlinking interno no `contentHtml`.
|
|
269
|
+
- **`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).
|
|
270
|
+
|
|
271
|
+
A decisão é **server-side e não-negociável** — o cliente não escolhe o modo.
|
|
272
|
+
|
|
273
|
+
### Helpers `/react`
|
|
274
|
+
|
|
275
|
+
- `<ArticleJsonLd article={article} />` — renderiza `<script type="application/ld+json">` com o `schemaJson`. No-op se ausente.
|
|
276
|
+
- `articleToMetadata(article)` → objeto compatível com o `Metadata` do Next.js App Router (title, description, keywords, openGraph, twitter, customMetaTags como `other`).
|
|
277
|
+
|
|
278
|
+
## Erros
|
|
279
|
+
|
|
280
|
+
```ts
|
|
281
|
+
import { IntelexiaApiError, MergeConflictError } from "@intelex-ia/sdk";
|
|
282
|
+
|
|
283
|
+
try {
|
|
284
|
+
await intelexia.getArticleBySlug("post-inexistente");
|
|
285
|
+
} catch (err) {
|
|
286
|
+
if (err instanceof IntelexiaApiError) {
|
|
287
|
+
console.error(err.status, err.message); // 404, "Artigo não encontrado."
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
try {
|
|
292
|
+
mergePosts({ local, remote, mapLocal, dedupe: "throw" });
|
|
293
|
+
} catch (err) {
|
|
294
|
+
if (err instanceof MergeConflictError) {
|
|
295
|
+
console.error(`Conflito de slug: ${err.slug}`);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
## Compatibilidade
|
|
301
|
+
|
|
302
|
+
- **Node** 18+ (fetch nativo), Edge Runtime, Browser.
|
|
303
|
+
- **React** 18+ (peer dep opcional — só pros helpers `/react`).
|
|
304
|
+
- **TypeScript:** tipos publicados no `dist`.
|
|
305
|
+
- **Frameworks:** Next.js (App Router e Pages Router), Astro, Remix, SvelteKit, Vite, qualquer JS.
|
|
306
|
+
|
|
307
|
+
## Changelog
|
|
308
|
+
|
|
309
|
+
- **0.2.0** — adiciona `mergePosts()` + tipo `UnifiedPost` + `MergeConflictError` (3 modos de integração).
|
|
310
|
+
- **0.1.0** — release inicial: `createClient`, `getArticles`, `getArticleBySlug`, `<ArticleJsonLd>`, `articleToMetadata`.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"autoria.test.d.ts","sourceRoot":"","sources":["../src/autoria.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { autoriaDoArtigo, SELO_IA } from "./react.js";
|
|
3
|
+
/**
|
|
4
|
+
* A autoria do artigo tem duas regras duras, e as duas vêm de fora da
|
|
5
|
+
* engenharia:
|
|
6
|
+
*
|
|
7
|
+
* 1. `author` só vale quando é `Organization`. Texto produzido com apoio de IA
|
|
8
|
+
* e assinado por uma PESSOA é o que os Termos do Google proíbem desde
|
|
9
|
+
* 30/07/2026. O profissional aparece como REVISOR.
|
|
10
|
+
*
|
|
11
|
+
* 2. A autoria NUNCA some. Sem cadastro, assina a equipe da empresa — e o selo
|
|
12
|
+
* de apoio de IA sai do mesmo jeito. Artigo no ar sem dizer quem assina foi
|
|
13
|
+
* exatamente o defeito medido no siteIA em 21/10/2026.
|
|
14
|
+
*/
|
|
15
|
+
describe("autoriaDoArtigo", () => {
|
|
16
|
+
const comOrganization = {
|
|
17
|
+
schemaJson: {
|
|
18
|
+
"@graph": [
|
|
19
|
+
{
|
|
20
|
+
"@type": "BlogPosting",
|
|
21
|
+
author: { "@type": "Organization", name: "Equipe Editorial Parceria" },
|
|
22
|
+
contributor: {
|
|
23
|
+
"@type": "Person",
|
|
24
|
+
name: "Mauro Roger Pinto",
|
|
25
|
+
jobTitle: "Contador responsável técnico",
|
|
26
|
+
identifier: "CRC-SC 020357/O-4",
|
|
27
|
+
},
|
|
28
|
+
dateModified: "2026-10-21",
|
|
29
|
+
publisher: { "@type": "Organization", name: "Parceria Assessoria Contábil" },
|
|
30
|
+
},
|
|
31
|
+
],
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
it("usa a Organization como autor e o contributor como revisor", () => {
|
|
35
|
+
const a = autoriaDoArtigo(comOrganization);
|
|
36
|
+
expect(a.autor).toBe("Equipe Editorial Parceria");
|
|
37
|
+
expect(a.revisor).toBe("Mauro Roger Pinto, Contador responsável técnico, CRC-SC 020357/O-4");
|
|
38
|
+
expect(a.revisadoEm).toBe("2026-10-21");
|
|
39
|
+
expect(a.temCadastro).toBe(true);
|
|
40
|
+
});
|
|
41
|
+
it("IGNORA author do tipo Person — e cai para a equipe da empresa", () => {
|
|
42
|
+
const a = autoriaDoArtigo({
|
|
43
|
+
schemaJson: {
|
|
44
|
+
"@type": "BlogPosting",
|
|
45
|
+
author: { "@type": "Person", name: "Fulano de Tal" },
|
|
46
|
+
publisher: { "@type": "Organization", name: "Contabilidade Acme" },
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
expect(a.autor).not.toContain("Fulano");
|
|
50
|
+
expect(a.autor).toBe("Equipe Editorial Contabilidade Acme");
|
|
51
|
+
expect(a.temCadastro).toBe(false);
|
|
52
|
+
});
|
|
53
|
+
it("sem autoria no schema, assina a equipe tirada do publisher", () => {
|
|
54
|
+
const a = autoriaDoArtigo({
|
|
55
|
+
schemaJson: { "@type": "BlogPosting", publisher: { "@type": "Organization", name: "Acme" } },
|
|
56
|
+
});
|
|
57
|
+
expect(a.autor).toBe("Equipe Editorial Acme");
|
|
58
|
+
expect(a.revisor).toBeNull();
|
|
59
|
+
expect(a.temCadastro).toBe(false);
|
|
60
|
+
});
|
|
61
|
+
it("a prop `empresa` vence o publisher do schema", () => {
|
|
62
|
+
const a = autoriaDoArtigo({ schemaJson: { publisher: { "@type": "Organization", name: "Nome Velho" } } }, { empresa: "Nome Novo" });
|
|
63
|
+
expect(a.autor).toBe("Equipe Editorial Nome Novo");
|
|
64
|
+
});
|
|
65
|
+
it("sem schema nenhum ainda assina — nunca devolve vazio", () => {
|
|
66
|
+
for (const vazio of [null, undefined, "", 0, [], "texto solto"]) {
|
|
67
|
+
const a = autoriaDoArtigo({ schemaJson: vazio });
|
|
68
|
+
expect(a.autor.length).toBeGreaterThan(0);
|
|
69
|
+
expect(a.temCadastro).toBe(false);
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
it("revisor sem cargo nem registro não gera pontuação solta", () => {
|
|
73
|
+
const a = autoriaDoArtigo({
|
|
74
|
+
schemaJson: {
|
|
75
|
+
author: { "@type": "Organization", name: "Equipe" },
|
|
76
|
+
contributor: { "@type": "Person", name: "Ana Souza" },
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
expect(a.revisor).toBe("Ana Souza");
|
|
80
|
+
});
|
|
81
|
+
it("o selo é o texto único do grupo", () => {
|
|
82
|
+
expect(SELO_IA).toBe("Apoio de IA · Revisão humana");
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
//# sourceMappingURL=autoria.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"autoria.test.js","sourceRoot":"","sources":["../src/autoria.test.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
CHANGED
|
@@ -6,4 +6,5 @@
|
|
|
6
6
|
*/
|
|
7
7
|
export { createClient, IntelexiaApiError, type ClientConfig, type ArticlesListResponse, type ArticleResponse, type IntelexiaClient, } from "./client.js";
|
|
8
8
|
export type { Lifecycle, Mode, Article, ArticleListItem, CustomMetaTag, ErrorResponse, } from "./types.js";
|
|
9
|
+
export { mergePosts, MergeConflictError, type UnifiedPost, type MergePostsOptions, } from "./merge.js";
|
|
9
10
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EACL,YAAY,EACZ,iBAAiB,EACjB,KAAK,YAAY,EACjB,KAAK,oBAAoB,EACzB,KAAK,eAAe,EACpB,KAAK,eAAe,GACrB,MAAM,aAAa,CAAC;AAErB,YAAY,EACV,SAAS,EACT,IAAI,EACJ,OAAO,EACP,eAAe,EACf,aAAa,EACb,aAAa,GACd,MAAM,YAAY,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EACL,YAAY,EACZ,iBAAiB,EACjB,KAAK,YAAY,EACjB,KAAK,oBAAoB,EACzB,KAAK,eAAe,EACpB,KAAK,eAAe,GACrB,MAAM,aAAa,CAAC;AAErB,YAAY,EACV,SAAS,EACT,IAAI,EACJ,OAAO,EACP,eAAe,EACf,aAAa,EACb,aAAa,GACd,MAAM,YAAY,CAAC;AAIpB,OAAO,EACL,UAAU,EACV,kBAAkB,EAClB,KAAK,WAAW,EAChB,KAAK,iBAAiB,GACvB,MAAM,YAAY,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -5,4 +5,7 @@
|
|
|
5
5
|
* `@intelex-ia/sdk/react`.
|
|
6
6
|
*/
|
|
7
7
|
export { createClient, IntelexiaApiError, } from "./client.js";
|
|
8
|
+
// Merge helpers (0.2.0+) — pro caso real "cliente já tem posts antigos
|
|
9
|
+
// e quer adicionar os do painel sem perder os antigos".
|
|
10
|
+
export { mergePosts, MergeConflictError, } from "./merge.js";
|
|
8
11
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EACL,YAAY,EACZ,iBAAiB,GAKlB,MAAM,aAAa,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EACL,YAAY,EACZ,iBAAiB,GAKlB,MAAM,aAAa,CAAC;AAWrB,uEAAuE;AACvE,wDAAwD;AACxD,OAAO,EACL,UAAU,EACV,kBAAkB,GAGnB,MAAM,YAAY,CAAC"}
|