Static site built with Eleventy and deployed to GitHub Pages at https://luketrusheim.com.
- Node.js 20+ and npm (matches the GitHub Actions workflow)
npm installnpm run dev- Serves and watches the site locally (default: http://localhost:8080).
- Source files live in
src/; Eleventy outputs the built site to_site/.
npm run build # production build for the custom domain_site/is generated output and should not be committed.
- Pushes to
maintrigger.github/workflows/deploy.yml, which runs the build and deploys_site/to GitHub Pages. - The
CNAMEfile keeps the custom domain configuration intact.
src/contains pages (*.njk), data (_data/projects.json), includes (_includes/), and assets/images copied through to the build.
- File:
src/_data/projects.json - Each object in the array represents one project used by:
src/portfolio.njk(portfolio cards)src/projects/project-page.njk(generated project pages)
slug(string): used for project page routes (/projects/<slug>/)title(string): project title shown on cards and project pageimage(string): default image path (used on portfolio cards and as fallback on project page)alt(string): image alt textdescription(string): short summary used in portfolio + project page headertags(array of strings): badge tagscurrentlyBuilding(boolean, optional): iftrue, project appears in the “Currently building” sectionlinks(array, optional): CTA buttons shown on portfolio/project pagelabel(string)url(string)newTab(boolean, optional; portfolio respects this)
date(object): used for sorting and display formatting (Mon. Year,Mon. Year - Mon. Year,Mon. Year - Present)- Keep
dates(string) if you want a human-readable reference while editing, but the site now usesdate
Supported date shapes:
Single month:
"date": {
"start": { "month": 1, "year": 2025 }
}Range:
"date": {
"start": { "month": 5, "year": 2025 },
"end": { "month": 6, "year": 2025 }
}Ongoing:
"date": {
"start": { "month": 9, "year": 2025 },
"present": true
}Year-only (supported):
"date": {
"start": { "year": 2023 }
}projectPage.enabled(boolean): controls whether the “Project Page” button appears and whether the page renders full content vs “not available yet”projectPage.url(string, optional/legacy): may still exist in data, but generated pages useslugprojectPage.images(array, optional): top-of-page image gallery shown before the project details/sectionsprojectPage.videos(array, optional): top-of-page video embeds/files shown before the project details/sectionsprojectPage.sections(array, optional): content blocks rendered in order on the project page
- Each item can be either:
- a string image path, or
- an object with:
src(string) (also acceptsurlorimage)alt(string, optional)caption(string, optional)
Example:
"images": [
{ "src": "/images/project-1.jpg", "alt": "Front view", "caption": "Prototype front view" },
{ "src": "/images/project-2.jpg", "caption": "Bench test setup" }
]- Each item should usually be an object.
- Supported formats:
- Embedded video (YouTube/Vimeo/etc.) via
embed(oriframeSrc) - Native hosted video via
src(orvideo/url)
- Embedded video (YouTube/Vimeo/etc.) via
Fields:
embedoriframeSrc(string, optional): iframe URLsrc/video/url(string, optional): video file URL/pathtype(string, optional): MIME type for native video (example:video/mp4)poster(string, optional): poster image for native videotitle(string, optional): iframe titlecaption(string, optional)
Example (embed + file):
"videos": [
{
"embed": "https://www.youtube.com/embed/VIDEO_ID",
"title": "Demo video",
"caption": "System demo"
},
{
"src": "/videos/bench-test.mp4",
"type": "video/mp4",
"poster": "/images/bench-test-poster.jpg",
"caption": "Bench test recording"
}
]textbulletskeyValuegalleryvideo
All section types may include:
title(string, optional): rendered as a section heading
{
"type": "text",
"title": "Project Overview",
"body": "Short paragraph..."
}Fields:
body(string)
{
"type": "bullets",
"title": "What I Did",
"items": ["Item one", "Item two"]
}Fields:
items(array of strings)
{
"type": "keyValue",
"title": "System Architecture",
"items": [
{ "label": "MCU", "value": "STM32" },
{ "label": "Bus", "value": "SPI" }
]
}Fields:
items(array of objects)label(string)value(string)
{
"type": "gallery",
"title": "Build Photos",
"images": [
{ "src": "/images/photo1.jpg", "caption": "Rev A board" },
{ "src": "/images/photo2.jpg", "caption": "Testing setup" }
]
}Fields:
images(array)- same item format as
projectPage.images(string path or object withsrc/alt/caption)
- same item format as
Supports either a single video directly on the section or a list via videos.
Single video (embed):
{
"type": "video",
"title": "Demo",
"embed": "https://www.youtube.com/embed/VIDEO_ID",
"caption": "End-to-end demo"
}Multiple videos:
{
"type": "video",
"title": "Test Videos",
"videos": [
{ "embed": "https://www.youtube.com/embed/VIDEO_ID_1", "caption": "Outdoor test" },
{ "src": "/videos/test-2.mp4", "type": "video/mp4", "caption": "Lab test" }
]
}Fields:
- Single video mode:
embedoriframeSrc(string), orsrc/video/url(string)mimeTypeorfileType(string, optional) for native videoposter(string, optional)caption(string, optional)
- Multi-video mode:
videos(array): same item format asprojectPage.videos
- Project pages are generated for every project via
src/projects/project-page.njk, but ifprojectPage.enabledis false the page shows a “not available yet” message. - Portfolio ordering is reverse chronological based on
date.start. - Portfolio cards use a responsive grid (3 columns desktop, then 2, then 1) and keep automatic card height sizing.