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
2 changes: 2 additions & 0 deletions app/globals.css

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion app/layout.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 <html lang="en"><body><a className="skip-link" href="#main">Skip to content</a>
Expand Down
1 change: 1 addition & 0 deletions app/page.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 <>
Expand Down
4 changes: 2 additions & 2 deletions app/team/page.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 <div className="people-grid">{members.map(p => <article className="person-card" key={p.id}><Link href={`/people/${p.id}/`}>{p.image ? <PersonPhoto person={p} loading="lazy" /> : <div className="person-placeholder" />}<h3>{p.name}</h3></Link><p>{p.role}</p></article>)}</div>; }
function PersonGrid({ members, headingLevel = 3 }) { const Heading = `h${headingLevel}`; return <div className="people-grid">{members.map(p => <article className="person-card" key={p.id}><Link href={`/people/${p.id}/`}>{p.image ? <PersonPhoto person={p} loading="lazy" /> : <div className="person-placeholder" />}<Heading>{p.name}</Heading></Link><p>{p.role}</p></article>)}</div>; }
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 <><PageHeading title="Our Group" /><PersonGrid members={members} />{visitors.length > 0 && <section className="section"><h2>Current Visitors</h2><PersonGrid members={visitors} /></section>}<section className="section"><h2>Past members</h2><ul className="alumni-list">{mergedAlumni.map(a => <li key={a.name}>{safeUrl(a.site) ? <a href={safeUrl(a.site)}>{a.name}</a> : a.name}</li>)}</ul></section></>;
return <><PageHeading title="Our Group" /><PersonGrid members={members} headingLevel={2} />{visitors.length > 0 && <section className="section"><h2>Current Visitors</h2><PersonGrid members={visitors} /></section>}<section className="section"><h2>Past members</h2><ul className="alumni-list">{mergedAlumni.map(a => <li key={a.name}>{safeUrl(a.site) ? <a href={safeUrl(a.site)}>{a.name}</a> : a.name}</li>)}</ul></section></>;
}
5 changes: 3 additions & 2 deletions components/paper-card.jsx
Original file line number Diff line number Diff line change
@@ -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 <article className={featured ? 'paper-card featured-paper' : 'paper-card'}>
{featured && <Link href={`/publications/${paper.id}/`} tabIndex={-1} aria-hidden="true" className="paper-art">{paper.image ? <img src={paper.image} alt="" loading="lazy" /> : <span>{paper.year}</span>}</Link>}
<div className="paper-copy"><div className="eyebrow">{paper.year} <span>·</span> {paper.venue || 'Publication'}</div>
<h3><Link href={`/publications/${paper.id}/`}>{paper.title}</Link></h3>
<Heading><Link href={`/publications/${paper.id}/`}>{paper.title}</Link></Heading>
<p className="authors">{paper.authorNames?.join(', ') || paper.authors}</p>
{paper.status && paper.status !== 'published' && <p className="tag">{paper.status}</p>}
{paper.award && <p className="award">{paper.award}</p>}
Expand Down
2 changes: 1 addition & 1 deletion components/publication-explorer.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,6 @@ export default function PublicationExplorer({ papers, people }) {
const reset = () => { setQuery(''); setYear(''); setAuthor(''); setAwardsOnly(false); setTopic(''); setVenue(''); setType(''); };
return <><div className="filters"><label className="field search">Search publications<input type="search" value={query} onChange={e => setQuery(e.target.value)} placeholder="Title, author, venue, or keyword" /></label><label className="field">Year<select value={year} onChange={e => setYear(e.target.value)}><option value="">All years</option>{years.map(y => <option key={y}>{y}</option>)}</select></label><label className="field">Lab author<select value={author} onChange={e => setAuthor(e.target.value)}><option value="">All authors</option>{people.map(p => <option key={p.id} value={p.id}>{p.name}</option>)}</select></label>{[['Topic',topic,setTopic,topics],['Venue',venue,setVenue,venues],['Type',type,setType,types]].map(([label,value,setValue,options]) => options.length > 0 && <label className="field" key={label}>{label}<select value={value} onChange={e => setValue(e.target.value)}><option value="">All</option>{options.map(v => <option key={v}>{v}</option>)}</select></label>)}<button className="filter-reset" onClick={reset}>Reset</button></div>
<div className="results-bar"><p className="results-count" role="status" aria-live="polite">{results.length} {results.length === 1 ? 'publication' : 'publications'}</p><label className="award-filter"><input type="checkbox" checked={awardsOnly} onChange={e => setAwardsOnly(e.target.checked)} /> Award-winning papers</label></div>
{results.length ? <div className="paper-list">{results.map(p => <PaperCard key={p.id} paper={p} />)}</div> : <div className="empty"><h2>No matching publications</h2><button className="button" onClick={reset}>Clear filters</button></div>}
{results.length ? <div className="paper-list">{results.map(p => <PaperCard key={p.id} paper={p} headingLevel={2} />)}</div> : <div className="empty"><h2>No matching publications</h2><button className="button" onClick={reset}>Clear filters</button></div>}
</>;
}
21 changes: 14 additions & 7 deletions docs/MAINTENANCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <https://behavioral-data.github.io/> 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.

Expand Down Expand Up @@ -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 <https://behavioral-data.github.io/>.
- 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.

Expand Down
9 changes: 7 additions & 2 deletions scripts/check-export.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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:
Expand Down