This is an automated email from the ASF dual-hosted git repository.
davsclaus pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel-website.git
The following commit(s) were added to refs/heads/main by this push:
new dc259de6 Fix #1763: publish every blog post as Markdown at
<post>/index.md
dc259de6 is described below
commit dc259de6476fb73161015de9f1173f1e63e6bc9c
Author: Claus Ibsen <[email protected]>
AuthorDate: Fri Sep 18 15:55:03 2026 +0200
Fix #1763: publish every blog post as Markdown at <post>/index.md
Blog posts are now converted from the rendered HTML like the documentation
pages, so Hugo's draft and date rules, permalinks and shortcodes are
honoured. The page chrome (back link, avatars, share links, table of
contents, previous/next, featured image, related posts) is dropped and the
date, authors and categories become one byline under the title. Embedded
video iframes become links. The generated blog/index.md now links to
files that exist.
Closes #1770
---
gulp/helpers/turndown-config.js | 10 ++++++++++
gulp/tasks/generate-markdown.js | 35 +++++++++++++++++++++++++++++++++--
llms-txt-template.md | 2 +-
test/generate-markdown-test.js | 33 +++++++++++++++++++++++++++++++++
4 files changed, 77 insertions(+), 3 deletions(-)
diff --git a/gulp/helpers/turndown-config.js b/gulp/helpers/turndown-config.js
index 7b503ab8..7e6e40d8 100644
--- a/gulp/helpers/turndown-config.js
+++ b/gulp/helpers/turndown-config.js
@@ -81,6 +81,16 @@ function createTurndownService() {
}
});
+ // Embedded videos (the youtube shortcode renders an iframe) become a link,
Turndown would drop them
+ turndownService.addRule('iframeLink', {
+ filter: 'iframe',
+ replacement: function (content, node) {
+ const src = node.getAttribute('src');
+ if (!src) return '';
+ return `[${node.getAttribute('title') || 'Embedded content'}](${src})`;
+ }
+ });
+
return turndownService;
}
diff --git a/gulp/tasks/generate-markdown.js b/gulp/tasks/generate-markdown.js
index eccd1f6a..863fb0b8 100644
--- a/gulp/tasks/generate-markdown.js
+++ b/gulp/tasks/generate-markdown.js
@@ -20,7 +20,8 @@ const { generateAllIndexes } =
require('../helpers/html-index');
* Hugo renders every website page as <page>/index.html, so an index.html is
converted too
* when it is a content page (has an article.doc): the .md then sits next to
it as
* <page>/index.md, which keeps the page's relative links valid. List and
section pages
- * (home, download, community, ...) have no article.doc and are skipped.
+ * (home, download, community, ...) have no article.doc and are skipped. Blog
posts are
+ * content pages too and are reduced to their title, byline and body, see
trimBlogPost.
*/
async function generateMarkdown() {
const turndownService = createTurndownService();
@@ -34,7 +35,6 @@ async function generateMarkdown() {
const htmlFiles = glob.sync('public/**/*.html', {
ignore: [
'public/404.html',
- 'public/blog/**/index.html', // blog posts: their Markdown is to come
from the source, see #1763
'public/releases/**/index.html' // release pages are converted by
generateAllIndexes below
]
});
@@ -129,6 +129,10 @@ function convertPage(htmlContent, turndownService, {
articleOnly = false } = {})
return { markdown: null, repaired };
}
+ if (mainContent.classList.contains('post')) {
+ trimBlogPost(mainContent);
+ }
+
// Remove navigation elements, headers, footers and the embedded table of
contents from the content
const elementsToRemove = mainContent.querySelectorAll('nav, header, footer,
.nav, .navbar, .toolbar, aside.toc');
elementsToRemove.forEach(el => el.remove());
@@ -162,5 +166,32 @@ function convertPage(htmlContent, turndownService, {
articleOnly = false } = {})
return { markdown, repaired };
}
+/**
+ * Reduces a rendered blog post (layouts/blog/post.html) to its title, lead
and body, with the
+ * date, authors and categories from the rail folded into one line under the
title. The rail
+ * itself (avatars, share links, table of contents, previous/next), the "All
posts" link, the
+ * featured image and the related posts are page chrome and are dropped.
+ *
+ * @param {HTMLElement} article the article.post element
+ */
+function trimBlogPost(article) {
+ const date =
article.querySelector('time.post-date')?.getAttribute('datetime');
+ const authors = article.querySelectorAll('.post-author-name').map(el =>
el.text.trim());
+ const categories = article.querySelectorAll('.post-tags a').map(el =>
el.text.trim());
+
+ article.querySelectorAll('a.post-back, .post-tags, aside.post-rail,
img.featured, section.post-related')
+ .forEach(el => el.remove());
+
+ const byline = [
+ date && `Published ${date}`,
+ authors.length && `by ${authors.join(', ')}`,
+ categories.length && `in ${categories.join(', ')}`
+ ].filter(Boolean).join(' ');
+ const title = article.querySelector('h1.post-title');
+ if (byline && title) {
+ title.insertAdjacentHTML('afterend', `<p>${byline}</p>`);
+ }
+}
+
module.exports = generateMarkdown;
module.exports.convertPage = convertPage;
diff --git a/llms-txt-template.md b/llms-txt-template.md
index 827fcf27..f19d0154 100644
--- a/llms-txt-template.md
+++ b/llms-txt-template.md
@@ -486,7 +486,7 @@ For high-quality answers about routing, code generation,
and troubleshooting, AI
- [Download](https://camel.apache.org/download/): Current versions, LTS
releases, and which version to use.
- [Releases](https://camel.apache.org/releases/index.md): Apache Camel
releases and version history.
-- [Blog](https://camel.apache.org/blog/index.md): Latest blog posts about
Apache Camel.
+- [Blog](https://camel.apache.org/blog/index.md): Index of every blog post
with its date and summary; each post is available as Markdown at its URL plus
`index.md`, for example
`https://camel.apache.org/blog/2026/09/camel-local-model-benchmark/index.md`.
- [Camel by the
Numbers](https://camel.apache.org/blog/2026/06/camel-by-the-numbers/):
Data-driven overview — commits, contributors, release cadence, bug fix times,
and community health metrics.
- [The DNA of Apache
Camel](https://camel.apache.org/blog/2026/06/camel-dna-19-years/): 19 years of
backwards compatibility — why Camel users don't have to rewrite their
integrations every few years.
- [Who Maintains Apache
Camel](https://camel.apache.org/blog/2026/07/camel-who-maintains/):
Year-by-year commit data showing who maintains the project — the same core
team, through multiple acquisitions, contributing 80–95% of all commits every
year since 2007.
diff --git a/test/generate-markdown-test.js b/test/generate-markdown-test.js
index 1a962b9c..3bd9c0ce 100644
--- a/test/generate-markdown-test.js
+++ b/test/generate-markdown-test.js
@@ -113,3 +113,36 @@ test('a page with only a main is converted by default but
skipped when an articl
assert.equal(convertPage(html, createTurndownService()).markdown, '#
Downloads\n\nCards.')
assert.deepEqual(convertPage(html, createTurndownService(), { articleOnly:
true }), { markdown: null, repaired: false })
})
+// Fixture after layouts/blog/post.html: the rail holds date, authors, share
links, TOC and
+// previous/next, the content starts with the featured image and ends with the
related posts.
+test('a blog post keeps its title, byline, lead and body and drops the page
chrome', () => {
+ const html = `<!DOCTYPE html><html><head><title>t</title></head><body><main
role="main blog">
+<article class="post blog doc" aria-labelledby="post-title">
+<a class="post-back" href="/blog/">← All posts</a>
+<div class="post-hero"><div class="post-tags"><a class="tag-chip"
href="/categories/ai/">AI</a><a class="tag-chip"
href="/categories/tooling/">Tooling</a></div>
+<h1 id="post-title" class="post-title">A post</h1><p class="detail-lead">The
lead.</p></div>
+<div class="post-layout"><aside class="post-rail" aria-label="Post details">
+<div class="post-authors"><div class="post-author"><img class="post-avatar"
src="a.png" alt=""><div class="post-author-name">Ada Lovelace</div></div>
+<div class="post-author"><div class="post-author-name">Grace Hopper</div></div>
+<time class="post-date" datetime="2026-09-15">September 15, 2026</time></div>
+<div class="post-share"><a class="post-share-link"
href="https://twitter.com/">X</a></div>
+<div class="post-toc toc"><ul><li><a href="#setup">Setup</a></li></ul></div>
+<div class="post-adjacent"><a href="/blog/2026/09/other/">←
Previous</a></div></aside>
+<div class="post-content"><img class="featured" alt="Blog post featured image"
src="featured.jpg">
+<p>Body text.</p><h2 id="setup">Setup</h2><iframe
src="https://www.youtube-nocookie.com/embed/x1" allowfullscreen title="YouTube
Video"></iframe>
+<section class="post-related"><h3>Related posts</h3><a class="card"
href="/blog/2026/09/other/">Other</a></section></div></div>
+</article></main></body></html>`
+
+ assert.deepEqual(convertPage(html, createTurndownService()), {
+ markdown: '# A post\n\nPublished 2026-09-15 by Ada Lovelace, Grace Hopper
in AI, Tooling\n\nThe lead.\n\n' +
+ 'Body text.\n\n## Setup\n\n[YouTube
Video](https://www.youtube-nocookie.com/embed/x1)',
+ repaired: false,
+ })
+})
+
+test('a blog post without the rail details still converts', () => {
+ const html = '<!DOCTYPE
html><html><head><title>t</title></head><body><main><article class="post blog
doc">' +
+ '<h1 class="post-title">Bare</h1><div
class="post-content"><p>Body.</p></div></article></main></body></html>'
+
+ assert.equal(convertPage(html, createTurndownService()).markdown, '#
Bare\n\nBody.')
+})