Skip to content

Add client-side search, pagination, and URL state to news index pages#701

Draft
Copilot wants to merge 16 commits intomainfrom
copilot/add-pagination-search-filtering
Draft

Add client-side search, pagination, and URL state to news index pages#701
Copilot wants to merge 16 commits intomainfrom
copilot/add-pagination-search-filtering

Conversation

Copy link
Contributor

Copilot AI commented Mar 1, 2026

  • Add search, searchPlaceholder, loadMore, showing fields to I18nStrings in types.ts
  • Add translations for all 14 languages in constants.ts
  • Update template.ts: search input, 20-article pagination + "Load more", debounced search, URL state management, WCAG 2.1 AA ARIA labels and focus management
  • Add CSS for search input, pagination controls, load-more button in styles.css
  • Use JSON.stringify for i18nLoadMore to avoid HTML entities in JS (fixes French apostrophe bug)
  • readURLParams() validates URL params against allowed select options before applying
  • Remove conflicting DOMContentLoaded handler that could overwrite pagination/search state
  • Fix search input CSS: :focus-visible with explicit outline (no outline: none)
  • Fix XSS in buildArticleCard: add esc() helper and escape all article fields before innerHTML interpolation
  • Add safeHref() helper — now uses strict allowlist [A-Za-z0-9._-]+\.html$ (rejects backslashes, control chars, non-html slugs); fixes ESLint no-useless-escape CI failure
  • Fix displayData JSON embedding: escape </ as <\/ to prevent </script> in article data from breaking the script tag
  • Replace inline onclick="loadMore()" with addEventListener('click', loadMore) to avoid inline handler / CSP issues
  • Fix brittle addEventListener test assertion to use regex /addEventListener\s*\(\s*['"]click['"]\s*,\s*loadMore\s*\)/
  • All 34 tests pass, ESLint clean, CodeQL 0 alerts
Original prompt

This section details on the original issue you should resolve

<issue_title>Add pagination and search filtering to news index pages</issue_title>
<issue_description>## 📋 Issue Type
UI/UX / Performance

🎯 Objective

Add client-side pagination, filtering, and search to the news index pages to improve navigation and performance when browsing 900+ articles.

📊 Current State

  • News index pages (news/index.html, news/index_sv.html, etc.) list all articles in a single page
  • With 949+ articles, the page is very long with no pagination
  • No client-side search or filtering capability
  • Article type filter buttons exist but all articles load at once
  • Mobile users must scroll through hundreds of article cards

🚀 Desired State

  • Client-side pagination (20-30 articles per page)
  • Search/filter by title, date, type, or topic
  • Article type filter buttons with instant filtering (CSS-based or minimal JS)
  • "Load more" progressive loading pattern
  • Responsive design maintained across all 14 languages
  • Accessible keyboard navigation for pagination controls
  • URL state management (e.g., ?page=2&type=committee-reports)

🔧 Implementation Approach

  1. Add CSS-based article type filtering using :checked pseudo-class selectors
  2. Implement "show more" progressive disclosure (start with 20 articles, load 20 more on button click)
  3. Add search input with debounced JavaScript filtering
  4. Update news index template in scripts/generate-news-indexes/template.ts
  5. Add keyboard navigation for pagination controls
  6. Support RTL layout for Arabic and Hebrew
  7. Test across all 14 language variants

🤖 Recommended Agent

ui-enhancement-specialist — Progressive loading and filtering

✅ Acceptance Criteria

  • News index shows max 20-30 articles initially
  • "Load more" or pagination to access older articles
  • Article type filtering works without full page reload
  • Search input filters articles by title
  • Keyboard navigable pagination controls
  • WCAG 2.1 AA compliant (ARIA labels, focus management)
  • Works in RTL layout (Arabic, Hebrew)
  • Mobile responsive (320px-1440px+)
  • All 14 language variants updated

📚 References

  • Template: scripts/generate-news-indexes/template.ts
  • News indexes: news/index.html, news/index_sv.html, etc.
  • Accessibility: WCAG 2.1 AA Level
  • Design system: styles.css (cyberpunk theme)

🏷️ Labels

type:feature, priority:medium, component:ui-ux</issue_description>

<agent_instructions>Add client-side pagination to news index pages. Start with 20 articles shown, then "Load more" button reveals 20 more. Add search input for title filtering. Update scripts/generate-news-indexes/template.ts. Ensure WCAG 2.1 AA compliant including RTL languages.</agent_instructions>

Comments on the Issue (you are @copilot in this section)


🔒 GitHub Advanced Security automatically protects Copilot coding agent pull requests. You can protect all pull requests by enabling Advanced Security for your repositories. Learn more about Advanced Security.

- Add search, searchPlaceholder, loadMore, showing to I18nStrings interface
- Add translations for all 14 languages in constants.ts
- Update template.ts: search input, 20-article pagination with Load More,
  debounced search, URL state management (?q=, ?type=, ?topic=, ?sort=),
  WCAG 2.1 AA ARIA labels and focus management
- Add CSS for search input, pagination controls, load-more button

Co-authored-by: pethers <1726836+pethers@users.noreply.github.com>
Copilot AI changed the title [WIP] Add pagination and search filtering to news index pages Add client-side search, pagination, and URL state to news index pages Mar 1, 2026
@github-actions github-actions bot added html-css HTML/CSS changes refactor Code refactoring size-m Medium change (50-250 lines) labels Mar 1, 2026
@github-actions
Copy link
Contributor

github-actions bot commented Mar 1, 2026

🔍 Lighthouse Performance Audit

Category Score Status
Performance 85/100 🟡
Accessibility 95/100 🟢
Best Practices 90/100 🟢
SEO 95/100 🟢

📥 Download full Lighthouse report

Budget Compliance: Performance budgets enforced via budget.json

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates the generated multi-language news index pages to remain usable with 900+ articles by adding client-side search, progressive “load more” pagination, and URL query-state syncing.

Changes:

  • Add debounced title search input and integrate it into existing type/topic/sort filtering.
  • Add progressive pagination (PAGE_SIZE=20) with a “Load more” button, plus an aria-live “showing X of Y” counter.
  • Add translations for the new UI strings across all 14 languages and add scoped CSS for the new controls.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 5 comments.

File Description
styles.css Adds .news-page-scoped styles for search input, counter, and load-more button.
scripts/generate-news-indexes/types.ts Extends I18nStrings with new keys for search/pagination UI.
scripts/generate-news-indexes/template.ts Implements search, pagination rendering, focus management, and URL param syncing in generated index HTML/JS.
scripts/generate-news-indexes/constants.ts Adds translations for the new i18n keys across all supported languages.

if (btn) {
if (total > visibleCount) {
btn.style.display = 'inline-block';
btn.setAttribute('aria-label', i18nLoadMore);
Copy link

Copilot AI Mar 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i18nLoadMore is built using escapeHtml(...) and then used via btn.setAttribute('aria-label', i18nLoadMore). Because HTML entities are not decoded when setting attributes from JS, languages containing apostrophes (e.g. French) will end up with a literal "'" in the aria-label. Define i18nLoadMore as a proper JS string literal (e.g. via JSON.stringify(...)) or avoid resetting the aria-label since it’s already set in the HTML.

Suggested change
btn.setAttribute('aria-label', i18nLoadMore);

Copilot uses AI. Check for mistakes.
Comment on lines 429 to 432
if (params.has('type')) document.getElementById('filter-type').value = params.get('type');
if (params.has('topic')) document.getElementById('filter-topic').value = params.get('topic');
if (params.has('sort')) document.getElementById('filter-sort').value = params.get('sort');
if (params.has('q')) document.getElementById('search-input').value = params.get('q');
Copy link

Copilot AI Mar 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

readURLParams() assigns query param values directly into the <select>/<input> values without validating them against allowed options. Invalid values (e.g. ?type=bogus or ?type=) can force the UI into a zero-results state and desync the selects from their option lists. Consider whitelisting allowed values (or checking for a matching <option value="...">) and falling back to defaults when invalid.

Suggested change
if (params.has('type')) document.getElementById('filter-type').value = params.get('type');
if (params.has('topic')) document.getElementById('filter-topic').value = params.get('topic');
if (params.has('sort')) document.getElementById('filter-sort').value = params.get('sort');
if (params.has('q')) document.getElementById('search-input').value = params.get('q');
// Safely apply type filter from URL if it matches an existing option
var typeSelect = document.getElementById('filter-type');
var typeParam = params.get('type');
if (typeSelect && typeSelect.tagName === 'SELECT' && typeParam) {
var typeOptions = Array.prototype.slice.call(typeSelect.options);
var hasTypeOption = typeOptions.some(function (option) {
return option.value === typeParam;
});
if (hasTypeOption) {
typeSelect.value = typeParam;
}
}
// Safely apply topic filter from URL if it matches an existing option
var topicSelect = document.getElementById('filter-topic');
var topicParam = params.get('topic');
if (topicSelect && topicSelect.tagName === 'SELECT' && topicParam) {
var topicOptions = Array.prototype.slice.call(topicSelect.options);
var hasTopicOption = topicOptions.some(function (option) {
return option.value === topicParam;
});
if (hasTopicOption) {
topicSelect.value = topicParam;
}
}
// Safely apply sort filter from URL if it matches an existing option
var sortSelect = document.getElementById('filter-sort');
var sortParam = params.get('sort');
if (sortSelect && sortSelect.tagName === 'SELECT' && sortParam) {
var sortOptions = Array.prototype.slice.call(sortSelect.options);
var hasSortOption = sortOptions.some(function (option) {
return option.value === sortParam;
});
if (hasSortOption) {
sortSelect.value = sortParam;
}
}
// Apply search query directly; arbitrary text is expected here
var searchInput = document.getElementById('search-input');
var qParam = params.get('q');
if (searchInput && qParam !== null && 'value' in searchInput) {
searchInput.value = qParam;
}

Copilot uses AI. Check for mistakes.
Comment on lines 421 to +425
filteredArticles = filtered;
renderArticles(filteredArticles);
visibleCount = PAGE_SIZE;
updateURL();
renderPage();
}
Copy link

Copilot AI Mar 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With pagination/search, the existing "Dynamic Content Loader" (DOMContentLoaded) logic later in this template can conflict with the new no-results/pagination rendering: when URL params yield 0 matches on initial load, renderPage() shows #no-results, but the DOMContentLoaded handler will see 0 .article-card elements and overwrite .articles-grid with a .no-articles paragraph. This can lead to duplicate/confusing empty states. Remove/disable that handler for news indexes (or make it aware of filteredArticles/#no-results).

Copilot uses AI. Check for mistakes.
styles.css Outdated
Comment on lines 8976 to 8981
.news-page .filter-group input[type="search"]:hover,
.news-page .filter-group input[type="search"]:focus {
border-color: var(--primary-cyan, #00d9ff);
outline: none;
}

Copy link

Copilot AI Mar 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The search input CSS removes the default focus outline (outline: none) on :focus without providing an equivalent visible focus indicator. This can fail WCAG 2.1 focus visibility expectations for keyboard users. Prefer using :focus-visible with an explicit outline (similar to .load-more-btn:focus-visible) and avoid removing focus styling entirely.

Suggested change
.news-page .filter-group input[type="search"]:hover,
.news-page .filter-group input[type="search"]:focus {
border-color: var(--primary-cyan, #00d9ff);
outline: none;
}
.news-page .filter-group input[type="search"]:hover {
border-color: var(--primary-cyan, #00d9ff);
}
.news-page .filter-group input[type="search"]:focus-visible {
border-color: var(--primary-cyan, #00d9ff);
outline: 2px solid var(--primary-cyan, #00d9ff);
outline-offset: 2px;
}

Copilot uses AI. Check for mistakes.
Comment on lines +231 to +233
<div class="filter-group search-group">
<label for="search-input">${escapeHtml(lang.i18n.search)}</label>
<input type="search" id="search-input" placeholder="${escapeHtml(lang.i18n.searchPlaceholder)}" aria-label="${escapeHtml(lang.i18n.search)}" autocomplete="off">
Copy link

Copilot AI Mar 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are existing unit tests for news index generation, but the newly introduced client-side behaviors (search input, load-more/counter, and URL param keys such as q/type/topic/sort) aren’t currently asserted. Adding a few HTML-string assertions in tests/generate-news-indexes.test.ts would help prevent regressions (e.g., ensure #search-input, #load-more-btn, PAGE_SIZE, and readURLParams()/updateURL() appear in generated pages).

Copilot uses AI. Check for mistakes.
@pethers
Copy link
Member

pethers commented Mar 1, 2026

@copilot apply changes based on the comments in this thread

@github-actions
Copy link
Contributor

github-actions bot commented Mar 1, 2026

🔍 Lighthouse Performance Audit

Category Score Status
Performance 85/100 🟡
Accessibility 95/100 🟢
Best Practices 90/100 🟢
SEO 95/100 🟢

📥 Download full Lighthouse report

Budget Compliance: Performance budgets enforced via budget.json

…ocus CSS, remove conflicting handler, add tests

- Use JSON.stringify for i18nLoadMore to avoid HTML entities in JS aria-label
- readURLParams() now validates select params against existing option values
- Remove conflicting DOMContentLoaded handler that could overwrite pagination state
- Fix search input CSS: use :focus-visible with explicit outline instead of outline:none
- Modernize safeSetSelect to use const/let and arrow functions
- Add 5 tests: search input, load-more/counter, pagination, URL state, no DOMContentLoaded

Co-authored-by: pethers <1726836+pethers@users.noreply.github.com>
Copy link
Contributor Author

Copilot AI commented Mar 1, 2026

@copilot apply changes based on the comments in this thread

Applied all 5 review comments in commit 630736f:

  1. i18nLoadMore encoding — switched from escapeHtml(...) to JSON.stringify(...) so French d'articles appears as a literal apostrophe in the JS string, not an HTML entity.
  2. URL param validationreadURLParams() now has a safeSetSelect() helper that checks Array.from(el.options).some(o => o.value === value) before setting any select value; the search <input> still accepts arbitrary text.
  3. Conflicting DOMContentLoaded handler — the old "Dynamic Content Loader" block that overwrote articles-grid on zero results has been removed; the pagination/search renderPage() is the single source of truth for empty-state rendering.
  4. Focus visibility — split the combined :hover, :focus { outline: none } rule into a :hover rule (border only) and a :focus-visible rule with an explicit outline: 2px solid to meet WCAG 2.1 AA.
  5. New tests — added 5 assertions to tests/generate-news-indexes.test.ts covering presence of #search-input, #load-more-btn/aria-live, PAGE_SIZE/loadMore(), readURLParams()/updateURL(), and absence of DOMContentLoaded/no-articles.

@pethers pethers requested a review from Copilot March 1, 2026 11:20
@github-actions github-actions bot added testing Test coverage size-l Large change (250-1000 lines) labels Mar 1, 2026
@github-actions
Copy link
Contributor

github-actions bot commented Mar 1, 2026

🔍 Lighthouse Performance Audit

Category Score Status
Performance 85/100 🟡
Accessibility 95/100 🟢
Best Practices 90/100 🟢
SEO 95/100 🟢

📥 Download full Lighthouse report

Budget Compliance: Performance budgets enforced via budget.json

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.

Comment on lines 294 to 301
<h2 class="article-title">
<a href="\${article.slug}">\${article.title}</a>
</h2>
<p class="article-excerpt">\${article.excerpt}</p>
\${availableDisplay}
<div class="article-tags">
\${article.tags.map(tag => \`<span class="tag">\${tag}</span>\`).join('')}
</div>
Copy link

Copilot AI Mar 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

buildArticleCard() interpolates article.title, article.excerpt, article.tags, and article.slug directly into an HTML string that is assigned via innerHTML. Because these values are parsed from article HTML meta/title fields (see parseArticleMetadata()), this can break markup on characters like </& and also creates an XSS injection surface if article content is ever compromised. Consider escaping these fields before insertion (or build DOM nodes and set textContent/setAttribute instead of using innerHTML).

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Copilot AI commented Mar 1, 2026

@copilot apply changes based on the comments in this thread

Fixed in commit d3f82b5. Removed onclick="loadMore()" from the button element and added document.getElementById('load-more-btn').addEventListener('click', loadMore) alongside the other filter event listeners in the script block. The test now also asserts the onclick attribute is absent and the addEventListener wiring is present.

@github-actions
Copy link
Contributor

github-actions bot commented Mar 1, 2026

🔍 Lighthouse Performance Audit

Category Score Status
Performance 85/100 🟡
Accessibility 95/100 🟢
Best Practices 90/100 🟢
SEO 95/100 🟢

📥 Download full Lighthouse report

Budget Compliance: Performance budgets enforced via budget.json

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.

Comment on lines 302 to 304
<h2 class="article-title">
<a href="\${esc(article.slug)}">\${esc(article.title)}</a>
</h2>
Copy link

Copilot AI Mar 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

esc() prevents HTML injection, but it does not prevent href-based XSS if article.slug ever contains a URL scheme (e.g. javascript:) because the browser will still treat it as a script URL even when escaped. Consider forcing generated links to be relative (e.g. prefixing with ./ or validating/rejecting slug values containing :///) before interpolating into the href attribute.

Copilot uses AI. Check for mistakes.
@pethers
Copy link
Member

pethers commented Mar 1, 2026

@copilot apply changes based on the comments in this thread

@github-actions
Copy link
Contributor

github-actions bot commented Mar 1, 2026

🔍 Lighthouse Performance Audit

Category Score Status
Performance 85/100 🟡
Accessibility 95/100 🟢
Best Practices 90/100 🟢
SEO 95/100 🟢

📥 Download full Lighthouse report

Budget Compliance: Performance budgets enforced via budget.json

- Add safeHref() helper to generated JS that rejects any slug containing
  a protocol scheme (e.g. javascript:) or absolute path separators,
  falling back to '#' for unsafe values
- Replace esc(article.slug) with safeHref(article.slug) in the href
  attribute of the article link
- Update test to assert safeHref() is present and used for hrefs

Co-authored-by: pethers <1726836+pethers@users.noreply.github.com>
Copy link
Contributor Author

Copilot AI commented Mar 1, 2026

@copilot apply changes based on the comments in this thread

Fixed in commit a9535ba. Added a safeHref() helper to the generated JS that checks the slug path component (before any ? or #) for : or / characters, and separately tests for a protocol scheme pattern (^[a-zA-Z][a-zA-Z0-9+-.]*:). Any slug that matches either check is replaced with #; otherwise it is passed through esc(). The article link now uses safeHref(article.slug) instead of esc(article.slug), and the test asserts both the helper and its usage are present.

@github-actions
Copy link
Contributor

github-actions bot commented Mar 1, 2026

🔍 Lighthouse Performance Audit

Category Score Status
Performance 85/100 🟡
Accessibility 95/100 🟢
Best Practices 90/100 🟢
SEO 95/100 🟢

📥 Download full Lighthouse report

Budget Compliance: Performance budgets enforced via budget.json

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated 3 comments.

Comment on lines 283 to 285
// Dynamic articles array - generated from news/ directory
const articles = ${JSON.stringify(displayData, null, 2)};

Copy link

Copilot AI Mar 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

displayData is embedded directly into an inline <script> via JSON.stringify(displayData, null, 2). If any article field ever contains the substring </script> (e.g., via a crafted <title>/og:title), the browser will terminate the script tag early and allow HTML/JS injection before esc() ever runs. Please serialize this data in a script-safe way (e.g., escape </</script in the JSON output, or place JSON in a <script type="application/json"> element and read textContent).

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in ff04ffb. The JSON.stringify(displayData, null, 2) call now chains .replace(/<\//g, '<\\/'), replacing every </ with <\/ in the serialized JSON. This prevents any </script> substring in article titles/excerpts from prematurely closing the script tag. At runtime the JS string value is unchanged since \/ evaluates as /.

Comment on lines 268 to 271
// Reject any value that looks like an absolute/protocol URL to prevent href-based XSS
if (/[:/]/.test(s.split('?')[0].split('#')[0]) || /^[a-zA-Z][a-zA-Z0-9+\-.]*:/.test(s)) {
return '#';
}
Copy link

Copilot AI Mar 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

safeHref() currently blocks : and /, but it still allows backslashes and other characters that browsers may normalize in surprising ways (e.g., \\example.com can be treated similarly to //example.com in some URL parsers). Consider tightening validation to an explicit allowlist for expected slugs (e.g., only [A-Za-z0-9._-] plus a required .html suffix) and rejecting \/control characters as well, instead of relying on the current substring checks.

Suggested change
// Reject any value that looks like an absolute/protocol URL to prevent href-based XSS
if (/[:/]/.test(s.split('?')[0].split('#')[0]) || /^[a-zA-Z][a-zA-Z0-9+\-.]*:/.test(s)) {
return '#';
}
// Only allow simple relative HTML filenames to avoid URL parsing/normalization issues.
// Reject empty strings, backslashes, and control characters outright.
if (!s || /[\\\x00-\x1F\x7F]/.test(s)) {
return '#';
}
// Allow only [A-Za-z0-9._-] characters with a required ".html" suffix.
if (!/^[A-Za-z0-9._-]+\.html$/.test(s)) {
return '#';
}

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in ff04ffb. safeHref() now uses an explicit allowlist: rejects empty strings, backslashes, and control characters (/[\\\x00-\x1F\x7F]/), then requires the slug to match /^[A-Za-z0-9._-]+\.html$/ exactly — backslashes and any non-HTML filename form return #. This also fixes the ESLint no-useless-escape CI failure.

expect(enContent).toContain('aria-live="polite"');
// load-more must use addEventListener, not an inline onclick handler
expect(enContent).not.toContain('onclick="loadMore()"');
expect(enContent).toContain("getElementById('load-more-btn').addEventListener('click', loadMore)");
Copy link

Copilot AI Mar 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test asserts an exact JS source substring for the addEventListener call, which makes the test brittle to harmless formatting/minification changes (quote style, whitespace, document. prefix, etc.). Prefer a regex or a less exact assertion (e.g., check that load-more-btn exists and that the generated script contains both addEventListener and loadMore in proximity).

Suggested change
expect(enContent).toContain("getElementById('load-more-btn').addEventListener('click', loadMore)");
expect(enContent).toMatch(/addEventListener\s*\(\s*['"]click['"]\s*,\s*loadMore\s*\)/);

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in ff04ffb. Changed from toContain(...) with a hardcoded string to toMatch(/addEventListener\s*\(\s*['"]click['"]\s*,\s*loadMore\s*\)/) as suggested.

@pethers
Copy link
Member

pethers commented Mar 1, 2026

@copilot apply changes based on the comments in this thread and Quality Checks / typescript-lint (pull_request)Failing after 37s

@github-actions
Copy link
Contributor

github-actions bot commented Mar 1, 2026

🔍 Lighthouse Performance Audit

Category Score Status
Performance 85/100 🟡
Accessibility 95/100 🟢
Best Practices 90/100 🟢
SEO 95/100 🟢

📥 Download full Lighthouse report

Budget Compliance: Performance budgets enforced via budget.json

…gex in test

Co-authored-by: pethers <1726836+pethers@users.noreply.github.com>
Copy link
Contributor Author

Copilot AI commented Mar 1, 2026

@copilot apply changes based on the comments in this thread and [Quality Checks / typescript-lint (pull_request)](htt...

All three review comments addressed and the ESLint CI failure fixed in commit ff04ffb:

  1. safeHref() tightened — strict allowlist [A-Za-z0-9._-]+\.html$, rejects backslashes/control chars, also resolves the no-useless-escape ESLint failure.
  2. </script> injection preventedJSON.stringify output now chains .replace(/<\//g, '<\\/') so article data can never close the script tag.
  3. addEventListener test — brittle toContain replaced with toMatch(/addEventListener\s*\(\s*['"]click['"]\s*,\s*loadMore\s*\)/).

All 34 tests pass, ESLint clean.

@github-actions
Copy link
Contributor

github-actions bot commented Mar 1, 2026

🔍 Lighthouse Performance Audit

Category Score Status
Performance 85/100 🟡
Accessibility 95/100 🟢
Best Practices 90/100 🟢
SEO 95/100 🟢

📥 Download full Lighthouse report

Budget Compliance: Performance budgets enforced via budget.json

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.

Comment on lines +295 to +306
const flag = LANGUAGE_FLAGS[article.lang] || '🌐';
const langBadge = \`<span class="language-badge" aria-label="\${esc(article.lang)} language"><span aria-hidden="true">\${flag}</span> \${esc(article.lang.toUpperCase())}</span>\`;

// Generate available languages display if multiple languages exist
const availableLangs = article.availableLanguages || [article.lang];
let availableDisplay = '';
if (availableLangs.length > 1) {
const availableBadges = availableLangs.map(l => {
const lf = LANGUAGE_FLAGS[l] || '🌐';
return \`<span class="lang-badge-sm"><span aria-hidden="true">\${lf}</span> \${esc(l.toUpperCase())}</span>\`;
}).join(' ');
availableDisplay = \`<p class="available-languages"><strong>\${AVAILABLE_IN_TEXT}:</strong> \${availableBadges}</p>\`;
Copy link

Copilot AI Mar 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

buildArticleCard() renders language badges without dir="ltr". On RTL pages (ar/he), language codes like EN/SV can be displayed in the wrong direction. There is already a repo pattern for this in generateLanguageBadge() (adds dir="ltr" when isRTL), so the client-side badges should apply the same RTL handling (both the main language-badge and the smaller lang-badge-sm badges).

Copilot uses AI. Check for mistakes.
Comment on lines +334 to 339
if (filteredArticles.length === 0) {
grid.innerHTML = '';
noResults.style.display = 'block';
if (counter) counter.textContent = '';
if (btn) btn.style.display = 'none';
return;
Copy link

Copilot AI Mar 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

renderPage() uses the no-results message whenever filteredArticles.length === 0, which also covers the "no articles exist at all" case. That makes the UI misleading ("No articles matched the filters") and leaves lang.i18n.noArticles unused. Consider distinguishing between articles.length === 0 (show i18n.noArticles) vs. filteredArticles.length === 0 (show lang.noResults).

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

html-css HTML/CSS changes refactor Code refactoring size-l Large change (250-1000 lines) size-m Medium change (50-250 lines) testing Test coverage

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add pagination and search filtering to news index pages

3 participants