Personal research website for Jens Sjölund, Assistant Professor of Machine Learning at Uppsala University. Live at https://jsjol.github.io.
Built with Jekyll on a heavily reduced fork of the
al-folio theme. Pushing to master
triggers the Deploy site workflow, which builds the site, purges unused CSS
and publishes _site to the gh-pages branch.
Requires Ruby with Bundler, and ImageMagick for the responsive image variants:
brew install imagemagick # provides `magick`
bundle install
bundle exec jekyll serve # http://localhost:4000The build needs nothing else — no Python, no Node. A clean build takes about five seconds and should print no warnings; treat any warning as a defect.
Before committing, format the sources:
npm install # first time only
npx prettier . --write| What | Where |
|---|---|
| About page | _pages/about.md |
| News items | _news/ |
| Publications | _bibliography/papers.bib |
| Research threads | _projects/ |
| Courses | _teachings/ |
| Group members | _data/people.yml |
| Social links | _data/socials.yml |
| Repository cards | _data/repositories.yml |
| CV | assets/pdf/cv.pdf, linked from _pages/cv.md |
| Citation counts | _data/citations.yml, regenerated by CI (see below) |
| Scholar exclusions | _data/scholar_ignore.yml |
Create _news/YYYY-MM-DD-slug.md whose only front matter is its date:
---
date: 2026-08-18
---
New paper at [Venue]: [**Title**](link). [One sentence.] Work led by [Author].News items are rendered inline in the news table on / and /news/, so they
get no standalone pages. Keep the filename date and the front-matter date equal.
Append a BibTeX entry to _bibliography/papers.bib. Beyond the standard fields,
the templates understand abbr, abstract, arxiv, award, award_name,
bibtex_show, code, html, pdf, poster, preview, selected, slides,
supp, video and website. selected={true} puts the entry on the front
page; preview={file.png} looks for that file in
assets/img/publication_preview/.
Nothing needs to be wider than 1400 px — that is the largest variant
_includes/figure.liquid puts in the srcset. Compress before committing:
magick in.png -strip -resize '1400x1400>' -quality 85 out.png
pngquant --quality=70-92 --strip --force --output out.png out.png- Deploy site — on every push to
masterthat touches site content. - Update publications — Mon/Wed/Fri, in two jobs of deliberately different trust levels. See below.
bin/sync_publications.py diffs the Google Scholar profile against
_bibliography/papers.bib. Run it with no arguments for a report:
python bin/sync_publications.py # report only
python bin/sync_publications.py --annotate # attach Scholar ids
python bin/sync_publications.py --append # add missing papersExisting entries are never rewritten. The bibliography is hand-curated —
preview in particular — so the script may only insert a google_scholar_id
line into an entry that lacks one, and append new entries at the end of the
file. Both are asserted afterwards: --append checks that the previous content
is a byte prefix of the result, and --annotate checks that the file grew by
exactly the inserted lines. No field value is ever touched.
The Update publications workflow splits this by how much review each half
needs. The refresh job updates citation counts and attaches Scholar ids, and
commits straight to master because both are mechanically safe. The propose
job appends papers that are on Scholar but not in the bibliography and opens a
pull request, because a new entry needs its venue and author list checked and
wants an abbr, a preview and possibly selected added by hand.
bin/update_scholar_citations.py takes about ten seconds here:
python bin/update_scholar_citations.py # needs `scholarly`, see requirements.txtIt reads the profile page directly — one request, standard library only, about
three seconds — and falls back to scholarly only if that fails. The old
scholarly-only path issued several requests per run, and Google answers
datacenter addresses with a CAPTCHA, which is why the scheduled workflow produced
nothing for seven weeks while reporting success. The direct path stands a better
chance from CI but is not guaranteed either, so the workflow warns on each
failure and turns red once _data/citations.yml is more than ten days old. When
that happens, run the command above and push.
The two paths were checked against each other: both return 107 records and 3444 citations, with no difference in any id, count or title.
Two things worth knowing:
- Detection costs no Scholar requests.
bin/update_scholar_citations.pyalready downloads the whole publication list with a single author-levelscholarly.fill(), so the sync only reads_data/citations.yml. It never callsscholarly.fill(pub)per paper, which is what triggers Scholar's CAPTCHA and stalls fetch-publications partway through the list. Metadata for a new paper comes from Crossref, and from the arXiv API when Crossref has no record — which is also why the generated BibTeX keys carry a real first-author surname rather thanunknown. - Scholar sometimes attributes someone else's paper to the profile. Add its
title or publication id to
_data/scholar_ignore.ymland it will not be proposed again. Supervised PhD theses are listed there too: they belong on the group page via_data/people.yml, not in the publication list. - OpenAlex is a second detection source, and is report-only. It works from CI even when Scholar does not, but it keeps separate records for the preprint and the published version of the same paper, leaves HTML in titles, and lists theses, so its findings are printed for a human to check rather than appended. Titles are matched exactly, then on a prefix (for Scholar's truncated titles), then on word overlap above 0.72 — which is what recognises "A linear programming approach to inverse planning in radiosurgery" as the entry already in the file under "… in Gamma Knife radiosurgery".
Where several Scholar records map onto one entry — the patent families all share titles like "Methods for inverse planning" — the script attaches no id and says so, rather than guessing which record belongs to which entry.
This fork has diverged from upstream al-folio deliberately, so updates have to be cherry-picked rather than merged. The substantive differences:
- The blog, bookshelf, CV-from-JSON, distill, repository-trophy, comment, newsletter and search features are removed, along with their layouts, includes, Sass partials, data files, assets and CDN libraries.
- MDBootstrap is gone. It cost 277 kB of CSS and 288 kB of JS to supply four
shadow rules, which now live in
_sass/_utilities.scss. _plugins/cache-bust.rbreplaces thejekyll-cache-bustgem, whose CSS digest hashed a directory that does not exist in this repository, so the?v=query string never changed and browsers served stale CSS.- An empty
title:in_config.ymlnames the site after its owner. Upstream requires the literal stringblankfor this and renders an empty<h1>and an empty navbar brand otherwise. _pages/people.mduses a CSS grid defined in_sass/_people.scssrather than Bootstrap row/column utilities.