From cf38794f2293d402b27b3d9cf87784bbe23574ac Mon Sep 17 00:00:00 2001 From: GianRomani Date: Mon, 17 Aug 2026 12:40:52 +0200 Subject: [PATCH 1/2] feat(seo): automate meta description extraction and structured schema (GIA-40) --- custom-site-assets/convert.py | 65 +++++++ custom-site-assets/templates/macros/head.html | 171 ++++++++++++++++++ 2 files changed, 236 insertions(+) create mode 100644 custom-site-assets/templates/macros/head.html diff --git a/custom-site-assets/convert.py b/custom-site-assets/convert.py index bf23192..0a9ed3e 100644 --- a/custom-site-assets/convert.py +++ b/custom-site-assets/convert.py @@ -12,6 +12,66 @@ write_settings, ) + +def extract_note_description(lines: List[str]) -> str: + """Extract a concise, clean summary (140-160 chars) from the markdown note content for SEO and OpenGraph.""" + cleaned_chunks: List[str] = [] + in_code_block = False + in_frontmatter = False + + for line in lines: + stripped = line.strip() + + # Handle YAML frontmatter delimiters + if stripped == "---": + in_frontmatter = not in_frontmatter + continue + if in_frontmatter: + continue + + # Handle code blocks + if stripped.startswith("```"): + in_code_block = not in_code_block + continue + if in_code_block: + continue + + # Skip empty lines, metadata markers, headings, list markers, quotes, and images + if not stripped or stripped.startswith( + ("#", "!", ">", "Created:", "Updated:", "|", "$$") + ): + continue + + # Remove wikilinks [[Target|Alias]] -> Alias, [[Target]] -> Target + text = re.sub(r"\[\[(?:[^|\]]*\|)?([^\]]+)\]\]", r"\1", stripped) + # Remove standard markdown links [text](url) -> text + text = re.sub(r"\[([^\]]+)\]\([^)]+\)", r"\1", text) + # Remove markdown bold/italics/code/strikethrough + text = re.sub(r"[*_`~]", "", text) + # Remove html tags + text = re.sub(r"<[^>]+>", "", text) + # Normalize whitespace + text = " ".join(text.split()) + + if text: + cleaned_chunks.append(text) + if sum(len(c) for c in cleaned_chunks) >= 150: + break + + full_summary = " ".join(cleaned_chunks) + # Sanitize double quotes and newlines for YAML frontmatter + full_summary = full_summary.replace('"', '\\"').replace("\n", " ").strip() + + if not full_summary: + return "Notes and research on machine learning, AI security, and cybersecurity." + + if len(full_summary) > 155: + # Trim cleanly at word boundary + truncated = full_summary[:155].rsplit(" ", 1)[0] + return f"{truncated}..." + return full_summary + + if __name__ == "__main__": Settings.parse_env() Settings.sub_file(site_dir / "config.toml") @@ -47,14 +107,19 @@ words = " ".join(content).split() reading_time = max(1, (len(words) + 199) // 200) + # Extract description for SEO & OpenGraph + description = extract_note_description(content) + content_frontmatter = [ "---", f'title: "{doc_path.page_title}"', + f'description: "{description}"', f"date: {doc_path.modified}", f"updated: {doc_path.modified}", "template: docs/page.html", "extra:", f" reading_time: {reading_time}", + f' meta_description: "{description}"', "---", # To add last line-break "", diff --git a/custom-site-assets/templates/macros/head.html b/custom-site-assets/templates/macros/head.html new file mode 100644 index 0000000..d9f7ec5 --- /dev/null +++ b/custom-site-assets/templates/macros/head.html @@ -0,0 +1,171 @@ +{% macro resource() %} + + + + +{% endmacro %} + + +{% macro stylesheet() %} + +{% endmacro %} + + +{% macro favicons() %} + + + + + {% if not config.extra.is_netlify %} + + {% endif %} +{% endmacro %} + + +{# SEO & OpenGraph Macro #} +{% macro seo( + title="", + title_addition="", + description="", + type="website", + is_home=false, + is_404=false, + is_page=false, + page_images="", + page_section="", + created_time="2026-08-17T12:00:00+02:00", + updated_time="2026-08-17T12:00:00+02:00" + ) +%} + +{% if is_404 %} + +{% else %} + + + +{% endif %} + +{% if current_url %} + {% set page_url = current_url %} +{% else %} + {% set page_url = get_url(path="404.html") %} +{% endif %} + +{% if description and description != "" %} + {% set meta_desc = description %} +{% else %} + {% set meta_desc = "Gianfranco Romani's digital garden and second brain covering Machine Learning, AI Security, MLOps, and Cybersecurity." %} +{% endif %} + +{% if title and title != "" %} + {% set page_title = title %} +{% else %} + {% set page_title = config.title | default(value="Gianfranco's Notes") %} +{% endif %} + +{% if title_addition and title_addition != "" %}{{ title_addition }}{% else %}{{ page_title }}{% endif %} + + + +{# Twitter Card Meta Tags #} + + + + + + +{# OpenGraph Meta Tags #} + + + + + + + +{# Structured Data (JSON-LD) #} +{% if is_home %} + + +{% endif %} + +{% if is_page %} + +{% endif %} + +{% set url_prefix = get_url(path="/") | split(pat="://") | first %} +{% set url_main = get_url(path="/") | split(pat="://") | last %} +{% set url_item = url_prefix ~ "://" ~ url_main ~ "/" %} + + +{% if config.extra.ganalytics %} + + +{% endif %} +{% endmacro %} From bdb0558587fb952e02df5db15ebae65b17042247 Mon Sep 17 00:00:00 2001 From: GianRomani Date: Tue, 18 Aug 2026 19:08:09 +0200 Subject: [PATCH 2/2] fix(seo): sanitize frontmatter descriptions and resolve 404/breadcrumb URL resolution (GIA-40) --- custom-site-assets/convert.py | 10 ++++++++-- custom-site-assets/templates/macros/head.html | 8 +++----- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/custom-site-assets/convert.py b/custom-site-assets/convert.py index 0a9ed3e..6bdcbbd 100644 --- a/custom-site-assets/convert.py +++ b/custom-site-assets/convert.py @@ -46,10 +46,14 @@ def extract_note_description(lines: List[str]) -> str: text = re.sub(r"\[\[(?:[^|\]]*\|)?([^\]]+)\]\]", r"\1", stripped) # Remove standard markdown links [text](url) -> text text = re.sub(r"\[([^\]]+)\]\([^)]+\)", r"\1", text) + # Remove LaTeX inline math $...$ + text = re.sub(r"\$[^$]+\$", "", text) # Remove markdown bold/italics/code/strikethrough text = re.sub(r"[*_`~]", "", text) # Remove html tags text = re.sub(r"<[^>]+>", "", text) + # Remove backslashes and replace double quotes with single quotes + text = text.replace("\\", "").replace('"', "'") # Normalize whitespace text = " ".join(text.split()) @@ -59,8 +63,10 @@ def extract_note_description(lines: List[str]) -> str: break full_summary = " ".join(cleaned_chunks) - # Sanitize double quotes and newlines for YAML frontmatter - full_summary = full_summary.replace('"', '\\"').replace("\n", " ").strip() + # Sanitize backslashes, double quotes, and newlines + full_summary = ( + full_summary.replace("\\", "").replace('"', "'").replace("\n", " ").strip() + ) if not full_summary: return "Notes and research on machine learning, AI security, and cybersecurity." diff --git a/custom-site-assets/templates/macros/head.html b/custom-site-assets/templates/macros/head.html index d9f7ec5..f4113ea 100644 --- a/custom-site-assets/templates/macros/head.html +++ b/custom-site-assets/templates/macros/head.html @@ -49,7 +49,7 @@ {% if current_url %} {% set page_url = current_url %} {% else %} - {% set page_url = get_url(path="404.html") %} + {% set page_url = config.base_url ~ "/404.html" %} {% endif %} {% if description and description != "" %} @@ -141,9 +141,7 @@ {% endif %} -{% set url_prefix = get_url(path="/") | split(pat="://") | first %} -{% set url_main = get_url(path="/") | split(pat="://") | last %} -{% set url_item = url_prefix ~ "://" ~ url_main ~ "/" %} +{% set url_item = config.base_url ~ "/" %} -{% if config.extra.ganalytics %} +{% if config.extra.ganalytics and config.extra.ganalytics != "" %}