Add contact antispam and fix gallery video playback.

English-only messages, rate limiting, min fill time, and normalized email
validation; improve modal video serving with posters, correct MIME types, and
no gzip on gallery media.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-04 00:38:48 +12:00
parent a9095727bf
commit 6c215d40e6
16 changed files with 385 additions and 16 deletions

View File

@@ -4,7 +4,7 @@ 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.
Interested in a project or collaboration? Send a message below. Messages must be in English.
</p>
if !enabled {
<p class="contact-unavailable">
@@ -33,7 +33,7 @@ templ ContactForm(enabled bool) {
</div>
<div class="form-row">
<label for="message">Message</label>
<textarea id="message" name="message" required rows="6" maxlength="8000"></textarea>
<textarea id="message" name="message" required minlength="20" rows="6" maxlength="8000" placeholder="Write at least a few sentences in English."></textarea>
</div>
<input
class="hp-field"
@@ -74,3 +74,9 @@ templ ContactSendError() {
<a href="mailto:hello@technical.kiwi">hello@technical.kiwi</a>.</p>
</div>
}
templ ContactRateLimited() {
<div class="alert alert-error" role="alert">
<p>Too many messages from your network. Please try again later.</p>
</div>
}

View File

@@ -65,7 +65,15 @@ templ ImageModal(img gallery.Image, prevPath, nextPath string) {
</button>
<figure class="modal-figure">
if img.IsVideo {
<video src={ img.URL } controls playsinline preload="metadata"></video>
<div class="modal-video">
<video controls playsinline preload="auto" poster={ img.ThumbURL }>
<source src={ img.URL } type={ videoMIME(img.Filename) }/>
</video>
<p class="video-unavailable" hidden>
Playback is not supported in this browser.
<a href={ img.URL } download={ img.Filename }>Download the video</a>
</p>
</div>
} else {
<img src={ img.URL } alt={ img.Filename }/>
}

View File

@@ -1,7 +1,22 @@
package templates
import "time"
import (
"path/filepath"
"strings"
"time"
)
func currentYear() string {
return time.Now().Format("2006")
}
func videoMIME(filename string) string {
switch strings.ToLower(filepath.Ext(filename)) {
case ".webm":
return "video/webm"
case ".mov":
return "video/quicktime"
default:
return "video/mp4"
}
}

View File

@@ -70,8 +70,25 @@ templ Layout(title string, preloadImage string, content templ.Component) {
hydrateLazyThumbs(document);
});
function initModalVideos(root) {
root.querySelectorAll(".modal-video video").forEach(function (video) {
var wrap = video.closest(".modal-video");
var notice = wrap && wrap.querySelector(".video-unavailable");
video.addEventListener(
"error",
function () {
video.classList.add("video-broken");
if (notice) notice.hidden = false;
},
{ once: true },
);
video.load();
});
}
document.body.addEventListener("htmx:afterSwap", function (event) {
hydrateLazyThumbs(event.target);
initModalVideos(event.target);
});
})();
</script>