diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index e8350138..981225f6 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -44,6 +44,11 @@
# instructions. These carry legal and financial statements.
/content/pybcn_association/ @pybcn/web
+# The code of conduct itself. The page under content/pybcn_association/
+# renders this file through the repo-markdown shortcode, so the legal text
+# lives at the root and not in content/, and the rule above does not reach it.
+/CODE_OF_CONDUCT.md @pybcn/web
+
# The custom domain of the GitHub Pages site. A change to this file points
# pybcn.org somewhere else.
/CNAME @pybcn/web
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
new file mode 100644
index 00000000..ff87fee2
--- /dev/null
+++ b/CODE_OF_CONDUCT.md
@@ -0,0 +1,35 @@
+# Code of Conduct
+
+PyBCN is dedicated to providing a respectful, harassment-free community for
+everyone. We do not tolerate harassment or bullying of any community member in
+any form. This does not only extend to members to local PyBCN communities,
+but to anyone who chooses to become involved in the larger PyBCN community of
+users, developers and integrators through events or interactions.
+
+Harassment includes offensive verbal/electronic comments related to personal
+characteristics or choices, sexual images or comments in public or online
+spaces, deliberate intimidation, bullying, stalking, following, harassing
+photography or recording, sustained disruption of talks, IRC chats, electronic
+meetings, physical meetings or other events, inappropriate physical contact, or
+unwelcome sexual attention. Participants asked to stop any harassing or
+bullying behavior are expected to comply immediately.
+
+If a participant engages in harassing behavior, representatives of the
+community may take reasonable action they deem appropriate, including warning
+the offender, expulsion from any PyBCN event, or expulsion from mailing
+lists, IRC chats, discussion boards and other electronic communications
+channels to resolve the issue. This may include expulsion from PyBCN Meetup
+group membership.
+
+If you are being harassed, notice that someone else is being harassed, or have
+any other concerns, please act to intercede or ask for help from any member of
+the PyBCN community, IRC chat admins, website admins, or
+organizers/representatives of any physical events put on under the auspices of
+PyBCN.
+
+This Code of Conduct has been adapted from the
+[PyLadies](https://www.pyladies.com/CodeOfConduct/) one, which is licensed
+under a
+[Creative Commons Attribution-Share Alike 3.0 Unported license](https://creativecommons.org/licenses/by-sa/3.0/),
+and is licensed under a
+[Creative Commons Attribution-ShareAlike 4.0 International license](https://creativecommons.org/licenses/by-sa/4.0/).
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 00000000..152a533f
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,127 @@
+# Contributing to the PyBCN site
+
+This repository holds the source of [pybcn.org](https://pybcn.org/), the site
+of the Python Barcelona association, built with [Hugo](https://gohugo.io). A
+change goes through a pull request against the `edition` branch, and this
+guide says how. The [README](README.md) documents the site itself: the content
+fields, the layouts, and what each check looks for.
+
+## Code of conduct
+
+The PyBCN [Code of Conduct](CODE_OF_CONDUCT.md) applies to every contribution:
+a pull request, an issue, a review, and the discussion around them. It is the
+same text the site publishes at
+[pybcn.org/pybcn_association/coc/](https://pybcn.org/pybcn_association/coc/),
+which renders that file.
+
+## Licence
+
+The code of the site is under the [MIT License](LICENSE) and the content is
+under [CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/).
+[LICENSING.md](LICENSING.md) says which paths each one covers, and which paths
+are not licensed at all: the photographs of people, the person pages, the
+sponsor logos, the association's own logos, the third-party libraries, and the
+archived sites.
+
+A pull request is a contribution under those terms. A change to a template, a
+script, or a stylesheet is offered under the MIT License, and a change to the
+text of a page is offered under CC BY-SA 4.0. Your own page under
+`content/people/` and your photo are different: you keep every right in them,
+and you give PyBCN permission to publish them on this site, which you can
+withdraw.
+
+## Which branch
+
+Open the pull request against `edition`. It is the default branch and it holds
+the source. `master` holds the built site: the `github-pages` workflow writes
+it on every merge to `edition` and replaces its history each time, so nothing
+is edited there, and a change made on `master` is lost on the next deploy.
+
+## Add content without writing code
+
+Most changes to this site are content: a person, a sponsor, an event. Each one
+is a Markdown file with a front matter, and the README documents the fields.
+Edit the file on GitHub, or clone the repository and follow the steps below.
+
+- **A person** (an organizer, a speaker, a mentor): copy a file under
+ `content/people/` and edit it. The front matter carries `id`, which has to
+ match the file name, `name`, `photo`, the role at PyBCN, and the links to
+ the person's own profiles. The bio goes in the body. Leave out any field the
+ person does not have. The photo goes under
+ `themes/pybcn_theme/assets/images/people/` and has to be square, see
+ [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.
+- **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
+ [Event page](README.md#event-page). An event lists its people and sponsors by
+ their `id`, so add those files first.
+
+`bin/check-content` reads every person, sponsor and event file. The pull
+request fails when a field is empty, an `id` does not match the file name, a
+declared photo is missing, or an event names a person or a sponsor that does
+not exist. Run it locally, step 5 below, to see what it would say.
+
+To report a problem with the site, open an issue and give the URL of the page.
+
+## Make the change
+
+Nothing is pushed to `edition` directly.
+
+1. Clone the repository. `git clone --single-branch --branch edition` is enough
+ and skips the built site, which lives on its own branch.
+2. Run `bin/install`. It downloads the pinned Hugo binary into `bin/hugo` and
+ verifies its checksum. You do not need Python, Go, or npm for this step.
+3. Run `bin/serve` to see the site at `http://localhost:1313` while you work.
+4. Make the change.
+5. Run the three checks locally, so you find what the pull request would find:
+
+ ```
+ pip install pyyaml pillow
+ bin/check-content
+ bin/check-html-safety
+ bin/hugo --minify -D -d public && bin/check-rendered
+ ```
+
+ Pillow is only needed for the black and white photo check. Without it the
+ rest still runs and the check says it was skipped, so a missing Pillow
+ never fails a build for the wrong reason.
+
+6. Open the pull request against `edition`.
+
+## What the pull request goes through
+
+The `pr-checks` workflow runs the same checks on your branch, plus a build and
+a link check. **All of them have to pass**, and one approving review is needed
+before the pull request can be merged.
+
+What each check is for:
+
+| Check | Fails when |
+|---|---|
+| Build the site | Hugo cannot build, or emits a warning |
+| `bin/check-content` | Front matter does not parse, a person `id` does not match its filename, an id is duplicated, a declared photo is missing, an event references a person or sponsor that does not exist, a social URL has the wrong shape, or a field is empty. It warns, without failing, about a photo that is too small or has no colour |
+| `bin/check-html-safety` | Content carries raw HTML that turns a content change into script execution, a redirect, a credential prompt, or a page overlay. See [Content safety check](README.md#content-safety-check) |
+| `bin/check-rendered` | The built pages carry that same markup, which catches a template that produces it even when no content file does, or they link to anything over `http`. See [Rendered page check](README.md#rendered-page-check) |
+| Check the links | Reported, never blocking, because external sites rate-limit |
+
+A merge to `edition` deploys the site. The `github-pages` workflow builds it and
+publishes the result, so a change is live within a few minutes of the merge.
+
+## Review by the web team
+
+`.github/CODEOWNERS` lists the paths where a change reaches every page of the
+site, or reaches money, or reaches the deploy: `.github/`, `bin/`,
+`config.toml`, `layouts/`, `static/`, `themes/`, `CNAME`, the association
+pages under `content/pybcn_association/`, `CODE_OF_CONDUCT.md`, and the
+sponsor and event files under `content/sponsors/` and `content/events/`.
+
+A pull request that touches one of those paths requests a review from
+`@pybcn/web` automatically, and the branch protection on `edition` requires
+it, so the pull request waits until a member of that team approves it. A
+person's file under `content/people/` is not on the list on purpose: a speaker
+edits their own page and needs no organizer for that, only the one approving
+review every pull request needs.
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 00000000..559e3176
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2013-2026 Associació Python Barcelona (PyBCN) and contributors
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/LICENSING.md b/LICENSING.md
new file mode 100644
index 00000000..2739d648
--- /dev/null
+++ b/LICENSING.md
@@ -0,0 +1,171 @@
+# Licensing
+
+This repository holds the source of [pybcn.org](https://pybcn.org/), the site
+of the Associació Python Barcelona (PyBCN). It holds three kinds of thing, and
+one licence does not fit all of them: the code of the site, the text of the
+site, and material that the association publishes but does not own. This file
+says which licence covers which path, and which paths no licence covers at all.
+
+GitHub reads the licence of a repository from the `LICENSE` file alone, and a
+second licence in that file stops the detection. So `LICENSE` holds the MIT
+License text and nothing else, and everything else about licensing is here.
+
+## Code: MIT License
+
+The MIT License in [`LICENSE`](LICENSE) covers the code that builds and runs
+the site:
+
+- `layouts/`: the Hugo templates and shortcodes of the site.
+- `themes/pybcn_theme/`: the theme. Its templates and partials (`layouts/`),
+ its stylesheets (`assets/scss/`), the scripts written for this site
+ (`assets/js/`, except `cookieconsent.min.js`, which is a third-party
+ library, see below), its `archetypes/`, `theme.toml`, and `README.md`. The
+ images and the vendored libraries under the theme are listed under
+ [Not licensed](#not-licensed).
+- `bin/`: the build, check, and maintenance scripts.
+- `.github/`: the workflows, the code owners, and the Dependabot configuration.
+- `config.toml`, `archetypes/`, and the other configuration files at the root
+ (`.gitignore`, `.hugo-version`, `CNAME`).
+
+The copyright line in `LICENSE` names the association and the contributors,
+and runs from 2013, the year of the first commit. The theme began as a copy of
+the `hugo-theme-bootstrap4-blog` theme, which is also under the MIT License;
+its notice stays in `themes/pybcn_theme/LICENSE.txt` and covers what remains
+of it.
+
+## Content: CC BY-SA 4.0
+
+The text of the site is licensed under the Creative Commons
+Attribution-ShareAlike 4.0 International licence (CC BY-SA 4.0):
+
+- `content/`, except `content/people/`, see [Person pages](#person-pages).
+- `README.md` and `CONTRIBUTING.md`.
+- `static/humans.txt`.
+- `CODE_OF_CONDUCT.md`, an adaptation of the PyLadies code of conduct, see
+ [Code of conduct](#code-of-conduct) below.
+
+Deed:
+
+Legal code:
+
+Attribute the text to "Python Barcelona (PyBCN), https://pybcn.org/". The
+licence requires attribution, a link to the licence, a note of any change, and
+the same licence on any adaptation.
+
+The full legal code is not copied here. Creative Commons keeps the canonical
+text at the URL above and says a link to it is enough, and a copy in this
+repository would be a second licence text at the root, which is what confuses
+the detection described above.
+
+### Code of conduct
+
+`CODE_OF_CONDUCT.md` was adapted from the
+[PyLadies code of conduct](https://www.pyladies.com/CodeOfConduct/), which is
+licensed under
+[CC BY-SA 3.0 Unported](https://creativecommons.org/licenses/by-sa/3.0/). The
+adaptation is licensed under CC BY-SA 4.0, like the rest of the content, so
+the whole repository is on one version of the licence family.
+
+The licence of the original permits that. Section 4(b) of the
+[CC BY-SA 3.0 legal code](https://creativecommons.org/licenses/by-sa/3.0/legalcode)
+says that an Adaptation may be distributed under "(ii) a later version of
+this License with the same License Elements as this License", and section 1
+names those elements: "Attribution, ShareAlike". CC BY-SA 4.0 is a later
+version of that licence, and its License Elements are Attribution and
+ShareAlike. The same section 4(b) requires the notices that refer to the
+licence of the original to stay intact, so the file keeps its attribution to
+PyLadies and the mention of CC BY-SA 3.0, and adds the link to CC BY-SA 4.0.
+
+ShareAlike is also why the file cannot go under the MIT License: an
+adaptation has to stay in the CC BY-SA family, at the same version or a later
+one.
+
+## Not licensed
+
+The paths below are published on the site but are not covered by either
+licence, because the rights in them are not the association's to give. A
+licence that promised more than that would tell people they may do things they
+may not.
+
+### Photographs of people
+
+`themes/pybcn_theme/assets/images/people/` holds one photograph per person on
+the site, and `themes/pybcn_theme/assets/images/photos/` holds photographs
+taken at PyBCN events.
+
+The people on this site sent their own photograph for publication on it.
+Anyone who did not want one did not send one, and the site shows a silhouette
+instead. The association publishes those photographs with the permission of
+the people in them, given for that purpose. That permission is not a licence
+for anyone else to reuse a photograph, for two reasons:
+
+- The copyright in a photograph belongs to whoever took it. Giving PyBCN a
+ copy to publish does not transfer that copyright, so PyBCN has nothing to
+ sub-license.
+- Consent to appear on this site is narrower than a CC BY-SA grant, which
+ would let a stranger reuse and adapt the image, commercially, for ever.
+ Under Spanish law (Ley Orgánica 1/1982) the right to one's own image is a
+ personality right and the consent is revocable, so it cannot become a
+ perpetual and irrevocable licence, which is what CC BY-SA is.
+
+So the photographs are not licensed. They are published with the permission
+of the people in them, and that permission can be withdrawn. The event
+photographs are the same case: the photographer keeps the copyright, and the
+people in them keep their image rights.
+
+### Person pages
+
+`content/people/` holds one page per person. Each person wrote their own text
+and sent it for publication here, so the same reasoning applies: the
+association publishes the text, it does not own it, and it cannot license it.
+
+There is a second reason, specific to these pages. A person can ask the
+association to erase their page, and the GDPR (Article 17.2) then obliges the
+association to take reasonable steps to tell anyone else holding a copy that
+erasure was requested. A licence that invites lawful copies of a person's
+entry multiplies the copies the association would have to chase, and works
+against the erasure it has to be able to deliver.
+
+### Sponsor logos
+
+`themes/pybcn_theme/assets/images/sponsors/` holds the logos of the sponsors.
+Each logo is the trade mark of its owner, shown here to acknowledge its
+support of PyBCN, with that owner's permission. A trade mark is not the
+association's to license, and CC BY-SA 4.0 does not license trade marks in any
+case (section 2(b)(2) of the legal code).
+
+### The association's own marks
+
+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/`)
+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
+association's.
+
+### Third-party libraries
+
+Each of these keeps its own licence, and the notice inside each file stays
+with it:
+
+- `themes/pybcn_theme/assets/vendor/`: jQuery 3.3.1 (MIT), jQuery Easing
+ 1.4.1 (BSD), Bootstrap 4.0.0 (MIT), and Font Awesome Free 5.8.2 (icons
+ CC BY 4.0, fonts SIL OFL 1.1, code MIT).
+- `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/css/cookieconsent.min.css` and
+ `themes/pybcn_theme/assets/js/cookieconsent.min.js`: the Cookie Consent
+ library by Osano (MIT).
+- `themes/pybcn_theme/assets/images/anon_member.png`: the Font Awesome `user`
+ icon as a PNG (CC BY 4.0).
+
+### Archived sites
+
+`static/archives/` holds copies of two sites as they were published, made
+with `bin/archive`: `pybcn.org/`, the previous site of the association, and
+`hacktoberfestbarcelona.com/`, the site of Hacktoberfest BCN 2018. They are
+kept as a record and are not maintained. Each carries its own photographs,
+logos, fonts, and libraries, so nothing under `static/archives/` is licensed by
+this repository.
diff --git a/README.md b/README.md
index 24a88c78..d606f222 100644
--- a/README.md
+++ b/README.md
@@ -29,7 +29,8 @@ You can bring a hugo hot reload server using the `bin/serve` script. This will b
### Publishing process
-During current implementation phase, the way to publish is to push new commits to the `edition` branch. However this will change in the future, to provide some review step in the publishing process.
+A change goes through a pull request against `edition`, and a merge deploys
+the site. [CONTRIBUTING.md](CONTRIBUTING.md) has the steps.
### How to add a new sponsor
@@ -84,43 +85,12 @@ Once the site was archived, you can create a new link in the navigational menu u
### Collaborate
-A change to this site goes through a pull request. Nothing is pushed to
-`edition` directly.
-
-1. Clone the repository. `git clone --single-branch --branch edition` is enough
- and skips the built site, which lives on its own branch.
-2. Run `bin/install`. It downloads the pinned Hugo binary into `bin/hugo` and
- verifies its checksum. You do not need Python, Go, or npm for this step.
-3. Run `bin/serve` to see the site at `http://localhost:1313` while you work.
-4. Make the change.
-5. Run the three checks locally, so you find what the pull request would find:
-
- ```
- pip install pyyaml
- bin/check-content
- bin/check-html-safety
- bin/hugo --minify -D -d public && bin/check-rendered
- ```
-
-6. Open the pull request against `edition`.
-
-The `pr-checks` workflow then runs the same checks on your branch, plus a build
-and a link check. **All of them have to pass**, and one approving review is
-needed before the pull request can be merged. The paths listed in
-`.github/CODEOWNERS` also request a review from the web team automatically.
-
-What each check is for:
-
-| Check | Fails when |
-|---|---|
-| Build the site | Hugo cannot build, or emits a warning |
-| `bin/check-content` | Front matter does not parse, a person `id` does not match its filename, an id is duplicated, a declared photo is missing, or an event references a person or sponsor that does not exist |
-| `bin/check-html-safety` | Content carries raw HTML that turns a content change into script execution, a redirect, a credential prompt, or a page overlay |
-| `bin/check-rendered` | The built pages carry that same markup, which catches a template that produces it even when no content file does |
-| Check the links | Reported, never blocking, because external sites rate-limit |
-
-A merge to `edition` deploys the site. The `github-pages` workflow builds it and
-publishes the result, so a change is live within a few minutes of the merge.
+The steps to make a change and open a pull request, what the pull request
+checks, and which paths wait for a review from the web team are in
+[CONTRIBUTING.md](CONTRIBUTING.md), which GitHub links from the pull request
+form. It lives there and not here, so there is one description of the process
+and not two that drift apart. The two sections below are the detail of the
+checks it names.
### Content safety check
@@ -446,4 +416,18 @@ people_sections:
people:
- mireia
- josep
-```
\ No newline at end of file
+```
+
+## Licence
+
+The code of the site (the templates, the stylesheets, the scripts, the
+workflows, and the configuration) is under the [MIT License](LICENSE). The
+content (the text under `content/`, the code of conduct, this README, and the
+contributing guide) is under
+[CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/). The
+photographs of people, the person pages, the sponsor logos, the association's
+own logos, the third-party libraries, and the archived sites are not licensed,
+each for a reason of its own. [LICENSING.md](LICENSING.md) lists every path
+and says why. The [code of conduct](CODE_OF_CONDUCT.md) was adapted from the
+PyLadies one, which is under CC BY-SA 3.0; that licence allows the adaptation
+to move to 4.0, and LICENSING.md says how.
diff --git a/bin/check-html-safety b/bin/check-html-safety
index 10dbeaab..f9468046 100755
--- a/bin/check-html-safety
+++ b/bin/check-html-safety
@@ -441,7 +441,11 @@ def main():
args = parser.parse_args()
root = args.root or os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
- targets = args.paths or [os.path.join(root, "content")]
+ # CODE_OF_CONDUCT.md sits at the root, where GitHub reads it, and the
+ # repo-markdown shortcode renders it on /pybcn_association/coc/. It is
+ # content that Hugo renders, so it is scanned like a file under content/.
+ targets = args.paths or [os.path.join(root, "content"),
+ os.path.join(root, "CODE_OF_CONDUCT.md")]
files, skipped, unknown = [], [], []
diff --git a/content/pybcn_association/coc.md b/content/pybcn_association/coc.md
index 45a513c0..5c0387d2 100644
--- a/content/pybcn_association/coc.md
+++ b/content/pybcn_association/coc.md
@@ -12,33 +12,4 @@ layout: single
heroBackground: /images/photos/people-pyday-2016.jpg
---
-PyBCN is dedicated to providing a respectful, harassment-free community for
-everyone. We do not tolerate harassment or bullying of any community member in
-any form. This does not only extend to members to local PyBCN communities,
-but to anyone who chooses to become involved in the larger PyBCN community of
-users, developers and integrators through events or interactions.
-
-Harassment includes offensive verbal/electronic comments related to personal
-characteristics or choices, sexual images or comments in public or online
-spaces, deliberate intimidation, bullying, stalking, following, harassing
-photography or recording, sustained disruption of talks, IRC chats, electronic
-meetings, physical meetings or other events, inappropriate physical contact, or
-unwelcome sexual attention. Participants asked to stop any harassing or
-bullying behavior are expected to comply immediately.
-
-If a participant engages in harassing behavior, representatives of the
-community may take reasonable action they deem appropriate, including warning
-the offender, expulsion from any PyBCN event, or expulsion from mailing
-lists, IRC chats, discussion boards and other electronic communications
-channels to resolve the issue. This may include expulsion from PyBCN Meetup
-group membership.
-
-If you are being harassed, notice that someone else is being harassed, or have
-any other concerns, please act to intercede or ask for help from any member of
-the PyBCN community, IRC chat admins, website admins, or
-organizers/representatives of any physical events put on under the auspices of
-PyBCN.
-
-This Code of Conduct has been adapted from the
-[PyLadies](https://www.pyladies.com/CodeOfConduct/) one and is licensed under a
-[Creative Commons Attribution-Share Alike 3.0 Unported license](https://creativecommons.org/licenses/by-sa/3.0/).
+{{< repo-markdown file="CODE_OF_CONDUCT.md" >}}
diff --git a/layouts/shortcodes/repo-markdown.html b/layouts/shortcodes/repo-markdown.html
new file mode 100644
index 00000000..e70a259c
--- /dev/null
+++ b/layouts/shortcodes/repo-markdown.html
@@ -0,0 +1,30 @@
+{{- /*
+ repo-markdown: render a Markdown file from the root of the repository.
+
+ Usage: {{< repo-markdown file="CODE_OF_CONDUCT.md" >}}
+
+ GitHub reads the code of conduct from CODE_OF_CONDUCT.md at the root of
+ the repository, never from the site, so the text has to live there. The
+ page at /pybcn_association/coc/ used to hold its own copy, and two copies
+ of one legal text drift apart. The page now renders the root file through
+ this shortcode, so the repository and the site always say the same thing.
+
+ The file name is checked against the list below, so content cannot read
+ an arbitrary file of the repository into a page.
+
+ A leading H1 is dropped: the file carries one so that it stands on its own
+ on GitHub, and the page layout already prints the title in the hero.
+*/ -}}
+{{- $allowed := slice "CODE_OF_CONDUCT.md" -}}
+{{- $file := .Get "file" -}}
+{{- if not (in $allowed $file) -}}
+ {{- errorf "repo-markdown: file %q is not one of %s (%s)" $file (delimit $allowed ", ") .Position -}}
+{{- end -}}
+{{- $text := os.ReadFile $file -}}
+{{- $text = replaceRE `^# [^\n]*\n+` "" $text -}}
+{{- /*
+ RenderString ends its output with a newline, and so does the content line
+ that calls this shortcode. One of them is trimmed, so the page is byte
+ identical to the copy it replaced.
+*/ -}}
+{{- .Page.RenderString (dict "display" "block") $text | strings.TrimSuffix "\n" | safeHTML -}}