Skip to content

Commit 1fd0fa6

Browse files
committed
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
1 parent 850d65d commit 1fd0fa6

File tree

1 file changed

+50
-3
lines changed

1 file changed

+50
-3
lines changed

blog-to-newsletter.html

Lines changed: 50 additions & 3 deletions
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 = '';
@@ -558,8 +559,8 @@ <h2>Links sent in previous newsletters</h2>
558559
'guide_slug', g.slug
559560
) as json,
560561
'https://simonwillison.net/guides/' || g.slug || '/' || c.slug || '/' as external_url
561-
from blog_chapter c
562-
join blog_guide g on c.guide_id = g.id
562+
from guides_chapter c
563+
join guides_guide g on c.guide_id = g.id
563564
where c.is_draft = 0
564565
union all
565566
select
@@ -579,7 +580,7 @@ <h2>Links sent in previous newsletters</h2>
579580
type,
580581
title,
581582
case
582-
when type = 'til'
583+
when type in ('til', 'chapter')
583584
then external_url
584585
else 'https://simonwillison.net/' || strftime('%Y/', created)
585586
|| substr('JanFebMarAprMayJunJulAugSepOctNovDec', (strftime('%m', created) - 1) * 3 + 1, 3) ||
@@ -713,6 +714,33 @@ <h2>Links sent in previous newsletters</h2>
713714
return await response.json();
714715
}
715716

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+
716744
// Filter content based on settings
717745
function filterContent() {
718746
const skipExisting = skipExistingInput.checked;
@@ -765,12 +793,27 @@ <h2>Links sent in previous newsletters</h2>
765793
});
766794
}
767795

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+
768810
content = filtered;
769811
entries = content.filter(e => e.type === 'entry');
770812
blogmarks = content.filter(e => e.type === 'blogmark');
771813
quotations = content.filter(e => e.type === 'quotation');
772814
tils = content.filter(e => e.type === 'til');
773815
notes = content.filter(e => e.type === 'note');
816+
chapters = content.filter(e => e.type === 'chapter');
774817

775818
// Initialize story order
776819
storyOrder = entries.map(e => `${e.id}: ${e.title}`).reverse();
@@ -1007,6 +1050,9 @@ <h2>Links sent in previous newsletters</h2>
10071050
if (notes.length) {
10081051
extras.push(`${notes.length} note${notes.length > 1 ? 's' : ''}`);
10091052
}
1053+
if (chapters.length) {
1054+
extras.push(`${chapters.length} guide chapter${chapters.length > 1 ? 's' : ''}`);
1055+
}
10101056
if (extras.length) {
10111057
headerHtml += `<p>Plus ${extras.join(' and ')}</p>`;
10121058
}
@@ -1056,6 +1102,7 @@ <h2>Links sent in previous newsletters</h2>
10561102
quotations = quotations.filter(e => !(type === 'quotation' && String(e.id) === String(id)));
10571103
tils = tils.filter(e => !(type === 'til' && String(e.id) === String(id)));
10581104
notes = notes.filter(e => !(type === 'note' && String(e.id) === String(id)));
1105+
chapters = chapters.filter(e => !(type === 'chapter' && String(e.id) === String(id)));
10591106
generateNewsletter();
10601107
}
10611108

0 commit comments

Comments
 (0)