Como aninhar um Row dentro de um Col no gpdf?
Não dá — ColBuilder não tem método Row no gpdf. A grade de 12 colunas é plana de propósito. Três idiomas substituem os rows aninhados.
A pergunta, em outras palavras
Você vem do Bootstrap ou do Tailwind, onde .row e .col se aninham livremente. Dá para colocar um .row dentro de um .col dentro de outro .row e a grade continua cascateando. Você senta com o gpdf, vê o mesmo idioma r.Col(span, fn), e vai procurar c.Row(...) dentro do callback da coluna. Não tem. Foi um esquecimento?
TL;DR
Não. A grade de 12 colunas do gpdf é plana de propósito. ColBuilder só aceita conteúdo — Text, Image, Table, Box, List, Spacer — e Row / AutoRow vivem em PageBuilder, não em ColBuilder. Se você veio aqui atrás da sintaxe, ela não existe. Siga lendo para as três coisas que a substituem.
A cara da API
Aqui está o que o conjunto de métodos de ColBuilder realmente contém (de gpdf/template/grid.go):
func (c *ColBuilder) Text(text string, opts ...TextOption)
func (c *ColBuilder) Image(src []byte, opts ...ImageOption)
func (c *ColBuilder) Box(fn func(c *ColBuilder), opts ...BoxOption)
func (c *ColBuilder) Table(header []string, rows [][]string, opts ...TableOption)
func (c *ColBuilder) Line(opts ...LineOption)
func (c *ColBuilder) List(items []string, opts ...ListOption)
func (c *ColBuilder) Spacer(height document.Value)
// …PageNumber, TotalPages, RichText, QRCode, Barcode
Sem Row. Sem AutoRow. Sem Col. O caminho Col → Row não existe como método, e c.Box(fn, ...) é o que mais se aproxima — mas Box aceita outro *ColBuilder, não um row. Você pode aninhar colunas dentro de colunas (mais ou menos, via Box), mas não pode abrir uma nova linha horizontal dentro de uma coluna. Essa é a restrição.
Idioma 1 — Rows irmãos no nível da página
É isso que 90% dos padrões "row aninhado" realmente querem.
package main
import (
"log"
"os"
"github.com/gpdf-dev/gpdf"
"github.com/gpdf-dev/gpdf/document"
"github.com/gpdf-dev/gpdf/template"
)
func main() {
doc := gpdf.NewDocument(
gpdf.WithPageSize(document.A4),
gpdf.WithMargins(document.UniformEdges(document.Mm(15))),
)
page := doc.AddPage()
// O que você queria escrever (mas não dá):
//
// page.AutoRow(func(r *template.RowBuilder) {
// r.Col(8, func(c *template.ColBuilder) {
// c.Row(...) ❌ não existe
// })
// })
// O que você escreve no lugar:
page.AutoRow(func(r *template.RowBuilder) {
r.Col(8, func(c *template.ColBuilder) {
c.Text("Título do artigo", template.FontSize(18), template.Bold())
})
r.Col(4, func(c *template.ColBuilder) {
c.Text("2026-05-16")
})
})
page.AutoRow(func(r *template.RowBuilder) {
r.Col(8, func(c *template.ColBuilder) {
c.Text("O parágrafo de abertura ocupa a mesma coluna de 8.")
})
r.Col(4, func(c *template.ColBuilder) {
c.Text("por Taiki Noda")
})
})
data, err := doc.Generate()
if err != nil {
log.Fatal(err)
}
_ = os.WriteFile("flat.pdf", data, 0o644)
}
Os dois AutoRow compartilham os mesmos spans 8+4, então as colunas se alinham visualmente. Não tem sub-grade; tem uma sequência plana de rows que por acaso usam a mesma divisão. A saída é idêntica à de um layout CSS que aninhasse .row dentro de .col-8 — porque a única coisa que a forma aninhada comprava era localidade sintática, e o gpdf prefere que você gaste esse orçamento em consistência de largura.
Idioma 2 — c.Box para agrupamento visual
Se a motivação de verdade era "quero um cartão com borda com dois elementos empilhados dentro desta coluna", você queria Box, não um sub-row:
page.AutoRow(func(r *template.RowBuilder) {
r.Col(6, func(c *template.ColBuilder) {
c.Box(func(c *template.ColBuilder) {
c.Text("Faturar para", template.Bold())
c.Text("Acme Ltda.")
c.Text("São Paulo, Brasil")
},
template.WithBoxBorder(template.Border(
template.BorderWidth(document.Pt(1)),
template.BorderColor(pdf.RGBHex(0xBDBDBD)),
)),
template.WithBoxPadding(document.UniformEdges(document.Mm(4))),
)
})
r.Col(6, func(c *template.ColBuilder) {
c.Box(func(c *template.ColBuilder) {
c.Text("Enviar para", template.Bold())
c.Text("Mesmo do faturamento")
},
template.WithBoxPadding(document.UniformEdges(document.Mm(4))),
)
})
})
O *ColBuilder que Box recebe empilha o conteúdo verticalmente. Também não dá para dividir um Box horizontalmente — para isso, você volta ao Idioma 1. Mas para o padrão de "cartão" que a sintaxe de row aninhado costuma tentar, esta é a ferramenta certa. A linha c.Box em gpdf/template/grid.go:246 é o único aninhamento que a grade permite, e é deliberadamente unidimensional.
Idioma 3 — Planeje a sub-grade direto em 12 colunas
Às vezes você quer mesmo um layout de 2 colunas dentro do que parece uma seção de meia página: uma miniatura e uma legenda na metade esquerda, um parágrafo na direita. O instinto é Col(6) > Row > Col(6) + Col(6). O equivalente plano é só Col(3) + Col(3) + Col(6):
page.AutoRow(func(r *template.RowBuilder) {
r.Col(3, func(c *template.ColBuilder) {
c.Image(thumbBytes)
})
r.Col(3, func(c *template.ColBuilder) {
c.Text("Foto por Ansel Adams", template.Italic())
c.Text("1942")
})
r.Col(6, func(c *template.ColBuilder) {
c.Text("O parágrafo do corpo ocupa a metade direita da página.")
})
})
3 + 3 somados dão 6, então o par miniatura/legenda ocupa exatamente a metade esquerda. Doze fatora em 2, 3, 4 e 6, então uma grade aninhada quase sempre achata limpo. Se sua grade aninhada era Col(8) > Row > Col(7) + Col(5), isso não achata — mas esses números também não significam nada num documento de verdade. Escolha a versão plana que significa.
Por que sem aninhamento
Uma grade plana resolve larguras em uma passada. O row é uma porcentagem da largura da página menos margens. Cada Col(span) é span / 12 disso. Pronto. Sem recursão, sem largura-de-uma-largura-de-uma-largura, sem contexto pai costurado pelo motor de layout. A linha em grid.go que calcula a largura da coluna é literalmente uma linha:
Width: document.Pct(float64(col.span) / float64(gridColumns) * 100),
Adicione aninhamento e essa linha vira um percurso de árvore. De repente você precisa decidir o que Col(6) dentro de Col(8) dentro de Col(12) significa — 6 é 50% da coluna pai, 50% do row, ou 50% da página? Bootstrap escolheu "50% do pai" e adicionou breakpoints e gutters para tornar isso suportável. PDFs não têm breakpoints. PDFs não têm contêiner fluido. Pegar emprestado o idioma de aninhamento importaria três problemas que não temos, em troca de um atalho sintático que não precisamos.
"Mas eu quero localidade sintática"
Justo. A desvantagem de achatar é que duas chamadas AutoRow que pertencem conceitualmente juntas podem se afastar no código conforme você edita. Um pequeno helper fecha a lacuna:
func card(page *template.PageBuilder, title, body string) {
page.AutoRow(func(r *template.RowBuilder) {
r.Col(12, func(c *template.ColBuilder) {
c.Text(title, template.Bold())
})
})
page.AutoRow(func(r *template.RowBuilder) {
r.Col(12, func(c *template.ColBuilder) {
c.Text(body)
})
})
}
A localidade vive na sua função, não na API. O gpdf não embute card porque são três linhas e a sua versão vai se ajustar melhor ao seu documento do que a nossa.
Receitas relacionadas
- Como a grade de 12 colunas do gpdf funciona? — a grade em detalhe
- Gerar um PDF de fatura em Go em menos de 50 linhas — um layout de grade plana sustentando um documento inteiro
- Layout guide — referência completa de rows, colunas e Box
Experimente o gpdf
O gpdf é uma biblioteca Go para gerar PDFs. Licença MIT, zero dependências externas, suporte CJK nativo.
go get github.com/gpdf-dev/gpdf