Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# CLAUDE.md

Personal site & blog for Yonatan Lourie — `https://yonatanlou.github.io`.

## Stack & deploy

- **Eleventy (11ty) v3** static site generator, ES modules (`"type": "module"`).
- Deployed to **GitHub Pages** via `.github/workflows/gh-pages.yml` (`peaceiris/actions-gh-pages`), which runs `npm run build-ghpages` on push to `main`. PRs build but do not deploy.
- **Static hosting only**: no custom HTTP headers, no request-time content negotiation, no server/edge. Anything requiring those is out of scope here.
- `netlify.toml` / `vercel.json` exist but are not the live pipeline.

## Commands

```bash
npm install
npx @11ty/eleventy --serve # local dev at http://localhost:8080
npm run build # build to _site/ (add ELEVENTY_RUN_MODE=build to drop drafts)
```

## Layout

- `content/` — all pages & posts (Eleventy `input` dir). Posts live in `content/blog/**`, each a Markdown file with `title`/`description`/`date` frontmatter; `content/blog/blog.11tydata.js` tags them `posts` and applies `layouts/post.njk`.
- `_includes/layouts/` — `base.njk` (the HTML shell / `<head>`), `home.njk`, `post.njk`.
- `_includes/` — shared partials (e.g. `structured-data.njk`, `postslist.njk`).
- `_data/metadata.js` — site-wide metadata (title, url, author, social `sameAs`, OG image, twitter handle). `_config/filters.js` — custom filters (`readableDate`, `htmlDateString`, `filterTagList`, …).
- `public/` — static assets copied to site root (CSS, `robots.txt`); `content/img/` → `/img`.
- Output goes to `_site/` (gitignored). Never commit `_site/` or `.cache/`.

## Conventions

- Match existing style; keep files small. Comments only when necessary.
- Nunjucks auto-escapes `{{ }}` — use `| safe` when emitting raw Markdown/HTML (e.g. `post.rawInput`).
- Build absolute URLs with `{{ page.url | htmlBaseUrl(metadata.url) }}`.
- Templates that should not appear in collections/sitemap set `eleventyExcludeFromCollections: true`.

## Agent-facing / AI-crawler features

This site is deliberately optimized for AI agents and LLM crawlers (design doc: `docs/superpowers/specs/2026-07-15-agent-ready-design.md`). When adding or changing content, keep these in sync:

- **`public/robots.txt`** — allows named AI agents (GPTBot, ClaudeBot, PerplexityBot, Google-Extended, CCBot, …) with a `Content-Signal: ai-train=yes, search=yes, ai-input=yes` per block, plus the `Sitemap:` line. Named-bot blocks must repeat their own rules (no inheritance from `*`).
- **`content/llms.txt.njk` → `/llms.txt`** — llmstxt.org index: one bullet per post (title + description + absolute URL) and key pages. Auto-generated from `collections.posts`, so new posts appear automatically as long as they have a `description`.
- **`content/llms-full.txt.njk` → `/llms-full.txt`** — every post's raw Markdown (`post.rawInput`) concatenated, newest first.
- **`content/blog/post-md.njk`** — paginates `collections.posts` to emit a clean `<post-url>index.md` for every post, advertised on the HTML page via `<link rel="alternate" type="text/markdown">`.
- **`_includes/structured-data.njk`** — JSON-LD included from `base.njk`: `BlogPosting` on post pages, `WebSite` + `Person` elsewhere. Values are emitted with `| dump` for valid JSON.
- **`base.njk` `<head>`** — page-aware `<title>`, `author`, canonical, Open Graph, Twitter Card, and sitewide `<link rel="describedby" href="/llms.txt">`.

**Every new post needs a `description` in its frontmatter** — it feeds `llms.txt`, the meta description, OG/Twitter, and JSON-LD. Post source filenames must be clean ASCII (a stray control char once produced a broken `%03` URL).

After changing any of the above, rebuild and sanity-check that `/robots.txt`, `/llms.txt`, `/llms-full.txt`, and the per-post `index.md` files are generated and that JSON-LD parses as valid JSON.
13 changes: 12 additions & 1 deletion _data/metadata.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,19 @@ export default {
author: {
name: "Yonatan Lourie",
email: "yonatanlou@gmail.com",
url: "https://yonatanlou.github.io/about-me/"
url: "https://yonatanlou.github.io/about/"
},
// Default image used for Open Graph / Twitter cards.
image: "/img/favicon/android-chrome-512x512.png",
// Twitter/X handle used for card attribution.
twitter: "@yonatanlou",
// Canonical profiles, reused by JSON-LD `sameAs` and the footer.
sameAs: [
"https://github.com/yonatanlou",
"https://www.linkedin.com/in/yonatanlourie/",
"https://x.com/yonatanlou",
"https://www.goodreads.com/user/show/103722180-yonatan-lourie"
],
gtag: "G-C6PG57BBFC"


Expand Down
28 changes: 27 additions & 1 deletion _includes/layouts/base.njk
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,35 @@
<link rel="icon" type="image/png" sizes="512x512" href="/android-chrome-512x512.png">
<link rel="manifest" href="/site.webmanifest">

<title>{{ metadata.title }}</title>
<title>{% if title %}{{ title }} | {{ metadata.title }}{% else %}{{ metadata.title }}{% endif %}</title>
<meta name="description" content="{{ description or metadata.description }}">
<meta name="author" content="{{ metadata.author.name }}">
<link rel="alternate" href="feed/feed.xml" type="application/atom+xml" title="{{ metadata.title }}">

{#- Canonical + agent-readable resources -#}
<link rel="canonical" href="{{ page.url | htmlBaseUrl(metadata.url) }}">
<link rel="describedby" href="/llms.txt" type="text/markdown">
{%- if tags and "posts" in tags %}
<link rel="alternate" type="text/markdown" href="{{ page.url }}index.md">
{%- endif %}

{#- Open Graph -#}
<meta property="og:site_name" content="{{ metadata.title }}">
<meta property="og:type" content="{% if tags and 'posts' in tags %}article{% else %}website{% endif %}">
<meta property="og:title" content="{{ title or metadata.title }}">
<meta property="og:description" content="{{ description or metadata.description }}">
<meta property="og:url" content="{{ page.url | htmlBaseUrl(metadata.url) }}">
<meta property="og:image" content="{{ metadata.image | htmlBaseUrl(metadata.url) }}">

{#- Twitter Card -#}
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:creator" content="{{ metadata.twitter }}">
<meta name="twitter:title" content="{{ title or metadata.title }}">
<meta name="twitter:description" content="{{ description or metadata.description }}">
<meta name="twitter:image" content="{{ metadata.image | htmlBaseUrl(metadata.url) }}">

{#- Structured data (JSON-LD) -#}
{% include "structured-data.njk" %}
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/Primer/19.1.1/tooltips.min.css">

Expand Down
58 changes: 58 additions & 0 deletions _includes/structured-data.njk
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
{%- if tags and "posts" in tags -%}
{#- Blog post pages -> BlogPosting -#}
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "BlogPosting",
"headline": {{ title | dump | safe }},
"description": {{ (description or metadata.description) | dump | safe }},
"datePublished": {{ (page.date | htmlDateString) | dump | safe }},
"dateModified": {{ (page.date | htmlDateString) | dump | safe }},
"url": {{ (page.url | htmlBaseUrl(metadata.url)) | dump | safe }},
"mainEntityOfPage": {{ (page.url | htmlBaseUrl(metadata.url)) | dump | safe }},
"inLanguage": {{ metadata.language | dump | safe }},
{%- set postKeywords = tags | filterTagList | join(", ") %}
{%- if postKeywords | length %}
"keywords": {{ postKeywords | dump | safe }},
{%- endif %}
"author": {
"@type": "Person",
"name": {{ metadata.author.name | dump | safe }},
"url": {{ metadata.author.url | dump | safe }},
"sameAs": {{ metadata.sameAs | dump | safe }}
},
"publisher": {
"@type": "Person",
"name": {{ metadata.author.name | dump | safe }},
"url": {{ metadata.url | dump | safe }}
}
}
</script>
{%- else -%}
{#- All other pages -> WebSite + Person -#}
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@graph": [
{
"@type": "WebSite",
"name": {{ metadata.title | dump | safe }},
"url": {{ metadata.url | dump | safe }},
"description": {{ metadata.description | dump | safe }},
"inLanguage": {{ metadata.language | dump | safe }},
"author": { "@type": "Person", "name": {{ metadata.author.name | dump | safe }} }
},
{
"@type": "Person",
"name": {{ metadata.author.name | dump | safe }},
"url": {{ metadata.author.url | dump | safe }},
"email": {{ metadata.author.email | dump | safe }},
"jobTitle": "AI Researcher",
"worksFor": { "@type": "Organization", "name": "Tavily", "url": "https://www.tavily.com/" },
"alumniOf": { "@type": "CollegeOrUniversity", "name": "Tel Aviv University" },
"sameAs": {{ metadata.sameAs | dump | safe }}
}
]
}
</script>
{%- endif -%}
22 changes: 22 additions & 0 deletions content/blog/post-md.njk
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
---
pagination:
data: collections.posts
size: 1
alias: post
permalink: "{{ post.url }}index.md"
layout: false
eleventyExcludeFromCollections: true
---
# {{ post.data.title }}

- URL: {{ post.url | htmlBaseUrl(metadata.url) }}
- Published: {{ post.date | htmlDateString }}
{%- if post.data.description %}
- Summary: {{ post.data.description }}
{%- endif %}
{%- set filteredTags = post.data.tags | filterTagList %}
{%- if filteredTags | length %}
- Tags: {% for tag in filteredTags %}{{ tag }}{% if not loop.last %}, {% endif %}{% endfor %}
{%- endif %}

{{ post.rawInput | safe }}
22 changes: 22 additions & 0 deletions content/llms-full.txt.njk
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
---
permalink: /llms-full.txt
layout: false
eleventyExcludeFromCollections: true
---
# Yonatan Lourie — Full Blog Text

> Every post on yonatanlou.github.io concatenated as raw Markdown, newest first.
{% for post in collections.posts | reverse %}

---

# {{ post.data.title }}

- URL: {{ post.url | htmlBaseUrl(metadata.url) }}
- Published: {{ post.date | htmlDateString }}
{%- if post.data.description %}
- Summary: {{ post.data.description }}
{%- endif %}

{{ post.rawInput | safe }}
{%- endfor %}
24 changes: 24 additions & 0 deletions content/llms.txt.njk
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
---
permalink: /llms.txt
layout: false
eleventyExcludeFromCollections: true
---
# Yonatan Lourie

> AI Researcher at Tavily building web infrastructure for agents. MSc in Statistics & Data Science from Tel Aviv University (Dead Sea Scrolls authorship attribution with NLP and Graph Neural Networks). This site collects writing on machine learning, clustering, NLP, and ML engineering.

## Blog
{% for post in collections.posts | reverse %}
- [{{ post.data.title }}]({{ post.url | htmlBaseUrl(metadata.url) }}): {{ post.data.description }}
{%- endfor %}

## Pages

- [About]({{ "/about/" | htmlBaseUrl(metadata.url) }}): who I am and what I work on.
- [Now]({{ "/now.html" | htmlBaseUrl(metadata.url) }}): what I'm focused on at the moment.
- [Projects]({{ "/projects/" | htmlBaseUrl(metadata.url) }}): selected projects and code.
- [Links]({{ "/Links/" | htmlBaseUrl(metadata.url) }}): things worth reading and following.

## Optional

- [Full text of all posts]({{ "/llms-full.txt" | htmlBaseUrl(metadata.url) }}): every post concatenated as raw Markdown.
55 changes: 55 additions & 0 deletions docs/superpowers/specs/2026-07-15-agent-ready-design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# Making the site agent-ready — design

Goal: make `yonatanlou.github.io` discoverable and consumable by AI agents and
LLM crawlers (ChatGPT, Claude, Perplexity, Gemini, etc.), following the
practices in [jlhernando.com](https://jlhernando.com/blog/make-static-site-agent-ready/)
and [joost.blog](https://joost.blog/agent-ready/).

## Constraint

The site is a static Eleventy build deployed to **GitHub Pages**. GitHub Pages
serves static files only — no custom HTTP headers and no request-time content
negotiation. So techniques that require an edge (HTTP `Link` headers,
`Accept: text/markdown` negotiation, MCP/WebMCP endpoints) are **out of scope**.
Everything below is achievable with static output.

## What we ship

1. **`/robots.txt`** (`public/robots.txt`) — explicit `Allow` for named AI agents
(GPTBot, ClaudeBot, PerplexityBot, Google-Extended, CCBot, …), a
`Content-Signal: ai-train=yes, search=yes, ai-input=yes` line, and a
`Sitemap:` reference. Named-bot blocks repeat their own rules (they do not
inherit from `User-agent: *`).

2. **`/llms.txt`** (`content/llms.txt.njk`) — llmstxt.org index: site summary,
a bullet per post (title + description + absolute URL), key pages, and a link
to `/llms-full.txt`.

3. **`/llms-full.txt`** (`content/llms-full.txt.njk`) — every post's raw
Markdown (`page.rawInput`) concatenated newest-first.

4. **Per-post Markdown** (`content/blog/post-md.njk`) — for each post, a static
`…/index.md` with clean, chrome-free source, advertised on the HTML page via
`<link rel="alternate" type="text/markdown">`.

5. **JSON-LD structured data** (`_includes/structured-data.njk`, included in
`base.njk`) — `BlogPosting` on posts; `WebSite` + `Person` (with `sameAs`,
`jobTitle`, `worksFor`, `alumniOf`) elsewhere.

6. **Meta tags** (`base.njk`) — page-aware `<title>`, `author`, canonical URL,
Open Graph, Twitter Card, and a sitewide `<link rel="describedby" href="/llms.txt">`.

7. **`_data/metadata.js`** — adds default OG `image`, `twitter` handle, and a
`sameAs` social list reused by the JSON-LD.

## Incidental fix

The newest post's source file had a stray control character (`\003`) at the
start of its filename, producing a broken `%03` URL on the live site. Renamed to
a clean filename.

## Explicitly out of scope

HTTP `Link` headers, `Accept`-based content negotiation, `.well-known/skills.json`,
MCP/WebMCP, `entitymap.json` — either impossible on GitHub Pages or overkill for
a personal blog.
67 changes: 67 additions & 0 deletions public/robots.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# robots.txt — yonatanlou.github.io
# This site welcomes AI crawlers, search-grounding agents, and assistants.
# Content-Signal follows the Cloudflare proposal (https://developers.cloudflare.com/bots/concepts/content-signals-policy/).

User-agent: *
Allow: /
Content-Signal: ai-train=yes, search=yes, ai-input=yes

# --- Named AI / search agents (rules do not inherit from "*", so repeat them) ---

User-agent: GPTBot
Allow: /
Content-Signal: ai-train=yes, search=yes, ai-input=yes

User-agent: ChatGPT-User
Allow: /
Content-Signal: ai-train=yes, search=yes, ai-input=yes

User-agent: OAI-SearchBot
Allow: /
Content-Signal: ai-train=yes, search=yes, ai-input=yes

User-agent: ClaudeBot
Allow: /
Content-Signal: ai-train=yes, search=yes, ai-input=yes

User-agent: Claude-User
Allow: /
Content-Signal: ai-train=yes, search=yes, ai-input=yes

User-agent: anthropic-ai
Allow: /
Content-Signal: ai-train=yes, search=yes, ai-input=yes

User-agent: PerplexityBot
Allow: /
Content-Signal: ai-train=yes, search=yes, ai-input=yes

User-agent: Perplexity-User
Allow: /
Content-Signal: ai-train=yes, search=yes, ai-input=yes

User-agent: Google-Extended
Allow: /
Content-Signal: ai-train=yes, search=yes, ai-input=yes

User-agent: Applebot-Extended
Allow: /
Content-Signal: ai-train=yes, search=yes, ai-input=yes

User-agent: CCBot
Allow: /
Content-Signal: ai-train=yes, search=yes, ai-input=yes

User-agent: Meta-ExternalAgent
Allow: /
Content-Signal: ai-train=yes, search=yes, ai-input=yes

User-agent: cohere-ai
Allow: /
Content-Signal: ai-train=yes, search=yes, ai-input=yes

User-agent: Bytespider
Allow: /
Content-Signal: ai-train=yes, search=yes, ai-input=yes

Sitemap: https://yonatanlou.github.io/sitemap.xml
Loading