Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 14 additions & 7 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,18 @@ it over anything on `$PATH`, and checks the version from `.hugo-version`.
**Do not `pip install hugo`.** That PyPI package now requires a Go toolchain and
fails. `bin/install` fetches the official release binary instead.

A clean build produces **109 HTML pages**: 53 Hugo-generated and 56 copied
from `static/archives/`. Recount rather than trusting this line: the number
moves with the content. It was 317 until #202 stopped publishing the 206
person and sponsor pages that nothing linked to, which is how quickly a
figure like this goes stale.
A clean build with `-D` produces **1369 HTML pages**: 1313 Hugo-generated and
56 copied from `static/archives/`. Recount rather than trusting this line: the
number moves with the content. It was 317 until #202 stopped publishing the
206 person and sponsor pages that nothing linked to, 109 until the archive
import added a page per meetup and per talk, and 991 until the person pages
came back with the Archive (378 of them, one per record of `content/people/`,
the canary fixture included), which is how quickly a figure like this goes
stale. Recount with:

```sh
find <built-dir> -name '*.html' | wc -l
```

## Traps that have already cost time

Expand Down Expand Up @@ -198,15 +205,15 @@ plausible but different result.
| `themes/pybcn_theme/` | The theme, **vendored in-tree**, not a submodule |
| `themes/pybcn_theme/layouts/partials/` | Most of the interesting template logic |
| `static/archives/` | Verbatim mirrors of pre-Hugo sites, copied unprocessed |
| `data/` | Tables the templates read: `roles.yaml` maps a heading to a role, `role_emoji.yaml` a role to a glyph |
| `data/` | Tables the templates read: `roles.yaml` maps a heading to a role, `role_emoji.yaml` a role to a glyph, `event_emoji.yaml` an event to a glyph, `venues.yaml` the exact `venue` string of an event to the short name the `/events/` list prints (the page of the meetup keeps the full string) |
| `bin/` | Setup and build scripts, the checks, `bin/list-channel` (names a channel video the inventory lacks; needs `yt-dlp` and the network), the one-time `bin/import-*` scripts, plus the gitignored Hugo binary |
| `.hugo-version` | The pinned Hugo version, single source of truth |

The checks, which are what a pull request has to pass:

| Script | What it rejects |
|---|---|
| `bin/check-content` | A broken reference, a photo that is not square, a source far larger than the build asks for, a field that no longer exists, an `end_date` that is not a date or not later than `date` |
| `bin/check-content` | A broken reference, a photo that is not square, a source far larger than the build asks for, a field that no longer exists, a `venue` string with no row in `data/venues.yaml`, a row of that table no event uses, an `end_date` that is not a date or not later than `date` |
| `bin/check-html-safety` | Dangerous raw HTML in content, with an allowlist for the legitimate embeds |
| `bin/check-rendered` | The same, on the built pages, plus any `http://` URL. Run it against a directory you built, never against `public/` while a dev server is writing there |

Expand Down
18 changes: 15 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -230,11 +230,22 @@ for those.

There are three pages displaying people: PyBCN Organizers, PyLadiesBCN Organizers and PyLadiesBCN speakers. More can be added to display, for example, collaborators of other events, etc.

`/people/` is the index of the whole section, and it is a list and not a
grid: one row per person record, grouped by the first letter of the id, with
the roles, the events, the talks, and a link to every page that credits the
person. It needs no front matter of its own and reads the records and
`partials/appearances_index.html`, so a new person file gets a row by being
added. The template is `themes/pybcn_theme/layouts/people/list.html` and the
style is in `themes/pybcn_theme/assets/scss/people_index.scss`. Each person
file builds a page of its own, `/people/<id>/`, from
`themes/pybcn_theme/layouts/people/single.html`; the card of a people grid
links to it.

All pages rely on the same template: `themes/pybcn_theme/layouts/_default/people.html`. The individual person summary is defined in a separate partial template: `themes/pybcn_theme/layouts/partials/people_summary.html`, and the style are defined in `themes/pybcn_theme/assets/scss/people.scss`.

The template iterates over the defined levels, and for each level:
- displays the level name, if 'name' is provided
- displays a summary of all the people listed, limiting the number of people per line to the provided number 'people_per_line' (which must be in [12, 6, 4, 3, 4, 1])
- displays a summary of all the people listed, limiting the number of people per line to the provided number 'people_per_line' (which must be in [12, 6, 4, 3, 2, 1])

To add new people to the site, there is a Hugo archetype defined in `themes/pybcn_theme/archetypes/people.md` that can be used by running the command: `hugo new people/my-new-person.md`.

Expand Down Expand Up @@ -427,9 +438,10 @@ it. The cost is that the reasons are scattered, so this is the index.

| Decision | Where it is argued |
|---|---|
| A person file builds no page, and `/people/` is a 404 | The `[[cascade]]` comment in `config.toml`, and issue #201 |
| A person file builds a page, `/people/<id>/`, and `/people/` is the list that reaches every one of them | The `[[cascade]]` comment in `config.toml`, `themes/pybcn_theme/layouts/people/single.html`, `themes/pybcn_theme/layouts/people/list.html`, and issue #201 |
| `/people/` is a list and not a second card grid, ordered by letter and not by how often a person appears | `themes/pybcn_theme/layouts/people/list.html` |
| An appearance line calls a page by `appearances_title`, not by its title | `themes/pybcn_theme/layouts/partials/appearances_index.html` |
| The modal is on its way to being a page per person, so its content cannot depend on the page it was opened from | `themes/pybcn_theme/layouts/partials/person_appearances.html` |
| A card of a people grid is a link to the page of the person, not a modal that shows a subset of it | `themes/pybcn_theme/layouts/partials/people_summary.html` |
| The menu sections are a disclosure, not an ARIA menu | `themes/pybcn_theme/layouts/partials/nav.html` |
| MIT for the code, CC BY-SA 4.0 for the content, and nothing for the photographs | [LICENSING.md](LICENSING.md) |
| `edition` is the source branch and `master` is the built site | [AGENTS.md](AGENTS.md) |
Expand Down
107 changes: 107 additions & 0 deletions bin/check-content
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@ match its filename, a duplicate id, a page that lists a person or sponsor id
with no page behind it, a declared photo with no file behind it, and a
remote photo_url. It warns about a person photo with no colour in it.

For the venues it checks data/venues.yaml, the short name the /events/ list
prints for each venue, against the pages: every venue string of an event
front matter has a row, and every row is the venue of some event. A venue
with no row prints its full string in the list, and a row with no event is
a name the site stopped using.

For the days it checks end_date, which an event carries when it ran more
than one day: it is a date, and it is later than date. The template prints
one day for an end_date it cannot use, so the mistake is silent on the page.
Expand Down Expand Up @@ -807,6 +813,104 @@ def check_links(path, label, links):
f"else is listed under Repository.")


# The short name of each venue, keyed by the exact string the venue field of
# an event carries. layouts/_default/events_index.html prints the short name
# in the /events/ list and falls back to the full string for a venue with no
# row, which is the gap check_venues reports.
VENUES = ROOT / "data" / "venues.yaml"

# What check_venues counted, for the summary line.
_venue_summary = None


def page_venues():
"""Return {venue string: [path]} for every venue of content/events/.

The venue field holds the name the way its source wrote it, one string
per page, and the string is the key: two spellings of one place are two
entries here and two rows of data/venues.yaml. A venue that is not a
string is reported and skipped, because the template would print it
as text and the table could never hold a row for it.
"""
found = {}
for path in sorted((CONTENT / "events").rglob("*.md")):
data = front_matter(path)
if data is None or "venue" not in data:
continue
venue = data["venue"]
if not isinstance(venue, str) or not venue.strip():
error(path, f"venue holds {venue!r}, which is not a name. It is the "
f"place as its source wrote it, one string.")
continue
found.setdefault(venue, []).append(path)
return found


def check_venues():
"""Check data/venues.yaml against the venue field of the event pages.

Two rules, one per direction, so the table cannot rot either way:

1. every venue string of an event front matter has a row. The list
prints the full string for a venue with no row, so a long name
comes back in silence the day an event arrives with a venue the
table does not know.
2. every row is the venue of at least one event. A row no page uses
is a name the site stopped using, or a string somebody edited in
the event and not here, and either way it is a row nobody checks.

Also reported: a row whose short name is empty or not a string, since
the list would print nothing for that venue.

Report, never crash: a missing or malformed table is an error with the
reason, and the rest of the check still runs.
"""
global _venue_summary
venues = page_venues()
if not VENUES.is_file():
error(VENUES, "is missing. It is the short name of each venue, which "
"the /events/ list prints, one row per venue string of "
"the event pages.")
return
try:
table = yaml.safe_load(VENUES.read_text(encoding="utf-8"))
except yaml.YAMLError as exc:
error(VENUES, f"is not valid YAML: {exc}")
return
if not isinstance(table, dict):
error(VENUES, f"holds {type(table).__name__}, not a mapping of the "
f"venue string to its short name")
return

for long_name, short_name in table.items():
label = f"row {long_name!r}"
if not isinstance(long_name, str):
error(VENUES, f"{label} has a key that is not a string. The key is "
f"the venue string exactly as the event carries it.")
continue
if not isinstance(short_name, str) or not short_name.strip():
error(VENUES, f"{label} has the short name {short_name!r}. It is the "
f"name the list prints, so it is a non-empty string.")
if long_name not in venues:
error(VENUES, f"{label} is the venue of no event page. Remove the "
f"row, or fix the venue of the event that renamed it.")

for long_name, paths in sorted(venues.items()):
if long_name in table:
continue
for path in paths:
error(path, f"venue {long_name!r} has no row in data/venues.yaml. "
f"Add the row with the short name the /events/ list "
f"prints; without it the list prints the full string.")

pages = sum(len(paths) for paths in venues.values())
shortened = sum(1 for k, v in table.items()
if isinstance(k, str) and isinstance(v, str) and k != v)
_venue_summary = (f"{len(table)} venues in data/venues.yaml, {shortened} "
f"shortened, {len(set(table.values()))} short names; "
f"{len(venues)} distinct venue strings on {pages} pages.")


def as_date(value):
"""The date a front matter value holds, or None when it holds none.

Expand Down Expand Up @@ -1072,6 +1176,7 @@ def main():
listed_people = check_listings(people, sponsors)
check_agenda(people)
check_dates()
check_venues()
check_orphans(people, listed_people)
check_security_txt()
check_association()
Expand All @@ -1084,6 +1189,8 @@ def main():
talks = sum(len(ids) for ids in _talk_ids.values())
print(f"\n{len(people)} people, {len(sponsors)} sponsors, "
f"{talks} talks on {len(_talk_ids)} pages checked.")
if _venue_summary:
print(_venue_summary)
print(f"{len(errors)} errors, {len(warnings)} warnings.")
return 1 if errors else 0

Expand Down
71 changes: 53 additions & 18 deletions config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,26 +33,54 @@ timeout = "300s"
# the modals through its front matter. Hugo also built a page from each one,
# with no inbound link anywhere on the site, and they were 206 of the 247
# entries in the sitemap (#201). The sponsor pages are empty; 73 of the 138
# person files carry a bio that did render. They all stop, bio or not:
# publishing only the people who happen to have a paragraph written about
# them would sort the organizers and speakers of this association into two
# classes. The pages come back with the Archive, when every person has one
# worth linking to.
# person files carried a bio that did render. #202 stopped them all, bio or
# not: publishing only the people who happen to have a paragraph written
# about them would sort the organizers and speakers of this association into
# two classes. The condition set then for their return was that the pages
# come back with the Archive, when every person has one worth linking to.
#
# render = "never" stops the page and keeps the default list = "always", so
# the file stays in site.Pages, where people-grid.html and sponsors-grid.html
# look it up by id. list = "local" would take it out of site.Pages and empty
# every grid. A page that is not rendered has no permalink, which is how the
# feed template leaves it out (layouts/_default/rss.xml).
# The Archive met it, and the person pages are back: /people/<id>/, from
# themes/pybcn_theme/layouts/people/single.html, one per record, the way
# the site has a page per event and a page per talk (David Arcos,
# 2026-10-13). Measured in this tree when the pages came back: 378 files
# under content/people/, one of them the draft canary fixture of
# bin/check-rendered, so 377 records. 353 are credited by at least one
# talk, 124 are listed in at least one page block (the board, a committee,
# a team, a speaker list), and 371 by one or the other. The content of a
# person page is what the Archive says about the person: the talks, each
# linking to its talk page, the photos of those talks, the events, and the
# roles, plus the photo, the bio, and the links of the record itself. So a
# page is no longer a heading with nothing under it, and every one has an
# inbound link: the speaker name of every talk row of /talks/ goes to it
# through partials/person_link.html, and so does the name on its row of
# /people/.
#
# /people/ had nothing but a heading and goes with them. /sponsors/ is a real
# page and stays.
[[cascade]]
[cascade._build]
render = "never"
[cascade._target]
path = "{/people,/people/**}"

# Six records are credited by nothing: david-blazquez-garcia, marc-pomar,
# patricio-reyes-valenzuela, peter-krupa, ricardo-martinez, and
# thomas-alisi (bin/check-content names them in a note). They get a page
# all the same, with the photo, the bio, the position, or the links their
# file carries, and the two-classes argument of #201 is the reason: taking
# the page away from the six whose programme has not been credited yet
# would sort the people of the archive into the ones with a page and the
# ones without, on a fact about the import and not about them. The
# figure that used to be the point here, that 240 of the 378 records hold
# an id and a name and nothing else, is still true and no longer matters:
# the content of a person page comes from the Archive, not from the front
# matter.
#
# The sponsor pages stay stopped. render = "never" stops the page and
# keeps the default list = "always", so the file stays in site.Pages,
# where sponsors-grid.html looks it up by id. list = "local" would take it
# out of site.Pages and empty the grid. A page that is not rendered has no
# permalink, which is how the feed template leaves it out
# (layouts/_default/rss.xml). /sponsors/ is a real page and stays.
#
# The person pages are in the sitemap and not in the home feed:
# layouts/_default/rss.xml leaves the people section out by name, as it
# does the talks, because 377 pages that arrive in one build are not 377
# publications and would push every item a reader could use out of the
# window their reader keeps. /people/ builds no feed of its own: section =
# ["HTML"] above stops every section feed (#201).
[[cascade]]
[cascade._build]
render = "never"
Expand Down Expand Up @@ -136,3 +164,10 @@ unsafe=true
link = "https://github.com/pyladies-bcn"
icon = "fab fa-github fa-2x"


[services]
[services.rss]
# A feed is a window, not an archive. The site has 282 event pages and
# one page per talk and per person, so an unlimited feed would be a few
# hundred items of which a reader would see the newest twenty anyway.
limit = 20
3 changes: 3 additions & 0 deletions content/events/_index.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,7 @@ title: "Events"
description: ""
menu: "main"
weight: 4
layout: events_index
---

The association runs workshops and talks [every month](/events/monthly_events/). The list below is every event it has organized, the most recent first: the monthly meetups since 2007, and the larger days, PyDay BCN and PyDataBCN.
8 changes: 8 additions & 0 deletions content/people/_index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
title: "People"
description: "Every person the archive of the association credits: the board, the organizers, the mentors, the volunteers, and the speakers."
menu: "main"
weight: 3
---

Everyone the archive credits, by name. A row holds the roles the person has held, the years they were here, how many talks they gave, and the pages that say so: the programme of a day, its photos, and the page of the board or the committee they sat on. The board and the committee of today are on [the organizers page](/pybcn_association/organizers/), with a photo and a bio each.
6 changes: 0 additions & 6 deletions content/people/david-blazquez-garcia.md

This file was deleted.

6 changes: 0 additions & 6 deletions content/people/marc-pomar.md

This file was deleted.

9 changes: 0 additions & 9 deletions content/people/patricio-reyes-valenzuela.md

This file was deleted.

10 changes: 0 additions & 10 deletions content/people/peter-krupa.md

This file was deleted.

6 changes: 0 additions & 6 deletions content/people/ricardo-martinez.md

This file was deleted.

6 changes: 0 additions & 6 deletions content/people/thomas-alisi.md

This file was deleted.

8 changes: 8 additions & 0 deletions content/talks/_index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
title: "Talks"
description: "Every talk of every PyBCN event, with its speakers, its photo, its video, and its links."
menu: "main"
weight: 6
---

Every talk the association has put on a programme, newest first. Each one has a page of its own with its speakers, the day it was given, its description, its photo, and whatever the speaker published: the video, the slides, and the code.
Loading
Loading