Skip to content

fix: derive a title from the URL when an external post has none - #12

Merged
alshedivat merged 1 commit into
mainfrom
claude/al-ext-posts-empty-title-xrj39u
Jul 27, 2026
Merged

fix: derive a title from the URL when an external post has none#12
alshedivat merged 1 commit into
mainfrom
claude/al-ext-posts-empty-title-xrj39u

Conversation

@alshedivat

Copy link
Copy Markdown
Contributor

The defect

A degraded fetch left external posts with an empty title, which was then published.

fetch_content_from_url returns '' when the request fails or the page has no <title>, and an RSS <item> can carry a blank <title> of its own. create_document copied that straight into doc.data['title'].

Reproduced in a real build of the al-folio starter, where the Google Blog entry in external_sources produced:

<a class="post-title" href="https://blog.google/technology/ai/google-gemini-update-flash-ai-assistant-io-2024/" target="_blank" rel="external nofollow noopener"></a>

a blank but clickable row in the blog index and on the home page, an untitled entry in the search index (id: "post-"), and 103 Warning: Empty `slug` generated for ''. lines from Jekyll (jekyll/utils.rb:222) slugifying that empty title downstream. With external_sources: [] the same build emits 0 such warnings.

How we got here

Neither is wrong, but neither filled in the title. The failure mode went from "build crashes" to "build silently publishes a blank post". This PR fixes the remaining half. (al_ext_posts has its own private slugify and never calls Jekyll::Utils.slugify; the Jekyll warnings come from Jekyll slugifying the synthetic posts' empty titles downstream, so fixing the title fixes the warnings too.)

The fix

Derive rather than skip. These URLs are explicitly listed by the user in _config.yml, so dropping them would silently lose content they asked for. The entry is kept, made readable, and the degradation is reported.

resolve_title (lib/al_ext_posts.rb:113) returns the title when it carries any word character, and otherwise derives one from the URL's last meaningful path segment and logs a warning naming the URL:

ExternalPosts: No title found for https://blog.google/technology/ai/google-gemini-update-flash-ai-assistant-io-2024/ - using "Google Gemini Update Flash Ai Assistant Io 2024" derived from the URL.

It is called from create_document, the single funnel both the RSS path (fetch_from_rssprocess_entries) and the explicit-URL path (fetch_from_urls) pass through, so an RSS item with a missing or blank <title> gets exactly the same treatment.

Post slugs, and therefore post URLs, are unchanged: build_slug still receives the raw title and still uses its source-name fallback. The built sitemap is byte-identical across the before/after builds.

The usability check for a display title is /[[:word:]]/ (Unicode-aware), not the ASCII \w that build_slug uses for a filesystem slug — so a CJK title like 你好世界 is preserved as the title even though the slug still falls back.

Edge cases

URL Title
.../google-gemini-update-flash-ai-assistant-io-2024/ Google Gemini Update Flash Ai Assistant Io 2024
https://example.com/posts/my-post/ (trailing slash) My Post
.../my-post.html?utm_source=x#intro (extension, query, fragment) My Post
.../hello%20world%21 (percent-encoding) Hello World
.../my_post_title My Post Title
https://example.com/archive/2024/05/deep-dive/ Deep Dive (numeric segments skipped)
https://example.com/posts/12345 (numeric last segment) Posts
https://example.com/posts/---/ (no words in last segment) Posts
https://example.com/post.v2 (unknown extension kept) Post V2
https://blog.google (domain only) blog.google
https://www.example.com/ example.com
https://example.com///// (empty segments only) example.com
https://example.com/2024/05/12/ (no segment carries words) example.com
nil, '' External post

Malformed input never raises: URI::Error falls back to trimming the query/fragment by hand, bad percent-escapes keep the raw segment, and invalid UTF-8 bytes are scrubbed before any regexp match. The function cannot return an empty string.

Verification

Rebuilt the al-folio starter against this branch, in a sandbox with no general outbound network — a faithful simulation of the degraded-fetch case.

before after
Empty `slug` generated warnings 103 0
empty post-title anchors in blog/index.html 1 0

The blog index now renders:

<a class="post-title" href="https://blog.google/..." target="_blank" rel="external nofollow noopener">Google Gemini Update Flash Ai Assistant Io 2024</a>

The home page news-title anchor and the search-index entry (id: "post-"id: "post-google-gemini-update-flash-ai-assistant-io-2024", with a real title) are fixed by the same change. The sitemap is unchanged, confirming no URL churn.

Tests

Added coverage for a missing title, a blank/whitespace title, a title of only non-word characters, a preserved non-Latin title, the warning naming the URL, every URL edge case above, a never-empty/never-raises sweep over malformed URLs, an end-to-end create_document assertion (title filled, slug unchanged), and the RSS path via process_entries.

CaptureGenerator#initialize now accepts and ignores an argument, because Jekyll::Site#setup instantiates every Generator subclass with the site config once a test builds a real site.

23 runs, 77 assertions, 0 failures, 0 errors, 0 skips

npm run lint:prettier passes.

Release note

Version bumped 1.0.2 → 1.0.3. 1.0.2 was never published to RubyGemsmain is ahead of the published 1.0.1 — so the eventual release should cover both the 1.0.2 slug-generation optimization (#11) and this 1.0.3 fix.


Generated by Claude Code

A degraded fetch left external posts with an empty title. `fetch_content_from_url`
returns `''` when the request fails or the page has no `<title>`, and RSS items can
carry a blank `<title>` of their own; `create_document` copied that straight into
`doc.data['title']`.

The result in a real build of the al-folio starter was a blank but clickable row in
the blog index and on the home page, an untitled entry in the search index
(`id: "post-"`), and 103 `Warning: Empty \`slug\` generated for ''.` lines from
Jekyll slugifying the empty title downstream.

#10 made `build_slug` nil-safe, which stopped the build aborting with a
NoMethodError, and #11 optimized the slug pass. Neither filled in the title, so the
failure mode went from "build crashes" to "build silently publishes a blank post".
This fixes the remaining half.

These URLs are listed in `_config.yml` by the user, so the entry is kept rather than
dropped: `resolve_title` derives a readable title from the last meaningful path
segment of the URL and logs a warning naming that URL, so the degradation is visible.
Trailing slashes, query strings, fragments, percent-escapes, page extensions and
segments with no words of their own (ids, `/2024/05/` date parts) are all handled; a
bare domain falls back to its host. It never raises and never returns an empty string.

The slug still comes from the raw title, so post URLs are unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@alshedivat
alshedivat merged commit 3849326 into main Jul 27, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant