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

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:

  1. Um arquivo JSON contendo uma lista de arquivos markdown, com título, descrição resumida, data de publicação e endereço do arquivo.
  2. 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:

  1. fetch(arquivos): Faz a requisição HTTP para obter o arquivo arquivos.json.
  2. response.text(): Extrai o conteúdo do arquivo JSON como uma string de texto.
  3. listar(resultado): Chama a função listar, passando o conteúdo do arquivo (o JSON em formato de string) como argumento.
  4. 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:

  1. Seleciona o elemento com id conteudo: const content = document.querySelector("#conteudo"); Este será o local onde o conteúdo do JSON será exibido.
  2. Converte o JSON em objeto: const lista = JSON.parse(markdown); O conteúdo do JSON é convertido de texto para um array de objetos.
  3. Itera sobre o array lista e, para cada item, cria os elementos de título, descrição, data e link com document.createElement, preenchendo cada um com .textContent antes 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:

  1. Seleciona o elemento #markdown: const visual = document.querySelector("#markdown");
  2. Faz um fetch para buscar o arquivo markdown: fetch(obterCaminhoCompleto(markdown))
  3. Extrai o conteúdo como texto: .then((response) => response.text())
  4. 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.

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:

  1. Captura qualquer clique no documento: document.addEventListener('click', function (event) { ... })
  2. Verifica se o clique foi em um link (A) e se está dentro do #conteudo: if (event.target.tagName === 'A' && event.target.closest('#conteudo'))
  3. Impede o comportamento padrão do link (navegação de página): event.preventDefault();
  4. 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, como npx serve ou a extensão Live Server do VS Code). Abrir o arquivo index.html diretamente no navegador costuma gerar erro de CORS ao tentar carregar o JSON ou o Markdown.
  • Links fora de #conteudo sendo interceptados: se seu site tiver outros links na página (menu, rodapé), a checagem event.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