diff --git a/app/globals.css b/app/globals.css index 7fa2de0..07809ff 100644 --- a/app/globals.css +++ b/app/globals.css @@ -19,3 +19,5 @@ @media(max-width:760px){.lab-brand{width:320px;max-width:100%}.lab-brand img{aspect-ratio:740/125;object-fit:contain}} .filters .field{min-width:140px;max-width:100%;flex:1}.filters .field.search{min-width:210px;flex:2}.filters select{width:100%;max-width:100%}.people-grid figure{margin:0}.people-grid figcaption{font-size:14px;margin-top:8px} + +.paper-card h2{font-size:22px;font-weight:500;margin:12px 0;line-height:1.35;max-width:820px}.person-card h2{font-size:19px;font-weight:500;margin:15px 0 5px} diff --git a/app/layout.jsx b/app/layout.jsx index 0ddefda..c248a29 100644 --- a/app/layout.jsx +++ b/app/layout.jsx @@ -6,7 +6,7 @@ export const metadata = { metadataBase: new URL(site.url), title: { default: 'Behavioral Data Science · University of Washington', template: '%s · Behavioral Data Science' }, description: site.description, - icons: { icon: '/images/logopic/Logo_Bdata_text_below.png' }, + icons: { icon: '/images/favicon.ico' }, }; export default function RootLayout({ children }) { return Skip to content diff --git a/app/page.jsx b/app/page.jsx index b14a1f7..a400c7d 100644 --- a/app/page.jsx +++ b/app/page.jsx @@ -3,6 +3,7 @@ import ReactMarkdown from 'react-markdown'; import Link from 'next/link'; import { papers, people, sponsors, pageContent } from '@/lib/content'; import PaperCard from '@/components/paper-card'; +export const metadata = { alternates: { canonical: '/' } }; export default function Home() { const selected = papers.filter(p => p.highlight).slice(0, 2); return <> diff --git a/app/team/page.jsx b/app/team/page.jsx index 5b7a850..e0bb453 100644 --- a/app/team/page.jsx +++ b/app/team/page.jsx @@ -3,12 +3,12 @@ import Link from 'next/link'; import { people, alumni, safeUrl } from '@/lib/content'; import PageHeading from '@/components/page-heading'; export const metadata = { title: 'People', alternates: { canonical: '/team/' } }; -function PersonGrid({ members }) { return
{members.map(p =>
{p.image ? :
}

{p.name}

{p.role}

)}
; } +function PersonGrid({ members, headingLevel = 3 }) { const Heading = `h${headingLevel}`; return
{members.map(p =>
{p.image ? :
}{p.name}

{p.role}

)}
; } export default function Team() { const members = people.filter(p => p.status === 'member'); const visitors = people.filter(p => p.status === 'visitor'); const mergedAlumni = [...alumni]; for (const p of people.filter(p => p.status === 'alumni')) { if (!mergedAlumni.some(a => a.name.toLowerCase().startsWith(p.name.toLowerCase()))) mergedAlumni.push({ name: `${p.name}, ${p.role}`, site: p.website }); } - return <>{visitors.length > 0 &&

Current Visitors

}

Past members

    {mergedAlumni.map(a =>
  • {safeUrl(a.site) ? {a.name} : a.name}
  • )}
; + return <>{visitors.length > 0 &&

Current Visitors

}

Past members

    {mergedAlumni.map(a =>
  • {safeUrl(a.site) ? {a.name} : a.name}
  • )}
; } diff --git a/components/paper-card.jsx b/components/paper-card.jsx index 7ab5d59..85de07c 100644 --- a/components/paper-card.jsx +++ b/components/paper-card.jsx @@ -1,9 +1,10 @@ import Link from 'next/link'; -export default function PaperCard({ paper, featured = false }) { +export default function PaperCard({ paper, featured = false, headingLevel = 3 }) { + const Heading = `h${headingLevel}`; return
{featured && {paper.image ? : {paper.year}}}
{paper.year} · {paper.venue || 'Publication'}
-

{paper.title}

+ {paper.title}

{paper.authorNames?.join(', ') || paper.authors}

{paper.status && paper.status !== 'published' &&

{paper.status}

} {paper.award &&

{paper.award}

} diff --git a/components/publication-explorer.jsx b/components/publication-explorer.jsx index 6866430..3be5bec 100644 --- a/components/publication-explorer.jsx +++ b/components/publication-explorer.jsx @@ -18,6 +18,6 @@ export default function PublicationExplorer({ papers, people }) { const reset = () => { setQuery(''); setYear(''); setAuthor(''); setAwardsOnly(false); setTopic(''); setVenue(''); setType(''); }; return <>
{[['Topic',topic,setTopic,topics],['Venue',venue,setVenue,venues],['Type',type,setType,types]].map(([label,value,setValue,options]) => options.length > 0 && )}

{results.length} {results.length === 1 ? 'publication' : 'publications'}

- {results.length ?
{results.map(p => )}
:

No matching publications

} + {results.length ?
{results.map(p => )}
:

No matching publications

} ; } diff --git a/docs/MAINTENANCE.md b/docs/MAINTENANCE.md index 0d76264..f6a586d 100644 --- a/docs/MAINTENANCE.md +++ b/docs/MAINTENANCE.md @@ -2,7 +2,7 @@ ## Status -The infrastructure is implemented on `codex/react-website-migration`. The current roster, initial Scholar profiles, author identities, and first local OpenAlex batch are configured. Candidate decisions, the maintenance pilot, and production deployment are still pending. No live schedule, signup test, or publication was triggered by this implementation. +The React site is live at from the `master` branch. The current roster, initial Scholar profiles, author identities, and first reviewed OpenAlex batch are configured. Scheduled discovery remains disabled pending reviewer ownership and the maintenance pilot. No live IdioFid signup test has been submitted. The site stays static: React/Next.js renders JSON and Markdown at build time. Python's standard library handles discovery and decisions. GitHub provides contribution forms, review branches, checks, artifacts, and deployment. There is no admin service or database to maintain. @@ -85,13 +85,20 @@ Run two weekly cycles before calling maintenance operational. Record false match ## Production switch and rollback -`deploy.yml` remains manual. Creating/pushing a migration or review branch does not deploy it. +`deploy.yml` remains manual. Creating or pushing a migration or review branch does not deploy it. -1. Finish the content pass and launch QA, including the IdioFid endpoint's ownership and one separately authorized test signup. -2. Review the migration PR and clean CI build; record the currently live revision and Pages settings. -3. Coordinate the approved merge with changing Pages Source to GitHub Actions. Run **Deploy React website** on the approved default branch. -4. Verify production routes, PDFs, logos, canonical URLs and the signup endpoint. Retain the old deployment revision/settings as a rollback reference. -5. For a bad React content change, revert the offending commit in a reviewed PR, merge, and manually redeploy. For failure of the initial Jekyll-to-React switch, restore the known-good pre-migration revision and prior Pages configuration together. +### Launch record + +- First React deployment: September 9, 2026, commit [`655d927`](https://github.com/behavioral-data/behavioral-data.github.io/commit/655d92724cf2bccc17adb5324ee4dcb7efcee3ff), [Actions run 34403337235](https://github.com/behavioral-data/behavioral-data.github.io/actions/runs/34403337235). +- Current Pages mode: GitHub Actions (`build_type: workflow`) on the canonical HTTPS domain . +- Known-good pre-migration revision: [`d6a7b92`](https://github.com/behavioral-data/behavioral-data.github.io/commit/d6a7b92de79642aee11c2c5bfe60c94588beae79). The prior Pages setting was the legacy publisher using `master` at `/`. +- Launch verification covered the home, team, publications, one preprint detail, IdioFid, and legacy `allnews.html` and `/vacancies/` routes. A representative publication PDF, the lab logo, `robots.txt`, `sitemap.xml`, and the custom 404 response were also checked on production. The IdioFid form itself still needs one separately authorized live submission. + +1. Run `npm run check` from a clean checkout of the approved `master` commit. +2. Run **Deploy React website** on `master` and wait for its GitHub Pages deployment job to succeed. +3. Verify production routes, PDFs, logos, canonical URLs and the signup form. A live signup submission requires separate authorization and must use a controlled address. +4. For a bad React content change, revert the offending commit in a reviewed PR, merge, and manually redeploy. +5. For a site-wide React deployment failure, restore the known-good pre-migration tree in a reviewed recovery commit, merge it, change Pages back to the legacy publisher using `master` at `/`, and verify the production domain. Do not rewrite `master` history. After launch, deployment on approved merges can be enabled by a small reviewed workflow change. Time-based content such as an expiring opportunity is filtered at build time; trigger a rebuild when its status/date changes or deadline arrives. A daily rebuild can be added after the lab approves production automation. diff --git a/scripts/check-export.py b/scripts/check-export.py index 05999d2..856d0d9 100644 --- a/scripts/check-export.py +++ b/scripts/check-export.py @@ -14,6 +14,11 @@ def check(root=ROOT): fragment_links = [] class Links(HTMLParser): def handle_starttag(self, tag, attrs): + if tag in ('h1','h2','h3','h4','h5','h6'): + level = int(tag[1]) + if self.heading_level and level > self.heading_level + 1: + errors.add(f'{self.file.relative_to(exported)}: heading jumps from h{self.heading_level} to h{level}') + self.heading_level = level for key, value in attrs: if key == 'id' and value: anchors.setdefault(self.file.resolve(), set()).add(value) @@ -32,14 +37,14 @@ def handle_starttag(self, tag, attrs): if not html_files: errors.add('No exported HTML. Run npm run build first.') for file in html_files: - parser = Links(); parser.file = file; parser.feed(file.read_text()) + parser = Links(); parser.file = file; parser.heading_level = None; parser.feed(file.read_text()) for source, target, fragment in fragment_links: if fragment not in anchors.get(target, set()): errors.add(f'{source.relative_to(exported)}: missing anchor {fragment}') for fragment in ['home','team','publications']: if fragment not in anchors.get(exported / 'index.html', set()): errors.add('Missing legacy homepage anchor: ' + fragment) - required = ['index.html','404.html','team/index.html','publications/index.html','allnews/index.html','vacancies/index.html','idiofid/index.html','awards/index.html','aboutwebsite.html','allnews.html','sitemap.xml','robots.txt','.nojekyll'] + required = ['index.html','404.html','team/index.html','publications/index.html','allnews/index.html','vacancies/index.html','idiofid/index.html','awards/index.html','aboutwebsite.html','allnews.html','sitemap.xml','robots.txt','images/favicon.ico','.nojekyll'] for collection, route in [('people','people'),('publications','publications')]: required += [f"{route}/{row['id']}/index.html" for row in json.loads((root / f'content/{collection}.json').read_text())] for name in required: