55 * Each page must contain marker comments:
66 * <!-- SUGGESTED_TOOLS_START --> ... <!-- SUGGESTED_TOOLS_END -->
77 *
8- * The home page gets a standalone <section>, while tool sub-pages get
9- * inline links appended to their existing "Related tools:" footer line.
8+ * The home page gets card-based layout (using ### groups with metadata).
9+ * Tool sub-pages get inline links appended to their existing footer line.
1010 */
1111
1212const fs = require ( 'fs' ) ;
@@ -27,24 +27,64 @@ const PAGE_MAP = {
2727// ---------------------------------------------------------------------------
2828function parseMarkdown ( src ) {
2929 const sections = { } ;
30- let current = null ;
30+ let currentSection = null ;
31+ let currentGroup = null ;
3132
3233 for ( const raw of src . split ( '\n' ) ) {
3334 const line = raw . trim ( ) ;
3435
3536 // H2 heading = page key
36- const h2 = line . match ( / ^ # # \s + ( . + ) $ / ) ;
37+ const h2 = line . match ( / ^ # # \s + ( [ ^ # ] . * ) $ / ) ;
3738 if ( h2 ) {
38- current = h2 [ 1 ] . trim ( ) . toLowerCase ( ) ;
39- sections [ current ] = [ ] ;
39+ currentSection = h2 [ 1 ] . trim ( ) . toLowerCase ( ) ;
40+ sections [ currentSection ] = { groups : [ ] , links : [ ] } ;
41+ currentGroup = null ;
4042 continue ;
4143 }
4244
45+ if ( ! currentSection ) continue ;
46+
47+ // H3 heading = card group (e.g., "### 📡 Feed Validators")
48+ const h3 = line . match ( / ^ # # # \s + ( .+ ) $ / ) ;
49+ if ( h3 ) {
50+ const titleText = h3 [ 1 ] . trim ( ) ;
51+ // Extract leading emoji icon if present
52+ const iconMatch = titleText . match ( / ^ ( \p{ Emoji_Presentation} | \p{ Emoji} \uFE0F ? ) \s * / u) ;
53+ const icon = iconMatch ? iconMatch [ 1 ] : null ;
54+ const name = iconMatch ? titleText . slice ( iconMatch [ 0 ] . length ) . trim ( ) : titleText ;
55+
56+ currentGroup = { icon, name, description : '' , tags : [ ] , links : [ ] } ;
57+ sections [ currentSection ] . groups . push ( currentGroup ) ;
58+ continue ;
59+ }
60+
61+ // Tags line (e.g., "Tags: RSS, Podcasts, Validation")
62+ if ( currentGroup ) {
63+ const tagsMatch = line . match ( / ^ T a g s : \s * ( .+ ) $ / i) ;
64+ if ( tagsMatch ) {
65+ currentGroup . tags = tagsMatch [ 1 ] . split ( ',' ) . map ( t => t . trim ( ) ) . filter ( Boolean ) ;
66+ continue ;
67+ }
68+ }
69+
4370 // Markdown link inside a list item
44- if ( current && / ^ - \s + \[ / . test ( line ) ) {
71+ if ( / ^ - \s + \[ / . test ( line ) ) {
4572 const match = line . match ( / \[ ( [ ^ \] ] + ) \] \( ( [ ^ ) ] + ) \) / ) ;
4673 if ( match ) {
47- sections [ current ] . push ( { name : match [ 1 ] , url : match [ 2 ] } ) ;
74+ const link = { name : match [ 1 ] , url : match [ 2 ] } ;
75+ if ( currentGroup ) {
76+ currentGroup . links . push ( link ) ;
77+ } else {
78+ sections [ currentSection ] . links . push ( link ) ;
79+ }
80+ }
81+ continue ;
82+ }
83+
84+ // Plain text line = description for current group
85+ if ( currentGroup && line && ! line . startsWith ( '#' ) && ! line . startsWith ( '-' ) ) {
86+ if ( ! currentGroup . description ) {
87+ currentGroup . description = line ;
4888 }
4989 }
5090 }
@@ -53,10 +93,58 @@ function parseMarkdown(src) {
5393}
5494
5595// ---------------------------------------------------------------------------
56- // Render HTML for each mode
96+ // Render HTML — card layout for home page
97+ // ---------------------------------------------------------------------------
98+ function renderCardSection ( groups ) {
99+ const cards = groups . map ( group => {
100+ const parts = [ ] ;
101+ parts . push ( ' <div class="tool-card tool-card--external">' ) ;
102+ parts . push ( ' <span class="external-badge" aria-label="External tool">External ↗</span>' ) ;
103+
104+ if ( group . icon ) {
105+ parts . push ( ` <span class="tool-icon" aria-hidden="true">${ group . icon } </span>` ) ;
106+ }
107+
108+ parts . push ( ` <h3 class="tool-name">${ group . name } </h3>` ) ;
109+
110+ if ( group . description ) {
111+ parts . push ( ` <p class="tool-description">${ group . description } </p>` ) ;
112+ }
113+
114+ if ( group . links . length > 0 ) {
115+ parts . push ( ' <div class="tool-card-links">' ) ;
116+ for ( const l of group . links ) {
117+ parts . push ( ` <a href="${ l . url } " target="_blank" rel="noopener">${ l . name } <span class="external-arrow" aria-hidden="true">↗</span></a>` ) ;
118+ }
119+ parts . push ( ' </div>' ) ;
120+ }
121+
122+ if ( group . tags . length > 0 ) {
123+ parts . push ( ' <div class="tool-tags">' ) ;
124+ for ( const t of group . tags ) {
125+ parts . push ( ` <span class="tool-tag">${ t } </span>` ) ;
126+ }
127+ parts . push ( ' </div>' ) ;
128+ }
129+
130+ parts . push ( ' </div>' ) ;
131+ return parts . join ( '\n' ) ;
132+ } ) . join ( '\n\n' ) ;
133+
134+ return [
135+ ' <section class="suggested-tools">' ,
136+ ' <h2 class="section-heading">Suggested Tools</h2>' ,
137+ ' <div class="tools-grid suggested-grid">' ,
138+ cards ,
139+ ' </div>' ,
140+ ' </section>' ,
141+ ] . join ( '\n' ) ;
142+ }
143+
144+ // ---------------------------------------------------------------------------
145+ // Render HTML — simple list fallback (home page without groups)
57146// ---------------------------------------------------------------------------
58- function renderSection ( tools ) {
59- if ( tools . length === 0 ) return '' ;
147+ function renderListSection ( tools ) {
60148 const items = tools
61149 . map ( t =>
62150 ` <li><a href="${ t . url } " target="_blank" rel="noopener">${ t . name } <span class="external-icon" aria-hidden="true">↗</span></a></li>`
@@ -72,9 +160,30 @@ function renderSection(tools) {
72160 ] . join ( '\n' ) ;
73161}
74162
75- function renderInline ( tools ) {
76- if ( tools . length === 0 ) return '' ;
77- return tools
163+ // ---------------------------------------------------------------------------
164+ // Render HTML — home page section (cards or list)
165+ // ---------------------------------------------------------------------------
166+ function renderSection ( section ) {
167+ if ( section . groups && section . groups . length > 0 ) {
168+ return renderCardSection ( section . groups ) ;
169+ }
170+ if ( section . links && section . links . length > 0 ) {
171+ return renderListSection ( section . links ) ;
172+ }
173+ return '' ;
174+ }
175+
176+ // ---------------------------------------------------------------------------
177+ // Render HTML — inline links for tool sub-pages
178+ // ---------------------------------------------------------------------------
179+ function renderInline ( section ) {
180+ // Collect all links from both groups and flat links
181+ const allLinks = [ ...section . links ] ;
182+ for ( const group of section . groups ) {
183+ allLinks . push ( ...group . links ) ;
184+ }
185+ if ( allLinks . length === 0 ) return '' ;
186+ return allLinks
78187 . map ( t =>
79188 ` <a href="${ t . url } " target="_blank" rel="noopener">${ t . name } </a>`
80189 )
@@ -109,8 +218,8 @@ const sections = parseMarkdown(md);
109218let changed = 0 ;
110219
111220for ( const [ key , config ] of Object . entries ( PAGE_MAP ) ) {
112- const tools = sections [ key ] ;
113- if ( ! tools || tools . length === 0 ) {
221+ const section = sections [ key ] ;
222+ if ( ! section || ( section . groups . length === 0 && section . links . length === 0 ) ) {
114223 console . log ( ` skip ${ config . file } (no tools listed under "${ key } ")` ) ;
115224 continue ;
116225 }
@@ -119,17 +228,18 @@ for (const [key, config] of Object.entries(PAGE_MAP)) {
119228 const html = fs . readFileSync ( filePath , 'utf-8' ) ;
120229
121230 const rendered = config . mode === 'section'
122- ? renderSection ( tools )
123- : renderInline ( tools ) ;
231+ ? renderSection ( section )
232+ : renderInline ( section ) ;
124233
125234 const result = inject ( html , rendered ) ;
126235 if ( result === null ) {
127236 console . error ( ` ERROR ${ config . file } : missing ${ START } / ${ END } markers` ) ;
128237 process . exit ( 1 ) ;
129238 }
130239
240+ const toolCount = section . groups . length + section . links . length ;
131241 fs . writeFileSync ( filePath , result , 'utf-8' ) ;
132- console . log ( ` wrote ${ config . file } (${ tools . length } tools )` ) ;
242+ console . log ( ` wrote ${ config . file } (${ toolCount } entries )` ) ;
133243 changed ++ ;
134244}
135245
0 commit comments