Skip to content

Commit 2f5bdd4

Browse files
simonwclaude
andauthored
Add guide chapters to newsletter content (#238)
* WIP: Add guide chapters SQL union to newsletter template Adds blog_chapter query to the content CTE in the newsletter SQL. Still needs: URL handling in collected CTE, JS rendering with first-3-paragraph truncation and [... word count] suffix, chapter state management, and extras count integration. https://claude.ai/code/session_01Df7tNiUDSXVwvF1yeChynr * Add guide chapters to newsletter with truncated excerpts - Query guides_chapter/guides_guide tables from Datasette API - Render chapter markdown with marked.js, extract first 3 paragraphs - Append [... N words] suffix linking to full chapter (matching blog display) - Show guide breadcrumb ("Agentic Engineering Patterns >") above each chapter - Include chapters in extras count and support deletion from preview https://claude.ai/code/session_01Df7tNiUDSXVwvF1yeChynr --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent cfc038f commit 2f5bdd4

File tree

1 file changed

+68
-1
lines changed

1 file changed

+68
-1
lines changed

blog-to-newsletter.html

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -398,6 +398,7 @@ <h2>Links sent in previous newsletters</h2>
398398
let quotations = [];
399399
let tils = [];
400400
let notes = [];
401+
let chapters = [];
401402
let previousLinks = [];
402403
let storyOrder = [];
403404
let newsletterHTML = '';
@@ -542,6 +543,26 @@ <h2>Links sent in previous newsletters</h2>
542543
'' as external_url
543544
from blog_note
544545
union all
546+
select
547+
c.id,
548+
'chapter' as type,
549+
c.title,
550+
c.created,
551+
c.slug,
552+
'No HTML' as html,
553+
json_object(
554+
'created', date(c.created),
555+
'title', c.title,
556+
'body', c.body,
557+
'chapter_slug', c.slug,
558+
'guide_title', g.title,
559+
'guide_slug', g.slug
560+
) as json,
561+
'https://simonwillison.net/guides/' || g.slug || '/' || c.slug || '/' as external_url
562+
from guides_chapter c
563+
join guides_guide g on c.guide_id = g.id
564+
where c.is_draft = 0
565+
union all
545566
select
546567
rowid,
547568
'til' as type,
@@ -559,7 +580,7 @@ <h2>Links sent in previous newsletters</h2>
559580
type,
560581
title,
561582
case
562-
when type = 'til'
583+
when type in ('til', 'chapter')
563584
then external_url
564585
else 'https://simonwillison.net/' || strftime('%Y/', created)
565586
|| substr('JanFebMarAprMayJunJulAugSepOctNovDec', (strftime('%m', created) - 1) * 3 + 1, 3) ||
@@ -693,6 +714,33 @@ <h2>Links sent in previous newsletters</h2>
693714
return await response.json();
694715
}
695716

717+
// Render first three paragraphs of a chapter with word count suffix
718+
function renderChapterExcerpt(body, chapterUrl) {
719+
const fullHtml = marked.parse(body);
720+
const tempDiv = document.createElement('div');
721+
tempDiv.innerHTML = fullHtml;
722+
const paragraphs = tempDiv.querySelectorAll(':scope > p');
723+
724+
if (paragraphs.length <= 3) return fullHtml;
725+
726+
const excerptDiv = document.createElement('div');
727+
for (let i = 0; i < 3; i++) {
728+
excerptDiv.appendChild(paragraphs[i].cloneNode(true));
729+
}
730+
let excerptHtml = excerptDiv.innerHTML;
731+
732+
const wordCount = body.split(/\s+/).filter(w => w.length > 0).length;
733+
const formattedCount = wordCount.toLocaleString();
734+
const suffix = ` <span style="font-size: 0.9em">[... <a href="${chapterUrl}">${formattedCount} word${wordCount !== 1 ? 's' : ''}</a>]</span>`;
735+
const lastP = excerptHtml.lastIndexOf('</p>');
736+
if (lastP !== -1) {
737+
excerptHtml = excerptHtml.substring(0, lastP) + suffix + excerptHtml.substring(lastP);
738+
} else {
739+
excerptHtml += suffix;
740+
}
741+
return excerptHtml;
742+
}
743+
696744
// Filter content based on settings
697745
function filterContent() {
698746
const skipExisting = skipExistingInput.checked;
@@ -745,12 +793,27 @@ <h2>Links sent in previous newsletters</h2>
745793
});
746794
}
747795

796+
// Always render chapter HTML from markdown
797+
filtered = filtered.map(e => {
798+
if (e.type === 'chapter' && e.json !== 'null') {
799+
const info = typeof e.json === 'string' ? JSON.parse(e.json) : e.json;
800+
const entry = { ...e };
801+
const chapterUrl = `https://simonwillison.net/guides/${info.guide_slug}/${info.chapter_slug}/`;
802+
const guideUrl = `https://simonwillison.net/guides/${info.guide_slug}/`;
803+
const excerptHtml = renderChapterExcerpt(info.body, chapterUrl);
804+
entry.html = `<p style="font-size: 0.85em; color: #999; margin: 0 0 -0.2em 0; line-height: 1.2;"><a href="${guideUrl}" style="color: #999; text-decoration: none;">${info.guide_title}</a> &gt;</p><h3 style="margin-top: 0.2em; margin-bottom: 0.5em;"><a href="${chapterUrl}">${info.title}</a> - ${info.created}</h3>${excerptHtml}`;
805+
return entry;
806+
}
807+
return e;
808+
});
809+
748810
content = filtered;
749811
entries = content.filter(e => e.type === 'entry');
750812
blogmarks = content.filter(e => e.type === 'blogmark');
751813
quotations = content.filter(e => e.type === 'quotation');
752814
tils = content.filter(e => e.type === 'til');
753815
notes = content.filter(e => e.type === 'note');
816+
chapters = content.filter(e => e.type === 'chapter');
754817

755818
// Initialize story order
756819
storyOrder = entries.map(e => `${e.id}: ${e.title}`).reverse();
@@ -987,6 +1050,9 @@ <h2>Links sent in previous newsletters</h2>
9871050
if (notes.length) {
9881051
extras.push(`${notes.length} note${notes.length > 1 ? 's' : ''}`);
9891052
}
1053+
if (chapters.length) {
1054+
extras.push(`${chapters.length} guide chapter${chapters.length > 1 ? 's' : ''}`);
1055+
}
9901056
if (extras.length) {
9911057
headerHtml += `<p>Plus ${extras.join(' and ')}</p>`;
9921058
}
@@ -1036,6 +1102,7 @@ <h2>Links sent in previous newsletters</h2>
10361102
quotations = quotations.filter(e => !(type === 'quotation' && String(e.id) === String(id)));
10371103
tils = tils.filter(e => !(type === 'til' && String(e.id) === String(id)));
10381104
notes = notes.filter(e => !(type === 'note' && String(e.id) === String(id)));
1105+
chapters = chapters.filter(e => !(type === 'chapter' && String(e.id) === String(id)));
10391106
generateNewsletter();
10401107
}
10411108

0 commit comments

Comments
 (0)