Source of the personal research and engineering site published at https://chengxuan-li.github.io/. The site is a static Astro build: one structured content model rendered as five views — Home (curated synthesis), Projects (technical case studies), Publications, CV (complete record), and News (activity stream).
Requires Node 24 (see .nvmrc; Node ≥ 22.18 works).
| Command | Purpose |
|---|---|
npm install |
Install dependencies. |
npm run dev |
Start the dev server at http://localhost:4321/. |
npm run dev:fixtures |
Dev server using the synthetic layout fixtures in fixtures/content/ (see below). |
npm run check |
Type-check .astro/.ts files. |
npm test |
Run the unit tests (schemas, validation, queries, formatting, build checks). |
npm run build |
Production build to dist/ (content cache cleared first), then post-build route/link checks. |
npm run build:fixtures |
Same build against the fixtures. |
npm run preview |
Serve dist/ locally. |
npm run validate |
check + test + build — run this before publishing. |
npm run brand-assets |
Regenerate the social-preview image, PNG icons, and fixture images from src/site.config.ts. |
Pushes to main run .github/workflows/deploy-pages.yml, which installs dependencies, runs npm run check,
npm test, and npm run build, uploads dist/ as the Pages artifact, and deploys it with the official
actions/deploy-pages action. The workflow can also be started manually from the Actions tab.
Two one-time repository settings:
- Settings → Pages → Build and deployment → Source: GitHub Actions.
- Settings → Environments →
github-pages→ Deployment branches: allowmain. GitHub creates this environment automatically and seeds it with whatever branch was default at the time, so a deploy frommainis rejected with "Branch main is not allowed to deploy to github-pages due to environment protection rules" untilmainis added (or the restriction removed).
The site is a user site, so it is served from the domain root; never configure a base path.
After a deployment, check https://chengxuan-li.github.io/, /projects/, /publications/, /cv/,
/news/, and one /projects/<slug>/ directly by URL.
All published facts live in src/content/ and are validated at build time (schemas in
src/lib/content/schemas.ts, cross-reference rules in src/lib/content/validate.ts, a raw-source date check in
src/lib/content/rawdates.ts). A broken reference, malformed or impossible date, invalid URL, duplicate
ordering value, or a DOI reused by two papers fails the build with a message naming the record.
Human-facing fields support bilingual authoring in the same YAML record. Existing English strings remain valid shorthand; add an adjacent optional Chinese value when a translation is available:
title:
en: Load Profile Inference
zh: 负荷曲线推断
summary:
en: Methods for inferring building load profiles.
# zh may be omitted; /zh/ displays the English value for this fieldThe English site keeps its existing routes. Simplified Chinese mirrors them under /zh/, and the header
language link preserves the current page. Interface labels are translated completely, while missing
record-level Chinese fields fall back independently to English. For project narratives, index.md is the
English body and an optional co-located index.zh.md supplies the Chinese body; without it, the Chinese
project route displays the English Markdown body.
Some fields are left in English on purpose. A field with no zh value is rendered with lang="en", so
the fallback is a deliberate editorial choice rather than an omission. Keep these consistent:
| Content | Convention |
|---|---|
| Publication and talk titles, venue names | Stay English. They are citations of published work; translating them breaks searchability and no longer matches the record. |
| Media headlines in news items, outlet names | Stay English. They quote an external article. |
| Organization, company, and society names | Stay Latin — EnergyAtlas.io, Nemetschek, Bentley Systems, Holcim, IBPSA-USA, NYSP2I, Architectural Association, Environmental Systems Lab. |
| Cornell University | 康奈尔大学 — the one institution with an established Chinese name in use here. |
| Author and person names | Never translated or transliterated. |
| Tool, language, and library names | Stay Latin — Python, C#, PyTorch, DuckDB, GeoPandas, SCADA, AMI, LiDAR. |
| Descriptive prose | Translated: bios, summaries, positioning, bullets, project bodies, skill concepts, award descriptions, degrees, locations. |
| Technical terms in Chinese prose | Gloss the English in parentheses on first use where the Chinese is ambiguous, e.g. 逆向建模(inverse modeling). |
Publication abstract values are stored but not rendered on any page, so they are not translated.
src/content/
├── bios/<version>.yaml title + biography text; each record becomes a CV tab
├── projects/<slug>/index.md Markdown case study + frontmatter; images sit next to index.md
├── publications/<id>.yaml
├── news/<YYYY-MM-DD>-<slug>.yaml
├── experiences/<id>.yaml type: research | professional | teaching | service
├── education/<id>.yaml
├── talks/<id>.yaml
├── awards/<id>.yaml
└── skills/<id>.yaml one category per file
- The file or folder name is the record's id: lowercase words joined by single hyphens. Ids come from the
path alone (
src/lib/content/entry-id.ts), so aslug:field inside a record does not rename it. - Unknown fields are rejected. Every record schema is strict, so a mistyped key fails the build naming
the file and the key (
organizaton,links: watch, …) instead of being silently dropped. - Every collection folder holds a
_template.*file (or_template/folder) documenting every field. Files and folders starting with_are ignored by the build. - Relationships are declared one way: publications, news items, talks, awards, and experiences list the
project_idsthey belong to; projects listrelated_project_ids. Reverse lookups (a project's papers, talks, activity) are generated. - Site-wide facts (name, role, affiliation, profile links, CV PDF links, meta description) live in
src/site.config.ts. Anullprofile link or emptycvPdfslist simply omits that link or button.
Link fields differ by record type — these are the only accepted keys:
| Record | Link fields |
|---|---|
| Publication | links: map with paper, preprint, code, slides, poster; plus a bare doi: (a DOI is rendered as a link but does not make the title clickable) |
| Talk | links: map with slides, video, event, abstract |
| Award | a single top-level url: |
| Project | links: list of { label, url, kind }, kind ∈ code, demo, paper, docs, data, other; optional videos: list with external HTTPS YouTube, Vimeo, or direct .mp4 URLs |
| News item | links: list of { label, url } |
| Experience, education | a single optional url: |
Talk type accepts invited, conference, seminar, webinar, poster, workshop, panel, other.
Publication type accepts journal, conference, workshop, preprint, thesis, report, abstract,
other. An optional top-level abstract field stores full abstract text for future use; the site does not
currently render that text.
- Typography: Geist Sans (Latin text) and Geist Mono (Latin labels, dates, navigation), plus Noto Serif SC
for Simplified Chinese characters, self-hosted from
@fontsource-variable/*packages (SIL Open Font License). Fonts are partitioned by Unicode range and fetched from the site's own origin; no external font requests are made. - Colour theme: the site follows the browser's
prefers-color-schemeby default. The sun/moon button in the header overrides it and stores the choice inlocalStorage(theme); choosing the theme the browser already uses clears the override. Logic insrc/lib/theme.ts, wiring inSiteHeader.astro, and a pre-paint inline script inBaseLayout.astro.
- Copy
src/content/projects/_template/index.mdtosrc/content/projects/<slug>/index.md. - Fill the frontmatter (
title,summary,status,start_date,end_date,types,topics, …). Setfeatured: trueand a uniquehome_orderto show it on the home page (about four projects fit). Setpublished: falseto keep the project and its assets in the repository while hiding its cards, cross-links, and generated route; remove the field or set it totrueto publish it again. - Write the case study body: Problem, System / Method, My Contribution, Technical Details, Results.
Put every project image (thumbnail, hero, inline figure, or video poster) next to
index.md; reference inline figures asand usehero_image/hero_altfor the card thumbnail. Setshow_hero_on_page: trueto also show that image above the project narrative; it defaults tofalse. Images are optimized at build time. Videos stay external: use the optional strictvideoslist with HTTPS YouTube, Vimeo, or direct.mp4URLs, required visible and accessible titles, and optional captions/posters. Direct MP4 records may setautoplay: truefor muted, viewport-aware looping andfit: coverfor a centered crop inside the 16:9 player; both options are rejected for hosted embeds. - Run
npm run build; the page appears at/projects/<slug>/and in the Projects index and CV.
Projects can use index.mdx instead of index.md to embed Astro components. Use one
entry file per project. index.zh.mdx is also supported for an optional translation.
Set presentation: paper, supply paper_pdf: /papers/<name>.pdf, and link exactly one
publication via its project_ids to use the paper
reading layout. ShadingZip provides the first complete example, including semantic
algorithms, KaTeX HTML/MathML, stable reference targets, and interactive SVG figures.
The paper manuscript remains English on the Chinese route, following the normal fallback.
npm run paper-data deterministically rebuilds the compact ShadingZip JSON/CSV datasets
from the preserved cap-300 source tables in references/shadingzip/plot_data. Commit the
generated datasets with their inputs. tests/shadingzip.test.ts checks point values,
bin normalization, case totals, and matrix means. Build checks also reject
missing or duplicate paper anchors. The full simulation archive is not required at runtime.
Figures render during the static build; JavaScript adds inspection, filtering, highlighting, and downloads. Printing restores the manuscript's default chart configuration. Chart data are numerical measurements; no interpolation, new clustering, or simulations run in the browser.
/news/ is a view, not a collection to maintain by hand. src/lib/content/activity.ts builds one
reverse-chronological stream from the records that already exist:
| Source | Enters the stream | Date used |
|---|---|---|
| Publications | Yes, unless status: in-preparation |
date, else year+month, else year |
| Awards | Yes | date |
| Talks | Yes | date |
| Manual news items | Yes | date |
| Projects | No — write a manual news item for a milestone worth announcing | — |
| Education, experience | No — same; add a manual news item if you want one | — |
So a paper, award, or talk is written once, in its own collection, and its title, date, and links stay
consistent everywhere. Never copy such a record into src/content/news/.
An entry's title becomes a link when its record offers a destination — a publication's or talk's first
entry under links (tried in a fixed order: paper, preprint, code, slides, poster for papers;
slides, video, event, abstract for talks), an award's url, or a manual item's first link. A
publication with only a doi and no links keeps a plain-text title; its DOI is still listed beneath.
The home page "Latest" section shows the four newest stream entries; a manual news item with
featured: false is kept off it. A project page's "Activity" section lists only awards and manual news
related to that project, because its publications and talks already appear under Outputs.
- Copy
src/content/news/_template.yamltosrc/content/news/<YYYY-MM-DD>-<slug>.yaml, for a software release, media coverage, a project or education milestone, or anything else that is not a publication, award, or talk. - Set
title,date(YYYY-MM-DD, orYYYY-MMwhen only the month is known),type, an optionalsummary, and the ids of related projects, publications, talks, or awards. Unknown ids fail the build. - The item appears on
/news/under its year and, unlessfeatured: false, among the four newest items on the home page and on the related project pages.
- Bios: copy
src/content/bios/_template.yamltosrc/content/bios/<version>.yamland fill the stricttitleandcontentfields. Every non-underscore YAML file becomes a version tab at the top of/cv/; file-derived ids are ordered with numeric awareness (1-sentence,50-words,100-words, …). - Education, experience, talks, awards, and skills each have a folder with a
_template.yaml. Experiencetypeselects the CV section;cv_orderpins a position, otherwise current roles sort first. - Publications: one YAML file per paper, listed in full on
/publications/and on the CV.statusandvenuemust reflect the real state (venue: nulluntil known). Setfeatured: true+home_orderfor the home page selection. Adddatewhen the exact publication day is known; it positions the paper in the news stream. researchInterestsinsrc/site.config.tsfills the one-line interests section; leave itnullto omit.- PDF CVs: copy approved public PDFs to
public/cv/and add their labels and root-relative URLs tocvPdfsinsrc/site.config.ts. Use web-safe filenames without spaces. Never link files fromreferences/archive/.
.github/workflows/deploy-pages.yml build + deploy to GitHub Pages
docs/specs/ the original implementation brief
docs/plans/ the implementation plan and its deviation log
docs/handoff/ current-state handoff for a new maintainer or agent
references/ source documents and their tracked extractions
fixtures/content/ synthetic records for layout checks (never real facts)
scripts/ check-dist, brand-assets, with-fixtures (Node .mts)
src/site.config.ts verified site-wide facts and toggles
src/content.config.ts collection definitions (loaders + schemas)
src/content/ the published content (see above)
src/lib/content/ schemas, validation, queries, formatting, the activity stream
src/lib/theme.ts light/dark toggle logic
src/components/ src/layouts/ src/pages/ src/styles/
tests/ Vitest unit tests mirroring src/lib
src/lib/content/ is where the model lives: schemas.ts (field shapes) → validate.ts (cross-record
rules) → queries.ts / activity.ts (selection and ordering) → load.ts (the single entry point pages
call). Components never read collections directly.
references/ is the tracked knowledge base that grounds every fact on the site — see
references/README.md.
references/archive/holds raw source documents (CV exports, manuscripts, bios). It is ignored by Git on purpose (only.gitkeepis tracked) so unpublished documents never reach the public repository.references/extracted/holds tracked, structured extractions (Markdown with YAML frontmatter, one per source record, indexed inextracted/index.md) with provenance and ambiguity notes.- Facts move
archive → extracted → src/content. Extraction is not publication; nothing goes intosrc/content/without a supporting extraction, existing repository content, or an explicit instruction.
npm run validateThis type-checks, runs the unit tests, builds the site (schema, cross-reference, and raw-date validation run
inside the build), and checks dist/ for missing routes, broken internal links, missing canonical URLs, and
any repository-name path prefix. For visual checks, run npm run dev:fixtures to see every layout populated
with clearly synthetic fixture data at mobile and desktop widths.
If a build ever reports that a cached record's source file no longer exists, the content cache is stale:
npm run build already clears it, and npm run dev -- --force does the same for the dev server.