(ajustes): ajustes da última refatoracao. Exclusao de coisas que nao se usa mais.

This commit is contained in:
Rafael Tavares Juliani 2025-12-21 21:20:54 -03:00
parent c64609a233
commit 2b8862c5cd
7 changed files with 133 additions and 202 deletions

2
.gitignore vendored
View File

@ -1,3 +1 @@
.env .env
pasta_publica/analytics/*
pasta_publica/!analytics/README.md

View File

@ -3,16 +3,26 @@
## O que você vai encontrar aqui? ## O que você vai encontrar aqui?
### As instruções a seguir são para o prefixo eM
Aqui, você encontrará: Aqui, você encontrará:
- 2 arquivos (ark_lista.yml e ark_lista.json) contendo uma lista de todos os identificadores ARK gerados pela Editora Moan. Nesses, arquivos há o identificador, o título, o tipo de objeto (formato), o autor e um link para o arquivo com os metadados completos. Substitua pelos seus ARK; - 2 arquivos (ark_lista.yml e ark_lista.json) contendo uma lista de todos os identificadores ARK gerados pela Editora Moan com o prefixo eM. Nesses, arquivos há o identificador, o título, o tipo de objeto (formato), o autor e um link para o arquivo com os metadados completos. Substitua pelos seus ARK;
- O arquivo gerar_ark.py para poder gerar os identificadores ARK da Editora Moan. Você poderá personalizar para o seu uso site_base para o endereço web que você vai colocar este repositório. Outro ponto importante é colocar um redirecionamento no seu servidor para cair no seu site_base toda vez que alguém estiver buscando um ARK. Colocamos o seguinte redirecionamento para funcionar com o index.php: "^/ark:(.*)$" o que captura tudo que começa com "/ark:" após o seu domínio. E redirecionamos para "$site_base/?ark=$1". Isto é, o ARK, sem o rótulo, estará no parâmetro "ark". Mais um detalhe importante... Se você não usar nenhum tratamento para quando os seus ARK forem acessados por leitura de um qr code, retire essa importação e todas as chamadas da função registrarAcessoQrCode. O arquivo "funcaoQRCode.php" possui uma chamada para o arquivo .env, mas .env nao está aqui porque são os dados do banco de dados. Se vc quiser usar a função de qr code, coloque um arquivo .env com os dados de conexão do seu banco de dados, conforme o arquivo "funcaoQRCode.php". Crie o banco de dados conforme a tabela de qr code usada no arquivo "funcaoQRCode.php"; - O arquivo gerar_ark.py para poder gerar os identificadores ARK da Editora Moan. Você poderá personalizar para o seu uso site_base para o endereço web que você vai colocar este repositório. Outro ponto importante é colocar um redirecionamento no seu servidor para cair no seu site_base toda vez que alguém estiver buscando um ARK. Colocamos o seguinte redirecionamento para funcionar com o index.php: "^/ark:(.*)$" o que captura tudo que começa com "/ark:" após o seu domínio. E redirecionamos para "$site_base/?ark=$1". Isto é, o ARK, sem o rótulo, estará no parâmetro "ark". Mais um detalhe importante... Se você não usar nenhum tratamento para quando os seus ARK forem acessados por leitura de um qr code, retire essa importação e todas as chamadas da função registrarAcessoQrCode. O arquivo "funcaoQRCode.php" possui uma chamada para o arquivo .env, mas .env nao está aqui porque são os dados do banco de dados. Se vc quiser usar a função de qr code, coloque um arquivo .env com os dados de conexão do seu banco de dados, conforme o arquivo "funcaoQRCode.php". Crie o banco de dados conforme a tabela de qr code usada no arquivo "funcaoQRCode.php";
- Os demais arquivos são os ARK com todos os seus metadados. Cada ARK possui um único arquivo YAML e uma cópia na versão JSON. Substitua pelos seus ARK. - Os demais arquivos são os ARK com todos os seus metadados. Cada ARK possui um único arquivo YAML e uma cópia na versão JSON. Substitua pelos seus ARK.
Os arquivos ark_lista estão na raiz do repositório, mas cada ARK YAML completo está na pasta yaml e os JSON, na pasta json. Os arquivos ark_lista estão na raiz do repositório, mas cada ARK YAML completo está na pasta yaml e os JSON, na pasta json.
## Sobre os nomes dos arquivos #### Sobre os nomes dos arquivos
Um exemplo de um identificador ARK pode ser "ark:777/a55/d3.By2". Como você pode observar, isso seria problemático para uso em URL e, por isso, uma pequena substituição será feita para nomear os arquivos ARK neste repositório. Um exemplo de um identificador ARK pode ser "ark:777/a55/d3.By2". Como você pode observar, isso seria problemático para uso em URL e, por isso, uma pequena substituição será feita para nomear os arquivos ARK neste repositório.
Os ":" (dois pontos) serão substituídos por \_dp\_; o "." (ponto), por \_p\_ e a "/" (barra), por \_b\_. Assim — o arquivo que conteria todos os metadados do hipotético ARK dado como exemplo no parágrafo anterior — seria "ark_dp_777_b_a55_b_d3_p_By2.yml" ou sua cópia em JSON, "ark_dp_777_b_a55_b_d3_p_By2.json". Os ":" (dois pontos) serão substituídos por \_dp\_; o "." (ponto), por \_p\_ e a "/" (barra), por \_b\_. Assim — o arquivo que conteria todos os metadados do hipotético ARK dado como exemplo no parágrafo anterior — seria "ark_dp_777_b_a55_b_d3_p_By2.yml" ou sua cópia em JSON, "ark_dp_777_b_a55_b_d3_p_By2.json".
### Instruções para o prefixo M
## Observações
Agora usamos o Plausible Analytics para as estatísticas de acesso ARK. Configure o seu.

View File

@ -28,32 +28,33 @@ function processarPrefixo_eM($NAAN, $ark_sem_NAAN, $tem_interrogacao, $resto) {
// Acesse o valor da chave 'apontamento' // Acesse o valor da chave 'apontamento'
$apontamento = $metadados['apontamento'] ?? ''; $apontamento = $metadados['apontamento'] ?? '';
$titulo = $metadados['título'] ?? '';
} else { } else {
return ['ok' => true, 'destino' => '', 'msg' => 'Erro ao decodificar o JSON no arquivo com os metadados do '.$ark_formatado.'. Informe o problema ao responsável.']; return ['ok' => true, 'destino' => '', 'msg' => 'Erro ao decodificar o JSON no arquivo com os metadados do '.$ark_formatado.'. Informe o problema ao responsável.', 'titulo' => ''];
} }
if($tem_interrogacao){ if($tem_interrogacao){
return ['ok' => true, 'destino' => $site_base.$caminho_arquivo_ark, 'msg' => $ark_formatado.' encontrado. Direcionando para os metadados...']; return ['ok' => true, 'destino' => $site_base.$caminho_arquivo_ark, 'msg' => $ark_formatado.' encontrado. Direcionando para os metadados...', 'titulo' => $titulo];
} }
if ($apontamento){ if ($apontamento){
return ['ok' => true, 'destino' => $apontamento.$resto, 'msg' => $ark_formatado.' encontrado. Direcionando...']; return ['ok' => true, 'destino' => $apontamento.$resto, 'msg' => $ark_formatado.' encontrado. Direcionando...', 'titulo' => $titulo];
} }
return ['ok' => true, 'destino' => $site_base.$caminho_arquivo_ark, 'msg' => $ark_formatado.' encontrado. Mas sem um apontamento configurado. Direcionando para os metadados...']; return ['ok' => true, 'destino' => $site_base.$caminho_arquivo_ark, 'msg' => $ark_formatado.' encontrado. Mas sem um apontamento configurado. Direcionando para os metadados...', 'titulo' => $titulo];
} }
return ['ok' => false, 'destino' => '', 'msg' => $ark_formatado.' nao encontrado.']; return ['ok' => false, 'destino' => '', 'msg' => $ark_formatado.' não encontrado.', 'titulo' => ''];
} }
?> ?>

View File

@ -0,0 +1,5 @@
<script defer data-domain="ark.livro.online" src="https://metricas.livro.online/js/script.manual.js"></script>
<script>window.plausible = window.plausible || function(){(window.plausible.q=window.plausible.q||[]).push(arguments)}</script>

View File

@ -1,64 +0,0 @@
import os
import shutil
import subprocess
def copiar_arquivos_e_pastas(arquivos, origem, destino):
# Imprime o caminho absoluto do destino
print("Caminho absoluto do destino:", os.path.abspath(destino))
# Se o destino for o servidor-web/livro.online/ark, apaga o diretório de destino inteiro
if destino == os.path.abspath('../servidor-web/livro.online/ark'):
print("Apagando diretório de destino...")
shutil.rmtree(destino)
# Percorre a lista de arquivos e pastas
for item in arquivos:
origem_item = os.path.join(origem, item)
destino_item = os.path.join(destino, item)
# Se for uma pasta, copia recursivamente
if os.path.isdir(origem_item):
shutil.copytree(origem_item, destino_item, dirs_exist_ok=True)
else:
# Se for um arquivo, copia
shutil.copy2(origem_item, destino_item)
# Diretório de origem é o diretório atual
diretorio_origem = os.getcwd()
# Diretório de destino
diretorio_destino = os.path.abspath('../servidor-web/livro.online/ark') # Aqui você pode colocar o diretório correto de acordo com a estrutura do seu projeto
# Lista de arquivos e pastas a serem copiados
arquivos_e_pastas = ['json', 'yaml', 'ark_lista.json', 'ark_lista.yml', 'funcaoQRCode.php', 'index.php']
# Chama a função para copiar os arquivos e pastas
copiar_arquivos_e_pastas(arquivos_e_pastas, diretorio_origem, diretorio_destino)
print("Arquivos e pastas copiados com sucesso no servidor-web!")
diretorio_livro_p_online = os.path.abspath('../site-moan-quarto-config/site-livroonline') # Aqui você pode colocar o diretório correto de acordo com a estrutura do seu projeto
# Navega para o diretorio_livro_p_online
os.chdir(diretorio_livro_p_online)
# Executa os comandos do terminal
"""
comandos = [
'quarto render publicacoes.qmd --cache-refresh',
'git add publicacoes.qmd',
'git commit -m "publicação novo"',
'git push'
]
"""
comandos = ['quarto render publicacoes.qmd --cache-refresh']
for comando in comandos:
subprocess.run(comando, shell=True)
print("Servidor atualizado!")
RESET = "\033[0m"
RED = "\033[91m"
print(RED+"\n======== ATENÇÃO!!! Ainda é preciso atualizar o github!!! ========\n"+RESET)

View File

@ -1,64 +0,0 @@
<?php
date_default_timezone_set('America/Sao_Paulo'); // Substitua 'America/Sao_Paulo' pelo fuso horário desejado
function registrarAcessoQrCode($chave, $destino_redirecionamento) {
// Forma de uso... Adicione &qrark=1 no final da URL. Se quiser consultar os metadados, adicione ?&qrark=1
$envPath = dirname(__DIR__) . '/.env';
$env = parse_ini_file($envPath);
if (isset($_GET["qrark"]) && $_GET["qrark"] == 1) {
// Pegando as variáveis de acesso
$db = $env['DB_NAME'];
$dbUser = $env['DB_USER'];
$dbPassword = $env['DB_PASSWORD'];
// Configurações do banco de dados
$servidor = "localhost";
$usuario = $dbUser;
$senha = $dbPassword;
$banco = $db;
// Conexão com o banco de dados
$conexao = new mysqli($servidor, $usuario, $senha, $banco);
// Verifique a conexão
if ($conexao->connect_error) {
die("Falha na conexão: " . $conexao->connect_error);
}
$endereco_ip = $_SERVER['REMOTE_ADDR'];
$data_acesso = date('Y-m-d H:i:s');
// Consulta SQL para inserir dados
$sql = "INSERT INTO acessos_qr_code (chave, url_destino, ip, data_acesso) VALUES (?, ?, ?, ?)";
// Preparar a consulta
$stmt = $conexao->prepare($sql);
// Verificar se a consulta está pronta
if ($stmt) {
// Vincular parâmetros
$stmt->bind_param('ssss', $chave, $destino_redirecionamento, $endereco_ip, $data_acesso);
// Executar a consulta
if ($stmt->execute()) {
/* echo "Registro inserido com sucesso!"; */
} else {
/* echo "Erro ao inserir o registro: " . $stmt->error; */
}
// Fechar a consulta
$stmt->close();
} else {
/* echo "Erro na preparação da consulta: " . $conexao->error; */
}
// Fechar a conexão com o banco de dados
$conexao->close();
}
}
?>

View File

@ -24,7 +24,7 @@ function reduzirArk($tamanho, $pedacos) {
function limpar_ark($str){ function limpar_ark($str){
// Lista de caracteres a serem removidos // Lista de caracteres a serem removidos
$caracteresParaRemover = array('<', '>', ':', ';', '(', ')', '&', '$'); $caracteresParaRemover = array('<', '>', ':', ';', '(', '&', ')', '$');
// Substitui os caracteres da lista por uma string vazia // Substitui os caracteres da lista por uma string vazia
$stringLimpa = str_replace($caracteresParaRemover, '', $str); $stringLimpa = str_replace($caracteresParaRemover, '', $str);
@ -47,7 +47,7 @@ function init() {
$ark = ""; $ark = "";
return ['ok'=>false,'destino'=>'','msg'=>'Nenhum ARK foi passado...','qrark'=> false,'ark'=>'']; return ['ok'=>false,'destino'=>'','msg'=>'Nenhum ARK foi passado...','qrark'=> false,'ark_full'=>'', 'tem_interrogacao'=>false];
} }
@ -67,22 +67,39 @@ function init() {
// Remova a barra no início // Remova a barra no início
$ark= substr($ark, 1); $ark= substr($ark, 1);
} }
$tem_interrogacao = false;
// remove até 2 interrogações no final e marca flag. Ark para consultar metadados se coloca uma interrogacao no final. Duas interrogacoes verifica a persistencia, o tempo de existencia do ark, mas aqui na Editora Moan nao implementamos isso
$ark = rtrim($ark);
for ($k = 0; $k < 2; $k++) {
if (substr($ark, -1) === '?') {
$tem_interrogacao = true;
$ark = substr($ark, 0, -1);
}
}
$pedacos = explode("/", $ark); $pedacos = explode("/", $ark);
// ARK completo “sem o prefixo ark:” e sem barra inicial, fixo:
$ark_full = implode("/", array_filter($pedacos, fn($p) => $p !== ''));
$NAAN = $pedacos[0]; //NAAN é o número de registro da Editora Moan na Ark Alliance $NAAN = $pedacos[0]; //NAAN é o número de registro da Editora Moan na Ark Alliance
$ark_sem_NAAN = reduzirArk(count($pedacos), $pedacos); $ark_sem_NAAN = reduzirArk(count($pedacos), $pedacos);
// sem NAAN
$ark_base = implode("/", array_slice($pedacos, 1));
$ark_completo_sem_NAAN = $ark_sem_NAAN;
$ark_completo_sem_NAAN = ltrim($ark_completo_sem_NAAN, '/');
if ($ark === '') { if ($ark === '') {
return ['ok'=>false,'destino'=>'','msg'=>'ARK vazio.','qrark'=>$qrark,'ark'=>'']; return ['ok'=>false,'destino'=>'','msg'=>'ARK vazio.','qrark'=>$qrark,'ark_full'=>'', 'tem_interrogacao'=>false];
} }
//-------------------------- PREFIXOS ARK -------------------------- //-------------------------- PREFIXOS ARK --------------------------
$prefixo_editora = "eM"; $prefixo_editora = "eM";
@ -90,12 +107,12 @@ function init() {
$buscar_ark = null; $buscar_ark = null;
if (str_starts_with($ark_completo_sem_NAAN, $prefixo_editora)) { if (str_starts_with($ark_base, $prefixo_editora)) {
// Prefixo eM // Prefixo eM
require __DIR__ . '/LogicaPrefixos/processarPrefixo_eM.php'; require __DIR__ . '/LogicaPrefixos/processarPrefixo_eM.php';
$buscar_ark = 'processarPrefixo_eM'; $buscar_ark = 'processarPrefixo_eM';
} elseif (str_starts_with($ark_completo_sem_NAAN, $prefixo_museu_da_matematica)) { } elseif (str_starts_with($ark_base, $prefixo_museu_da_matematica)) {
// Prefixo M // Prefixo M
require __DIR__ . '/LogicaPrefixos/processarPrefixo_M.php'; require __DIR__ . '/LogicaPrefixos/processarPrefixo_M.php';
$buscar_ark = 'processarPrefixo_M'; $buscar_ark = 'processarPrefixo_M';
@ -107,7 +124,7 @@ function init() {
//-------------------------- FIM PREFIXOS ARK -------------------------- //-------------------------- FIM PREFIXOS ARK --------------------------
if (!is_callable($buscar_ark)) { if (!is_callable($buscar_ark)) {
return ['ok'=>false,'destino'=>'','msg'=>'Prefixo ARK desconhecido.','qrark'=>$qrark,'ark'=>$ark_completo_sem_NAAN]; return ['ok'=>false,'destino'=>'','msg'=>'Prefixo ARK desconhecido.','qrark'=>$qrark,'ark_full'=>$ark_full, 'tem_interrogacao'=>$tem_interrogacao];
} }
@ -117,8 +134,10 @@ function init() {
$identificador_ark = false; $identificador_ark = false;
$resultado = ['ok'=>false,'destino'=>'','msg'=>'ARK não encontrado.Erro não identificado.','qrark'=>$qrark,'ark'=>$ark_completo_sem_NAAN]; $resultado = ['ok'=>false,'destino'=>'','msg'=>'ARK não encontrado.Erro não identificado.','qrark'=>$qrark,'ark'=>$ark_full, 'tem_interrogacao'=>$tem_interrogacao];
do { do {
$resto = ""; $resto = "";
@ -145,29 +164,6 @@ function init() {
break; break;
} }
$len_ark_sem_NAAN = strlen($ark_sem_NAAN);
$tem_interrogacao = false;
if($ark_sem_NAAN[$len_ark_sem_NAAN-1] == "?"){// Um ARK com ? é uma pergunta para saber seus metadados e, por isso, em vez da obra em si, temos que retornar os metadados do ARK
$tem_interrogacao = true;
$ark_sem_NAAN = substr($ark_sem_NAAN, 0, -1);
}//Fecha if
$len_ark_sem_NAAN = strlen($ark_sem_NAAN);
if($ark_sem_NAAN[$len_ark_sem_NAAN-1] == "?"){// É executado duas vezes porque um ARK, pela especificação, pode conter duas interrogações e nesse caso deveria retornar se o arquivo é temporário ou não. Mas na Editora Moan não implementamos isso.
$ark_sem_NAAN = substr($ark_sem_NAAN, 0, -1);
}//Fecha if
$resultado = $buscar_ark($NAAN, $ark_sem_NAAN, $tem_interrogacao, $resto); $resultado = $buscar_ark($NAAN, $ark_sem_NAAN, $tem_interrogacao, $resto);
@ -177,8 +173,12 @@ function init() {
} while ($tamanho > 1 && !$identificador_ark); } while ($tamanho > 1 && !$identificador_ark);
$resultado['qrark'] = $qrark;
$resultado['ark'] = $ark_completo_sem_NAAN;
$resultado['qrark'] = $qrark;
$resultado['ark_full'] = $ark_full; // <-- use este no JS/analytics
$resultado['tem_interrogacao'] = $tem_interrogacao;
return $resultado; return $resultado;
@ -192,10 +192,32 @@ $destino = $conteudo["destino"];
$msg = $conteudo["msg"]; $msg = $conteudo["msg"];
$ark = $conteudo["ark"]; $arkFull = $conteudo['ark_full'] ?? '';
$qrark = $conteudo["qrark"]; $qrark = $conteudo["qrark"];
$tem_interrogacao = $conteudo['tem_interrogacao'];
function slug_titulo($s) {
$s = trim((string)$s);
if ($s === '') return '';
// remove acentos
$s = iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $s);
$s = strtolower($s);
// troca tudo que não é letra/número por hífen
$s = preg_replace('/[^a-z0-9]+/', '-', $s);
$s = trim($s, '-');
// limita tamanho (pra não virar um trem sem freio)
return substr($s, 0, 80);
}
$tituloSlug = slug_titulo($conteudo['titulo'] ?? '');
?> ?>
@ -207,22 +229,21 @@ $qrark = $conteudo["qrark"];
<title><?= $ok ? "Redirecionando…" : "ARK não encontrado" ?></title> <title><?= $ok ? "Redirecionando…" : "ARK não encontrado" ?></title>
<style> <style>
html,body{margin:0;width:100%;height:100%;display:flex;align-items:center;justify-content:center;font-family:system-ui,sans-serif;background:#fff} html,body{margin:0;width:100%;height:100%;display:flex;align-items:center;justify-content:center;font-family:system-ui,sans-serif;background: #FF5F6D; /* fallback for old browsers */
background: -webkit-linear-gradient(to right, #FFC371, #FF5F6D); /* Chrome 10-25, Safari 5.1-6 */
background: linear-gradient(to right, #FFC371, #FF5F6D); /* W3C, IE 10+/ Edge, Firefox 16+, Chrome 26+, Opera 12+, Safari 7+ */
}
.spinner{width:52px;height:52px;border:5px solid #ddd;border-top-color:#555;border-radius:50%;animation:spin 1s linear infinite} .spinner{width:52px;height:52px;border:5px solid #ddd;border-top-color:#555;border-radius:50%;animation:spin 1s linear infinite}
@keyframes spin{to{transform:rotate(360deg)}} @keyframes spin{to{transform:rotate(360deg)}}
.msg{max-width:420px;text-align:center;padding:24px} .msg{max-width:420px;text-align:center;padding:24px}
</style> </style>
<?php <?php
// Injeta o snippet do plausible (arquivo ignorado no git) // Injeta o snippet do plausible (arquivo ignorado no git)
@include __DIR__ . '/analytics/plausible.php'; @include __DIR__ . '/analytics/plausible.php';
?> ?>
<?php if ($ok): ?>
<meta http-equiv="refresh" content="0;url=<?= htmlspecialchars($destino, ENT_QUOTES, 'UTF-8') ?>">
<?php endif; ?>
</head> </head>
<body> <body>
<div class="spinner" id="spinner" aria-label="Carregando"></div> <div class="spinner" id="spinner" aria-label="Carregando"></div>
@ -235,30 +256,54 @@ $qrark = $conteudo["qrark"];
</div> </div>
<script> <script>
// Passando PHP -> JS (seguro) const ok = <?= json_encode($ok) ?>;
const ok = <?= json_encode($ok) ?>; const destino = <?= json_encode($destino) ?>;
const destino = <?= json_encode($destino) ?>; const msg = <?= json_encode($msg) ?>;
const msg = <?= json_encode($msg) ?>;
const ark = <?= json_encode($ark) ?>;
const qrark = <?= json_encode($qrark) ?>;
const spinner = document.getElementById('spinner'); const arkFull = <?= json_encode($arkFull) ?>; // "68745/eM8Rf/gJ.ht"
const msgBox = document.getElementById('msgBox'); const qrark = <?= json_encode($qrark) ?>; // boolean
const msgTitle = document.getElementById('msgTitle'); const consultaMetadados = <?= json_encode($tem_interrogacao) ?>; // boolean
const msgText = document.getElementById('msgText'); const tituloSlug = <?= json_encode($tituloSlug) ?>; // "o-titulo-do-livro"
if (ok) { const spinner = document.getElementById('spinner');
if (typeof plausible === "function") { const msgBox = document.getElementById('msgBox');
plausible("ARK Resolve", { props: { ark, qrark } }); const msgTitle = document.getElementById('msgTitle');
} const msgText = document.getElementById('msgText');
setTimeout(() => location.replace(destino), 120);
function pathSafe(a) {
return String(a)
.split('/')
.filter(Boolean)
.map(encodeURIComponent)
.join('/');
}
const go = () => location.replace(destino);
if (ok) {
const parts = [];
if (qrark) parts.push('qrcodeLivro');
if (consultaMetadados) parts.push('meta');
if (tituloSlug) parts.push(tituloSlug);
const suffix = parts.length ? '_' + parts.join('_') : '';
const virtualUrl = location.origin + '/' + pathSafe(arkFull) + suffix;
if (typeof plausible === "function") {
plausible("pageview", {
u: virtualUrl,
callback: () => go()
});
setTimeout(go, 1500); // fallback
} else { } else {
// terminou e deu ruim: troca spinner por mensagem go();
spinner.style.display = 'none';
msgBox.style.display = 'block';
msgTitle.textContent = 'Erro';
msgText.textContent = msg;
} }
} else {
spinner.style.display = 'none';
msgBox.style.display = 'block';
msgTitle.textContent = 'Erro';
msgText.textContent = msg;
}
</script> </script>
</body> </body>