[object Object]

← back to Cypress Awards

fix: remove backup source-leak files from served dir, harden .gitignore, add rel=noopener to blank-target links

851d492020b95ca24eed449df1105a5501a5cb2c · 2026-05-30 23:52:03 -0700 · SteveStudio2

- Deleted frontend/src/index.html.backup and app.js.backup which were publicly served by express.static
- Added *.bak, *.backup, *.bak-*, *.pre-*, *.orig, *.save, *.old, *~, copy-of-* to .gitignore
- Added rel="noopener noreferrer" to all target="_blank" links in news.html, cases.html, outreach.html, simple.html

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

Files touched

Diff

commit 851d492020b95ca24eed449df1105a5501a5cb2c
Author: SteveStudio2 <stevestudio2@SteveStacStudio.lan>
Date:   Sat May 30 23:52:03 2026 -0700

    fix: remove backup source-leak files from served dir, harden .gitignore, add rel=noopener to blank-target links
    
    - Deleted frontend/src/index.html.backup and app.js.backup which were publicly served by express.static
    - Added *.bak, *.backup, *.bak-*, *.pre-*, *.orig, *.save, *.old, *~, copy-of-* to .gitignore
    - Added rel="noopener noreferrer" to all target="_blank" links in news.html, cases.html, outreach.html, simple.html
    
    Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---
 .gitignore                     |   9 +
 frontend/src/app.js.backup     | 392 -----------------------------------------
 frontend/src/cases.html        |  10 +-
 frontend/src/index.html.backup | 129 --------------
 frontend/src/news.html         |  12 +-
 frontend/src/outreach.html     |   2 +-
 frontend/src/simple.html       |   4 +-
 7 files changed, 23 insertions(+), 535 deletions(-)

diff --git a/.gitignore b/.gitignore
index 1d7a585..0c3327a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -11,4 +11,13 @@ coverage/
 .DS_Store
 *.log
 tmp/
+*.bak
+*.bak-*
+*.backup
+*.pre-*
+*.orig
+*.save
+*.old
+*~
+copy-of-*
 
diff --git a/frontend/src/app.js.backup b/frontend/src/app.js.backup
deleted file mode 100644
index d7352f6..0000000
--- a/frontend/src/app.js.backup
+++ /dev/null
@@ -1,392 +0,0 @@
-// CyPresAwards Frontend Application
-
-const API_BASE_URL = 'http://45.61.58.125:3010/api';
-
-// Application state
-const state = {
-  organizations: [],
-  categories: [],
-  legalTopics: [],
-  currentPage: 1,
-  pageSize: 20,
-  totalResults: 0,
-  filters: {
-    search: '',
-    legalTopics: [],
-    categories: [],
-    state: ''
-  }
-};
-
-// DOM Elements
-const elements = {
-  searchInput: document.getElementById('searchInput'),
-  searchBtn: document.getElementById('searchBtn'),
-  legalTopicFilter: document.getElementById('legalTopicFilter'),
-  categoryFilter: document.getElementById('categoryFilter'),
-  stateFilter: document.getElementById('stateFilter'),
-  applyFiltersBtn: document.getElementById('applyFilters'),
-  clearFiltersBtn: document.getElementById('clearFilters'),
-  organizationsList: document.getElementById('organizationsList'),
-  loadingSpinner: document.getElementById('loadingSpinner'),
-  errorMessage: document.getElementById('errorMessage'),
-  resultsCount: document.getElementById('resultsCount'),
-  totalOrgs: document.getElementById('totalOrgs'),
-  totalCategories: document.getElementById('totalCategories'),
-  totalTopics: document.getElementById('totalTopics'),
-  prevPageBtn: document.getElementById('prevPage'),
-  nextPageBtn: document.getElementById('nextPage'),
-  pageInfo: document.getElementById('pageInfo')
-};
-
-// Initialize application
-async function init() {
-  console.log('Application initializing...');
-  console.log('API_BASE_URL:', API_BASE_URL);
-
-  try {
-    console.log('Loading initial data...');
-    await Promise.all([
-      loadCategories(),
-      loadLegalTopics(),
-      loadStats()
-    ]);
-
-    console.log('Loading organizations...');
-    await loadOrganizations();
-    setupEventListeners();
-    console.log('Application initialized successfully');
-  } catch (error) {
-    console.error('Initialization error:', error);
-    showError('Failed to initialize application. Please refresh the page.');
-  }
-}
-
-// Setup event listeners
-function setupEventListeners() {
-  elements.searchBtn.addEventListener('click', handleSearch);
-  elements.searchInput.addEventListener('keypress', (e) => {
-    if (e.key === 'Enter') handleSearch();
-  });
-
-  elements.applyFiltersBtn.addEventListener('click', handleApplyFilters);
-  elements.clearFiltersBtn.addEventListener('click', handleClearFilters);
-
-  elements.prevPageBtn.addEventListener('click', () => changePage(-1));
-  elements.nextPageBtn.addEventListener('click', () => changePage(1));
-}
-
-// API Functions
-async function fetchAPI(endpoint, params = {}) {
-  const queryString = new URLSearchParams(params).toString();
-  const url = `${API_BASE_URL}${endpoint}${queryString ? '?' + queryString : ''}`;
-
-  console.log('Fetching:', url);
-
-  try {
-    const response = await fetch(url, {
-      method: 'GET',
-      headers: {
-        'Accept': 'application/json',
-        'Content-Type': 'application/json'
-      },
-      mode: 'cors',
-      credentials: 'omit'
-    });
-
-    console.log('Response status:', response.status);
-    console.log('Response ok:', response.ok);
-
-    if (!response.ok) {
-      const errorText = await response.text();
-      console.error('Response error text:', errorText);
-      throw new Error(`API request failed: ${response.status} ${response.statusText}`);
-    }
-
-    const data = await response.json();
-    console.log('Response data:', data);
-    return data;
-  } catch (error) {
-    console.error('Fetch error:', error);
-    console.error('Error type:', error.constructor.name);
-    throw error;
-  }
-}
-
-async function loadCategories() {
-  try {
-    const response = await fetchAPI('/categories');
-    state.categories = response.data;
-    populateFilterSelect(elements.categoryFilter, state.categories);
-  } catch (error) {
-    console.error('Error loading categories:', error);
-  }
-}
-
-async function loadLegalTopics() {
-  try {
-    const response = await fetchAPI('/legal-topics');
-    state.legalTopics = response.data;
-    populateFilterSelect(elements.legalTopicFilter, state.legalTopics);
-  } catch (error) {
-    console.error('Error loading legal topics:', error);
-  }
-}
-
-async function loadStats() {
-  try {
-    const response = await fetchAPI('/stats');
-    elements.totalOrgs.textContent = response.data.totalOrganizations.toLocaleString();
-    elements.totalCategories.textContent = response.data.totalCategories;
-    elements.totalTopics.textContent = response.data.totalLegalTopics;
-  } catch (error) {
-    console.error('Error loading stats:', error);
-  }
-}
-
-async function loadOrganizations() {
-  console.log('loadOrganizations() called');
-  showLoading(true);
-  hideError();
-
-  try {
-    const params = {
-      limit: state.pageSize,
-      offset: (state.currentPage - 1) * state.pageSize
-    };
-
-    if (state.filters.search) {
-      params.search = state.filters.search;
-    }
-
-    if (state.filters.legalTopics.length > 0) {
-      params.legalTopics = state.filters.legalTopics.join(',');
-    }
-
-    if (state.filters.categories.length > 0) {
-      params.categories = state.filters.categories.join(',');
-    }
-
-    if (state.filters.state) {
-      params.state = state.filters.state;
-    }
-
-    console.log('Params:', params);
-    const response = await fetchAPI('/organizations', params);
-    console.log('Organizations loaded:', response.data.length);
-
-    state.organizations = response.data;
-    state.totalResults = response.meta.total;
-
-    renderOrganizations();
-    updatePagination();
-    updateResultsCount();
-
-  } catch (error) {
-    console.error('Error loading organizations:', error);
-    console.error('Error stack:', error.stack);
-    console.error('Error name:', error.name);
-    console.error('Error message:', error.message);
-    showError(`Failed to load organizations: ${error.message}. Please check console and try again.`);
-  } finally {
-    showLoading(false);
-  }
-}
-
-// UI Rendering Functions
-function populateFilterSelect(selectElement, items) {
-  // Keep the first option (e.g., "All Topics")
-  const firstOption = selectElement.options[0];
-  selectElement.innerHTML = '';
-  selectElement.appendChild(firstOption);
-
-  items.forEach(item => {
-    const option = document.createElement('option');
-    option.value = item.name;
-    option.textContent = item.name;
-    selectElement.appendChild(option);
-  });
-}
-
-function renderOrganizations() {
-  elements.organizationsList.innerHTML = '';
-
-  if (state.organizations.length === 0) {
-    elements.organizationsList.innerHTML = `
-      <div style="text-align: center; padding: 3rem; color: var(--text-secondary);">
-        <h3>No organizations found</h3>
-        <p>Try adjusting your search filters</p>
-      </div>
-    `;
-    return;
-  }
-
-  state.organizations.forEach(org => {
-    const card = createOrganizationCard(org);
-    elements.organizationsList.appendChild(card);
-  });
-}
-
-function createOrganizationCard(org) {
-  const card = document.createElement('div');
-  card.className = 'org-card';
-
-  // Parse arrays from PostgreSQL
-  const categories = org.categories ? parseArray(org.categories) : [];
-  const legalTopics = org.legal_topics ? parseArray(org.legal_topics) : [];
-
-  // Create logo or placeholder
-  const logoHTML = org.logo_url
-    ? `<img src="${org.logo_url}" alt="${org.name} logo" class="org-logo" onerror="this.style.display='none'">`
-    : `<div class="org-logo-placeholder">${org.name.charAt(0).toUpperCase()}</div>`;
-
-  // Create tags
-  const categoryTags = categories.filter(c => c).map(cat =>
-    `<span class="tag category">${cat}</span>`
-  ).join('');
-
-  const topicTags = legalTopics.filter(t => t).map(topic =>
-    `<span class="tag legal-topic">${topic}</span>`
-  ).join('');
-
-  // Location
-  const location = [org.location_city, org.location_state, org.location_country]
-    .filter(Boolean)
-    .join(', ') || 'Location not specified';
-
-  card.innerHTML = `
-    ${logoHTML}
-    <div class="org-content">
-      <div class="org-header">
-        <h3 class="org-name">${org.name}</h3>
-        <p class="org-location">📍 ${location}</p>
-      </div>
-
-      ${org.mission_statement ? `<p class="org-mission">${truncateText(org.mission_statement, 350)}</p>` : ''}
-
-      <div class="org-tags">
-        ${topicTags}
-        ${categoryTags}
-      </div>
-
-      <div class="org-links">
-        <a href="${org.website_url}" target="_blank" rel="noopener" class="org-link">
-          Visit Website →
-        </a>
-        ${org.cypres_page_url ? `<a href="${org.cypres_page_url}" target="_blank" rel="noopener" class="org-link">
-          Cy Pres Page →
-        </a>` : ''}
-      </div>
-    </div>
-  `;
-
-  return card;
-}
-
-// Event Handlers
-function handleSearch() {
-  state.filters.search = elements.searchInput.value.trim();
-  state.currentPage = 1;
-  loadOrganizations();
-}
-
-function handleApplyFilters() {
-  state.filters.legalTopics = Array.from(elements.legalTopicFilter.selectedOptions)
-    .map(opt => opt.value)
-    .filter(val => val !== '');
-
-  state.filters.categories = Array.from(elements.categoryFilter.selectedOptions)
-    .map(opt => opt.value)
-    .filter(val => val !== '');
-
-  state.filters.state = elements.stateFilter.value;
-
-  state.currentPage = 1;
-  loadOrganizations();
-}
-
-function handleClearFilters() {
-  elements.searchInput.value = '';
-  elements.legalTopicFilter.selectedIndex = 0;
-  elements.categoryFilter.selectedIndex = 0;
-  elements.stateFilter.selectedIndex = 0;
-
-  state.filters = {
-    search: '',
-    legalTopics: [],
-    categories: [],
-    state: ''
-  };
-
-  state.currentPage = 1;
-  loadOrganizations();
-}
-
-function changePage(delta) {
-  const newPage = state.currentPage + delta;
-  const maxPage = Math.ceil(state.totalResults / state.pageSize);
-
-  if (newPage >= 1 && newPage <= maxPage) {
-    state.currentPage = newPage;
-    loadOrganizations();
-    window.scrollTo({ top: 0, behavior: 'smooth' });
-  }
-}
-
-function updatePagination() {
-  const maxPage = Math.ceil(state.totalResults / state.pageSize);
-
-  elements.prevPageBtn.disabled = state.currentPage === 1;
-  elements.nextPageBtn.disabled = state.currentPage >= maxPage || maxPage === 0;
-  elements.pageInfo.textContent = `Page ${state.currentPage} of ${maxPage || 1}`;
-}
-
-function updateResultsCount() {
-  const start = (state.currentPage - 1) * state.pageSize + 1;
-  const end = Math.min(state.currentPage * state.pageSize, state.totalResults);
-
-  if (state.totalResults === 0) {
-    elements.resultsCount.textContent = 'No results';
-  } else {
-    elements.resultsCount.textContent = `Showing ${start}-${end} of ${state.totalResults.toLocaleString()} results`;
-  }
-}
-
-// Utility Functions
-function showLoading(show) {
-  elements.loadingSpinner.classList.toggle('hidden', !show);
-  elements.organizationsList.classList.toggle('hidden', show);
-}
-
-function showError(message) {
-  elements.errorMessage.textContent = message;
-  elements.errorMessage.classList.remove('hidden');
-}
-
-function hideError() {
-  elements.errorMessage.classList.add('hidden');
-}
-
-function truncateText(text, maxLength) {
-  if (!text) return '';
-  if (text.length <= maxLength) return text;
-  return text.substring(0, maxLength) + '...';
-}
-
-function parseArray(pgArray) {
-  if (!pgArray) return [];
-  if (Array.isArray(pgArray)) return pgArray;
-
-  // PostgreSQL returns arrays as strings like "{item1,item2,item3}"
-  if (typeof pgArray === 'string') {
-    return pgArray
-      .replace(/^\{|\}$/g, '')
-      .split(',')
-      .filter(Boolean);
-  }
-
-  return [];
-}
-
-// Start the application
-document.addEventListener('DOMContentLoaded', init);
diff --git a/frontend/src/cases.html b/frontend/src/cases.html
index 517b787..94b2ec4 100644
--- a/frontend/src/cases.html
+++ b/frontend/src/cases.html
@@ -254,7 +254,7 @@
                     <span class="recipient-tag">Public Interest Research Group (PIRG) - Rejected</span>
                 </div>
                 <div class="case-links">
-                    <a href="https://www.insideclassactions.com/2024/01/04/federal-court-rejects-class-action-settlement-over-cy-pres-provision/" target="_blank" class="case-link">Read Opinion →</a>
+                    <a href="https://www.insideclassactions.com/2024/01/04/federal-court-rejects-class-action-settlement-over-cy-pres-provision/" target="_blank" rel="noopener noreferrer" class="case-link">Read Opinion →</a>
                 </div>
             </div>
 
@@ -285,8 +285,8 @@
                     <span class="recipient-tag">Nonprofit organization providing student loan borrower counseling (to be established)</span>
                 </div>
                 <div class="case-links">
-                    <a href="https://www.courtlistener.com/docket/7974080/hyland-v-navient-corporation/" target="_blank" class="case-link">View Docket →</a>
-                    <a href="https://law.justia.com/cases/federal/appellate-courts/ca2/20-3765/20-3765-2022-09-07.html" target="_blank" class="case-link">Read Opinion →</a>
+                    <a href="https://www.courtlistener.com/docket/7974080/hyland-v-navient-corporation/" target="_blank" rel="noopener noreferrer" class="case-link">View Docket →</a>
+                    <a href="https://law.justia.com/cases/federal/appellate-courts/ca2/20-3765/20-3765-2022-09-07.html" target="_blank" rel="noopener noreferrer" class="case-link">Read Opinion →</a>
                 </div>
             </div>
 
@@ -321,7 +321,7 @@
                     <span class="recipient-tag">National Center for Youth Law</span>
                 </div>
                 <div class="case-links">
-                    <a href="https://www.phoenixclassaction.com/washington-esa/" target="_blank" class="case-link">Read Opinion →</a>
+                    <a href="https://www.phoenixclassaction.com/washington-esa/" target="_blank" rel="noopener noreferrer" class="case-link">Read Opinion →</a>
                 </div>
             </div>
 
@@ -353,7 +353,7 @@
                     <span class="recipient-tag">Public Counsel</span>
                 </div>
                 <div class="case-links">
-                    <a href="http://wifibluetoothgrayoutsettlement.com/" target="_blank" class="case-link">View Docket →</a>
+                    <a href="http://wifibluetoothgrayoutsettlement.com/" target="_blank" rel="noopener noreferrer" class="case-link">View Docket →</a>
                 </div>
             </div>
 
diff --git a/frontend/src/index.html.backup b/frontend/src/index.html.backup
deleted file mode 100644
index 05682ab..0000000
--- a/frontend/src/index.html.backup
+++ /dev/null
@@ -1,129 +0,0 @@
-<!DOCTYPE html>
-<html lang="en">
-<head>
-    <meta charset="UTF-8">
-    <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <title>CyPresAwards - Non-Profit Cy Pres Directory</title>
-    <meta name="description" content="Comprehensive database of US non-profit organizations with cy pres statements for legal settlement distribution.">
-    <link rel="stylesheet" href="styles.css">
-</head>
-<body>
-    <header>
-        <div class="container">
-            <div class="header-content">
-                <div class="header-text">
-                    <h1>CyPresAwards</h1>
-                    <p class="tagline">Directory of Non-Profits with Cy Pres Statements</p>
-                </div>
-                <nav class="header-nav">
-                    <a href="index.html" class="nav-link active">Search Organizations</a>
-                    <a href="submit.html" class="nav-link">Add My Org</a>
-                    <a href="cases.html" class="nav-link">Cy Pres Cases</a>
-                    <a href="settlement-firms.html" class="nav-link">Settlement Firms</a>
-                </nav>
-            </div>
-        </div>
-    </header>
-
-    <main class="container">
-        <!-- Search and Filter Section -->
-        <section class="search-section">
-            <div class="search-bar">
-                <input
-                    type="text"
-                    id="searchInput"
-                    placeholder="Search by organization name or mission..."
-                    aria-label="Search organizations">
-                <button id="searchBtn" class="btn btn-primary">Search</button>
-            </div>
-
-            <div class="filters">
-                <div class="filter-group">
-                    <label for="legalTopicFilter">Legal Topic:</label>
-                    <select id="legalTopicFilter" multiple size="5">
-                        <option value="">All Topics</option>
-                    </select>
-                </div>
-
-                <div class="filter-group">
-                    <label for="categoryFilter">Category:</label>
-                    <select id="categoryFilter" multiple size="5">
-                        <option value="">All Categories</option>
-                    </select>
-                </div>
-
-                <div class="filter-group">
-                    <label for="stateFilter">State:</label>
-                    <select id="stateFilter">
-                        <option value="">All States</option>
-                        <option value="CA">California</option>
-                        <option value="NY">New York</option>
-                        <option value="TX">Texas</option>
-                        <option value="FL">Florida</option>
-                        <option value="IL">Illinois</option>
-                        <!-- Add more states as needed -->
-                    </select>
-                </div>
-            </div>
-
-            <div class="filter-actions">
-                <button id="applyFilters" class="btn btn-primary">Apply Filters</button>
-                <button id="clearFilters" class="btn btn-secondary">Clear Filters</button>
-            </div>
-        </section>
-
-        <!-- Statistics Section -->
-        <section class="stats-section">
-            <div class="stat-card">
-                <h3 id="totalOrgs">0</h3>
-                <p>Total Organizations</p>
-            </div>
-            <div class="stat-card">
-                <h3 id="totalCategories">0</h3>
-                <p>Categories</p>
-            </div>
-            <div class="stat-card">
-                <h3 id="totalTopics">0</h3>
-                <p>Legal Topics</p>
-            </div>
-        </section>
-
-        <!-- Results Section -->
-        <section class="results-section">
-            <div class="results-header">
-                <div>
-                    <h2>🏛️ Organizations</h2>
-                    <p class="results-subtitle">Nonprofits accepting cy pres awards</p>
-                </div>
-                <span id="resultsCount" class="results-count">Showing 0 results</span>
-            </div>
-
-            <div id="loadingSpinner" class="loading hidden">
-                <div class="spinner"></div>
-                <p>Loading organizations...</p>
-            </div>
-
-            <div id="errorMessage" class="error-message hidden"></div>
-
-            <div id="organizationsList" class="organizations-list">
-                <!-- Organizations will be dynamically inserted here -->
-            </div>
-
-            <div class="pagination">
-                <button id="prevPage" class="btn btn-secondary" disabled>Previous</button>
-                <span id="pageInfo">Page 1</span>
-                <button id="nextPage" class="btn btn-secondary">Next</button>
-            </div>
-        </section>
-    </main>
-
-    <footer>
-        <div class="container">
-            <p>&copy; 2024 CyPresAwards. A comprehensive database for legal professionals to identify suitable non-profits for cy pres awards.</p>
-            <p class="disclaimer">This database is provided for informational purposes. Please verify all information directly with organizations.</p>
-        </div>
-    </footer>
-
-    <script src="app.js"></script>
-</body>
-</html>
diff --git a/frontend/src/news.html b/frontend/src/news.html
index 0367181..094e34f 100644
--- a/frontend/src/news.html
+++ b/frontend/src/news.html
@@ -181,7 +181,7 @@
             <div class="news-card">
                 <div class="news-header">
                     <h3 class="news-title">
-                        <a href="https://www.insideclassactions.com/2024/01/04/federal-court-rejects-class-action-settlement-over-cy-pres-provision/" target="_blank">
+                        <a href="https://www.insideclassactions.com/2024/01/04/federal-court-rejects-class-action-settlement-over-cy-pres-provision/" target="_blank" rel="noopener noreferrer">
                             Court Rejects Cy Pres Settlement in Hawes v. Macy's
                         </a>
                     </h3>
@@ -193,7 +193,7 @@
                 <p class="news-description">
                     U.S. District Court (S.D. Ohio) rejected a $10.5M settlement because proposed recipient PIRG was not sufficiently related to false advertising claims in bed sheets case. This ruling underscores the importance of selecting cy pres recipients that have a clear nexus to the underlying claims.
                 </p>
-                <a href="https://www.insideclassactions.com/2024/01/04/federal-court-rejects-class-action-settlement-over-cy-pres-provision/" target="_blank" class="news-link">
+                <a href="https://www.insideclassactions.com/2024/01/04/federal-court-rejects-class-action-settlement-over-cy-pres-provision/" target="_blank" rel="noopener noreferrer" class="news-link">
                     Read Full Article →
                 </a>
             </div>
@@ -202,7 +202,7 @@
             <div class="news-card">
                 <div class="news-header">
                     <h3 class="news-title">
-                        <a href="https://www.scotusblog.com/case-files/cases/hyland-v-navient-corporation/" target="_blank">
+                        <a href="https://www.scotusblog.com/case-files/cases/hyland-v-navient-corporation/" target="_blank" rel="noopener noreferrer">
                             Supreme Court Denies Review of Cy Pres-Only Settlement in Hyland v. Navient
                         </a>
                     </h3>
@@ -214,7 +214,7 @@
                 <p class="news-description">
                     SCOTUS declined to hear challenge to Second Circuit's approval of $2.25M cy pres-only settlement with no distribution to class members. The decision allows the Second Circuit's approval to stand, representing a significant development in cy pres jurisprudence.
                 </p>
-                <a href="https://www.scotusblog.com/case-files/cases/hyland-v-navient-corporation/" target="_blank" class="news-link">
+                <a href="https://www.scotusblog.com/case-files/cases/hyland-v-navient-corporation/" target="_blank" rel="noopener noreferrer" class="news-link">
                     Read Full Article →
                 </a>
             </div>
@@ -223,7 +223,7 @@
             <div class="news-card">
                 <div class="news-header">
                     <h3 class="news-title">
-                        <a href="https://law.justia.com/cases/federal/appellate-courts/ca2/20-3765/20-3765-2022-09-07.html" target="_blank">
+                        <a href="https://law.justia.com/cases/federal/appellate-courts/ca2/20-3765/20-3765-2022-09-07.html" target="_blank" rel="noopener noreferrer">
                             Second Circuit Approves Cy Pres-Only Settlement in Student Loan Case
                         </a>
                     </h3>
@@ -235,7 +235,7 @@
                 <p class="news-description">
                     Court approved settlement establishing nonprofit for student loan counseling despite no direct payments to class members in Hyland v. Navient. This precedent-setting decision addresses the validity of cy pres-only settlements where direct distribution is impractical.
                 </p>
-                <a href="https://law.justia.com/cases/federal/appellate-courts/ca2/20-3765/20-3765-2022-09-07.html" target="_blank" class="news-link">
+                <a href="https://law.justia.com/cases/federal/appellate-courts/ca2/20-3765/20-3765-2022-09-07.html" target="_blank" rel="noopener noreferrer" class="news-link">
                     Read Full Article →
                 </a>
             </div>
diff --git a/frontend/src/outreach.html b/frontend/src/outreach.html
index 3fb18d5..7c68fc1 100644
--- a/frontend/src/outreach.html
+++ b/frontend/src/outreach.html
@@ -204,7 +204,7 @@
 
             orgList.innerHTML = orgs.map(org => `
                 <div class="org-item">
-                    <a href="${org.url}" target="_blank" class="org-url">${org.url}</a>
+                    <a href="${org.url}" target="_blank" rel="noopener noreferrer" class="org-url">${org.url}</a>
                     <span class="org-date">${new Date(org.scraped_at).toLocaleDateString()}</span>
                 </div>
             `).join('');
diff --git a/frontend/src/simple.html b/frontend/src/simple.html
index c2f83c7..5b11f0e 100644
--- a/frontend/src/simple.html
+++ b/frontend/src/simple.html
@@ -52,8 +52,8 @@
                     <div class="org-card">
                         <h3 class="org-name">${org.name}</h3>
                         <p><strong>Location:</strong> ${org.location_city || ''} ${org.location_state || ''} ${org.location_country || ''}</p>
-                        <p><strong>Website:</strong> <a href="${org.website_url}" target="_blank">${org.website_url}</a></p>
-                        ${org.cypres_page_url ? `<p><strong>Cy Pres:</strong> <a href="${org.cypres_page_url}" target="_blank">${org.cypres_page_url}</a></p>` : ''}
+                        <p><strong>Website:</strong> <a href="${org.website_url}" target="_blank" rel="noopener noreferrer">${org.website_url}</a></p>
+                        ${org.cypres_page_url ? `<p><strong>Cy Pres:</strong> <a href="${org.cypres_page_url}" target="_blank" rel="noopener noreferrer">${org.cypres_page_url}</a></p>` : ''}
                         ${org.mission_statement ? `<p>${org.mission_statement.substring(0, 200)}...</p>` : ''}
                     </div>
                 `).join('');

← 7a2e112 initial snapshot — gitify all builds (CLAUDE.md rule 2026-05  ·  back to Cypress Awards  ·  (newest)