{
"title": "Keuken BL",
"description": "Subtiel gebruik van materialen in deze keuken.",
"thumbnail": "/images/projects/keuken-1.png",
"images": ["/images/projects/keuken-2.png"],
"order": 3,
"updated": "2026-08-18"
}Niels Thiel — independent web developer
Whole websites,built by one person.
Design, build, deploy, maintain. No hand-offs and no agency in the middle. Every site below is the real thing, live inside this page, and the page can drive it.
- 3 live sites
- Astro, fully static
- Deployed on Vercel
Projects
Live site
if (carousel.dataset["carouselInitialized"] === "true") {
return;
}
carousel.dataset["carouselInitialized"] = "true";
const slides = carousel.querySelectorAll<HTMLElement>(".carousel__slide");
const dots = carousel.querySelectorAll<HTMLElement>(".carousel__dot");
if (slides.length <= 1) return;
let current = 0;
function goTo(index: number): void {
slides[current]?.classList.remove("carousel__slide--active");
dots[current]?.classList.remove("carousel__dot--active");
current = (index + slides.length) % slides.length;
slides[current]?.classList.add("carousel__slide--active");
dots[current]?.classList.add("carousel__dot--active");
}
const controls: SlideControls = {
next: () => goTo(current + 1),
prev: () => goTo(current - 1),
};
registerCarouselControls(controls);
carousel.querySelector(".carousel__btn--prev")?.addEventListener("click", controls.prev);
carousel.querySelector(".carousel__btn--next")?.addEventListener("click", controls.next);
dots.forEach((dot) => {
dot.addEventListener("click", () => {
const i = Number(dot.dataset["index"]);
if (!Number.isNaN(i)) goTo(i);
});
export async function getStaticPaths() {
const projects = await getCollection("projects");
return projects.map((project) => ({
params: {
id: project.id,
},
props: { project },
}));
}
const { project } = Astro.props;
const slug = project.id;
const renderedBody = renderMarkdown(project.body ?? "");
const description = metaDescription(project.body ?? "") ?? project.data.description;
const imageRefs = [project.data.thumbnail, ...(project.data.images ?? [])];
const carouselMarkup = renderCarouselMarkup(
await Promise.all(
imageRefs.map(async (ref, i) => ({
source: await progressiveImageSource(ref, PROJECT_CAROUSEL_SLIDE),
viewTransitionName: i === 0 ? `project-img-${slug}` : undefined,
})),
),
project.data.title,
);{
"trailingSlash": true
} PageKey.Contact,
];
const slugs: Readonly<Record<Locale, Readonly<Record<PageKey, string>>>> = {
[Locale.Dutch]: {
[PageKey.Home]: "",
[PageKey.Appointment]: "afspraak",
[PageKey.OnCall]: "wachtdienst",
[PageKey.Team]: "team",
[PageKey.PracticalInfo]: "praktisch",
[PageKey.Announcements]: "mededelingen",
[PageKey.Contact]: "contact",
[PageKey.Privacy]: "privacy",
},
[Locale.French]: {
[PageKey.Home]: "",
[PageKey.Appointment]: "rendez-vous",
[PageKey.OnCall]: "garde",
[PageKey.Team]: "equipe",
[PageKey.PracticalInfo]: "infos-pratiques",
[PageKey.Announcements]: "actualites",
[PageKey.Contact]: "contact",
[PageKey.Privacy]: "vie-privee",export function resolveStatus(instant: Date, schedule: PracticeSchedule): PracticeStatus {
const wall = brusselsWallTime(instant);
const closure = schedule.closures.find(
(candidate) =>
compareLocalDates(candidate.from, wall.date) <= 0 &&
compareLocalDates(wall.date, candidate.to) <= 0,
);
if (closure) {
return {
kind: PracticeStatusKind.OnCall,
reason: OnCallReason.Closure,
closureReason: closure.reason,
};
}
if (isBelgianPublicHoliday(wall.date)) {
return {
kind: PracticeStatusKind.OnCall,
reason: OnCallReason.PublicHoliday,
closureReason: null,
};
}
if (wall.weekday === Weekday.Saturday || wall.weekday === Weekday.Sunday) {
return { kind: PracticeStatusKind.OnCall, reason: OnCallReason.Weekend, closureReason: null };
}
const day = schedule.week[wall.weekday];
if (day.secretariat && wall.minute >= day.secretariat.from && wall.minute < day.secretariat.to) {
return { kind: PracticeStatusKind.SecretariatOpen, until: day.secretariat.to };
}
if (day.phone && wall.minute >= day.phone.from && wall.minute < day.phone.to) {
return { kind: PracticeStatusKind.PhoneOnly, until: day.phone.to };
}
return {
kind: PracticeStatusKind.OnCall,
reason: OnCallReason.OutsideHours,
closureReason: null,
};
}
<div data-practice-status-badge aria-busy="true" data-props={JSON.stringify(props)}>
<p>
{messages.status.fallbackQuestion}{" "}
<a href={ladderHref}>{messages.status.fallbackLinkLabel}</a>
</p>
</div>
<script>
import { PracticeStatusScenario } from "@mono-studio/frame-bridge/protocol";
import { resolveStatus } from "@mono-studio/practice-status/status";
import { messagesFor } from "../i18n/messages";
import { applyBadge } from "../status/badge-dom";
import { badgeState, decodeBadgeProps } from "../status/practice-status-badge";
import { previewedStatus } from "../status/status-preview";
import { previewScenario, subscribePreviewScenario } from "../status/status-preview-store";
import { startStatusTicker } from "../status/status-ticker";
for (const root of document.querySelectorAll<HTMLElement>("[data-practice-status-badge]")) {
const payload = root.dataset["props"];
if (payload === undefined) continue;
const props = decodeBadgeProps(payload);
const messages = messagesFor(props.locale);
function render(): void {
const scenario = previewScenario();
const status =
scenario === PracticeStatusScenario.Live
? resolveStatus(new Date(), props.schedule)
: previewedStatus(scenario, props.schedule);
applyBadge(root, badgeState(status, props.phones, messages, props.detail));
}
startStatusTicker(render);{
"trailingSlash": true
}{
"name": "louq.",
"tagline": "Architecture & interior design portfolio",
"description": "Portfolio site for architect Louis Cocquyt. Fully prerendered Astro with an in-browser admin overlay that commits straight to GitHub, so the site has no backend at all.",
"url": "https://www.louq.be",
"status": "live",
"accent": "#c9c5c0",
"tags": ["astro", "static", "github-api cms", "view transitions", "playwright"],
"year": 2025,
"order": 1,
"pages": [
{ "label": "Projecten", "path": "/projecten/" },
{ "label": "Keuken BL", "path": "/projecten/keuken/" },
{ "label": "Over mij", "path": "/over-mij/" },
{ "label": "Contact", "path": "/contact/" }
],
"layers": [
{ Hello = "hello",
Navigate = "navigate",
SetLocale = "setLocale",
PreviewPracticeStatus = "previewPracticeStatus",
StepCarousel = "stepCarousel",
}
export type Command =
| { readonly kind: CommandKind.Hello }
| { readonly kind: CommandKind.Navigate; readonly path: string }
| { readonly kind: CommandKind.SetLocale; readonly locale: LocaleCode }
| {
readonly kind: CommandKind.PreviewPracticeStatus;
readonly scenario: PracticeStatusScenario;
}
| { readonly kind: CommandKind.StepCarousel; readonly step: CarouselStep };
export type SiteCommand = Exclude<Command, { readonly kind: CommandKind.Hello }>;
export enum CapabilityKind {
SetLocale = "setLocale",---
import { getImage } from "astro:assets";
import { readLayerSnippet } from "../layers/read-layer-source";
import type { Project } from "../portfolio/project";
import { firstPage, projectOrigin } from "../portfolio/projects";
import { siteCopy } from "../portfolio/site-copy";
import { PosterDevice, posterDevices } from "../screenshots/poster-device";
import { posterImage } from "../screenshots/project-posters";
import { devicePresets } from "../stage/device-preset";
import { MAX_FRAME_WIDTH_PX, MIN_FRAME_WIDTH_PX } from "../stage/frame-geometry";
import type { PosterSources, StageProject } from "../stage/stage-project";
import LayerCard from "./LayerCard.astro";
import ProjectRail from "./ProjectRail.astro";
type Props = {
readonly projects: readonly Project[];
};
const { projects } = Astro.props;
const copy = siteCopy.stage;
async function posterSources(project: Project): Promise<PosterSources> {
const entries = await Promise.all(
posterDevices.map(async (device) => {
const image = posterImage(project.id, device);
if (image === null) return [device, null] as const;
const optimized = await getImage({ src: image, format: "webp", width: image.width });
return [device, optimized.src] as const;{ "trailingSlash": true }