From bc7b198e112978fea554e087bdcf07832d482122 Mon Sep 17 00:00:00 2001
From: "google-labs-jules[bot]"
<161369871+google-labs-jules[bot]@users.noreply.github.com>
Date: Wed, 28 Oct 2026 22:22:06 +0000
Subject: [PATCH 1/3] refactor(app): replace react-helmet-async with React 19
native metadata
---
app/package.json | 1 -
.../components/RouteErrorBoundary.test.tsx | 3 --
app/src/pages/AboutPage.tsx | 5 ++--
app/src/pages/AgentChatPage.feedback.test.tsx | 3 --
app/src/pages/AgentChatPage.language.test.tsx | 3 --
app/src/pages/AgentChatPage.save.test.tsx | 3 --
app/src/pages/AgentChatPage.test.tsx | 3 --
app/src/pages/AgentChatPage.tsx | 5 ++--
app/src/pages/LandingPage.test.tsx | 5 ----
app/src/pages/LandingPage.tsx | 5 ++--
app/src/pages/LegalPage.test.tsx | 3 --
app/src/pages/LegalPage.tsx | 6 ++--
app/src/pages/LibrariesPage.test.tsx | 4 ---
app/src/pages/LibrariesPage.tsx | 5 ++--
app/src/pages/MapPage.test.tsx | 3 --
app/src/pages/MapPage.tsx | 5 ++--
app/src/pages/McpPage.test.tsx | 3 --
app/src/pages/McpPage.tsx | 6 ++--
app/src/pages/NotFoundPage.test.tsx | 4 ---
app/src/pages/NotFoundPage.tsx | 5 ++--
app/src/pages/PalettePage.tsx | 6 ++--
app/src/pages/PlotsPage.test.tsx | 9 ++----
app/src/pages/PlotsPage.tsx | 5 ++--
app/src/pages/SpecPage.test.tsx | 3 --
app/src/pages/SpecPage.tsx | 5 ++--
app/src/pages/SpecsListPage.test.tsx | 3 --
app/src/pages/SpecsListPage.tsx | 5 ++--
app/src/pages/StatsPage.test.tsx | 3 --
app/src/pages/StatsPage.tsx | 5 ++--
app/src/routes/index.tsx | 5 ++--
app/yarn.lock | 28 +------------------
changelog.d/native-document-metadata.md | 6 ++++
32 files changed, 37 insertions(+), 126 deletions(-)
create mode 100644 changelog.d/native-document-metadata.md
diff --git a/app/package.json b/app/package.json
index 9e3aa4b1831..d8ee9374bb2 100644
--- a/app/package.json
+++ b/app/package.json
@@ -31,7 +31,6 @@
"react": "^19.3.1",
"react-dom": "^19.3.1",
"react-force-graph-2d": "^1.29.2",
- "react-helmet-async": "^3.0.0",
"react-router-dom": "^7.18.4",
"react-syntax-highlighter": "^16.1.2",
"web-vitals": "^6.2.3"
diff --git a/app/src/components/RouteErrorBoundary.test.tsx b/app/src/components/RouteErrorBoundary.test.tsx
index 39ae0b684fe..67a19902c02 100644
--- a/app/src/components/RouteErrorBoundary.test.tsx
+++ b/app/src/components/RouteErrorBoundary.test.tsx
@@ -7,9 +7,6 @@ import { createTheme, ThemeProvider } from '@mui/material/styles';
import { RouteErrorBoundary } from 'src/components/RouteErrorBoundary';
-vi.mock('react-helmet-async', () => ({
- Helmet: ({ children }: { children: React.ReactNode }) => <>{children}>,
-}));
const theme = createTheme();
diff --git a/app/src/pages/AboutPage.tsx b/app/src/pages/AboutPage.tsx
index 641f82902ae..a9e5217c7aa 100644
--- a/app/src/pages/AboutPage.tsx
+++ b/app/src/pages/AboutPage.tsx
@@ -1,6 +1,5 @@
import { useEffect } from 'react';
-import { Helmet } from 'react-helmet-async';
import { Link as RouterLink } from 'react-router-dom';
import Box from '@mui/material/Box';
@@ -50,7 +49,7 @@ export function AboutPage() {
return (
<>
-
+
about | anyplot.ai
-
+
{/* About (lede) */}
diff --git a/app/src/pages/AgentChatPage.feedback.test.tsx b/app/src/pages/AgentChatPage.feedback.test.tsx
index 2498136ada3..114c48a669f 100644
--- a/app/src/pages/AgentChatPage.feedback.test.tsx
+++ b/app/src/pages/AgentChatPage.feedback.test.tsx
@@ -24,9 +24,6 @@ vi.mock('src/global-config', async importOriginal => {
};
});
-vi.mock('react-helmet-async', () => ({
- Helmet: ({ children }: { children: React.ReactNode }) => <>{children}>,
-}));
const analytics = vi.hoisted(() => ({ trackEvent: vi.fn(), trackPageview: vi.fn() }));
vi.mock('src/hooks/useAnalytics', () => ({ useAnalytics: () => analytics }));
diff --git a/app/src/pages/AgentChatPage.language.test.tsx b/app/src/pages/AgentChatPage.language.test.tsx
index 16baa9a7924..0e8bdccbdfb 100644
--- a/app/src/pages/AgentChatPage.language.test.tsx
+++ b/app/src/pages/AgentChatPage.language.test.tsx
@@ -24,9 +24,6 @@ vi.mock('src/global-config', async importOriginal => {
};
});
-vi.mock('react-helmet-async', () => ({
- Helmet: ({ children }: { children: React.ReactNode }) => <>{children}>,
-}));
const analytics = vi.hoisted(() => ({ trackEvent: vi.fn(), trackPageview: vi.fn() }));
vi.mock('src/hooks/useAnalytics', () => ({ useAnalytics: () => analytics }));
diff --git a/app/src/pages/AgentChatPage.save.test.tsx b/app/src/pages/AgentChatPage.save.test.tsx
index fb2260e5810..c88c016020b 100644
--- a/app/src/pages/AgentChatPage.save.test.tsx
+++ b/app/src/pages/AgentChatPage.save.test.tsx
@@ -23,9 +23,6 @@ vi.mock('src/global-config', async importOriginal => {
};
});
-vi.mock('react-helmet-async', () => ({
- Helmet: ({ children }: { children: React.ReactNode }) => <>{children}>,
-}));
const analytics = vi.hoisted(() => ({ trackEvent: vi.fn(), trackPageview: vi.fn() }));
vi.mock('src/hooks/useAnalytics', () => ({ useAnalytics: () => analytics }));
diff --git a/app/src/pages/AgentChatPage.test.tsx b/app/src/pages/AgentChatPage.test.tsx
index 4c51530c7d3..af8101b7eff 100644
--- a/app/src/pages/AgentChatPage.test.tsx
+++ b/app/src/pages/AgentChatPage.test.tsx
@@ -28,9 +28,6 @@ vi.mock('src/global-config', async importOriginal => {
};
});
-vi.mock('react-helmet-async', () => ({
- Helmet: ({ children }: { children: React.ReactNode }) => <>{children}>,
-}));
// Stable identities, like the real hook (effects depend on them).
const analytics = vi.hoisted(() => ({ trackEvent: vi.fn(), trackPageview: vi.fn() }));
diff --git a/app/src/pages/AgentChatPage.tsx b/app/src/pages/AgentChatPage.tsx
index 1fdb9745129..b1af239d5b7 100644
--- a/app/src/pages/AgentChatPage.tsx
+++ b/app/src/pages/AgentChatPage.tsx
@@ -51,7 +51,6 @@
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
-import { Helmet } from 'react-helmet-async';
import { Link as RouterLink, useSearchParams } from 'react-router-dom';
import Box from '@mui/material/Box';
@@ -525,10 +524,10 @@ function AgentChat({ specId, library, language, token, onLibraryChange }: AgentC
return (
<>
-
+
{`agent · ${specId} | anyplot.ai`}
-
+
{/* `colorScheme` gives the native controls (scrollbars, select menus) the site theme. */}
({
NumbersStrip: () => ,
}));
-vi.mock('react-helmet-async', () => ({
- Helmet: ({ children }: { children: React.ReactNode }) => (
- {children}
- ),
-}));
import { LandingPage } from 'src/pages/LandingPage';
diff --git a/app/src/pages/LandingPage.tsx b/app/src/pages/LandingPage.tsx
index 6d2053fa38d..48f2c4be859 100644
--- a/app/src/pages/LandingPage.tsx
+++ b/app/src/pages/LandingPage.tsx
@@ -1,6 +1,5 @@
import { useEffect, useState } from 'react';
-import { Helmet } from 'react-helmet-async';
import { Link as RouterLink, useNavigate } from 'react-router-dom';
import Box from '@mui/material/Box';
@@ -37,14 +36,14 @@ export function LandingPage() {
return (
<>
-
+
any.plot() — any library.
-
+
{/* Hero — fills the viewport so the fold shows only the hero */}
({
- Helmet: ({ children }: { children: React.ReactNode }) => <>{children}>,
-}));
const trackEvent = vi.fn();
vi.mock('src/hooks', () => ({
diff --git a/app/src/pages/LegalPage.tsx b/app/src/pages/LegalPage.tsx
index 4db795b06ba..2493168b009 100644
--- a/app/src/pages/LegalPage.tsx
+++ b/app/src/pages/LegalPage.tsx
@@ -1,7 +1,5 @@
import { useEffect } from 'react';
-import { Helmet } from 'react-helmet-async';
-
import Box from '@mui/material/Box';
import Link from '@mui/material/Link';
import Table from '@mui/material/Table';
@@ -136,7 +134,7 @@ export function LegalPage() {
return (
<>
-
+
legal | anyplot.ai
-
+
{/* Legal Notice */}
diff --git a/app/src/pages/LibrariesPage.test.tsx b/app/src/pages/LibrariesPage.test.tsx
index 2dee9179855..423e98ab8af 100644
--- a/app/src/pages/LibrariesPage.test.tsx
+++ b/app/src/pages/LibrariesPage.test.tsx
@@ -1,4 +1,3 @@
-import type { ReactNode } from 'react';
import { beforeEach, describe, expect, it, vi } from 'vitest';
@@ -24,9 +23,6 @@ vi.mock('src/hooks', async () => {
};
});
-vi.mock('react-helmet-async', () => ({
- Helmet: ({ children }: { children: ReactNode }) => {children}
,
-}));
vi.mock('react-router-dom', async () => {
const actual = await vi.importActual('react-router-dom');
diff --git a/app/src/pages/LibrariesPage.tsx b/app/src/pages/LibrariesPage.tsx
index ce6e7784d04..1c18f1629a5 100644
--- a/app/src/pages/LibrariesPage.tsx
+++ b/app/src/pages/LibrariesPage.tsx
@@ -1,6 +1,5 @@
import { useEffect, useState } from 'react';
-import { Helmet } from 'react-helmet-async';
import { useNavigate } from 'react-router-dom';
import Box from '@mui/material/Box';
@@ -71,7 +70,7 @@ export function LibrariesPage() {
return (
<>
-
+
libraries | anyplot.ai
-
+
libraries} />
diff --git a/app/src/pages/MapPage.test.tsx b/app/src/pages/MapPage.test.tsx
index 4dd4a40ead1..3271b4835ed 100644
--- a/app/src/pages/MapPage.test.tsx
+++ b/app/src/pages/MapPage.test.tsx
@@ -6,9 +6,6 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { MapPage } from 'src/pages/MapPage';
import { act, render, screen, waitFor } from 'src/test-utils';
-vi.mock('react-helmet-async', () => ({
- Helmet: ({ children }: { children: React.ReactNode }) => <>{children}>,
-}));
const mockNavigate = vi.fn();
const mockTrackEvent = vi.fn();
diff --git a/app/src/pages/MapPage.tsx b/app/src/pages/MapPage.tsx
index c9b3e103cca..7b01c7156aa 100644
--- a/app/src/pages/MapPage.tsx
+++ b/app/src/pages/MapPage.tsx
@@ -1,7 +1,6 @@
import { useEffect, useMemo, useRef, useState } from 'react';
import ForceGraph2D from 'react-force-graph-2d';
-import { Helmet } from 'react-helmet-async';
import { Link, useNavigate } from 'react-router-dom';
import Box from '@mui/material/Box';
@@ -344,13 +343,13 @@ export function MapPage() {
return (
<>
-
+
map() — anyplot
-
+
({
- Helmet: ({ children }: { children: React.ReactNode }) => <>{children}>,
-}));
vi.mock('src/hooks', () => ({
useAnalytics: () => ({
diff --git a/app/src/pages/McpPage.tsx b/app/src/pages/McpPage.tsx
index 27fd3a329b5..42e82505b39 100644
--- a/app/src/pages/McpPage.tsx
+++ b/app/src/pages/McpPage.tsx
@@ -1,7 +1,5 @@
import { useEffect } from 'react';
-import { Helmet } from 'react-helmet-async';
-
import Box from '@mui/material/Box';
import Link from '@mui/material/Link';
import Table from '@mui/material/Table';
@@ -44,7 +42,7 @@ export function McpPage() {
return (
<>
-
+
mcp | anyplot.ai
-
+
{/* What is MCP */}
diff --git a/app/src/pages/NotFoundPage.test.tsx b/app/src/pages/NotFoundPage.test.tsx
index cb610c41f63..8a1a579b5b4 100644
--- a/app/src/pages/NotFoundPage.test.tsx
+++ b/app/src/pages/NotFoundPage.test.tsx
@@ -5,10 +5,6 @@ import { render, screen } from 'src/test-utils';
const mockTrackEvent = vi.fn();
-// Mock react-helmet-async to avoid provider requirement
-vi.mock('react-helmet-async', () => ({
- Helmet: ({ children }: { children: React.ReactNode }) => <>{children}>,
-}));
vi.mock('src/hooks/useAnalytics', () => ({
useAnalytics: () => ({
diff --git a/app/src/pages/NotFoundPage.tsx b/app/src/pages/NotFoundPage.tsx
index 05a7c46cd91..a2c86e58fe5 100644
--- a/app/src/pages/NotFoundPage.tsx
+++ b/app/src/pages/NotFoundPage.tsx
@@ -1,6 +1,5 @@
import { useEffect } from 'react';
-import { Helmet } from 'react-helmet-async';
import { Link } from 'react-router-dom';
import Box from '@mui/material/Box';
@@ -38,10 +37,10 @@ export function NotFoundPage({ source = 'catch_all' }: NotFoundPageProps = {}) {
return (
<>
-
+
page not found | anyplot.ai
-
+
-
+
imprint palette | anyplot.ai
-
+
{/* 1. HERO — short lede + a compact hue–chroma wheel. Click the wheel to
diff --git a/app/src/pages/PlotsPage.test.tsx b/app/src/pages/PlotsPage.test.tsx
index e44745e6cac..3f8aa8dc3f8 100644
--- a/app/src/pages/PlotsPage.test.tsx
+++ b/app/src/pages/PlotsPage.test.tsx
@@ -47,11 +47,6 @@ vi.mock('src/hooks', () => ({
useTheme: () => ({ isDark: false, toggle: vi.fn() }),
}));
-vi.mock('react-helmet-async', () => ({
- Helmet: ({ children }: { children: React.ReactNode }) => (
- {children}
- ),
-}));
vi.mock('src/sections/plots-gallery/FilterBar', () => ({
FilterBar: () => FilterBar
,
@@ -132,9 +127,9 @@ describe('PlotsPage', () => {
expect(screen.getByTestId('images-grid')).toBeInTheDocument();
});
- it('renders Helmet for SEO', () => {
+ it('renders document metadata for SEO', () => {
render();
- expect(screen.getByTestId('helmet')).toBeInTheDocument();
+ expect(document.title).toBe('plots | anyplot.ai');
});
it('renders the shared scroll-to-top button', () => {
diff --git a/app/src/pages/PlotsPage.tsx b/app/src/pages/PlotsPage.tsx
index 5eda93471eb..662c0d102be 100644
--- a/app/src/pages/PlotsPage.tsx
+++ b/app/src/pages/PlotsPage.tsx
@@ -1,6 +1,5 @@
import { useCallback, useEffect, useRef, useState } from 'react';
-import { Helmet } from 'react-helmet-async';
import { Link as RouterLink, useNavigate, useSearchParams } from 'react-router-dom';
import Alert from '@mui/material/Alert';
@@ -178,14 +177,14 @@ export function PlotsPage() {
return (
-
+
plots | anyplot.ai
-
+
{error && (
diff --git a/app/src/pages/SpecPage.test.tsx b/app/src/pages/SpecPage.test.tsx
index 5f64676f871..ea448907c3d 100644
--- a/app/src/pages/SpecPage.test.tsx
+++ b/app/src/pages/SpecPage.test.tsx
@@ -18,9 +18,6 @@ vi.mock('react-router-dom', async () => {
};
});
-vi.mock('react-helmet-async', () => ({
- Helmet: ({ children }: { children: React.ReactNode }) => <>{children}>,
-}));
// The real useAnalytics returns useCallback-stable functions. Rebuilding them
// per render here would give every consumer a fresh identity, which turns any
diff --git a/app/src/pages/SpecPage.tsx b/app/src/pages/SpecPage.tsx
index 2068fd0afc0..e4cd22b55b5 100644
--- a/app/src/pages/SpecPage.tsx
+++ b/app/src/pages/SpecPage.tsx
@@ -1,6 +1,5 @@
import { lazy, Suspense, useCallback, useEffect, useMemo, useState } from 'react';
-import { Helmet } from 'react-helmet-async';
import { Link, useNavigate, useParams, useSearchParams } from 'react-router-dom';
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
@@ -493,7 +492,7 @@ export function SpecPage() {
return (
<>
-
+
{`${specData.title}${titleSuffix} | anyplot.ai`}
@@ -509,7 +508,7 @@ export function SpecPage() {
{JSON.stringify(softwareSourceJsonLd).replace(/
)}
-
+
{mode === 'hub' && (
diff --git a/app/src/pages/SpecsListPage.test.tsx b/app/src/pages/SpecsListPage.test.tsx
index 7f624ebe17a..b84908eb214 100644
--- a/app/src/pages/SpecsListPage.test.tsx
+++ b/app/src/pages/SpecsListPage.test.tsx
@@ -3,9 +3,6 @@ import { beforeEach, describe, expect, it, vi } from 'vitest';
import { SpecsListPage } from 'src/pages/SpecsListPage';
import { render, screen, waitFor } from 'src/test-utils';
-vi.mock('react-helmet-async', () => ({
- Helmet: ({ children }: { children: React.ReactNode }) => <>{children}>,
-}));
vi.mock('src/hooks', () => ({
useAnalytics: () => ({
diff --git a/app/src/pages/SpecsListPage.tsx b/app/src/pages/SpecsListPage.tsx
index 4b9895ca8b3..d66b2011383 100644
--- a/app/src/pages/SpecsListPage.tsx
+++ b/app/src/pages/SpecsListPage.tsx
@@ -1,6 +1,5 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
-import { Helmet } from 'react-helmet-async';
import { Link } from 'react-router-dom';
import Box from '@mui/material/Box';
@@ -148,7 +147,7 @@ export function SpecsListPage() {
return (
<>
-
+
specs | anyplot.ai
-
+
specs} />
diff --git a/app/src/pages/StatsPage.test.tsx b/app/src/pages/StatsPage.test.tsx
index 274c5d18f6c..52a6e09b09d 100644
--- a/app/src/pages/StatsPage.test.tsx
+++ b/app/src/pages/StatsPage.test.tsx
@@ -12,9 +12,6 @@ const darkThemeValue: ThemeContextValue = {
cycle: vi.fn(),
};
-vi.mock('react-helmet-async', () => ({
- Helmet: ({ children }: { children: React.ReactNode }) => <>{children}>,
-}));
const mockTrackEvent = vi.fn();
diff --git a/app/src/pages/StatsPage.tsx b/app/src/pages/StatsPage.tsx
index 0fc1fd298a0..92a8d49211c 100644
--- a/app/src/pages/StatsPage.tsx
+++ b/app/src/pages/StatsPage.tsx
@@ -1,6 +1,5 @@
import { useEffect, useState } from 'react';
-import { Helmet } from 'react-helmet-async';
import { Link as RouterLink } from 'react-router-dom';
import Box from '@mui/material/Box';
@@ -207,14 +206,14 @@ export function StatsPage() {
return (
<>
-
+
stats | anyplot.ai
-
+
{/* Summary Counters */}
diff --git a/app/src/routes/index.tsx b/app/src/routes/index.tsx
index bfaa9e26141..35fc9dd0fd9 100644
--- a/app/src/routes/index.tsx
+++ b/app/src/routes/index.tsx
@@ -1,4 +1,3 @@
-import { HelmetProvider } from 'react-helmet-async';
import { createBrowserRouter, Navigate, RouterProvider, useParams } from 'react-router-dom';
import Box from '@mui/material/Box';
@@ -119,12 +118,12 @@ const router = createBrowserRouter([
export function AppRouter() {
return (
-
+
-
+
);
}
diff --git a/app/yarn.lock b/app/yarn.lock
index c6a566c0f55..43e3496bbce 100644
--- a/app/yarn.lock
+++ b/app/yarn.lock
@@ -1962,13 +1962,6 @@ index-array-by@1:
resolved "https://registry.yarnpkg.com/internmap/-/internmap-2.0.3.tgz#6685f23755e43c524e251d29cbc97248e3061009"
integrity sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==
-invariant@^2.2.4:
- version "2.2.4"
- resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6"
- integrity sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==
- dependencies:
- loose-envify "^1.0.0"
-
is-alphabetical@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/is-alphabetical/-/is-alphabetical-2.0.1.tgz#01072053ea7c1036df3c7d19a6daaec7f19e789b"
@@ -2205,7 +2198,7 @@ lodash-es@4:
resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.18.1.tgz#b962eeb80d9d983a900bf342961fb7418ca10b1d"
integrity sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==
-loose-envify@^1.0.0, loose-envify@^1.4.0:
+loose-envify@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==
@@ -2496,11 +2489,6 @@ react-dom@^19.3.1:
dependencies:
scheduler "^0.28.0"
-react-fast-compare@^3.2.2:
- version "3.2.2"
- resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-3.2.2.tgz#929a97a532304ce9fee4bcae44234f1ce2c21d49"
- integrity sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==
-
react-force-graph-2d@^1.29.2:
version "1.29.2"
resolved "https://registry.yarnpkg.com/react-force-graph-2d/-/react-force-graph-2d-1.29.2.tgz#d9fba29051682bc8dce0743964be6239b457a6eb"
@@ -2510,15 +2498,6 @@ react-force-graph-2d@^1.29.2:
prop-types "15"
react-kapsule "^2.6"
-react-helmet-async@^3.0.0:
- version "3.0.0"
- resolved "https://registry.yarnpkg.com/react-helmet-async/-/react-helmet-async-3.0.0.tgz#16f31779ea4e4e01827c071b2f15301d074dd570"
- integrity sha512-nA3IEZfXiclgrz4KLxAhqJqIfFDuvzQwlKwpdmzZIuC1KNSghDEIXmyU0TKtbM+NafnkICcwx8CECFrZ/sL/1w==
- dependencies:
- invariant "^2.2.4"
- react-fast-compare "^3.2.2"
- shallowequal "^1.1.0"
-
react-is@^16.13.1, react-is@^16.7.0:
version "16.13.1"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
@@ -2681,11 +2660,6 @@ set-cookie-parser@^2.6.0:
resolved "https://registry.yarnpkg.com/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz#ccd08673a9ae5d2e44ea2a2de25089e67c7edf68"
integrity sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==
-shallowequal@^1.1.0:
- version "1.1.0"
- resolved "https://registry.yarnpkg.com/shallowequal/-/shallowequal-1.1.0.tgz#188d521de95b9087404fd4dcb68b13df0ae4e7f8"
- integrity sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==
-
shebang-command@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea"
diff --git a/changelog.d/native-document-metadata.md b/changelog.d/native-document-metadata.md
new file mode 100644
index 00000000000..646b6c56964
--- /dev/null
+++ b/changelog.d/native-document-metadata.md
@@ -0,0 +1,6 @@
+### Changed
+
+- **Per-route page metadata uses React 19 native hoisting.** Pages render
+ bare ``, `` and `` elements instead of
+ wrapping them in `react-helmet-async`, which is archived upstream; the
+ dependency and its `HelmetProvider` are gone.
From 4f58d8fb9ff88b7e3f0ba0d7caf4013a82a95265 Mon Sep 17 00:00:00 2001
From: "google-labs-jules[bot]"
<161369871+google-labs-jules[bot]@users.noreply.github.com>
Date: Wed, 28 Oct 2026 22:28:58 +0000
Subject: [PATCH 2/3] refactor(app): replace react-helmet-async with React 19
native metadata
---
.../components/RouteErrorBoundary.test.tsx | 1 -
app/src/pages/AboutPage.tsx | 24 ++++++++--------
app/src/pages/AgentChatPage.feedback.test.tsx | 1 -
app/src/pages/AgentChatPage.language.test.tsx | 1 -
app/src/pages/AgentChatPage.save.test.tsx | 1 -
app/src/pages/AgentChatPage.test.tsx | 1 -
app/src/pages/AgentChatPage.tsx | 5 ++--
app/src/pages/LandingPage.test.tsx | 1 -
app/src/pages/LandingPage.tsx | 14 ++++------
app/src/pages/LegalPage.test.tsx | 1 -
app/src/pages/LegalPage.tsx | 24 ++++++++--------
app/src/pages/LibrariesPage.test.tsx | 2 --
app/src/pages/LibrariesPage.tsx | 24 ++++++++--------
app/src/pages/MapPage.test.tsx | 1 -
app/src/pages/MapPage.tsx | 11 ++++----
app/src/pages/McpPage.test.tsx | 1 -
app/src/pages/McpPage.tsx | 24 ++++++++--------
app/src/pages/NotFoundPage.test.tsx | 1 -
app/src/pages/NotFoundPage.tsx | 5 ++--
app/src/pages/PalettePage.tsx | 24 ++++++++--------
app/src/pages/PlotsPage.test.tsx | 1 -
app/src/pages/PlotsPage.tsx | 14 ++++------
app/src/pages/SpecPage.test.tsx | 1 -
app/src/pages/SpecPage.tsx | 28 +++++++++----------
app/src/pages/SpecsListPage.test.tsx | 1 -
app/src/pages/SpecsListPage.tsx | 21 ++++++--------
app/src/pages/StatsPage.test.tsx | 1 -
app/src/pages/StatsPage.tsx | 14 ++++------
app/src/routes/index.tsx | 12 ++++----
29 files changed, 108 insertions(+), 152 deletions(-)
diff --git a/app/src/components/RouteErrorBoundary.test.tsx b/app/src/components/RouteErrorBoundary.test.tsx
index 67a19902c02..4d8ce5fa3a4 100644
--- a/app/src/components/RouteErrorBoundary.test.tsx
+++ b/app/src/components/RouteErrorBoundary.test.tsx
@@ -7,7 +7,6 @@ import { createTheme, ThemeProvider } from '@mui/material/styles';
import { RouteErrorBoundary } from 'src/components/RouteErrorBoundary';
-
const theme = createTheme();
function renderWithRouter(thrown: unknown) {
diff --git a/app/src/pages/AboutPage.tsx b/app/src/pages/AboutPage.tsx
index a9e5217c7aa..2d17daeabc6 100644
--- a/app/src/pages/AboutPage.tsx
+++ b/app/src/pages/AboutPage.tsx
@@ -49,19 +49,17 @@ export function AboutPage() {
return (
<>
-
- about | anyplot.ai
-
-
-
-
-
+ about | anyplot.ai
+
+
+
+
{/* About (lede) */}
diff --git a/app/src/pages/AgentChatPage.feedback.test.tsx b/app/src/pages/AgentChatPage.feedback.test.tsx
index 114c48a669f..9d69961c635 100644
--- a/app/src/pages/AgentChatPage.feedback.test.tsx
+++ b/app/src/pages/AgentChatPage.feedback.test.tsx
@@ -24,7 +24,6 @@ vi.mock('src/global-config', async importOriginal => {
};
});
-
const analytics = vi.hoisted(() => ({ trackEvent: vi.fn(), trackPageview: vi.fn() }));
vi.mock('src/hooks/useAnalytics', () => ({ useAnalytics: () => analytics }));
diff --git a/app/src/pages/AgentChatPage.language.test.tsx b/app/src/pages/AgentChatPage.language.test.tsx
index 0e8bdccbdfb..24f46128c84 100644
--- a/app/src/pages/AgentChatPage.language.test.tsx
+++ b/app/src/pages/AgentChatPage.language.test.tsx
@@ -24,7 +24,6 @@ vi.mock('src/global-config', async importOriginal => {
};
});
-
const analytics = vi.hoisted(() => ({ trackEvent: vi.fn(), trackPageview: vi.fn() }));
vi.mock('src/hooks/useAnalytics', () => ({ useAnalytics: () => analytics }));
diff --git a/app/src/pages/AgentChatPage.save.test.tsx b/app/src/pages/AgentChatPage.save.test.tsx
index c88c016020b..1b0ee96cd82 100644
--- a/app/src/pages/AgentChatPage.save.test.tsx
+++ b/app/src/pages/AgentChatPage.save.test.tsx
@@ -23,7 +23,6 @@ vi.mock('src/global-config', async importOriginal => {
};
});
-
const analytics = vi.hoisted(() => ({ trackEvent: vi.fn(), trackPageview: vi.fn() }));
vi.mock('src/hooks/useAnalytics', () => ({ useAnalytics: () => analytics }));
diff --git a/app/src/pages/AgentChatPage.test.tsx b/app/src/pages/AgentChatPage.test.tsx
index af8101b7eff..1a1b27423cc 100644
--- a/app/src/pages/AgentChatPage.test.tsx
+++ b/app/src/pages/AgentChatPage.test.tsx
@@ -28,7 +28,6 @@ vi.mock('src/global-config', async importOriginal => {
};
});
-
// Stable identities, like the real hook (effects depend on them).
const analytics = vi.hoisted(() => ({ trackEvent: vi.fn(), trackPageview: vi.fn() }));
vi.mock('src/hooks/useAnalytics', () => ({ useAnalytics: () => analytics }));
diff --git a/app/src/pages/AgentChatPage.tsx b/app/src/pages/AgentChatPage.tsx
index b1af239d5b7..63263db9d74 100644
--- a/app/src/pages/AgentChatPage.tsx
+++ b/app/src/pages/AgentChatPage.tsx
@@ -524,9 +524,8 @@ function AgentChat({ specId, library, language, token, onLibraryChange }: AgentC
return (
<>
-
- {`agent · ${specId} | anyplot.ai`}
-
+ {`agent · ${specId} | anyplot.ai`}
+
{/* `colorScheme` gives the native controls (scrollbars, select menus) the site theme. */}
({
NumbersStrip: () => ,
}));
-
import { LandingPage } from 'src/pages/LandingPage';
describe('LandingPage', () => {
diff --git a/app/src/pages/LandingPage.tsx b/app/src/pages/LandingPage.tsx
index 48f2c4be859..b4a8f0b125a 100644
--- a/app/src/pages/LandingPage.tsx
+++ b/app/src/pages/LandingPage.tsx
@@ -36,14 +36,12 @@ export function LandingPage() {
return (
<>
-
- any.plot() — any library.
-
-
-
+ any.plot() — any library.
+
+
{/* Hero — fills the viewport so the fold shows only the hero */}
({
useAnalytics: () => ({
diff --git a/app/src/pages/LegalPage.tsx b/app/src/pages/LegalPage.tsx
index 2493168b009..d77404f0f0b 100644
--- a/app/src/pages/LegalPage.tsx
+++ b/app/src/pages/LegalPage.tsx
@@ -134,19 +134,17 @@ export function LegalPage() {
return (
<>
-
- legal | anyplot.ai
-
-
-
-
-
+ legal | anyplot.ai
+
+
+
+
{/* Legal Notice */}
diff --git a/app/src/pages/LibrariesPage.test.tsx b/app/src/pages/LibrariesPage.test.tsx
index 423e98ab8af..9af7b439623 100644
--- a/app/src/pages/LibrariesPage.test.tsx
+++ b/app/src/pages/LibrariesPage.test.tsx
@@ -1,4 +1,3 @@
-
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { render, screen, userEvent } from 'src/test-utils';
@@ -23,7 +22,6 @@ vi.mock('src/hooks', async () => {
};
});
-
vi.mock('react-router-dom', async () => {
const actual = await vi.importActual('react-router-dom');
return { ...actual, useNavigate: () => navigate };
diff --git a/app/src/pages/LibrariesPage.tsx b/app/src/pages/LibrariesPage.tsx
index 1c18f1629a5..78e7bf6cfba 100644
--- a/app/src/pages/LibrariesPage.tsx
+++ b/app/src/pages/LibrariesPage.tsx
@@ -70,19 +70,17 @@ export function LibrariesPage() {
return (
<>
-
- libraries | anyplot.ai
-
-
-
-
-
+ libraries | anyplot.ai
+
+
+
+
libraries} />
diff --git a/app/src/pages/MapPage.test.tsx b/app/src/pages/MapPage.test.tsx
index 3271b4835ed..e382df082f4 100644
--- a/app/src/pages/MapPage.test.tsx
+++ b/app/src/pages/MapPage.test.tsx
@@ -6,7 +6,6 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { MapPage } from 'src/pages/MapPage';
import { act, render, screen, waitFor } from 'src/test-utils';
-
const mockNavigate = vi.fn();
const mockTrackEvent = vi.fn();
diff --git a/app/src/pages/MapPage.tsx b/app/src/pages/MapPage.tsx
index 7b01c7156aa..7163647550c 100644
--- a/app/src/pages/MapPage.tsx
+++ b/app/src/pages/MapPage.tsx
@@ -343,12 +343,11 @@ export function MapPage() {
return (
<>
-
- map() — anyplot
-
+ map() — anyplot
+
({
useAnalytics: () => ({
trackPageview: vi.fn(),
diff --git a/app/src/pages/McpPage.tsx b/app/src/pages/McpPage.tsx
index 42e82505b39..9f2b7cb0b14 100644
--- a/app/src/pages/McpPage.tsx
+++ b/app/src/pages/McpPage.tsx
@@ -42,19 +42,17 @@ export function McpPage() {
return (
<>
-
- mcp | anyplot.ai
-
-
-
-
-
+ mcp | anyplot.ai
+
+
+
+
{/* What is MCP */}
diff --git a/app/src/pages/NotFoundPage.test.tsx b/app/src/pages/NotFoundPage.test.tsx
index 8a1a579b5b4..ab8688785ea 100644
--- a/app/src/pages/NotFoundPage.test.tsx
+++ b/app/src/pages/NotFoundPage.test.tsx
@@ -5,7 +5,6 @@ import { render, screen } from 'src/test-utils';
const mockTrackEvent = vi.fn();
-
vi.mock('src/hooks/useAnalytics', () => ({
useAnalytics: () => ({
trackPageview: vi.fn(),
diff --git a/app/src/pages/NotFoundPage.tsx b/app/src/pages/NotFoundPage.tsx
index a2c86e58fe5..d98ab6a5935 100644
--- a/app/src/pages/NotFoundPage.tsx
+++ b/app/src/pages/NotFoundPage.tsx
@@ -37,9 +37,8 @@ export function NotFoundPage({ source = 'catch_all' }: NotFoundPageProps = {}) {
return (
<>
-
- page not found | anyplot.ai
-
+ page not found | anyplot.ai
+
-
- imprint palette | anyplot.ai
-
-
-
-
-
+ imprint palette | anyplot.ai
+
+
+
+
{/* 1. HERO — short lede + a compact hue–chroma wheel. Click the wheel to
diff --git a/app/src/pages/PlotsPage.test.tsx b/app/src/pages/PlotsPage.test.tsx
index 3f8aa8dc3f8..07313ac0408 100644
--- a/app/src/pages/PlotsPage.test.tsx
+++ b/app/src/pages/PlotsPage.test.tsx
@@ -47,7 +47,6 @@ vi.mock('src/hooks', () => ({
useTheme: () => ({ isDark: false, toggle: vi.fn() }),
}));
-
vi.mock('src/sections/plots-gallery/FilterBar', () => ({
FilterBar: () => FilterBar
,
}));
diff --git a/app/src/pages/PlotsPage.tsx b/app/src/pages/PlotsPage.tsx
index 662c0d102be..d5cfbe015d7 100644
--- a/app/src/pages/PlotsPage.tsx
+++ b/app/src/pages/PlotsPage.tsx
@@ -177,14 +177,12 @@ export function PlotsPage() {
return (
-
- plots | anyplot.ai
-
-
-
+ plots | anyplot.ai
+
+
{error && (
diff --git a/app/src/pages/SpecPage.test.tsx b/app/src/pages/SpecPage.test.tsx
index ea448907c3d..354831c0f66 100644
--- a/app/src/pages/SpecPage.test.tsx
+++ b/app/src/pages/SpecPage.test.tsx
@@ -18,7 +18,6 @@ vi.mock('react-router-dom', async () => {
};
});
-
// The real useAnalytics returns useCallback-stable functions. Rebuilding them
// per render here would give every consumer a fresh identity, which turns any
// effect that lists trackEvent as a dependency into an infinite render loop —
diff --git a/app/src/pages/SpecPage.tsx b/app/src/pages/SpecPage.tsx
index e4cd22b55b5..857837544b2 100644
--- a/app/src/pages/SpecPage.tsx
+++ b/app/src/pages/SpecPage.tsx
@@ -492,23 +492,21 @@ export function SpecPage() {
return (
<>
-
- {`${specData.title}${titleSuffix} | anyplot.ai`}
-
-
-
- {currentImpl?.preview_url && }
-
-
+ {`${specData.title}${titleSuffix} | anyplot.ai`}
+
+
+
+ {currentImpl?.preview_url && }
+
+
+
+ {softwareSourceJsonLd && (
- {softwareSourceJsonLd && (
-
- )}
-
+ )}
{mode === 'hub' && (
diff --git a/app/src/pages/SpecsListPage.test.tsx b/app/src/pages/SpecsListPage.test.tsx
index b84908eb214..a9063cc5cb2 100644
--- a/app/src/pages/SpecsListPage.test.tsx
+++ b/app/src/pages/SpecsListPage.test.tsx
@@ -3,7 +3,6 @@ import { beforeEach, describe, expect, it, vi } from 'vitest';
import { SpecsListPage } from 'src/pages/SpecsListPage';
import { render, screen, waitFor } from 'src/test-utils';
-
vi.mock('src/hooks', () => ({
useAnalytics: () => ({
trackPageview: vi.fn(),
diff --git a/app/src/pages/SpecsListPage.tsx b/app/src/pages/SpecsListPage.tsx
index d66b2011383..f4fa44a3b17 100644
--- a/app/src/pages/SpecsListPage.tsx
+++ b/app/src/pages/SpecsListPage.tsx
@@ -147,19 +147,14 @@ export function SpecsListPage() {
return (
<>
-
- specs | anyplot.ai
-
-
-
-
-
+ specs | anyplot.ai
+
+
+
+
specs} />
diff --git a/app/src/pages/StatsPage.test.tsx b/app/src/pages/StatsPage.test.tsx
index 52a6e09b09d..e131ae06044 100644
--- a/app/src/pages/StatsPage.test.tsx
+++ b/app/src/pages/StatsPage.test.tsx
@@ -12,7 +12,6 @@ const darkThemeValue: ThemeContextValue = {
cycle: vi.fn(),
};
-
const mockTrackEvent = vi.fn();
vi.mock('src/hooks', () => ({
diff --git a/app/src/pages/StatsPage.tsx b/app/src/pages/StatsPage.tsx
index 92a8d49211c..a4cda4166cc 100644
--- a/app/src/pages/StatsPage.tsx
+++ b/app/src/pages/StatsPage.tsx
@@ -206,14 +206,12 @@ export function StatsPage() {
return (
<>
-
- stats | anyplot.ai
-
-
-
+ stats | anyplot.ai
+
+
{/* Summary Counters */}
diff --git a/app/src/routes/index.tsx b/app/src/routes/index.tsx
index 35fc9dd0fd9..16fa41ab8da 100644
--- a/app/src/routes/index.tsx
+++ b/app/src/routes/index.tsx
@@ -118,12 +118,10 @@ const router = createBrowserRouter([
export function AppRouter() {
return (
-
-
-
-
-
-
-
+
+
+
+
+
);
}
From 918f58f000aabe84d561287d5814dfa167d4ba92 Mon Sep 17 00:00:00 2001
From: "google-labs-jules[bot]"
<161369871+google-labs-jules[bot]@users.noreply.github.com>
Date: Thu, 29 Oct 2026 00:04:37 +0000
Subject: [PATCH 3/3] refactor(app): replace react-helmet-async with React 19
native metadata
---
app/src/hooks/useUrlSync.test.ts | 3 -
app/src/hooks/useUrlSync.ts | 2 -
app/src/pages/PlotsPage.test.tsx | 114 +++++++++++++++++--------------
app/src/pages/PlotsPage.tsx | 25 +++++--
fix_plots_page.cjs | 9 +++
fix_plots_page2.cjs | 8 +++
fix_plots_page3.cjs | 20 ++++++
fix_plots_page_order.cjs | 37 ++++++++++
fix_plots_page_test.cjs | 23 +++++++
fix_useUrlSync.cjs | 24 +++++++
fix_useUrlSync.sh | 18 +++++
11 files changed, 220 insertions(+), 63 deletions(-)
create mode 100644 fix_plots_page.cjs
create mode 100644 fix_plots_page2.cjs
create mode 100644 fix_plots_page3.cjs
create mode 100644 fix_plots_page_order.cjs
create mode 100644 fix_plots_page_test.cjs
create mode 100644 fix_useUrlSync.cjs
create mode 100755 fix_useUrlSync.sh
diff --git a/app/src/hooks/useUrlSync.test.ts b/app/src/hooks/useUrlSync.test.ts
index bbb12366c11..87850d2d5bc 100644
--- a/app/src/hooks/useUrlSync.test.ts
+++ b/app/src/hooks/useUrlSync.test.ts
@@ -98,7 +98,6 @@ describe('useUrlSync', () => {
renderHook(() => useUrlSync({ activeFilters: filters, onTrackPageview }));
expect(window.location.search).toBe('?lib=matplotlib');
- expect(document.title).toBe('lib:matplotlib | anyplot.ai');
expect(onTrackPageview).toHaveBeenCalled();
});
@@ -106,7 +105,5 @@ describe('useUrlSync', () => {
const onTrackPageview = vi.fn();
renderHook(() => useUrlSync({ activeFilters: [], onTrackPageview }));
-
- expect(document.title).toBe('anyplot.ai');
});
});
diff --git a/app/src/hooks/useUrlSync.ts b/app/src/hooks/useUrlSync.ts
index 88f07c78290..204a81772a3 100644
--- a/app/src/hooks/useUrlSync.ts
+++ b/app/src/hooks/useUrlSync.ts
@@ -62,8 +62,6 @@ export function useUrlSync({ activeFilters, onTrackPageview }: UseUrlSyncOptions
useEffect(() => {
const newUrl = buildFilterUrl(activeFilters);
window.history.replaceState({}, '', newUrl);
-
- // Update document title
const filterParts = activeFilters
.filter(f => f.values.length > 0)
.map(f => `${f.category}:${f.values.join(',')}`)
diff --git a/app/src/pages/PlotsPage.test.tsx b/app/src/pages/PlotsPage.test.tsx
index 07313ac0408..2041bec5854 100644
--- a/app/src/pages/PlotsPage.test.tsx
+++ b/app/src/pages/PlotsPage.test.tsx
@@ -47,6 +47,12 @@ vi.mock('src/hooks', () => ({
useTheme: () => ({ isDark: false, toggle: vi.fn() }),
}));
+vi.mock('react-helmet-async', () => ({
+ Helmet: ({ children }: { children: React.ReactNode }) => (
+ {children}
+ ),
+}));
+
vi.mock('src/sections/plots-gallery/FilterBar', () => ({
FilterBar: () => FilterBar
,
}));
@@ -131,66 +137,72 @@ describe('PlotsPage', () => {
expect(document.title).toBe('plots | anyplot.ai');
});
- it('renders the shared scroll-to-top button', () => {
+ it('renders document metadata with filters', () => {
+ state.activeFilters = [{ category: 'lib', values: ['matplotlib'] }];
render();
- expect(screen.getByLabelText(/scroll to top/i)).toBeInTheDocument();
+ expect(document.title).toBe('lib:matplotlib | plots | anyplot.ai');
});
+});
- describe('global keyboard shortcuts', () => {
- it('triggers random navigation on Space when nothing interactive is focused', () => {
- render();
- fireEvent.keyDown(document.body, { key: ' ' });
- expect(mockHandleRandom).toHaveBeenCalledWith('space');
- });
+it('renders the shared scroll-to-top button', () => {
+ render();
+ expect(screen.getByLabelText(/scroll to top/i)).toBeInTheDocument();
+});
- it('does not fire when a focusable card owns the keystroke', () => {
- render();
- const card = document.createElement('div');
- card.setAttribute('role', 'button');
- card.tabIndex = 0;
- document.body.appendChild(card);
- try {
- fireEvent.keyDown(card, { key: ' ' });
- expect(mockHandleRandom).not.toHaveBeenCalled();
- } finally {
- card.remove();
- }
- });
+describe('global keyboard shortcuts', () => {
+ it('triggers random navigation on Space when nothing interactive is focused', () => {
+ render();
+ fireEvent.keyDown(document.body, { key: ' ' });
+ expect(mockHandleRandom).toHaveBeenCalledWith('space');
+ });
- it('does not fire when a native button owns the keystroke', () => {
- render();
- const button = document.createElement('button');
- document.body.appendChild(button);
- try {
- fireEvent.keyDown(button, { key: ' ' });
- expect(mockHandleRandom).not.toHaveBeenCalled();
- } finally {
- button.remove();
- }
- });
+ it('does not fire when a focusable card owns the keystroke', () => {
+ render();
+ const card = document.createElement('div');
+ card.setAttribute('role', 'button');
+ card.tabIndex = 0;
+ document.body.appendChild(card);
+ try {
+ fireEvent.keyDown(card, { key: ' ' });
+ expect(mockHandleRandom).not.toHaveBeenCalled();
+ } finally {
+ card.remove();
+ }
});
- describe('scrollRestoration', () => {
- const original = history.scrollRestoration;
+ it('does not fire when a native button owns the keystroke', () => {
+ render();
+ const button = document.createElement('button');
+ document.body.appendChild(button);
+ try {
+ fireEvent.keyDown(button, { key: ' ' });
+ expect(mockHandleRandom).not.toHaveBeenCalled();
+ } finally {
+ button.remove();
+ }
+ });
+});
- afterEach(() => {
- history.scrollRestoration = original;
- });
+describe('scrollRestoration', () => {
+ const original = history.scrollRestoration;
- it("sets scrollRestoration to 'manual' on mount and restores the previous value on unmount", () => {
- history.scrollRestoration = 'auto';
- const { unmount } = render();
- expect(history.scrollRestoration).toBe('manual');
- unmount();
- expect(history.scrollRestoration).toBe('auto');
- });
+ afterEach(() => {
+ history.scrollRestoration = original;
+ });
- it('restores a non-default previous value on unmount instead of forcing auto', () => {
- history.scrollRestoration = 'manual';
- const { unmount } = render();
- expect(history.scrollRestoration).toBe('manual');
- unmount();
- expect(history.scrollRestoration).toBe('manual');
- });
+ it("sets scrollRestoration to 'manual' on mount and restores the previous value on unmount", () => {
+ history.scrollRestoration = 'auto';
+ const { unmount } = render();
+ expect(history.scrollRestoration).toBe('manual');
+ unmount();
+ expect(history.scrollRestoration).toBe('auto');
+ });
+
+ it('restores a non-default previous value on unmount instead of forcing auto', () => {
+ history.scrollRestoration = 'manual';
+ const { unmount } = render();
+ expect(history.scrollRestoration).toBe('manual');
+ unmount();
+ expect(history.scrollRestoration).toBe('manual');
});
});
diff --git a/app/src/pages/PlotsPage.tsx b/app/src/pages/PlotsPage.tsx
index d5cfbe015d7..e655bd11b38 100644
--- a/app/src/pages/PlotsPage.tsx
+++ b/app/src/pages/PlotsPage.tsx
@@ -1,4 +1,4 @@
-import { useCallback, useEffect, useRef, useState } from 'react';
+import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { Link as RouterLink, useNavigate, useSearchParams } from 'react-router-dom';
@@ -59,6 +59,15 @@ export function PlotsPage() {
onTrackEvent: trackEvent,
});
+ const filterParts = useMemo(() => {
+ return activeFilters
+ .filter(f => f.values.length > 0)
+ .map(f => `${f.category}:${f.values.join(',')}`)
+ .join(' ');
+ }, [activeFilters]);
+
+ const pageTitle = filterParts ? `${filterParts} | plots | anyplot.ai` : 'plots | anyplot.ai';
+
const { loadMoreRef } = useInfiniteScroll({
allImages,
displayedImages,
@@ -177,12 +186,14 @@ export function PlotsPage() {
return (
- plots | anyplot.ai
-
-
+ <>
+ {pageTitle}
+
+
+ >
{error && (
diff --git a/fix_plots_page.cjs b/fix_plots_page.cjs
new file mode 100644
index 00000000000..98688b35cbc
--- /dev/null
+++ b/fix_plots_page.cjs
@@ -0,0 +1,9 @@
+const fs = require('fs');
+
+const pageFile = 'app/src/pages/PlotsPage.tsx';
+let content = fs.readFileSync(pageFile, 'utf8');
+
+// The reason useMemo is unused is because I am using it, but it might be imported twice or something?
+// Let's print out the first 20 lines to see.
+const lines = content.split('\n');
+console.log(lines.slice(0, 20).join('\n'));
diff --git a/fix_plots_page2.cjs b/fix_plots_page2.cjs
new file mode 100644
index 00000000000..57669af1090
--- /dev/null
+++ b/fix_plots_page2.cjs
@@ -0,0 +1,8 @@
+const fs = require('fs');
+
+const pageFile = 'app/src/pages/PlotsPage.tsx';
+let content = fs.readFileSync(pageFile, 'utf8');
+
+// I inserted it at the end of the file, around useFilterState, where is that? Let's check lines 30-60
+const lines = content.split('\n');
+console.log(lines.slice(20, 70).join('\n'));
diff --git a/fix_plots_page3.cjs b/fix_plots_page3.cjs
new file mode 100644
index 00000000000..a64df09a71d
--- /dev/null
+++ b/fix_plots_page3.cjs
@@ -0,0 +1,20 @@
+const fs = require('fs');
+
+const pageFile = 'app/src/pages/PlotsPage.tsx';
+let content = fs.readFileSync(pageFile, 'utf8');
+
+// I mistakenly replaced `clearFilters,\n } = useFilterState(specsData, librariesData, languagesData);`
+// but it looks like `clearFilters` isn't there, or the hook signature changed.
+// Let's insert the useMemo logic right after `setHasMore,` on line 43.
+content = content.replace(" const { loadMoreRef } = useInfiniteScroll({", ` const filterParts = useMemo(() => {
+ return activeFilters
+ .filter(f => f.values.length > 0)
+ .map(f => \`\${f.category}:\${f.values.join(',')}\`)
+ .join(' ');
+ }, [activeFilters]);
+
+ const pageTitle = filterParts ? \`\${filterParts} | plots | anyplot.ai\` : 'plots | anyplot.ai';
+
+ const { loadMoreRef } = useInfiniteScroll({`);
+
+fs.writeFileSync(pageFile, content);
diff --git a/fix_plots_page_order.cjs b/fix_plots_page_order.cjs
new file mode 100644
index 00000000000..a1677d13f0e
--- /dev/null
+++ b/fix_plots_page_order.cjs
@@ -0,0 +1,37 @@
+const fs = require('fs');
+
+const pageFile = 'app/src/pages/PlotsPage.tsx';
+let content = fs.readFileSync(pageFile, 'utf8');
+
+// Fix the import order for useMemo and useState
+content = content.replace("import { useCallback, useEffect, useRef, useState, useMemo } from 'react';", "import { useCallback, useEffect, useMemo, useRef, useState } from 'react';");
+
+// Move filterParts computation after activeFilters is declared
+// activeFilters is declared at line:
+// const {
+// activeFilters,
+// ...
+// clearFilters,
+// } = useFilterState(specsData, librariesData, languagesData);
+
+const filterLogic = `
+ const filterParts = useMemo(() => {
+ return activeFilters
+ .filter(f => f.values.length > 0)
+ .map(f => \`\${f.category}:\${f.values.join(',')}\`)
+ .join(' ');
+ }, [activeFilters]);
+
+ const pageTitle = filterParts ? \`\${filterParts} | plots | anyplot.ai\` : 'plots | anyplot.ai';
+`;
+
+// Remove it from its current place
+content = content.replace(/\s*const filterParts = useMemo\(\(\) => \{[\s\S]*?\}, \[activeFilters\]\);\s*const pageTitle = filterParts \? `\$\{filterParts\} \| plots \| anyplot\.ai` : 'plots \| anyplot\.ai';/, "");
+
+// Insert after clearFilters
+content = content.replace(/\s*clearFilters,\s*\} = useFilterState\(specsData, librariesData, languagesData\);/, (match) => {
+ return `${match}\n\n${filterLogic}`;
+});
+
+
+fs.writeFileSync(pageFile, content);
diff --git a/fix_plots_page_test.cjs b/fix_plots_page_test.cjs
new file mode 100644
index 00000000000..b69f08baead
--- /dev/null
+++ b/fix_plots_page_test.cjs
@@ -0,0 +1,23 @@
+const fs = require('fs');
+
+const testFile = 'app/src/pages/PlotsPage.test.tsx';
+let testContent = fs.readFileSync(testFile, 'utf8');
+
+// I accidentally duplicated the render and expect logic outside the test block or something!
+// Look at the grep output:
+// 140: it('renders document metadata with filters', () => {
+// 141- state.activeFilters = [{ category: 'lib', values: ['matplotlib'] }];
+// 142- render();
+// 143- expect(document.title).toBe('lib:matplotlib | plots | anyplot.ai');
+// 144- });
+// 145-
+// 146- render();
+// 147- expect(document.title).toBe('lib:matplotlib | plots | anyplot.ai');
+// 148-});
+
+testContent = testContent.replace(
+ /it\('renders document metadata with filters', \(\) => \{\n\s*state\.activeFilters = \[\{ category: 'lib', values: \['matplotlib'\] \}\];\n\s*render\(\);\n\s*expect\(document\.title\)\.toBe\('lib:matplotlib \| plots \| anyplot\.ai'\);\n\s*\}\);\n\n\s*render\(\);\n\s*expect\(document\.title\)\.toBe\('lib:matplotlib \| plots \| anyplot\.ai'\);\n\}\);/,
+ "it('renders document metadata with filters', () => {\n state.activeFilters = [{ category: 'lib', values: ['matplotlib'] }];\n render();\n expect(document.title).toBe('lib:matplotlib | plots | anyplot.ai');\n });\n});"
+);
+
+fs.writeFileSync(testFile, testContent);
diff --git a/fix_useUrlSync.cjs b/fix_useUrlSync.cjs
new file mode 100644
index 00000000000..8e8d83abf52
--- /dev/null
+++ b/fix_useUrlSync.cjs
@@ -0,0 +1,24 @@
+const fs = require('fs');
+
+// 1. Update useUrlSync.ts
+const file = "app/src/hooks/useUrlSync.ts";
+let content = fs.readFileSync(file, 'utf8');
+
+content = content.replace(/\/\*\*[\s\S]*?Hook to synchronize active filters with URL and updates document title\.[\s\S]*?\*\//, "/**\n * Hook to synchronize active filters with URL.\n */");
+
+const titleRegex = /\s*const filterParts = activeFilters[\s\S]*?if \(filterParts\) \{[\s\S]*?document\.title = `\$\{filterParts\} \| anyplot\.ai`;[\s\S]*?\} else \{[\s\S]*?document\.title = 'anyplot\.ai';[\s\S]*?\}/;
+content = content.replace(titleRegex, "");
+
+content = content.replace(/\s*\/\/ Update document title/, "");
+
+fs.writeFileSync(file, content);
+
+// 1b. Update useUrlSync.test.ts
+const file_test = "app/src/hooks/useUrlSync.test.ts";
+let content_test = fs.readFileSync(file_test, 'utf8');
+
+content_test = content_test.replace("updates URL and title", "updates URL");
+content_test = content_test.replace(/\s*expect\(document\.title\)\.toBe\('lib:matplotlib \| anyplot\.ai'\);/, "");
+content_test = content_test.replace(/\s*expect\(document\.title\)\.toBe\('anyplot\.ai'\);/, "");
+
+fs.writeFileSync(file_test, content_test);
diff --git a/fix_useUrlSync.sh b/fix_useUrlSync.sh
new file mode 100755
index 00000000000..41d1485ef70
--- /dev/null
+++ b/fix_useUrlSync.sh
@@ -0,0 +1,18 @@
+#!/bin/bash
+
+# 1. Update useUrlSync.ts
+file="app/src/hooks/useUrlSync.ts"
+
+sed -i '/const filterParts = activeFilters/,/document.title =/d' $file
+sed -i "/document.title = 'anyplot.ai'/d" $file
+sed -i "/if (filterParts) {/d" $file
+sed -i "/} else {/d" $file
+sed -i "/}/d" $file
+sed -i "s/and updates document title//" $file
+sed -i "s/\/\/ Update document title//g" $file
+
+# 1b. Update useUrlSync.test.ts
+file_test="app/src/hooks/useUrlSync.test.ts"
+sed -i "s/updates URL and title/updates URL/" $file_test
+sed -i "/expect(document.title).toBe('lib:matplotlib | anyplot.ai');/d" $file_test
+sed -i "/expect(document.title).toBe('anyplot.ai');/d" $file_test