Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions docs/field-velocity-visibility.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# Field-velocity launch boundary

Two deliberately separate changes:

1. **Hide now:** remove the `AreaFieldVelocity` import and mount from both public focus-area page templates (the generic template and the hardcoded Economies & Governance route). No CSS-only hiding, public preview-discovery links, data loading, or chart-fragment dialogs remain on those pages. The reusable component stays staged in source for restoration.
2. **Restore later:** a separate draft PR reverses only that public-detail wiring and restores the original positive mounting tests. It does not publish `/impact/`, change navigation, or promote the cross-field preview. Merge the hiding PR first; the restore PR is stacked on its branch and must target `main` after the hiding PR merges. Do not merge the restore PR into the hiding branch.

The cross-field page remains at `/impact-preview-eb61fba1b98e/`, with all four areas, chart deep links, source faces, inflections, methodology and toolkit. Its `noindex,nofollow` metadata and exclusions from navigation, sitemap, search, RSS and robots discovery remain unchanged. The read-only field-velocity API/data are preserved for consumers. Unlisted/noindex is not authentication or access control.

This is distinct from the older draft PR #105, which would promote `/impact/` into navigation. That PR is not changed by either detail-page visibility PR.

## Verification (September 10, 2026)

Application revisions exercised:
- Hidden: `7e696f9b64aba340c4aa15b374e1c1c23d320ed5` — 79 tests passing.
- Restored: `e426c490736f055794428e39733b160c31de295b` — 75 tests passing.
- The restored `src` tree is exactly the pre-hide main/PR153 tree at `cea75883bbed07cb751a3bc17c5b677cfa587b74` (tree `61229e854adeb104781b01b0747df0027898d1c4`). No other source, styling, data, API or site changes.

Both passed pnpm 10 frozen installs, TypeScript and production builds, plus independent isolated code reviews. Test-first regressions failed on all four public routes before hiding; the original mounting tests failed on all four hidden routes before restoring.

Headed local production-build browser checks at 1440, 390 and 320px:
- All four public detail routes preserve Opportunity Spaces and their existing Insights/Explore content.
- Hidden state has zero field-velocity panels, tiles, preview-discovery links or old `#fv/...` chart dialogs.
- Restored state has the field panel, all five tiles, three overview/methodology/toolkit links and working old chart fragments.
- All four selected cross-field overview tabs retain five tiles and methodology. A real chart URL per field opens its modal; the data/source flip and Escape dismissal work.
- 24 route/viewport cases per state, 48 across both. Screenshots below are local captures of these real source revisions, not claims about production deployment.

36 HTTP checks across both local production builds cover home, area index, all four detail routes, the exact preview, sitemap, robots, search, RSS, each field API, malformed API area, and missing `/impact/` and `/field-velocity/` routes.

## Existing limitations, not changed by these PRs

- Both states produce exactly the same measured horizontal overflow: 8px on the unchanged cross-field preview at 1440/390px; 15px at a 320px viewport with a native scrollbar (the site's existing 320px body minimum). Public detail pages have no horizontal overflow at 1440/390px. These visibility changes do not attempt a preview layout redesign.
- Builds report existing provider fallback/cache warnings (including Ma Earth snapshot fallback and indexer GraphQL errors). No new provider integration or data freshness is claimed.
- The production URL was inspected through HTTP extraction. The authenticated Chrome policy blocked direct `www.plrd.org` navigation; visual evidence uses local production builds, with hosted PR deployment verification reported separately in the PR descriptions.

## Screenshots

### Hidden public detail: desktop
![Hidden public detail, desktop](screenshots/field-velocity-visibility/detail-hidden-desktop.png)

### Hidden public detail: mobile
![Hidden public detail, mobile](screenshots/field-velocity-visibility/detail-hidden-mobile.png)

### Exact pre-hide detail behavior restored: desktop
![Restored public detail, desktop](screenshots/field-velocity-visibility/detail-restored-desktop.png)

### Exact pre-hide detail behavior restored: mobile
![Restored public detail, mobile](screenshots/field-velocity-visibility/detail-restored-mobile.png)

### Cross-field preview retained
![Retained cross-field preview](screenshots/field-velocity-visibility/cross-field-retained-desktop.png)
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
15 changes: 5 additions & 10 deletions scripts/velocity/field-velocity-panel.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -45,19 +45,14 @@ function elements(node) {
}

for (const { key, label } of FOCUS_AREAS) {
test(`${key} overview includes the shared panel, original strategy and selected-area methodology/toolkit links`, async (t) => {
test(`${key} staged panel retains charts and selected-area methodology/toolkit links without public route wiring`, async (t) => {
assert.ok(existsSync('src/components/AreaFieldVelocity.tsx'), 'missing reusable area panel')
t.mock.method(globalThis, 'fetch', async () => new Response(null, { status: 503 }))
t.mock.method(console, 'warn', () => {})
const Panel = source('components/AreaFieldVelocity.tsx').default
const Page = source(key === 'economies-governance' ? 'app/areas/economies-governance/page.tsx' : 'app/areas/[slug]/page.tsx').default
const tree = await Page({ params: Promise.resolve({ slug: key }) })
const children = elements(tree)
const panel = children.find(el => el.type === Panel)
assert.ok(panel, `${key} page does not mount shared panel`)
assert.equal(panel.props.area, key)
assert.ok(children.some(el => el.props.id === 'opportunity-spaces'), 'existing strategy preserved')
const panelTree = await Panel(panel.props)
// The component is retained for a later launch, but no public page mounts it.
// Public-route absence is covered separately by preview-only.test.mjs.
const panelTree = await Panel({ area: key })
if (key === 'economies-governance') {
assert.deepEqual(elements(panelTree).find(el => el.type === Dashboard).props.liveOutputs, {}, 'area panel must retain the overview live-output channel even when providers are unavailable')
}
Expand All @@ -69,7 +64,7 @@ for (const { key, label } of FOCUS_AREAS) {

assert.match(html, /id="field-velocity"/)
assert.ok(html.includes(`${label} field velocity</h2>`))
assert.ok(tree.props.className.includes('area-overview'))

assert.ok(panelTree.props.className.includes('area-field-velocity'))
assert.ok(panelTree.props.className.includes(key === 'neurotech' ? 'bg-gray-100' : 'bg-gray-200'))
assert.doesNotMatch(panelTree.props.className, /border-y|px-4|bg-gray-50/)
Expand Down
64 changes: 64 additions & 0 deletions scripts/velocity/preview-only.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { test } from 'node:test'
import assert from 'node:assert/strict'
import React from 'react'
import { readFileSync } from 'node:fs'
import { source } from './test-source-loader.mjs'

const Panel = source('components/AreaFieldVelocity.tsx').default
const Dashboard = source('components/ImpactDashboardV2.tsx').default
const Methodology = source('components/MeasuringQuestionsV2.tsx').default
const { FOCUS_AREAS, FIELD_VELOCITY_OVERVIEW } = source('lib/field-velocity.ts')

function elements(node) {
if (!React.isValidElement(node)) return []
return [node, ...React.Children.toArray(node.props.children).flatMap(elements)]
}

function offlineProviders(t) {
t.mock.method(globalThis, 'fetch', async () => new Response(null, { status: 503 }))
t.mock.method(console, 'warn', () => {})
}

for (const { key } of FOCUS_AREAS) {
test(`${key} public detail keeps existing content but does not mount field velocity or preview links`, async (t) => {
offlineProviders(t)
const route = key === 'economies-governance' ? 'app/areas/economies-governance/page.tsx' : 'app/areas/[slug]/page.tsx'
const Page = source(route).default
const nodes = elements(await Page({ params: Promise.resolve({ slug: key }) }))
assert.ok(!nodes.some(node => node.type === Panel || node.type === Dashboard || node.type === Methodology), 'unreleased field velocity must not mount on public pages')
assert.ok(!nodes.some(node => node.props.id === 'field-velocity'))
assert.ok(!nodes.some(node => /impact-preview|#fv\/|#field-velocity|#methodology|#toolkit/.test(node.props.href ?? '')), 'no discovery links to the unlisted preview')
assert.ok(nodes.some(node => node.props.id === 'opportunity-spaces'), 'original strategy remains')
const existingLink = key === 'economies-governance' ? '/areas/economies-governance/projects/' : '/insights/'
assert.ok(nodes.some(node => node.props.href === existingLink), 'original Explore/Insights content remains')
assert.doesNotMatch(readFileSync(`src/${route}`, 'utf8'), /from ['"]@\/(?:components\/(?:AreaFieldVelocity|ImpactDashboardV2|MeasuringQuestionsV2)|lib\/field-velocity[^'"]*)['"]/, 'public routes must not load field-velocity modules')
})
}

test('the exact unlisted overview retains all fields, charts, methodology and noindex metadata', async (t) => {
offlineProviders(t)
assert.equal(FIELD_VELOCITY_OVERVIEW, '/impact-preview-eb61fba1b98e/')
const { default: Overview, metadata } = source(`app${FIELD_VELOCITY_OVERVIEW}page.tsx`)
assert.deepEqual(metadata.robots, { index: false, follow: false, googleBot: { index: false, follow: false } })
for (const { key } of FOCUS_AREAS) {
const nodes = elements(await Overview({ searchParams: Promise.resolve({ area: key }) }))
const dashboard = nodes.find(node => node.type === Dashboard)
assert.ok(dashboard)
assert.equal(dashboard.props.initialArea, key)
assert.equal(dashboard.props.fixedArea, undefined)
assert.deepEqual(Object.keys(dashboard.props.recordsByArea).sort(), FOCUS_AREAS.map(area => area.key).sort())
assert.ok(nodes.some(node => node.type === Methodology))
assert.ok(nodes.some(node => node.props.id === 'methodology'))
}
})

test('the preview stays absent from public navigation, sitemap, search, RSS and robots discovery', () => {
const sitemap = source('app/sitemap.ts').default()
const { mainNav, footerNav } = source('lib/site-config.ts')
for (const [label, value] of Object.entries({ sitemap, mainNav, footerNav })) {
assert.doesNotMatch(JSON.stringify(value), /impact-preview|\/field-velocity\/|"[^" ]*\/impact\/"/, label)
}
for (const file of ['public/search-index.json', 'public/feed.xml', 'public/robots.txt']) {
assert.doesNotMatch(readFileSync(file, 'utf8'), /impact-preview-eb61fba1b98e|\/field-velocity\//, file)
}
})
4 changes: 1 addition & 3 deletions src/app/areas/[slug]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
import type { Metadata } from 'next'
import AreaFieldVelocity from '@/components/AreaFieldVelocity'
import { isFocusAreaKey } from '@/lib/field-velocity-data'

import EditPageButton from '@/components/EditPageButton'
import { PageEditHistoryByline } from '@/components/EditHistoryByline'
Expand Down Expand Up @@ -289,7 +287,7 @@ export default async function AreaPage({ params }: Props) {
</section>
)}

{isFocusAreaKey(slug) && <AreaFieldVelocity area={slug} />}
{/* Field velocity is preview-only until a separate public launch. */}

{/* Insights — latest posts, publications, and talks for this focus area */}
{areaInsights.length > 0 && (
Expand Down
4 changes: 2 additions & 2 deletions src/app/areas/economies-governance/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import Link from 'next/link'
import type { Metadata } from 'next'
import AreaFieldVelocity from '@/components/AreaFieldVelocity'

import EditPageButton from '@/components/EditPageButton'
import { PageEditHistoryByline } from '@/components/EditHistoryByline'
import AuthorCard from '@/components/AuthorCard'
Expand Down Expand Up @@ -255,7 +255,7 @@ export default async function FA2MainPage() {
</div>
</section>

<AreaFieldVelocity area="economies-governance" />
{/* Field velocity is preview-only until a separate public launch. */}

{/* Explore */}
<h2 className="text-sm text-gray-500 uppercase tracking-wide mb-6">Explore</h2>
Expand Down