- Vue 94.4%
- TypeScript 2.9%
- CSS 1.8%
- JavaScript 0.9%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
|
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
Reviewed-on: #6 |
||
| assets/css | ||
| components | ||
| content | ||
| deploy | ||
| layouts | ||
| pages | ||
| public | ||
| utils | ||
| .dockerignore | ||
| .gitignore | ||
| .npmrc | ||
| .nvmrc | ||
| .woodpecker.yml | ||
| app.vue | ||
| error.vue | ||
| nuxt.config.ts | ||
| package.json | ||
| pnpm-lock.yaml | ||
| README.md | ||
| tailwind.config.ts | ||
| tsconfig.json | ||
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.
.nvmrcpins it (nvm use), andenginesinpackage.jsonstates 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.
- Make a branch:
git checkout -b content/<what-you-change>. - Edit the files in
content/(see the table below). - Check the result with
pnpm devat http://localhost:3000. The dev server reloads on every save. - Commit, push the branch and open a pull request into
main. When it is merged, the push tomaindeploys 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 namedatelier-grafiek-2.jpgis 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
srcand analt. Thealtdescribes 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 afA gallery (
imageson 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
downloadin{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. Usealt: ""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
linkis 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:
- builds the image from
deploy/dockerfileand tags it twice:academie:prodandacademie:<first 8 characters of the commit> - starts
academie:prodwithdeploy/docker-compose.ymlon thepangolinnetwork
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>.