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
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
8 changes: 8 additions & 0 deletions .github/workflows/gh-pages.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,14 @@ jobs:
- name: Build
run: hugo --minify

- name: Build the search index
# Pagefind reads the pages Hugo wrote and writes public/pagefind/:
# the engine, the word index in chunks, one fragment per page. It
# runs after Hugo because it indexes what is on disk, and it is
# pinned the way Hugo is: bin/install-pagefind fetches the release
# named in .pagefind-version and checks its SHA-256.
run: bin/install-pagefind && bin/search public

- name: Deploy
uses: peaceiris/actions-gh-pages@84c30a85c19949d7eee79c4ff27748b70285e453 # v4.1.0
with:
Expand Down
60 changes: 60 additions & 0 deletions .github/workflows/links.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# Check the external links of the site.
#
# Not part of the pull request checks. A link to another site breaks when
# that site changes, which is not something a pull request of ours causes
# or can fix, and asking for 139 external URLs on every pull request is
# both slow and close to behaving like a crawler. Run this when you want
# to know, which in practice is before a round of archive work.
#
# It never turns anything red. Read the report in the job summary.
name: links

on:
workflow_dispatch:

permissions:
contents: read

jobs:
external:
name: Check the external links
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

- name: Read the Hugo version
run: echo "HUGO_VERSION=$(tr -d '[:space:]' < .hugo-version)" >> "$GITHUB_ENV"

- name: Setup Hugo
uses: peaceiris/actions-hugo@2752ce1d29631191ea3f27c23495fa06139a5b78 # v3.2.1
with:
hugo-version: ${{ env.HUGO_VERSION }}
extended: true

- name: Build
run: hugo --minify -d public --baseURL /

- name: Check links
uses: lycheeverse/lychee-action@e7477775783ea5526144ba13e8db5eec57747ce8 # v2.9.0
with:
# --exclude-path: the frozen 2016-2019 snapshots under archives/
# keep their dead links on purpose.
# --exclude: hosts that answer bots with 400, 403, or 999, and
# canary.invalid, the host the canary fixture names as text.
# lychee reads URLs out of text nodes too, and .invalid never
# resolves.
args: >-
--no-progress
--exclude-all-private
--max-concurrency 4
--cache
--max-cache-age 1w
--root-dir "$GITHUB_WORKSPACE/public"
--exclude-path 'public/archives/'
--exclude 'twitter\.com'
--exclude '^https?://(www\.)?x\.com/'
--exclude 'linkedin\.com'
--exclude 'pybcn\.slack\.com'
--exclude 'canary\.invalid'
'public/**/*.html'
fail: false
56 changes: 39 additions & 17 deletions .github/workflows/pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,21 @@ jobs:
- name: Build
run: hugo --minify

- name: Build the search index
# Pagefind reads the pages Hugo wrote and writes public/pagefind/:
# the engine, the word index in chunks, one fragment per page. It
# runs after Hugo because it indexes what is on disk, and it is
# pinned the way Hugo is: bin/install-pagefind fetches the release
# named in .pagefind-version and checks its SHA-256.
run: bin/install-pagefind && bin/search public

content:
name: Check the content
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0 # bin/authors --check reads the whole history

- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
Expand All @@ -64,6 +74,19 @@ jobs:
- name: Check content for dangerous raw HTML
run: bin/check-html-safety

- name: Check that AUTHORS is what bin/authors writes
# The check writes the file again in memory and compares, so a new
# contributor, a second account of somebody already here, and a name
# that changed on a person record are all caught. It reads the
# GitHub API for the account of each commit and for the reviews,
# with the token the job already has; when the API cannot be reached
# it says it skipped and passes, because an outage of somebody else
# is not a broken build. The reviews come through GraphQL, three
# requests for the 205 pull requests, which is why it takes seconds.
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: bin/authors --check

- name: Check the text normalizer
run: python3 bin/canon.py --selftest

Expand Down Expand Up @@ -117,29 +140,28 @@ jobs:
# runner's python3 is enough.
run: bin/check-rendered public

- name: Check links
- name: Check the internal links
uses: lycheeverse/lychee-action@e7477775783ea5526144ba13e8db5eec57747ce8 # v2.9.0
with:
# --root-dir: where "/path/" links resolve. Without it lychee flags
# every absolute link in a local file as an error.
# --offline: only the links that point inside this build. The
# external ones are the job of .github/workflows/links.yml,
# which runs on request. The site holds 139 distinct external
# URLs outside the frozen snapshots, and checking them took 25
# minutes on the pull request that added the person pages: with
# the concurrency low enough not to look like a crawler, every
# pull request asked every host we have ever linked to whether
# it is still there, and reported rot that is not ours to fix.
# An internal link that does not resolve is a typo, is ours,
# and is found in seconds.
# --root-dir: where "/path/" links resolve. Without it lychee
# flags every absolute link in a local file as an error.
# --exclude-path: the frozen 2016-2019 snapshots under archives/
# keep their dead links on purpose.
# --exclude: hosts that answer bots with 400, 403, or 999, and
# canary.invalid, the host the canary fixture names as text.
# lychee reads URLs out of text nodes too, and .invalid never
# resolves.
args: >-
--no-progress
--exclude-all-private
--max-concurrency 4
--offline
--root-dir "$GITHUB_WORKSPACE/public"
--exclude-path 'public/archives/'
--exclude 'twitter\.com'
--exclude '^https?://(www\.)?x\.com/'
--exclude 'linkedin\.com'
--exclude 'pybcn\.slack\.com'
--exclude 'canary\.invalid'
'public/**/*.html'
# External sites rate-limit. Keep the report in the job summary and
# do not turn the check red.
fail: false
# An internal link is ours, so this one does turn the check red.
fail: true
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
.idea
bin/hugo
bin/pagefind
public/**

# Hugo build output and caches. resources/ covers the SCSS cache and the
Expand Down
1 change: 1 addition & 0 deletions .pagefind-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
1.5.2
44 changes: 39 additions & 5 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,22 +14,35 @@ bin/install # downloads the pinned Hugo binary, verifies its SHA256
bin/serve # dev server, Hugo's default :1313
bin/build # writes to docs/
bin/hugo --quiet -d /tmp/out # the binary directly
bin/install-pagefind # the pinned Pagefind binary, verifies its SHA256
bin/search /tmp/out # the search index of a built site, after the build
```

**The Hugo binary lives at `bin/hugo` and is gitignored.** `bin/helpers` prefers
it over anything on `$PATH`, and checks the version from `.hugo-version`.

**The search index is not built by Hugo.** `bin/search <built-dir>` runs
Pagefind (pinned in `.pagefind-version`, binary at the gitignored
`bin/pagefind`) over the pages Hugo wrote, and CI runs it right after
`hugo --minify`. The dev server has none, so `/search/` says so there. What
is indexed is what the templates mark `data-pagefind-body`: the record of
each talk, event and person, and the photo captions, not the whole page.
`partials/search_kind.html` says which pages, and `partials/hero_header.html`
and the layouts of the three kinds say what of each.

**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 with `-D` produces **1369 HTML pages**: 1313 Hugo-generated and
A clean build with `-D` produces **1377 HTML pages**: 1321 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
import added a page per meetup and per talk, 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:
the canary fixture included), and 1363 until the tabs of `/talks/` and
`/people/` added a page per talk type and per role (the two content adapters
under `content/`), which is how quickly a figure like this goes stale.
Recount with:

```sh
find <built-dir> -name '*.html' | wc -l
Expand All @@ -45,6 +58,26 @@ and checking the output, not by reading the file.
**`merge.ff = only`.** Any local merge needs `--no-ff`. A failed merge here reports
"Not possible to fast-forward", which is not a conflict.

**A photograph belongs to the event its text names, not to the day it was
posted.** `bin/import-x-photos --ids FILE` takes the posts to import, and the
day stops deciding. Reading the export by date looks right and is wrong three
ways, each found the hard way on 2026-10-17:

- The account replies to its own posts of years before, so following the chain
of replies builds threads that run from 2014 to 2021. Cut the chain at four
days from the photograph.
- A talk titled in two or three generic words matches any post that uses them:
a 2014 talk swallowed every post that said "machine learning" since. A title
has to be long and belong to one event before it can attribute on its own.
- Some titles repeat anyway. There are several "Introduction to Python", and
"Barcelona Python Meetup" is both the name of the event of 2007 and what
every monthly meetup is called. There the text says what the event is and
the day says which one of them, which is the rule a person would use.

A post with photographs and no text at all is not imported: the caption is the
alt text of the image, and a gallery leaves a photograph out rather than
publish one nobody can read.

**The archive imports run in one order and only once.** `bin/import-meetups`,
then `bin/import-talks`, then `bin/import-videos`. `bin/import-talks` replaces
the whole `talks` block of a page and everything after it in the front matter,
Expand Down Expand Up @@ -219,8 +252,9 @@ plausible but different result.
| `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, `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). Plus `channel_videos.yaml`, the inventory of the YouTube channel, which `bin/check-content` reads and `bin/list-channel` refreshes |
| `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 |
| `bin/` | Setup and build scripts, the checks, `bin/list-channel` (names a channel video the inventory lacks; needs `yt-dlp` and the network), `bin/new-meetup` (writes the page of one past meetup from its Meetup URL, and `--discover` names the ones with no page; needs the network), the one-time `bin/import-*` scripts, plus the gitignored Hugo binary |
| `.hugo-version` | The pinned Hugo version, single source of truth |
| `.pagefind-version` | The pinned Pagefind version, same rule; `bin/install-pagefind` reads it |

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

Expand Down
52 changes: 52 additions & 0 deletions AUTHORS
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
PyBCN publishes this site. These are the people who built it, in the order
they arrived. Writing it and reviewing it both count, because a change that
nobody reads is not finished, and the years are the first and the last one
each person worked on it.

The name of each one is the name their page of the site carries, or the name
they signed their own commits with when they have no page. A person who would
rather appear another way can change it in a pull request, because this file
is generated: from the commits, from the reviews, and from the pages.

The people behind the events themselves, the speakers, the organizers, the
venues and the sponsors, are named on the pages that stay current, which
static/humans.txt links.

Written by bin/authors.

Areski Belaid (@areski) wrote, 2013 to 2015.
https://pybcn.org/people/areski-belaid/
Núria Pujol Vilanova (@llevaNEUS) wrote, 2014 to 2015.
https://pybcn.org/people/nuria-pujol-vilanova/
David Arcos (@DZPM) wrote and reviewed, 2015 to 2026.
https://pybcn.org/people/david-arcos/
Jordi Soucheiron (@jsoucheiron) wrote, 2015 to 2016.
https://pybcn.org/people/jordi-soucheiron/
Loris Mularoni (@batterio) wrote, 2015.
https://pybcn.org/people/loris-mularoni/
Ignasi Fosch (@ifosch) wrote and reviewed, 2016 to 2026.
https://pybcn.org/people/ignasi-fosch/
Laura Pérez Mayos (@lpmayos) wrote and reviewed, 2016 to 2025.
https://pybcn.org/people/laura-perez-mayos/
Israel Saeta Pérez (@dukebody) wrote and reviewed, 2018 to 2024.
https://pybcn.org/people/israel-saeta-perez/
Ruben Berenguel (@rberenguel) wrote and reviewed, 2018 to 2023.
https://pybcn.org/people/ruben-berenguel/
Juan Luis Cano Rodríguez (@astrojuanlu) reviewed, 2018.
https://pybcn.org/people/juan-luis-cano-rodriguez/
Natàlia Padilla (@NataliaSirera) wrote and reviewed, 2019 to 2025.
https://pybcn.org/people/natalia-padilla/
Dimitri Herrero (@dim1tri) wrote, 2019.
https://pybcn.org/people/dimitri-herrero/
Xavi Francisco Gilabert (@srxavi) wrote and reviewed, 2021 to 2022.
https://pybcn.org/people/xavi-francisco-gilabert/
Ferran Jovell (@mrswats) wrote and reviewed, 2023 to 2024.
https://pybcn.org/people/ferran-jovell/
Alicia Morales Carrasco (@amoralca16) wrote and reviewed, 2024 to 2025.
https://pybcn.org/people/alicia-morales-carrasco/
Sergi Ramírez (@ramitjansIA, @ramitjans) wrote, 2025.
https://pybcn.org/people/sergi-ramirez/
Daniel Mesejo León (@mesejo) wrote and reviewed, 2025 to 2026.
https://pybcn.org/people/daniel-mesejo-leon/
Alberto Cámara (@ber2) reviewed, 2026.
https://pybcn.org/people/alberto-camara/
14 changes: 12 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,8 +97,18 @@ Edit the file on GitHub, or clone the repository and follow the steps below.
[Person photos](README.md#person-photos).
- **A sponsor**: run `bin/hugo new sponsors/my-sponsor.md` and fill the four
fields listed in [How to add a new sponsor](README.md#how-to-add-a-new-sponsor).
The logo goes under `themes/pybcn_theme/assets/images/sponsors/`. Then list
the sponsor on the sponsors page, or on the event, that shows it.
The logo goes under `themes/pybcn_theme/assets/images/sponsors/`. The
[sponsors page](https://pybcn.org/sponsors/) reads the directory, so the
new file reaches it by existing; list the sponsor on its event as well, in
the `sponsor_levels` of that page, which is what gives it a level.
- **A monthly meetup**, after it happened: run `bin/new-meetup --discover`
to see which ones have no page yet, then
`bin/new-meetup <the Meetup URL of the event>`. It writes the page from the
public event page: the title, the day, the venue, the number of
registrations, the source URL, and the group. It writes no talk, because the
announcement says them in prose: pass `--description` to print that prose,
and see [Import a meetup](README.md#import-a-meetup) for the rest of the
path, which is the same one the 268 pages of the archive came through.
- **An event**: copy the previous edition under `content/events/`, for example
`content/events/pyday_bcn/pyday_bcn_2025.md`, and edit it. The sections, the
agenda and the people grids are described in
Expand Down
6 changes: 3 additions & 3 deletions LICENSING.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ case (section 2(b)(2) of the legal code).

The PyBCN logo and its variants (`themes/pybcn_theme/assets/images/logo.png`,
`themes/pybcn_theme/assets/images/favicon.png`, `static/favicon.ico`,
`static/favicon.png`, and the files under `static/images/backgrounds/`)
and the files under `static/images/backgrounds/`)
identify the association. They are not covered by the content licence: use
them to refer to PyBCN, not to suggest that PyBCN endorses something it has
not seen. The PyLadies name and logo in that same folder are not the
Expand All @@ -174,8 +174,8 @@ with it:
- `themes/pybcn_theme/static/vendor/font-awesome/webfonts/`: the Font Awesome
fonts (SIL OFL 1.1).
- `themes/pybcn_theme/assets/css/bootstrap.min.css`: Bootstrap 4.0.0 (MIT).
- `themes/pybcn_theme/assets/images/anon_member.png`: the Font Awesome `user`
icon as a PNG (CC BY 4.0).
- `themes/pybcn_theme/assets/images/anon_member.webp`: the Font Awesome `user`
icon as a WebP (CC BY 4.0).

### Archived sites

Expand Down
Loading
Loading