Add Technical Kiwi website with Go, templ, and HTMX.

Single-page site with gallery by album and event, contact form over SMTP,
Docker dev/prod setup, and on-server image derivatives. Gallery photos stay
local (app/images/ is gitignored).

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-05-25 23:57:59 +12:00
parent c21be097b0
commit 509e7ccb43
33 changed files with 2635 additions and 1 deletions

View File

@@ -0,0 +1,76 @@
package templates
templ ContactForm(enabled bool) {
<section id="contact" class="contact">
<h2>Contact</h2>
<p class="contact-intro">
Interested in a project or collaboration? Send a message below.
</p>
if !enabled {
<p class="contact-unavailable">
The contact form is not configured yet. Email
<a href="mailto:hello@technical.kiwi">hello@technical.kiwi</a>
directly.
</p>
} else {
<form
class="contact-form"
method="post"
action="/contact"
hx-post="/contact"
hx-target="#contact-result"
hx-swap="innerHTML"
hx-boost="false"
hx-disabled-elt="find button[type=submit]"
>
<div class="form-row">
<label for="name">Name</label>
<input type="text" id="name" name="name" required autocomplete="name" maxlength="120"/>
</div>
<div class="form-row">
<label for="email">Email</label>
<input type="email" id="email" name="email" required autocomplete="email" maxlength="254"/>
</div>
<div class="form-row">
<label for="message">Message</label>
<textarea id="message" name="message" required rows="6" maxlength="8000"></textarea>
</div>
<input
class="hp-field"
type="text"
name="website"
id="website"
tabindex="-1"
autocomplete="off"
aria-hidden="true"
/>
<button type="submit" class="btn btn-primary">Send message</button>
</form>
}
<div id="contact-result" class="contact-result"></div>
</section>
}
templ ContactSuccess() {
<div class="alert alert-success" role="status">
<p>Thanks — your message has been sent. We will get back to you soon.</p>
</div>
}
templ ContactValidationAlert(errs map[string]string) {
<div class="alert alert-error" role="alert">
<p>Please fix the following:</p>
<ul>
for _, msg := range errs {
<li>{ msg }</li>
}
</ul>
</div>
}
templ ContactSendError() {
<div class="alert alert-error" role="alert">
<p>Something went wrong sending your message. Please try again later or email
<a href="mailto:hello@technical.kiwi">hello@technical.kiwi</a>.</p>
</div>
}

112
app/templates/gallery.templ Normal file
View File

@@ -0,0 +1,112 @@
package templates
import (
"fmt"
"time"
"technical.kiwi/website/internal/gallery"
)
templ GalleryGrid(images []gallery.Image) {
if len(images) == 0 {
<p class="gallery-empty">No images for this filter.</p>
} else {
for _, img := range images {
<button
type="button"
class="gallery-item"
hx-get={ fmt.Sprintf("/gallery/%s", gallery.URLPath(img.RelPath)) }
hx-target="#modal-root"
hx-swap="innerHTML"
aria-label={ fmt.Sprintf("Open %s", gallery.AlbumLabel(img.Album)) }
>
if img.ThumbURL != "" {
<img
src={ img.ThumbURL }
alt={ fmt.Sprintf("%s — %s", gallery.AlbumLabel(img.Album), formatDate(img.Date)) }
loading="lazy"
decoding="async"
sizes="(max-width: 480px) 45vw, 160px"
/>
} else {
<span class="gallery-placeholder" aria-hidden="true"></span>
}
if img.Collection != "" {
<span class="gallery-album">{ img.Collection }</span>
} else if img.Album != "" {
<span class="gallery-album">{ gallery.AlbumLabel(img.Album) }</span>
}
if !img.Date.IsZero() {
<span class="gallery-date">{ formatDate(img.Date) }</span>
}
</button>
}
}
}
templ ImageModal(img gallery.Image, prevPath, nextPath string) {
<div
class="modal-backdrop"
hx-on:click="if (event.target === this) this.remove()"
>
<div class="modal" role="dialog" aria-modal="true" aria-label="Image viewer">
<button
type="button"
class="modal-close"
onclick="document.getElementById('modal-root').innerHTML = ''"
aria-label="Close"
>
&times;
</button>
<figure class="modal-figure">
<img src={ img.URL } alt={ img.Filename }/>
if !img.Date.IsZero() {
<figcaption>{ modalCaption(img) }</figcaption>
}
</figure>
<nav class="modal-nav" aria-label="Gallery navigation">
if prevPath != "" {
<button
type="button"
class="modal-nav-btn"
hx-get={ fmt.Sprintf("/gallery/%s", gallery.URLPath(prevPath)) }
hx-target="#modal-root"
hx-swap="innerHTML"
>
&larr; Previous
</button>
} else {
<span></span>
}
if nextPath != "" {
<button
type="button"
class="modal-nav-btn"
hx-get={ fmt.Sprintf("/gallery/%s", gallery.URLPath(nextPath)) }
hx-target="#modal-root"
hx-swap="innerHTML"
>
Next &rarr;
</button>
}
</nav>
</div>
</div>
}
func formatDate(t time.Time) string {
if t.IsZero() {
return ""
}
return t.Format("2 Jan 2006")
}
func modalCaption(img gallery.Image) string {
if img.Collection != "" {
return img.Collection + " — " + formatDate(img.Date)
}
if img.Album != "" {
return gallery.AlbumLabel(img.Album) + " — " + formatDate(img.Date)
}
return formatDate(img.Date)
}

7
app/templates/helpers.go Normal file
View File

@@ -0,0 +1,7 @@
package templates
import "time"
func currentYear() string {
return time.Now().Format("2006")
}

153
app/templates/home.templ Normal file
View File

@@ -0,0 +1,153 @@
package templates
import (
"fmt"
"technical.kiwi/website/internal/gallery"
)
templ Home(images []gallery.Image, hero gallery.Image, hasHero bool, contactEnabled bool) {
@Layout("Technical Kiwi Limited", heroPreload(hero, hasHero), homeContent(images, hero, hasHero, contactEnabled))
}
func heroPreload(hero gallery.Image, hasHero bool) string {
if !hasHero {
return ""
}
return hero.HeroURL
}
templ homeContent(images []gallery.Image, hero gallery.Image, hasHero bool, contactEnabled bool) {
<section class="hero">
<div class="hero-copy">
<p class="eyebrow">Technical Kiwi Limited</p>
<h1>Electronic engineering, DevOps, and interactive art</h1>
<p class="lead">
We design and build lighting installations, embedded systems, and the infrastructure
that keeps creative technology running — from workshop bench to stage.
</p>
<div class="hero-actions">
<a href="#gallery" class="btn btn-primary">View our work</a>
<a href="#contact" class="btn btn-ghost">Get in touch</a>
</div>
</div>
if hasHero && hero.HeroURL != "" {
<figure class="hero-visual">
<img
src={ hero.HeroURL }
alt="Technical Kiwi — Connection machine"
loading="eager"
decoding="sync"
fetchpriority="high"
/>
</figure>
}
</section>
<section id="services" class="services">
<h2>What we do</h2>
<div class="service-grid">
<article class="service-card">
<h3>Electronic engineering</h3>
<p>Custom LED systems, control electronics, PCB design, and fabrication for installations and products.</p>
</article>
<article class="service-card">
<h3>DevOps &amp; infrastructure</h3>
<p>Servers, CI/CD, monitoring, and reliable deployments — so your systems stay up when the show goes live.</p>
</article>
<article class="service-card">
<h3>Interactive art</h3>
<p>Lighting, sound-reactive visuals, and experiential tech for events, venues, and public spaces.</p>
</article>
</div>
</section>
<section id="code" class="code-section">
<div class="section-head">
<h2>Code</h2>
<p>
All repositories on
<a href="https://git.technical.kiwi/" rel="noopener noreferrer" target="_blank">git.technical.kiwi</a>
are written and maintained by Technical Kiwi — firmware, lighting control, web services,
and the infrastructure behind them.
</p>
</div>
<a
href="https://git.technical.kiwi/"
class="code-card"
rel="noopener noreferrer"
target="_blank"
>
<div class="code-card-copy">
<h3>git.technical.kiwi</h3>
<p>
Source for installations, internal tools, and open components we ship or deploy
{ "for" } clients. Browse projects, history, and releases in one place.
</p>
</div>
<span class="btn btn-primary">Browse repositories</span>
</a>
</section>
<section id="gallery" class="gallery-section">
<div class="section-head">
<h2>Gallery</h2>
<p>Installations, prototypes, and behind-the-scenes work.</p>
</div>
@GalleryControls(images)
@GalleryCollectionControls(images)
<div id="gallery-grid" class="gallery-grid">
@GalleryGrid(images)
</div>
</section>
@ContactForm(contactEnabled)
}
templ GalleryControls(images []gallery.Image) {
<div class="gallery-controls">
<button
type="button"
class="filter-btn active"
hx-get="/gallery"
hx-target="#gallery-grid"
hx-swap="innerHTML"
>
All
</button>
for _, album := range gallery.Albums(images) {
<button
type="button"
class="filter-btn"
hx-get={ fmt.Sprintf("/gallery?album=%s", album) }
hx-target="#gallery-grid"
hx-swap="innerHTML"
>
{ gallery.AlbumLabel(album) }
</button>
}
</div>
}
templ GalleryCollectionControls(images []gallery.Image) {
if len(gallery.Collections(images, "templeoftechno")) > 0 {
<div class="gallery-controls gallery-controls-collections">
<span class="gallery-controls-label">Temple of techno</span>
<button
type="button"
class="filter-btn"
hx-get="/gallery?album=templeoftechno"
hx-target="#gallery-grid"
hx-swap="innerHTML"
>
All events
</button>
for _, key := range gallery.Collections(images, "templeoftechno") {
<button
type="button"
class="filter-btn"
hx-get={ fmt.Sprintf("/gallery?album=templeoftechno&collection=%s", key) }
hx-target="#gallery-grid"
hx-swap="innerHTML"
>
{ gallery.CollectionLabel(key, images) }
</button>
}
</div>
}
}

View File

@@ -0,0 +1,43 @@
package templates
templ Layout(title string, preloadImage string, content templ.Component) {
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"/>
<meta name="theme-color" content="#14100c"/>
<title>{ title }</title>
<meta name="description" content="Technical Kiwi Limited — electronic engineering, DevOps, and interactive art from New Zealand."/>
<link rel="stylesheet" href="/static/style.css"/>
if preloadImage != "" {
<link rel="preload" as="image" href={ preloadImage } fetchpriority="high"/>
}
</head>
<body hx-boost="true">
<header class="site-header">
<a href="/" class="logo">
<span class="logo-mark" aria-hidden="true">T</span>
<span class="logo-text">Technical Kiwi</span>
</a>
<nav class="site-nav">
<a href="#services">Services</a>
<a href="https://git.technical.kiwi/" rel="noopener noreferrer" target="_blank">Code</a>
<a href="#gallery">Gallery</a>
<a href="#contact">Contact</a>
</nav>
</header>
<main>
@content
</main>
<footer class="site-footer">
<p>
&copy; { currentYear() } Technical Kiwi Limited. New Zealand.
<a href="https://git.technical.kiwi/" rel="noopener noreferrer" target="_blank">git.technical.kiwi</a>
</p>
</footer>
<div id="modal-root"></div>
<script src="/static/htmx.min.js" defer></script>
</body>
</html>
}