Skip to content

The person card, the modal, and one field per thing - #204

Merged
DZPM merged 39 commits into
editionfrom
pr/10-github-handles
Oct 8, 2026
Merged

DZPM merged 39 commits into
editionfrom
pr/10-github-handles

Conversation

@DZPM

@DZPM DZPM commented Oct 7, 2026

Copy link
Copy Markdown
Member

Sixth in the stack, on top of #202. It finishes the organizers page, gives each organizer a record of what they have done, and tightens the content model behind both.

227 files, but 195 of them are one mechanical edit repeated: a trailing slash removed, a field renamed, a bio moved. The part to read is 32 files and about 1250 added lines, mostly people.scss, five partials and the two check scripts.

The person card

Every card is the same width on every page and in every section, so no section reads as more important than another. The photo reaches the border, the name is one size everywhere and is cut with an ellipsis when it does not fit, the role sits in italics with the same space above and below it, and the name, the role and the icons scale with the card, which stops four icons wrapping onto a second line on a narrow one.

The Permanent Committee fits six on a row. That costs 22px of card width across the site: six cards of 200px need 1320px and the grid is the 1140px the rest of the page uses, so the card is 178px and the gutter 12px. The alternative was to widen the people grid past the heading bands, which looked worse.

Measured on the organizers page, PyDay 2025, Hands-on Django and the PyLadies page, at 1440, 992, 860, 768 and 390: no name, role or icon row on two lines.

The modal

It used to show a photo, a name, a bio and a role, and for most people that was almost nothing. It is now a photo column and a text column with labelled sections: Bio, Roles at PyBCN, Events and Social Links. Each section disappears when its field is empty.

The Events list is new and needs no new content. Every event page already declares who took part and under what heading, so inverting that index gives each organizer a line per event, newest first, with a link to the event. The headings map to roles through data/roles.yaml, which anyone can edit without knowing Hugo.

The content

Roles lose the redundant PyBCN prefix: a card on a page titled PyBCN says "Organizer". Three people move from the Permanent Committee to Collaborators, which matches what the association does today. The Management Board of 2026 gains the LinkedIn of all five members.

Laura Perez Mayos and Natalia Padilla each had two person files, so neither told their whole story: Laura's Collaborators card collected seven events and her Management Board card none. They are one file each now, and a page entry can override the role, so the 2018 board still reads "Former President".

short_bio is gone. There were two places to put the same text and nobody could tell which to use: 98 of 136 people had filled only one of them, so a reader saw a different heading depending on who wrote the card. The text now lives in the body of the file, and the rendered text of all 185 modals is identical before and after.

A sponsor has four fields, because four is all any template reads. Thirty-two sponsor files carried a twitter, linkedin, mastodont or instagram URL that had never been published, written by somebody who expected it to show.

Every event page carries a year and every event title ends with the year in parentheses, so a list of them reads the same everywhere. No URL changed: the output paths are identical before and after.

The checks

bin/check-content gains the shape of a social link: the host it must start with, no query parameters on a profile, no trailing slash, https, and no empty value. A trailing slash was on 110 of 322 values. It also warns when a person photo is black and white, which ten are, and rejects a sponsor field nothing renders.

bin/check-rendered gains insecure links in the built pages, which no field check can see: one person had an http URL inside their bio, and Hugo turned it into a real anchor.

Where to start

If you only read two files, read themes/pybcn_theme/layouts/partials/person_appearances.html and people_summary.html. That is the new behaviour. people.scss is long but it is one idea repeated: the card is a fixed width, the row caps itself, and the type is a share of the card.

The commit messages carry the measurement behind each change, so a commit-by-commit read is the fastest way to see why something is the way it is.

@DZPM
DZPM requested a review from a team as a code owner October 7, 2026 06:29
@DZPM DZPM self-assigned this Oct 7, 2026
@DZPM
DZPM requested review from ber2, mesejo and mrswats October 7, 2026 06:33
@DZPM

DZPM commented Oct 7, 2026

Copy link
Copy Markdown
Member Author

To compare, some examples, I'm showing the first person per category:


Before

Before: https://pybcn.org/pybcn_association/organizers/

image image image image image

After:

Main goal: show all the info we have, in an automated way (we don't need to maintain every time we add an event, but it's built automatically), with usability and accessibility in mind.

Notice the layout is different: bigger image, 6 per row (they fit now!), centered and styled text, centered icons, new x icon...

Also, the ex-Permanent Committee has been moved to Organizers, that helps too

image

Added his Linkedin too. Notice we now show all roles, events, etc.

image

Different fields. The social urls are now clean (no https, GET params, trailing /); and a specific check on this.
Notice his photo could be bigger, but still, we degrade gracefully.
Photos are now validated to be square, and in a decent size/resolution.

image

Notice the "short bio" and "description" field have been collapsed into a single one (it was a huge, inconsistent mess)

image

Also, Laura and Natàlia had duplicated files (one as organized, one as ex-board), it's all consolidated now.

image

More ideas, suggestions, feedback, very welcome.

Future work:

  • only the "organizers" cards have this feature. I'd like to extend to any speaker/volunteer/etc role.
  • if we import all past meetups and events, we could have a card per person
  • ideally, we would have a url per person. But that will happen in a different discussion

@DZPM
DZPM force-pushed the pr/9-fix-xml-surface branch from e127979 to 71ed7e4 Compare October 7, 2026 09:53
DZPM added 21 commits October 7, 2026 11:53
Nothing checked the four link fields (linkedin, github, twitter, site)
in the person and sponsor files. The link checker in CI reads the built
pages, and a URL broken badly enough never becomes a link at all, so it
is invisible to both. Three kinds of drift had accumulated, three links
had never worked, and four organizers had no GitHub account on file.

**Four GitHub accounts recorded.** 80 of the 139 person files carried a
`github` field and these four did not, so there was no way to go from a
name on the organizers page to the account that reviews pull requests.
That gap cost real time this week: I read `ber2` as one person when it
is another, and could not tell whether a member of the Permanent
Committee had ever been invited to the organisation, because nothing
connected his name to `mrswats`. Each handle comes from the name the
account itself declares on GitHub, matched against the name in the
person file. Nothing is guessed:

  alberto.md        ber2         Alberto Camara
  nuria.md          llevaNEUS    Nuria Pujol Vilanova
  ferran-jovell.md  mrswats      Ferran Jovell
  daniel-mesejo.md  mesejo       Daniel Mesejo

Daniel's is the odd one: the orphan duplicate `daniel_mesejo.md`, which
no page lists, already had the field, while the file the organizers page
actually uses did not. Five members of the organisation have no person
file at all and are left alone: areski, chdoig, mikanchu, paulinus and
tris-sondon.

**88 Twitter links, now x.com.** Both `twitter.com` and `www.twitter.com`
redirect there, so every one of them cost a redirect and showed a brand
that no longer exists. The two association accounts in config.toml and
the archetype that generates a new sponsor file go with them.

**11 GitHub links carried `www.`**, which `github.com` redirects away.
They now match the other 81.

**120 LinkedIn links, now `www.linkedin.com`.** One was broken and had
been for as long as it has existed: patricia-lamadrid.md began
`ttps://`, a missing h in the scheme. Four pointed at `es.linkedin.com`,
which serves the same profile but pins the reader to one interface
language, and one at `linkedin.com` without `www`. All 120 now use
`www.linkedin.com`, which is the host LinkedIn itself serves and which
115 already used. No LinkedIn URL carries query parameters, which is
worth stating because a profile URL copied from a browser usually does.
`/in/`, `/company/` and `/school/` all stay: they are different kinds of
LinkedIn page, not an inconsistency.

**One personal site pointed at a subdirectory**, `davidarcos.net/blog/`,
rather than at the site. The other personal sites are left alone. Three of
them are written with `www`, and none of the three needs it:
paulaszewach.com answers with it and without it, and netbcn.cat and
enricdomingo.com answer nothing either way, so they are down rather than
www-only. They keep the form their owner gave us. A URL somebody chose for
themselves is not a cosmetic inconsistency to tidy up.

**check-content now holds the shape.** It rejects a github, linkedin or
twitter link that does not start with the one host each service serves,
rejects query parameters in any of them, and warns about a personal site
served over http. A twitter link has to be https://x.com/USERNAME and
nothing else. It found six things on the first run:

  patricia-lamadrid    linkedin began "ttps://", missing the h
  alejandro-nicolas    linkedin had no scheme at all
  amalia_vradi         linkedin had no scheme at all
  colorsensing         twitter carried ?lang=es
  eduard_cespedes      site carried ?lang=en
  juan-luis-cano       twitter held a Mastodon address

The first three had never worked. A browser reads "ttps://www.linkedin.com"
as a path relative to the current page, so those three people have had a
dead link on their card for as long as the file has existed. Juan Luis's
Mastodon address moves to `site`, where it renders a globe: it was
drawing an X icon over a profile on the network he left. An empty
`twitter:` key in the CodeOp sponsor file is gone too: it rendered
nothing and meant nothing.

**The icons are ordered by how many people have each.** They came out
twitter, github, linkedin, site. Counting the person files, that is
close to backwards:

  linkedin   114 of the 139 person files
  github      84
  twitter     64
  site        23

The network four in five people have was third, and the one fewer than
half of them have was first, so the first icon on most cards was the one
least likely to be there, and the row opened with a gap on more than half
of them. The order is now linkedin, github, twitter, site, in the
card and in the dialog. It is fixed in the template rather than
computed, because the shape of this list changes about once a decade and
a sort at build time would cost more to read than it saves. The order of
the keys in a person's front matter has no effect on any of this and is
left alone: the template decides.

Verified: each new account renders its icon on the organizers page;
x.com/PyBCN, x.com/PyLadiesBCN, davidarcos.net and github.com/ber2 all
answer 200 with no redirect; and the three content checks pass.
Loris Mularoni, Ruben Berenguel and Xavi Francisco are listed as the
Permanent Committee and are not in it. The page had gone stale: the
committee that meets today is six people, and the site showed nine.

They move to Collaborators rather than off the page, which is what has been
done with everyone who stepped back from the committee before them. The
association owes them the listing either way.

Permanent Committee goes from 9 to 6, Collaborators from 5 to 8. Nobody is
removed and the Management Board is untouched.

Checked against the committee's own membership, not against the site.
… rows

The three sites that the content linked over http all answer over https:

  http://marcpous.com      https://marcpous.com/
  http://orkei.com/        https://orkei.com/
  http://www.liopic.me/    https://liopic.me/

Nothing on the site is linked over http any more.

The Permanent Committee is six people at five per row, which left one card
alone on the second row. Three per row gives two even rows and larger cards.
The other three sections keep five, which is what their counts suit.

Two people are left with a dead link on their card: netbcn.cat and
enricdomingo.com answer nothing, with `www` or without. Repairing those
means asking them, so it is not done here.
…rame

The number of cards on a row used to be a fraction of the row, so three
cards in the Permanent Committee sat 180px apart while five cards in the
Board sat shoulder to shoulder, and the sections did not read as one
page.

The card is now a fixed 200px and the gutter a fixed 20px in every
section. The per_line_N class moves from the card to the row and caps
the width of the row at N slots, so the row itself decides how many
cards it holds. The last row of a section is centred. The Permanent
Committee goes 3+3 and the Collaborators 4+4.

Inside the card, 10px of padding inside the border put the photo in a
white frame that took a tenth of the 200px width, and 25px of margin
under the photo plus 1rem under the role plus 10px over the icons spread
the text over the card. The card is now 311px tall where it was 346px.

The photo reaches the border. The text block keeps 10px at each side and
5px over the name, the same as between the name and the role, and the
icons start where the role ends. The icons grow from 32px to 40px, which
is the largest size at which four of them fit on one line with 6px of
margin each: 176px of the 188px the row allows. The size is set in the
stylesheet, not with the fa-2x class, because it follows from the card
width. line-height: 1 on each link drops the 3px of empty line box that
the strut of the link's own text left under every icon.
…dIn for the Board

A card under the PyBCN heading does not need to say PyBCN again before
the role, so "PyBCN Organizer" is now "Organizer" and "Board member of
Events" is "Events". The PyLadiesBCN roles keep their prefix: that
names a different group.

The five people on the Management Board had no LinkedIn link, so their
cards showed no icon at all.
The slot was a fixed 220px, so a row only held as many cards as the
container could fit. An event page puts the people grid in a col-md-9,
which leaves 795px, and four 220px slots need 880: the PyDay pages
dropped from four cards a row to three. The slot is now the fraction of
the row that the section asks for, and the row keeps its own width cap,
so a container with the room still gives a 200px card and a narrower one
gives the same count with a smaller card.

The fraction only applies from 992px up. Below 768px the slot is the
full width, which is what the old col-md-N classes did. Between the two
the count is capped at three, because five 220px slots need 1100px and a
768px screen gives 720, and a 118px card is too narrow for the four
social icons.

Note that a literal divisor is not a division in Dart Sass: 100% / 3
compiles to the string 100%/3 and the browser drops the rule, which
shows one card a row on a tablet, while 100% / $n divides.

The icons go back to 32px, the size they have always been, with 6px of
margin on each side instead of 3px: the complaint was the spacing
between them, not the glyph. The row is 170px, and the side padding of
.social_links drops to 2px so the row still fits the narrowest card on
the site, the 179px one on an event page.

The name and the role on the card face sat on the left edge while the
icon row under them was centred, so one card showed two alignments.
Both are centred now. The role is italic to set it apart from the name.
The modal keeps its alignment and its bold role line.

A long name wrapped onto two lines: on the organizers page four names
need up to 226px in the 178px the card gives, and on an event page,
where a four-card row inside the col-md-9 leaves 157px, seven names
wrapped. The template adds a long_name class to a name of 18 characters
or more (RuneCount, not len, so an accent does not count twice), and
the stylesheet sets that heading to 13px from the md breakpoint up,
which makes the longest name 153px. The threshold and the size come
from measuring every name on both pages: no name wraps at 1440px or at
390px, and no name is clipped. Below 768px one card takes the row and
the rule does not apply, so a phone keeps the full size.

Also capitalise the role on eloi, which the prefix strip left lowercase.
…ier from two Organizer roles

The four people with pybcn_position PyLadiesBCN Collaborator are
organizers of PyLadiesBCN, so the role says so and keeps the prefix.
Practice sessions organizer and Meetup organizer become Organizer, the
role every other organizer carries, so one card does not read as a
different kind of organizer from the next.
… fill its column

The card is not the same width on every page. The organizers page gives
it 200px and an event page 186px, and the window widths between the
breakpoints give down to 150px. A fixed type size has to serve the
narrowest card, so the previous rule put every long name at 13px, which
is smaller than the role above it and reads as the wrong hierarchy.

The name, the role and the social icons are now a share of the card,
through a container query unit on .person_wrapper. 9.6cqi is the 19.2px
the name has always had on a 200px card, 6.8cqi is 13.6px for the role
and 16cqi is 32px for the icons, and each has a clamp so a card that
takes the whole row on a phone does not scale the type with it.

A container query on inline-size measures the content box, which is the
card itself and not the slot that holds it. An earlier version keyed on
210px to catch the 179px card and caught the 200px one as well.

The grid also loses its horizontal padding from the md breakpoint up. A
Bootstrap container adds 15px inside the padding the column already has,
so on an event page the level heading band spanned the full width of the
col-md-9 while the first card sat 15px inside it. The card there goes
from 179px to 186px. The phone keeps the padding, because the card would
otherwise touch the edge of the screen.

Measured on the organizers page, PyDay 2025, Hands-on Django and the
PyLadies page, at 1440, 992, 860, 768 and 390: no name, role or icon row
on two lines, except one 26-character name on the 2022 Django page below
992px.
…es level

No event page had a date field. The year lived only in the title and in
the file name, so no template could order the events. The eight pages
with people_sections now carry year in the front matter, taken from the
title. The Hands-on Django page has no year in its title: the value comes
from the file name and the photo paths, and 11 June 2022, the date the
page gives, was a Saturday as the page says.

The level on the PyLadies page was named Main organizers. The page is
the PyLadiesBCN organizers page, so the heading now says which group the
people organize: PyLadiesBCN Organizers. The same heading is the label
the person modal shows for an appearance on this page, where Main
organizers said nothing.
The modal showed the links as a row of bare icons next to the name. The
row says which networks the person is on, and nothing more: a reader had
to follow a link to learn the handle. The new person_links.html partial
lists the same links at the foot of the modal, each with its icon and
the text that says where it goes: the LinkedIn path, the GitHub or X
user name, or the whole address of a personal site. The icon row by the
name stays as it was.

The text is cut from the stored URL. The person files carry one form per
network, which bin/check-content enforces, so the prefix is known and a
second copy of the handle stays out of the front matter.

A person with no link gets no list, not an empty one. Checked on
/organizers/ with david, who has all four, and on PyDay BCN 2023 with
emanuel-frazao, who has none.
The modal listed the links twice: a row of bare icons next to the name,
and the list at the foot that also prints the handle or the host. The
bare row said less and came first, so it read as the real one.

The list is the only copy now, and its icons go up from 16px to 20px,
since they no longer compete with a larger row above them.
The name size was a share of the card, so a long name came out visibly
smaller than its neighbours on the same row and read as a different
heading level rather than as the same name in less room. It is one size
on every card again, and a name wider than the card is cut with an
ellipsis. The whole name is in the title of the button and in the modal
the button opens, so nothing is lost.

The ellipsis needs a box with a width to overflow, so the button is a
block of the full width. An inline button takes the width of its text
and never overflows.

The role also gets the same space above and below it. It had 5px over it
and none under, because margin-top: auto on the icon row leaves no gap
of its own on a card whose content fills it.

The role and the icons stay a share of the card. There the scaling fixes
real defects: four icons wrapped onto a second line on a 150px card, and
PyLadiesBCN Organizer wrapped on an event page.

Measured on the organizers page and PyDay 2025 at 1440, 992 and 390: the
name is 19.2px everywhere, no name, role or icon row is on two lines,
and the gap above and below the role is 5px on every card.
The modal of most organizers held a photo, a role and the links, and
nothing a reader could follow to the events the person took part in.
Every event page already lists its speakers and organizers by id, so
the modal now inverts that index: one row per role, with a link to each
page, newest first.

    Speaker      PyDay BCN 2025
    Organizer    PyDay BCN 2024, PyDay BCN 2021, PyDay BCN 2020
    Volunteer    Hands-on Django with PyLadiesBCN

The index is built once per build, in appearances_index.html through
partialCached with a constant variant, from people_sections on the event
pages and people_levels on the association pages. Only the people listed
on /pybcn_association/organizers/ are indexed; every other person is a
speaker or a volunteer of one event, whose modal would list that one
event and nothing else.

The page the modal is on is left out. Everyone on /organizers/ would
otherwise get a link back to the page they are reading, which says
nothing their role line does not. Six organizers then have no
appearance at all, and the block emits nothing for them.

An appearance is one (heading, page) pair. Ferran Jovell is listed
under two levels of one section of the Django page, and the modal would
otherwise link that page twice under one label.

The heading a page uses, such as "Speakers", goes through
data/roles.yaml to become the singular role the modal shows. The
heading itself is the fallback, so a new heading shows up unmapped
rather than not at all, and the map is a plain data file that anyone
can edit. "Mentors, speakers & volunteers" names three roles under one
heading, so the single word it maps to, Volunteer, is an approximation
for the people listed there.
Laura Pérez Mayos and Natàlia Padilla each had two person files,
lpmayos and lpmayos-president, natalia and natalia-secretary, so that
/organizers/ could show them twice with a different role: Organizer
under Collaborators and Former President or Former Secretary under the
old board. The split meant neither file told the whole story. The modal
of the Collaborators card listed seven events, and the modal of the
board card listed none, because no event page referenced the second id.

The ids that stay are the ones the event pages already reference:
lpmayos in 8 files against 1 for lpmayos-president, natalia in 9 files
against 1 for natalia-secretary. The two extra files go, and the only
page that listed them, organizers.md, now lists the surviving id.

To keep the two roles, an entry of a people list can now be a map with
an id and a role, next to the bare ids:

    people:
      - id: lpmayos
        role: Former President
      - mireia

The role replaces the pybcn_position of the person file on that page
only, on the card and in the modal. people-grid.html reads the shape
and documents it. bin/check-content reads the id out of a map entry,
which used to raise a TypeError, and rejects a map without an id, a
misspelt key, or a map in a sponsor list, which the template would drop
in silence.

/organizers/ renders the same people in the same sections with the same
role text as before, and both merged people now show their full
appearance list on both of their cards.
The appearance index took the section title as the heading of every
person in a people_sections entry. The Django page has one section titled
"Mentors, speakers & volunteers" with three named levels, so the index
called every mentor and speaker of that page a Volunteer, which was true
only of Mireia.

A level with a name is now its own heading, and a level without one falls
back to the section title. The Speakers and Organizers sections of the
event pages have one unnamed level each, so nothing changes for them.

data/roles.yaml maps Mentors and Volunteers to their singular role and
drops the combined title, which no level falls back to any more.

On /organizers/, David, Elisabeth, Natalia, Nuria and Xavi go from
Volunteer to Mentor on the Django page, and Ferran gets that page under
both Mentor and Speaker: the (label, page) dedup keeps two rows when the
labels differ.
The person modal sorts appearances by the year of the page, most recent
first, and a page with no year goes last as year 0. Only the eight pages
that list people carried the field. The other five single events now
carry it too, taken from the title, so the same field is on every event
and a page that starts to list people sorts in its place at once.

The section pages and the monthly events page are not single events and
get no year.
The list left out the page the modal was opened from, which made the
modal depend on the page around it, and on /organizers/ hid the level of
the page, which is the information: the modal of Jordi Bosch did not say
Management Board (2026), the one of Laura Pérez Mayos did not say
Collaborators and Management Board (2018 - 2025), and the one of David
Arcos did not say Permanent Committee.

The modal is on its way to becoming a page per person with its own URL,
and for that its content has to be the same from everywhere: a list
that changes with the page it was opened from cannot become one page.
Every appearance is now shown with its link, on every page. On
/organizers/ the first row links to the page the reader is on; that is
the price of one list, and it stops being odd the day the modal is a
page.

A page with no year is a role held now, not an event, and sorts before
every dated event, so the current board heads the list. The rule used to
send it last, which put Management Board (2026) under the events of
2025.

The page argument went with the exclusion. It was threaded through
event.html, people.html, people-grid.html and people_summary.html for
this one partial and nothing else read it.

The empty branch stays: the index covers only the people listed on
/organizers/, so a speaker who is not an organizer still has no
appearance.
The titles named the year in three ways: at the end (PyDay BCN 2025), in
the middle (PyData 2017 - Barcelona edition) or not at all (Hands-on
Django with PyLadiesBCN). The modal lists several events in one row, and
the navigation menu lists them in a column, so the shapes sat side by
side. Every single event now reads Name (Year): PyDay BCN (2025),
PyDataBCN (2023), Hacktoberfest (2018), Hands-on Django with PyLadiesBCN
(2022), PyData Barcelona (2017).

The URLs come from the file names and do not change. The full list of
output paths is the same before and after, and the aliases still point
at the same pages.
The Permanent Committee has six people and sat as three and three. Six
cards of 200px with a 20px gutter need 1320px and the grid is 1140, the
Bootstrap container width the rest of the page uses, so the card goes to
178px and the gutter to 12px.

Every card on the site changes, not only this section: the rule is that
a card is the same width everywhere, so no section reads as more
important than another.

The alternative was to widen the people grid to 1320px and keep the
200px card. That made the section wider than the heading bands and the
rest of the page, which looked worse than the smaller card.

Measured on the organizers page and PyDay 2025 at 1440, 992 and 390: no
name, role or icon row on two lines.
…role line

Two fixes in the person card and its modal.

The headings of a page with no year sorted by label, so a reader saw
Management Board (2018 - 2025) above Permanent Committee, and the
PyLadies page above both. They now sort by the page and then by the
order the page itself lists its levels, which organizers.md writes
current board first. The index carries that position as ord.

The role also had 5px of box above and below it but did not look it. The
person data block set line-height: 1rem, so the 19.2px name overflowed
its own 16px line box by about 3px top and bottom while the smaller role
sat inside its box with room to spare. A reader saw 2px over the role
and 7px under it. A ratio of 1.2 makes each box track its own text.

Measured on the organizers page at 1440px: 5px above and below the role
on every card, and the modal reads Permanent Committee, Management Board
(2018 - 2025), PyLadiesBCN Organizer, then the events newest first.
DZPM added 18 commits October 7, 2026 11:53
The four link fields (linkedin, github, twitter, site) in content/people
and content/sponsors mixed two styles: 110 of the 322 values ended with a
slash and 212 did not. A host carries no trailing slash of its own, as in
https://ber2.github.io, so a path under it gets none either. One rule, no
exceptions.

Only the final slash changes. Host, path, case, and the quoting of each
line stay as they were. canary.md, a test fixture, is not touched.

Per field, values with a slash before -> after:
  people:   linkedin 84/117, github 7/82, twitter 1/63, site 11/23 -> 0
  sponsors: linkedin 5/6, twitter 2/31 -> 0

check_social() validated the host prefix and refused query parameters,
but said nothing about the final slash, which is how the values drifted.
The fix is mechanical and has one right answer, so it is now an error
and not a warning: it must never drift again. The rule covers the three
prefixed fields and site, and the message names the value to use. For
site, the http warning still fires on its own, so a slash does not hide
it.
The boxes had 5px above and below the role, and it still looked closer
to the icons than to the name. What a reader sees is the ink, not the
box. A line box reserves room under the baseline for descenders that the
text may not use, so the gap moves by about 4px with the letters alone:
Organizer fills that room and President does not, David Arcos does not
and Natalia Padilla nearly does.

Measured on the ink of all 22 cards of the organizers page, the gap was
7.9px above the role and 11.3px below. The name now has 3px under it and
the icon row 7px over it, which gives 9.6px above and 8.7px below. The
spread of 7 to 12 on each side is the letters, and no pair of numbers
removes it.
The modal was a stack of blocks with no label: the photo floated at
400px beside the name, a line of text, a bold word, the body text, the
page list and the links. A reader had to guess that the line of text
was a bio, that the bold word was a role, and that the body text was a
description, and a short modal wrapped its last links under the photo.
The page list itself was one list grouped by role, so "Organizer" with
four events sat on one row under "Permanent Committee" with one page,
and the rows were in no date order. A reader had to work out that one
row was a position and the other a history, and merge the rows to see
when the person was active. When the meetups arrive from the Archive
the list gets long, and a list that is not in date order cannot be
read.

Now the photo sits in a row with the name, the role in italics as on
the card, and the short bio behind a "Bio:" label. Below the row every
block is a section with a heading: Description for the body text,
Roles at PyBCN and Events from the appearance partial, and Links for
the social links. A section is left out with its heading when it is
empty, and the role line is left out when the person has no position,
which most speakers do not; it used to print an empty paragraph.

The pages with no year are the "Roles at PyBCN" list, grouped by label
in the order the page lists its levels, as before. The pages with a year
are the "Events" list, one line per event, newest first, with the role
in italics before the linked title. Within one year the tiebreak is the
title and then the URL. A person with two roles at one event gets one
line with both roles, sorted and joined with "and": two lines would read
as two events. The index comment says so: the modal lists a page once
with both roles, not once under each role.

The photo shows at 200px, the size of the card, and the 400px file
stays as the 2x image. On a phone it is 120px beside a smaller title,
and the roles stack under their labels because the label column left
80px for the page title.

The h3 rule for links in the title goes: the title has carried no link
since the links moved to the foot of the modal.
A sponsor stored its home page in `web` and a person in `site`.
bin/check-content read `site` for both, so the 66 sponsor URLs were
never checked: 29 ended with a slash, which the social link rule
forbids, one is http and one carries a query string.

Rename the field in the 66 sponsor files and in sponsor_summary.html,
the only template that reads it, and remove the trailing slash from the
29 values. Nothing else about any URL changes: the sponsor links in the
built pages are the same as before apart from that slash. The old name
is an error now, so a sponsor file copied from an old commit does not
escape the check again. README.md documented the field as `url`, which
it never was.

The check now reports jobfluent.md, whose URL carries `?q=python`. The
value is left as it was, pending a decision on whether to drop the
query or keep that landing page.
The person cards grey every photo with a CSS filter on a device that
can hover, so a photo that is already black and white looks like the
others there. On a phone and in the modal nothing greys them, and that
photo is the one grey face in a wall of colour. Ten of the 130 photos
are like that. Only the person can fix it, with another photo, so this
is a warning and never an error, and the exit code does not change.

The measure is the mean of max(r,g,b) - min(r,g,b) per pixel over a
64px thumbnail. The ten score 0.01 or less and the next photo 9.5, so
the threshold is 4. Two of the ten are stored as greyscale; the other
eight are RGB JPEGs with no colour in them, so a header check cannot do
this and the pixels are read, through Pillow. The 130 photos add about
0.7 s to the run.

Pillow is what bin/square-photos already needs. pr.yml installs it for
this check. A machine without it still runs every other check and gets
one note that the colour check did not run.
The rule was written for the three profile fields, where a query is
always what the browser added: one LinkedIn value carried a lipi
parameter that named the search session it was copied from. A site is
any address, and there the query can be the address. A sponsor links to
empleos-barcelona?q=python, and without it the link lands on every job
in Barcelona rather than the Python ones, which is not what the sponsor
is paying for.

An error on a profile field, a warning on a site, so tracking in a site
is still visible without failing the build.
The photo was a flex item of the header only, beside the name, the role
and the bio, and the sections ran full width under it. The header is
three short lines, so the photo stood over a gap that the first section
had to clear. The body is now two columns: the photo on the left,
everything else on the right, top to bottom, and nothing runs under the
photo.

The photo is 300px, half again the 200px of before, and never larger
than the short side of its source: image_square.html enlarges a small
source to 400px for the grid, and 21 of the photos are under 300px, so
at 300px they would be soft next to the name. The column is as wide as
the photo, so a 200px photo leaves the room to the text.

The modal was 70% of the window: 1006px at 1440px, where a line of text
ran 650px and the foot of the modal was blank to the right of the
social links, and a percentage of a 2560px screen is wider still. The
cap is now 900px, from the content: the 300px photo column and its 20px
gap, a 548px text column and the body padding. The text column holds
the longest line the modal renders in one line, the 498px event line
behind its list indent, and is about 65 characters of the body font.

On a phone the columns stack and the photo is capped at 200px, so the
name stays on the first screen. The close button is positioned, because
a flex body ignores Bootstrap's float and would make it the first item
of the row, and its clearance sits on the title, the one line that
reaches that corner, so the lists get the whole column. A link text is
one unbroken string, and one LinkedIn path is 600px of percent-encoded
emoji: it breaks where it has to instead of running out of the modal.

The bio is a section with a heading, like the description and the lists
under it, and it is left out when the person has none, heading and all.
It was a line of the header with a bold "Bio:" label, the one block of
the modal with a label of its own kind. One "About" section for the
bio, the description and the links was the other option. It would mix
prose and a list under one heading, and 14 people have neither text, so
it would have to decide what to show over the links alone.

The links section is called Social Links: the modal links to event
pages and to the organizers pages as well, and "Links" alone did not
say which of them the last section held.

The event and social link lists are marked as lists. Both dropped the
marker and the padding, so five events were five loose lines with
nothing to say they were one list. One rule on the section lists gives
both a disc marker and the indent that goes with it, the width of the
marker and a space, so the two lists read the same.

On an event line, bold carries the roles and italic the words that join
them, "and" and "at". The role was italic and "at" plain, which read as
emphasis on the role and left "Mentor and Speaker" as one italic run
where the "and" looked like part of a role name. Now a reader can pick
the role out of the line.
A link published over http invites a reader to follow it in the clear,
and the site it points at is not ours to make safe. The check now fails
on one rather than warning.

Two values moved. content/sponsors/tecnologas.md had the only http site
field, which nothing checked until the sponsor field was renamed from
web to site. content/people/ifosch.md had one inside a short bio, which
no field check sees: Hugo turns a bare URL in that text into a real
anchor, so the page carried an http link that no rule covered.

Neither domain resolves any more, over http or https: tecnologas.org and
entredevyops.es have no A record and no NS record at all. The links were
already broken, and David's standing decision is to leave a dead link
alone because we cannot know whether its owner will fix it. What changes
is that we no longer publish it insecurely.
… any

bin/check-content refuses an http value in a site field, and it stops
there: a URL written inside prose or inside raw HTML in a content file
is not a field. One short_bio ended with a bare http URL, Hugo turned it
into a live anchor, and the page shipped an insecure link that no field
rule could see. The built site still carried six more: three on the
PyData 2017 page, the Pytorch guide on PyDay 2018, the Meetup
registration redirect on PyDay 2022 (written twice), and the PyLadies
parent site on the PyLadies about page.

Each host answers over TLS: 200 for the blog, pydata.org, slides.com and
pyladies.com, 403 for esade.edu (a bot block, not a TLS failure), and
404 for the bare clicks.meetup.com root, which shows the host serves
https even though the full tracking path was not requested. Only the
scheme changes: host, path, query and fragment are as they were.

The clicks.meetup.com URL is a newsletter tracking redirect with a
per-recipient token in its query. It keeps its scheme change here and
nothing else; whether to replace it with its destination is a separate
decision.

The built page is the one place where that class is visible whatever
printed it, so the rule now lives in bin/check-rendered. Every attribute
in URL_ATTRIBUTES counts the same, href and src alike, plus a meta
refresh and a url() in an inline style. What the browser does with each
differs (mixed content blocked, a reader sent out in the clear), the fix
does not, so one kind and one severity. The rule rides the attribute
walk and split_url that already exist, and costs about 0.02 s on the
108-page build.

A localhost or 127.0.0.1 address is reported under the same kind with a
note: https would not make it reachable, the fix is to remove the link.

An error with no allowlist entry. This site publishes no insecure link;
when a host does not answer over https its owner has to fix it, and the
message says so.

archives/ is exempt from this one rule, through INSECURE_EXEMPT. Those
56 pages are frozen copies of retired sites and their 132 http URLs are
part of the record. The rest of the check still reads them. An allowlist
entry would have printed two lines per URL on every run; the report
prints one line with the count instead.
The two registration links on that page went through
clicks.meetup.com/ls/click, a click tracker from a Meetup newsletter.
Its upn parameter is 825 characters that decode to an opaque
per-recipient token, so the page handed every reader to a tracker, and
the destination was not readable from the URL.

The destination is the Eventbrite page the event always had, and every
other year links it directly:

    2018  entradas-pyday-barcelona-2018-50585714233
    2021  entradas-pydaybcn-2021-212686419807
    2022  entradas-pydaybcn-2022-470666304587   (this one)
    2023  entradas-pydaybcn-2023-751258453257
    2024  entradas-pydaybcn-2024-1068930563089

2022 was the only page that did not. The URL answers 200 and its slug
carries the year, so it is the same event.
The blog in that bio is a GitHub Pages site whose CNAME is on www only:
www.entredevyops.es resolves to edyo.github.io and answers 200, and the
apex entredevyops.es has no record at all.

An earlier commit here said the domain was dead. That was wrong, and the
mistake was testing only the apex.
Running bin/check-content writes bin/__pycache__. It was invisible here
only because this machine's global gitignore lists __pycache__, and a
global gitignore does not travel with the repository: on anyone else's
clone the directory shows as untracked and can be committed by accident.
…io section

A person file carried free text in two places: a short_bio field in the
front matter and the body. For 98 of 136 people only one of the two was
filled, and which one depended on who filled the card in, so the modal
showed a "Bio" heading for some people and a "Description" heading for
others over the same kind of text. The field was not short either: 22
values were over 200 characters and the longest was 1508.

The field is gone. Where a file had only a short_bio, that text is now
the body. Where it had both (24 people), the short_bio goes first, then
a blank line, then the body as it was: in every one of the 24 the bio
is a one-line title and the body is the paragraph. The 15 empty
short_bio keys are removed with nothing to move.

content/people/marc-pomar.md had short_bio "t", one letter, which is
not a bio: it is dropped, and so is the empty github value next to it.

The front matter was parsed, not edited with a regex: each removal was
checked to leave the other keys unchanged and the body is written back
verbatim.

The modal now has one text section where it had Bio and Description.
It is called Bio, because this is a person and "Description" says
nothing, and it is left out when the body is empty, which is 14 people.

The archetype no longer declares short_bio, so a new person file does
not reintroduce the field. bin/check-content reports the field as an
error with the fix in the message: the one way it comes back is a file
copied from an old commit, and the template would then drop the text in
silence. The bin/check-rendered note on the http link it once caught in
a short_bio stays, and now says where that text lives.

Checked: the planted field makes bin/check-content exit 1; without it,
0 errors and 22 warnings, as before. bin/build emits the same output
paths and bin/check-rendered passes. The text of all 185 person modals
on the built pages is the same before and after, apart from the 24 that
now show one block.
An empty value is the same as no field to Hugo, so it shows a reader
nothing. What it does is look like a checked fact: seven person files
carried github: "" and nobody had looked for a GitHub account for any of
them. The check now refuses one, so the choice is to fill it or to drop
the line.

The archetypes are what should say which fields exist, and both were
wrong:

  people.md was TOML while all 137 person files are YAML, so hugo new
  made a file in a format the rest of the folder does not use. It also
  listed twitter before linkedin, not the order the icons are in, had no
  site, and still declared short_bio.

  sponsors.md declared a logo field that no template has ever read, and
  web for the home page, which is called site since the person and
  sponsor names were merged.

Both now carry a comment saying to delete a field that does not apply,
which is what the new rule requires.
The README listed the sponsor fields and never the person ones, so the
only record of what a person file can carry was the archetype. It also
said people_per_line has to be one of the divisors of twelve, which was
true while the grid used Bootstrap column classes and has not been since
the row caps its own width.

Added: a Person fields section with every field, the rule that an empty
field comes out rather than stays, where the bio goes now that short_bio
is gone, and a table of the five shape rules the check applies to a URL
with the defect each one caught. The sponsor list now points at the same
rules instead of describing its own, since one function checks both.

Also brought up to date: pillow in the local check command, the two rows
of the checks table, and the rendered check, which now reports an http
URL and exempts the frozen archives from that rule alone.
A sponsor renders as its logo, linked to its site. There is no page per
sponsor, so id, name, logo_image and site are every field a template
reads and nothing else in the file reaches a reader.

Thirty-two files carried a twitter, linkedin, mastodont or instagram URL
that has never been published, and one carried a body. Somebody wrote
each of them expecting it to show, and nothing told them it did not.
Removed, and the check now rejects any field that is not one of the
four, so the next one is caught when it is written rather than years
later.

The hybridtheory note moves from the body into a comment, which is what
it always was: it explains why that sponsor has no site field.

The archetype and the README said a body appears in a detail view. There
is no detail view. Both now say what the four fields are and that a body
is never published.
The comments explain each rule with the numbers it comes from, and six
blocks still quoted the numbers from before the card went to six on a
row. They said a 200px card and a 220px slot, when the card is 178px and
the slot 190px, and one said an event page card is 186px when it is the
same 178px as everywhere else.

That is worse than no comment. A reader checking the arithmetic against
the constants two lines above finds it does not hold, and has no way to
tell which of the two is current.

Rewritten against the tree as it is: 6.8cqi is 12.1px on the 178px card,
16cqi is 28.5px on it and reaches its 32px cap on a wider one, and four
slots of 190px are what fit the 795px an event page leaves.

Where a number only illustrated a point, it is gone rather than updated:
the exact width the icon row survives on the narrowest card is a figure
nobody can check without a browser, and the point stands without it.

No rule changed, only the comments. The sections still render 5, 6, 4+4
and 3 with a 178px card.
The archetypes emitted every field with an empty value, and the check
that landed with them rejects an empty value. So the documented way to
start a person file produced six errors before the contributor had typed
anything, and the sponsor one two, with the archetype's own comment
explaining the rule it was breaking.

Only id and name are filled in now, name from the file name so it is
obviously a placeholder to correct. Every other field is commented out
with what it is for, so the list of what exists is still there to read
and nothing empty reaches the file.

Checked by running both commands and the check: 0 errors, and the two
warnings a new unlisted file is expected to raise.
@DZPM
DZPM force-pushed the pr/10-github-handles branch from 59906f8 to 14c88d9 Compare October 7, 2026 09:54
@DZPM

DZPM commented Oct 7, 2026

Copy link
Copy Markdown
Member Author

I'm closing this PR, and will open another PR with the feedback.

Add your feedback here, or in private (as Laura & Ruben did)

@DZPM DZPM closed this Oct 7, 2026
@DZPM DZPM reopened this Oct 7, 2026
@DZPM

DZPM commented Oct 7, 2026

Copy link
Copy Markdown
Member Author

My mistake, and we need #202 merged first

@DZPM
DZPM changed the base branch from pr/9-fix-xml-surface to edition October 8, 2026 16:03
@DZPM
DZPM merged commit 3f63e88 into edition Oct 8, 2026
6 checks passed
@DZPM DZPM mentioned this pull request Oct 8, 2026
32 of 34 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants