Repository navigation
The person card, the modal, and one field per thing - #204
Conversation
|
To compare, some examples, I'm showing the first person per category: BeforeBefore: https://pybcn.org/pybcn_association/organizers/
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
Added his Linkedin too. Notice we now show all roles, events, etc.
Different fields. The social urls are now clean (no https, GET params, trailing
Notice the "short bio" and "description" field have been collapsed into a single one (it was a huge, inconsistent mess)
Also, Laura and Natàlia had duplicated files (one as organized, one as ex-board), it's all consolidated now.
More ideas, suggestions, feedback, very welcome. Future work:
|
e127979 to
71ed7e4
Compare
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.
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.
59906f8 to
14c88d9
Compare
|
I'm closing this PR, and will open another PR with the feedback. Add your feedback here, or in private (as Laura & Ruben did) |
|
My mistake, and we need #202 merged first |










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_biois 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-contentgains 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-renderedgains 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.htmlandpeople_summary.html. That is the new behaviour.people.scssis 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.