Website voor Sint-Lukas Academie
  • Vue 94.4%
  • TypeScript 2.9%
  • CSS 1.8%
  • JavaScript 0.9%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
2026-10-28 14:26:25 +00:00
assets/css fix(a11y): a <main> landmark, a named logo link, an h1 on every page, no heading skip 2026-10-28 14:44:06 +01:00
components fix(a11y): draw the thumbnail focus ring inside the thumbnail 2026-10-28 14:47:49 +01:00
content fix(a11y): every iframe has a Dutch title 2026-10-28 14:39:30 +01:00
deploy fix(docker): get pnpm from Corepack so the build uses the pinned 9.1.2 2026-10-28 09:07:50 +01:00
layouts fix(a11y): a <main> landmark, a named logo link, an h1 on every page, no heading skip 2026-10-28 14:44:06 +01:00
pages feat(seo): <html lang="nl">, a <title> per page and a meta description 2026-10-28 11:12:01 +01:00
public chore(assets): remove 14 files nothing uses (5.5 MB) 2026-10-28 12:26:04 +01:00
utils fix: stable aanbod URLs from a frozen slug field 2026-10-28 11:07:52 +01:00
.dockerignore chore(docker): add .dockerignore so the build context leaves out .git and local builds 2026-10-28 09:07:43 +01:00
.gitignore remove staging config 2026-10-27 17:21:05 +01:00
.npmrc added decap 2024-08-19 14:47:37 +02:00
.nvmrc chore: pin Node 22 with .nvmrc and engines, rename the package to sla-website 2026-10-28 09:07:59 +01:00
.woodpecker.yml feat(deploy): tag every build with its commit so a bad deploy can be rolled back 2026-10-28 09:08:20 +01:00
app.vue changed nuxt layout position 2024-09-12 15:23:37 +02:00
error.vue fix(error): drop the empty banner band above the 404 heading 2026-10-28 11:41:45 +01:00
nuxt.config.ts chore(deps): drop @nuxt/ui, use @nuxtjs/tailwindcss directly 2026-10-28 13:34:16 +01:00
package.json chore(deps): drop @nuxt/ui, use @nuxtjs/tailwindcss directly 2026-10-28 13:34:16 +01:00
pnpm-lock.yaml chore(deps): drop @nuxt/ui, use @nuxtjs/tailwindcss directly 2026-10-28 13:34:16 +01:00
README.md docs: a Toegankelijkheid note for content editors 2026-10-28 14:44:06 +01:00
tailwind.config.ts chore(deps): drop @nuxt/ui, use @nuxtjs/tailwindcss directly 2026-10-28 13:34:16 +01:00
tsconfig.json added decap 2024-08-19 14:47:37 +02:00

Sint-Lukas Academie website

Website for academie.sintlukas.brussels, built with Nuxt 3, Nuxt Content and Tailwind CSS. Content lives as files in content/ and is changed through a pull request (see Editing content).

Requirements

  • Node.js 22, the same version as the production image. .nvmrc pins it (nvm use), and engines in package.json states it.
  • pnpm 9.1.2, pinned in package.json (packageManager). Enable it with Corepack, which ships with Node; the production image does the same.
nvm use
corepack enable

Development

pnpm install
pnpm dev

The site runs at http://localhost:3000.

Other commands

Command Description
pnpm build Build for production into .output/
pnpm preview Serve the production build locally
pnpm generate Generate a fully static version of the site

To run the production build:

pnpm build
node .output/server/index.mjs

Stack

Layer What
Framework Nuxt 3, server-rendered (nuxt build, a Node server in the container)
Content @nuxt/content v2: Markdown, YAML and JSON files in content/
Styling Tailwind CSS 3 through @nuxtjs/tailwindcss, with @tailwindcss/typography for prose text. Colours (main, second, black, third) live in tailwind.config.ts
Images @nuxt/image (IPX resizes the images on request)
UI Plain Vue components with Tailwind classes, no UI kit: buttons (elements/button.vue), the header nav and mobile menu (main/header.vue), the gallery lightbox, a native <dialog> (elements/images.vue), icons as inline SVG (components/svg/)
Maps, video, calendar, newsletter OpenStreetMap, Vimeo and TimeTree iframes; the MailerLite embed

Project structure

Path Contents
content/ Site content (Markdown/YAML/JSON), Dutch only
pages/ Nuxt routes
components/ Vue components: main/ header and footer, pages/ one per page, elements/ the building blocks, content/ the components Markdown can use, svg/ logos and icons
layouts/ Page layouts
assets/css/ Global CSS (main.css)
tailwind.config.ts Tailwind theme: the colour palette, font and prose text settings
public/assets/ Static files: images and PDFs in public/assets/uploads/, site graphics in the other folders
deploy/ Dockerfile and docker-compose file for production

Editing content

There is no CMS. All content is plain Markdown, YAML and JSON in content/, and you edit those files in your editor.

  1. Make a branch: git checkout -b content/<what-you-change>.
  2. Edit the files in content/ (see the table below).
  3. Check the result with pnpm dev at http://localhost:3000. The dev server reloads on every save.
  4. Commit, push the branch and open a pull request into main. When it is merged, the push to main deploys the site.

Never push straight to main: every push to main is a live deploy.

What's where

Content Files
Footer text and logo content/site/footer.json
Site name and description content/site/info.json
Fixed pages (home, aanbod, academie, inschrijven, contact) content/fixedpages/*.yml
Aanbod (categories with their ateliers) content/aanbod/nl/*.md
News content/news/*.md
Locations content/locations/nl/*.md
Team content/persons/nl/*.md
Images and downloads (the files) public/assets/uploads/

Uploaded files (images, PDFs) live in public/assets/uploads/ and are served from /assets/uploads/.

Een afbeelding of download toevoegen

One step, no id: put the file in public/assets/uploads/, then write its path (and for an image, its alt text) in the content where it should appear.

  • File name: lowercase letters, digits and - only; no spaces. A file named atelier-grafiek-2.jpg is served at /assets/uploads/atelier-grafiek-2.jpg.

  • Size: at most 2400 px on the long side, and keep JPEGs under about 1 MB. The site makes smaller versions itself; a bigger original only makes the repo heavier.

  • An image is written as a src and an alt. The alt describes what is in the picture for people who cannot see it; leave it "" only for a purely decorative image.

    mainImage:
      src: /assets/uploads/atelier-grafiek-2.jpg
      alt: Twee leerlingen drukken een linosnede af
    

    A gallery (images on an atelier) is a list of the same:

    images:
      - src: /assets/uploads/atelier-grafiek-3.jpg
        alt: Drukpers in het grafiekatelier
      - src: /assets/uploads/atelier-grafiek-4.jpg
        alt: Prenten te drogen aan een lijn
    
  • A download is a Markdown link in any text field, with the label you want on the page:

    [Lessenrooster 18+ 2025-2026](/assets/uploads/2025-2026_kleursintlukasacademie_lessenrooster-18.pdf){target="_blank" download}
    

    The download in {target="_blank" download} is what makes the link a download button (outlined, with the download icon), and the file is saved under its own name. A paragraph that holds only a download link sits next to the other download buttons; put each one on its own line, with an empty line in between. Keep the {target="_blank" download} part as it is: without it the link is plain text, and the site would try to open the PDF as one of its own pages.

  • Never rename or delete a PDF that is already online. Other sites and mails link straight to it. To publish a new version, upload it under a new name (for example with the new school year in it) and change the link.

After any change, open every page you touched in pnpm dev: a wrong path shows up there as a missing image or a link to a 404.

Content fields: aanbod

Each file in content/aanbod/nl/ is one category (Kinderen, Jongeren, Volwassenen, ...) with its ateliers in a list. The URL of an atelier page is /aanbod/<category slug>/<atelier slug>.

slug is the URL, and it never changes. Every category and every atelier has a slug. The site builds its links and finds its pages by slug, not by title, so you can rename a title freely and the URL stays the same. Changing an existing slug breaks every link to that page (search engines, posters, mails, other sites). For a new category or atelier, pick a slug once: lowercase, words joined by -, for example ruimtelijk-atelier, and make it unique within its category. An entry without a slug falls back to its title with spaces, /, & and ? turned into - and lowercased (slugify() in utils/slug.js); always set one anyway, so a later title change cannot move the URL.

Category fields (top of the file):

Field Required What it does
id yes Unique id. Never change it
nl yes Category name, shown as the heading on /aanbod and in the home page list
slug yes First part of the URL. Never change it
order yes Sort order on the home page and /aanbod (1 first)
color yes Card and label colour, for example "#EDBAA6"
side no true puts the category in the side column on /aanbod
ateliers yes The list of ateliers, fields below

thumb, texture and background on a category are not read by any page.

Atelier fields (one list item under ateliers):

Field Required What it does
id yes Unique id. Never change it
title yes Page heading, card text and page <title>
slug yes Last part of the URL. Never change it
thumb yes Card image on /aanbod, a path under /assets/uploads/
age no Age group under the heading, for example 18+
showSubtitleOnCard no true also prints age on the card
mainImage no Banner image: src and alt (see above)
markdown yes Main text (Markdown)
side1, side2 no Side columns next to the main text (Markdown); leave out or "" when empty
persons no Teachers: file names (without .md) from content/persons/nl/
images no "In beeld" gallery: a list of src and alt
videos no "Afgelopen trajecten" video block: a list of url (the Vimeo player URL) and description, shown in this order

Images on the fixed pages (content/fixedpages/) use the same src and alt: the banner of academie, inschrijven and contact is in images (one image, despite the name), and the home page uses block_about.image1 to image3. block_banner.image and block_calendar.image on the home page are not shown by any page.

The site name and description (page <title> and meta description) come from content/site/info.json.

Toegankelijkheid

The site is built to work with a keyboard and a screen reader. Three things depend on the content, so check them when you edit:

  • Every image needs an alt (see above): a short description of what is in the picture. A screen reader reads it out, and in a gallery it is also the name of the thumbnail button that opens the lightbox. Use alt: "" only for a purely decorative image.
  • Every video needs a description. It becomes the title of the video player for screen readers. Without one, the player is only called "Video 1", "Video 2", ...
  • A Link button without a link is not clickable. :dispatch{:data='{"label":"OKAN"}' type='Link'} shows the green label, but it is plain text: you cannot click or Tab to it. Add "link":"https://..." to make it a real link.

Headings in Markdown text are written as ###. The site turns them into the heading level directly under the page title (an <h2>, with the small ### look), so do not use # or ## in content.

Deployment

A push to main triggers the Woodpecker pipeline (.woodpecker.yml), which:

  1. builds the image from deploy/dockerfile and tags it twice: academie:prod and academie:<first 8 characters of the commit>
  2. starts academie:prod with deploy/docker-compose.yml on the pangolin network

Traefik routes academie.sintlukas.brussels to the container on port 3000 and requests a Let's Encrypt certificate. This needs an A record for academie.sintlukas.brussels in Cloudflare that points to the server. The *.sintlukas.brussels wildcard doesn't cover this name, because it already has MX/TXT records for email.

Rollback

Every build keeps its own academie:<sha> tag, so going back one deploy needs no rebuild. On the server, in a checkout of this repo:

docker image ls academie                      # find the tag of the last good build
docker tag academie:<sha> academie:prod       # point :prod back at it
docker compose --project-name academie -f deploy/docker-compose.yml up -d academie-prod

Then revert the bad commit on main, otherwise the next push builds it again. Old tags pile up; remove the ones you no longer need with docker image rm academie:<sha>.