[{"data":1,"prerenderedAt":2200},["ShallowReactive",2],{"blog-visitcard-generator-nuxt-pdf-do-zero":3},{"id":4,"title":5,"author":6,"body":7,"date":2185,"description":2186,"extension":2187,"faq":2188,"image":2189,"meta":2190,"navigation":404,"path":2191,"seo":2192,"stem":2193,"tags":2194,"updated":2188,"__hash__":2199},"blog\u002Fblog\u002Fvisitcard-generator-nuxt-pdf-do-zero.md","VisitCardGenerator: como construí um gerador de cartões de visita em PDF do zero com Nuxt 3","Larissa Santos",{"type":8,"value":9,"toc":2159},"minimark",[10,14,17,20,23,28,33,36,40,43,47,63,67,70,74,77,79,83,86,89,151,154,158,168,191,194,200,203,632,650,660,674,680,864,871,875,1126,1129,1133,1141,1351,1354,1360,1390,1393,1397,1422,1425,1427,1434,1443,1452,1562,1570,1572,1576,1579,1754,1761,1831,1833,1837,1842,1957,1976,1978,1982,1988,2047,2058,2060,2064,2070,2122,2129,2131,2135,2142,2145,2153,2156],[11,12,13],"p",{},"Criar um cartão de visita profissional deveria ser simples. Mas a realidade é que a maioria das ferramentas disponíveis exige conta, cobra pelo download, ou te coloca numa interface de drag-and-drop onde você passa mais tempo ajustando posição de elementos do que produzindo algo de valor.",[11,15,16],{},"A ideia do VisitCardGenerator surgiu exatamente daí: um gerador onde qualquer pessoa preenche campos, visualiza o resultado em tempo real e baixa o PDF pronto para gráfica, sem cadastro, sem custo, sem complicação.",[11,18,19],{},"Neste artigo vou detalhar as decisões técnicas, os desafios que enfrentei e o que aprendi no processo.",[21,22],"hr",{},[24,25,27],"h2",{"id":26},"decisões-técnicas","Decisões técnicas",[29,30,32],"h3",{"id":31},"nuxt-3","Nuxt 3",[11,34,35],{},"O Nuxt foi escolhido por entregar SSR, SEO nativo e performance de carregamento, essenciais para um produto que precisa de visibilidade orgânica. O file-based routing elimina configuração manual de rotas, o sistema de módulos acelera integrações, e o ecossistema do NuxtUI com Tailwind v4 entregou velocidade de desenvolvimento sem abrir mão de qualidade visual. A escolha veio da experiência prévia com a tecnologia e da confiança no que ela entrega.",[29,37,39],{"id":38},"pinia","Pinia",[11,41,42],{},"O estado do editor tem uma característica importante: precisa ser acessado por pelo menos três camadas diferentes, a página que orquestra, o formulário que edita, e os cards que renderizam. Passar tudo via props e emits geraria um prop drilling desnecessário e acoplamento entre componentes que não precisam se conhecer. O Pinia resolve isso com uma store central que qualquer componente acessa diretamente, sem intermediários.",[29,44,46],{"id":45},"html-to-image-no-lugar-de-html2canvas","html-to-image no lugar de html2canvas",[11,48,49,50,54,55,58,59,62],{},"O ",[51,52,53],"code",{},"html2canvas"," é a biblioteca mais popular para captura de DOM, mas não suporta o espaço de cor ",[51,56,57],{},"oklch()",", que é exatamente o que o Tailwind v4 usa para gerar suas cores. O resultado seria um card completamente branco no PDF. O ",[51,60,61],{},"html-to-image"," não tem essa limitação e entrega o mesmo resultado visual com uma API equivalente.",[29,64,66],{"id":65},"jspdf","jsPDF",[11,68,69],{},"Biblioteca consolidada para geração de PDF no browser, sem dependência de servidor. Permite definir o formato exato em milímetros, essencial para gerar o PDF no tamanho padrão de gráficas (88.9x50.8mm) sem distorção.",[29,71,73],{"id":72},"typescript","TypeScript",[11,75,76],{},"Usado em todo o projeto. Com um formulário que tem ~15 campos tipados e componentes que recebem subconjuntos diferentes desses campos como props, o TypeScript eliminou uma classe inteira de bugs e tornou o autocomplete confiável durante o desenvolvimento.",[21,78],{},[24,80,82],{"id":81},"arquitetura-do-editor","Arquitetura do editor",[11,84,85],{},"O editor é a peça central do projeto. Visualmente é uma tela dividida em duas colunas: à esquerda o formulário de edição, à direita o preview do cartão em tempo real. No mobile, o preview ocupa a tela inteira e o formulário abre como um painel deslizante a partir de baixo.",[11,87,88],{},"A arquitetura foi pensada com separação clara de responsabilidades, seguindo o princípio da responsabilidade única: cada camada faz exatamente uma coisa e não sabe mais do que precisa.",[90,91,92,103,112,125,134],"ul",{},[93,94,95,102],"li",{},[96,97,98,101],"strong",{},[51,99,100],{},"useEditorStore"," (Pinia):"," fonte da verdade do conteúdo do cartão. Gerencia o estado do formulário, as imagens, os getters de props dos cards, a persistência no localStorage e o reset.",[93,104,105,111],{},[96,106,107,110],{},[51,108,109],{},"useCardExport"," (composable):"," toda a lógica de captura dos elementos do DOM e geração do PDF. Não conhece a store nem o formulário, recebe os elementos e o nome do arquivo.",[93,113,114,120,121,124],{},[96,115,116,119],{},[51,117,118],{},"editor.vue"," (página):"," orquestra. Carrega o estado salvo no ",[51,122,123],{},"onMounted",", conecta o preview com os getters da store e delega a exportação ao composable.",[93,126,127,133],{},[96,128,129,132],{},[51,130,131],{},"Editor\u002FForm\u002Findex.vue"," (formulário):"," acessa a store diretamente e tem autonomia sobre sua própria apresentação: qual tab está ativa, validação de e-mail, upload de imagem, reset com modal de confirmação.",[93,135,136,146,147,150],{},[96,137,138,141,142,145],{},[51,139,140],{},"BusinessFront.vue"," e ",[51,143,144],{},"BusinessBack.vue"," (cards):"," puramente apresentacionais. Recebem props, calculam estilos via ",[51,148,149],{},"computed"," e renderizam. Não têm estado próprio e podem ser usados em qualquer contexto sem adaptação.",[11,152,153],{},"Esse isolamento tem consequências práticas: mudar a lógica de exportação não afeta os cards, adicionar um campo no formulário não exige tocar no preview, e os cards são reutilizados sem modificação no editor, na landing page e na exportação do PDF.",[29,155,157],{"id":156},"estrutura-de-componentes","Estrutura de componentes",[159,160,165],"pre",{"className":161,"code":163,"language":164},[162],"language-text","components\u002F\n  Animations\u002F             ← animações reutilizáveis (ex: reveal on scroll)\n  Editor\u002F\n    Card\u002F\n      BusinessFront.vue   ← frente do cartão (apresentação pura)\n      BusinessBack.vue    ← verso do cartão (apresentação pura)\n      Preview.vue         ← preview escalado para a tela\n    Form\u002F\n      index.vue           ← formulário principal com tabs\n      ColorPicker.vue     ← seletor de cor customizado\n      PatternPicker.vue   ← seletor de padrão geométrico\n      Field.vue           ← wrapper de campo com label\n      Label.vue           ← label reutilizável\n    Header.vue            ← cabeçalho do editor\n    Footer.vue            ← rodapé do editor\n  LandingPage\u002F\n    Card.vue              ← exemplo interativo com flip 3D\n    \u002F\u002F demais seções da landing page...\n  OgImage\u002F                ← componente de OG Image customizado\n","text",[51,166,163],{"__ignoreMap":167},"",[11,169,170,171,174,175,178,179,182,183,186,187,190],{},"O formulário foi quebrado em componentes menores dentro de ",[51,172,173],{},"Form\u002F"," seguindo o SRP: ",[51,176,177],{},"ColorPicker"," só sabe selecionar cores, ",[51,180,181],{},"PatternPicker"," só sabe exibir e selecionar padrões, ",[51,184,185],{},"Field"," só sabe envolver um input com label e espaçamento consistente. O ",[51,188,189],{},"index.vue"," orquestra esses blocos nas tabs, mas não conhece os detalhes internos de nenhum deles.",[11,192,193],{},"Os cards são abertos para extensão via props, novos campos, alinhamentos e tamanhos de logo foram adicionados sem modificar a estrutura base, mas fechados para modificação de comportamento externo, já que não expõem estado interno nem emitem eventos. É o princípio aberto\u002Ffechado aplicado a componentes Vue.",[29,195,197,198],{"id":196},"a-store-useeditorstore","A store ",[51,199,100],{},[11,201,202],{},"A store centraliza tudo que pertence ao domínio do cartão:",[159,204,208],{"className":205,"code":206,"language":207,"meta":167,"style":167},"language-ts shiki shiki-themes material-theme-lighter github-dark github-dark","export const useEditorStore = defineStore('editor', () => {\n  const form        = reactive\u003CEditorForm>({ ...DEFAULT_FORM })\n  const logoPreview = ref\u003Cstring | null>(null)\n  const bgImages    = ref\u003CRecord\u003Cstring, string | null>>({ front: null, back: null })\n\n  const cardFrontProps = computed(() => ({ ... }))\n  const cardBackProps  = computed(() => ({ ... }))\n  const isFormValid    = computed(() => form.companyName.trim().length > 0)\n\n  function save()  { \u002F* persiste no localStorage *\u002F }\n  function load()  { \u002F* recupera do localStorage *\u002F }\n  function reset() { \u002F* limpa estado e storage   *\u002F }\n\n  return { form, logoPreview, bgImages, cardFrontProps, cardBackProps, isFormValid, save, load, reset }\n})\n","ts",[51,209,210,262,305,340,399,406,438,467,514,519,540,557,575,580,624],{"__ignoreMap":167},[211,212,215,219,223,227,231,235,239,243,247,249,253,256,259],"span",{"class":213,"line":214},"line",1,[211,216,218],{"class":217},"s3Er8","export",[211,220,222],{"class":221},"sFsEu"," const",[211,224,226],{"class":225},"sVPC0"," useEditorStore",[211,228,230],{"class":229},"sFfmW"," =",[211,232,234],{"class":233},"sK_r7"," defineStore",[211,236,238],{"class":237},"sMo7A","(",[211,240,242],{"class":241},"sF_wb","'",[211,244,246],{"class":245},"s0vBq","editor",[211,248,242],{"class":241},[211,250,252],{"class":251},"sG-J9",",",[211,254,255],{"class":251}," ()",[211,257,258],{"class":221}," =>",[211,260,261],{"class":251}," {\n",[211,263,265,268,271,274,277,280,284,287,290,293,296,299,302],{"class":213,"line":264},2,[211,266,267],{"class":221},"  const",[211,269,270],{"class":225}," form",[211,272,273],{"class":229},"        =",[211,275,276],{"class":233}," reactive",[211,278,279],{"class":251},"\u003C",[211,281,283],{"class":282},"soiBB","EditorForm",[211,285,286],{"class":251},">",[211,288,238],{"class":289},"sdv8B",[211,291,292],{"class":251},"{",[211,294,295],{"class":229}," ...",[211,297,298],{"class":225},"DEFAULT_FORM",[211,300,301],{"class":251}," }",[211,303,304],{"class":289},")\n",[211,306,308,310,313,315,318,320,324,327,330,332,334,338],{"class":213,"line":307},3,[211,309,267],{"class":221},[211,311,312],{"class":225}," logoPreview",[211,314,230],{"class":229},[211,316,317],{"class":233}," ref",[211,319,279],{"class":251},[211,321,323],{"class":322},"s3afY","string",[211,325,326],{"class":229}," |",[211,328,329],{"class":322}," null",[211,331,286],{"class":251},[211,333,238],{"class":289},[211,335,337],{"class":336},"swu5b","null",[211,339,304],{"class":289},[211,341,343,345,348,351,353,355,358,360,362,364,367,369,371,374,376,378,381,384,386,388,391,393,395,397],{"class":213,"line":342},4,[211,344,267],{"class":221},[211,346,347],{"class":225}," bgImages",[211,349,350],{"class":229},"    =",[211,352,317],{"class":233},[211,354,279],{"class":251},[211,356,357],{"class":282},"Record",[211,359,279],{"class":251},[211,361,323],{"class":322},[211,363,252],{"class":251},[211,365,366],{"class":322}," string",[211,368,326],{"class":229},[211,370,329],{"class":322},[211,372,373],{"class":251},">>",[211,375,238],{"class":289},[211,377,292],{"class":251},[211,379,380],{"class":289}," front",[211,382,383],{"class":251},":",[211,385,329],{"class":336},[211,387,252],{"class":251},[211,389,390],{"class":289}," back",[211,392,383],{"class":251},[211,394,329],{"class":336},[211,396,301],{"class":251},[211,398,304],{"class":289},[211,400,402],{"class":213,"line":401},5,[211,403,405],{"emptyLinePlaceholder":404},true,"\n",[211,407,409,411,414,416,419,421,424,426,429,431,433,435],{"class":213,"line":408},6,[211,410,267],{"class":221},[211,412,413],{"class":225}," cardFrontProps",[211,415,230],{"class":229},[211,417,418],{"class":233}," computed",[211,420,238],{"class":289},[211,422,423],{"class":251},"()",[211,425,258],{"class":221},[211,427,428],{"class":289}," (",[211,430,292],{"class":251},[211,432,295],{"class":229},[211,434,301],{"class":251},[211,436,437],{"class":289},"))\n",[211,439,441,443,446,449,451,453,455,457,459,461,463,465],{"class":213,"line":440},7,[211,442,267],{"class":221},[211,444,445],{"class":225}," cardBackProps",[211,447,448],{"class":229},"  =",[211,450,418],{"class":233},[211,452,238],{"class":289},[211,454,423],{"class":251},[211,456,258],{"class":221},[211,458,428],{"class":289},[211,460,292],{"class":251},[211,462,295],{"class":229},[211,464,301],{"class":251},[211,466,437],{"class":289},[211,468,470,472,475,477,479,481,483,485,487,490,493,495,498,500,502,505,508,512],{"class":213,"line":469},8,[211,471,267],{"class":221},[211,473,474],{"class":225}," isFormValid",[211,476,350],{"class":229},[211,478,418],{"class":233},[211,480,238],{"class":289},[211,482,423],{"class":251},[211,484,258],{"class":221},[211,486,270],{"class":237},[211,488,489],{"class":251},".",[211,491,492],{"class":237},"companyName",[211,494,489],{"class":251},[211,496,497],{"class":233},"trim",[211,499,423],{"class":289},[211,501,489],{"class":251},[211,503,504],{"class":225},"length",[211,506,507],{"class":229}," >",[211,509,511],{"class":510},"s_k96"," 0",[211,513,304],{"class":289},[211,515,517],{"class":213,"line":516},9,[211,518,405],{"emptyLinePlaceholder":404},[211,520,522,525,528,530,533,537],{"class":213,"line":521},10,[211,523,524],{"class":221},"  function",[211,526,527],{"class":233}," save",[211,529,423],{"class":251},[211,531,532],{"class":251},"  {",[211,534,536],{"class":535},"sutJx"," \u002F* persiste no localStorage *\u002F",[211,538,539],{"class":251}," }\n",[211,541,543,545,548,550,552,555],{"class":213,"line":542},11,[211,544,524],{"class":221},[211,546,547],{"class":233}," load",[211,549,423],{"class":251},[211,551,532],{"class":251},[211,553,554],{"class":535}," \u002F* recupera do localStorage *\u002F",[211,556,539],{"class":251},[211,558,560,562,565,567,570,573],{"class":213,"line":559},12,[211,561,524],{"class":221},[211,563,564],{"class":233}," reset",[211,566,423],{"class":251},[211,568,569],{"class":251}," {",[211,571,572],{"class":535}," \u002F* limpa estado e storage   *\u002F",[211,574,539],{"class":251},[211,576,578],{"class":213,"line":577},13,[211,579,405],{"emptyLinePlaceholder":404},[211,581,583,586,588,590,592,594,596,598,600,602,604,606,608,610,612,614,616,618,620,622],{"class":213,"line":582},14,[211,584,585],{"class":217},"  return",[211,587,569],{"class":251},[211,589,270],{"class":237},[211,591,252],{"class":251},[211,593,312],{"class":237},[211,595,252],{"class":251},[211,597,347],{"class":237},[211,599,252],{"class":251},[211,601,413],{"class":237},[211,603,252],{"class":251},[211,605,445],{"class":237},[211,607,252],{"class":251},[211,609,474],{"class":237},[211,611,252],{"class":251},[211,613,527],{"class":237},[211,615,252],{"class":251},[211,617,547],{"class":237},[211,619,252],{"class":251},[211,621,564],{"class":237},[211,623,539],{"class":251},[211,625,627,630],{"class":213,"line":626},15,[211,628,629],{"class":251},"}",[211,631,304],{"class":237},[11,633,634,635,141,638,641,642,645,646,649],{},"Os ",[51,636,637],{},"cardFrontProps",[51,639,640],{},"cardBackProps"," são getters computados que encapsulam toda a lógica de composição, incluindo a regra de ",[51,643,644],{},"patternFront"," vs ",[51,647,648],{},"pattern"," para frente e verso, e a intensidade do padrão compartilhada entre os dois lados. A página e os componentes consomem esses getters diretamente.",[11,651,652,653,141,656,659],{},"O estado do formulário e as imagens ficam separados intencionalmente: ",[51,654,655],{},"logoPreview",[51,657,658],{},"bgImages"," armazenam strings base64 que podem ter centenas de KB. Misturá-los com os campos primitivos do formulário criaria um objeto pesado sendo monitorado desnecessariamente.",[11,661,662,663,666,667,670,671,673],{},"A persistência é explícita: ",[51,664,665],{},"save()"," é chamado pelo botão no formulário, e ",[51,668,669],{},"load()"," no ",[51,672,123],{}," da página. O usuário decide quando salvar, o que é mais previsível e evita gravar centenas de KB a cada keystroke.",[29,675,677,678],{"id":676},"o-composable-usecardexport","O composable ",[51,679,109],{},[159,681,683],{"className":205,"code":682,"language":207,"meta":167,"style":167},"export function useCardExport() {\n  const isGenerating = ref(false)\n\n  async function exportPDF(\n    frontEl: HTMLElement,\n    backEl: HTMLElement,\n    fileName: string\n  ) {\n    isGenerating.value = true\n    try {\n      \u002F\u002F captura com html-to-image, monta o PDF com jsPDF, faz o download\n    } finally {\n      isGenerating.value = false\n    }\n  }\n\n  return { isGenerating, exportPDF }\n}\n",[51,684,685,699,718,722,735,749,760,770,777,792,799,804,814,828,833,838,843,858],{"__ignoreMap":167},[211,686,687,689,692,695,697],{"class":213,"line":214},[211,688,218],{"class":217},[211,690,691],{"class":221}," function",[211,693,694],{"class":233}," useCardExport",[211,696,423],{"class":251},[211,698,261],{"class":251},[211,700,701,703,706,708,710,712,716],{"class":213,"line":264},[211,702,267],{"class":221},[211,704,705],{"class":225}," isGenerating",[211,707,230],{"class":229},[211,709,317],{"class":233},[211,711,238],{"class":289},[211,713,715],{"class":714},"sMrrN","false",[211,717,304],{"class":289},[211,719,720],{"class":213,"line":307},[211,721,405],{"emptyLinePlaceholder":404},[211,723,724,727,729,732],{"class":213,"line":342},[211,725,726],{"class":221},"  async",[211,728,691],{"class":221},[211,730,731],{"class":233}," exportPDF",[211,733,734],{"class":251},"(\n",[211,736,737,741,743,746],{"class":213,"line":401},[211,738,740],{"class":739},"sk1zL","    frontEl",[211,742,383],{"class":229},[211,744,745],{"class":282}," HTMLElement",[211,747,748],{"class":251},",\n",[211,750,751,754,756,758],{"class":213,"line":408},[211,752,753],{"class":739},"    backEl",[211,755,383],{"class":229},[211,757,745],{"class":282},[211,759,748],{"class":251},[211,761,762,765,767],{"class":213,"line":440},[211,763,764],{"class":739},"    fileName",[211,766,383],{"class":229},[211,768,769],{"class":322}," string\n",[211,771,772,775],{"class":213,"line":469},[211,773,774],{"class":251},"  )",[211,776,261],{"class":251},[211,778,779,782,784,787,789],{"class":213,"line":516},[211,780,781],{"class":237},"    isGenerating",[211,783,489],{"class":251},[211,785,786],{"class":237},"value",[211,788,230],{"class":229},[211,790,791],{"class":714}," true\n",[211,793,794,797],{"class":213,"line":521},[211,795,796],{"class":217},"    try",[211,798,261],{"class":251},[211,800,801],{"class":213,"line":542},[211,802,803],{"class":535},"      \u002F\u002F captura com html-to-image, monta o PDF com jsPDF, faz o download\n",[211,805,806,809,812],{"class":213,"line":559},[211,807,808],{"class":251},"    }",[211,810,811],{"class":217}," finally",[211,813,261],{"class":251},[211,815,816,819,821,823,825],{"class":213,"line":577},[211,817,818],{"class":237},"      isGenerating",[211,820,489],{"class":251},[211,822,786],{"class":237},[211,824,230],{"class":229},[211,826,827],{"class":714}," false\n",[211,829,830],{"class":213,"line":582},[211,831,832],{"class":251},"    }\n",[211,834,835],{"class":213,"line":626},[211,836,837],{"class":251},"  }\n",[211,839,841],{"class":213,"line":840},16,[211,842,405],{"emptyLinePlaceholder":404},[211,844,846,848,850,852,854,856],{"class":213,"line":845},17,[211,847,585],{"class":217},[211,849,569],{"class":251},[211,851,705],{"class":237},[211,853,252],{"class":251},[211,855,731],{"class":237},[211,857,539],{"class":251},[211,859,861],{"class":213,"line":860},18,[211,862,863],{"class":251},"}\n",[11,865,866,867,870],{},"Não conhece a store, não conhece o formulário. Recebe os elementos do DOM e o nome do arquivo, só isso. O ",[51,868,869],{},"isGenerating"," é exposto para a UI reagir durante a geração.",[29,872,874],{"id":873},"como-a-página-ficou","Como a página ficou",[159,876,878],{"className":205,"code":877,"language":207,"meta":167,"style":167},"const store = useEditorStore()\nconst { isGenerating, exportPDF } = useCardExport()\n\nonMounted(() => store.load())\n\nasync function handleGenerate() {\n  const frontEl = previewRef.value?.cardFrenteRef?.$el as HTMLElement\n  const backEl = previewRef.value?.cardVersoRef?.$el as HTMLElement\n  const fileName = (store.form.companyName || 'cartao')\n    .toLowerCase()\n    .replace(\u002F\\s+\u002Fg, '-')\n  await exportPDF(frontEl, backEl, fileName)\n}\n",[51,879,880,895,915,919,939,943,957,990,1018,1054,1064,1100,1122],{"__ignoreMap":167},[211,881,882,885,888,890,892],{"class":213,"line":214},[211,883,884],{"class":221},"const",[211,886,887],{"class":225}," store",[211,889,230],{"class":229},[211,891,226],{"class":233},[211,893,894],{"class":237},"()\n",[211,896,897,899,901,903,905,907,909,911,913],{"class":213,"line":264},[211,898,884],{"class":221},[211,900,569],{"class":251},[211,902,705],{"class":225},[211,904,252],{"class":251},[211,906,731],{"class":225},[211,908,301],{"class":251},[211,910,230],{"class":229},[211,912,694],{"class":233},[211,914,894],{"class":237},[211,916,917],{"class":213,"line":307},[211,918,405],{"emptyLinePlaceholder":404},[211,920,921,923,925,927,929,931,933,936],{"class":213,"line":342},[211,922,123],{"class":233},[211,924,238],{"class":237},[211,926,423],{"class":251},[211,928,258],{"class":221},[211,930,887],{"class":237},[211,932,489],{"class":251},[211,934,935],{"class":233},"load",[211,937,938],{"class":237},"())\n",[211,940,941],{"class":213,"line":401},[211,942,405],{"emptyLinePlaceholder":404},[211,944,945,948,950,953,955],{"class":213,"line":408},[211,946,947],{"class":221},"async",[211,949,691],{"class":221},[211,951,952],{"class":233}," handleGenerate",[211,954,423],{"class":251},[211,956,261],{"class":251},[211,958,959,961,964,966,969,971,973,976,979,981,984,987],{"class":213,"line":440},[211,960,267],{"class":221},[211,962,963],{"class":225}," frontEl",[211,965,230],{"class":229},[211,967,968],{"class":237}," previewRef",[211,970,489],{"class":251},[211,972,786],{"class":237},[211,974,975],{"class":251},"?.",[211,977,978],{"class":237},"cardFrenteRef",[211,980,975],{"class":251},[211,982,983],{"class":237},"$el",[211,985,986],{"class":217}," as",[211,988,989],{"class":282}," HTMLElement\n",[211,991,992,994,997,999,1001,1003,1005,1007,1010,1012,1014,1016],{"class":213,"line":469},[211,993,267],{"class":221},[211,995,996],{"class":225}," backEl",[211,998,230],{"class":229},[211,1000,968],{"class":237},[211,1002,489],{"class":251},[211,1004,786],{"class":237},[211,1006,975],{"class":251},[211,1008,1009],{"class":237},"cardVersoRef",[211,1011,975],{"class":251},[211,1013,983],{"class":237},[211,1015,986],{"class":217},[211,1017,989],{"class":282},[211,1019,1020,1022,1025,1027,1029,1032,1034,1037,1039,1041,1044,1047,1050,1052],{"class":213,"line":516},[211,1021,267],{"class":221},[211,1023,1024],{"class":225}," fileName",[211,1026,230],{"class":229},[211,1028,428],{"class":289},[211,1030,1031],{"class":237},"store",[211,1033,489],{"class":251},[211,1035,1036],{"class":237},"form",[211,1038,489],{"class":251},[211,1040,492],{"class":237},[211,1042,1043],{"class":229}," ||",[211,1045,1046],{"class":241}," '",[211,1048,1049],{"class":245},"cartao",[211,1051,242],{"class":241},[211,1053,304],{"class":289},[211,1055,1056,1059,1062],{"class":213,"line":521},[211,1057,1058],{"class":251},"    .",[211,1060,1061],{"class":233},"toLowerCase",[211,1063,894],{"class":289},[211,1065,1066,1068,1071,1073,1076,1080,1083,1085,1089,1091,1093,1096,1098],{"class":213,"line":542},[211,1067,1058],{"class":251},[211,1069,1070],{"class":233},"replace",[211,1072,238],{"class":289},[211,1074,1075],{"class":241},"\u002F",[211,1077,1079],{"class":1078},"sSJ72","\\s",[211,1081,1082],{"class":229},"+",[211,1084,1075],{"class":241},[211,1086,1088],{"class":1087},"s1Wpa","g",[211,1090,252],{"class":251},[211,1092,1046],{"class":241},[211,1094,1095],{"class":245},"-",[211,1097,242],{"class":241},[211,1099,304],{"class":289},[211,1101,1102,1105,1107,1109,1112,1114,1116,1118,1120],{"class":213,"line":559},[211,1103,1104],{"class":217},"  await",[211,1106,731],{"class":233},[211,1108,238],{"class":289},[211,1110,1111],{"class":237},"frontEl",[211,1113,252],{"class":251},[211,1115,996],{"class":237},[211,1117,252],{"class":251},[211,1119,1024],{"class":237},[211,1121,304],{"class":289},[211,1123,1124],{"class":213,"line":577},[211,1125,863],{"class":251},[11,1127,1128],{},"A página tem ~40 linhas de lógica. Sem estado de formulário, sem composição de props, sem persistência. Só orquestração.",[29,1130,1132],{"id":1131},"como-o-card-é-construído","Como o card é construído",[11,1134,1135,1137,1138,1140],{},[51,1136,140],{}," recebe props e converte tudo em estilos inline via ",[51,1139,149],{},". O card tem dimensões fixas de 520x296px, que correspondem ao tamanho real de um cartão de visita padrão para gráficas (88.9x50.8mm em 150dpi):",[159,1142,1144],{"className":205,"code":1143,"language":207,"meta":167,"style":167},"const cardStyle = computed(() => ({\n  width: '520px',\n  height: '296px',\n  padding: '28px 34px 26px',\n  background: backgroundColor.value,\n  fontFamily: \"'DM Sans', sans-serif\",\n  position: 'relative',\n  overflow: 'hidden',\n  display: 'flex',\n  flexDirection: 'column',\n  justifyContent: 'space-between',\n  boxSizing: 'border-box'\n}))\n",[51,1145,1146,1168,1184,1200,1216,1232,1250,1266,1282,1298,1314,1330,1345],{"__ignoreMap":167},[211,1147,1148,1150,1153,1155,1157,1159,1161,1163,1165],{"class":213,"line":214},[211,1149,884],{"class":221},[211,1151,1152],{"class":225}," cardStyle",[211,1154,230],{"class":229},[211,1156,418],{"class":233},[211,1158,238],{"class":237},[211,1160,423],{"class":251},[211,1162,258],{"class":221},[211,1164,428],{"class":237},[211,1166,1167],{"class":251},"{\n",[211,1169,1170,1173,1175,1177,1180,1182],{"class":213,"line":264},[211,1171,1172],{"class":289},"  width",[211,1174,383],{"class":251},[211,1176,1046],{"class":241},[211,1178,1179],{"class":245},"520px",[211,1181,242],{"class":241},[211,1183,748],{"class":251},[211,1185,1186,1189,1191,1193,1196,1198],{"class":213,"line":307},[211,1187,1188],{"class":289},"  height",[211,1190,383],{"class":251},[211,1192,1046],{"class":241},[211,1194,1195],{"class":245},"296px",[211,1197,242],{"class":241},[211,1199,748],{"class":251},[211,1201,1202,1205,1207,1209,1212,1214],{"class":213,"line":342},[211,1203,1204],{"class":289},"  padding",[211,1206,383],{"class":251},[211,1208,1046],{"class":241},[211,1210,1211],{"class":245},"28px 34px 26px",[211,1213,242],{"class":241},[211,1215,748],{"class":251},[211,1217,1218,1221,1223,1226,1228,1230],{"class":213,"line":401},[211,1219,1220],{"class":289},"  background",[211,1222,383],{"class":251},[211,1224,1225],{"class":237}," backgroundColor",[211,1227,489],{"class":251},[211,1229,786],{"class":237},[211,1231,748],{"class":251},[211,1233,1234,1237,1239,1242,1245,1248],{"class":213,"line":408},[211,1235,1236],{"class":289},"  fontFamily",[211,1238,383],{"class":251},[211,1240,1241],{"class":241}," \"",[211,1243,1244],{"class":245},"'DM Sans', sans-serif",[211,1246,1247],{"class":241},"\"",[211,1249,748],{"class":251},[211,1251,1252,1255,1257,1259,1262,1264],{"class":213,"line":440},[211,1253,1254],{"class":289},"  position",[211,1256,383],{"class":251},[211,1258,1046],{"class":241},[211,1260,1261],{"class":245},"relative",[211,1263,242],{"class":241},[211,1265,748],{"class":251},[211,1267,1268,1271,1273,1275,1278,1280],{"class":213,"line":469},[211,1269,1270],{"class":289},"  overflow",[211,1272,383],{"class":251},[211,1274,1046],{"class":241},[211,1276,1277],{"class":245},"hidden",[211,1279,242],{"class":241},[211,1281,748],{"class":251},[211,1283,1284,1287,1289,1291,1294,1296],{"class":213,"line":516},[211,1285,1286],{"class":289},"  display",[211,1288,383],{"class":251},[211,1290,1046],{"class":241},[211,1292,1293],{"class":245},"flex",[211,1295,242],{"class":241},[211,1297,748],{"class":251},[211,1299,1300,1303,1305,1307,1310,1312],{"class":213,"line":521},[211,1301,1302],{"class":289},"  flexDirection",[211,1304,383],{"class":251},[211,1306,1046],{"class":241},[211,1308,1309],{"class":245},"column",[211,1311,242],{"class":241},[211,1313,748],{"class":251},[211,1315,1316,1319,1321,1323,1326,1328],{"class":213,"line":542},[211,1317,1318],{"class":289},"  justifyContent",[211,1320,383],{"class":251},[211,1322,1046],{"class":241},[211,1324,1325],{"class":245},"space-between",[211,1327,242],{"class":241},[211,1329,748],{"class":251},[211,1331,1332,1335,1337,1339,1342],{"class":213,"line":559},[211,1333,1334],{"class":289},"  boxSizing",[211,1336,383],{"class":251},[211,1338,1046],{"class":241},[211,1340,1341],{"class":245},"border-box",[211,1343,1344],{"class":241},"'\n",[211,1346,1347,1349],{"class":213,"line":577},[211,1348,629],{"class":251},[211,1350,437],{"class":237},[11,1352,1353],{},"Todos os outros elementos, cores, fontes, espaçamentos, posicionamento, seguem o mesmo padrão: computeds derivados das props.",[11,1355,1356,1359],{},[96,1357,1358],{},"Por que 100% inline e não Tailwind?"," Porque o card serve três contextos diferentes ao mesmo tempo:",[90,1361,1362,1376,1382],{},[93,1363,1364,1367,1368,1371,1372,1375],{},[96,1365,1366],{},"Preview:"," o ",[51,1369,1370],{},"Preview.vue"," escala o card com ",[51,1373,1374],{},"transform: scale()"," para caber na tela sem alterar seus 520x296px reais.",[93,1377,1378,1381],{},[96,1379,1380],{},"Landing page:"," o mesmo componente aparece no exemplo interativo com efeito 3D de perspectiva no mouse, sem adaptação.",[93,1383,1384,1367,1387,1389],{},[96,1385,1386],{},"Exportação:",[51,1388,61],{}," captura o DOM diretamente. Se qualquer estilo viesse de classe Tailwind ou variável CSS externa, a captura poderia falhar silenciosamente ou produzir resultado diferente do preview.",[11,1391,1392],{},"O inline style garante que o que está no DOM é exatamente o que vai para o PDF.",[29,1394,1396],{"id":1395},"alinhamento-e-padrões-geométricos","Alinhamento e padrões geométricos",[11,1398,1399,1400,1403,1404,1403,1407,141,1410,1413,1414,1417,1418,1421],{},"O card suporta 4 opções de alinhamento: ",[51,1401,1402],{},"custom",", ",[51,1405,1406],{},"left",[51,1408,1409],{},"center",[51,1411,1412],{},"right",". Em vez de condicionais espalhadas pelo template, toda a configuração visual de cada opção vive num objeto estático chamado ",[51,1415,1416],{},"ALIGN_MAP",". Cada entrada define o comportamento do cabeçalho, alinhamento do texto, posição dos contatos e se o logo aparece antes ou depois do nome. O template usa apenas o resultado do computed que consulta esse objeto, sem nenhum ",[51,1419,1420],{},"v-if"," de layout. Adicionar um novo alinhamento é só adicionar uma entrada no objeto.",[11,1423,1424],{},"Os padrões geométricos são SVGs gerados por funções que recebem a cor de destaque como parâmetro, garantindo que o padrão sempre combine com as cores escolhidas. A intensidade é controlada por um slider que ajusta a opacidade do SVG no DOM.",[21,1426],{},[24,1428,1430,1431,1433],{"id":1429},"o-problema-com-html2canvas-e-oklch","O problema com ",[51,1432,53],{}," e oklch()",[11,1435,1436,1437,1439,1440,1442],{},"O desafio técnico mais relevante do projeto: o Tailwind v4 gera cores em ",[51,1438,57],{},", e o ",[51,1441,53],{}," simplesmente não suporta esse espaço de cor. O resultado era um card completamente branco no PDF.",[11,1444,1445,1446,1448,1449,383],{},"A solução foi trocar para ",[51,1447,61],{}," com ",[51,1450,1451],{},"skipFonts: true",[159,1453,1455],{"className":205,"code":1454,"language":207,"meta":167,"style":167},"const [pngFront, pngBack] = await Promise.all([\n  toPng(frontEl, { pixelRatio: 3, skipFonts: true }),\n  toPng(backEl, { pixelRatio: 3, skipFonts: true })\n])\n",[51,1456,1457,1491,1528,1557],{"__ignoreMap":167},[211,1458,1459,1461,1464,1467,1469,1472,1475,1477,1480,1483,1485,1488],{"class":213,"line":214},[211,1460,884],{"class":221},[211,1462,1463],{"class":251}," [",[211,1465,1466],{"class":225},"pngFront",[211,1468,252],{"class":251},[211,1470,1471],{"class":225}," pngBack",[211,1473,1474],{"class":251},"]",[211,1476,230],{"class":229},[211,1478,1479],{"class":217}," await",[211,1481,1482],{"class":322}," Promise",[211,1484,489],{"class":251},[211,1486,1487],{"class":233},"all",[211,1489,1490],{"class":237},"([\n",[211,1492,1493,1496,1499,1501,1503,1506,1508,1511,1513,1516,1518,1521,1523,1526],{"class":213,"line":264},[211,1494,1495],{"class":233},"  toPng",[211,1497,1498],{"class":237},"(frontEl",[211,1500,252],{"class":251},[211,1502,569],{"class":251},[211,1504,1505],{"class":289}," pixelRatio",[211,1507,383],{"class":251},[211,1509,1510],{"class":510}," 3",[211,1512,252],{"class":251},[211,1514,1515],{"class":289}," skipFonts",[211,1517,383],{"class":251},[211,1519,1520],{"class":714}," true",[211,1522,301],{"class":251},[211,1524,1525],{"class":237},")",[211,1527,748],{"class":251},[211,1529,1530,1532,1535,1537,1539,1541,1543,1545,1547,1549,1551,1553,1555],{"class":213,"line":307},[211,1531,1495],{"class":233},[211,1533,1534],{"class":237},"(backEl",[211,1536,252],{"class":251},[211,1538,569],{"class":251},[211,1540,1505],{"class":289},[211,1542,383],{"class":251},[211,1544,1510],{"class":510},[211,1546,252],{"class":251},[211,1548,1515],{"class":289},[211,1550,383],{"class":251},[211,1552,1520],{"class":714},[211,1554,301],{"class":251},[211,1556,304],{"class":237},[211,1558,1559],{"class":213,"line":342},[211,1560,1561],{"class":237},"])\n",[11,1563,49,1564,1566,1567,1569],{},[51,1565,1451],{}," é necessário porque o ",[51,1568,61],{}," tenta serializar as fontes externas (Google Fonts) e falha silenciosamente em alguns navegadores. Com a flag, ele ignora a tentativa e usa o que já está renderizado no browser, o resultado visual é idêntico.",[21,1571],{},[24,1573,1575],{"id":1574},"geração-do-pdf","Geração do PDF",[11,1577,1578],{},"O PDF final tem duas páginas em formato 88.9x50.8mm (tamanho padrão para cartões de visita em gráficas brasileiras):",[159,1580,1582],{"className":205,"code":1581,"language":207,"meta":167,"style":167},"const pdf = new jsPDF({\n  orientation: 'landscape',\n  unit: 'mm',\n  format: [88.9, 50.8]\n})\n\npdf.addImage(pngFront, 'PNG', 0, 0, 88.9, 50.8)\npdf.addPage()\npdf.addImage(pngBack, 'PNG', 0, 0, 88.9, 50.8)\n",[51,1583,1584,1603,1619,1635,1655,1661,1665,1706,1717],{"__ignoreMap":167},[211,1585,1586,1588,1591,1593,1596,1599,1601],{"class":213,"line":214},[211,1587,884],{"class":221},[211,1589,1590],{"class":225}," pdf",[211,1592,230],{"class":229},[211,1594,1595],{"class":229}," new",[211,1597,1598],{"class":233}," jsPDF",[211,1600,238],{"class":237},[211,1602,1167],{"class":251},[211,1604,1605,1608,1610,1612,1615,1617],{"class":213,"line":264},[211,1606,1607],{"class":289},"  orientation",[211,1609,383],{"class":251},[211,1611,1046],{"class":241},[211,1613,1614],{"class":245},"landscape",[211,1616,242],{"class":241},[211,1618,748],{"class":251},[211,1620,1621,1624,1626,1628,1631,1633],{"class":213,"line":307},[211,1622,1623],{"class":289},"  unit",[211,1625,383],{"class":251},[211,1627,1046],{"class":241},[211,1629,1630],{"class":245},"mm",[211,1632,242],{"class":241},[211,1634,748],{"class":251},[211,1636,1637,1640,1642,1644,1647,1649,1652],{"class":213,"line":342},[211,1638,1639],{"class":289},"  format",[211,1641,383],{"class":251},[211,1643,1463],{"class":237},[211,1645,1646],{"class":510},"88.9",[211,1648,252],{"class":251},[211,1650,1651],{"class":510}," 50.8",[211,1653,1654],{"class":237},"]\n",[211,1656,1657,1659],{"class":213,"line":401},[211,1658,629],{"class":251},[211,1660,304],{"class":237},[211,1662,1663],{"class":213,"line":408},[211,1664,405],{"emptyLinePlaceholder":404},[211,1666,1667,1670,1672,1675,1678,1680,1682,1685,1687,1689,1691,1693,1695,1697,1700,1702,1704],{"class":213,"line":440},[211,1668,1669],{"class":237},"pdf",[211,1671,489],{"class":251},[211,1673,1674],{"class":233},"addImage",[211,1676,1677],{"class":237},"(pngFront",[211,1679,252],{"class":251},[211,1681,1046],{"class":241},[211,1683,1684],{"class":245},"PNG",[211,1686,242],{"class":241},[211,1688,252],{"class":251},[211,1690,511],{"class":510},[211,1692,252],{"class":251},[211,1694,511],{"class":510},[211,1696,252],{"class":251},[211,1698,1699],{"class":510}," 88.9",[211,1701,252],{"class":251},[211,1703,1651],{"class":510},[211,1705,304],{"class":237},[211,1707,1708,1710,1712,1715],{"class":213,"line":469},[211,1709,1669],{"class":237},[211,1711,489],{"class":251},[211,1713,1714],{"class":233},"addPage",[211,1716,894],{"class":237},[211,1718,1719,1721,1723,1725,1728,1730,1732,1734,1736,1738,1740,1742,1744,1746,1748,1750,1752],{"class":213,"line":516},[211,1720,1669],{"class":237},[211,1722,489],{"class":251},[211,1724,1674],{"class":233},[211,1726,1727],{"class":237},"(pngBack",[211,1729,252],{"class":251},[211,1731,1046],{"class":241},[211,1733,1684],{"class":245},[211,1735,242],{"class":241},[211,1737,252],{"class":251},[211,1739,511],{"class":510},[211,1741,252],{"class":251},[211,1743,511],{"class":510},[211,1745,252],{"class":251},[211,1747,1699],{"class":510},[211,1749,252],{"class":251},[211,1751,1651],{"class":510},[211,1753,304],{"class":237},[11,1755,1756,1757,1760],{},"Um detalhe importante: antes da captura, os dois cards precisam estar visíveis no DOM mesmo que apenas um esteja sendo exibido ao usuário. A solução foi forçar ",[51,1758,1759],{},"display: flex\u002Fblock"," temporariamente, capturar, e restaurar o estado original:",[159,1762,1764],{"className":205,"code":1763,"language":207,"meta":167,"style":167},"const prevFront = frontEl.style.display\nfrontEl.style.display = 'flex'\n\u002F\u002F captura...\nfrontEl.style.display = prevFront\n",[51,1765,1766,1787,1809,1814],{"__ignoreMap":167},[211,1767,1768,1770,1773,1775,1777,1779,1782,1784],{"class":213,"line":214},[211,1769,884],{"class":221},[211,1771,1772],{"class":225}," prevFront",[211,1774,230],{"class":229},[211,1776,963],{"class":237},[211,1778,489],{"class":251},[211,1780,1781],{"class":237},"style",[211,1783,489],{"class":251},[211,1785,1786],{"class":237},"display\n",[211,1788,1789,1791,1793,1795,1797,1800,1803,1805,1807],{"class":213,"line":264},[211,1790,1111],{"class":237},[211,1792,489],{"class":251},[211,1794,1781],{"class":237},[211,1796,489],{"class":251},[211,1798,1799],{"class":237},"display ",[211,1801,1802],{"class":229},"=",[211,1804,1046],{"class":241},[211,1806,1293],{"class":245},[211,1808,1344],{"class":241},[211,1810,1811],{"class":213,"line":307},[211,1812,1813],{"class":535},"\u002F\u002F captura...\n",[211,1815,1816,1818,1820,1822,1824,1826,1828],{"class":213,"line":342},[211,1817,1111],{"class":237},[211,1819,489],{"class":251},[211,1821,1781],{"class":237},[211,1823,489],{"class":251},[211,1825,1799],{"class":237},[211,1827,1802],{"class":229},[211,1829,1830],{"class":237}," prevFront\n",[21,1832],{},[24,1834,1836],{"id":1835},"ícones-de-contato-como-svg-inline","Ícones de contato como SVG inline",[11,1838,1839,1840,383],{},"Os ícones de telefone, e-mail, site e endereço no card são SVGs inline gerados por ",[51,1841,149],{},[159,1843,1845],{"className":205,"code":1844,"language":207,"meta":167,"style":167},"const icon = (path: string) =>\n  computed(\n    () =>\n      `\u003Csvg width=\"11\" height=\"11\" viewBox=\"0 0 24 24\" fill=\"none\"\n      stroke=\"${accentColor.value}\" stroke-width=\"2\">${path}\u003C\u002Fsvg>`\n  )\n\nconst phoneIcon = icon(`\u003Cpath d=\"M22 16.92v3...\"\u002F>`)\n",[51,1846,1847,1871,1878,1885,1893,1925,1930,1934],{"__ignoreMap":167},[211,1848,1849,1851,1855,1857,1859,1862,1864,1866,1868],{"class":213,"line":214},[211,1850,884],{"class":221},[211,1852,1854],{"class":1853},"sqHAQ"," icon",[211,1856,230],{"class":229},[211,1858,428],{"class":251},[211,1860,1861],{"class":739},"path",[211,1863,383],{"class":229},[211,1865,366],{"class":322},[211,1867,1525],{"class":251},[211,1869,1870],{"class":221}," =>\n",[211,1872,1873,1876],{"class":213,"line":264},[211,1874,1875],{"class":233},"  computed",[211,1877,734],{"class":237},[211,1879,1880,1883],{"class":213,"line":307},[211,1881,1882],{"class":251},"    ()",[211,1884,1870],{"class":221},[211,1886,1887,1890],{"class":213,"line":342},[211,1888,1889],{"class":241},"      `",[211,1891,1892],{"class":245},"\u003Csvg width=\"11\" height=\"11\" viewBox=\"0 0 24 24\" fill=\"none\"\n",[211,1894,1895,1898,1901,1904,1906,1908,1910,1913,1915,1917,1919,1922],{"class":213,"line":401},[211,1896,1897],{"class":245},"      stroke=\"",[211,1899,1900],{"class":241},"${",[211,1902,1903],{"class":237},"accentColor",[211,1905,489],{"class":241},[211,1907,786],{"class":237},[211,1909,629],{"class":241},[211,1911,1912],{"class":245},"\" stroke-width=\"2\">",[211,1914,1900],{"class":241},[211,1916,1861],{"class":237},[211,1918,629],{"class":241},[211,1920,1921],{"class":245},"\u003C\u002Fsvg>",[211,1923,1924],{"class":241},"`\n",[211,1926,1927],{"class":213,"line":408},[211,1928,1929],{"class":237},"  )\n",[211,1931,1932],{"class":213,"line":440},[211,1933,405],{"emptyLinePlaceholder":404},[211,1935,1936,1938,1941,1943,1945,1947,1950,1953,1955],{"class":213,"line":469},[211,1937,884],{"class":221},[211,1939,1940],{"class":225}," phoneIcon",[211,1942,230],{"class":229},[211,1944,1854],{"class":233},[211,1946,238],{"class":237},[211,1948,1949],{"class":241},"`",[211,1951,1952],{"class":245},"\u003Cpath d=\"M22 16.92v3...\"\u002F>",[211,1954,1949],{"class":241},[211,1956,304],{"class":237},[11,1958,1959,1960,1963,1964,1966,1967,1970,1971,1973,1974,489],{},"A cor do ícone (",[51,1961,1962],{},"stroke",") acompanha automaticamente a ",[51,1965,1903],{},". Por ser SVG inline via ",[51,1968,1969],{},"v-html",", é capturado perfeitamente pelo ",[51,1972,61],{},", diferente de fontes de ícones externas como MDI, que seriam ignoradas com ",[51,1975,1451],{},[21,1977],{},[24,1979,1981],{"id":1980},"seo-e-og-image","SEO e OG Image",[11,1983,1984,1985,383],{},"Para o OG Image customizado usei o sistema de componentes do ",[51,1986,1987],{},"@nuxtjs\u002Fseo",[159,1989,1991],{"className":205,"code":1990,"language":207,"meta":167,"style":167},"defineOgImageComponent('OgImageVisitCardOg', {\n  title: 'Editor de Cartão',\n  description: 'Personalize cores, logo e padrões...'\n})\n",[51,1992,1993,2011,2027,2041],{"__ignoreMap":167},[211,1994,1995,1998,2000,2002,2005,2007,2009],{"class":213,"line":214},[211,1996,1997],{"class":233},"defineOgImageComponent",[211,1999,238],{"class":237},[211,2001,242],{"class":241},[211,2003,2004],{"class":245},"OgImageVisitCardOg",[211,2006,242],{"class":241},[211,2008,252],{"class":251},[211,2010,261],{"class":251},[211,2012,2013,2016,2018,2020,2023,2025],{"class":213,"line":264},[211,2014,2015],{"class":289},"  title",[211,2017,383],{"class":251},[211,2019,1046],{"class":241},[211,2021,2022],{"class":245},"Editor de Cartão",[211,2024,242],{"class":241},[211,2026,748],{"class":251},[211,2028,2029,2032,2034,2036,2039],{"class":213,"line":307},[211,2030,2031],{"class":289},"  description",[211,2033,383],{"class":251},[211,2035,1046],{"class":241},[211,2037,2038],{"class":245},"Personalize cores, logo e padrões...",[211,2040,1344],{"class":241},[211,2042,2043,2045],{"class":213,"line":342},[211,2044,629],{"class":251},[211,2046,304],{"class":237},[11,2048,2049,2050,2057],{},"Um aprendizado: o LinkedIn cacheia OG Images de forma agressiva. Para forçar a atualização após deploy, é preciso usar o ",[2051,2052,2056],"a",{"href":2053,"rel":2054},"https:\u002F\u002Fwww.linkedin.com\u002Fpost-inspector\u002F",[2055],"nofollow","LinkedIn Post Inspector"," para invalidar o cache manualmente.",[21,2059],{},[24,2061,2063],{"id":2062},"layouts-no-nuxt","Layouts no Nuxt",[11,2065,2066,2067,383],{},"O projeto tem duas páginas com layouts completamente diferentes: landing page e editor. No Nuxt, isso é resolvido com ",[51,2068,2069],{},"definePageMeta",[159,2071,2073],{"className":205,"code":2072,"language":207,"meta":167,"style":167},"definePageMeta({ layout: 'editor-layout' })\ndefinePageMeta({ layout: 'land-page-layout' })\n",[51,2074,2075,2099],{"__ignoreMap":167},[211,2076,2077,2079,2081,2083,2086,2088,2090,2093,2095,2097],{"class":213,"line":214},[211,2078,2069],{"class":233},[211,2080,238],{"class":237},[211,2082,292],{"class":251},[211,2084,2085],{"class":289}," layout",[211,2087,383],{"class":251},[211,2089,1046],{"class":241},[211,2091,2092],{"class":245},"editor-layout",[211,2094,242],{"class":241},[211,2096,301],{"class":251},[211,2098,304],{"class":237},[211,2100,2101,2103,2105,2107,2109,2111,2113,2116,2118,2120],{"class":213,"line":264},[211,2102,2069],{"class":233},[211,2104,238],{"class":237},[211,2106,292],{"class":251},[211,2108,2085],{"class":289},[211,2110,383],{"class":251},[211,2112,1046],{"class":241},[211,2114,2115],{"class":245},"land-page-layout",[211,2117,242],{"class":241},[211,2119,301],{"class":251},[211,2121,304],{"class":237},[11,2123,2124,2125,2128],{},"É o equivalente ao ",[51,2126,2127],{},"meta: {}"," do Vue Router convencional, sem necessidade de middleware ou lógica adicional.",[21,2130],{},[24,2132,2134],{"id":2133},"conclusão","Conclusão",[11,2136,2137,2138,2141],{},"O VisitCardGenerator cresceu além do escopo original à medida que eu resolvia problemas reais: captura de DOM com cores ",[51,2139,2140],{},"oklch",", geração de PDF em tamanho exato para gráfica, alinhamento flexível do layout, responsividade do editor, gerenciamento de estado com Pinia.",[11,2143,2144],{},"O resultado é uma ferramenta que eu mesma usaria, e que qualquer pessoa pode usar, sem cadastro e sem pagar nada.",[11,2146,2147,2148,489],{},"O projeto está no ar em ",[2051,2149,2152],{"href":2150,"rel":2151},"https:\u002F\u002Fvisitcard-www.larisantos.com.br\u002F",[2055],"visitcard-www.larisantos.com.br",[11,2154,2155],{},"Se você tiver dúvidas sobre alguma das decisões técnicas ou quiser trocar ideia sobre o projeto, me chama!",[1781,2157,2158],{},"html pre.shiki code .s3Er8, html code.shiki .s3Er8{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#F97583;--shiki-default-font-style:inherit;--shiki-dark:#F97583;--shiki-dark-font-style:inherit}html pre.shiki code .sFsEu, html code.shiki .sFsEu{--shiki-light:#9C3EDA;--shiki-default:#F97583;--shiki-dark:#F97583}html pre.shiki code .sVPC0, html code.shiki .sVPC0{--shiki-light:#90A4AE;--shiki-default:#79B8FF;--shiki-dark:#79B8FF}html pre.shiki code .sFfmW, html code.shiki .sFfmW{--shiki-light:#39ADB5;--shiki-default:#F97583;--shiki-dark:#F97583}html pre.shiki code .sK_r7, html code.shiki .sK_r7{--shiki-light:#6182B8;--shiki-default:#B392F0;--shiki-dark:#B392F0}html pre.shiki code .sMo7A, html code.shiki .sMo7A{--shiki-light:#90A4AE;--shiki-default:#E1E4E8;--shiki-dark:#E1E4E8}html pre.shiki code .sF_wb, html code.shiki .sF_wb{--shiki-light:#39ADB5;--shiki-default:#9ECBFF;--shiki-dark:#9ECBFF}html pre.shiki code .s0vBq, html code.shiki .s0vBq{--shiki-light:#91B859;--shiki-default:#9ECBFF;--shiki-dark:#9ECBFF}html pre.shiki code .sG-J9, html code.shiki .sG-J9{--shiki-light:#39ADB5;--shiki-default:#E1E4E8;--shiki-dark:#E1E4E8}html pre.shiki code .soiBB, html code.shiki .soiBB{--shiki-light:#E2931D;--shiki-default:#B392F0;--shiki-dark:#B392F0}html pre.shiki code .sdv8B, html code.shiki .sdv8B{--shiki-light:#E53935;--shiki-default:#E1E4E8;--shiki-dark:#E1E4E8}html pre.shiki code .s3afY, html code.shiki .s3afY{--shiki-light:#E2931D;--shiki-default:#79B8FF;--shiki-dark:#79B8FF}html pre.shiki code .swu5b, html code.shiki .swu5b{--shiki-light:#39ADB5;--shiki-default:#79B8FF;--shiki-dark:#79B8FF}html pre.shiki code .s_k96, html code.shiki .s_k96{--shiki-light:#F76D47;--shiki-default:#79B8FF;--shiki-dark:#79B8FF}html pre.shiki code .sutJx, html code.shiki .sutJx{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#6A737D;--shiki-default-font-style:inherit;--shiki-dark:#6A737D;--shiki-dark-font-style:inherit}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sMrrN, html code.shiki .sMrrN{--shiki-light:#FF5370;--shiki-default:#79B8FF;--shiki-dark:#79B8FF}html pre.shiki code .sk1zL, html code.shiki .sk1zL{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#FFAB70;--shiki-default-font-style:inherit;--shiki-dark:#FFAB70;--shiki-dark-font-style:inherit}html pre.shiki code .sSJ72, html code.shiki .sSJ72{--shiki-light:#91B859;--shiki-default:#79B8FF;--shiki-dark:#79B8FF}html pre.shiki code .s1Wpa, html code.shiki .s1Wpa{--shiki-light:#F76D47;--shiki-default:#F97583;--shiki-dark:#F97583}html pre.shiki code .sqHAQ, html code.shiki .sqHAQ{--shiki-light:#90A4AE;--shiki-default:#B392F0;--shiki-dark:#B392F0}",{"title":167,"searchDepth":264,"depth":264,"links":2160},[2161,2168,2178,2180,2181,2182,2183,2184],{"id":26,"depth":264,"text":27,"children":2162},[2163,2164,2165,2166,2167],{"id":31,"depth":307,"text":32},{"id":38,"depth":307,"text":39},{"id":45,"depth":307,"text":46},{"id":65,"depth":307,"text":66},{"id":72,"depth":307,"text":73},{"id":81,"depth":264,"text":82,"children":2169},[2170,2171,2173,2175,2176,2177],{"id":156,"depth":307,"text":157},{"id":196,"depth":307,"text":2172},"A store useEditorStore",{"id":676,"depth":307,"text":2174},"O composable useCardExport",{"id":873,"depth":307,"text":874},{"id":1131,"depth":307,"text":1132},{"id":1395,"depth":307,"text":1396},{"id":1429,"depth":264,"text":2179},"O problema com html2canvas e oklch()",{"id":1574,"depth":264,"text":1575},{"id":1835,"depth":264,"text":1836},{"id":1980,"depth":264,"text":1981},{"id":2062,"depth":264,"text":2063},{"id":2133,"depth":264,"text":2134},"2026-03-15T08:15:00-03:00","Como construí um gerador de cartões de visita profissionais em PDF com Nuxt 3, Vue 3, jsPDF e html-to-image — sem cadastro, sem custo, sem complicação. Detalhes das decisões técnicas, desafios e aprendizados.","md",null,"\u002Fimages\u002Fblog\u002Fvisitcard-editor.png",{},"\u002Fblog\u002Fvisitcard-generator-nuxt-pdf-do-zero",{"title":5,"description":2186},"blog\u002Fvisitcard-generator-nuxt-pdf-do-zero",[2195,2196,2197,72,2198],"nuxt","vue-js","javascript","frontend","PuW_mg2I6zu3LKMUfMYi_Blmz3syoLWg0EHxu1WngiQ",1783037053161]