Skip to content

Blog gerenciado e o plugin do site

O atpares guarda os artigos dos quatro projetos e os serve por uma API de leitura. O site não mantém mais markdown no próprio repositório: ele instala uma layer do Nuxt que busca o conteúdo em tempo de render.

As três peças

PeçaOnde viveO que faz
GerenciadorPainel, em /p/:projectId/blogEscrever, agendar, publicar e excluir artigo
API de conteúdo/api/content, públicaServir só o que está publicado, por chave de site
Pluginatpares-plugin, publicado no npmO que o site instala para consumir a API

O plugin é um repositório próprio e um pacote só no npm, atpares. O site instala esse pacote e importa de dentro dele o que precisar: atpares/nuxt é a layer do Nuxt, atpares/next/blog é o caminho do Next resolvido no build, atpares/client é o cliente da API em TypeScript, sem dependência, e atpares/blog.css é a folha do blog. O que mais for gerenciado pelo painel entra como mais um caminho.

Estados do artigo

draft fica só no painel. scheduled guarda data e hora e entra no ar pela varredura de meia em meia hora da automação, que publica em uma consulta tudo que já venceu. published é o que a API de conteúdo enxerga. Rascunho e agendado que ainda não saiu respondem 404 para o site, não conteúdo vazio.

O que o autor não preenche o servidor deriva do texto: resumo do primeiro parágrafo, palavra-chave do título, texto alternativo da capa e tempo de leitura pela contagem de palavras. O painel manda apenas o que foi escrito, então a regra existe em um lugar só.

Chave do site

A chave é criada no painel, em Configurações, aba Blog, onde cada site conectado é um cartão. Ela tem o formato pk_<prefixo>.<segredo>: o prefixo identifica a linha em blog_keys e o segredo é guardado apenas como hash sha256, comparado em tempo constante. A chave completa aparece uma vez, no momento da criação, na gaveta que abre logo em seguida.

Cada chave guarda também o origin do site que a usa. Em branco, ela herda o domínio do projeto. É esse valor que entra no public.atpares.origin do trecho de instalação, então produção e homologação saem com o canonical certo cada uma.

O cartão mostra a última leitura daquela chave, que é o jeito de saber se o site está mesmo buscando. Sem leitura nenhuma, o cartão diz isso em vez de parecer conectado.

Cada chave pertence a um projeto. A chave do Contrasync não lê artigo do Guentaí, e revogar corta o acesso na requisição seguinte. Um projeto aceita até cinco chaves ativas, o que permite trocar a do site sem janela sem conteúdo.

A chave guarda ainda a tecnologia do site, entre Nuxt e Next, porque é ela que decide o caminho e o trecho de instalação que o painel monta.

A chave viaja no cabeçalho x-atpares-key e nunca chega ao navegador. No Nuxt, o plugin registra rotas em /_atpares no servidor do próprio site, e são elas que falam com o atpares. No Next estático não existe servidor em runtime: a chave é lida durante o next build, no ambiente de quem constrói o site, e não entra no HTML gerado.

API de conteúdo

Todas as rotas exigem a chave e respondem com Cache-Control: public, max-age=300.

RotaResposta
GET /api/content/siteNome, domínio, categorias e tags com contagem
GET /api/content/postsPágina de cartões, com page, perPage, category, tag e search
GET /api/content/posts/:slugArtigo com corpo em markdown, HTML convertido, títulos com âncora e relacionados
GET /api/content/sitemapSlug, data de publicação e última alteração
GET /api/content/media/:idBytes da capa, já comprimida

Cada artigo volta com cover, a url pública e imutável da capa servida pelo atpares. Nenhum site copia imagem para o próprio repositório nem mantém rota para servi-la: a capa vive aqui e é referenciada de fora.

O markdown é convertido no servidor. O que o autor escreveu é escapado, título vira h2 para não brigar com o h1 da página, e a conversão cobre lista, citação, tabela, bloco de código, imagem e link com esquema validado. O site não precisa de biblioteca de markdown.

Os relacionados saem primeiro da lista escolhida no artigo e completam com vizinhos da mesma categoria, sem repetir e sem citar o próprio artigo.

Instalar num site Nuxt

bash
npm install atpares
ts
export default defineNuxtConfig({
  extends: ['atpares/nuxt'],
  runtimeConfig: {
    atpares: {
      key: process.env.ATPARES_KEY,
      base: 'https://app.atpares.com/api/content'
    },
    public: {
      atpares: {
        origin: 'https://seusite.com',
        name: 'Seu site',
        logo: '/logo.svg'
      }
    }
  }
})

A layer entrega /blog, /blog/[slug] e /blog-sitemap.xml, com canonical, Open Graph, Article e BreadcrumbList montados a partir do artigo. origin é o domínio canônico; sem ele a layer usa o host da requisição, que não é confiável quando a página vem do cache de rota.

A capa também passa pelo proxy, então a imagem é servida pelo domínio do site e entra no Open Graph com endereço absoluto.

Sobrescrever

Arquivo do site com o mesmo caminho vence o da layer. Para trocar só a chamada final do artigo existe o slot cta. Para montar a página inteira, os composables ficam disponíveis:

ts
const { data: page } = useAtparesPosts(() => ({ page: 1, category: 'clm' }))
const { ready, data, cover } = useAtparesPost('o-que-e-clm')
const { linkOf, coverOf, origin, prefix } = useAtparesBlog()

useAtparesPost expõe ready em vez de ser aguardada direto, porque o await do useFetch devolve o objeto interno do Nuxt e descartaria o que a composable acrescenta. A página aguarda ready e só então decide entre renderizar e devolver 404.

A layer define swr: 300 para /blog e /blog/**. O site pode trocar esse comportamento nas próprias routeRules, que têm precedência.

Site Nuxt estático

Contrasync e Guentaí podem ser gerados com nuxt generate ou nitro.preset: 'static'. A layer percebe isso no build e troca de modo: lê o acervo e acrescenta /blog e cada /blog/<slug> às rotas de prerender, sem nada listado à mão. As capas continuam vindo do atpares por url, então não há imagem para copiar.

Com servidor, o caso da Prompteira, nada disso acontece e o artigo continua aparecendo sem deploy.

Um detalhe do estático: o crawler do Nitro segue os links internos dos artigos, e um link para página inexistente derruba o build, porque prerender.failOnError vem ligado.

Instalar num site Next.js

O caso do Drawgg, que é export estático para S3 e CloudFront. Não há servidor em runtime, então tudo é resolvido no build.

bash
npm install atpares

O pacote traz as páginas prontas, então a instalação não cria arquivo de configuração. As duas rotas do App Router viram re-export:

tsx
// app/blog/page.tsx
export { blogIndexMetadata as generateMetadata, BlogIndexPage as default } from 'atpares/next/blog'

// app/blog/[slug]/page.tsx
export const dynamicParams = false
export { blogStaticParams as generateStaticParams, blogPostMetadata as generateMetadata, BlogPostPage as default } from 'atpares/next/blog'
bash
ATPARES_BASE=https://app.atpares.com/api/content
ATPARES_KEY=pk_xxxxxxxxxxxx.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
ATPARES_SITE_NAME=Drawgg
ATPARES_SITE_ORIGIN=https://drawgg.com

Quem quiser as próprias páginas continua tendo os dados: blog() devolve cards, post, site e sitemap, e PostCard e PostMeta são exportados. O acervo é percorrido uma vez por build e fica em memória, então trinta e cinco páginas geradas não viram trinta e cinco requisições.

app/sitemap.ts precisa de export const dynamic = 'force-static' quando o site usa output: 'export'. Sem isso o build para com um erro sobre revalidate.

O preço do estático: publicar no atpares não coloca o artigo no ar sozinho, é preciso construir e publicar de novo. Para um site que já vivia de markdown no repositório isso não muda a rotina, só tira o commit do caminho.

Importar markdown existente

bash
node scripts/import-markdown.mjs --project contrasync \
  --dir ~/Contrasync/contrasync-nuxt-landing/src/blog/content \
  --token "$ATPARES_TOKEN"

Sem --apply o comando apenas lista o que faria. Com --apply ele cria o que falta e atualiza o que já existe pelo slug, então rodar duas vezes não duplica acervo. O front matter vira título, resumo, palavra-chave, categoria, chamada final, relacionados e data de publicação.

--covers aponta para a pasta pública do site. Quando o front matter traz cover com um caminho local, a imagem é enviada junto e o acervo chega completo ao atpares, em vez de perder a capa na migração.