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/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/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/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/'); + }); +}); 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([]); + }); + }); +});