SEO: metadatos, datos estructurados y encabezados semánticos
All checks were successful
Zola / build-and-deploy (push) Successful in 17s
All checks were successful
Zola / build-and-deploy (push) Successful in 17s
- <html lang="es"> en lugar de "en" - <title> con sufijo del sitio, canonical, meta robots y author - Open Graph completo (url, type, locale, image, article:*) y Twitter Card - JSON-LD: WebSite + Person en portada, BlogPosting en cada post - rel="me" hacia Mastodon/GitHub/X - Imagen para redes (og-image.png) y favicon - Títulos de página como <h1> (antes <div>); listados y modal de búsqueda en <h2> - Notas sueltas con noindex y fuera del sitemap; sin paginación duplicada - Páginas de etiquetas y 404 en español, con títulos propios - alt/width/height/lazy en los badges de la portada - MathJax solo cuando un post lo pida ([extra] mathjax = true) - Descripción en /posts y etiquetas en "Hola Mundo" - Limpieza de ficheros de respaldo del editor + .gitignore Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
1d74bceae8
commit
00dc194e86
22 changed files with 714 additions and 26 deletions
396
templates/partials/header.html
Normal file
396
templates/partials/header.html
Normal file
|
|
@ -0,0 +1,396 @@
|
|||
{% import "macros/macros.html" as post_macros %}
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="base" content="{{ config.base_url }}" />
|
||||
|
||||
{% if config.extra.fediverse %}
|
||||
<meta name="fediverse:creator"
|
||||
content="{{ config.extra.fediverse_creator }}" />
|
||||
{% endif %}
|
||||
|
||||
{% if page.extra.meta %}
|
||||
<!-- the meta data config goes here -->
|
||||
{% for data in page.extra.meta %}
|
||||
<meta {% for key, value in data %} {% if key == "property" and value == "og:title" %} {% set_global page_has_og_title = true -%} {% endif %}
|
||||
{% if key == "property" and value == "og:description" %} {% set_global page_has_og_description = true -%} {% endif %}
|
||||
{% if key == "name" and value == "description" %} {% set_global page_has_description = true -%} {% endif %}
|
||||
{{ key }}
|
||||
None="{{ value }}"
|
||||
{% endfor %} />
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
{# ===== SEO ===== #}
|
||||
{% set is_404 = current_path is not defined %}
|
||||
{% set current_path = current_path | default(value="/") %}
|
||||
{% set site_name = config.extra.site_name | default(value=config.title) %}
|
||||
{% set author = config.extra.author | default(value=site_name) %}
|
||||
|
||||
{# Título #}
|
||||
{% if is_404 %}
|
||||
{% set_global seo_title = "Página no encontrada | " ~ site_name %}
|
||||
{% elif current_path == "/" %}
|
||||
{% set_global seo_title = site_name ~ " — " ~ config.description | trim_end_matches(pat=".") %}
|
||||
{% elif term %}
|
||||
{% set_global seo_title = "Entradas sobre «" ~ term.name ~ "» | " ~ site_name %}
|
||||
{% elif taxonomy %}
|
||||
{% set_global seo_title = "Etiquetas | " ~ site_name %}
|
||||
{% elif page.title %}
|
||||
{% set_global seo_title = page.title ~ " | " ~ site_name %}
|
||||
{% elif section.title %}
|
||||
{% set_global seo_title = section.title ~ " | " ~ site_name %}
|
||||
{% else %}
|
||||
{% set_global seo_title = site_name %}
|
||||
{% endif %}
|
||||
|
||||
{# Descripción #}
|
||||
{% if page.description %}
|
||||
{% set_global seo_description = page.description %}
|
||||
{% elif section.description %}
|
||||
{% set_global seo_description = section.description %}
|
||||
{% elif term %}
|
||||
{% set_global seo_description = "Artículos de " ~ site_name ~ " sobre " ~ term.name ~ "." %}
|
||||
{% elif taxonomy %}
|
||||
{% set_global seo_description = "Todas las etiquetas del blog de " ~ site_name ~ "." %}
|
||||
{% else %}
|
||||
{% set_global seo_description = config.description %}
|
||||
{% endif %}
|
||||
|
||||
{# URL canónica #}
|
||||
{% if page.permalink %}
|
||||
{% set_global seo_url = page.permalink %}
|
||||
{% elif section.permalink %}
|
||||
{% set_global seo_url = section.permalink %}
|
||||
{% elif term %}
|
||||
{% set_global seo_url = term.permalink %}
|
||||
{% else %}
|
||||
{% set_global seo_url = current_url | default(value=config.base_url) %}
|
||||
{% endif %}
|
||||
|
||||
{# Imagen para redes sociales #}
|
||||
{% if page.extra.image %}
|
||||
{% set_global seo_image = get_url(path=page.extra.image) %}
|
||||
{% elif section.extra.image %}
|
||||
{% set_global seo_image = get_url(path=section.extra.image) %}
|
||||
{% elif config.extra.og_image %}
|
||||
{% set_global seo_image = get_url(path=config.extra.og_image) %}
|
||||
{% else %}
|
||||
{% set_global seo_image = "" %}
|
||||
{% endif %}
|
||||
|
||||
{% set is_article = page.date and page.components and page.components | first == "posts" %}
|
||||
|
||||
<title>{{ seo_title }}</title>
|
||||
{% if not is_404 %}<link rel="canonical" href="{{ seo_url | safe }}" />{% endif %}
|
||||
<meta name="author" content="{{ author }}" />
|
||||
{% set is_nota = page.components and page.components | first == "notas" %}
|
||||
{% if page.extra.noindex or section.extra.noindex or is_404 or is_nota %}
|
||||
<meta name="robots" content="noindex, follow" />
|
||||
{% else %}
|
||||
<meta name="robots" content="index, follow, max-image-preview:large" />
|
||||
{% endif %}
|
||||
|
||||
{% if not page_has_description %}
|
||||
<meta name="description" content="{{ seo_description }}" />
|
||||
{% endif %}
|
||||
|
||||
{# Open Graph #}
|
||||
<meta property="og:site_name" content="{{ site_name }}" />
|
||||
<meta property="og:locale" content="{{ config.extra.locale | default(value='es_ES') }}" />
|
||||
<meta property="og:url" content="{{ seo_url | safe }}" />
|
||||
{% if not page_has_og_title %}
|
||||
<meta property="og:title" content="{% if page.title %}{{ page.title }}{% else %}{{ seo_title }}{% endif %}" />
|
||||
{% endif %}
|
||||
{% if not page_has_og_description %}
|
||||
<meta property="og:description" content="{{ seo_description }}" />
|
||||
{% endif %}
|
||||
{% if seo_image %}
|
||||
<meta property="og:image" content="{{ seo_image | safe }}" />
|
||||
<meta property="og:image:alt" content="{{ seo_title }}" />
|
||||
{% endif %}
|
||||
{% if is_article %}
|
||||
<meta property="og:type" content="article" />
|
||||
<meta property="article:published_time" content="{{ page.date | date(format='%Y-%m-%d') }}" />
|
||||
{% if page.updated %}
|
||||
<meta property="article:modified_time" content="{{ page.updated | date(format='%Y-%m-%d') }}" />
|
||||
{% endif %}
|
||||
<meta property="article:author" content="{{ author }}" />
|
||||
{% if page.taxonomies.tags %}
|
||||
{% for tag in page.taxonomies.tags %}
|
||||
<meta property="article:tag" content="{{ tag }}" />
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<meta property="og:type" content="website" />
|
||||
{% endif %}
|
||||
|
||||
{# Twitter / X #}
|
||||
<meta name="twitter:card" content="{% if seo_image %}summary_large_image{% else %}summary{% endif %}" />
|
||||
{% if config.extra.twitter_handle %}
|
||||
<meta name="twitter:site" content="{{ config.extra.twitter_handle }}" />
|
||||
<meta name="twitter:creator" content="{{ config.extra.twitter_handle }}" />
|
||||
{% endif %}
|
||||
|
||||
{# Verificación de identidad (Mastodon, IndieWeb) #}
|
||||
{% set_global same_as = [] %}
|
||||
{% for social in config.extra.socials | default(value=[]) %}
|
||||
{% if social.url is not starting_with("mailto:") %}
|
||||
{% set_global same_as = same_as | concat(with=social.url) %}
|
||||
<link rel="me" href="{{ social.url }}" />
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
{# Datos estructurados (schema.org) #}
|
||||
{% if current_path == "/" and not is_404 %}
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "WebSite",
|
||||
"name": {{ site_name | json_encode | safe }},
|
||||
"url": {{ config.base_url | json_encode | safe }},
|
||||
"description": {{ config.description | json_encode | safe }},
|
||||
"inLanguage": {{ config.default_language | json_encode | safe }},
|
||||
"author": {
|
||||
"@type": "Person",
|
||||
"name": {{ author | json_encode | safe }},
|
||||
"url": {{ config.base_url | json_encode | safe }},
|
||||
"sameAs": {{ same_as | json_encode | safe }}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{% elif is_article %}
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "BlogPosting",
|
||||
"headline": {{ page.title | json_encode | safe }},
|
||||
"description": {{ seo_description | json_encode | safe }},
|
||||
"url": {{ page.permalink | json_encode | safe }},
|
||||
"mainEntityOfPage": {{ page.permalink | json_encode | safe }},
|
||||
"datePublished": {{ page.date | date(format="%Y-%m-%d") | json_encode | safe }},
|
||||
"dateModified": {{ page.updated | default(value=page.date) | date(format="%Y-%m-%d") | json_encode | safe }},
|
||||
"inLanguage": {{ page.lang | json_encode | safe }},
|
||||
"wordCount": {{ page.word_count }},
|
||||
{% if page.taxonomies.tags %}"keywords": {{ page.taxonomies.tags | join(sep=", ") | json_encode | safe }},{% endif %}
|
||||
{% if seo_image %}"image": {{ seo_image | json_encode | safe }},{% endif %}
|
||||
"author": {
|
||||
"@type": "Person",
|
||||
"name": {{ author | json_encode | safe }},
|
||||
"url": {{ config.base_url | json_encode | safe }}
|
||||
},
|
||||
"publisher": {
|
||||
"@type": "Person",
|
||||
"name": {{ author | json_encode | safe }}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{% endif %}
|
||||
{# ===== /SEO ===== #}
|
||||
|
||||
{# Favicon #}
|
||||
{% if config.extra.favicon %}
|
||||
<link rel="icon"
|
||||
type="image/png"
|
||||
href="{{ get_url(path=config.extra.favicon) }}" />
|
||||
{% endif %}
|
||||
|
||||
{# Font from cdn or disk #}
|
||||
{% if config.extra.use_cdn | default(value=false) %}
|
||||
<link rel="preconnect" href="https://cdn.jsdelivr.net" crossorigin />
|
||||
<link rel="stylesheet"
|
||||
href="https://cdn.jsdelivr.net/npm/jetbrains-mono@1.0.6/css/jetbrains-mono.min.css" />
|
||||
<link rel="stylesheet"
|
||||
href="https://cdn.jsdelivr.net/npm/@fontsource/space-grotesk@4.5.8/index.min.css" />
|
||||
{% else %}
|
||||
<link href={{ get_url(path="fonts.css") }} rel="stylesheet" />
|
||||
{% endif %}
|
||||
|
||||
{# Syntax theme #}
|
||||
{% if config.markdown.highlight_code and config.markdown.highlight_theme == "css" %}
|
||||
<link rel="stylesheet"
|
||||
type="text/css"
|
||||
href="/syntax-theme-dark.css"
|
||||
media="(prefers-color-scheme: dark)" />
|
||||
<link rel="stylesheet"
|
||||
type="text/css"
|
||||
href="/syntax-theme-light.css"
|
||||
media="(prefers-color-scheme: light)" />
|
||||
{% endif %}
|
||||
|
||||
{# Analytics #}
|
||||
{% if config.extra.analytics.enabled %}
|
||||
{% if config.extra.analytics.umami.website_id %}
|
||||
{% set website_id = config.extra.analytics.umami.website_id %}
|
||||
{% set host_url = config.extra.analytics.umami.host_url | default(value="https://api-gateway.umami.dev/") %}
|
||||
|
||||
<script defer
|
||||
src="/js/imamu.js"
|
||||
data-website-id="{{ website_id }}"
|
||||
data-host-url="{{ host_url }}"></script>
|
||||
{% endif %}
|
||||
|
||||
{% if config.extra.analytics.goatcounter.user %}
|
||||
{% set user = config.extra.analytics.goatcounter.user %}
|
||||
{% set host = config.extra.analytics.goatcounter.host | default(value="goatcounter.com") %}
|
||||
|
||||
<script data-goatcounter="https://{{ user }}.{{ host }}/count"
|
||||
async
|
||||
src="{{ get_url(path="js/count.js") }}"></script>
|
||||
<noscript>
|
||||
{# EasyList blocks '.com/count?', so we have to use '.com//count' #}
|
||||
<img src="https://{{ user }}.{{ host }}//count?p={{ current_path }}&t={{ page.title | default(value=config.title) }}" />
|
||||
</noscript>
|
||||
{% endif %}
|
||||
|
||||
{% if config.extra.analytics.google.tracking_id %}
|
||||
{% set tracking_id = config.extra.analytics.google.tracking_id %}
|
||||
|
||||
<!-- Google tag (gtag.js) -->
|
||||
<script async
|
||||
src="https://www.googletagmanager.com/gtag/js?id={{ tracking_id }}"></script>
|
||||
<script>
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
|
||||
function gtag() {
|
||||
dataLayer.push(arguments);
|
||||
}
|
||||
gtag('js', new Date());
|
||||
gtag('config', '{{ tracking_id }}');
|
||||
</script>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{# Fancy Codeblock #}
|
||||
{% if config.extra.fancy_code %}
|
||||
<script defer src={{ get_url(path="js/codeblock.js") }}></script>
|
||||
{% endif %}
|
||||
|
||||
{# Table of contents #}
|
||||
{% if config.extra.toc | default(value=false) %}
|
||||
<script defer src={{ get_url(path="js/toc.js") }}></script>
|
||||
{% endif %}
|
||||
|
||||
{# Dynamic Note #}
|
||||
{% if config.extra.dynamic_note | default(value=false) %}
|
||||
<script defer src={{ get_url(path="js/note.js") }}></script>
|
||||
{% endif %}
|
||||
|
||||
{% if page.extra.mathjax | default(value=config.extra.mathjax | default(value=false)) %}
|
||||
{% if config.extra.mathjax_dollar_inline_enable | default(value=false) %}
|
||||
<script>
|
||||
MathJax = {
|
||||
tex: {
|
||||
inlineMath: [
|
||||
['$', '$'],
|
||||
['\\(', '\\)']
|
||||
]
|
||||
}
|
||||
};
|
||||
</script>
|
||||
{% endif %}
|
||||
<script type="text/javascript" id="MathJax-script" async src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js">
|
||||
</script>
|
||||
{% endif %}
|
||||
|
||||
{# RSS #}
|
||||
{% if config.generate_feeds %}
|
||||
{% for feed_filenames in config.feed_filenames %}
|
||||
{% if feed_filenames == "atom.xml" %}
|
||||
<link rel="alternate"
|
||||
type="application/atom+xml"
|
||||
title="{{ config.title }}"
|
||||
href="{{ get_url(path='atom.xml', trailing_slash=false) }}" />
|
||||
{% elif feed_filenames == "rss.xml" %}
|
||||
<link rel="alternate"
|
||||
type="application/rss+xml"
|
||||
title="{{ config.title }}"
|
||||
href="{{ get_url(path='rss.xml', trailing_slash=false) }}" />
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
|
||||
{% set theme = config.extra.theme | default(value="toggle") %}
|
||||
{% if theme == "dark" %}
|
||||
<link rel="stylesheet"
|
||||
type="text/css"
|
||||
href="{{ get_url(path='theme/dark.css') }}" />
|
||||
{% elif theme == "light" %}
|
||||
<link rel="stylesheet"
|
||||
type="text/css"
|
||||
href="{{ get_url(path='theme/light.css') }}" />
|
||||
{% elif theme == "auto" %}
|
||||
<link rel="stylesheet"
|
||||
type="text/css"
|
||||
href="{{ get_url(path='theme/light.css') }}" />
|
||||
<link rel="stylesheet"
|
||||
type="text/css"
|
||||
href="{{ get_url(path='theme/dark.css') }}"
|
||||
media="(prefers-color-scheme: dark)" />
|
||||
{% elif theme == "toggle" %}
|
||||
<link rel="stylesheet"
|
||||
type="text/css"
|
||||
href="{{ get_url(path='theme/light.css') }}" />
|
||||
<link id="darkModeStyle"
|
||||
rel="stylesheet"
|
||||
type="text/css"
|
||||
href="{{ get_url(path='theme/dark.css') }}" />
|
||||
{% endif %}
|
||||
|
||||
<!-- Set the correct theme in the script -->
|
||||
|
||||
{% if theme == "auto" or theme == "toggle" %}
|
||||
<script src={{ get_url(path="js/themetoggle.js") }}></script>
|
||||
|
||||
{% if theme == "auto" %}
|
||||
<script>
|
||||
if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
|
||||
setTheme("dark");
|
||||
} else {
|
||||
setTheme("light");
|
||||
}
|
||||
</script>
|
||||
{% else %}
|
||||
<script>
|
||||
setTheme(getSavedTheme());
|
||||
</script>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
|
||||
<link rel="stylesheet"
|
||||
type="text/css"
|
||||
media="screen"
|
||||
href="{{ get_url(path='main.css') }}" />
|
||||
|
||||
{% if config.extra.stylesheets %}
|
||||
{% for stylesheet in config.extra.stylesheets %}
|
||||
<link rel="stylesheet" href="{{ get_url(path=stylesheet) }}" />
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
{# Search #}
|
||||
{%- if config.build_search_index -%}
|
||||
{%- if config.search.index_format -%}
|
||||
{%- set search_index_format = config.search.index_format -%}
|
||||
{%- elif config.extra.index_format -%}
|
||||
{# Necessary to support Zola 0.17.X, as it doesn't have access to config.search.index_format #}
|
||||
{# See: https://github.com/getzola/zola/issues/2165 #}
|
||||
{%- set search_index_format = config.extra.index_format -%}
|
||||
{%- else -%}
|
||||
{%- set search_index_format = "elasticlunr_json" -%}
|
||||
{%- endif -%}
|
||||
|
||||
{%- if search_index_format == "elasticlunr_javascript" -%}
|
||||
<script defer
|
||||
src="{{ get_url(path='search_index.' ~ lang ~ '.js', cachebust=true) | safe }}"></script>
|
||||
{%- endif -%}
|
||||
|
||||
{# Main search script #}
|
||||
<script defer
|
||||
src="{{ get_url(path='js/searchElasticlunr.min.js', cachebust=true) | safe }}"></script>
|
||||
{%- endif -%}
|
||||
</head>
|
||||
97
templates/partials/nav.html
Normal file
97
templates/partials/nav.html
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
<nav>
|
||||
<div class="left-nav">
|
||||
{% if config.extra.logo %}
|
||||
<a href={{ config.base_url }} class="logo">
|
||||
<img alt="{{ config.title }}" src="{{ get_url(path=config.extra.logo) }}" />
|
||||
</a>
|
||||
{% else %}
|
||||
<a href={{ config.base_url }}>{{ config.title }}</a>
|
||||
{% endif %}
|
||||
|
||||
|
||||
<div class="socials">
|
||||
{% if config.markdown.external_links_target_blank %}
|
||||
{% set link_attrs = "rel='me noopener' target='_blank'" %}
|
||||
{% else %}
|
||||
{% set link_attrs = "rel='me'" %}
|
||||
{% endif %}
|
||||
{% for social in config.extra.socials %}
|
||||
<a {{ link_attrs | safe }} href="{{ social.url }}" class="social">
|
||||
<img alt="{{ social.name }}"
|
||||
src="{{ get_url(path='icons/social/' ~ social.icon ~ '.svg') }}" />
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="right-nav">
|
||||
{% for menu in config.extra.menu %}
|
||||
<a href={{ get_url(path=menu.url) }} style="margin-right: 0.5em">{{ menu.name }}</a>
|
||||
{% endfor %}
|
||||
|
||||
{% if config.build_search_index %}
|
||||
<button id="search-button"
|
||||
class="search-button"
|
||||
title="$SHORTCUT to open search">
|
||||
<img src="{{ get_url(path='icons/search.svg') }}"
|
||||
alt="Search"
|
||||
class="search-icon" />
|
||||
</button>
|
||||
|
||||
<div id="searchModal"
|
||||
class="search-modal js"
|
||||
role="dialog"
|
||||
aria-labelledby="modalTitle">
|
||||
<div id="modal-content">
|
||||
<h2 id="modalTitle" class="page-header">
|
||||
Search
|
||||
</h2>
|
||||
<div id="searchBar">
|
||||
<input id="searchInput"
|
||||
role="combobox"
|
||||
autocomplete="off"
|
||||
spellcheck="false"
|
||||
aria-expanded="false"
|
||||
aria-controls="results-container"
|
||||
placeholder="Search..." />
|
||||
<button id="clear-search" class="clear-button" title="Clear search">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960">
|
||||
<path d="m256-200-56-56 224-224-224-224 56-56 224 224 224-224 56 56-224 224 224 224-56 56-224-224-224 224Z" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div id="results-container">
|
||||
<div id="results-info">
|
||||
<span id="zero_results" style="display: none;">No results</span>
|
||||
<span id="one_result" style="display: none;">1 result</span>
|
||||
<span id="many_results" style="display: none;">$NUMBER results</span>
|
||||
</div>
|
||||
<div id="results" role="listbox">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if config.extra.theme == "toggle" %}
|
||||
<a id="dark-mode-toggle"
|
||||
onclick="toggleTheme(); event.preventDefault();"
|
||||
href="#">
|
||||
<img src="{{ get_url(path='icons/sun.svg') }}" id="sun-icon" alt="Light" />
|
||||
<img src="{{ get_url(path='icons/moon.svg') }}"
|
||||
id="moon-icon"
|
||||
style="filter: invert(1)"
|
||||
alt="Dark" />
|
||||
<img src="{{ get_url(path='icons/auto.svg') }}"
|
||||
id="auto-icon"
|
||||
style="filter: invert(1)"
|
||||
alt="Auto" />
|
||||
</a>
|
||||
|
||||
<!-- Initialize the theme toggle icons -->
|
||||
<script>
|
||||
updateItemToggleTheme()
|
||||
</script>
|
||||
{% endif %}
|
||||
</div>
|
||||
</nav>
|
||||
Loading…
Add table
Add a link
Reference in a new issue