-
Notifications
You must be signed in to change notification settings - Fork 0
Enhance SEO with llms.txt, sitemap updates, and RSS feed support #21
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -10,31 +10,37 @@ | |
| {% set imageAlt = imageAlt or "CourtNotes site logo" %} | ||
| {% set absolutePageUrl = page.url | absoluteUrl(metadata.siteUrl) %} | ||
| {% set fullImage %}{% if image and image.indexOf('http') == 0 %}{{ image }}{% else %}{{ image | absoluteUrl(metadata.siteUrl) }}{% endif %}{% endset %} | ||
|
|
||
|
|
||
|
|
||
| {% set isPost = tags and tags.indexOf('post') > -1 %} | ||
|
|
||
| {# Meta tags for SEO #} | ||
| <title>{{ seoTitle }}</title> | ||
| <meta name="description" content="{{ description }}"> | ||
| <meta name="author" content="{{ author }}"> | ||
| <meta name="keywords" content="courtney fradreck, notes, personal, blog, courtney fradreck's blog, reflections, software engineering, learning, tech, growth"> | ||
| <meta name="robots" content="index, follow"> | ||
|
|
||
| {# Canonical link #} | ||
| <link rel="canonical" href="{{ absolutePageUrl }}"> | ||
|
|
||
| {# RSS feed discovery #} | ||
| <link rel="alternate" type="application/rss+xml" title="{{ metadata.title }} RSS Feed" href="{{ '/feed.xml' | absoluteUrl(metadata.siteUrl) }}"> | ||
|
|
||
| <!-- Theme Colors for Light and Dark --> | ||
| <meta name="theme-color" content="#f2f2f7" media="(prefers-color-scheme: light)"> | ||
| <meta name="theme-color" content="#1c1c1e" media="(prefers-color-scheme: dark)"> | ||
|
|
||
| {# Open Graph for social previews #} | ||
| <meta property="og:title" content="{{ seoTitle }}"> | ||
| <meta property="og:description" content="{{ description }}"> | ||
| <meta property="og:type" content="article"> | ||
| <meta property="og:type" content="{% if isPost %}article{% else %}website{% endif %}"> | ||
| <meta property="og:url" content="{{ absolutePageUrl }}"> | ||
| <meta property="og:image" content="{{ fullImage }}"> | ||
| <meta property="og:image:alt" content="{{ imageAlt }}"> | ||
| <meta property="og:site_name" content="CourtNotes by {{ author }}"> | ||
| {% if isPost %} | ||
| <meta property="article:author" content="{{ author }}"> | ||
| <meta property="article:published_time" content="{{ date.toISOString() if date }}"> | ||
| {% endif %} | ||
|
|
||
| {# Twitter card #} | ||
| <meta name="twitter:card" content="summary_large_image"> | ||
|
|
@@ -45,47 +51,61 @@ | |
| <meta name="twitter:creator" content="@CourtneyFraddy"> | ||
| <meta name="twitter:site" content="@CourtneyFraddy"> | ||
|
|
||
|
|
||
|
|
||
| <!-- Schema.org JSON-LD --> | ||
| {# Schema.org JSON-LD — conditional by page type #} | ||
| {% if isPost %} | ||
| <script type="application/ld+json"> | ||
| { | ||
| "@context": "https://schema.org", | ||
| "@type": "Blog", | ||
| "name": "{{ seoTitle }}", | ||
| "url": "{{ absolutePageUrl }}", | ||
| "@type": "BlogPosting", | ||
| "mainEntityOfPage": { | ||
| "@type": "WebPage", | ||
| "@id": "{{ absolutePageUrl }}" | ||
| }, | ||
| "headline": "{{ title }}", | ||
| "description": "{{ description }}", | ||
| "image": ["{{ fullImage }}"], | ||
| "author": { | ||
| "@type": "Person", | ||
| "name": "{{ author }}" | ||
| }, | ||
| "publisher": { | ||
| "@type": "Organization", | ||
| "name": "{{ metadata.title }}", | ||
| "logo": { | ||
| "@type": "ImageObject", | ||
| "url": "{{ '/assets/logo.svg' | absoluteUrl(metadata.siteUrl) }}" | ||
| } | ||
| }, | ||
| "datePublished": "{{ date.toISOString() if date else '2024-01-01' }}", | ||
| "dateModified": "{{ date.toISOString() if date else '2024-01-01' }}" | ||
| } | ||
| </script> | ||
| {% elif title == "Home" %} | ||
| <script type="application/ld+json"> | ||
| { | ||
| "@context": "https://schema.org", | ||
| "@type": "WebSite", | ||
| "name": "{{ metadata.title }}", | ||
| "url": "{{ metadata.siteUrl }}", | ||
| "description": "{{ description }}", | ||
| "author": { | ||
| "@type": "Person", | ||
| "name": "{{ metadata.author }}" | ||
| } | ||
| } | ||
| </script> | ||
| {% else %} | ||
| <script type="application/ld+json"> | ||
| { | ||
| "@context": "https://schema.org", | ||
| "@type": "BlogPosting", | ||
| "mainEntityOfPage": { | ||
| "@type": "WebPage", | ||
| "@id": "{{ absolutePageUrl }}" | ||
| }, | ||
| "headline": "{{ seoTitle | safe }}", | ||
| "description": "{{ description | safe }}", | ||
| "image": ["{{ fullImage }}"], | ||
| "author": { | ||
| "@type": "Person", | ||
| "name": "{{ author or metadata.author }}" | ||
| }, | ||
| "publisher": { | ||
| "@type": "Organization", | ||
| "name": "{{ seoTitle }}", | ||
| "logo": { | ||
| "@type": "ImageObject", | ||
| "url": "{{ '/assets/logo.svg' | absoluteUrl(metadata.siteUrl) }}" | ||
| } | ||
| }, | ||
| "datePublished": "{{ date or '2024-01-01' }}", | ||
| "dateModified": "{{ date or '2024-01-01' }}" | ||
| } | ||
| </script> | ||
|
|
||
| { | ||
| "@context": "https://schema.org", | ||
| "@type": "WebPage", | ||
| "name": "{{ seoTitle }}", | ||
| "url": "{{ absolutePageUrl }}", | ||
| "description": "{{ description }}", | ||
| "author": { | ||
| "@type": "Person", | ||
| "name": "{{ metadata.author }}" | ||
| } | ||
| } | ||
| </script> | ||
| {% endif %} | ||
|
Comment on lines
+54
to
+111
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: find . -name "seo.njk" -type fRepository: CourtneyFradreck/CourtNotes Length of output: 94 🏁 Script executed: cat -n ./src/_includes/seo.njk | head -120Repository: CourtneyFradreck/CourtNotes Length of output: 4898 🏁 Script executed: find . -name ".eleventy.js" -o -name "eleventy.config.js" -o -name "*.config.js" | head -5Repository: CourtneyFradreck/CourtNotes Length of output: 85 🏁 Script executed: cat -n ./.eleventy.jsRepository: CourtneyFradreck/CourtNotes Length of output: 2143 🏁 Script executed: rg -i "filter|tojson|jsonencode|jsonescape" --type=js . | head -20Repository: CourtneyFradreck/CourtNotes Length of output: 415 🏁 Script executed: cat -n package.jsonRepository: CourtneyFradreck/CourtNotes Length of output: 493 🏁 Script executed: find src -name "*.md" -type f | head -3 | xargs head -30Repository: CourtneyFradreck/CourtNotes Length of output: 5280 🏁 Script executed: rg -i "nunjucks|njk" package.jsonRepository: CourtneyFradreck/CourtNotes Length of output: 53 🏁 Script executed: rg "tojson|toJSON" src/ --type=njkRepository: CourtneyFradreck/CourtNotes Length of output: 98 🏁 Script executed: rg "tojson|toJSON" src/Repository: CourtneyFradreck/CourtNotes Length of output: 53 Apply JSON escaping to all dynamic values in JSON-LD blocks. These template variables ( 🤖 Prompt for AI Agents |
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,24 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| --- | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| permalink: "feed.xml" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| eleventyExcludeFromCollections: true | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| --- | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <?xml version="1.0" encoding="UTF-8"?> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <channel> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <title>{{ metadata.title }}</title> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <link>{{ metadata.siteUrl }}</link> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <description>Personal blog by {{ metadata.author }} — covering software engineering, startup building, and learning in public.</description> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <language>en</language> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <atom:link href="{{ metadata.siteUrl }}/feed.xml" rel="self" type="application/rss+xml"/> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {%- for post in collections.post | reverse %} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <item> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <title>{{ post.data.title }}</title> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <link>{{ metadata.siteUrl }}{{ post.url }}</link> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <guid isPermaLink="true">{{ metadata.siteUrl }}{{ post.url }}</guid> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <description>{{ post.data.description }}</description> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <pubDate>{{ post.date.toUTCString() }}</pubDate> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+15
to
+19
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Escape RSS item fields before writing XML.
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <author>{{ metadata.author }}</author> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+8
to
+20
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <title>{{ metadata.title }}</title> | |
| <link>{{ metadata.siteUrl }}</link> | |
| <description>Personal blog by {{ metadata.author }} — covering software engineering, startup building, and learning in public.</description> | |
| <language>en</language> | |
| <atom:link href="{{ metadata.siteUrl }}/feed.xml" rel="self" type="application/rss+xml"/> | |
| {%- for post in collections.post | reverse %} | |
| <item> | |
| <title>{{ post.data.title }}</title> | |
| <link>{{ metadata.siteUrl }}{{ post.url }}</link> | |
| <guid isPermaLink="true">{{ metadata.siteUrl }}{{ post.url }}</guid> | |
| <description>{{ post.data.description }}</description> | |
| <pubDate>{{ post.date.toUTCString() }}</pubDate> | |
| <author>{{ metadata.author }}</author> | |
| <title>{{ metadata.title | escape }}</title> | |
| <link>{{ metadata.siteUrl | escape }}</link> | |
| <description>Personal blog by {{ metadata.author | escape }} — covering software engineering, startup building, and learning in public.</description> | |
| <language>en</language> | |
| <atom:link href="{{ (metadata.siteUrl ~ '/feed.xml') | escape }}" rel="self" type="application/rss+xml"/> | |
| {%- for post in collections.post | reverse %} | |
| <item> | |
| <title>{{ post.data.title | escape }}</title> | |
| <link>{{ (metadata.siteUrl ~ post.url) | escape }}</link> | |
| <guid isPermaLink="true">{{ (metadata.siteUrl ~ post.url) | escape }}</guid> | |
| <description>{{ post.data.description | escape }}</description> | |
| <pubDate>{{ post.date.toUTCString() }}</pubDate> | |
| <author>{{ metadata.author | escape }}</author> |
Copilot
AI
Apr 14, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
<author> in RSS 2.0 items is intended to be an email address (often in the form email (name)); a plain display name may fail validation in stricter feed readers. Consider removing <author>, adding a real email, or switching to dc:creator (with the proper namespace) for a name-only author field.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| --- | ||
| permalink: "llms.txt" | ||
| eleventyExcludeFromCollections: true | ||
| --- | ||
| # {{ site.title }} | ||
|
|
||
| > {{ site.description }} | ||
|
Comment on lines
+5
to
+7
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Description: Compare site metadata sources to detect inconsistencies
echo "=== Checking .eleventy.js siteMetadata ==="
rg -A 5 'const siteMetadata = \{' .eleventy.js
echo -e "\n=== Checking src/_data/metadata.js ==="
cat src/_data/metadata.js
echo -e "\n=== Checking other templates' data source usage ==="
rg -n 'metadata\.(siteUrl|title|author)' --type=njk src/
rg -n 'site\.(siteUrl|title|author|description)' --type=njk src/Repository: CourtneyFradreck/CourtNotes Length of output: 578 🏁 Script executed: #!/bin/bash
# Find all .njk template files mentioned in the review
echo "=== Locating .njk template files ==="
fd -e njk src/
echo -e "\n=== Content of llms.txt.njk ==="
cat src/llms.txt.njk
echo -e "\n=== Content of sitemap.xml.njk ==="
cat src/sitemap.xml.njk
echo -e "\n=== Content of robots.txt.njk ==="
cat src/robots.txt.njkRepository: CourtneyFradreck/CourtNotes Length of output: 2510 🏁 Script executed: #!/bin/bash
# Check how siteMetadata is exported/registered in .eleventy.js
echo "=== Checking .eleventy.js for how siteMetadata is made available ==="
cat .eleventy.jsRepository: CourtneyFradreck/CourtNotes Length of output: 1855 Standardize template data source usage. This template uses Standardize on a single data source across all templates. 🤖 Prompt for AI Agents |
||
|
|
||
| CourtNotes is a personal blog by {{ site.author }}, a Zimbabwe-based software engineering student, startup builder, and tech community lead. The site captures the journey of learning in public — from experiments in Eleventy to reflections on leadership, startup execution, and Africa-focused technology. | ||
|
|
||
| Courtney is the CEO and project lead at Proxyon Technologies, building AgriLease — a mobile-first marketplace helping farmers discover, book, and pay for agricultural equipment. He also serves as Tech Lead for GDG on Campus at his university. | ||
|
|
||
| ## Pages | ||
|
|
||
| - [About]({{ site.siteUrl }}/about/): Get to know Courtney Fradreck — the person behind CourtNotes. | ||
| - [Blog]({{ site.siteUrl }}/blog/): All blog posts and articles. | ||
|
|
||
| ## Blog Posts | ||
| {% for post in collections.post | reverse %} | ||
| - [{{ post.data.title }}]({{ site.siteUrl }}{{ post.url }}): {{ post.data.description }} | ||
| {%- endfor %} | ||
| Original file line number | Diff line number | Diff line change | ||
|---|---|---|---|---|
|
|
@@ -6,3 +6,4 @@ User-agent: * | |||
| Allow: / | ||||
|
|
||||
| Sitemap: {{ metadata.siteUrl }}/sitemap.xml | ||||
| Llms-Txt: {{ metadata.siteUrl }}/llms.txt | ||||
|
||||
| Llms-Txt: {{ metadata.siteUrl }}/llms.txt |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🌐 Web query:
Is "Llms-Txt" a recognized directive in robots.txt for advertising llms.txt files?
💡 Result:
No, "Llms-Txt" is not a recognized directive in robots.txt for advertising llms.txt files. The robots.txt standard, as defined by robotstxt.org, the original 1994 specification, RFC 9309, and Google's implementation, only recognizes specific directives: User-agent, Disallow, Allow, and Sitemap. No source mentions "Llms-Txt" as a valid directive. llms.txt is a separate proposed standard (from llmstxt.org, 2024) for AI guidance, distinct from robots.txt. Some non-standard practices suggest adding Allow: /llms.txt or informal lines like "LLMS: https://example.com/llms.txt" in robots.txt to hint at the file, but these are not part of the official specification and crawlers are not required to recognize or act on them.
Citations:
- 1: https://www.robotstxt.org/robotstxt
- 2: https://developers.google.com/crawling/docs/robots-txt/robots-txt-spec
- 3: https://ftp.registro.br/rfc/rfc9309.txt
- 4: https://www.robotstxt.org/orig.html
- 5: https://tools.ietf.org/html/rfc9309
- 6: https://llmstxt.org/
- 7: https://www.reddit.com/r/TechSEO/comments/1nlhvqa/can_robotstxt_be_used_to_allow_ai_crawling_of/
Remove the non-standard "Llms-Txt" directive from robots.txt.
"Llms-Txt" is not a recognized directive in the robots.txt specification (RFC 9309). Standard robots.txt parsers only recognize User-agent, Disallow, Allow, and Sitemap. This directive will be ignored by all standard crawlers.
The llms.txt standard is a separate specification (from llmstxt.org, 2024) distinct from robots.txt. To advertise an llms.txt file, use the official Sitemap directive instead, or ensure the file is accessible at the standard path (/llms.txt) where AI crawlers can discover it.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/robots.txt.njk` at line 9, Remove the non-standard "Llms-Txt: {{
metadata.siteUrl }}/llms.txt" directive from src/robots.txt.njk; either delete
that line or replace it with a standard directive such as "Sitemap: {{
metadata.siteUrl }}/llms.txt" so crawlers can discover the llms.txt via the
recognized Sitemap directive, and ensure the llms.txt file is served at the
standard path (/llms.txt).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This duplicates the RSS autodiscovery link already emitted by
seo.njk(included on line 6). Keeping both results in two identical<link rel="alternate" ...>tags in the page head; remove one (prefer keeping it centralized inseo.njk).