Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
142 changes: 136 additions & 6 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
}
</style>
</head>
<body>
Expand Down Expand Up @@ -284,12 +373,53 @@ <h2 class="tool-name">HTML Formatter &amp; Tidy</h2>
<!-- SUGGESTED_TOOLS_START -->
<section class="suggested-tools">
<h2 class="section-heading">Suggested Tools</h2>
<ul class="suggested-list">
<li><a href="https://www.castfeedvalidator.com/" target="_blank" rel="noopener">Cast Feed Validator <span class="external-icon" aria-hidden="true">&#8599;</span></a></li>
<li><a href="https://validator.w3.org/feed/" target="_blank" rel="noopener">W3C Feed Validator <span class="external-icon" aria-hidden="true">&#8599;</span></a></li>
<li><a href="https://embedresponsively.com/" target="_blank" rel="noopener">Embed Responsively <span class="external-icon" aria-hidden="true">&#8599;</span></a></li>
<li><a href="https://chatgpt.com/g/g-6821f526808c81918e50e06207c3f359-image-alt-writer-2" target="_blank" rel="noopener">Image Alt Writer 2 <span class="external-icon" aria-hidden="true">&#8599;</span></a></li>
</ul>
<div class="tools-grid suggested-grid">
<div class="tool-card tool-card--external">
<span class="external-badge" aria-label="External tool">External &#8599;</span>
<span class="tool-icon" aria-hidden="true">📡</span>
<h3 class="tool-name">Feed Validators</h3>
<p class="tool-description">Validate podcast and web feeds for errors and compatibility before publishing.</p>
<div class="tool-card-links">
<a href="https://www.castfeedvalidator.com/" target="_blank" rel="noopener">Cast Feed Validator <span class="external-arrow" aria-hidden="true">&#8599;</span></a>
<a href="https://validator.w3.org/feed/" target="_blank" rel="noopener">W3C Feed Validator <span class="external-arrow" aria-hidden="true">&#8599;</span></a>
</div>
<div class="tool-tags">
<span class="tool-tag">RSS</span>
<span class="tool-tag">Podcasts</span>
<span class="tool-tag">Validation</span>
</div>
</div>

<div class="tool-card tool-card--external">
<span class="external-badge" aria-label="External tool">External &#8599;</span>
<span class="tool-icon" aria-hidden="true">📐</span>
<h3 class="tool-name">Responsive Embeds</h3>
<p class="tool-description">Generate responsive embed codes for videos, maps, and other media.</p>
<div class="tool-card-links">
<a href="https://embedresponsively.com/" target="_blank" rel="noopener">Embed Responsively <span class="external-arrow" aria-hidden="true">&#8599;</span></a>
</div>
<div class="tool-tags">
<span class="tool-tag">Responsive</span>
<span class="tool-tag">Embeds</span>
<span class="tool-tag">Video</span>
</div>
</div>

<div class="tool-card tool-card--external">
<span class="external-badge" aria-label="External tool">External &#8599;</span>
<span class="tool-icon" aria-hidden="true">🖼️</span>
<h3 class="tool-name">Image Alt Text</h3>
<p class="tool-description">AI-powered tool to generate descriptive, accessible alt text for images.</p>
<div class="tool-card-links">
<a href="https://chatgpt.com/g/g-6821f526808c81918e50e06207c3f359-image-alt-writer-2" target="_blank" rel="noopener">Image Alt Writer 2 <span class="external-arrow" aria-hidden="true">&#8599;</span></a>
</div>
<div class="tool-tags">
<span class="tool-tag">Accessibility</span>
<span class="tool-tag">AI</span>
<span class="tool-tag">Images</span>
</div>
</div>
</div>
</section>
<!-- SUGGESTED_TOOLS_END -->
</main>
Expand Down
148 changes: 129 additions & 19 deletions scripts/build-suggested-tools.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
* Each page must contain marker comments:
* <!-- SUGGESTED_TOOLS_START --> ... <!-- SUGGESTED_TOOLS_END -->
*
* The home page gets a standalone <section>, 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');
Expand All @@ -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;
}
}
}
Expand All @@ -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(' <div class="tool-card tool-card--external">');
parts.push(' <span class="external-badge" aria-label="External tool">External &#8599;</span>');

if (group.icon) {
parts.push(` <span class="tool-icon" aria-hidden="true">${group.icon}</span>`);
}

parts.push(` <h3 class="tool-name">${group.name}</h3>`);

if (group.description) {
parts.push(` <p class="tool-description">${group.description}</p>`);
}

if (group.links.length > 0) {
parts.push(' <div class="tool-card-links">');
for (const l of group.links) {
parts.push(` <a href="${l.url}" target="_blank" rel="noopener">${l.name} <span class="external-arrow" aria-hidden="true">&#8599;</span></a>`);
}
parts.push(' </div>');
}

if (group.tags.length > 0) {
parts.push(' <div class="tool-tags">');
for (const t of group.tags) {
parts.push(` <span class="tool-tag">${t}</span>`);
}
parts.push(' </div>');
}

parts.push(' </div>');
return parts.join('\n');
}).join('\n\n');

return [
' <section class="suggested-tools">',
' <h2 class="section-heading">Suggested Tools</h2>',
' <div class="tools-grid suggested-grid">',
cards,
' </div>',
' </section>',
].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 =>
` <li><a href="${t.url}" target="_blank" rel="noopener">${t.name} <span class="external-icon" aria-hidden="true">&#8599;</span></a></li>`
Expand All @@ -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 =>
` <a href="${t.url}" target="_blank" rel="noopener">${t.name}</a>`
)
Expand Down Expand Up @@ -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;
}
Expand All @@ -119,17 +228,18 @@ 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) {
console.error(` ERROR ${config.file}: missing ${START} / ${END} markers`);
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++;
}

Expand Down
19 changes: 18 additions & 1 deletion suggested-tools.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down