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
2 changes: 1 addition & 1 deletion .github/workflows/news-article-generator.md
Original file line number Diff line number Diff line change
Expand Up @@ -1483,7 +1483,7 @@ Remember: You are producing world-class political journalism that informs Swedis
**Article Maintenance & Fixing:**
| Script | Usage | Description |
|--------|-------|-------------|
| `scripts/fix-article-navigation.py` | `python3 scripts/fix-article-navigation.py [--dry-run]` | **Fallback only** — adds language switcher + article-top-nav to articles missing them (idempotent) |
| `scripts/fix-article-navigation.ts` | `npx tsx scripts/fix-article-navigation.ts [--dry-run]` | **Fallback only** — adds language switcher + article-top-nav to articles missing them (idempotent) |
| `scripts/fix-language-switchers-and-css.py` | `python3 scripts/fix-language-switchers-and-css.py` | Updates switchers to show only existing languages, removes embedded CSS |
| `scripts/fix-mixed-language-descriptions.py` | `python3 scripts/fix-mixed-language-descriptions.py` | Fixes articles with mixed-language meta descriptions |

Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/news-committee-reports.md
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,7 @@ npx tsx scripts/generate-news-enhanced.ts \
These elements are validated by `bash scripts/validate-news-generation.sh` (Checks 8–10). The fix script is a **fallback only** — do not run it by default:
```bash
# FALLBACK ONLY — use if validate-news-generation.sh reports missing navigation elements
python3 scripts/fix-article-navigation.py
npx tsx scripts/fix-article-navigation.ts
```

### Step 4: Translate Swedish Content & Verify Analysis Quality
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/news-evening-analysis.md
Original file line number Diff line number Diff line change
Expand Up @@ -1120,7 +1120,7 @@ For deeper analysis, combine MCP tools: `search_voteringar` → `get_voting_grou
| Script | Usage | Description |
|--------|-------|-------------|
| `scripts/generate-news-enhanced.ts` | `npx tsx scripts/generate-news-enhanced.ts --types=evening-analysis --languages=LANGS` | Main article generator |
| `scripts/fix-article-navigation.py` | `python3 scripts/fix-article-navigation.py` | **Fallback only** — fix missing language switcher + article-top-nav (idempotent) |
| `scripts/fix-article-navigation.ts` | `npx tsx scripts/fix-article-navigation.ts` | **Fallback only** — fix missing language switcher + article-top-nav (idempotent) |
| `scripts/validate-news-generation.sh` | `bash scripts/validate-news-generation.sh` | Validate generated article structure |
| `scripts/mcp-setup.sh` | `source scripts/mcp-setup.sh` | Set MCP environment variables |
| `scripts/mcp-query-cli.ts` | `npx tsx scripts/mcp-query-cli.ts <tool> '<json>'` | Query individual MCP tools |
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/news-month-ahead.md
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,7 @@ npx tsx scripts/generate-news-enhanced.ts \
These elements are validated by `bash scripts/validate-news-generation.sh` (Checks 8–10). The fix script is a **fallback only** — do not run it by default:
```bash
# FALLBACK ONLY — use if validate-news-generation.sh reports missing navigation elements
python3 scripts/fix-article-navigation.py
npx tsx scripts/fix-article-navigation.ts
```

### Step 4: Translate, Validate & Verify Analysis Quality
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/news-monthly-review.md
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,7 @@ npx tsx scripts/generate-news-enhanced.ts \
These elements are validated by `bash scripts/validate-news-generation.sh` (Checks 8–10). The fix script is a **fallback only** — do not run it by default:
```bash
# FALLBACK ONLY — use if validate-news-generation.sh reports missing navigation elements
python3 scripts/fix-article-navigation.py
npx tsx scripts/fix-article-navigation.ts
```

### Step 4: Translate, Validate & Verify Analysis Quality
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/news-motions.md
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,7 @@ npx tsx scripts/generate-news-enhanced.ts \
These elements are validated by `bash scripts/validate-news-generation.sh` (Checks 8–10). The fix script is a **fallback only** — do not run it by default:
```bash
# FALLBACK ONLY — use if validate-news-generation.sh reports missing navigation elements
python3 scripts/fix-article-navigation.py
npx tsx scripts/fix-article-navigation.ts
```

### Step 4: Translate, Validate & Verify Analysis Quality
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/news-propositions.md
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,7 @@ npx tsx scripts/generate-news-enhanced.ts \
These elements are validated by `bash scripts/validate-news-generation.sh` (Checks 8–10). The fix script is a **fallback only** — do not run it by default:
```bash
# FALLBACK ONLY — use if validate-news-generation.sh reports missing navigation elements
python3 scripts/fix-article-navigation.py
npx tsx scripts/fix-article-navigation.ts
```

### Step 4: Translate, Validate & Verify Analysis Quality
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/news-week-ahead.md
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,7 @@ npx tsx scripts/generate-news-enhanced.ts \
These elements are validated by `bash scripts/validate-news-generation.sh` (Checks 8–10). The fix script is a **fallback only** — do not run it by default:
```bash
# FALLBACK ONLY — use if validate-news-generation.sh reports missing navigation elements
python3 scripts/fix-article-navigation.py
npx tsx scripts/fix-article-navigation.ts
```

### Step 4: Translate, Validate & Verify Analysis Quality
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/news-weekly-review.md
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,7 @@ npx tsx scripts/generate-news-enhanced.ts \
These elements are validated by `bash scripts/validate-news-generation.sh` (Checks 8–10). The fix script is a **fallback only** — do not run it by default:
```bash
# FALLBACK ONLY — use if validate-news-generation.sh reports missing navigation elements
python3 scripts/fix-article-navigation.py
npx tsx scripts/fix-article-navigation.ts
```

### Step 4: Translate, Validate & Verify Analysis Quality
Expand Down
231 changes: 231 additions & 0 deletions scripts/fix-article-navigation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
#!/usr/bin/env -S npx tsx
/**
* Fix Article Navigation: Language Switcher + Back-to-News Top Nav
*
* This script ensures ALL news articles have:
* 1. A language switcher nav (14 languages) after <body>
* 2. An article-top-nav div with a localized back-to-news link before the article
*
* It auto-discovers all articles in the news/ directory and processes them
* idempotently — safe to run multiple times.
*
* Usage:
* npx tsx scripts/fix-article-navigation.ts
* npx tsx scripts/fix-article-navigation.ts --dry-run
*
* @author Hack23 AB
* @license Apache-2.0
*/

import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import type { Language } from './types/language.js';
import { ALL_LANG_CODES, FOOTER_LABELS } from './article-template/constants.js';
import { getNewsIndexFilename, generateArticleLanguageSwitcher } from './article-template/helpers.js';

// ── Helpers ───────────────────────────────────────────────────────────────

function extractLang(filename: string): Language | null {
const name = filename.replace(/\.html$/, '');
for (const lang of ALL_LANG_CODES) {
if (name.endsWith(`-${lang}`)) return lang;
}
return null;
}

function extractBase(filename: string): string | null {
const name = filename.replace(/\.html$/, '');
for (const lang of ALL_LANG_CODES) {
if (name.endsWith(`-${lang}`)) return name.slice(0, -(lang.length + 1));
}
return null;
}

function generateTopNav(lang: Language): string {
const label = FOOTER_LABELS[lang].backToNews;
const index = getNewsIndexFilename(lang);
return `\n<div class="article-top-nav">\n <a href="${index}" class="back-to-news">\n ← ${label}\n </a>\n</div>\n`;
}

// ── Processing ────────────────────────────────────────────────────────────

export interface ProcessResult {
addedSwitcher: boolean;
addedTopnav: boolean;
fixedTopnav: boolean;
}

/**
* Transform article HTML string: ensures language-switcher and article-top-nav
* with a back-to-news link are present. Idempotent — safe to call multiple times.
*
* @param content Original HTML string
* @param baseSlug Article base slug without directory prefix (e.g. "2026-01-01-article")
* @param lang Language code for this variant
* @returns Updated HTML string and flags indicating what changed
*/
export function transformContent(
content: string,
baseSlug: string,
lang: Language,
): { content: string } & ProcessResult {
let result = content;
let addedSwitcher = false;
let addedTopnav = false;
let fixedTopnav = false;

// ── 1. Language switcher ──────────────────────────────────────────
const hasSwitcher = result.includes('language-switcher');
if (!hasSwitcher) {
const switcherHtml = generateArticleLanguageSwitcher(baseSlug, lang);
// Prefer inserting AFTER the skip-link so it remains the first focusable element.
const skipLinkPattern = /(<a[^>]*class="skip-link"[^>]*>[\s\S]*?<\/a>)/;
if (skipLinkPattern.test(result)) {
result = result.replace(skipLinkPattern, `$1\n${switcherHtml}`);
} else {
result = result.replace(/(<body[^>]*>)/, `$1\n${switcherHtml}`);
}
addedSwitcher = true;
} else {
// Update existing switcher to have all 14 languages.
// Use [^\S\n]* (spaces/tabs, not newlines) to consume any indentation before <nav>,
// so the replacement string's own leading spaces don't accumulate on repeated runs.
const newSwitcher = generateArticleLanguageSwitcher(baseSlug, lang);
result = result.replace(/[^\S\n]*<nav class="language-switcher"[^>]*>[\s\S]*?<\/nav>/, newSwitcher);
}

// ── 2. article-top-nav ────────────────────────────────────────────
const hasTopnav = result.includes('article-top-nav');

// If top-nav exists but is missing back-to-news link, replace it
if (hasTopnav) {
const topNavHasBackLink =
/<div class="article-top-nav">[\s\S]*?class="back-to-news"[\s\S]*?<\/div>/.test(result);
if (!topNavHasBackLink) {
result = result.replace(
/<div class="article-top-nav">[\s\S]*?<\/div>/,
generateTopNav(lang).trim(),
);
fixedTopnav = true;
}
}

if (!hasTopnav) {
const topNavHtml = generateTopNav(lang);
let inserted = false;

// Pattern A: insert after closing </nav> of language-switcher, before article/div.news-article
if (result.includes('</nav>')) {
const navPattern = /((<\/nav>)([\s]*)(<(?:article|div)\s+class="(?:news-article|container)"))/s;
const match = navPattern.exec(result);
if (match) {
const endOfNav = match.index + match[2].length;
const whitespace = match[3];
const articleTag = match[4];
const afterFull = result.slice(match.index + match[0].length);
result = result.slice(0, endOfNav) + topNavHtml + whitespace + articleTag + afterFull;
inserted = true;
}
}

// Pattern B: insert directly before <article class="news-article"> or <div class="container">
if (!inserted) {
const articlePattern = /(<(?:article|div)\s+class="(?:news-article|container)")/;
const match = articlePattern.exec(result);
if (match) {
result = result.slice(0, match.index) + topNavHtml + '\n' + result.slice(match.index);
inserted = true;
}
}

if (inserted) addedTopnav = true;
}

return { content: result, addedSwitcher, addedTopnav, fixedTopnav };
}

function processArticle(filepath: string, baseSlug: string, lang: Language, dryRun: boolean): ProcessResult {
const original = fs.readFileSync(filepath, 'utf-8');
const { content, addedSwitcher, addedTopnav, fixedTopnav } = transformContent(original, baseSlug, lang);

// ── Write if changed ──────────────────────────────────────────────
if (content !== original && !dryRun) {
fs.writeFileSync(filepath, content, 'utf-8');
}

return { addedSwitcher, addedTopnav, fixedTopnav };
}

interface ArticleMap {
[baseSlug: string]: Partial<Record<Language, string>>;
}

function discoverArticles(newsDir: string): ArticleMap {
const articles: ArticleMap = {};
const files = fs.readdirSync(newsDir).sort();
for (const name of files) {
if (!name.endsWith('.html') || name.startsWith('index')) continue;
const lang = extractLang(name);
const base = extractBase(name);
if (lang && base) {
if (!articles[base]) articles[base] = {};
articles[base][lang] = path.join(newsDir, name);
}
}
return articles;
}

function main(): void {
const dryRun = process.argv.includes('--dry-run');
if (dryRun) {
console.log('=== DRY RUN — no files will be modified ===\n');
}

const scriptDir = path.dirname(fileURLToPath(import.meta.url));
const newsDir = path.resolve(scriptDir, '..', 'news');

if (!fs.existsSync(newsDir)) {
console.error(`ERROR: news directory not found at ${newsDir}`);
process.exit(1);
}

console.log('=== Fix Article Navigation ===');
console.log(`News directory: ${newsDir}\n`);

const articles = discoverArticles(newsDir);
const slugs = Object.keys(articles).sort();
console.log(`Discovered ${slugs.length} unique article slugs\n`);

let total = 0;
let switchersAdded = 0;
let topnavsAdded = 0;
let topnavsFixed = 0;

for (const baseSlug of slugs) {
const langFiles = articles[baseSlug];
for (const lang of ALL_LANG_CODES) {
const filepath = langFiles[lang];
if (!filepath) continue;
total++;
const { addedSwitcher, addedTopnav, fixedTopnav } = processArticle(filepath, baseSlug, lang, dryRun);
if (addedSwitcher) switchersAdded++;
if (addedTopnav) topnavsAdded++;
if (fixedTopnav) topnavsFixed++;
}
}

console.log('=== Summary ===');
console.log(`Total files processed: ${total}`);
console.log(`Language switchers added: ${switchersAdded}`);
console.log(`Top nav (article-top-nav) added: ${topnavsAdded}`);
console.log(`Top nav fixed (missing back-to-news link): ${topnavsFixed}`);
if (dryRun) {
console.log('\n(Dry run — no files were modified)');
}
console.log('\n✓ Done!');
}

if (import.meta.url === `file://${process.argv[1]}`) {
main();
}
Loading
Loading