Build-time minification, image optimization, htmx SPA navigation, and local htmx bundle

This commit is contained in:
bronku 2026-06-10 16:15:42 +02:00
parent 50a5ca2c71
commit b95d5c2ee5
13 changed files with 186 additions and 115 deletions

View file

@ -133,3 +133,20 @@ ul {
img {
color: transparent;
}
main {
transition: opacity 0.15s ease;
}
#about-us,
#featured-delights,
#kawiarnia,
#kawiarnia-content,
#portfolio-grid,
#wyroby,
#slodkie_stoly,
#lody,
#wyroby-torty > section {
content-visibility: auto;
contain-intrinsic-size: 600px;
}

6
go.mod
View file

@ -10,14 +10,16 @@ require (
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/cli/browser v1.3.0 // indirect
github.com/fatih/color v1.16.0 // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/natefinch/atomic v1.0.1 // indirect
github.com/tdewolff/minify/v2 v2.24.13 // indirect
github.com/tdewolff/parse/v2 v2.8.12 // indirect
golang.org/x/mod v0.26.0 // indirect
golang.org/x/net v0.51.0 // indirect
golang.org/x/sync v0.16.0 // indirect
golang.org/x/sys v0.41.0 // indirect
golang.org/x/sys v0.43.0 // indirect
golang.org/x/tools v0.35.0 // indirect
)

9
go.sum
View file

@ -14,6 +14,8 @@ github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
@ -27,6 +29,11 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/tdewolff/minify/v2 v2.24.13 h1:xrcF7gKDnUszseEY9WX9mUlZII2v2Go/QAcAwRASw58=
github.com/tdewolff/minify/v2 v2.24.13/go.mod h1:emvwoYeIl8bfAKqRU5ww95LX9Gpggpqv/naal9a8Yq0=
github.com/tdewolff/parse/v2 v2.8.12 h1:5BBjfaCv482v3nltlS0u6wH1xJaxjR6ofDrWttNvROg=
github.com/tdewolff/parse/v2 v2.8.12/go.mod h1:Hwlni2tiVNKyzR1o6nUs4FOF07URA+JLBLd6dlIXYqo=
github.com/tdewolff/test v1.0.11/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8=
golang.org/x/mod v0.26.0 h1:EGMPT//Ezu+ylkCijjPc+f4Aih7sZvaAr+O3EHBxvZg=
golang.org/x/mod v0.26.0/go.mod h1:/j6NAhSk8iQ723BGAUyoAcn7SlD7s15Dp9Nd/SfeaFQ=
golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
@ -37,6 +44,8 @@ golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/tools v0.35.0 h1:mBffYraMEf7aa0sB+NuKnuCy8qI/9Bughn8dC2Gu5r0=
golang.org/x/tools v0.35.0/go.mod h1:NKdj5HkL/73byiZSJjqJgKn3ep7KjFkBOkR/Hps3VPw=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=

View file

@ -24,6 +24,13 @@ func processImage(fileName string) string {
return dst[4:]
}
templ AvifImage(srcPath, alt string) {
<img src={ processImage(srcPath) } alt={ alt }/>
templ AvifImage(srcPath, alt string, lazy bool, priority bool) {
<img src={ processImage(srcPath) } alt={ alt }
if lazy {
loading="lazy"
}
if priority {
fetchpriority="high"
}
/>
}

View file

@ -13,17 +13,17 @@ templ Index() {
<a href="wyroby.html" class="btn">Zobacz naszą ofertę</a>
</div>
<div>
@AvifImage("001.avif", "tort")
@AvifImage("001.avif", "tort", false, true)
</div>
</header>
<section id="about-us">
<div>
<div>
<div>
@AvifImage("002.avif", "kawiarnia")
@AvifImage("002.avif", "kawiarnia", true, false)
</div>
<div>
@AvifImage("003.avif", "kawiarnia")
@AvifImage("003.avif", "kawiarnia", true, false)
</div>
</div>
<div>
@ -42,7 +42,7 @@ templ Index() {
<h2>Nasze Wyroby</h2>
<div class="card">
<div>
@AvifImage("004.avif", "tort")
@AvifImage("004.avif", "tort", true, false)
</div>
<div>
<h3>Torty Okazjonalne</h3>
@ -69,7 +69,7 @@ templ Index() {
</div>
<div class="card">
<div>
@AvifImage("005.avif", "ciasto")
@AvifImage("005.avif", "ciasto", true, false)
</div>
<div class="greener">
<h3>Domowe Ciasta</h3>
@ -96,7 +96,7 @@ templ Index() {
</div>
<div class="card">
<div>
@AvifImage("006.avif", "slodkosci")
@AvifImage("006.avif", "slodkosci", true, false)
</div>
<div>
<h3>Słodkie Stoły</h3>
@ -174,10 +174,10 @@ templ Index() {
</div>
<div>
<div>
@AvifImage("007.avif", "kawiarnia")
@AvifImage("007.avif", "kawiarnia", true, false)
</div>
<div>
@AvifImage("008.avif", "ciasto i kawa")
@AvifImage("008.avif", "ciasto i kawa", true, false)
</div>
</div>
</div>

View file

@ -13,7 +13,7 @@ templ Kawiarnia() {
</p>
</div>
<div>
@AvifImage("009.avif", "kawiarnia")
@AvifImage("009.avif", "kawiarnia", false, true)
<div></div>
</div>
</header>

View file

@ -13,9 +13,10 @@ templ layout(activePage string) {
<meta name="description" content=""/>
<link href="style.css" rel="stylesheet"/>
<link rel="icon" href="images/favicon.avif"/>
<script src="htmx.min.js"></script>
<script defer src="portfolio.js"></script>
</head>
<body>
<body hx-boost="true">
<nav>
<div>
<a href="index.html">

View file

@ -7,7 +7,7 @@ templ Portfolio(images []string, activePage string, pages []string) {
<div id="portfolio-grid">
for _, img := range images {
<div>
@AvifImage("portfolio/"+img, "portfolio")
@AvifImage("portfolio/"+img, "portfolio", true, false)
</div>
}
</div>

View file

@ -5,7 +5,7 @@ import "piekarnikgosi/types"
templ card(product types.Product) {
<div class="card">
<div>
@AvifImage("wyroby/"+product.Image+".avif", product.Name)
@AvifImage("wyroby/"+product.Image+".avif", product.Name, true, false)
</div>
<div>
<h3>{ product.Name }</h3>

View file

@ -171,7 +171,7 @@ templ WyrobyTorty() {
<header>
<h2>Torty Musowe</h2>
<div>
@AvifImage("010.avif", "tort musowy")
@AvifImage("010.avif", "tort musowy", true, false)
</div>
</header>
<div>

1
js/htmx.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View file

@ -1,104 +1,94 @@
(function () {
var grid = document.getElementById("portfolio-grid");
if (!grid) return;
window.initPortfolio = function () {
var old = document.getElementById('lightbox')
if (old) old.remove()
var items = Array.from(grid.children);
if (!items.length) return;
var grid = document.getElementById('portfolio-grid')
if (!grid) return
var lb = document.createElement("div");
lb.id = "lightbox";
var items = Array.from(grid.children)
if (!items.length) return
var lb = document.createElement('div')
lb.id = 'lightbox'
lb.innerHTML =
'<button id="lb-close" aria-label="Zamknij"><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg></button>' +
'<button id="lb-prev" aria-label="Poprzednie"><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m15 18-6-6 6-6"/></svg></button>' +
'<img src="" alt="">' +
'<button id="lb-next" aria-label="Następne"><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m9 18 6-6-6-6"/></svg></button>';
document.body.appendChild(lb);
'<button id="lb-next" aria-label="Następne"><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m9 18 6-6-6-6"/></svg></button>'
document.body.appendChild(lb)
var lbImg = lb.querySelector("img");
var current = -1;
var lbImg = lb.querySelector('img')
var current = -1
function open(idx) {
current = idx;
var img = items[current].querySelector("img");
lbImg.removeAttribute("src");
lbImg.alt = "";
lb.classList.add("open");
lbImg.src = img.src;
lbImg.alt = img.alt;
current = idx
var img = items[current].querySelector('img')
lbImg.removeAttribute('src')
lbImg.alt = ''
lb.classList.add('open')
lbImg.src = img.src
lbImg.alt = img.alt
}
function close() {
lb.classList.remove("open");
current = -1;
lb.classList.remove('open')
current = -1
}
function prev() {
if (current < 0) return;
open((current - 1 + items.length) % items.length);
if (current < 0) return
open((current - 1 + items.length) % items.length)
}
function next() {
if (current < 0) return;
open((current + 1) % items.length);
if (current < 0) return
open((current + 1) % items.length)
}
items.forEach(function (item, i) {
item.addEventListener("click", function () {
open(i);
});
});
item.addEventListener('click', function () { open(i) })
})
lb.querySelector("#lb-close").addEventListener("click", close);
lb.querySelector("#lb-prev").addEventListener("click", function (e) {
e.stopPropagation();
prev();
});
lb.querySelector("#lb-next").addEventListener("click", function (e) {
e.stopPropagation();
next();
});
lb.querySelector('#lb-close').addEventListener('click', close)
lb.querySelector('#lb-prev').addEventListener('click', function (e) {
e.stopPropagation()
prev()
})
lb.querySelector('#lb-next').addEventListener('click', function (e) {
e.stopPropagation()
next()
})
document.addEventListener("keydown", function (e) {
if (!lb.classList.contains("open")) return;
if (e.key === "Escape") {
close();
e.preventDefault();
}
if (e.key === "ArrowLeft") {
prev();
e.preventDefault();
}
if (e.key === "ArrowRight") {
next();
e.preventDefault();
}
});
document.addEventListener('keydown', function (e) {
if (!lb.classList.contains('open')) return
if (e.key === 'Escape') { close(); e.preventDefault() }
if (e.key === 'ArrowLeft') { prev(); e.preventDefault() }
if (e.key === 'ArrowRight') { next(); e.preventDefault() }
})
lb.addEventListener("click", function (e) {
if (e.target === lb) close();
});
lb.addEventListener('click', function (e) {
if (e.target === lb) close()
})
var touchStartX = 0;
var touchStartY = 0;
lb.addEventListener(
"touchstart",
function (e) {
touchStartX = e.changedTouches[0].screenX;
touchStartY = e.changedTouches[0].screenY;
},
{ passive: true },
);
var touchStartX = 0
var touchStartY = 0
lb.addEventListener('touchstart', function (e) {
touchStartX = e.changedTouches[0].screenX
touchStartY = e.changedTouches[0].screenY
}, { passive: true })
lb.addEventListener(
"touchend",
function (e) {
var dx = e.changedTouches[0].screenX - touchStartX;
var dy = e.changedTouches[0].screenY - touchStartY;
lb.addEventListener('touchend', function (e) {
var dx = e.changedTouches[0].screenX - touchStartX
var dy = e.changedTouches[0].screenY - touchStartY
if (Math.abs(dx) > Math.abs(dy) && Math.abs(dx) > 50) {
if (dx < 0) next();
else prev();
if (dx < 0) next()
else prev()
}
},
{ passive: true },
);
})();
}, { passive: true })
}
window.initPortfolio()
document.addEventListener('htmx:afterSwap', window.initPortfolio)
})()

60
main.go
View file

@ -3,6 +3,7 @@ package main
//go:generate go tool templ generate templates
import (
"bytes"
"context"
"fmt"
"io"
@ -15,20 +16,57 @@ import (
"piekarnikgosi/utils"
"sort"
"strings"
"sync"
"github.com/tdewolff/minify/v2"
mcss "github.com/tdewolff/minify/v2/css"
mhtml "github.com/tdewolff/minify/v2/html"
mjs "github.com/tdewolff/minify/v2/js"
)
type ComponentRenderer interface {
Render(ctx context.Context, w io.Writer) error
}
func saveFile(name string, comp ComponentRenderer) {
file, err := os.Create("www/" + name + ".html")
var minifier = sync.OnceValue(func() *minify.M {
m := minify.New()
m.Add("text/html", &mhtml.Minifier{
KeepDocumentTags: true,
KeepEndTags: true,
})
m.Add("text/css", &mcss.Minifier{})
m.Add("application/javascript", &mjs.Minifier{})
return m
})
func minifyFile(path, mediatype string) {
data, err := os.ReadFile(path)
if err != nil {
log.Fatal(err)
}
defer file.Close()
err = os.WriteFile(path, minifyData(mediatype, data), 0666)
if err != nil {
log.Fatal(err)
}
}
err = comp.Render(context.Background(), file)
func minifyData(mediatype string, data []byte) []byte {
b, err := minifier().Bytes(mediatype, data)
if err != nil {
log.Fatal(err)
}
return b
}
func saveFile(name string, comp ComponentRenderer) {
var buf bytes.Buffer
err := comp.Render(context.Background(), &buf)
if err != nil {
log.Fatal(err)
}
minified := minifyData("text/html", buf.Bytes())
err = os.WriteFile("www/"+name+".html", minified, 0666)
if err != nil {
log.Fatal(err)
}
@ -117,7 +155,8 @@ func mergeStyles() {
bundledCSS = append(bundledCSS, '\n')
}
err := os.WriteFile("www/style.css", bundledCSS, 0666)
minified := minifyData("text/css", bundledCSS)
err := os.WriteFile("www/style.css", minified, 0666)
if err != nil {
log.Fatal(err)
}
@ -125,9 +164,12 @@ func mergeStyles() {
func ensureDir() {
log.Println("creating directories")
err := os.MkdirAll("www/images", 0777)
if err != nil {
log.Fatal(err)
os.MkdirAll("www/images", 0777)
entries, _ := os.ReadDir("www")
for _, e := range entries {
if !e.IsDir() && filepath.Ext(e.Name()) == ".html" {
os.Remove("www/" + e.Name())
}
}
}
@ -145,6 +187,8 @@ func main() {
mustCopy("img/favicon.avif", "www/images/favicon.avif")
mustCopy("img/logo.svg", "www/images/logo.svg")
mustCopy("js/portfolio.js", "www/portfolio.js")
minifyFile("www/portfolio.js", "application/javascript")
mustCopy("js/htmx.min.js", "www/htmx.min.js")
if len(os.Args) > 1 {
return
}