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/">&larr; 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/">&larr; 
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.')
+})

Reply via email to