A bilingual (English/Chinese) research group website built with Hugo and the theme-research-group theme. Deployed to xulab.pages.dev via Cloudflare Pages.
⚠️ GUIDING PRINCIPLE
Keep the code and the content base minimal.
Resist the urge to add features, pages, customizations, or dependencies unless strictly necessary. Every addition has a maintenance cost. When in doubt, leave it out.
For local development and testing changes before pushing to github:
academic package: pip install academicpython update_publications.py --local # reads the running Zotero app; or set ZOTERO_API_KEY and omit --local
academic import --bibtex "Exported Items.bib" content/en/publication/ --overwrite --compact
academic import --bibtex "Exported Items.bib" content/zh/publication/ --overwrite --compact
hugo server
Open http://localhost:1313 in your browser. The server auto-reloads on file changes.
To customize theme templates, vendor the theme first:
hugo mod vendor
Then copy templates from _vendor/github.com/HugoBlox/hugo-blox-builder/modules/blox-bootstrap/v5/layouts/ into layouts/ and edit there. Prefer CSS over custom layout overrides — only copy a theme template if CSS cannot achieve the same result.
All content exists in parallel under content/en/ and content/zh/. Changes to one language usually need a matching change in the other. Language config is in config/_default/languages.yaml.
The layouts/partials/blocks/people.html template implements a fallback mechanism: the Chinese site uses English author profiles as fallback, so only authors with actual Chinese translations need a content/zh/authors/<name>/ directory.
config/_default/hugo.yaml — site title, baseURL, taxonomies, build settingsconfig/_default/params.yaml — theme parameters (colors, features, contact info)config/_default/menus.yaml — navigation menu items (English)config/_default/languages.yaml — language settings and Chinese menuconfig/_default/module.yaml — Hugo module/theme dependencycontent/*/authors/<name>/_index.md + avatar.jpg — lab member profilescontent/*/publication/ — auto-generated from Zotero via Exported Items.bib (do not edit manually)content/*/post/<name>/index.md — blog postscontent/*/event/<name>/index.md — lab eventslayouts/partials/blocks/people.html — custom people/team display blocklayouts/partials/page_author_card.html — author card with avatar, bio, role on publication pageslayouts/partials/page_metadata_authors.html — author list in publication metadata with linking/highlightinglayouts/partials/custom_head.html — extra HTML head contentlayouts/shortcodes/alert.html — alert box shortcode (template placeholder)assets/scss/template.scss — custom SCSS (compiled by Hugo)assets/css/custom.css — additional CSS (processed through Hugo’s asset pipeline)The publication list lives in Zotero, not in this repo: the Publications collection of the xu-research-lab Zotero group. Every build fetches it fresh (update_publications.py → Exported Items.bib, untracked) and generates the pages under content/*/publication/ — do not edit those files manually.
noexport.jiangHighResolutionMicrobiomeAnalysis2025 → /publication/jiang-high-resolution-microbiome-analysis-2025/). Avoid duplicate items: a second copy gets a key with an a suffix and its own page.When the site picks up changes:
gh workflow run publish.yaml;master.To show/hide authors on the publications page, edit config/_default/hugo.yaml:
taxonomies:
author: authors # comment out to hide authors
Each member is defined by a folder under content/en/authors/<name>/ containing:
_index.md — profile data (name, role, bio, interests, education, social links, user_groups)avatar.jpg — profile photo (recommended: portrait aspect ratio)Author profiles follow a consistent Hugo Blox template format with these sections (commented out when empty): bio, interests, education, social.
The user_groups field determines which section the member appears in:
Faculty — professorsPostdoctoral ResearchersPh.D. student — doctoral studentsM.S. student — master’s studentsAlumni — former lab membersSet no_link: false to enable links to the member’s profile page, or no_link: true to disable.
Only create content/zh/authors/<name>/ if you have an actual Chinese translation (the English profile is used as fallback).
To remove a member: delete their folder from content/en/authors/ (and from content/zh/authors/ if a Chinese version exists).
To sort members: within each group, members are ordered by weight defined in their profiles. The weight value combines role priority (Postdoc = 30, Ph.D. = 20, M.S. = 10) with year. Use graduation year for alumni, or start year for current members, e.g., 102021. The sorting is configured in content/en/people/index.md.
Each post is a folder under content/en/post/<slug>/ containing index.md.
To add a post: create the folder and index.md. Set draft: true to hide it from the live site. Mirror under content/zh/post/<slug>/ if a Chinese version is needed.
To remove a post: delete the folder from both language directories.
The slider is configured in content/en/activity/index.md and content/zh/activity/index.md.
To add a photo:
assets/media/ (e.g. assets/media/group3.jpg)- title: "2025.9"
content: Lab retreat
align: end
background:
image:
filename: group3.jpg
filters:
brightness: 1.0
To remove a photo: delete its slide entry from both files (and optionally delete the image from assets/media/).
Events are listed at /event/. Each event is a folder under content/en/event/<slug>/ containing index.md.
A minimal event file:
---
title: "Talk: Title of the Talk"
event: Conference or Seminar Name
location: Room 503, Building X
date: 2025-09-01T14:00:00
date_end: 2025-09-01T15:00:00
all_day: false
abstract: "Brief description of the talk."
---
Mirror the file under content/zh/event/<slug>/ with a Chinese version.
Links are defined in content/en/_index.md and content/zh/_index.md as a markdown block:
- block: markdown
content:
title: Useful Links
text: |
- [**Link Name**](https://example.com)
Brief description
design:
columns: '1'
css_class: section-useful-links
Add, remove, or reorder items in the markdown list. Mirror the change in content/zh/_index.md.
Edit content/en/contact/index.md and content/zh/contact/index.md. The address, coordinates, and office details are set in the front matter of those files.
Edit config/_default/menus.yaml (English) and the menu.main section in config/_default/languages.yaml (Chinese). Each entry has a name, url, and weight (lower weight = appears first).
Replace assets/media/true.jpg, or edit the design.background.image.filename field in content/en/_index.md and content/zh/_index.md.
Replace assets/media/icon.png (keep the filename).
Edit content/en/authors/we-want-you/_index.md (and content/zh/authors/we-want-you/_index.md) to reflect current open positions.
Specific posts are encrypted after the Hugo build using StaticCrypt. The encrypted HTML is what gets deployed — the plaintext is never served to the browser.
The CI step:
staticrypt public/en/post/lab-nas-guide/index.html -p "$HUGO_SITE_PASSWORD" -d public/en/post/lab-nas-guide/ --short
The --short flag suppresses the interactive confirmation prompt in non-interactive CI environments.
To protect an additional post, add a line in the same form to the “Encrypt protected pages” step in .github/workflows/publish.yaml.
The password is stored as a GitHub secret named HUGO_SITE_PASSWORD. To set or update it:
HUGO_SITE_PASSWORD and Value to your chosen password.Pushes to the master branch, a daily schedule, or a manual run trigger .github/workflows/publish.yaml, which:
update_publications.py to fetch the Zotero group’s “Publications” collection into Exported Items.bib.academic import "Exported Items.bib" content/*/publication/ --compact to regenerate publication pages.--minify --baseURL "https://xulab.pages.dev/").wrangler pages deploy.xulab and complete the wizard.Navigate to Workers & Pages. The Account ID is the hex string in the dashboard URL:
https://dash.cloudflare.com/<ACCOUNT_ID>/workers-and-pages
Go to My Profile → API Tokens → Create Token. Use the “Edit Cloudflare Workers” template, or create a custom token with the “Cloudflare Pages: Edit” permission.
Go to Settings → Secrets and variables → Actions in the GitHub repository and add:
| Secret name | Value |
|---|---|
CLOUDFLARE_API_TOKEN |
The API token from step 3 |
CLOUDFLARE_ACCOUNT_ID |
The Account ID from step 2 |
HUGO_SITE_PASSWORD |
The site password for protected posts |
ZOTERO_API_KEY |
A read-only Zotero API key with access to the xu-research-lab group (zotero.org/settings/keys) |
The site’s search-engine verification and SEO signals live in four places:
layouts/partials/custom_head.html — injected into every page’s <head>:
google-site-verification) and Bing (msvalidate.01) meta tags.Person JSON-LD block (homepage only) for 徐振江 with sameAs links to ORCID, Google Scholar, Web of Science, GitHub, and Twitter. This binds the name query to the site as an entity.baidu-site-verification) slot, unused until the site moves to a China-reachable, ICP-filed domain (Baidu barely crawls *.pages.dev).static/BingSiteAuth.xml — root-level Bing verification file. Needed because the site root (/) is only a redirect stub to /en/, so Bing’s meta-tag check fails there; the XML file verifies against a standalone root URL instead.static/google5d6fb96f91cf1978.html — root-level Google verification file (a second method alongside the meta tag).config/_default/languages.yaml + content/zh/_index.md — a Chinese marketing.seo.description for the zh site (the theme default is English).Both verification files sit in static/ (not the repo root or public/) because Hugo copies static/ verbatim to the site root — that is the Hugo equivalent of “upload the file to your site root.”