Jekyll site for the Helsinki Social Computing Group, built for GitHub Pages.
Built on the real University of Helsinki Design System component
library
(@uh-design-system/component-library), loaded from Datacloud — the
no-build-step distribution channel, appropriate since this is a plain Jekyll
site with no npm build step. See _layouts/default.html for the three
<link>/<script> tags (component CSS, fonts CSS, loader script) and
_config.yml for the pinned ds_version. Datacloud only serves specific
versions (no "latest" alias), so bumping the library means bumping
ds_version by hand — check the package on
npm for
the current release.
Templates use the actual <ds-*> custom elements (ds-card, ds-link,
ds-button, ds-action-menu, ds-badge, ds-divider, ds-grid) rather
than hand-rolled markup — see _includes/nav.html and content/people.md.
assets/css/layout.css handles page composition only (header/nav/footer
layout, grid spacing) and reuses the --ds-* custom properties the
component library already defines at :root; it does not redeclare any
design tokens itself.
The footer carries the University of Helsinki crest
(_includes/uh-logo.html, included from _includes/footer.html), just
the emblem mark with no wordmark. It isn't part of the public component
library, so its SVG path was extracted directly from the real site's own
hy-icon-hy-logo component bundle. It's inlined as raw <svg fill="currentColor"> rather than <img src="...">, so it can actually
pick up this context's CSS color — an <img>-referenced SVG can't see
page CSS. .uh-logo__mark in assets/css/layout.css has a 2rem fallback
size so the crest never silently collapses to 0×0 (an <svg> with only a
viewBox, no width/height, does that as a flex child) if it isn't
otherwise sized.
Checked against the official rules at
https://www.helsinki.fi/en/brand-book/brand-and-logo: the emblem should be
"almost exclusively" paired with the "UNIVERSITY OF HELSINKI" wordmark,
never shown alone, and only ever black on light surfaces or white
(negative) on dark/photo surfaces. The current markup shows the emblem
alone (white, on the black footer), which departs from that "never shown
alone" rule — assets/css/layout.css still has .uh-logo--header/
.uh-logo--footer rules built for a wordmark-plus-emblem lockup (the
brand book's centred "basic format"), but _includes/uh-logo.html no
longer renders any wordmark markup for those rules to size or position,
so they're currently inert. The header doesn't show any UH logo at all.
All editorial pages live under content/. The main menu
(_includes/nav.html) is generated from that folder's layout, ordered by
each page's nav_order front matter (currently Home=0, People=1,
Research=2, Methodological development=3, Publications=4, Teaching=5 —
edit that field to reorder; pages without it sort last):
- A file directly in
content/(e.g.content/people.md) becomes a top-level menu link, rendered as a real<ds-link>anchor. This includescontent/index.mditself (the homepage), which shows up as "Home". - A subfolder would become a dropdown (
<ds-action-menu>/<ds-action-list-item>, the design system's own disclosure-menu pattern), with each file inside it as a popup item — seeassets/js/nav.jsfor thedsSelect-based navigation this requires, sinceds-action-list-itemhas nohrefof its own. Nothing currently uses this (Research is a single page, see below), but the mechanism is there if a section ever needs it.jekyll-sitemapis enabled so pages reachable only from inside a JS-driven popup would still stay crawlable.
Nav text (link labels, the "MENU" mobile toggle, the brand name) is
uppercased with Liquid's | upcase on the source string in
_includes/nav.html, not CSS text-transform. The ds-button/ds-link/
ds-action-menu components are Stencil "scoped" components, not real
shadow DOM (element.shadowRoot is null), but they still render their
label text through their own internal markup, which has its own
text-transform that wins over anything inherited from an ancestor — so
forcing the actual case at the template level is the only reliable fix.
The nav also sets font-family: var(--ds-fontFamily-body) explicitly on
.site-nav (Open Sans) — this one is just inheritance, already true by
default since body sets the same, but stated so the nav doesn't silently
depend on that.
_data/people.yaml— group members (name, image, description, alumni status, ORCID iD). Rendered oncontent/people.md, alphabetically by first name, current members separate from alumni. One-off data, not auto-updated. Photos are downloaded toassets/images/people/byscripts/download_people_images.py(re-run by hand after changing an image URL in this file).orcidis only populated for current members — it drives publication fetching (see below) — and left blank where no public ORCID record could be found (currently just Otso Rajala)._data/focus_areas.yaml— the group's two research focus areas (name,slug, banner image, description mirroring https://www.helsinki.fi/en/researchgroups/social-computing/projects). Slugs are short and simple (digital-society,dcm) — this is the cross-reference key:_data/people.yamllists which slugs a person belongs to (focus_areas: [slug, ...]— e.g. Matti Nelimarkka has both), and_data/projects.yamltags each project with one (focus_area: slug). Rename a slug and update it in both files to match. Rendered oncontent/projects.md(served at/research/— see below) as a hero-style block per area (large image left, heading/description/ researchers right — matching https://www.helsinki.fi/en/researchgroups/social-computing/projects, stacking to image-on-top on mobile), each withid="{{ area.slug }}"so/research/#dcmlinks straight to a section, followed by that area's projects. Researchers (current members whosefocus_areascontains the slug) show as a small photo (capped at 50px, see.researcher-chipinassets/css/layout.css) + name chip, not a text badge — missing photos (e.g. Jesse Haapoja) fall back to a plain grey circle. One-off data, not auto-updated._data/projects.yaml— research projects (name, description, funder, start/end year,focus_areaslug). One-off data, not auto-updated.content/methods.md(/methods/) is plain markdown, not data-driven — each method is just a##heading, a paragraph and a couple of markdown links, copied from https://www.helsinki.fi/en/researchgroups/social-computing/methodological-development. There used to be a_data/methods.yamlbacking it; it was folded straight into the page to make editing a method (or adding a new one) a single markdown edit instead of touching two files._data/courses.yaml— the manually curated, editable source for the Teaching page: a list of headers (currently "Digital society and politics" and "Digital and computational methods", matching https://www.helsinki.fi/en/researchgroups/social-computing/teaching), each with a list of courses. A course needs either acode(University of Helsinki course code, e.g.COS-D421— resolved daily against the course catalogue for its real link, dates, and current official name; anynamegiven alongside acodeis ignored, kept only as a trailing comment for human reference when hand-editing) or anameon its own, optionally with a directurl(used as-is, no lookup — most of the source page's course names don't map to a code we know)._data/teaching.json— generated daily byscripts/fetch_teaching.py(JSON, likepublications.jsonandblog_posts.json, for consistency —_data/courses.yamlis the only hand-edited course data), which resolves everycodein_data/courses.yamlagainst the course search API (the soonest upcoming offering, falling back to the most recent past one) while preserving the header groups. Do not edit by hand — edit_data/courses.yamlinstead. Note: that API wraps course codes in stray␟separator characters (e.g.␟COS-D421␟), so the lookup strips those before comparing. The Teaching page itself (content/teaching.md) renders this through_includes/course-list.html, which also decides whether to label each course "next teaching time" or "last teaching time" by comparing today's date against the resolvedstart_date._data/publications.json— a single JSON array, generated daily byscripts/fetch_publications.py, which fetches from the ORCID public API for every current member with anorcidin_data/people.yaml. A paper co-authored by more than one group member is written once — deduplicated by DOI (falling back to a slugified title for the rare work without one) — with anauthorslist naming every group member found on it. Each publication with a DOI also gets a full, untruncatedabstract: pulled from the same Crossref lookup already used for author matching where available (~60% of DOIs in practice, tags stripped), falling back to one batched Semantic Scholar Graph API lookup for any DOI Crossref missed. No DOI, or neither source has an abstract, means noabstractkey — same soft-fail spirit asblog_posts.json. Do not edit by hand. Powers the "Recent publications" list oncontent/index.md(5 most recent, sorted by full publication date where ORCID provides one; format: "Title: Venue (Year) [Author] [Author]", plus an abstract teaser —{{ pub.abstract | truncatewords: 30 }}— styled to match the "Recent blog posts" column right next to it),content/publications.md(all of them, same format without the abstract teaser, 25 at a time with a "Load more" button — seeassets/js/load-more.js), and the hand-rolled RSS feed atfeed/publications.xml(/feed/publications.xml— a plain Liquid template oversite.data.publications, not a Jekyll collection, since there's no per-publication page anymore)._data/feeds.yaml— a flat, hand-maintained list of RSS/Atom feed URLs. Add a URL here to have that feed's posts show up in "Recent blog posts" oncontent/index.md— no code changes needed. Currently just the group's Substack feed (https://sociallycompute.substack.com/feed — this is the actual feed behind "Recent blog posts" on the source homepage; found by inspecting the network requests the live site's blog widget makes, since the widget itself just points at an internal Drupal block id)._data/blog_posts.json— generated daily byscripts/fetch_blog_posts.py, which fetches every feed in_data/feeds.yaml, merges all their entries, and keeps only the 10 newest across all feeds combined (sorted by date, not per-feed — a feed that publishes less often never has a genuinely newer post wrongly dropped by a per-feed cap). Each feed is fetched independently and soft-fails to no posts (with a warning) if it can't be reached, rather than taking the others down with it. Do not edit by hand. Powers the "Recent blog posts" column oncontent/index.md(5 most recent)._data/cowork.json— a single JSON array, generated daily byscripts/fetch_cowork.pyfrom a private Notion database (https://www.notion.so/matnel/7f67eaa07e10418497bb03da9627d632) where Matti tracks individual research project and thesis topic ideas. Only rows with the "Available" checkbox on are included; each entry carries its Notion "Multi-select" categories ("Small project"and/or"Thesis topic"). Reading the database needs a Notion integration token with the database shared to it, set as theNOTION_TOKENsecret/env var — without it the script writes an empty list rather than failing the build. Do not edit by hand. Powerscontent/cowork.md(/work-with-us), which filters this one list into its "Individual research project" and "Master thesis supervision" sections via_includes/cowork-list.html, rendering each topic as a<ds-accordion>(header = title, content = description plus work-area/paid-position/Finnish-skill badges).
content/index.md otherwise mirrors
https://www.helsinki.fi/en/researchgroups/social-computing directly: the
same hero image and intro copy, the same three image link-boxes (Research
projects / People / Methods renewal, rendered as <ds-card>), and — where
the source page has "Recent blog posts" above "Recent academic
publications" stacked vertically — a two-column <ds-grid> with blog posts
and publications side by side. Images for all of this live in
assets/images/home/.
The hero itself (.hero in assets/css/layout.css) is full viewport width
— it breaks out of main.wrapper's centered max-width with the standard
width: 100vw; margin-left: calc(50% - 50vw) trick — with the photo as a
true background and the heading/tagline overlaid on top of a dark gradient
scrim for contrast (a horizontal gradient on desktop, since the text sits
on the left; a bottom-heavy vertical one on mobile, since the text stacks
at the bottom of the image there). The source page uses a fancier hy-hero
component with a colour variant that isn't part of the public component
library, so this is a hand-styled equivalent, not a 1:1 copy.
.github/workflows/pages.yml is the only workflow. On every push to main,
on a daily schedule (04:00 UTC), and on manual dispatch, it: fetches
publications, blog posts, teaching data and co-working opportunities fresh
into the checkout, builds the Jekyll site with that data, and deploys the
result to GitHub Pages. The NOTION_TOKEN secret (see _data/cowork.json
above) needs to be set in the repo's Actions secrets for the co-working
fetch to return real data; it fails soft (empty list) if missing.
The fetched data is never committed back to the repo — it only ever
lives in that run's build output, uploaded as the Pages deployment
artifact. This keeps the repo's history free of daily bot commits; the
tradeoff is that a fresh local checkout won't have current publications/
blog posts/teaching data until you run the fetch scripts yourself (see
below) — site.data.publications etc. are just empty/undefined until you
do, which Jekyll and Liquid handle fine (empty lists, not errors).
bundle install
bundle exec jekyll serveTo refresh the generated data by hand:
pip install requests pyyaml python-dotenv
python scripts/fetch_publications.py
python scripts/fetch_blog_posts.py
python scripts/fetch_teaching.py
python scripts/fetch_cowork.py # needs a Notion integration token — see below
python scripts/download_people_images.py # after editing image URLs in _data/people.yamlfetch_cowork.py needs NOTION_TOKEN set. Rather than exporting it by hand every
time, drop it in a .env file at the repo root (already gitignored, never
committed):
echo "NOTION_TOKEN=<your integration token>" > .envpython-dotenv (installed above) loads it automatically; if it's not
installed, the script just skips loading .env and falls back to
whatever's already in the environment.