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 ``, `<meta>` and `<link rel="canonical">` 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(<PlotsPage />); +// 143- expect(document.title).toBe('lib:matplotlib | plots | anyplot.ai'); +// 144- }); +// 145- +// 146- render(<PlotsPage />); +// 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\(<PlotsPage \/>\);\n\s*expect\(document\.title\)\.toBe\('lib:matplotlib \| plots \| anyplot\.ai'\);\n\s*\}\);\n\n\s*render\(<PlotsPage \/>\);\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(<PlotsPage />);\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