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
{
  "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"
}
Sheet01
ContentContent
Sourceapps/louq/src/content/projects/keuken/meta.json L1–8
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);
  });
Sheet02
ComponentCarousel
Sourceapps/louq/src/lib/site-scripts/project-carousel.ts L52–84
 
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,
);
Sheet03
PageProject page
Sourceapps/louq/src/pages/projecten/[id].astro L18–44
{
  "trailingSlash": true
}
Sheet04
DeployDeploy
Sourceapps/louq/vercel.json L1–3