From ad265e2af7776b81118d6da3f972fd8be3209e94 Mon Sep 17 00:00:00 2001 From: emscape Date: Wed, 4 Nov 2026 15:32:35 -0800 Subject: [PATCH 1/2] fix: hide the I/O Puzzle 2025 project from project listings WHY: - The project's live link pointed at emilysueanderson.com, a domain that now belongs to someone else, so the card linked to an unrelated site. - Its images and the sitemap's /projects/io-puzzle-2025/ URL point at a folder that does not exist. EXPECTED: - prepareProjects() drops projects with "hidden": true before sorting and limiting, so the home page still fills all three slots. - io-puzzle-2025 is marked hidden; its data is kept for restoring. - The dead /projects/io-puzzle-2025/ sitemap entry is removed. - tests/project-loader.test.js passes: hidden projects omitted on both pages, visible projects kept, and no displayed project links to emilysueanderson.com. --- js/project-loader.js | 7 ++-- projects/projects-data.json | 1 + sitemap.xml | 11 ------ tests/project-loader.test.js | 67 ++++++++++++++++++++++++++++++++++++ 4 files changed, 72 insertions(+), 14 deletions(-) create mode 100644 tests/project-loader.test.js diff --git a/js/project-loader.js b/js/project-loader.js index 7d41c88..9160089 100644 --- a/js/project-loader.js +++ b/js/project-loader.js @@ -55,15 +55,16 @@ const limitProjectsForPage = (projects, pathname) => { }; /** - * Prepares projects for display (sort + limit) + * Prepares projects for display (drop hidden + sort + limit) * Pure function - * + * * @param {Array} projects - Raw projects array * @param {string} pathname - Window location pathname * @returns {Array} - Prepared projects */ const prepareProjects = (projects, pathname) => { - const sorted = sortProjects(projects); + const visible = projects.filter(project => project.hidden !== true); + const sorted = sortProjects(visible); return limitProjectsForPage(sorted, pathname); }; diff --git a/projects/projects-data.json b/projects/projects-data.json index 47942dc..8b0aa13 100644 --- a/projects/projects-data.json +++ b/projects/projects-data.json @@ -61,6 +61,7 @@ "liveUrl": "https://www.emilysueanderson.com/io-puzzle-2025-solutions", "blogPostUrl": "/blog/solving-io-puzzles-2025/", "featured": false, + "hidden": true, "completionDate": "2025-03" }, { diff --git a/sitemap.xml b/sitemap.xml index 808c4eb..99c02ec 100644 --- a/sitemap.xml +++ b/sitemap.xml @@ -256,17 +256,6 @@ - - https://helloemily.dev/projects/io-puzzle-2025/ - 2026-01-13 - never - 0.8 - - https://helloemily.dev/projects/io-puzzle-2025/io-puzzle-1.jpeg - I/O Puzzle 2025 Solutions - - - https://helloemily.dev/projects/sean-cameron-website/ 2026-01-13 diff --git a/tests/project-loader.test.js b/tests/project-loader.test.js new file mode 100644 index 0000000..0809f55 --- /dev/null +++ b/tests/project-loader.test.js @@ -0,0 +1,67 @@ +/** + * Tests for the project loader's display preparation + */ + +import { describe, it, expect } from 'vitest'; +import { readFileSync } from 'fs'; +import { prepareProjects } from '../js/project-loader.js'; + +const project = (id, overrides = {}) => ({ + id, + featured: false, + completionDate: '2025-01', + ...overrides, +}); + +describe('Project Loader', () => { + describe('prepareProjects', () => { + it('omits hidden projects on the projects page', () => { + const projects = [ + project('shown-a', { completionDate: '2025-03' }), + project('hidden', { hidden: true, featured: true }), + project('shown-b', { completionDate: '2025-01' }), + ]; + + const ids = prepareProjects(projects, '/pages/projects.html').map((p) => p.id); + + expect(ids).toEqual(['shown-a', 'shown-b']); + }); + + it('fills all three home page slots from visible projects when one is hidden', () => { + const projects = [ + project('hidden', { hidden: true, featured: true, completionDate: '2025-06' }), + project('p1', { completionDate: '2025-05' }), + project('p2', { completionDate: '2025-04' }), + project('p3', { completionDate: '2025-03' }), + project('p4', { completionDate: '2025-02' }), + ]; + + const ids = prepareProjects(projects, '/').map((p) => p.id); + + expect(ids).toEqual(['p1', 'p2', 'p3']); + }); + + it('keeps projects with hidden set to false or absent', () => { + const projects = [ + project('explicit', { hidden: false, completionDate: '2025-02' }), + project('absent', { completionDate: '2025-01' }), + ]; + + const ids = prepareProjects(projects, '/pages/projects.html').map((p) => p.id); + + expect(ids).toEqual(['explicit', 'absent']); + }); + }); + + describe('projects-data.json', () => { + const { projects } = JSON.parse(readFileSync('projects/projects-data.json', 'utf8')); + + it('does not display any project linking to emilysueanderson.com', () => { + const displayed = prepareProjects(projects, '/pages/projects.html'); + + const offending = displayed.filter((p) => /emilysueanderson\.com/i.test(JSON.stringify(p))); + + expect(offending.map((p) => p.id)).toEqual([]); + }); + }); +}); From b0094433c1c9b79cb43027d85681717638036584 Mon Sep 17 00:00:00 2001 From: emscape Date: Wed, 4 Nov 2026 18:26:27 -0800 Subject: [PATCH 2/2] fix: skip hidden projects in the search indexing script WHY: - getProjectUrls() mapped every projects-data.json entry to a URL, so --projects/--all still submitted the hidden io-puzzle-2025 page to the Google Indexing API. - Loading the script ran main(), which submits URLs, so it could not be tested without calling the API. EXPECTED: - projectUrls() drops entries with "hidden": true, matching js/project-loader.js. - main() runs only when the script is executed directly. - tests/index-all-content.test.js passes: hidden projects are skipped, visible ones kept, and io-puzzle-2025 is not submitted. - `--projects --dry-run` lists 3 URLs, none for io-puzzle-2025. --- scripts/index-all-content.js | 29 +++++++++++++++++++++-------- tests/index-all-content.test.js | 31 +++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 8 deletions(-) create mode 100644 tests/index-all-content.test.js diff --git a/scripts/index-all-content.js b/scripts/index-all-content.js index 96fa9dd..3a0e32c 100644 --- a/scripts/index-all-content.js +++ b/scripts/index-all-content.js @@ -63,6 +63,17 @@ function getBlogPostUrls() { } } +/** + * Map project entries to their URLs, skipping hidden ones (they are not + * listed on the site, matching js/project-loader.js) + */ +function projectUrls(projects) { + return projects.filter(project => project.hidden !== true).map(project => { + // Projects use 'id' not 'slug' + return `${SITE_BASE_URL}/projects/${project.id}/`; + }); +} + /** * Get all project URLs */ @@ -74,10 +85,7 @@ function getProjectUrls() { } const projectsData = JSON.parse(fs.readFileSync(PATHS.projectsData, 'utf8')); - return (projectsData.projects || []).map(project => { - // Projects use 'id' not 'slug' - return `${SITE_BASE_URL}/projects/${project.id}/`; - }); + return projectUrls(projectsData.projects || []); } catch (error) { console.error('Error reading projects data:', error.message); return []; @@ -251,7 +259,12 @@ async function main() { } } -main().catch(error => { - console.error('Fatal error:', error.message); - process.exit(1); -}); +module.exports = { projectUrls }; + +// Only submit when run directly, not when require()'d from a test. +if (require.main === module) { + main().catch(error => { + console.error('Fatal error:', error.message); + process.exit(1); + }); +} diff --git a/tests/index-all-content.test.js b/tests/index-all-content.test.js new file mode 100644 index 0000000..52d299f --- /dev/null +++ b/tests/index-all-content.test.js @@ -0,0 +1,31 @@ +/** + * Tests for the search indexing script's project URLs + */ + +import { describe, it, expect } from 'vitest'; +import { createRequire } from 'module'; +import { readFileSync } from 'fs'; + +const require = createRequire(import.meta.url); +const { projectUrls } = require('../scripts/index-all-content.js'); + +describe('index-all-content projectUrls', () => { + it('maps visible projects to their project URLs and skips hidden ones', () => { + const projects = [ + { id: 'shown' }, + { id: 'gone', hidden: true }, + { id: 'also-shown', hidden: false }, + ]; + + expect(projectUrls(projects)).toEqual([ + 'https://helloemily.dev/projects/shown/', + 'https://helloemily.dev/projects/also-shown/', + ]); + }); + + it('does not submit the hidden io-puzzle-2025 project from projects-data.json', () => { + const { projects } = JSON.parse(readFileSync('projects/projects-data.json', 'utf8')); + + expect(projectUrls(projects)).not.toContain('https://helloemily.dev/projects/io-puzzle-2025/'); + }); +});