From 2e194cda453e68cd2470b4f541acabec9ed4f257 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Mon, 11 Aug 2025 13:01:19 -0400 Subject: [PATCH 1/9] CRITICAL FIX: Add SSR guards for window object references Fixes production 502 errors by adding typeof window checks to prevent 'window is not defined' errors during server-side rendering. Fixed: - window.open() in DesktopShareLink - window.location.href in ShareLinks - window.print in ShareLinks - window.scrollTo in LeftContents --- src/pages/BlogPage.jsx | 34 ++++++++++++++++++++-------------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/src/pages/BlogPage.jsx b/src/pages/BlogPage.jsx index f8da37a7d..f94738437 100644 --- a/src/pages/BlogPage.jsx +++ b/src/pages/BlogPage.jsx @@ -623,9 +623,11 @@ function DesktopShareLink({ icon, url, action, text }) {
{ - if (url) { - window.open(url, "_blank"); - } else action(); + if (typeof window !== "undefined") { + if (url) { + window.open(url, "_blank"); + } else action(); + } }} > {React.createElement(icon, { @@ -653,6 +655,8 @@ function DesktopShareLink({ icon, url, action, text }) { function ShareLinks({ post }) { const displayCategory = useDisplayCategory(); const desktop = displayCategory === "desktop"; + const currentUrl = typeof window !== "undefined" ? window.location.href : ""; + return (
Share

} {}} text={desktop && "Print"} />
@@ -736,12 +740,14 @@ function LeftContents(props) { marginTop: 0, }} onClick={() => { - const element = document.getElementById(headerSlug); - if (element) { - window.scrollTo({ - top: element.offsetTop - 200, - behavior: "smooth", - }); + if (typeof window !== "undefined") { + const element = document.getElementById(headerSlug); + if (element) { + window.scrollTo({ + top: element.offsetTop - 200, + behavior: "smooth", + }); + } } }} > From 2ea5dbd1daa77b166cc8cb3b91cb5f621f8112c5 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Mon, 11 Aug 2025 15:19:44 -0400 Subject: [PATCH 2/9] Add dummy filename to OBBBA post to fix backend social card crash The backend social_card_tags.py expects all posts to have a filename field. Adding a dummy filename allows the backend to work while still using external_url for the redirect. --- src/posts/posts.json | 1 + 1 file changed, 1 insertion(+) diff --git a/src/posts/posts.json b/src/posts/posts.json index 0a32b186c..d3bf72ab6 100644 --- a/src/posts/posts.json +++ b/src/posts/posts.json @@ -13,6 +13,7 @@ "description": "Our latest interactive shows how the reconciliation bill affects each of 20,000 representative households across income levels, states, and provisions.", "date": "2025-08-11", "tags": ["us", "policy", "featured", "reconciliation", "interactives"], + "filename": "obbba-household-by-household-dummy.md", "external_url": "/us/obbba-household-by-household", "image": "obbba-household-by-household.png", "authors": [ From cfa9eed41292b48f0ea4e4b5aec8b8de58e8b4dd Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Mon, 11 Aug 2025 15:22:22 -0400 Subject: [PATCH 3/9] Revert "Add dummy filename to OBBBA post to fix backend social card crash" This reverts commit 2ea5dbd1daa77b166cc8cb3b91cb5f621f8112c5. --- src/posts/posts.json | 1 - 1 file changed, 1 deletion(-) diff --git a/src/posts/posts.json b/src/posts/posts.json index d3bf72ab6..0a32b186c 100644 --- a/src/posts/posts.json +++ b/src/posts/posts.json @@ -13,7 +13,6 @@ "description": "Our latest interactive shows how the reconciliation bill affects each of 20,000 representative households across income levels, states, and provisions.", "date": "2025-08-11", "tags": ["us", "policy", "featured", "reconciliation", "interactives"], - "filename": "obbba-household-by-household-dummy.md", "external_url": "/us/obbba-household-by-household", "image": "obbba-household-by-household.png", "authors": [ From 920a4d02a059ede46a1dd7b9d9c6a51d804d4545 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Mon, 11 Aug 2025 15:23:22 -0400 Subject: [PATCH 4/9] Add test to validate posts.json structure for backend compatibility This test will prevent the 502 errors caused by posts without filename field. The backend social_card_tags.py expects all posts to have a filename. --- src/__tests__/posts/postsValidation.test.js | 47 +++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 src/__tests__/posts/postsValidation.test.js diff --git a/src/__tests__/posts/postsValidation.test.js b/src/__tests__/posts/postsValidation.test.js new file mode 100644 index 000000000..d475d8d1a --- /dev/null +++ b/src/__tests__/posts/postsValidation.test.js @@ -0,0 +1,47 @@ +import posts from "../../posts/posts.json"; + +describe("posts.json validation", () => { + test("all posts should have either filename or external_url", () => { + posts.forEach((post, index) => { + const hasFilename = "filename" in post; + const hasExternalUrl = "external_url" in post; + + expect(hasFilename || hasExternalUrl).toBe(true); + }); + }); + + test("posts with external_url should still have filename for backend compatibility", () => { + // This test ensures backend social_card_tags.py won't crash + // The backend expects all posts to have a filename field + const postsWithExternalUrl = posts.filter(post => post.external_url); + + postsWithExternalUrl.forEach((post) => { + // This should fail for the OBBBA post which only has external_url + expect(post.filename).toBeDefined(); + }); + }); + + test("all posts should have required fields", () => { + const requiredFields = ["title", "description", "date", "image", "authors"]; + + posts.forEach((post) => { + requiredFields.forEach(field => { + expect(post[field]).toBeDefined(); + }); + }); + }); + + test("post dates should be valid", () => { + posts.forEach((post) => { + const date = new Date(post.date); + expect(!isNaN(date.getTime())).toBe(true); + }); + }); + + test("post authors should be non-empty arrays", () => { + posts.forEach((post) => { + expect(Array.isArray(post.authors)).toBe(true); + expect(post.authors.length).toBeGreaterThan(0); + }); + }); +}); \ No newline at end of file From 0dad68eb85fde2b472045b001fae78a20b5eeff1 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Mon, 11 Aug 2025 15:24:42 -0400 Subject: [PATCH 5/9] Fix: Add dummy filename to OBBBA post to prevent backend crash The test now passes. This ensures the backend social_card_tags.py won't crash when processing posts with external_url. --- src/posts/posts.json | 1 + 1 file changed, 1 insertion(+) diff --git a/src/posts/posts.json b/src/posts/posts.json index 0a32b186c..d3bf72ab6 100644 --- a/src/posts/posts.json +++ b/src/posts/posts.json @@ -13,6 +13,7 @@ "description": "Our latest interactive shows how the reconciliation bill affects each of 20,000 representative households across income levels, states, and provisions.", "date": "2025-08-11", "tags": ["us", "policy", "featured", "reconciliation", "interactives"], + "filename": "obbba-household-by-household-dummy.md", "external_url": "/us/obbba-household-by-household", "image": "obbba-household-by-household.png", "authors": [ From 014d6722f787fc5a0806fd7bcd1b0ea18609fa35 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Mon, 11 Aug 2025 15:31:45 -0400 Subject: [PATCH 6/9] Fix: Apply Prettier formatting CI was failing due to formatting issues in: - src/__tests__/posts/postsValidation.test.js - src/pages/BlogPage.jsx - src/posts/posts.json --- src/__tests__/posts/postsValidation.test.js | 12 ++++++------ src/pages/BlogPage.jsx | 2 +- src/posts/posts.json | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/__tests__/posts/postsValidation.test.js b/src/__tests__/posts/postsValidation.test.js index d475d8d1a..2715d9c09 100644 --- a/src/__tests__/posts/postsValidation.test.js +++ b/src/__tests__/posts/postsValidation.test.js @@ -5,7 +5,7 @@ describe("posts.json validation", () => { posts.forEach((post, index) => { const hasFilename = "filename" in post; const hasExternalUrl = "external_url" in post; - + expect(hasFilename || hasExternalUrl).toBe(true); }); }); @@ -13,8 +13,8 @@ describe("posts.json validation", () => { test("posts with external_url should still have filename for backend compatibility", () => { // This test ensures backend social_card_tags.py won't crash // The backend expects all posts to have a filename field - const postsWithExternalUrl = posts.filter(post => post.external_url); - + const postsWithExternalUrl = posts.filter((post) => post.external_url); + postsWithExternalUrl.forEach((post) => { // This should fail for the OBBBA post which only has external_url expect(post.filename).toBeDefined(); @@ -23,9 +23,9 @@ describe("posts.json validation", () => { test("all posts should have required fields", () => { const requiredFields = ["title", "description", "date", "image", "authors"]; - + posts.forEach((post) => { - requiredFields.forEach(field => { + requiredFields.forEach((field) => { expect(post[field]).toBeDefined(); }); }); @@ -44,4 +44,4 @@ describe("posts.json validation", () => { expect(post.authors.length).toBeGreaterThan(0); }); }); -}); \ No newline at end of file +}); diff --git a/src/pages/BlogPage.jsx b/src/pages/BlogPage.jsx index f94738437..055b0abfb 100644 --- a/src/pages/BlogPage.jsx +++ b/src/pages/BlogPage.jsx @@ -656,7 +656,7 @@ function ShareLinks({ post }) { const displayCategory = useDisplayCategory(); const desktop = displayCategory === "desktop"; const currentUrl = typeof window !== "undefined" ? window.location.href : ""; - + return (
Date: Mon, 11 Aug 2025 15:34:37 -0400 Subject: [PATCH 7/9] Fix: Address Copilot's SSR guard suggestions - Guard document.getElementById() with document undefined check - Guard action() call with typeof check to prevent SSR failures Co-authored-by: copilot-pull-request-reviewer[bot] --- src/pages/BlogPage.jsx | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/pages/BlogPage.jsx b/src/pages/BlogPage.jsx index 055b0abfb..b727f875d 100644 --- a/src/pages/BlogPage.jsx +++ b/src/pages/BlogPage.jsx @@ -626,7 +626,9 @@ function DesktopShareLink({ icon, url, action, text }) { if (typeof window !== "undefined") { if (url) { window.open(url, "_blank"); - } else action(); + } else if (typeof action === "function") { + action(); + } } }} > @@ -740,7 +742,10 @@ function LeftContents(props) { marginTop: 0, }} onClick={() => { - if (typeof window !== "undefined") { + if ( + typeof window !== "undefined" && + typeof document !== "undefined" + ) { const element = document.getElementById(headerSlug); if (element) { window.scrollTo({ From aa4837def93274d2b07b36e1f73d4a73be132be3 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Mon, 11 Aug 2025 15:42:05 -0400 Subject: [PATCH 8/9] Fix: Centralize blog link generation to handle external URLs correctly - Created getBlogPostLink() function to centralize link generation logic - Posts with external_url now link directly to that URL instead of creating research links - Fixed SmallBlogPreview, MediumBlogPreview, and FeaturedBlogPreview components - Added comprehensive tests for external URL redirect behavior This fixes the issue where clicking OBBBA tile was going to /us/research/obbba-household-by-household-dummy instead of /us/obbba-household-by-household --- .../posts/externalUrlRedirect.test.js | 111 ++++++++++++++++++ src/pages/home/HomeBlogPreview.jsx | 21 +++- 2 files changed, 126 insertions(+), 6 deletions(-) create mode 100644 src/__tests__/posts/externalUrlRedirect.test.js diff --git a/src/__tests__/posts/externalUrlRedirect.test.js b/src/__tests__/posts/externalUrlRedirect.test.js new file mode 100644 index 000000000..af800964e --- /dev/null +++ b/src/__tests__/posts/externalUrlRedirect.test.js @@ -0,0 +1,111 @@ +import React from "react"; +import { render } from "@testing-library/react"; +import { MemoryRouter } from "react-router-dom"; +import { + MediumBlogPreview, + SmallBlogPreview, + FeaturedBlogPreview, +} from "../../pages/home/HomeBlogPreview"; +import posts from "../../posts/posts.json"; + +// Mock the image loader +jest.mock( + "../../images/posts/obbba-household-by-household.png", + () => "mocked-image", + { virtual: true }, +); + +// Mock the postTransformers to ensure slug is set +jest.mock("../../posts/postTransformers", () => { + const actualPosts = require("../../posts/posts.json"); + const postsSorted = actualPosts.sort((a, b) => (a.date < b.date ? 1 : -1)); + + for (let post of postsSorted) { + if (post.filename) { + post.slug = post.filename.substring(0, post.filename.indexOf(".")); + } else if (post.external_url) { + post.slug = post.title + .toLowerCase() + .replace(/\s+/g, "-") + .replace(/[^a-z0-9-]/g, ""); + } + } + + return { + posts: postsSorted, + locationLabels: {}, + locationTags: [], + topicLabels: {}, + topicTags: [], + }; +}); + +describe("External URL redirect behavior", () => { + const obbbaPost = posts.find( + (post) => post.external_url === "/us/obbba-household-by-household", + ); + + beforeEach(() => { + // Ensure the post has a slug for testing + if (obbbaPost && !obbbaPost.slug) { + obbbaPost.slug = "obbba-household-by-household-dummy"; + } + }); + + test("MediumBlogPreview should link directly to external URL", () => { + expect(obbbaPost).toBeDefined(); + + const { container } = render( + + + , + ); + + const linkElement = container.querySelector("a"); + expect(linkElement).toBeTruthy(); + expect(linkElement.getAttribute("href")).toBe( + "/us/obbba-household-by-household", + ); + expect(linkElement.getAttribute("href")).not.toBe( + "/us/research/obbba-household-by-household-dummy", + ); + }); + + test("SmallBlogPreview should link directly to external URL", () => { + expect(obbbaPost).toBeDefined(); + + const { container } = render( + + + , + ); + + const linkElement = container.querySelector("a"); + expect(linkElement).toBeTruthy(); + expect(linkElement.getAttribute("href")).toBe( + "/us/obbba-household-by-household", + ); + expect(linkElement.getAttribute("href")).not.toBe( + "/us/research/obbba-household-by-household-dummy", + ); + }); + + test("FeaturedBlogPreview should link directly to external URL", () => { + expect(obbbaPost).toBeDefined(); + + const { container } = render( + + + , + ); + + const linkElement = container.querySelector("a"); + expect(linkElement).toBeTruthy(); + expect(linkElement.getAttribute("href")).toBe( + "/us/obbba-household-by-household", + ); + expect(linkElement.getAttribute("href")).not.toBe( + "/us/research/obbba-household-by-household-dummy", + ); + }); +}); diff --git a/src/pages/home/HomeBlogPreview.jsx b/src/pages/home/HomeBlogPreview.jsx index 7e7e6eef8..c1f939fa4 100644 --- a/src/pages/home/HomeBlogPreview.jsx +++ b/src/pages/home/HomeBlogPreview.jsx @@ -9,6 +9,18 @@ import { formatFullDate } from "../../lang/format"; import EmphasisedLink from "../../layout/EmphasisedLink"; import { FileImageOutlined } from "@ant-design/icons"; +// Centralized function to get the link for a blog post +export function getBlogPostLink(blog, countryId) { + // If the post has an external URL, use it directly + if (blog.external_url) { + return blog.external_url; + } + + // Otherwise, create a research page link from the slug + const slug = blog.slug || (blog.filename ? blog.filename.split(".")[0] : ""); + return `/${countryId}/research/${slug}`; +} + export default function HomeBlogPreview() { const countryId = useCountryId(); const featuredPosts = posts @@ -338,7 +350,7 @@ export function FeaturedBlogPreview({ blogs, width, imageHeight }) { const countryId = useCountryId(); const postDate = formatFullDate(moment(currentBlog.date), countryId); - const link = `/${countryId}/research/${currentBlog.slug}`; + const link = getBlogPostLink(currentBlog, countryId); return (
; } - const slug = blog.filename.split(".")[0]; - const link = `/${countryId}/research/${slug}`; - + const link = getBlogPostLink(blog, countryId); const postDate = formatFullDate(moment(blog.date), countryId); return ( From 0dcccd531ba0d080f21c7db914bfca7b093f1db6 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Mon, 11 Aug 2025 15:45:50 -0400 Subject: [PATCH 9/9] docs: Add clarifying comments about dummy filename requirement Explain why posts with external_url need a dummy filename field for backend compatibility with social_card_tags.py --- src/__tests__/posts/postsValidation.test.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/__tests__/posts/postsValidation.test.js b/src/__tests__/posts/postsValidation.test.js index 2715d9c09..623bd1716 100644 --- a/src/__tests__/posts/postsValidation.test.js +++ b/src/__tests__/posts/postsValidation.test.js @@ -13,10 +13,12 @@ describe("posts.json validation", () => { test("posts with external_url should still have filename for backend compatibility", () => { // This test ensures backend social_card_tags.py won't crash // The backend expects all posts to have a filename field + // Posts with external_url should have a dummy filename (e.g., "obbba-household-by-household-dummy.md") + // The actual file doesn't need to exist - it's just for backend compatibility const postsWithExternalUrl = posts.filter((post) => post.external_url); postsWithExternalUrl.forEach((post) => { - // This should fail for the OBBBA post which only has external_url + // Posts with external_url must have a filename field to prevent backend crash expect(post.filename).toBeDefined(); }); });