Skip to content
This repository was archived by the owner on Jul 6, 2026. It is now read-only.
Merged
111 changes: 111 additions & 0 deletions src/__tests__/posts/externalUrlRedirect.test.js
Original file line number Diff line number Diff line change
@@ -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(
<MemoryRouter>
<MediumBlogPreview blog={obbbaPost} minHeight={300} />
</MemoryRouter>,
);

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(
<MemoryRouter>
<SmallBlogPreview blog={obbbaPost} />
</MemoryRouter>,
);

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(
<MemoryRouter>
<FeaturedBlogPreview blogs={obbbaPost} width="100%" imageHeight={200} />
</MemoryRouter>,
);

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",
);
});
});
49 changes: 49 additions & 0 deletions src/__tests__/posts/postsValidation.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
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
// 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) => {
// Posts with external_url must have a filename field to prevent backend crash
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);
});
});
});
39 changes: 25 additions & 14 deletions src/pages/BlogPage.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -623,9 +623,13 @@ function DesktopShareLink({ icon, url, action, text }) {
<div
style={{ display: "flex", alignItems: "center", cursor: "pointer" }}
onClick={() => {
if (url) {
window.open(url, "_blank");
} else action();
if (typeof window !== "undefined") {
if (url) {
window.open(url, "_blank");
} else if (typeof action === "function") {
action();
}
}
}}
>
{React.createElement(icon, {
Expand Down Expand Up @@ -653,6 +657,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 (
<div
style={{
Expand All @@ -665,27 +671,27 @@ function ShareLinks({ post }) {
{desktop && <p className="spaced-sans-serif">Share</p>}
<DesktopShareLink
icon={TwitterOutlined}
url={`https://twitter.com/intent/tweet?text=${post.title}&url=${window.location.href}`}
url={`https://twitter.com/intent/tweet?text=${post.title}&url=${currentUrl}`}
text={desktop && "Twitter"}
/>
<DesktopShareLink
icon={FacebookOutlined}
url={`https://www.facebook.com/sharer/sharer.php?u=${window.location.href}`}
url={`https://www.facebook.com/sharer/sharer.php?u=${currentUrl}`}
text={desktop && "Facebook"}
/>
<DesktopShareLink
icon={LinkedinOutlined}
url={`https://www.linkedin.com/shareArticle?mini=true&url=${window.location.href}&title=${post.title}&summary=${post.description}`}
url={`https://www.linkedin.com/shareArticle?mini=true&url=${currentUrl}&title=${post.title}&summary=${post.description}`}
text={desktop && "LinkedIn"}
/>
<DesktopShareLink
icon={MailOutlined}
url={`mailto:?subject=${post.title}&body=${window.location.href}`}
url={`mailto:?subject=${post.title}&body=${currentUrl}`}
text={desktop && "Email"}
/>
<DesktopShareLink
icon={PrinterOutlined}
action={window.print}
action={typeof window !== "undefined" ? window.print : () => {}}
text={desktop && "Print"}
/>
</div>
Expand Down Expand Up @@ -736,12 +742,17 @@ 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" &&
typeof document !== "undefined"
) {
const element = document.getElementById(headerSlug);
if (element) {
window.scrollTo({
top: element.offsetTop - 200,
behavior: "smooth",
});
}
}
}}
>
Expand Down
21 changes: 15 additions & 6 deletions src/pages/home/HomeBlogPreview.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 (
<div
style={{
Expand Down Expand Up @@ -403,8 +415,7 @@ export function FeaturedBlogPreview({ blogs, width, imageHeight }) {
export function MediumBlogPreview({ blog, minHeight }) {
const displayCategory = useDisplayCategory();
const countryId = useCountryId();
const slug = blog.filename ? blog.filename.split(".")[0] : blog.slug;
const link = blog.external_url || `/${countryId}/research/${slug}`;
const link = getBlogPostLink(blog, countryId);

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

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

const slug = blog.filename.split(".")[0];
const link = `/${countryId}/research/${slug}`;

const link = getBlogPostLink(blog, countryId);
const postDate = formatFullDate(moment(blog.date), countryId);

return (
Expand Down
3 changes: 2 additions & 1 deletion src/posts/posts.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,12 @@
"image": "enhanced-cps-launch.png",
"authors": ["max-ghenis", "nikhil-woodruff", "ben-ogorek", "maria-juaristi"]
},
{
{
"title": "The One Big Beautiful Bill Act, household by household",
"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": [
Expand Down
Loading