This document provides guidelines for AI agents working on the StartupOulu website codebase.
StartupOulu is a Jekyll-based static website for the startup and entrepreneurship ecosystem in Oulu, Finland. The site aggregates events, blog posts, ecosystem services, incubator programs, and mentoring resources for entrepreneurs at various stages.
main)CNAME file)_includes/header.html), Plausible is deprecated.github/
workflows/
validate-content.yml # CI: validates content and builds site on push/PR
_config.yml # Jekyll configuration (PROTECTED)
_data/
services.yaml # Ecosystem services/partners data
_events/ # Event files (YYYY-MM-slug.html)
_posts/ # Blog posts (YYYY-MM-DD-slug.markdown)
_layouts/ # Page templates
_includes/ # Reusable HTML components
events/ # Event-specific components (card, datetime, image, badge)
services/ # Service-specific components (card, filtered-list)
header.html # Head tag with analytics (PROTECTED)
footer.html # Site footer with sponsors and social links
menu.html # Navigation structure
nav.html # Navigation links
meta_tags.html # SEO meta tags
newsletter.html # Newsletter signup
_sass/ # SCSS stylesheets
_variables.scss # Colors, typography, breakpoints
_main.scss # Main import file
_components/ # Component styles (event_card, menu, newsletter, etc.)
_pages/ # Page-specific styles (home, event, service, blog, etc.)
_responsive/ # Mobile/responsive overrides
_scripts/
events.rb # Utility: convert events CSV to HTML
assets/
css/styles.scss # SCSS entry point
main.js # Site JavaScript
images/
events/ # Event cover images
blogs/ # Blog post images
contacts/ # Team member photos
mentors/ # Mentor profile images
logos/ # Logo files (SVG, PNG)
services/ # Service pages by entrepreneurship stage
ai-incubator/ # AI incubator program pages
incubator/ # Incubator program pages
mentoring/ # Mentoring program pages
oulu-startup-database/ # Startup database section
kiosk/ # Kiosk display for Samsung SmartTV
stand/ # Stand/booth display for the same TVs
index.html # Single-file page — logo + static sine-wave banner, CSS and JS inline
brand/ # Self-contained brand/media page (logos, colors, gradients, typeface, CSS vars)
index.html # Single-file page — all CSS inline, no Jekyll layout dependency
assets/
logos/
svg/ # SVG logo variants (naming: startupoulu_logo_{shape}_{text}_{outline}.svg)
png/ # PNG logo variants (same naming convention as SVG)
debug/ # Debug/troubleshooting pages
index.html # Homepage (layout: frontpage)
events.html # Events listing page
blogs.html # Blog listing page
archive.html # Blog archive
contact.html # Contact page
debug.html # Debug page showing all events data
ai-content.txt # AI-readable content feed (all posts/events as plain text)
events.ics # iCal feed of events
404.html # Custom 404 page
These files require explicit user approval before any modification:
_config.yml - Site-wide Jekyll configuration affecting the entire build_includes/header.html - Contains Plausible and Umami analytics tracking codesDo not modify these files without asking the user first.
All filenames in this project follow these rules:
startup-pitch.html, not Startup-Pitch.htmlmy-event.html, not my event.html or my_event.html2026-03-my-event.html, not 2026-03-my.event.htmlYYYY-MM-, blog posts use YYYY-MM-DD- or YYYY-MM-| Content type | Format | Example |
|---|---|---|
| Event | YYYY-MM-slug.html |
2026-03-startup-pitch.html |
| Blog post | YYYY-MM-DD-slug.markdown |
2026-03-15-building-bridges.markdown |
| Blog post (alt) | YYYY-MM-slug.markdown |
2026-03-building-bridges.markdown |
| Event image | YYYY-MM-descriptive-name.ext |
2026-03-startup-pitch.jpg |
| Blog image | YYYY-MM-descriptive-name.ext |
2026-03-building-bridges.png |
Location: _events/
Naming: YYYY-MM-slug.html (e.g., 2026-02-polar-bear-pitching.html)
Layout: event
---
layout: event
title: Event Title
start_time: 2026-02-15 18:00:00 # Format: YYYY-MM-DD HH:MM:SS
end_time: 2026-02-15 21:00:00 # Format: YYYY-MM-DD HH:MM:SS
location: Venue Name, Address
cover_image: event-image.jpg # Filename only (path added automatically)
cta_title: Register # Button text (optional)
cta_link: https://example.com # Button URL (optional)
special: true # Major event: kiosk takeover screen (optional)
pinned: true # Show first on the events page until end_time (optional)
excerpt: | # Short description, max 60 words (optional)
Brief event description for cards and previews.
description: | # Full event description (required)
Complete event description with all details.
---
Notes:
cover_image is just the filename; the path assets/images/events/ is prepended automatically. Do NOT use image_url — that is a deprecated field name that won’t workexcerpt is omitted, description is used for previews (truncated at 60 words)start_time and end_time must use the exact format YYYY-MM-DD HH:MM:SS (e.g., 2026-02-15 18:00:00). Omitting the time component or using non-zero-padded months (e.g., 2026-4-22) causes YAML to parse the value as an array instead of a date, which breaks sortingassets/images/events/ (recommended: 960x540px, 16:9 ratio)published: false to the front matterpinned: true shows the event first on the events page (events.html) and the homepage event list (_includes/upcoming_events.html) and keeps it listed until its end_time has passed, even if start_time is already in the past. Useful for application periods such as volunteer calls. Omit the field for ordinary eventspinned: first works like pinned: true but puts the event above all other pinned events (pinned events are otherwise ordered by start_time)special: true marks a major event. On the lobby kiosk it replaces the normal event slide with a full-bleed takeover screen (hype / countdown / QR call-to-action) that then takes every other slot in the rotation — see kiosk/KIOSK.md. It has no effect on the website itself. Omit the field for ordinary eventsLocation: _posts/
Naming: YYYY-MM-DD-slug.markdown (e.g., 2026-02-06-building-bridges.markdown)
Layout: blog
---
layout: blog
title: Blog Post Title
description: Meta description for SEO and social sharing (1-2 sentences)
blog_image: /assets/images/blogs/image.jpg # Full path required
---
Post content in Markdown format...
Notes:
blog_image requires the full path starting with /assets/images/blogs/assets/images/blogs/ (recommended: 960x540px, 16:9 ratio)published: false to the front matterLocation: _data/services.yaml
- title: Service Name
description: Brief description of the service
link_label: Learn More
link_url: https://service-url.com
stage:
- just-exploring # For people exploring entrepreneurship
- getting-started # For early-stage startups
- ready-to-grow # For growth-stage companies
A service can belong to one or more stages.
Experience the Avanto: Dive In will break YAML parsing. Wrap it in quotes: title: "Experience the Avanto: Dive In"| pipe syntax for multi-line text — description and excerpt fields must use | followed by indented text on the next linesstart_time must include the time — 2026-04-22 09:00:00 works, but 2026-4-22 gets parsed as a YAML array [2026, 4, 22] and breaks the sitecover_image, not image_url — older events may use image_url but the current layouts expect cover_image<br> in description are fine — templates must escape them (see Liquid Templates). If such text shows up as stray text at the top of a page, fix the template, not the contentContent can be in English or Finnish. Match the language of surrounding content or ask the user if unclear.
Always ask the user before creating new content files. This includes:
_events/_posts/| Content Type | Directory | Recommended Size | Formats |
|---|---|---|---|
| Event covers | assets/images/events/ |
960x540px (16:9) | JPG, PNG |
| Blog images | assets/images/blogs/ |
960x540px (16:9) | JPG, PNG |
| Contact photos | assets/images/contacts/ |
As needed | JPG, PNG |
| Mentor photos | assets/images/mentors/ |
As needed | JPG, PNG |
Filenames must be lowercase with dashes (no spaces). Example: 2026-02-startup-pitch.jpg
| Layout | Purpose |
|---|---|
default.html |
Bare page: <html>, header.html and ``, nothing else |
frontpage.html |
Homepage with hero, nav, footer |
event.html |
Single event detail page |
events.html |
Events listing page |
blog.html |
Single blog post page |
blogs.html |
Blog listing page |
service.html |
Single service page |
services.html |
Services listing page |
contact.html |
Contact page |
plain.html |
Minimal layout for standalone pages |
post.html |
Simple post wrapper |
about.html |
About page |
Every layout is a standalone page: it writes its own <!DOCTYPE html>, <html> and `<head>
<script defer data-domain="startupoulu.com" src="https://plausible.io/js/script.js"></script>
<script defer src="https://cloud.umami.is/script.js" data-website-id="b612b215-f6db-487c-92d6-afb99c98bfbf"></script>
<link rel="icon" type="image/png" href="/assets/images/startupoulu-favicon.png"/>
</head>
. Do not give a layout a parent (layout: in its front matter), or the page gets a second <html>/<head>` with duplicate meta tags.
jekyll-redirect-from, jekyll-feed, jekyll-sass-converter_sass/_variables.scss)#070540#FF4600#FF3198#EAEAEA800pxbrand/index.html)The brand page defines the complete identity system. Use these CSS variable names when referencing brand values:
| Name | HEX | CSS variable |
|---|---|---|
| Canvas | #EDF2F5 |
--color-canvas |
| Knowledge | #070540 |
--color-knowledge |
| Passion | #FF3296 |
--color-passion |
| Inspiration | #E9DEFF |
--color-inspiration |
| Growth | #D2FACD |
--color-growth |
| Fame | #FF4600 |
--color-fame |
Typeface: Hanken Grotesk — Black (900) for headlines, Regular (400) for body. Available on Google Fonts (fonts.google.com/specimen/Hanken+Grotesk).
Logo files follow the naming pattern startupoulu_logo_{shape}_{text-color}_text_{outline-color}_outline.{ext} and live in brand/assets/logos/svg/ and brand/assets/logos/png/.
bundle install # Install Ruby dependencies
bundle exec jekyll serve # Start local server at http://localhost:4000
A GitHub Actions workflow (.github/workflows/validate-content.yml) runs on every push to main and on pull requests. It validates:
YYYY-MM-slug.html for events, YYYY-MM-DD-slug.markdown or YYYY-MM-slug.markdown for posts)Errors appear as annotations in the Actions tab and on the commit/PR page.
Before committing changes, verify the build succeeds:
bundle exec jekyll build
This catches Liquid syntax errors, invalid YAML front matter, and broken templates.
Visit /debug/ on the live site (or debug.html locally) to see a table of all events with their parsed data. This helps verify that event front matter is correctly formatted.
ruby _scripts/events.rb # Convert events CSV to HTML format
_sass/_sass/_components/ (e.g., _event_card.scss, _menu.scss)_sass/_pages/ (e.g., _home.scss, _single_event.scss)_sass/_responsive/ (mirror page style files)_sass/_variables.scss_sass/_main.scss (imports all partials)assets/main.js_layouts/ and _includes/_includes/events/ (card, datetime, image, badge)_includes/services/ (card, filtered-list)description, excerpt and title may contain quotes ("startup idea") and HTML (<br>). Output into an attribute (content="...", alt="...", title="...") must go through | strip_html | normalize_whitespace | escape (titles: | escape). Unescaped, a quote ends the attribute early and the rest of the text leaks onto the page as visible text. See _includes/meta_tags.htmlWrite one descriptive sentence in the imperative mood, starting with a capital letter, with no trailing period and no conventional commit prefixes (feat:, fix:). Say what changed and, where it helps, why:
Add Cofounder Corner event on 28.10.2026
Fix Cofounder Corner front matter so the event is published
Add visitor CTAs to home page hero, above the fold
Track hero CTA clicks with Umami events
Commits like “Update 2026-11-fund-day.html” or “Add files via upload” come from GitHub’s web editor. Don’t copy them as examples.
Before finalizing changes:
bundle exec jekyll build - must complete without errorsstart_time), plus pinned events until their end_timestart_time matches the current date (set hide_today_badge: true in front matter to suppress it)excerpt field on events is truncated at 60 words on cards/services/events.ics) and an AI content feed (ai-content.txt)See kiosk/KIOSK.md for kiosk documentation (screens, technical constraints, how to add new screens).
stand/index.html targets the same TVs but is a single static screen: the logo at 84% of the screen height with a non-scrolling sine-wave banner over its lower part. It is self-contained (CSS and JS inline, no shared stylesheet to fall out of sync) and shares the kiosk’s ES5 / CSS 2.1 constraints — the file’s own header comment carries the details. Its banner text is the SINE_TEXT constant at the top of the script.
If uncertain about any changes, ask the user before proceeding. This is especially important for:
_config.yml, _includes/header.html)