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..4d8ce5fa3a4 100644
--- a/app/src/components/RouteErrorBoundary.test.tsx
+++ b/app/src/components/RouteErrorBoundary.test.tsx
@@ -7,10 +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();
function renderWithRouter(thrown: unknown) {
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/AboutPage.tsx b/app/src/pages/AboutPage.tsx
index 641f82902ae..2d17daeabc6 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,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 2498136ada3..9d69961c635 100644
--- a/app/src/pages/AgentChatPage.feedback.test.tsx
+++ b/app/src/pages/AgentChatPage.feedback.test.tsx
@@ -24,10 +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..24f46128c84 100644
--- a/app/src/pages/AgentChatPage.language.test.tsx
+++ b/app/src/pages/AgentChatPage.language.test.tsx
@@ -24,10 +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..1b0ee96cd82 100644
--- a/app/src/pages/AgentChatPage.save.test.tsx
+++ b/app/src/pages/AgentChatPage.save.test.tsx
@@ -23,10 +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..1a1b27423cc 100644
--- a/app/src/pages/AgentChatPage.test.tsx
+++ b/app/src/pages/AgentChatPage.test.tsx
@@ -28,10 +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() }));
vi.mock('src/hooks/useAnalytics', () => ({ useAnalytics: () => analytics }));
diff --git a/app/src/pages/AgentChatPage.tsx b/app/src/pages/AgentChatPage.tsx
index 1fdb9745129..63263db9d74 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,9 @@ 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: () => ,
}));
-vi.mock('react-helmet-async', () => ({
- Helmet: ({ children }: { children: React.ReactNode }) => (
- {children}
- ),
-}));
-
import { LandingPage } from 'src/pages/LandingPage';
describe('LandingPage', () => {
diff --git a/app/src/pages/LandingPage.tsx b/app/src/pages/LandingPage.tsx
index 6d2053fa38d..b4a8f0b125a 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,12 @@ export function LandingPage() {
return (
<>
-
- any.plot() — any library.
-
-
-
+ 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', () => ({
useAnalytics: () => ({
diff --git a/app/src/pages/LegalPage.tsx b/app/src/pages/LegalPage.tsx
index 4db795b06ba..d77404f0f0b 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,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 2dee9179855..9af7b439623 100644
--- a/app/src/pages/LibrariesPage.test.tsx
+++ b/app/src/pages/LibrariesPage.test.tsx
@@ -1,5 +1,3 @@
-import type { ReactNode } from 'react';
-
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { render, screen, userEvent } from 'src/test-utils';
@@ -24,10 +22,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');
return { ...actual, useNavigate: () => navigate };
diff --git a/app/src/pages/LibrariesPage.tsx b/app/src/pages/LibrariesPage.tsx
index ce6e7784d04..78e7bf6cfba 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,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 4dd4a40ead1..e382df082f4 100644
--- a/app/src/pages/MapPage.test.tsx
+++ b/app/src/pages/MapPage.test.tsx
@@ -6,10 +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..7163647550c 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,12 @@ export function MapPage() {
return (
<>
-
- map() — anyplot
-
-
+ map() — anyplot
+
+
({
- Helmet: ({ children }: { children: React.ReactNode }) => <>{children}>,
-}));
-
vi.mock('src/hooks', () => ({
useAnalytics: () => ({
trackPageview: vi.fn(),
diff --git a/app/src/pages/McpPage.tsx b/app/src/pages/McpPage.tsx
index 27fd3a329b5..9f2b7cb0b14 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,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 cb610c41f63..ab8688785ea 100644
--- a/app/src/pages/NotFoundPage.test.tsx
+++ b/app/src/pages/NotFoundPage.test.tsx
@@ -5,11 +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: () => ({
trackPageview: vi.fn(),
diff --git a/app/src/pages/NotFoundPage.tsx b/app/src/pages/NotFoundPage.tsx
index 05a7c46cd91..d98ab6a5935 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,9 @@ 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 e44745e6cac..2041bec5854 100644
--- a/app/src/pages/PlotsPage.test.tsx
+++ b/app/src/pages/PlotsPage.test.tsx
@@ -132,71 +132,77 @@ 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', () => {
+ 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 5eda93471eb..e655bd11b38 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 { useCallback, useEffect, useMemo, 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';
@@ -60,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,
@@ -178,14 +186,14 @@ export function PlotsPage() {
return (
-
- plots | anyplot.ai
+ <>
+ {pageTitle}
-
-
+
+ >
{error && (
diff --git a/app/src/pages/SpecPage.test.tsx b/app/src/pages/SpecPage.test.tsx
index 5f64676f871..354831c0f66 100644
--- a/app/src/pages/SpecPage.test.tsx
+++ b/app/src/pages/SpecPage.test.tsx
@@ -18,10 +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
// 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 2068fd0afc0..857837544b2 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,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 7f624ebe17a..a9063cc5cb2 100644
--- a/app/src/pages/SpecsListPage.test.tsx
+++ b/app/src/pages/SpecsListPage.test.tsx
@@ -3,10 +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: () => ({
trackPageview: vi.fn(),
diff --git a/app/src/pages/SpecsListPage.tsx b/app/src/pages/SpecsListPage.tsx
index 4b9895ca8b3..f4fa44a3b17 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,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 274c5d18f6c..e131ae06044 100644
--- a/app/src/pages/StatsPage.test.tsx
+++ b/app/src/pages/StatsPage.test.tsx
@@ -12,10 +12,6 @@ const darkThemeValue: ThemeContextValue = {
cycle: vi.fn(),
};
-vi.mock('react-helmet-async', () => ({
- Helmet: ({ children }: { children: React.ReactNode }) => <>{children}>,
-}));
-
const mockTrackEvent = vi.fn();
vi.mock('src/hooks', () => ({
diff --git a/app/src/pages/StatsPage.tsx b/app/src/pages/StatsPage.tsx
index 0fc1fd298a0..a4cda4166cc 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,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 bfaa9e26141..16fa41ab8da 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,10 @@ 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.
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