Criando sites dinâmico com Markdown e JSON usando JavaScript e markedjs

Introdução
Neste tutorial, vamos aprender a manipular arquivos JSON e a renderizar o conteúdo de arquivos Markdown utilizando JavaScript, HTML, CSS e a biblioteca marked.js. Abordaremos a definição das constantes, a requisição do arquivo JSON, a exibição dos dados no HTML e a captura de cliques nos links para renderizar o conteúdo Markdown. Em resumo, vamos criar um site dinâmico que exibe arquivos Markdown a partir de uma lista em JSON, permitindo a visualização do conteúdo Markdown ao clicar nos links. Utilizaremos a biblioteca marked.js para converter o conteúdo Markdown em HTML, detalhando cada parte do código e suas funcionalidades.
Estrutura do Projeto
Teremos dois componentes principais:
- Um arquivo JSON contendo uma lista de arquivos markdown, com título, descrição resumida, data de publicação e endereço do arquivo.
- Lista de Arquivos markdown que serão exibidos no site.
Explicação do Código
1. Definição da constante do caminho do arquivo
const arquivos = "../pasta/arquivos.json";
Aqui, a variável arquivos armazena o caminho relativo de um arquivo arquivos.json, que será usado para carregar as informações.
2. fetch do arquivo JSON
fetch(arquivos)
.then((response) => response.text())
.then((resultado) => listar(resultado))
.catch((error) => console.error(error));
Fluxo de execução:
fetch(arquivos): Faz a requisição HTTP para obter o arquivoarquivos.json.response.text(): Extrai o conteúdo do arquivo JSON como uma string de texto.listar(resultado): Chama a funçãolistar, passando o conteúdo do arquivo (o JSON em formato de string) como argumento.- Tratamento de erro (
catch): Caso ocorra algum erro na requisição, ele será exibido no console do navegador.
3. Função listar(markdown)
function listar(markdown) {
const content = document.querySelector("#conteudo");
const lista = JSON.parse(markdown);
lista.forEach((item) => {
const container = document.createElement("div");
const titulo = document.createElement("h4");
titulo.textContent = item.titulo;
const descricao = document.createElement("p");
descricao.textContent = item.descricao;
const data = document.createElement("p");
data.textContent = "Publicado em: " + item.data;
const link = document.createElement("a");
link.href = obterCaminhoCompleto(item.link);
link.textContent = link.href;
container.append(titulo, descricao, data, link);
content.append(container, document.createElement("hr"));
});
}
Fluxo de execução:
- Seleciona o elemento com id
conteudo:const content = document.querySelector("#conteudo");Este será o local onde o conteúdo do JSON será exibido. - Converte o JSON em objeto:
const lista = JSON.parse(markdown);O conteúdo do JSON é convertido de texto para um array de objetos. - Itera sobre o array
listae, para cada item, cria os elementos de título, descrição, data e link comdocument.createElement, preenchendo cada um com.textContentantes de anexá-los ao container.
Por que createElement + textContent, em vez de concatenar innerHTML? Uma implementação comum desse tipo de listagem monta o HTML assim:
// Evite este padrão:
content.innerHTML += "<h4>" + item.titulo + "</h4>";
Isso funciona enquanto os dados do JSON forem 100% confiáveis, mas é uma prática arriscada: se titulo ou descricao algum dia vierem de uma fonte que você não controla totalmente — um formulário, um CMS, uma API de terceiros — qualquer HTML ou <script> embutido no texto seria interpretado e executado pelo navegador. Isso é uma vulnerabilidade conhecida como XSS (Cross-Site Scripting). Usar textContent garante que o conteúdo seja sempre tratado como texto puro, nunca como marcação, eliminando esse risco por padrão — mesmo que hoje o arquivos.json seja escrito à mão por você.
Como efeito colateral positivo, append também é mais rápido que innerHTML += em listas longas, porque evita que o navegador reprocesse todo o HTML já inserido a cada iteração do laço.
4. Função obterCaminhoCompleto(caminhoRelativo)
const obterCaminhoCompleto = (caminhoRelativo) => {
const caminhoPaginaAtual = window.location.href;
return new URL(caminhoRelativo, caminhoPaginaAtual).href;
};
Explicação:
- Recebe um caminho relativo (
caminhoRelativo) e converte para caminho absoluto. window.location.href: Captura a URL da página atual.new URL(caminhoRelativo, caminhoPaginaAtual): Cria um objeto URL combinando o caminho base com o caminho relativo..href: Obtém a URL completa como string.
Exemplo de uso: Se a URL atual for https://meusite.com/pagina/ e o caminho relativo for ../conteudo/arquivo.md, a URL absoluta será:
https://meusite.com/conteudo/arquivo.md
5. Função mostrarMark(markdown)
function mostrarMark(markdown) {
const visual = document.querySelector("#markdown");
fetch(obterCaminhoCompleto(markdown))
.then((response) => response.text())
.then((resultado) => visual.innerHTML = marked.parse(resultado))
.catch((error) => console.error(error));
}
Fluxo de execução:
- Seleciona o elemento
#markdown:const visual = document.querySelector("#markdown"); - Faz um
fetchpara buscar o arquivo markdown:fetch(obterCaminhoCompleto(markdown)) - Extrai o conteúdo como texto:
.then((response) => response.text()) - Converte o Markdown em HTML e exibe:
.then((resultado) => visual.innerHTML = marked.parse(resultado))
Nota: aqui usamos innerHTML de propósito, diferente da função listar: o objetivo de marked.parse() é justamente transformar Markdown em marcação HTML (títulos, listas, links, negrito) para ser renderizada. Se os arquivos .md forem escritos só por você, o risco de XSS é baixo. Caso um dia esses arquivos venham de terceiros ou de conteúdo enviado por usuários, considere sanitizar o HTML resultante com uma biblioteca como DOMPurify antes de atribuí-lo a innerHTML. Certifique-se também de incluir a biblioteca marked.js no seu projeto para que o método marked.parse() funcione corretamente.
6. Captura de cliques nos links
document.addEventListener('click', function (event) {
if (event.target.tagName === 'A' && event.target.closest('#conteudo')) {
event.preventDefault();
mostrarMark(event.target.getAttribute('href'));
}
});
Fluxo de execução:
- Captura qualquer clique no documento:
document.addEventListener('click', function (event) { ... }) - Verifica se o clique foi em um link (
A) e se está dentro do#conteudo:if (event.target.tagName === 'A' && event.target.closest('#conteudo')) - Impede o comportamento padrão do link (navegação de página):
event.preventDefault(); - Chama a função
mostrarMark()para renderizar o conteúdo markdown:mostrarMark(event.target.getAttribute('href'));
Esse padrão — capturar o clique no elemento pai (document) em vez de em cada link individualmente — é chamado de delegação de eventos. Ele tem uma vantagem prática importante aqui: como os links são criados dinamicamente pela função listar(), um listener adicionado diretamente a cada <a> no momento da criação funcionaria, mas exigiria reanexar o listener sempre que a lista fosse atualizada. Delegando o clique ao document, um único listener cobre todos os links, inclusive os que ainda nem existem no momento em que o código é executado.
Exemplo de arquivos.json
Para melhor entendimento, veja um possível conteúdo do arquivo arquivos.json:
[
{
"titulo": "Primeiro arquivo",
"descricao": "Uma breve descrição do primeiro arquivo.",
"data": "2024-01-01",
"link": "arquivos/primeiro.md"
},
{
"titulo": "Segundo arquivo",
"descricao": "Uma breve descrição do segundo arquivo.",
"data": "2024-02-01",
"link": "arquivos/segundo.md"
}
]
Exemplo de saída no HTML
<div id="conteudo">
<div>
<h4>Primeiro arquivo</h4>
<p>Uma breve descrição do primeiro arquivo.</p>
<p>Publicado em: 2024-01-01</p>
<a href="https://meusite.com/arquivos/primeiro.md">https://meusite.com/arquivos/primeiro.md</a>
</div>
<hr>
<div>
<h4>Segundo arquivo</h4>
<p>Uma breve descrição do segundo arquivo.</p>
<p>Publicado em: 2024-02-01</p>
<a href="https://meusite.com/arquivos/segundo.md">https://meusite.com/arquivos/segundo.md</a>
</div>
<hr>
</div>
Erros comuns
- CORS ao abrir o HTML direto pelo navegador (
file://):fetch()para arquivos locais só funciona de forma confiável quando servido por um servidor HTTP (mesmo que local, comonpx serveou a extensão Live Server do VS Code). Abrir o arquivoindex.htmldiretamente no navegador costuma gerar erro de CORS ao tentar carregar o JSON ou o Markdown. - Links fora de
#conteudosendo interceptados: se seu site tiver outros links na página (menu, rodapé), a checagemevent.target.closest('#conteudo')é o que evita que cliques nesses links também sejam capturados pela lógica de Markdown.
Conclusão
Neste tutorial, aprendemos a listar arquivos a partir de um JSON, exibi-los de forma segura no DOM (evitando innerHTML para dados não confiáveis) e a renderizar o conteúdo Markdown correspondente ao clicar em um link, usando delegação de eventos e a biblioteca marked.js. Esse padrão — dados em JSON, conteúdo em Markdown, renderização sob demanda — é a base de geradores de blog estáticos mais simples, e um bom exercício para entender manipulação de DOM sem depender de frameworks.
Comentários
Não foi possível carregar os comentários no momento.
Deixe seu comentário