Repository navigation
Say what we do in words people use — and fix what was under the copy - #25
Open
drmowinckels wants to merge 57 commits into
Open
drmowinckels wants to merge 57 commits into
drmowinckels wants to merge 57 commits into
Conversation
|
✅ Preview deployed at https://25--capro-uio.netlify.app |
drmowinckels
force-pushed
the
service-copy
branch
from
October 16, 2026 18:11
d6be710 to
32ce500
Compare
✅ Accessibility: no issuesaxe-core over 19 routes × 2 viewports (one page per layout), plus 69 manual checks. All 69 manual checks passed. Automated tooling catches roughly a third of accessibility problems. A clean run is a floor, not a ceiling. |
drmowinckels
force-pushed
the
service-copy
branch
from
October 16, 2026 20:02
32ce500 to
5f75afb
Compare
The old entries described a category. A researcher reading one could not tell whether their problem was in it, whether we would do the work or advise on it, or what they would end up holding. Each of the seven now leads with the problem the reader arrived with, lists the situations that produce it, and says what you get. The category name stays as a secondary label, so someone who does know the term can still find it. Named systems do a lot of the work: someone who has never heard "data engineering" knows what "I have a Nettskjema and can't get the data out" means. Every name used already appears elsewhere on the site. Also fixes a pre-existing inconsistency where the English list had Workshops but no Data capture, and the Norwegian had Datafangst but no separate processing entry. The two languages advertised different services; both now carry the same seven. Drops the duplicated skipToContent block in both i18n files. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
"Data flow and processing 90%, Registry data handling 55%" told a reader nothing they could act on, and the numbers were not sourced from anything. A visitor trying to work out whether we can help them cannot use a percentage. It is now three plain lists — the languages and tools we use, the UiO and national systems we work inside, and the kinds of data we handle. Someone with MRI data on TSD can now recognise themselves on the page, which is the whole point. Drops the now-unused progress bar CSS. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
People kept arriving unsure whether we could help them, and the copy was a large part of why. The site described itself in terms a reader only understands if they already know what we do: "data engineering", "data science team", "FAIR principles", "containerization", "integrated solutions", "standardized data output". Hero and contact: the hero now names the concrete things we do — tidy up messy data, set up collection, make analyses run twice, get jobs onto university servers, teach groups to do it themselves — rather than "capture, process, manage and analyse". Contact says you do not need to know the technical name for your problem, because that is the actual barrier. Feature section: "Integrated Solutions / Standardized Data Output / Customization on Demand" said nothing a reader could act on. It now answers what working with us is like — we use systems UiO already pays for, you own what we build, we can teach instead of doing, and you can start with a single hour. Pricing: explains that the rate depends on how close you are to the department, and that computing time is applied for separately. The bullets describe what you get rather than listing line items. CTA, portfolio, team and testimonials follow the same rule: ordinary words, concrete claims, and an explicit invitation to ask even if your problem is not on the list. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
drmowinckels
force-pushed
the
service-copy
branch
from
October 19, 2026 10:09
5f75afb to
254bb17
Compare
Five lines of body copy over a photographic background is hard to read, and the list of services below it already carries the detail. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
"What you get" was a hard-coded h4 under a problem heading that is an h2 on /services/, so every item skipped h3. axe flagged 28 instances across the two language versions. The label now tracks the item level the same way the problem heading does, so the pair stays one step apart whichever level the section sits at. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The copy claimed "anyone at UiO can ask", which told a public agency or a company that the site was not for them — while the pricing table has had non-commercial and commercial rates all along. The two contradicted each other, and the copy was the wrong one. UiO stays the primary audience, since that is where most of the work comes from, but the hero, services intro, contact, CTA and pricing now all say plainly that public agencies and private companies can hire us. "We use what UiO already pays for" also assumed a UiO reader; it now covers working inside whatever systems you already have. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The hero ended on who we do and don't work for, and the contact and CTA both ended on who gets charged. Neither is what someone reads those for, and the "free for some, billed for others" construction read as a disclaimer rather than an invitation. Getting in touch is free for everyone, so saying otherwise next to the contact form was also just wrong. The rates live on the pricing page, which is where someone goes looking for them. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Seven identically shaped blocks down the left half of the page, with the right 40% empty, gave a reader nothing to navigate by and no sense of where one item ended and the next began. Each item is now a two-column grid: the problem, its category and a numeral on the left, the evidence and the payoff on the right. The numeral is decorative and aria-hidden — the items are not a sequence and nothing refers to them by number; it is there to give the eye something to count. "What you get" picks up an accent rule, since that is what a reader is scanning for. Collapses to one column below 900px. Also fixes the h1 promotion under `hugo server`. section-title.html marks the page's h1 as used via Scratch, which outlives a render, so after the first rebuild the flag stayed set and no page emitted an h1 again. Production builds render each page once and were unaffected, so this only ever showed up locally. baseof.html now clears it where each render starts. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
"Real projects, and what we actually did on each one. If something here looks like your situation, say so when you get in touch." read as client case studies. Everything listed is open source or openly released science that anyone can pick up without hiring us, which is a stronger thing to say and happens to be true. The nav label "Our work" carried the same wrong implication; it is now "Open source". ggseg becomes ggsegverse, covering the whole family of packages rather than the one entry point. /portfolio/ggseg/ redirects to the new URL. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
--color-primary (#c04545) is a background fill. Used as a foreground it lands at 2.68:1 on the grey sections — under the 3:1 WCAG asks for meaningful non-text content, and nowhere near the 4.5:1 for text. The palette already carries --color-primary-text (#e57575, 4.58:1) for exactly this. axe does not catch it: it has no text node to measure on an <i> icon, which is how all five instances were styled. Found by walking every rendered element and comparing its computed colour against its resolved background. Feature and contact icons, the testimonial quote mark and two team links move across. The quote mark also goes from opacity-40 to 60, since 40% of an already-dim red was close to invisible. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
og:image was the site logo resized to 512px tall, paired with twitter:card=summary_large_image. Platforms crop that to roughly 1.91:1 and render it large, so a transparent mark arrived floating on whatever background the platform chose, at the wrong aspect ratio. Every page now composes its own 1200x630 card on the brand's dark ground: the page's own image when it has one — a team portrait, a package logo, a post's figure — and the CAPRO mark when it does not. Remote images are skipped deliberately, so a build never depends on someone else's host being reachable. Also adds what was missing around it: og:site_name, image width/height and alt, og:locale:alternate for the other language, article published and modified times, and explicit twitter:title/description rather than leaving them to fall back. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Every card's call to action reads "Read more", so tabbing through the news or portfolio list gives no way to tell them apart. The post title now follows it in a screen-reader-only span. The card image was built at 300px and stretched to roughly 400 by w-full, so it was upscaled on every screen and soft on high-density ones. It now ships 1x and 2x sources with sizes, plus width and height so the card stops reflowing when the image lands. Lighthouse on /news/: SEO 92 to 100. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
axe-core passes clean on all 58 pages, but it reports colour-contrast as "incomplete" wherever text sits over a photograph, and it cannot see focus indicators at all. Measuring those by hand found four AA failures: 1.4.11 Non-text contrast. The focus ring was the fill red, which is 1.00:1 against the skip link, 1.06:1 against .btn-primary and 2.68:1 against the page. On the two most important targets on the site it was invisible. White with a dark halo clears 3:1 on every surface the ring lands on, including Leaflet's white map controls. `.btn` also used `transition: all`, which animated the ring in over 200ms, so the first frame of focus was the browser default; the transition now names its properties. 1.4.3 Contrast. The CTA band composites its photo at 70% rather than 60%, matching what banner.html already did. The paragraph measured 3.85:1 with 42% of its area below AA; it now measures 5.67:1 with none. 3.2.2 On Input. The language `<select onchange="location=this.value">` fired on every arrow key, so a keyboard user could never reach the second language. It is now two links, which also drops an inline-JS dependency and carries hreflang. Ordered by site.Languages, since AllTranslations is not sorted and the pair swapped between pages. 2.5.8 Target size. Nav links, the nav group button and the team social links measured 20-28px tall; all now have a 24px floor. The byte work is the same kind of gap. banner.html resized its background to 1600px webp; feature.html and cta.html emitted .RelPermalink, so /services/ transferred the 3500px originals — 8.2 MB. Leaflet was gated on site.Params.map, a site-wide param, so 144 KiB of JS shipped to all 58 pages for a map on one. The hero preload fired on every page although baseof.html renders the banner only on the home page. The logo was a 1624x599 PNG served at 130x48 with width="auto", which is not a valid HTML width. FontAwesome imported a 'regular' weight no glyph on the site uses. Favicon declarations were a PNG labelled image/x-icon under the legacy relation, leaving /favicon.ico to 404 — the only thing costing best-practices points. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
With the oversized photographs gone, the header logo is what Lighthouse now picks as the largest contentful paint on most interior pages, and it flagged the missing priority hint. LCP there is bound by render-blocking CSS rather than bytes, so this is a small effect; the local score is bimodal between 2.6s and 4.6s either way. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Three placeholders that had been rendering literally.
Every team page announced "About Member" in 36px, and its two section
headings read "Posts by Member" / "Publications by Member". All three came
from `index (split .Params.single " ") 1` against the team section's
`single: About Member`. The person's name was meanwhile buried at text-xl
inside the figcaption. The name is now the page's h1, styled like every
other page's section title, with `text-transform: none` so that CSS
`capitalize` does not rewrite "José-Luis Alatorre-Warren" at the hyphen.
The portrait's alt is now empty because the name sits immediately above it;
repeating it made a screen reader say the name twice.
The footer said "Copyright © 2022" on every page of a site whose newest
post is from 2025. The year now comes from site.Lastmod rather than from a
translated string, so it tracks content instead of drifting. CI checks out
with fetch-depth 0, so enableGitInfo gives a real date there.
The home help grid drew its hairlines by painting the container and letting
the cards cover it, so seven items in three columns left cells 8 and 9
filled — a slab that read as a missing card. The hairlines move onto the
cards; empty cells now fall back to the page.
The same grid also broke the hero's promise. It headed each card with
`name` ("Large-scale computing") immediately after a hero saying you do not
need to know the technical name, while `problem` ("The analysis is too big
for your laptop.") sat unused in the same data file — the line /services/
already uses as its headline. Cards now lead with `problem` and demote
`name` to a subtitle. Not another tracked-uppercase label: the theme has
six of those already.
While in team/single.html: the social links derived their aria-label from
the last hyphen-segment of the icon class, so "ti-twitter-alt" announced as
"alt". They now name the person and the host.
axe-core still passes 0 violations across 120 runs, and every page still
has exactly one h1.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
**Contact.** The step the site exists to produce read value-then-label, sat on the same bg-dark as the team section above it, and said nothing about what you get for writing. The note moves above the address, the section moves to bg-grey so it has its own ground, and three reassurances sit under it. Each is sourced from copy already on the site — the first conversation is priced at 0 for UiO in content/pricing, and "someone who does the work, not an account manager" is the team page's own line. No reply-time commitment: that would be inventing a promise. **Stable ordering.** people.html, team.html and testimonial.html all ranged `| shuffle`, so the home strip never matched /team/ and the order changed on every deploy. Now weight, then title. **The heading split.** section-title.html cut every title after word one, which broke the compound in "Open | Source we have built" and left a lone grey "R" on a tag page with an empty accent span beside it. The split point is now a caller option (portfolio passes 2), and a single-word title is accented in full. **Term pages.** Headed by the bare term, no count, no way out. Now "Tagged R" / "Filed under MRI" with a post count and a link to all news. **Portfolio.** The filters had no group label, no pressed state and no announcement, so pressing one silently removed cards; they now carry role=group, aria-pressed and a live count, and their styling keys off aria-pressed rather than a parallel Alpine :class that could drift. The hover overlay held a second link to the same URL that was invisible on touch and focusable while transparent — it is decorative now, the title carries the link, and the overlay also reveals on :focus-within. Descriptions were `truncate 120`, cutting mid-clause; they take the first sentence instead. **Testimonials.** The nav buttons sat at left/right: -20px, on top of the first and last card's text below ~1480px. They move above the track, they disable at the ends instead of scrolling into nothing, and the step is measured from a real card rather than a hardcoded 392px. **Hierarchy.** .section-title h1 and h2 were the same size, so page title and section title were indistinguishable. Prose h2/h3/h4 now differ in size and colour and carry top margin; they were all teal and glued to the paragraph above. **The chart.** content/skill/growth.png was six curves over dashed gridlines with no axes, labels or units, beside the list of tools we actually use, on a site whose product is trustworthy data. Removed; the list takes the full width in three columns. **Side-stripe.** .svc__deliver was a 2px red left border, the one genuine instance of the pattern in the theme. It is a bordered, tinted callout now. (.icon-box keeps its 2px sides — those are the hexagon's edges, not an accent stripe.) **English on a bilingual site.** 404.html and the whole ORCID publications UI were hardcoded English. Both translated; 404 also stops being a dead end and leads with the message rather than the status code. **Card images.** `img.w-full` stretched a 262x302 source across a 400px card. Capped at the intrinsic width. axe-core: 0 violations across 120 runs. Every page has exactly one h1. Desktop Lighthouse 99-100 across performance, a11y, best practices and SEO. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The services answer "can you help with my problem". They do not answer "do you do the specific thing I came for", and that is how most would-be clients arrive — asking about MR-based research, or about Nettskjema, by name. CAPRO_services.docx frames GOAT, MRI, statistical analysis and Bevex as four pillars. They are not a service taxonomy: two are named products, one is a research domain, and one is already the "Processing and analysis" service under a different name. Making them the spine would leave work that fits none of the four homeless. So they go in as a second layer instead. The seven problems stay the spine; a short block under them names the things a visitor already has a word for, on both the landing page and /services/. Nettskjema is in that block although the document does not mention it, because it is the one name clients were reported to ask about most. Two of the document's claims are not ours to make yet, and are not made here: - We do not offer MR scanning. The document describes collection through LCBC's MRI-trained staff at Oslo University Hospital; that is their capacity, not a CAPRO service. The card starts from data that already exists, whoever collected it. - Bevex is not advertised. It is in testing, beta is expected in spring 2027, and a site should not sell what it cannot deliver. The card describes what we can do today — build browser-based experiments in JavaScript — and mentions the platform as in development, with no date. Also fixes a bug in this branch's own CSS: markdownify returns text without its wrapping <p>, so `.svc__deliver p` never matched and "What you get" rendered unstyled. The paragraph is now written in the template. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The page said "Open source we have built" and listed four things. The organisation has twelve worth showing: uiothemes, tsdrtools, neuromat, ebrains.kg and nva in R, jsPsych Studio and the Nettskjema data inspector on the web, and the R project-management workshop materials. Someone judging whether we work in the open was judging us on a third of the evidence. The reason the rest were missing is that the card template required a local image resource, and only one of the new entries ships a logo. Rather than hunt for or invent logos, the image panel goes: each card is now the categories, the linked name and a sentence. The filter chips above already carried the same categories, so they are not new information — they are the anchor the card lost with the logo. Two new categories, Web apps and Teaching, since R-packages and Analyses did not cover what was being added. Four logo files are deleted with the panel. Nothing referenced them any more, and this branch has already been removing bytes that ship without being used. Also fixes the blurb fallback: .Plain returns the rendered HTML's entities, so "Norway's" reached the card as the literal "’" once an entry had an apostrophe in it. None of the original four did. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`--panicOnWarning` turns the Hugo 0.156 deprecation of `.Site.Languages` into a hard error, so the strict build in CI fails before it renders the taxonomy pages. Both language switchers used it, for one property: the ordering by configured weight, which `AllTranslations` does not give. `hugo.Sites` is ordered the same way, so the walk is unchanged apart from reaching the code through `.Language.Lang`. Verified the switcher still renders English before Norsk on both language trees, with `aria-current` on the right entry. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two sections sat on pages whose subject they did not match. The testimonials were the closing section of /portfolio/, under the heading "Open source we have built". The quotes are about working with us on projects — a database built over fifteen years of collection, workshops, a data structure someone can finally use — not about released code. They move to /services/, straight after "What working with us is like", where they are the evidence for the claim immediately above them. The CTA asks "Not sure your problem is on the list?" and invites you to describe the problem in ordinary words. On /services/ that list is right there. On /pricing/ there is no list, and the page already closes on its own ask — "You can always ask what something would cost before committing to anything" — so the block was both a non sequitur and a second ask. It goes. /portfolio/ now ends on the projects, which is what a reader came to that page for. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Same mismatch as /pricing/: "Not sure your problem is on the list?" after a page about who works here, where there is no list. The skills section already closes on its own invitation — "If something you use is not on this list, ask anyway" — so the page keeps an ask, and a better-aimed one. The CTA now appears only on /services/, which is the page its copy was written for. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The page was flat for structural reasons, not stylistic ones. Seven items rendered with identical geometry, one hairline between each, nothing larger than 1.9rem and nothing smaller than 0.72rem, and in seven screens of scroll the only colour was a red icon and a 7%-red box. Three things were scaffolding rather than information: - The numbers. The old comment said it outright — "the number is rhythm, not information, the items are not a sequence and nothing refers to them by number". Rhythm is what this commit gets from the layout instead. - The "What you get" label. Seven of them, and the box under each made the heaviest element on the page also the most repetitive, so the emphasis stopped registering as emphasis. - The tracked-uppercase category. Two eyebrows per item, fourteen down the page. The category stays, in sentence case, for the reader who knows the term; it no longer competes with the problem above it. What replaces them: - The problem statement is the one piece of scale, up to 3.25rem, with the measure capped at 19ch so it breaks into two or three lines and reads as a statement rather than a table row. - Items alternate which edge they sit on above 900px, and the hairline under each is only as wide as the item, so the stagger is visible before any of the text is read. - The payoff is the largest body text in the item and the only teal on the page, marked with an arrow rather than a label and a filled box. Gold (`--color-tertiary`) stays unused. It is defined in the tokens and appears nowhere on the site; introducing it on one page would orphan it. Checked at 1280 and 390, both languages. Contrast holds: the symptom text is 4.9:1 on the section background, the teal payoff and the category well above. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two pairs were describing one problem each. "Data capture" and "Data engineering" both answer "I cannot work with my own data". The split was ours — before collection versus after — and the reader arriving with a Nettskjema they cannot get the data out of does not know which side of it they are on. One item now covers both, and the payoff says both halves: data put where it is allowed to be, and one documented dataset out the other end. "Processing and analysis" and "Large-scale computing" both answer "the analysis does not work". One is "it gives a different answer each time", the other "it never finishes", and the same packaging and version control fixes the first while the job scripts fix the second. One item, four symptoms covering both failures. The order was arbitrary. It now follows when the reader actually hits each problem: the data management plan first, because that is grant-application stage and before any data exists; then collecting it, then getting the analysis to run, then seeing the state of the project, then learning to do it without us. Norwegian carries the same five in the same order, so the two languages do not advertise different services again. The landing page renders these as cards, and five in a three-column grid left a hole in the second row. At exactly five the grid runs six columns and the last two take half the final row each; a sixth service restores the plain three-up. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
His bio had him on fMRI analysis and HPC, which is the back half of the job. He also sets up the MRI side of a study: working with the physicists at the hospital to keep protocols valid and current, and keeping collection running once it starts. "MRI protocols" joins his roles. This is his expertise, not a scanning service — the services page still starts from scans that already exist, whoever collected them. Protocol development was already listed there, and this is the person it rests on. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Read all four UiO profiles and rewrote from them, so each bio now names real work rather than gesturing at competence: - Mo ran data processing and management for the EU Lifebrain consortium, leads CAPRO, wrote ggseg, ggseg3d and nettskjemar, and teaches research methods at the department. - Inge has worked on Lifebrain, UK Biobank, the Human Connectome Project and ABCD — which says more about what he can handle than any adjective. - Markus studies visual memory with fMRI and psychophysics, which is the research behind the MRI setup work. José-Luis's own text is unchanged: his UiO page is an unfilled template, still carrying literal "<Task 1>" placeholders, so there was nothing to draw on. Worth him filling in, and then this can be revisited. Each bio keeps the closing line naming what that person can be asked for, and each still needs its subject's sign-off. Also drops the comment above uio_profile. The field name says what it is. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Replaces the old landscape photo with the 2024 staff portrait from his UiO page. The original is a 4432x6647 PNG at 28 MB; this is resampled to 1400px on the long edge and saved as JPEG at quality 85, which lands at 728K — in line with the other portraits in the repo and far more than the 600px square the templates crop to. Photo by Erik Engblad. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Silje is an adviser at LCBC. Her bio is from her own description, kept close to her words — data collection planned end to end, a tidy and documented process, and the participant having a good time being tested — with the title and background from her UiO page. She needs to approve it like the others. Photo credits are now a field. Photographers have a right to be named and a staff portrait is someone's work, so `photo_credit` renders under the portrait on the person's page wherever one is recorded. Only José-Luis's is filled in, from the filename of the UiO original: Erik Engblad. The other four need whoever took them; the field simply renders nothing until then. Both people grids wrap and centre instead of running a fixed four columns. At five people a four-up grid left a hole exactly the width of a card, and the next person to join would have moved the problem rather than fixed it. The landing page also got the no-upscale guard the team page has. It filled to 500x500 unconditionally, and Silje's portrait is 133px on its short edge. Her photo is the only one UiO publishes and it is small: 133x200, rendering at roughly 2.4x upscale in the grid. It wants replacing with a proper portrait. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The cards printed the whole bio. Each person now has a `summary` in front matter, which .Summary picks up without a template change — one line saying what they do, so the cards are scannable and the same height. The bio stays in full on the person's own page, where someone has asked for it. The tags were three different kinds of thing at once: "R" (a tool) beside "Workflow" (a way of working) beside "Developmental and evolutionary neuroscience" (a research field, and three lines in a card). They are now one register — a capability someone could ask for, one to three words, no "and" chains, at most one tool name each — and where a tag matches a service it uses the service's own words, so "Large-scale computing" reads the same on both pages. Silje is a research adviser, which is the accurate title, and her photo is the press portrait rather than the 133px thumbnail: 1063x1600, no resizing needed. Hugo's Smart crop anchored low on it and cut the top of her head off, so `photo_anchor` is now an optional override, defaulting to Smart, applied in all three places a portrait is cropped. Hers is set to Top. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The page was a narrow centred column. That put the bio in centred serif, which is fine for a line and poor for a paragraph — five lines with a ragged left edge — and left two thirds of a wide screen empty. Photo on the left, everything else on the right and left-aligned. It stacks on narrow screens. The portrait is now square and grey, the same treatment as the card that links here. It was round and in colour, so clicking a card landed you on something that did not look like the thing you clicked. `role` is a new field, because the page never actually said what the person is. It was buried mid-sentence in the prose. No unit on it — everyone here is at the same one. Also gone: the two hairlines that bracketed the single bio paragraph, and the dead end at the bottom. There is a link back to the team now. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
His page was an unfilled template last time, still carrying "<Task 1>" placeholders, so his bio was the only one not written from source. It now has his background and his work, and the bio follows: - A PhD in evolutionary biology on top of applied physics and mechatronics, rather than the vaguer "BSc in engineering". - Diffusion MRI across humans and animal samples, which is the specialism the page leads on. - Early brain development, with collaborators at Harvard Medical School and Boston Children's Hospital. The card summary moves with it, from the generic "the analysis that makes sense of the scans" to what he actually works on. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Statistics was the last word in his tag list and a conditional at the end of the bio, which buried the thing he is best at. It now leads in all three places the page uses: first tag, the card summary, and the opening line. The MRI stays, demoted to what the modelling sits on top of rather than the subject in its own right. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ress The data has come from OpenAlex for a while, but everything around it still said ORCID: the file was orcid-pubs.js, every element id was orcid-pubs-*, and the only thing ORCID still provides is the id used as the lookup key. Anyone debugging this would have gone to the wrong API first. Renamed to publications.js, with a comment saying where the data comes from and why — OpenAlex indexes the whole career, including work published before someone came to UiO, which the institutional list does not. The OpenAlex polite-pool mailto was a personal work address, hardcoded and shipped in the bundle on every page load. That sat badly beside the decision to keep emails off the team pages: they were out of the HTML and one was in the JavaScript anyway. It is the shared inbox now. Renaming the select's id left the label's `for` pointing at the old one, which would have broken the association. Caught before pushing. Verified against the live API: 87 publications, sixteen years in the filter, no error state. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Removed along with its styles. The nav and the footer both reach the team, and the browser's back button is what people actually use. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
tachisto has a brand package now. Its LOGOS.md says the wordmark is the primary mark for light grounds, which is what the card panel is, and that the inks are fixed so the file has to match the ground rather than be recoloured. It is an SVG, and Hugo cannot run Fit, Resize or Fill on one. Four templates called those methods unguarded, so adding the file broke the build four times over, each in a different place: the portfolio card, the article card, the social-card partial and the default single page. Each now checks reflect.IsImageResourceProcessable first. The three that display the image pass the vector straight through — there is no 2x of an SVG and no derivative worth making. The social card cannot: Open Graph needs a raster at an exact size, so a vector subject falls back to the site logo instead of failing the build. This is the fix for any future SVG logo, not just this one. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The card carried the ggseg hex, which is one package inside the family the entry describes. ggsegverse has its own wordmark; this is the light-ground file, checked by eye rather than by filename, since "light" names the ink in some brand packages and the ground in others. Here the ink is dark, which is what the white panel needs. Also commits the Tailwind bundle. The previous commit added `max-w-[400px] w-full` to the single template, which generates a new utility, and I staged the templates without it — so CI's stale-bundle check failed, exactly as it is meant to. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The Norwegian portfolio page listed both "R-packages" and "R-pakke" as separate filters, because the nine entries added earlier carried the English category names into the Norwegian files while the three older ones used Norwegian. Picking either chip hid the entries filed under the other — eight of them under "R-packages". The chips are user-facing text, so they are Norwegian on the Norwegian page: R-pakker, Analyser, MR, Undervisning, Webapper. Five in each language now, matching one to one. The wording wants a look in the translation pass; the split was the bug. Also tidies the "key : value" spacing the older entries used. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The entry was five years old and generic: "a family of R packages for plotting brain atlas data", plus a line about being open source that the page intro above it already says about everything. It now names the pieces — ggseg for 2D with ggplot2, ggseg3d for 3D with Three.js, and the atlas packages — and mentions the playground, which runs a real R session in the browser and is the fastest way for a sceptic to see whether it does what they need. The link moves from the R-universe listing to ggsegverse.github.io. One is a package index, the other explains the thing and lets you run it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
It was a named text link sitting apart from the social icons. As an icon it joins them, and the mark is the colon from the UiO lockup — the same one the CAPRO logo carries in "UiO: Life science". No icon font has it, so it is two circles drawn inline, inheriting colour from the link like its neighbours do. The label survives the change: it was visible text and is now the aria-label, so a screen reader still hears "Employee page at the University of Oslo (opens in a new tab)" rather than a shape. The row also renders now when a person has a UiO page and no social accounts, which was Silje's case — the wrapper was inside `with .Params.Social`, so her link had nowhere to live once it moved. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The CNAME is www.capro.dev; baseURL said capro.dev, which 301s to it. So every canonical, og:url, hreflang and sitemap loc named a URL that redirects — search engines were told the canonical version of each page was somewhere else, and unfurlers followed a hop per card. Internal assets were paying for it on every page view: the logo, the hero background and the hero preload all used .Permalink, so they were fetched from the redirecting host. Those are relative now, along with the article and language-switcher links. meta.html keeps .Permalink — canonical, hreflang and og:image have to be absolute. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Team, testimonial and news bundles were copying their originals into /public alongside the processed derivatives the templates actually use — 18 files, 6.3M, zero references. A 1049K portrait PNG and a 793K one were the largest things on the site. Portfolio already had this cascade. Derivatives are published whatever this flag says, so nothing on the site changes; the deploy goes from 12M to 5.7M. Checked all 2564 local references in the built output afterwards: none of the pruned files was reachable from any page. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
www.capro.dev/robots.txt was a 404, so nothing pointed crawlers at the sitemap. The hreflang set listed en and no but no x-default, which is what Google falls back to when a visitor matches neither. x-default resolves through hugo.Sites, ordered by the configured language weight, so it tracks defaultContentLanguage rather than hardcoding "en". Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Automatic source detection was walking assets/js/vendor, so minified Alpine and Leaflet contributed utility candidates: blur, collapse, container, inline, resize, ring, shadow, start, sticky, transform, transition and visible were all generated from strings in third-party JS, and none is used in any template. It also made the bundle depend on build order. `npm run build` cleans assets/js/vendor before compiling CSS and restores it afterwards, so a full build emitted 42828 bytes while a bare `npm run build:css` emitted 46022 from the same sources. Anyone running the documented build got a dirty tree and a CI staleness failure they had not caused — which is how this was found. source(none) leaves only the explicit @source, which is where every class on this site lives. Both commands now produce the same 42513 bytes, 3.4K off the render-blocking bundle. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Both lockfiles are committed but all three workflows ran `npm install`, so CI could resolve versions you do not have locally. That matters here because the committed tailwind.css is diffed against a CI rebuild: a Tailwind patch release would fail that check with no source change. `npm ci` installs the lockfile exactly. Also: cache ~/.npm, drop `submodules: true` (there is no .gitmodules), and give lint the full history it needs for enableGitInfo, which the other two workflows already had. Nothing was watching the dependencies. Dependabot now covers both npm projects and the actions, monthly, grouped so it opens three PRs rather than twenty. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Five aria-labels and one label fragment were hardcoded English, so the Norwegian site announced "Toggle navigation", "Main navigation", "Mobile navigation", "Footer navigation", "Back to top", and "<name> on github.com". profileOn takes name and host as a dict so the preposition is part of the translation rather than concatenated around it. The Norwegian wordings are mine and want your eye. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
site.Params.social is not defined anywhere in config/, so the row rendered as an empty flex container with a 32px bottom margin on every page. Its aria-label also still used the split-on-hyphen trick that team/single.html documents as a bug — fixed there, left here. The .footer-social-icon rules go with it. If you want CAPRO's GitHub org in the footer, this comes back as four lines of params.yaml plus the row; say so and I will add it with the host-based label the team pages use. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
.reveal set opacity: 0 and waited for scroll-reveal.js to add .visible. The noscript and prefers-reduced-motion overrides covered the cases they were written for, but not the one that matters: JS enabled and the script not running. A blocked request, an SRI mismatch or a throw left every section at opacity 0 with no error anywhere. Hiding is now gated on a .js class that the reveal script itself sets, only once its observer exists, and removes again if anything throws. The script is inlined in <head> rather than fetched, so there is no separate request that can fail after the page has committed to hiding things — and no flash, which an arm-on-DOMContentLoaded approach would have had. Verified in Chrome over five paths: normal, JS off, no IntersectionObserver, constructor throwing, observe() throwing. The last four leave all 14 sections at opacity 1; the first animates as before and reveals everything on scroll. The noscript override is gone, being unreachable now. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Reverses the direction of the earlier fix: the two sides disagreed, and the apex is the one we want. static/CNAME now says capro.dev, so GitHub Pages serves the apex and redirects www to it, and baseURL goes back to https://capro.dev/. All 1419 absolute URLs in the build are apex, none www. DNS already supports this: the apex carries GitHub's four A records, www is a CNAME to capro-uio.github.io, and the live certificate already covers both names, so nothing needs reissuing. Ten portfolio links pointed at www.capro.dev. Those paths are other repos' project pages, served under this site's custom domain, so they move with the CNAME — they are apex now and will resolve without a hop once this deploys. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
CSS nothing emits: .service-block and its descendants (the services design that .svc replaced), .person-uio (replaced by the icon in .person__links), .section-title__name. Config nothing reads: post_share, preloader, cookies, and a plugins.css block holding a cdnjs Font Awesome <link> — the only reason the site was not loading Font Awesome twice, once from a third party, is that no template ever looks at site.Params.plugins. map keeps its coordinates, which contact.html does use, and loses gmap_api and map_marker: the map is Leaflet over OpenStreetMap with an inline-SVG marker. HUGO_GMAP_API goes out of all three workflows with it. themes/capro-hugo: 172 files, 4.3M of jQuery, Bootstrap, Slick, Shuffle and Magnific Popup. theme is capro-tw and nothing referenced it. The attribution in capro-tw's README and theme.toml stays. Built output is the same file list; the CSS bundle drops 838 bytes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Teal on body text is 1.36:1, where WCAG 1.4.1 wants 3:1 for a link marked by colour alone. The fix existed four times over — .prose, .section-intro, the three .svc bodies, .offer__body, and a .person-uio rule that had already gone stale — each with its own copy of the same comment, and each needing to be remembered for every new prose container. One had already been missed: .person__bio holds each team member's rendered content and was in none of the lists, so the first link in a bio would have failed silently. So were .contact-reassure and .help-card__short. Underline is the base rule now, with an opt-out list for links whose surroundings already make them unmistakable: buttons, the skip link, nav, the language switcher, footer links, card headings and the contact address, which carries its own border-bottom. The ORCID fallback loses its hand-applied underline utilities too. Checked in Chrome across seven pages: all 167 links in the opt-out contexts render undecorated, testimonial sources and the Leaflet attribution stay underlined, and an anchor injected into each of the ten prose containers comes out underlined. Also the third copy of the split-on-hyphen aria-label — the team grid's social row, which announced "github" rather than naming the person. It now uses profileOn like the other two. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Twenty-nine glyphs were costing 737K of font files and 93K of CSS. Font Awesome shipped fa-regular and fa-v4compatibility, which nothing referenced at all; Themify shipped .eot, .ttf and .svg — 382K of formats no supported browser asks for — and no .woff2, so browsers took the 55K .woff. Both are gone. assets/icons holds the 29 SVGs the site actually uses, synced from the Font Awesome package by npm run sync:icons, which is also where the list of used icons lives. A helper partial inlines one and errors out on an unknown name, where a mistyped icon-font class used to render nothing. Themify had no SVG source and only nine icons in use, so those are Font Awesome equivalents now and the whole site draws from one set: user, pen-ruler, infinity, rocket, box, chalkboard, square-check, location-dot, arrow-up-right-from-square. Content keeps an icon: field but names the icon plainly — "database", not "fa-solid fa-database" — so nothing in content refers to a font that is no longer there. This also removes the media="print" onload trick in head/css.html. That was a workaround for a stylesheet too heavy to block on; with no icon stylesheet there is nothing to defer, and icons paint with the page rather than after it. Render-blocking CSS: 54888 to 41162 bytes. Static assets: 815K lighter. Font Awesome stays as a devDependency for the SVG source and its licence; attribution is in assets/icons/README.md, since the per-file licence comment would otherwise be inlined into every page. Measured in Chrome at 1280px and 390px, both languages: 206 visible icons, every one sanely sized. Two things that only showed up there — an auto-width SVG stretches to its container under display:block, and again as a flex item in the help cards, so those are pinned. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The tag "hub/node" made Hugo build /tags/hub/node/, so it read as two tags and left /tags/hub/ as a bare directory. It is hub-node now, which also matches how the offerings copy writes HUB-node. Testimonial portraits carried the person's name as alt text, which the line immediately beside them already gives, so a screen reader heard it twice. The portrait is decorative there. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
/services/ stacked five sections: the services, the named tools, "what working with us is like", the testimonial carousel and a closing CTA. That is the "too much information and a bit scattered" note applied to the page rather than to the services block, which was the part already fixed. The page is the services and the tools now — 36.9K to 17.8K. The carousel was the only thing rendering content/testimonial, so cutting it would have dropped nine named testimonials off the site entirely. They move to the landing page instead, between the team and the contact section, which is where social proof does the most work. Verified: nine render on both language home pages, portraits included. "What we run and support" stays on both / and /services/ by choice: the repetition is the point for anyone arriving looking for GOAT or Nettskjema by name. Nothing renders sections/feature or sections/cta any more, so content/feature and content/cta are unreferenced. Left in place rather than deleted. Also: lb_wb2's English file carried the Norwegian job title. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
People arrive unsure whether we can help them, and the copy was a large part of
why. The site described itself in terms a reader only understands if they
already know what we do — "data engineering", "FAIR principles",
"containerization", "integrated solutions", "registry data handling 55%".
That was the starting point. The branch grew from there into the structure
around the copy, the team pages, and a set of things the audit at the end
turned up underneath.
Copy and structure
Services are problem-led, and there are five of them. Each leads with the
problem the reader arrived with, lists the situations that produce it, and says
what they get. The category name stays as a small secondary label, so someone
who does know the term can still find it.
Seven became five, in the order people meet them: data management and sharing →
data capture and engineering → processing, analysis and computing →
applications and databases → workshops and training.
"What we run and support" is a second layer, not the spine. Barbara's four
pillars are real, but they are specific tools rather than services, so they sit
in their own block — GOAT, Nettskjema, MRI, behavioural experiments — on both
the landing page and
/services/. The repetition is deliberate: it is foranyone arriving looking for one of those by name.
Two claims came out. We do not advertise MR scanning, and we do not
advertise Bevex, which is not in beta until spring 2027. The experiments card
says what is true today: JavaScript tasks that run anywhere, the
tachistobuilder, and the platforms you can actually upload them to.
/services/is two sections. It had grown to five — services, tools, "whatworking with us is like", testimonials and a closing CTA. That was the
"scattered" note applied to the page rather than to the services block. 36.9K
to 17.8K.
Skills, pricing, hero, contact. The percentage bars are gone; "registry
data handling 55%" was not sourced from anything and not something a visitor
can act on. Pricing explains that the rate depends on how close you are to the
department and that computing time is applied for separately. Contact says
explicitly that you do not need to know the technical name for your problem.
The team
Bios written from the employee pages, with
summaryfields so cards show aline rather than the whole bio, and task tags at one consistent level of
granularity. Person pages lay the photo beside the text instead of under it.
Silje Forsland added; José-Luis's 2024 portrait and his statistics focus;
Markus's MRI protocol work. Employee pages are linked through an icon drawn
from the colon in the UiO lockup. Photographers are credited where we know who
they are.
Publications moved from ORCID to OpenAlex, which returns the full list in one
request and does not need a personal address in the query.
Open source
The portfolio shows everything, not just the four entries that happened to have
a logo, with project marks back on the cards — grey until you point at them.
jsPsych Studioistachistothroughout, with its new wordmark;ggsegis nowthe
ggsegversemark and an entry that describes what it is today.Accessibility
where 1.4.1 wants 3:1 for colour alone. The fix existed four times over and
had already missed
.person__bio,.contact-reassureand.help-card__short.Norwegian site — "Toggle navigation", "Back to top", " on github.com".
aria-labelbuilt by splitting an icon class on hyphens,which announced "github" rather than naming the person.
opacity: 0andwaited for a script that might not arrive; the flag that hides content is now
set by that script itself, inlined in
<head>, and removed again if anythingthrows.
switcher that is links rather than a navigating
<select>.Infrastructure
The canonical host was wrong.
baseURLsaidcapro.dev,CNAMEsaidwww.capro.dev, and the apex redirected to www — so every canonical,og:url,hreflangand sitemap<loc>named a URL that 301s. The apex is canonical nowand the CNAME matches. DNS has since moved to Cloudflare; both names are
proxied, the edge certificate covers them, and
/.well-known/acme-challenge/still reaches GitHub so its certificate can renew.
The deploy was 12M, of which 6.3M was images nothing linked to. Team,
testimonial and news bundles published their originals next to the derivatives
templates actually use. With the icon fonts also gone it is 4.8M.
Both icon fonts are gone. 29 glyphs were costing 737K of font files and 93K
of CSS, including two Font Awesome faces nothing referenced and three Themify
formats no browser asks for. Icons are inline SVG now, synced from the Font
Awesome package by
npm run sync:icons. This also removes themedia="print" onloadtrick, which was a workaround for a stylesheet too heavyto block on.
Tailwind was scanning
assets/js/vendor, so minified Alpine and Leafletcontributed utility candidates, and the bundle depended on build order — a full
npm run buildand a barenpm run build:cssproduced different output fromthe same sources. Render-blocking CSS is 59,831 → 41,088 bytes across the branch.
Also:
robots.txt(a 404 in production),hreflang="x-default", Dependabot forboth npm projects and the actions,
npm ciinstead ofnpm installso CIcannot resolve versions you do not have, and the deletion of
themes/capro-hugo— 172 files of jQuery, Bootstrap and Slick that nothing referenced.
Still to do before merge
truth.
content/offerings/_index.no.mdis two revisions behind itsEnglish twin — its experiments card has neither the platform list nor the
tachistomention. The portfolio category chips are also my Norwegian.phrase an outside reader will not parse. Keep only if it earns its place
on credibility.
IPv6 — but worth confirming nothing else in the org hardcodes
www.capro.dev, since those now take a redirect.Notes for review
content/featureandcontent/ctaare no longer rendered anywhere. Left inplace rather than deleted, pending a decision.
--color-tertiary(gold) is still defined and still unused.static/webfonts/leafletships five PNGs that Leaflet's stylesheet looks forat a different path. Nothing requests them — the map uses an inline-SVG
marker and adds no layers control — so they are dead weight rather than a
live 404.
Test plan
CI covers formatting, the content accessibility lint, a strict build, the CSS
bundle freshness check, the link check and the axe audit. Beyond that, verified
by hand in Chrome:
390px, all sanely sized. Two bugs only visible there: an auto-width SVG
stretches to its container under
display: block, and again as a flex item.IntersectionObserver,constructor throwing,
observe()throwing. The last four leave all sectionsat
opacity: 1; the first animates and reveals on scroll.injected into each of the ten prose containers comes out underlined.
none of the removed files was reachable from any page.
🤖 Generated with Claude Code