@@ -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> ></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