Skip to content

Commit baab093

Browse files
authored
Merge pull request #2726 from PolicyEngine/hotfix/ssr-window-errors
Fix 502 errors: Add posts.json validation test and dummy filename for OBBBA
2 parents 9929d49 + 0dcccd5 commit baab093

5 files changed

Lines changed: 202 additions & 21 deletions

File tree

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import React from "react";
2+
import { render } from "@testing-library/react";
3+
import { MemoryRouter } from "react-router-dom";
4+
import {
5+
MediumBlogPreview,
6+
SmallBlogPreview,
7+
FeaturedBlogPreview,
8+
} from "../../pages/home/HomeBlogPreview";
9+
import posts from "../../posts/posts.json";
10+
11+
// Mock the image loader
12+
jest.mock(
13+
"../../images/posts/obbba-household-by-household.png",
14+
() => "mocked-image",
15+
{ virtual: true },
16+
);
17+
18+
// Mock the postTransformers to ensure slug is set
19+
jest.mock("../../posts/postTransformers", () => {
20+
const actualPosts = require("../../posts/posts.json");
21+
const postsSorted = actualPosts.sort((a, b) => (a.date < b.date ? 1 : -1));
22+
23+
for (let post of postsSorted) {
24+
if (post.filename) {
25+
post.slug = post.filename.substring(0, post.filename.indexOf("."));
26+
} else if (post.external_url) {
27+
post.slug = post.title
28+
.toLowerCase()
29+
.replace(/\s+/g, "-")
30+
.replace(/[^a-z0-9-]/g, "");
31+
}
32+
}
33+
34+
return {
35+
posts: postsSorted,
36+
locationLabels: {},
37+
locationTags: [],
38+
topicLabels: {},
39+
topicTags: [],
40+
};
41+
});
42+
43+
describe("External URL redirect behavior", () => {
44+
const obbbaPost = posts.find(
45+
(post) => post.external_url === "/us/obbba-household-by-household",
46+
);
47+
48+
beforeEach(() => {
49+
// Ensure the post has a slug for testing
50+
if (obbbaPost && !obbbaPost.slug) {
51+
obbbaPost.slug = "obbba-household-by-household-dummy";
52+
}
53+
});
54+
55+
test("MediumBlogPreview should link directly to external URL", () => {
56+
expect(obbbaPost).toBeDefined();
57+
58+
const { container } = render(
59+
<MemoryRouter>
60+
<MediumBlogPreview blog={obbbaPost} minHeight={300} />
61+
</MemoryRouter>,
62+
);
63+
64+
const linkElement = container.querySelector("a");
65+
expect(linkElement).toBeTruthy();
66+
expect(linkElement.getAttribute("href")).toBe(
67+
"/us/obbba-household-by-household",
68+
);
69+
expect(linkElement.getAttribute("href")).not.toBe(
70+
"/us/research/obbba-household-by-household-dummy",
71+
);
72+
});
73+
74+
test("SmallBlogPreview should link directly to external URL", () => {
75+
expect(obbbaPost).toBeDefined();
76+
77+
const { container } = render(
78+
<MemoryRouter>
79+
<SmallBlogPreview blog={obbbaPost} />
80+
</MemoryRouter>,
81+
);
82+
83+
const linkElement = container.querySelector("a");
84+
expect(linkElement).toBeTruthy();
85+
expect(linkElement.getAttribute("href")).toBe(
86+
"/us/obbba-household-by-household",
87+
);
88+
expect(linkElement.getAttribute("href")).not.toBe(
89+
"/us/research/obbba-household-by-household-dummy",
90+
);
91+
});
92+
93+
test("FeaturedBlogPreview should link directly to external URL", () => {
94+
expect(obbbaPost).toBeDefined();
95+
96+
const { container } = render(
97+
<MemoryRouter>
98+
<FeaturedBlogPreview blogs={obbbaPost} width="100%" imageHeight={200} />
99+
</MemoryRouter>,
100+
);
101+
102+
const linkElement = container.querySelector("a");
103+
expect(linkElement).toBeTruthy();
104+
expect(linkElement.getAttribute("href")).toBe(
105+
"/us/obbba-household-by-household",
106+
);
107+
expect(linkElement.getAttribute("href")).not.toBe(
108+
"/us/research/obbba-household-by-household-dummy",
109+
);
110+
});
111+
});
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import posts from "../../posts/posts.json";
2+
3+
describe("posts.json validation", () => {
4+
test("all posts should have either filename or external_url", () => {
5+
posts.forEach((post, index) => {
6+
const hasFilename = "filename" in post;
7+
const hasExternalUrl = "external_url" in post;
8+
9+
expect(hasFilename || hasExternalUrl).toBe(true);
10+
});
11+
});
12+
13+
test("posts with external_url should still have filename for backend compatibility", () => {
14+
// This test ensures backend social_card_tags.py won't crash
15+
// The backend expects all posts to have a filename field
16+
// Posts with external_url should have a dummy filename (e.g., "obbba-household-by-household-dummy.md")
17+
// The actual file doesn't need to exist - it's just for backend compatibility
18+
const postsWithExternalUrl = posts.filter((post) => post.external_url);
19+
20+
postsWithExternalUrl.forEach((post) => {
21+
// Posts with external_url must have a filename field to prevent backend crash
22+
expect(post.filename).toBeDefined();
23+
});
24+
});
25+
26+
test("all posts should have required fields", () => {
27+
const requiredFields = ["title", "description", "date", "image", "authors"];
28+
29+
posts.forEach((post) => {
30+
requiredFields.forEach((field) => {
31+
expect(post[field]).toBeDefined();
32+
});
33+
});
34+
});
35+
36+
test("post dates should be valid", () => {
37+
posts.forEach((post) => {
38+
const date = new Date(post.date);
39+
expect(!isNaN(date.getTime())).toBe(true);
40+
});
41+
});
42+
43+
test("post authors should be non-empty arrays", () => {
44+
posts.forEach((post) => {
45+
expect(Array.isArray(post.authors)).toBe(true);
46+
expect(post.authors.length).toBeGreaterThan(0);
47+
});
48+
});
49+
});

src/pages/BlogPage.jsx

Lines changed: 25 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -623,9 +623,13 @@ function DesktopShareLink({ icon, url, action, text }) {
623623
<div
624624
style={{ display: "flex", alignItems: "center", cursor: "pointer" }}
625625
onClick={() => {
626-
if (url) {
627-
window.open(url, "_blank");
628-
} else action();
626+
if (typeof window !== "undefined") {
627+
if (url) {
628+
window.open(url, "_blank");
629+
} else if (typeof action === "function") {
630+
action();
631+
}
632+
}
629633
}}
630634
>
631635
{React.createElement(icon, {
@@ -653,6 +657,8 @@ function DesktopShareLink({ icon, url, action, text }) {
653657
function ShareLinks({ post }) {
654658
const displayCategory = useDisplayCategory();
655659
const desktop = displayCategory === "desktop";
660+
const currentUrl = typeof window !== "undefined" ? window.location.href : "";
661+
656662
return (
657663
<div
658664
style={{
@@ -665,27 +671,27 @@ function ShareLinks({ post }) {
665671
{desktop && <p className="spaced-sans-serif">Share</p>}
666672
<DesktopShareLink
667673
icon={TwitterOutlined}
668-
url={`https://twitter.com/intent/tweet?text=${post.title}&url=${window.location.href}`}
674+
url={`https://twitter.com/intent/tweet?text=${post.title}&url=${currentUrl}`}
669675
text={desktop && "Twitter"}
670676
/>
671677
<DesktopShareLink
672678
icon={FacebookOutlined}
673-
url={`https://www.facebook.com/sharer/sharer.php?u=${window.location.href}`}
679+
url={`https://www.facebook.com/sharer/sharer.php?u=${currentUrl}`}
674680
text={desktop && "Facebook"}
675681
/>
676682
<DesktopShareLink
677683
icon={LinkedinOutlined}
678-
url={`https://www.linkedin.com/shareArticle?mini=true&url=${window.location.href}&title=${post.title}&summary=${post.description}`}
684+
url={`https://www.linkedin.com/shareArticle?mini=true&url=${currentUrl}&title=${post.title}&summary=${post.description}`}
679685
text={desktop && "LinkedIn"}
680686
/>
681687
<DesktopShareLink
682688
icon={MailOutlined}
683-
url={`mailto:?subject=${post.title}&body=${window.location.href}`}
689+
url={`mailto:?subject=${post.title}&body=${currentUrl}`}
684690
text={desktop && "Email"}
685691
/>
686692
<DesktopShareLink
687693
icon={PrinterOutlined}
688-
action={window.print}
694+
action={typeof window !== "undefined" ? window.print : () => {}}
689695
text={desktop && "Print"}
690696
/>
691697
</div>
@@ -736,12 +742,17 @@ function LeftContents(props) {
736742
marginTop: 0,
737743
}}
738744
onClick={() => {
739-
const element = document.getElementById(headerSlug);
740-
if (element) {
741-
window.scrollTo({
742-
top: element.offsetTop - 200,
743-
behavior: "smooth",
744-
});
745+
if (
746+
typeof window !== "undefined" &&
747+
typeof document !== "undefined"
748+
) {
749+
const element = document.getElementById(headerSlug);
750+
if (element) {
751+
window.scrollTo({
752+
top: element.offsetTop - 200,
753+
behavior: "smooth",
754+
});
755+
}
745756
}
746757
}}
747758
>

src/pages/home/HomeBlogPreview.jsx

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,18 @@ import { formatFullDate } from "../../lang/format";
99
import EmphasisedLink from "../../layout/EmphasisedLink";
1010
import { FileImageOutlined } from "@ant-design/icons";
1111

12+
// Centralized function to get the link for a blog post
13+
export function getBlogPostLink(blog, countryId) {
14+
// If the post has an external URL, use it directly
15+
if (blog.external_url) {
16+
return blog.external_url;
17+
}
18+
19+
// Otherwise, create a research page link from the slug
20+
const slug = blog.slug || (blog.filename ? blog.filename.split(".")[0] : "");
21+
return `/${countryId}/research/${slug}`;
22+
}
23+
1224
export default function HomeBlogPreview() {
1325
const countryId = useCountryId();
1426
const featuredPosts = posts
@@ -338,7 +350,7 @@ export function FeaturedBlogPreview({ blogs, width, imageHeight }) {
338350
const countryId = useCountryId();
339351
const postDate = formatFullDate(moment(currentBlog.date), countryId);
340352

341-
const link = `/${countryId}/research/${currentBlog.slug}`;
353+
const link = getBlogPostLink(currentBlog, countryId);
342354
return (
343355
<div
344356
style={{
@@ -403,8 +415,7 @@ export function FeaturedBlogPreview({ blogs, width, imageHeight }) {
403415
export function MediumBlogPreview({ blog, minHeight }) {
404416
const displayCategory = useDisplayCategory();
405417
const countryId = useCountryId();
406-
const slug = blog.filename ? blog.filename.split(".")[0] : blog.slug;
407-
const link = blog.external_url || `/${countryId}/research/${slug}`;
418+
const link = getBlogPostLink(blog, countryId);
408419

409420
const imageUrl = blog.image ? handleImageLoad(blog.image) : "";
410421

@@ -529,9 +540,7 @@ export function SmallBlogPreview({ blog }) {
529540
left = <SideTags tags={blog.tags} />;
530541
}
531542

532-
const slug = blog.filename.split(".")[0];
533-
const link = `/${countryId}/research/${slug}`;
534-
543+
const link = getBlogPostLink(blog, countryId);
535544
const postDate = formatFullDate(moment(blog.date), countryId);
536545

537546
return (

src/posts/posts.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,12 @@
88
"image": "enhanced-cps-launch.png",
99
"authors": ["max-ghenis", "nikhil-woodruff", "ben-ogorek", "maria-juaristi"]
1010
},
11-
{
11+
{
1212
"title": "The One Big Beautiful Bill Act, household by household",
1313
"description": "Our latest interactive shows how the reconciliation bill affects each of 20,000 representative households across income levels, states, and provisions.",
1414
"date": "2025-08-11",
1515
"tags": ["us", "policy", "featured", "reconciliation", "interactives"],
16+
"filename": "obbba-household-by-household-dummy.md",
1617
"external_url": "/us/obbba-household-by-household",
1718
"image": "obbba-household-by-household.png",
1819
"authors": [

0 commit comments

Comments
 (0)