diff --git a/index.html b/index.html index 759976e..7c25f84 100644 --- a/index.html +++ b/index.html @@ -235,6 +235,95 @@ from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } } + + /* ------------------------------------------------------- + * Suggested (external) tool cards + * ------------------------------------------------------- */ + + .suggested-tools { + margin-top: var(--space-2xl); + } + + .section-heading { + font-size: var(--text-xl); + font-weight: 600; + color: var(--color-text-heading); + margin-bottom: var(--space-sm); + letter-spacing: -0.01em; + } + + .suggested-grid { + margin-top: var(--space-lg); + } + + /* External card modifier — dashed border + relative for badge */ + .tool-card--external { + position: relative; + border-style: dashed; + cursor: default; + } + + .tool-card--external:hover { + border-color: var(--color-border-hover); + transform: translateY(-2px); + } + + /* "External ↗" badge */ + .external-badge { + position: absolute; + top: var(--space-md); + right: var(--space-md); + font-size: 0.6875rem; + font-weight: 500; + line-height: 1; + padding: 3px 8px; + border-radius: 999px; + color: var(--color-primary); + background: color-mix(in srgb, var(--color-primary) 12%, var(--color-surface-raised)); + border: 1px solid color-mix(in srgb, var(--color-primary) 25%, var(--color-border)); + letter-spacing: 0.02em; + white-space: nowrap; + pointer-events: none; + } + + /* Links list inside an external card */ + .tool-card-links { + display: flex; + flex-direction: column; + gap: 6px; + margin-top: var(--space-md); + } + + .tool-card-links a { + font-size: var(--text-sm); + color: var(--color-primary); + text-decoration: none; + display: inline-flex; + align-items: center; + gap: 3px; + width: fit-content; + } + + .tool-card-links a:hover { + color: var(--color-primary-hover); + text-decoration: underline; + } + + .external-arrow { + font-size: 0.75em; + opacity: 0.7; + } + + /* Staggered entrance for suggested cards */ + @media (prefers-reduced-motion: no-preference) { + .suggested-grid .tool-card { + animation: fadeIn 0.3s ease both; + } + .suggested-grid .tool-card:nth-child(1) { animation-delay: 100ms; } + .suggested-grid .tool-card:nth-child(2) { animation-delay: 200ms; } + .suggested-grid .tool-card:nth-child(3) { animation-delay: 300ms; } + .suggested-grid .tool-card:nth-child(4) { animation-delay: 400ms; } + } @@ -284,12 +373,53 @@

HTML Formatter & Tidy

Suggested Tools

- +
+
+ External ↗ + +

Feed Validators

+

Validate podcast and web feeds for errors and compatibility before publishing.

+ +
+ RSS + Podcasts + Validation +
+
+ +
+ External ↗ + +

Responsive Embeds

+

Generate responsive embed codes for videos, maps, and other media.

+ +
+ Responsive + Embeds + Video +
+
+ +
+ External ↗ + +

Image Alt Text

+

AI-powered tool to generate descriptive, accessible alt text for images.

+ +
+ Accessibility + AI + Images +
+
+
diff --git a/scripts/build-suggested-tools.js b/scripts/build-suggested-tools.js index 80d733a..1d0eabf 100644 --- a/scripts/build-suggested-tools.js +++ b/scripts/build-suggested-tools.js @@ -5,8 +5,8 @@ * Each page must contain marker comments: * ... * - * The home page gets a standalone
, while tool sub-pages get - * inline links appended to their existing "Related tools:" footer line. + * The home page gets card-based layout (using ### groups with metadata). + * Tool sub-pages get inline links appended to their existing footer line. */ const fs = require('fs'); @@ -27,24 +27,64 @@ const PAGE_MAP = { // --------------------------------------------------------------------------- function parseMarkdown(src) { const sections = {}; - let current = null; + let currentSection = null; + let currentGroup = null; for (const raw of src.split('\n')) { const line = raw.trim(); // H2 heading = page key - const h2 = line.match(/^##\s+(.+)$/); + const h2 = line.match(/^##\s+([^#].*)$/); if (h2) { - current = h2[1].trim().toLowerCase(); - sections[current] = []; + currentSection = h2[1].trim().toLowerCase(); + sections[currentSection] = { groups: [], links: [] }; + currentGroup = null; continue; } + if (!currentSection) continue; + + // H3 heading = card group (e.g., "### 📡 Feed Validators") + const h3 = line.match(/^###\s+(.+)$/); + if (h3) { + const titleText = h3[1].trim(); + // Extract leading emoji icon if present + const iconMatch = titleText.match(/^(\p{Emoji_Presentation}|\p{Emoji}\uFE0F?)\s*/u); + const icon = iconMatch ? iconMatch[1] : null; + const name = iconMatch ? titleText.slice(iconMatch[0].length).trim() : titleText; + + currentGroup = { icon, name, description: '', tags: [], links: [] }; + sections[currentSection].groups.push(currentGroup); + continue; + } + + // Tags line (e.g., "Tags: RSS, Podcasts, Validation") + if (currentGroup) { + const tagsMatch = line.match(/^Tags:\s*(.+)$/i); + if (tagsMatch) { + currentGroup.tags = tagsMatch[1].split(',').map(t => t.trim()).filter(Boolean); + continue; + } + } + // Markdown link inside a list item - if (current && /^-\s+\[/.test(line)) { + if (/^-\s+\[/.test(line)) { const match = line.match(/\[([^\]]+)\]\(([^)]+)\)/); if (match) { - sections[current].push({ name: match[1], url: match[2] }); + const link = { name: match[1], url: match[2] }; + if (currentGroup) { + currentGroup.links.push(link); + } else { + sections[currentSection].links.push(link); + } + } + continue; + } + + // Plain text line = description for current group + if (currentGroup && line && !line.startsWith('#') && !line.startsWith('-')) { + if (!currentGroup.description) { + currentGroup.description = line; } } } @@ -53,10 +93,58 @@ function parseMarkdown(src) { } // --------------------------------------------------------------------------- -// Render HTML for each mode +// Render HTML — card layout for home page +// --------------------------------------------------------------------------- +function renderCardSection(groups) { + const cards = groups.map(group => { + const parts = []; + parts.push('
'); + parts.push(' External ↗'); + + if (group.icon) { + parts.push(` `); + } + + parts.push(`

${group.name}

`); + + if (group.description) { + parts.push(`

${group.description}

`); + } + + if (group.links.length > 0) { + parts.push(' '); + } + + if (group.tags.length > 0) { + parts.push('
'); + for (const t of group.tags) { + parts.push(` ${t}`); + } + parts.push('
'); + } + + parts.push('
'); + return parts.join('\n'); + }).join('\n\n'); + + return [ + '
', + '

Suggested Tools

', + '
', + cards, + '
', + '
', + ].join('\n'); +} + +// --------------------------------------------------------------------------- +// Render HTML — simple list fallback (home page without groups) // --------------------------------------------------------------------------- -function renderSection(tools) { - if (tools.length === 0) return ''; +function renderListSection(tools) { const items = tools .map(t => `
  • ${t.name}
  • ` @@ -72,9 +160,30 @@ function renderSection(tools) { ].join('\n'); } -function renderInline(tools) { - if (tools.length === 0) return ''; - return tools +// --------------------------------------------------------------------------- +// Render HTML — home page section (cards or list) +// --------------------------------------------------------------------------- +function renderSection(section) { + if (section.groups && section.groups.length > 0) { + return renderCardSection(section.groups); + } + if (section.links && section.links.length > 0) { + return renderListSection(section.links); + } + return ''; +} + +// --------------------------------------------------------------------------- +// Render HTML — inline links for tool sub-pages +// --------------------------------------------------------------------------- +function renderInline(section) { + // Collect all links from both groups and flat links + const allLinks = [...section.links]; + for (const group of section.groups) { + allLinks.push(...group.links); + } + if (allLinks.length === 0) return ''; + return allLinks .map(t => ` ${t.name}` ) @@ -109,8 +218,8 @@ const sections = parseMarkdown(md); let changed = 0; for (const [key, config] of Object.entries(PAGE_MAP)) { - const tools = sections[key]; - if (!tools || tools.length === 0) { + const section = sections[key]; + if (!section || (section.groups.length === 0 && section.links.length === 0)) { console.log(` skip ${config.file} (no tools listed under "${key}")`); continue; } @@ -119,8 +228,8 @@ for (const [key, config] of Object.entries(PAGE_MAP)) { const html = fs.readFileSync(filePath, 'utf-8'); const rendered = config.mode === 'section' - ? renderSection(tools) - : renderInline(tools); + ? renderSection(section) + : renderInline(section); const result = inject(html, rendered); if (result === null) { @@ -128,8 +237,9 @@ for (const [key, config] of Object.entries(PAGE_MAP)) { process.exit(1); } + const toolCount = section.groups.length + section.links.length; fs.writeFileSync(filePath, result, 'utf-8'); - console.log(` wrote ${config.file} (${tools.length} tools)`); + console.log(` wrote ${config.file} (${toolCount} entries)`); changed++; } diff --git a/suggested-tools.md b/suggested-tools.md index ee8d707..fe4c413 100644 --- a/suggested-tools.md +++ b/suggested-tools.md @@ -5,13 +5,30 @@ sections across the site. Run `node scripts/build-suggested-tools.js` or push to trigger the GitHub Action to rebuild. Each `##` section maps to a page (`home`, `formatter`, `og-image`). -Add a standard markdown link to include a tool on that page. + +For the **home** page, use `###` sub-headings to define card groups. +Each group gets an icon (emoji in the heading), a plain-text description +line, a `Tags:` line, and one or more markdown links. Similar tools +share a single card. + +Sub-pages (`formatter`, `og-image`) keep the flat link list format. ## home +### 📡 Feed Validators +Validate podcast and web feeds for errors and compatibility before publishing. +Tags: RSS, Podcasts, Validation - [Cast Feed Validator](https://www.castfeedvalidator.com/) - [W3C Feed Validator](https://validator.w3.org/feed/) + +### 📐 Responsive Embeds +Generate responsive embed codes for videos, maps, and other media. +Tags: Responsive, Embeds, Video - [Embed Responsively](https://embedresponsively.com/) + +### 🖼️ Image Alt Text +AI-powered tool to generate descriptive, accessible alt text for images. +Tags: Accessibility, AI, Images - [Image Alt Writer 2](https://chatgpt.com/g/g-6821f526808c81918e50e06207c3f359-image-alt-writer-2) ## formatter