Na parte 1 eu justifiquei a loucura de fazer um blog do zero em 2026. Agora vem a parte que a maioria dos tutoriais pula: o que de fato acontece quando você para de planejar e começa a codar.
Spoiler: vai quebrar. Muito. Mas cada quebra ensina algo que nenhum tutorial de 10 minutos no YouTube vai te dar.
Spoiler: vai quebrar. Muito. Mas cada quebra ensina algo que nenhum tutorial de 10 minutos no YouTube vai te dar.
1. O Editor: Por que o Trix e não o ChatGPT me recomendar outro
Quando você decide fazer um blog, a primeira pergunta técnica não é o banco de dados. É: como eu vou escrever os textos?
Testei o Quill. Bonito, flexível, cheio de plugins. E cheio de `<span style="font-size: 11pt; font-family: Arial">` quando você cola qualquer coisa do Word ou do Notion. O HTML que ele gera parece o código-fonte de um e-mail corporativo de 2008.
O Trix, editor do Basecamp, é opinionado demais pra ser popular, e exatamente por isso eu escolhi. Ele normaliza tudo que você cola.
1 - o tool box la em cima e nós 50 parágrafos a baixo, ficar indo para cima e para baixo toda hora seria um saco ai entao eu decidi que a tool box do trix tinha de me aconhar enquanto escrevia, esse era o codigo muito simples:
<style>
/* Toolbar Trix flutuante */
trix-toolbar {
position: sticky;
top: 0;
z-index: 30;
background-color: #ffffff;
border-bottom: 1px solid #e5e7eb;
padding: 0.25rem 0;
}
.dark trix-toolbar {
background-color: #1f2937;
border-bottom-color: #374151;
}Só não funcionou, a classe .trix-floating só aplica position: fixed quando o JS a adiciona:
trix-toolbar.trix-floating {
position: fixed;
top: 0;
z-index: 50;
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
background-color: #ffffff;
border-bottom: 1px solid #e5e7eb;
}
.dark trix-toolbar.trix-floating {
background-color: #1f2937;
border-bottom-color: #374151;
}no evento trix-initialize, escuta o scroll e verifica a posição do editor:
if (rect.top < 0 && rect.bottom > height) {
spacer.style.cssText = `display:block;height:${height}px`;
toolbar.style.left = rect.left + 'px';
toolbar.style.width = rect.width + 'px';
toolbar.classList.add('trix-floating');
} else {
spacer.style.cssText = 'display:none';
toolbar.style.left = '';
toolbar.style.width = '';
toolbar.classList.remove('trix-floating');
}2 - O trade-off? Upload de imagens não vem de graça. O Trix dispara um evento `trix-attachment-add` quando você arrasta uma foto pro editor, mas o resto é por sua conta.
A solução foi usar o `WithFileUploads` do próprio Livewire, que já tínhamos para o upload da capa. O JS captura o arquivo, manda pro servidor, o Laravel salva e devolve a URL pro Trix posicionar a imagem no cursor. Simples na teoria. Na prática levei dois erros bons antes de chegar lá.
PHP (componente Volt):
public $trixImage = null;
public function storeTrixImage(): void
{
$this->validate(['trixImage' => 'required|image|max:5120']);
$path = $this->trixImage->store('post-images', 'public');
$this->dispatch('trix-image-ready', url: asset('storage/' . $path));
$this->trixImage = null;
}
JS (4 etapas em sequência):
1. Captura o arquivo, trix-attachment-add dispara quando você arrasta/cola uma imagem
2. Sobe pelo DOM, .closest('[wire:id]') a partir do <trix-editor> garante que pega o componente certo (não a navbar)
3. Faz o upload, component.upload('trixImage', file, onSuccess, onError, onProgress) usa o sistema de upload do Livewire com barra de progresso nativa do Trix
4. Recebe a URL, quando o PHP salva o arquivo e dispara trix-image-ready, o JS chama attachment.setAttributes({ url }) e a imagem aparece no cursor
2. O Sumário: JavaScript Raiz, Sem Biblioteca
O Sumário lateral (o "Nesta Página" que você vê aqui do lado) é 100% JavaScript puro. Sem dependência, sem pacote npm, sem nada.
A lógica é simples: varrer o conteúdo em busca de `h1, h2, h3`, gerar IDs únicos para cada um, montar uma lista de links.
O problema é que conteúdo colado do Trix frequentemente não usa headings. Quando você cola um texto, o editor não sabe que aquele parágrafo em negrito era um título de seção. Então adicionei um fallback: se um `<strong>` é o único conteúdo do seu parágrafo pai, ele entra no sumário como se fosse um heading.
<aside class="hidden lg:block sticky top-28 self-start z-20 w-72">
<div class="max-h-[calc(100vh-10rem)] overflow-y-auto pr-4">
<p class="text-xs font-bold text-gray-500 uppercase tracking-widest mb-6 flex items-center gap-2">
<span class="w-8 h-px bg-gray-200"></span>
Nesta Página
</p>
<nav>
<ul id="toc-list" class="space-y-4 text-xs font-medium text-gray-500 border-l border-gray-100">
<!-- Gerado via JavaScript -->
</ul>
</nav>
</div>
</aside> document.addEventListener('DOMContentLoaded', () => {
const content = document.getElementById('article-content');
const tocList = document.getElementById('toc-list');
// 1. Busca h1, h2, h3
let headings = Array.from(content.querySelectorAll('h1, h2, h3'));
// 2. Fallback: <strong> que ocupa o parágrafo inteiro = título informal (comportamento do Trix)
content.querySelectorAll('strong').forEach(strong => {
const parent = strong.parentElement;
if (parent && (parent.tagName === 'DIV' || parent.tagName === 'P')
&& strong.textContent.trim().length > 10
&& parent.textContent.trim() === strong.textContent.trim()) {
headings.push(strong);
}
});
// 3. Ordena pela posição no DOM
headings.sort((a, b) =>
a.compareDocumentPosition(b) & Node.DOCUMENT_POSITION_FOLLOWING ? -1 : 1
);
if (headings.length === 0) {
tocList.innerHTML = '<li class="italic text-gray-400">Nenhum sumário disponível.</li>';
return;
}
// 4. Monta os itens
headings.forEach((heading, index) => {
let id = heading.getAttribute('id');
if (!id) {
id = 'secao-' + index + '-' + heading.innerText
.toLowerCase().replace(/[^a-z0-9]+/g, '-')
.replace(/(^-|-$)/g, '').substring(0, 30);
heading.setAttribute('id', id);
}
const li = document.createElement('li');
li.className = '-ml-px border-l border-transparent hover:border-blue-500 transition-colors';
const a = document.createElement('a');
a.href = location.pathname + '#' + id;
a.innerText = heading.innerText;
a.className = 'pl-4 hover:text-blue-600 transition-colors block line-clamp-2 leading-tight py-0.5';
if (heading.tagName === 'H2') a.classList.add('pl-6');
if (heading.tagName === 'H3') a.classList.add('pl-8');
li.appendChild(a);
tocList.appendChild(li);
});
});Funciona na maioria dos casos. Não é perfeito. Mas é melhor que nada e melhor que instalar uma biblioteca de 50kb pra fazer isso.
O detalhe mais recente: o último item do sumário é sempre o comentário do Kikito , com o avatar dele, o nome configurado e a cor que eu escolhi no painel. Um link que diz implicitamente "tem uma opinião não solicitada te esperando lá embaixo".
3. Docker: O Ambiente que Te Ensina Respeito
Esse tópico eu já toquei na Parte 1, mas vale aprofundar porque aprendi mais coisas.
O Dockerfile de produção tem uma linha que parece boba mas salvou minha sanidade:
```dockerfile
RUN rm -rf bootstrap/cache/*.php
RUN composer install --no-dev --optimize-autoloader
```
A ordem importa. Se você rodar o `composer install` antes de limpar o cache, o Laravel vai tentar usar o cache do seu ambiente de desenvolvimento, que referencia pacotes que você mandou ignorar com `--no-dev`. Resultado: erro de classe não encontrada no primeiro request. Em produção. Com usuário tentando acessar.
O segundo aprendizado foi o `nginx.conf`. Por padrão, o Nginx rejeita uploads acima de 1MB. Silenciosamente. Você fica olhando pro formulário sem entender por que a foto de capa não sobe. Um `client_max_body_size 100M` resolve, mas só depois que você perder tempo debugando o lado errado do sistema.
4. RSS: Porque Sim
Adicionei um feed RSS. Em 2026. Sem ninguém pedir.
O RSS é um contrato direto com o leitor: "se você me seguir por aqui, eu te aviso quando tiver coisa nova". Sem algoritmo decidindo pra quem mostrar, sem engajamento forçado, sem plataforma intermediária.
Leitores técnicos ainda usam RSS. Muito. Feedly, newsboat, leitores de terminal. E bots de indexação adoram. É um sinal de que o site é sério.
A implementação foi zero dependências: um controller simples, uma rota pública `/feed.rss` e uma view Blade que gera o XML na mão. Cinquenta linhas. Funciona. Vai durar décadas sem precisar de atualização.
class FeedController extends Controller
{
public function __invoke()
{
$posts = Post::published()
->with('user')
->latest('published_at')
->limit(20)
->get();
return response()
->view('feed', compact('posts'))
->header('Content-Type', 'application/rss+xml; charset=UTF-8');
}
}<?php echo '<?xml version="1.0" encoding="UTF-8"?>'; ?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
<channel>
<title>{{ config('app.name') }}</title>
<link>{{ url('/') }}</link>
<description>Blog pessoal de tecnologia, código e cultura
digital.</description>
<language>pt-BR</language>
<lastBuildDate>{{ $posts->first()?->published_at?->toRfc822String()
}}</lastBuildDate>
<atom:link href="{{ route('feed') }}" rel="self" type="application/rss+xml"/>
@foreach($posts as $post)
<item>
<title><![CDATA[{{ $post->title }}]]></title>
<link>{{ route('posts.show', $post->slug) }}</link>
<guid isPermaLink="true">{{ route('posts.show', $post->slug) }}</guid>
<pubDate>{{ $post->published_at->toRfc822String() }}</pubDate>
<description><![CDATA[{{ Str::limit(strip_tags($post->content), 300)
}}]]></description>
@if($post->cover_image)
<enclosure url="{{ asset('storage/' . $post->cover_image) }}"
type="image/jpeg"/>
@endif
</item>
@endforeach
</channel>
</rss>```
GET /feed.rss → últimos 20 artigos publicados, RSS 2.0
```
Pronto. Próximo.
GET /feed.rss → últimos 20 artigos publicados, RSS 2.0
```
Pronto. Próximo.
5. O Kikito Ganhou Personalidade Configurável
Na Parte 1 eu apresentei o Kikito, a maritaca IA que comenta meus artigos. O que não contei é que ele evoluiu.
Agora qualquer pessoa que usar esse blog pode configurar o próprio bot: nome, foto, modelo do Gemini (do econômico ao mais inteligente) e a persona completa em texto livre. É como contratar um funcionário imaginário, você escreve a descrição da vaga e o modelo interpreta o papel.
Mais recente ainda: o bloco de comentário da IA ganhou cor configurável. O painel de perfil tem um color picker. Você escolhe a cor, o bloco no artigo usa aquela cor com opacidade suave, fundo e borda, via CSS inline calculado no servidor. Sem Tailwind hardcoded, sem classe arbitrária. A cor sai do banco, vira `rgba()` e vai pro `style=""`.
E a cor aparece também no sumário, no link do Kikito, junto com o avatar dele. Consistência visual sem esforço do autor.
6. O que Ainda Está Faltando (e eu sei disso)
Ser honesto é parte do contrato aqui.
O sumário com bold como heading funciona, mas mal. Se você escrever um parágrafo onde a primeira frase é em negrito, ele vai parar no sumário. Não é o comportamento esperado. Preciso de um critério melhor, provavelmente tamanho mínimo de texto e posição no parágrafo.
O sistema de comentários é o Disqus. Funciona, é grátis, mas carrega um iframe pesado e rastreia o leitor. Num blog sobre soberania técnica, terceirizar os comentários para uma plataforma de publicidade é uma ironia que não me escapa. Pode virar uma Parte 3.
O deploy ainda é manual: `git pull`, rebuild da imagem Docker, migrate. Funciona, mas um webhook do GitHub que dispara o deploy automaticamente no push seria o próximo passo natural.
Conclusão: O Sistema que Você Entende
A Parte 1 foi sobre identidade. A Parte 2 é sobre consequência.
Cada decisão aqui tem um motivo. Trix porque normaliza. Grid porque escala. Docker porque isola. RSS porque dura. Kikito porque, por que não ter uma maritaca que te julga em público.
Não é o sistema mais sofisticado do mercado. É o sistema que eu consigo depurar às 11 da noite sem Stack Overflow, porque eu mesmo construí, erro por erro, linha por linha.
O código está no GitHub, aberto, livre pra quem quiser usar ou melhorar.
Parte 3? Se tiver, vai falar sobre CDN, cache de borda e a pergunta que todo dev adia até não poder mais: quanto isso custa pra rodar de verdade?