diff --git a/.github/workflows/build-suggested-tools.yml b/.github/workflows/build-suggested-tools.yml
new file mode 100644
index 0000000..629f8ad
--- /dev/null
+++ b/.github/workflows/build-suggested-tools.yml
@@ -0,0 +1,28 @@
+name: Build suggested tools
+
+on:
+ push:
+ paths:
+ - 'suggested-tools.md'
+ workflow_dispatch:
+
+permissions:
+ contents: write
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Build suggested tools HTML
+ run: node scripts/build-suggested-tools.js
+
+ - name: Commit changes (if any)
+ run: |
+ git config user.name "github-actions[bot]"
+ git config user.email "github-actions[bot]@users.noreply.github.com"
+ git diff --quiet && exit 0
+ git add index.html formatter/index.html og-image/index.html
+ git commit -m "Rebuild suggested tools from suggested-tools.md"
+ git push
diff --git a/formatter/index.html b/formatter/index.html
index 8529551..d1a9b77 100644
--- a/formatter/index.html
+++ b/formatter/index.html
@@ -265,9 +265,12 @@
Live Preview
All Tools ·
W3C Validator ·
Can I Use ·
+
W3C Feed Validator ·
Cast Feed Validator ·
- Embed Responsively
+ Embed Responsively ·
+ Image Alt Writer 2
+
+
+
diff --git a/og-image/index.html b/og-image/index.html
index 65ac36b..5c23545 100644
--- a/og-image/index.html
+++ b/og-image/index.html
@@ -252,7 +252,10 @@ Suggested Meta Tags
Related tools:
HTML Formatter ·
All Tools ·
- Embed Responsively
+
+ Embed Responsively ·
+ Image Alt Writer 2
+
Platform debug tools:
diff --git a/scripts/build-suggested-tools.js b/scripts/build-suggested-tools.js
new file mode 100644
index 0000000..80d733a
--- /dev/null
+++ b/scripts/build-suggested-tools.js
@@ -0,0 +1,136 @@
+#!/usr/bin/env node
+/**
+ * Reads suggested-tools.md and injects generated HTML into each page.
+ *
+ * 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.
+ */
+
+const fs = require('fs');
+const path = require('path');
+
+const ROOT = path.resolve(__dirname, '..');
+const MD_PATH = path.join(ROOT, 'suggested-tools.md');
+
+// Map section names in the markdown to their HTML file paths and render mode.
+const PAGE_MAP = {
+ home: { file: 'index.html', mode: 'section' },
+ formatter: { file: 'formatter/index.html', mode: 'inline' },
+ 'og-image': { file: 'og-image/index.html', mode: 'inline' },
+};
+
+// ---------------------------------------------------------------------------
+// Parse the markdown
+// ---------------------------------------------------------------------------
+function parseMarkdown(src) {
+ const sections = {};
+ let current = null;
+
+ for (const raw of src.split('\n')) {
+ const line = raw.trim();
+
+ // H2 heading = page key
+ const h2 = line.match(/^##\s+(.+)$/);
+ if (h2) {
+ current = h2[1].trim().toLowerCase();
+ sections[current] = [];
+ continue;
+ }
+
+ // Markdown link inside a list item
+ if (current && /^-\s+\[/.test(line)) {
+ const match = line.match(/\[([^\]]+)\]\(([^)]+)\)/);
+ if (match) {
+ sections[current].push({ name: match[1], url: match[2] });
+ }
+ }
+ }
+
+ return sections;
+}
+
+// ---------------------------------------------------------------------------
+// Render HTML for each mode
+// ---------------------------------------------------------------------------
+function renderSection(tools) {
+ if (tools.length === 0) return '';
+ const items = tools
+ .map(t =>
+ ` ${t.name} ↗ `
+ )
+ .join('\n');
+ return [
+ ' ',
+ ].join('\n');
+}
+
+function renderInline(tools) {
+ if (tools.length === 0) return '';
+ return tools
+ .map(t =>
+ ` ${t.name} `
+ )
+ .join(' ·\n');
+}
+
+// ---------------------------------------------------------------------------
+// Inject between markers
+// ---------------------------------------------------------------------------
+const START = '';
+const END = '';
+
+function inject(html, rendered) {
+ const startIdx = html.indexOf(START);
+ const endIdx = html.indexOf(END);
+ if (startIdx === -1 || endIdx === -1) {
+ return null; // markers not found
+ }
+ return (
+ html.slice(0, startIdx + START.length) +
+ '\n' + rendered + '\n' +
+ html.slice(endIdx)
+ );
+}
+
+// ---------------------------------------------------------------------------
+// Main
+// ---------------------------------------------------------------------------
+const md = fs.readFileSync(MD_PATH, 'utf-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) {
+ console.log(` skip ${config.file} (no tools listed under "${key}")`);
+ continue;
+ }
+
+ const filePath = path.join(ROOT, config.file);
+ const html = fs.readFileSync(filePath, 'utf-8');
+
+ const rendered = config.mode === 'section'
+ ? renderSection(tools)
+ : renderInline(tools);
+
+ const result = inject(html, rendered);
+ if (result === null) {
+ console.error(` ERROR ${config.file}: missing ${START} / ${END} markers`);
+ process.exit(1);
+ }
+
+ fs.writeFileSync(filePath, result, 'utf-8');
+ console.log(` wrote ${config.file} (${tools.length} tools)`);
+ changed++;
+}
+
+console.log(`\nDone — updated ${changed} file(s).`);
diff --git a/suggested-tools.md b/suggested-tools.md
new file mode 100644
index 0000000..ee8d707
--- /dev/null
+++ b/suggested-tools.md
@@ -0,0 +1,27 @@
+# Suggested Tools
+
+External tools listed here are automatically added to the "Suggested Tools"
+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.
+
+## home
+
+- [Cast Feed Validator](https://www.castfeedvalidator.com/)
+- [W3C Feed Validator](https://validator.w3.org/feed/)
+- [Embed Responsively](https://embedresponsively.com/)
+- [Image Alt Writer 2](https://chatgpt.com/g/g-6821f526808c81918e50e06207c3f359-image-alt-writer-2)
+
+## formatter
+
+- [W3C Feed Validator](https://validator.w3.org/feed/)
+- [Cast Feed Validator](https://www.castfeedvalidator.com/)
+- [Embed Responsively](https://embedresponsively.com/)
+- [Image Alt Writer 2](https://chatgpt.com/g/g-6821f526808c81918e50e06207c3f359-image-alt-writer-2)
+
+## og-image
+
+- [Embed Responsively](https://embedresponsively.com/)
+- [Image Alt Writer 2](https://chatgpt.com/g/g-6821f526808c81918e50e06207c3f359-image-alt-writer-2)