Portfolio website

This project revolves around my personal portfolio website: a place where I bring together my work, working methods, and technical decisions. Not just to showcase end results, but to provide context — why I make certain choices, how I approach projects, and the role I play within them.

home-portfolio.png

Project info

Start
October 2025
End
January 0001
Complexity
4 / 10
[[Team size]]
1
Type
Hobby Portfolio
[[Stack]]
C# Javascript HTML & CSS SQL ASP.NET Core RazorPages Bootstrap GIT Umbraco
[[Live]]
https://bartvanderburg.nl
[[Source]]
https://github.com/Bart023/PortfolioWebsite.Static

Goal and Motivation

The goal of this website is to give a clear and honest view of how I work. I want to show what I build, but more importantly how and why. This includes code examples, architectural decisions, and the trade-offs that come with them.

In addition, this project was a deliberate choice to gain hands-on experience with Umbraco, within a setup that remains easy to extend. New projects and content should be simple to add without sacrificing technical control.


Technical Stack

The website is built using Umbraco CMS in combination with ASP.NET Core Razor Pages. Because I already had experience with Razor Pages, I was able to become productive quickly and focus on structure, reusable components, and consistent styling.

For the front end, I opted for a lightweight setup using Bootstrap along with custom CSS and JavaScript. No unnecessary frameworks, but enough flexibility to create a clean and accessible UI. Interactive elements are intentionally kept simple to keep the code readable and maintainable.


Hosting and Static Generation

For hosting, I chose a static approach. The Umbraco site is converted locally into a static website, with all pages and assets generated automatically. This allows the site to be hosted entirely for free, without compromising performance or stability.

To support this workflow, I built a simple local static host that starts alongside Umbraco and immediately serves the generated files. This makes it easy to test changes quickly and iterate efficiently.


Architecture and Code Quality

The architecture is intentionally kept simple. The project contains little complex business logic and largely follows the patterns prescribed by Umbraco. This provides clarity and encourages structured decision-making without introducing unnecessary complexity.

The focus was on a clear content structure, reusable block components, consistent naming, and clean Razor views. Below, I provide insight into the project setup through several images. To give a concrete impression of my code style and way of working, I also include a few code examples, demonstrating how I approach structure, separation of concerns, and readability.

Solution-folder-structure-overview.png
Umbraco-content.png
Umbraco-settings-1.png
Umbraco-settings-2.png
Umbraco-translations.png

ProjectsOverviewPage.cshtml

ProjectsOverviewPage.cshtml
@using Umbraco.Cms.Core.Models
@using Umbraco.Cms.Web.Common.PublishedModels;
@inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage
@{
    Layout = "master.cshtml";

    var projects = Model?
        .ChildrenOfType("ProjectDetailPage")?
        .Where(x => x.IsVisible())
        .OrderBy(x => x.SortOrder)
        .Select(x => new
        {
            Url = x.Url(),
            Title = x.Name,
            SortOrder = x.SortOrder,
            Intro = x.Value<string>("intro") ?? "",
            Img = x.Value<MediaWithCrops>("headerImage"),
            Employer = x.Value<string>("employer"),
            Start = x.Value<DateTime?>("startDate"),
            End = x.Value<DateTime?>("endDate"),
            Difficulty = x.Value<int>("difficulty"),
            Type = (x.Value<IEnumerable<string>>("projectType") ?? Enumerable.Empty<string>()).ToArray(),
            Stack_languages = x.Value<IEnumerable<string>>("languages") ?? Enumerable.Empty<string>(),
            Stack_frameworks = x.Value<IEnumerable<string>>("frameworks") ?? Enumerable.Empty<string>(),
            Stack_tools = x.Value<IEnumerable<string>>("tools") ?? Enumerable.Empty<string>(),
            Stack_methodologies = x.Value<IEnumerable<string>>("methodologies") ?? Enumerable.Empty<string>()
        });

    if(projects?.Any() != true)
        return;

    var totalProjects = projects.Count();
    var employers = projects
        .Select(x => x.Employer)
        .Where(e => !string.IsNullOrWhiteSpace(e))
        .Distinct()
        .OrderBy(e => e)
        .ToList();
    var employerCounts = projects
        .Where(p => !string.IsNullOrWhiteSpace(p.Employer))
        .GroupBy(p => p.Employer ?? "-")
        .ToDictionary(g => g.Key, g => g.Count()) ?? new Dictionary<string, int>();

    var stackFilters = new[]
    {
        new { Label = "Languages", Items = BuildFilterList(projects, p => p.Stack_languages) },
        new { Label = "Frameworks", Items = BuildFilterList(projects, p => p.Stack_frameworks) },
        new { Label = "Tools", Items = BuildFilterList(projects, p => p.Stack_tools) },
        new { Label = "Methodologies", Items = BuildFilterList(projects, p => p.Stack_methodologies) }
    };
}

@functions
{
    private Dictionary<string, int> BuildFilterList<T>(IEnumerable<T> projects, Func<T, IEnumerable<string>> selector) where T : class
    {
        return projects?
            .SelectMany(selector)
            .Where(s => !string.IsNullOrWhiteSpace(s))
            .GroupBy(s => s)
            .OrderBy(g => g.Key)
            .ToDictionary(g => g.Key, g => g.Count()) ?? new Dictionary<string, int>();
    }
}

<section id="projects" class="projects container view-list">

    @* Sidebar / Filterbar *@
    <aside id="projects-sidebar" class="sidebar filterbar-content">
        <div class="d-flex mb-2 border-bottom">
            <div class="d-flex justify-content-between align-items-center gap-2">
                <h5 class="fw-semibold mt-3">Filters</h5>
                <span class="active-filter-count badge rounded-pill text-bg-warning gap-1" style="display:inline-block; top:0;">0</span>
                <small class="visible-projects-count badge rounded-pill text-bg-light border">@totalProjects projects</small>
            </div>
            <button id="projects-sidebar-close-btn" class="btn">×</button>
        </div>

        @if (employers?.Any() ?? false)
        {
            <div class="mb-4">
                <h6 class="fw-semibold">Employer</h6>
                <div class="list-group small">
                    @foreach (var employer in employers)
                    {
                        var count = employerCounts.ContainsKey(employer ?? "") ? employerCounts[employer ?? ""] : 0;
                        <label class="list-group-item d-flex justify-content-between align-items-center">
                            <span>@employer <span class="text-muted">(@count)</span></span>
                            <input type="checkbox" class="form-check-input employer-filter" value="@employer" />
                        </label>
                    }
                </div>
            </div>
        }

        @foreach (var filter in stackFilters)
        {
            @if (filter.Items?.Any() ?? false)
            {
                <div class="mb-4">
                    <h6 class="fw-semibold">@filter.Label</h6>
                    <div class="list-group small">
                        @foreach (var item in filter.Items)
                        {
                            <label class="list-group-item d-flex justify-content-between align-items-center">
                                <span>@item.Key <span class="text-muted">(@item.Value)</span></span>
                                <input type="checkbox" class="form-check-input stack-filter" value="@item.Key" />
                            </label>
                        }
                    </div>
                </div>
            }
        }

    </aside>

    <div id="projects-overlay" class="overlay"></div>

    <div class="projects-content-container m-2">

        @* Top bar *@
        <div class="projects-header-container d-flex flex-wrap justify-content-between align-items-center mt-1 gap-2">
            <div class="d-flex gap-2 mb-2 w-100">
                <div class="dropdown w-100">
                    <button class="btn btn-outline-secondary dropdown-toggle w-100" type="button" data-bs-toggle="dropdown" aria-expanded="false">
                        Sorteren op
                    </button>
                    <ul class="dropdown-menu" id="sortOptions">
                        <li><a class="dropdown-item active" href="#" data-sort="default">Standaard</a></li>
                        <li><a class="dropdown-item" href="#" data-sort="date-desc">Datum: Hoog - Laag</a></li>
                        <li><a class="dropdown-item" href="#" data-sort="date-asc">Datum: Laag - Hoog</a></li>
                        <li><a class="dropdown-item" href="#" data-sort="complexity">Complexity</a></li>
                    </ul>
                </div>
                <div class="d-flex align-items-center gap-2 w-100">
                    <button id="btnList" type="button" class="btn btn-outline-secondary w-100">☰ List</button>
                    <button id="btnTiles" type="button" class="btn btn-outline-secondary w-100 d-none">▦ Tiles</button>
                </div>
            </div>
            <button id="projects-sidebar-toggler" class="btn btn-outline-secondary sidebar-toggle-projects gap-2 mb-3">
                <span>☰ Filters</span>
                <span class="active-filter-count badge rounded-pill text-bg-warning" style="display:inline-block; top:0;">0</span>
                <small class="visible-projects-count badge rounded-pill text-bg-light border">@totalProjects projects</small>
            </button>
        </div>

        @* Content *@
        <div id="projectList" class="row g-3">
            @foreach (var project in projects ?? [])
            {
                <article class="col-12 col-sm-6 col-lg-4 project"
                         data-sort-order="@project.SortOrder"
                         data-start="@project.Start?.ToString("yyyy-MM-dd")"
                         data-difficulty="@project.Difficulty">
                    <a href="@project.Url" class="card project-card">

                        @if (project.Img != null)
                        {
                            <div class="image-container m-2">
                                <img class="project-img card-img-top" src="@project.Img.Url()" alt="@project.Title" />
                            </div>
                        }
                        <div class="project-body">
                            <div class="project-main">
                                <h3 class="mb-2">@project.Title</h3>
                                @if (!string.IsNullOrWhiteSpace(project.Intro))
                                {
                                    <div class="project-intro">
                                        @Html.Raw(project.Intro)
                                    </div>
                                }
                            </div>

                            <div class="project-meta small text-body-secondary">
                                @if (!string.IsNullOrWhiteSpace(project.Employer))
                                {
                                    <div>Employer: @project.Employer</div>
                                }
                                @if (project.Start.HasValue && project.Start != DateTime.MinValue)
                                {
                                    <div>Start: @project.Start?.ToString("MMMM yyyy")</div>
                                }
                                <div>End: @(project.End != null ? project.End?.ToString("MMMM yyyy") : "active")</div>
                                @if (project.Difficulty > 0)
                                {
                                    <div>
                                        Complexity:
                                        <span class="badge rounded-pill text-bg-secondary">@project.Difficulty / 10</span>
                                    </div>
                                }
                                @if (((string[])project.Type).Any())
                                {
                                    <div>
                                        Type: 
                                        @foreach (var type in project.Type)
                                        {
                                            <span class="badge rounded-pill text-bg-light border">@type</span>
                                        }
                                    </div>
                                }
                                @if (project.Stack_languages?.Any() ?? false)
                                {
                                    <div>
                                        Languages:
                                        @foreach (var lang in project.Stack_languages)
                                        {
                                            <span class="badge rounded-pill text-bg-light border">@lang</span>
                                        }
                                    </div>
                                }
                                @if (project.Stack_frameworks?.Any() ?? false)
                                {
                                    <div>
                                        Frameworks:
                                        @foreach (var framework in project.Stack_frameworks)
                                        {
                                            <span class="badge rounded-pill text-bg-light border">@framework</span>
                                        }
                                    </div>
                                }
                                @if (project.Stack_tools?.Any() ?? false)
                                {
                                    <div>
                                        Tools:
                                        @foreach (var tool in project.Stack_tools)
                                        {
                                            <span class="badge rounded-pill text-bg-light border">@tool</span>
                                        }
                                    </div>
                                }
                                @if (project.Stack_methodologies?.Any() ?? false)
                                {
                                    <div class="mb-4">
                                        Methodologies:
                                        @foreach (var methodology in project.Stack_methodologies)
                                        {
                                            <span class="badge rounded-pill text-bg-light border">@methodology</span>
                                        }
                                    </div>
                                }
                            </div>
                        </div>
                    </a>
                </article>
            }
        </div>
    </div>
</section>

ProjectsOverviewPage.css

ProjectsOverviewPage.css
.projects.container {
    padding: 0;
    display: flex;
}

/* Sidebar area */
#projects .filter-group {
    padding: 8px;
    box-shadow: var(--box-shadow-light);
}
#projects .sidebar-toggle-projects {
    padding: 0;
    width: 100%;
    cursor: pointer;
    font-size: 24px;
    align-items: center;
    display: inline-flex;
    justify-content: center !important;
}
#projects .filterbar-content {
    padding: 0 8px 8px 8px;
    min-height: -webkit-fill-available;
    background-color: var(--bs-body-bg);
}
#projects #projects-sidebar-close-btn {
    right: 8px;
    color: red;
    font-size: 24px;
    position: absolute;
    background: transparent;
}
@media (max-width: 960px) {
    #projects .filterbar-content {
        overflow-y: auto;
        z-index: 1040;
    }
}
@media (min-width: 960.02px) {
    #projects .sidebar-toggle-projects {
       display: none;
    }
    #projects #projects-sidebar-close-btn {
        display: none;
    }
}

/* Projects area */
#projects .projects-content-container{
    width: 100%;
}

#projects .project-card {
    text-decoration: none;
}
#projects .project-body > * {
    padding: .25rem;
}

#projects .project-img {
    aspect-ratio: 1 / 1;
    object-fit: cover;
}

#projects .project-intro {
    display: -webkit-box;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 3;
    overflow: hidden;
}
    #projects .project-intro p {
        margin: 0;
    }

/* Tiles view */
#projects.view-tiles .project-meta {
    border-top: 1px solid var(--bs-card-border-color);
}
#projects.view-tiles .project-intro {
    display: none;
}

/* List view */
#projects.view-list .project {
    flex: 0 0 100% !important;
}

#projects.view-list .project-card {
    flex-direction: row;
}

#projects.view-list .project-body {
    width: 100%;
}

#projects.view-list .image-container .project-img {
    border-radius: var(--bs-border-radius);
    max-width: 220px;
}

#projects.view-list .project-meta {
    margin-left: auto;
}

@media (max-width:767.98px) {
    #projects .project-intro {
        display: none;
    }

    #projects.view-list .project-meta {
        border-top: 1px solid var(--bs-card-border-color);
    }
}
@media (min-width:768px) {
    #projects.view-list .project-body {
        display: flex;
    }

    #projects.view-list .project-meta {
        min-width: 35%;
        max-width: 35%;
        border-left: 1px solid var(--bs-card-border-color);
    }
}

ProjectsOverviewPage.js

ProjectsOverviewPage.js
// portfolio overview page - Toggle list/tile view
document.addEventListener("DOMContentLoaded", () => {
    const root = document.getElementById("projects");
    if (!root) return;

    const btnTiles = document.getElementById("btnTiles");
    const btnList = document.getElementById("btnList");

    const setView = view => {
        root.classList.toggle("view-tiles", view === "tiles");
        root.classList.toggle("view-list", view === "list");
        btnTiles.classList.toggle("d-none", view === "tiles");
        btnList.classList.toggle("d-none", view === "list");
        localStorage.setItem("projectView", view);
    };

    const saved = localStorage.getItem("projectView");
    setView(saved === 'list' ? 'list' : 'tiles');

    btnTiles.addEventListener("click", () => setView("tiles"));
    btnList.addEventListener("click", () => setView("list"));
});

// Portfolio overview page - Sorting
document.addEventListener("DOMContentLoaded", () => {
    const sortOptions = document.querySelectorAll("#sortOptions .dropdown-item");
    const projectsContainer = document.getElementById("projectList");
    if (!projectsContainer || sortOptions.length === 0) return;

    function sortProjects(sortType) {
        const projectItems = Array.from(projectsContainer.children);

        projectItems.sort((projectA, projectB) => {
            const sortOrderA = parseInt(projectA.dataset.sortOrder || "0", 10);
            const sortOrderB = parseInt(projectB.dataset.sortOrder || "0", 10);
            const startDateA = new Date(projectA.dataset.start || 0).getTime();
            const startDateB = new Date(projectB.dataset.start || 0).getTime();
            const difficultyA = parseInt(projectA.dataset.difficulty || "0", 10);
            const difficultyB = parseInt(projectB.dataset.difficulty || "0", 10);

            switch (sortType) {
                case "date-desc": return startDateB - startDateA;
                case "date-asc": return startDateA - startDateB;
                case "complexity": return difficultyB - difficultyA;
                default: return sortOrderA - sortOrderB;
            }
        });

        projectItems.forEach(project => projectsContainer.appendChild(project));
    }

    sortOptions.forEach(option => {
        option.addEventListener("click", event => {
            event.preventDefault();

            sortOptions.forEach(optionItem => optionItem.classList.remove("active"));
            option.classList.add("active");

            const sortType = option.dataset.sort;
            sortProjects(sortType);
        });
    });
});

// portfolio overview page - Sidebar toggle
document.addEventListener("DOMContentLoaded", () => {
    const sidebar = document.getElementById("projects-sidebar");
    const toggleBtn = document.getElementById("projects-sidebar-toggler");
    const closeBtn = document.getElementById("projects-sidebar-close-btn");
    const overlay = document.getElementById("projects-overlay");
    function openSidebar() {
        sidebar.classList.add("open");
        overlay.classList.add("show");
        document.body.style.overflow = "hidden";
    }

    function closeSidebar() {
        sidebar.classList.remove("open");
        overlay.classList.remove("show");
        document.body.style.overflow = "";
    }

    if (toggleBtn) toggleBtn.addEventListener("click", openSidebar);
    if (closeBtn) closeBtn.addEventListener("click", closeSidebar);
    if (overlay) overlay.addEventListener("click", closeSidebar);
});

// portfolio overview page - Filter bar
document.addEventListener("DOMContentLoaded", () => {
    const projects = document.querySelectorAll(".project");
    const badges = document.querySelectorAll(".active-filter-count");
    const visibleCountElements = document.querySelectorAll(".visible-projects-count");

    // Define all filter groups here (easily extendable)
    const filterGroups = {
        employers: document.querySelectorAll(".employer-filter"),
        stack: document.querySelectorAll(".stack-filter")
    };

    function updateVisibleProjectCount() {
        const visibleCount = Array.from(projects).filter(project => {
            return project.style.display !== "none";
        }).length;

        visibleCountElements.forEach(element => {
            element.textContent = `${visibleCount} project${visibleCount !== 1 ? "s" : ""}`;
        });
    }

    function applyFilters() {
        const params = new URLSearchParams(window.location.search);
        const activeFilters = {};

        // Collect all active filters for each group
        Object.keys(filterGroups).forEach(group => {
            const values = Array.from(filterGroups[group])
                .filter(cb => cb.checked)
                .map(cb => cb.value.toLowerCase());
            activeFilters[group] = values;

            // Update URL
            if (values.length > 0) params.set(group, values.join(","));
            else params.delete(group);
        });

        // Update URL without reload
        const query = params.toString();
        const newUrl = query ? `${window.location.pathname}?${query}` : window.location.pathname;
        window.history.replaceState({}, "", newUrl);

        // Filter projects (AND logic between groups, OR logic within each group)
        projects.forEach(project => {
            const employer = (project.querySelector(".project-meta div:nth-child(1)")?.innerText || "").toLowerCase();
            const types = Array.from(project.querySelectorAll(".project-meta .badge.border")).map(b => b.innerText.toLowerCase());

            const matchEmployer =
                activeFilters.employers.length === 0 ||
                activeFilters.employers.some(e => employer.includes(e));

            const matchStack =
                activeFilters.stack.length === 0 ||
                activeFilters.stack.some(s => types.includes(s));

            project.style.display = matchEmployer && matchStack ? "" : "none";
        });

        // Update badges with total active filters
        const totalActive = Object.values(activeFilters).reduce((sum, list) => sum + list.length, 0);
        badges.forEach(badge => {
            badge.textContent = totalActive;
            badge.style.display = totalActive > 0 ? "inline-block" : "none";
        });

        // Update visible project count
        updateVisibleProjectCount();
    }

    // Hook up all checkboxes
    Object.keys(filterGroups).forEach(group => {
        filterGroups[group].forEach(cb => cb.addEventListener("change", applyFilters));
    });

    // Read URL params and set initial checkbox states
    const params = new URLSearchParams(window.location.search);
    Object.keys(filterGroups).forEach(group => {
        const urlValues = params.get(group)?.split(",").map(v => v.toLowerCase()) || [];
        if (urlValues.length > 0) {
            filterGroups[group].forEach(cb => {
                if (urlValues.includes(cb.value.toLowerCase())) cb.checked = true;
            });
        }
    });

    applyFilters();
});