Neue Skills, Referenzen & OpenWiki-Doku integriert

Umfangreiche Erweiterung der Skill-Bibliothek: Neue Skills für Humanisierung (Englisch/PT-BR), Design-Validierung, AI-SEO und Coolify-Deployment inkl. Regelwerke, Presets, Pattern-Referenzen, Testfälle und Automatisierungsskripte. Zusätzliche Skills für Revenue-Centric Design, Pier Cloud, OKF, Lebenslauf- und LinkedIn-Optimierung sowie zahlreiche Referenzdateien, Checklisten und YAML/JSON/Markdown-Templates. Einführung einer vollständigen OpenWiki-Dokumentation mit Architektur-, Domain- und Workflow-Beschreibungen, zentralem Index und automatisierten Updates. Modularer Aufbau, restriktive Lizenzen und umfassende Qualitäts- und Evaluationsmechanismen für alle neuen Inhalte.
This commit is contained in:
Tim Krampitz
2026-07-26 14:00:58 +02:00
parent 070727d5cd
commit 01046b01e4
202 changed files with 31290 additions and 0 deletions

View File

@@ -0,0 +1,394 @@
# Related Content com Vector Embeddings
Conteúdo relacionado semântico para Astro content collections usando `@philnash/astro-related-content`. Gera sugestões de posts relacionados via vector embeddings locais (transformers.js) sem depender de APIs externas em runtime.
---
## Conceito
A integração calcula similaridade semântica entre posts usando embeddings (vetores numéricos que representam o "significado" do texto). Posts com vetores próximos são semanticamente similares. Tudo roda em build time — zero impacto no visitante.
---
## Instalação
```bash
npm install @philnash/astro-related-content
```
---
## Configuração Básica
```typescript
// astro.config.ts
import astroRelatedContent from '@philnash/astro-related-content'
export default defineConfig({
integrations: [
astroRelatedContent({
collections: ['blog'],
generation: {
limit: 4, // posts relacionados por item
watch: false, // não regenerar em dev mode (economiza CPU)
},
embeddings: {
model: 'onnx-community/embeddinggemma-300m-ONNX',
dtype: 'fp32',
pooling: 'mean',
batchSize: 1,
},
}),
],
})
```
---
## Escolha de Modelo
| Modelo | Idiomas | Context | Tamanho | Pooling | Uso |
|--------|---------|---------|---------|---------|-----|
| `Xenova/all-MiniLM-L6-v2` | EN only | 256 tokens | ~22MB | `mean` | Default, ruim para PT-BR |
| `onnx-community/embeddinggemma-300m-ONNX` | Multilingual | 2048 tokens | ~300MB | `mean` | **Recomendado para PT-BR** |
| `onnx-community/Qwen3-Embedding-0.6B-ONNX` | Multilingual | 32k tokens | ~600MB | `last_token` | Posts muito longos |
| `onnx-community/granite-embedding-small-english-r2-ONNX` | EN | 8192 tokens | ~130MB | `cls` | EN com context longo |
**Regra:** Para conteúdo em português, NUNCA usar o modelo default (`all-MiniLM-L6-v2`). Use `embeddinggemma-300m-ONNX` ou superior.
---
## Custom Provider (LiteLLM, OpenAI, etc.)
A integração aceita custom providers via interface `EmbeddingProvider`:
```typescript
// litellm-provider.ts
import { createEmbeddingProvider } from '@philnash/astro-related-content/providers'
export const litellmProvider = createEmbeddingProvider({
name: 'litellm',
version: '1.0.0',
async embed(texts, options) {
const response = await fetch(`${options.baseUrl}/embeddings`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${options.apiKey}`,
},
body: JSON.stringify({ model: options.model, input: texts }),
})
const data = await response.json()
return data.data.map((item: any) => item.embedding)
},
getMetadata(options) {
return { model: options.model, baseUrl: options.baseUrl }
},
})
```
```typescript
// astro.config.ts
import { litellmProvider } from './litellm-provider'
astroRelatedContent({
collections: ['blog'],
embeddings: {
provider: litellmProvider,
baseUrl: 'http://localhost:4000',
apiKey: 'sk-...',
model: 'text-embedding-3-small',
batchSize: 10,
},
})
```
---
## Artefatos Gerados
A integração gera na pasta `.astro-related-content/`:
| Arquivo | Conteúdo | Tamanho típico (32 posts) |
|---------|----------|---------------------------|
| `data.json` | Rankings (top N related por post) | ~14KB |
| `vectors.json` | Cache de embeddings + metadata | ~736KB |
O modelo ONNX é cacheado em `.astro/astro-related-content/models/` (não vai pro repo — `.astro/` está no `.gitignore`).
---
## Uso no Componente
### Bug de compatibilidade Astro v7
O `getRelatedContent()` do virtual module não funciona com Astro v7 glob loader. O motivo: a integração gera IDs como `slug/index` no `data.json`, mas o Astro v7 usa `slug` (sem `/index`) como `entry.id`.
**Workaround:** Usar `getRelatedContentMatches()` + lookup manual:
```astro
---
// RelatedPosts.astro
import { getPostRoute } from '@/lib/data-utils'
import { formatDate } from '@/lib/utils'
import { Icon } from 'astro-icon/components'
import { Image } from 'astro:assets'
import { getCollection, type CollectionEntry } from 'astro:content'
import { getRelatedContentMatches } from 'virtual:astro-related-content'
import Link from './Link.astro'
interface Props {
postId: string
}
const { postId } = Astro.props
const matches = getRelatedContentMatches('blog', `${postId}/index`)
let relatedContent: { entry: CollectionEntry<'blog'>; score: number }[] = []
if (matches.length > 0) {
const allEntries = await getCollection('blog')
const entryById = new Map(allEntries.map((e) => [e.id, e]))
relatedContent = matches.flatMap((match) => {
// match.id = "slug/index", entry.id no Astro v7 = "slug"
const normalizedId = match.id.replace(/\/index$/, '')
const entry = entryById.get(normalizedId) || entryById.get(match.id)
return entry ? [{ entry, score: match.score }] : []
})
}
---
{
relatedContent.length > 0 && (
<section class="mt-12 border-t pt-8">
<h2 class="mb-6 flex items-center gap-2 text-xl font-medium">
<Icon name="lucide:sparkles" class="size-5" />
Leitura Relacionada
</h2>
<div class="grid gap-4 sm:grid-cols-2">
{relatedContent.map((item) => (
<Link
href={getPostRoute(item.entry)}
class="hover:bg-muted/50 flex gap-3 rounded-xl border p-3 transition-colors duration-300"
>
{item.entry.data.image && (
<div class="hidden w-16 shrink-0 sm:block">
<Image
src={item.entry.data.image}
alt={item.entry.data.title}
width={128}
height={67}
class="rounded-md object-cover"
/>
</div>
)}
<div class="min-w-0">
<h3 class="mb-1 truncate text-sm font-medium">
{item.entry.data.title}
</h3>
<p class="text-muted-foreground text-xs">
{formatDate(item.entry.data.date)}
</p>
</div>
</Link>
))}
</div>
</section>
)
}
```
---
## Deploy no Coolify — Sem Baixar Modelo em Produção
### O Problema
Na primeira build Docker, a integração baixa o modelo ONNX (~300MB) e processa todos os embeddings. Em um servidor com bandwidth limitada, isso pode levar 20+ minutos e esgotar disco.
### A Solução: Dual-Mode (Local + CI)
**Princípio:** Gerar embeddings localmente, commitar o cache, e em CI usar apenas o `data.json` pré-gerado via Vite plugin leve (sem modelo, sem transformers.js).
#### 1. Commitar os artefatos
Garantir que `.astro-related-content/` **NÃO** está no `.gitignore`:
```bash
# Verificar
grep "astro-related-content" .gitignore
# Se aparecer, remover a linha
# Commitar cache
git add .astro-related-content/
git commit -m "chore: cache embeddings related content"
```
#### 2. Configuração condicional no astro.config.ts
```typescript
import { existsSync } from 'node:fs'
import { resolve } from 'node:path'
import astroRelatedContent from '@philnash/astro-related-content'
// Em CI: usa data.json pré-gerado sem baixar modelo
// Local: roda integração completa com embeddings
const isCI = Boolean(process.env.CI || process.env.DOCKER)
const dataJsonPath = resolve('.astro-related-content/data.json')
const hasPrebuiltData = existsSync(dataJsonPath)
const relatedContentIntegrations = isCI && hasPrebuiltData
? [] // Virtual module vem do Vite plugin abaixo
: [
astroRelatedContent({
collections: ['blog'],
generation: { limit: 4 },
embeddings: {
model: 'onnx-community/embeddinggemma-300m-ONNX',
dtype: 'fp32',
pooling: 'mean',
batchSize: 1,
},
}),
]
// Plugin Vite leve para CI — serve virtual module do data.json commitado
function relatedContentVitePlugin() {
const VIRTUAL_ID = 'virtual:astro-related-content'
const RESOLVED_ID = '\0' + VIRTUAL_ID
return {
name: 'related-content-prebuilt',
resolveId(id: string) {
if (id === VIRTUAL_ID) return RESOLVED_ID
},
load(id: string) {
if (id !== RESOLVED_ID) return
const absPath = resolve('.astro-related-content/data.json')
return `
import { getCollection } from "astro:content";
import relatedContentData from ${JSON.stringify(`/@fs/${absPath}`)};
export function getRelatedContentMatches(collection, id) {
const collectionData = relatedContentData[collection];
if (!collectionData) return [];
const matches = collectionData[id];
return Array.isArray(matches) ? matches.map((m) => ({ ...m })) : [];
}
export function getRelatedContentIds(collection, id) {
return getRelatedContentMatches(collection, id).map((m) => m.id);
}
export async function getRelatedContent(collection, id) {
const matches = getRelatedContentMatches(collection, id);
const entries = await getCollection(collection);
const entryById = new Map(
entries.flatMap((entry) => {
const normalizedId = String(entry.id).replace(/\\.(md|mdx)$/, "");
return normalizedId === entry.id
? [[entry.id, entry]]
: [[entry.id, entry], [normalizedId, entry]];
}),
);
return matches.flatMap((match) => {
const entry = entryById.get(match.id);
return entry ? [{ entry, score: match.score }] : [];
});
}
`
},
}
}
export default defineConfig({
integrations: [
// ... outras integrações
...relatedContentIntegrations,
],
vite: {
plugins: [
// ... outros plugins
...(isCI && hasPrebuiltData ? [relatedContentVitePlugin()] : []),
],
},
})
```
#### 3. Dockerfile com `ENV CI=true`
```dockerfile
FROM node:22-slim AS build
WORKDIR /app
ENV CI=true
ENV NODE_OPTIONS="--max-old-space-size=512"
COPY package*.json .npmrc ./
RUN npm ci
COPY . .
RUN npm run build
FROM nginx:alpine
COPY --from=build /app/dist /usr/share/nginx/html
EXPOSE 80
```
O `ENV CI=true` ativa o Vite plugin leve. Nenhum modelo é baixado. Build completa em ~10-30s.
---
## Workflow Operacional
| Ação | Onde | O que acontece |
|------|------|----------------|
| Novo post | Local | `astro build` → regenera embedding só do post novo → commit cache → push |
| Editar post | Local | `astro build` → recalcula embedding do editado → commit cache → push |
| Deploy | Coolify | Usa `data.json` pré-commitado → build rápido (~30s) |
| Primeiro setup | Local | Download modelo (~300MB) + embeddings de todos os posts (1-20min) |
### Tempos Reais (32 posts, EmbeddingGemma 300m)
| Etapa | Tempo |
|-------|-------|
| Primeira geração (download modelo + 32 embeddings) | ~22 min |
| Build subsequente local (cache hit) | ~10 s |
| Build CI com data.json pré-gerado | ~10 s |
| Build CI sem cache (modelo baixando) | ~22+ min ❌ |
---
## Checklist de Implementação
- [ ] `npm install @philnash/astro-related-content`
- [ ] Configurar integração no `astro.config.ts` (com lógica CI/local)
- [ ] Criar componente `RelatedPosts.astro` (com workaround Astro v7)
- [ ] Integrar componente no template de post (`[...id].astro` ou similar)
- [ ] Rodar `astro build` localmente para gerar embeddings
- [ ] Verificar `.astro-related-content/` NÃO está no `.gitignore`
- [ ] Commitar `data.json` + `vectors.json`
- [ ] Setar `ENV CI=true` no Dockerfile
- [ ] Deploy e validar no Coolify
---
## Troubleshooting
### Build no Coolify demora 20+ minutos
**Causa:** `CI=true` não setado no Dockerfile, ou `.astro-related-content/data.json` não commitado. A integração completa está rodando e baixando o modelo.
**Fix:** Setar `ENV CI=true` no Dockerfile E commitar a pasta `.astro-related-content/`.
### Related posts não renderizam (array vazio)
**Causa:** Bug de ID entre integração e Astro v7. O `getRelatedContent()` do virtual module não faz match porque IDs diferem.
**Fix:** Usar `getRelatedContentMatches()` + lookup manual com `normalizedId = match.id.replace(/\/index$/, '')`.
### Embeddings ruins para português
**Causa:** Usando modelo default (`all-MiniLM-L6-v2`) que é English-only.
**Fix:** Usar `onnx-community/embeddinggemma-300m-ONNX` (multilingual).
### Cache invalidado a cada build
**Causa:** A metadata do provider (model, dtype, pooling, version) mudou entre builds. A integração invalida todo o cache quando metadata difere.
**Fix:** Não alterar configuração de embeddings após gerar o cache. Se precisar mudar modelo, regenerar tudo localmente e re-commitar.
### `Cannot find module '@huggingface/transformers'` em CI
**Causa:** O pacote `@huggingface/transformers` é dependência transitiva só necessária quando a integração completa roda. Em CI com o Vite plugin, não é necessário.
**Fix:** Se usar o dual-mode (CI plugin), isso não acontece. Se rodar integração em CI, garantir que `npm ci` instala todas deps.