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 { img {
color: transparent; 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/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/cli/browser v1.3.0 // indirect github.com/cli/browser v1.3.0 // indirect
github.com/fatih/color v1.16.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-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/natefinch/atomic v1.0.1 // 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/mod v0.26.0 // indirect
golang.org/x/net v0.51.0 // indirect golang.org/x/net v0.51.0 // indirect
golang.org/x/sync v0.16.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 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/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 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= 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 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 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/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 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 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 h1:EGMPT//Ezu+ylkCijjPc+f4Aih7sZvaAr+O3EHBxvZg=
golang.org/x/mod v0.26.0/go.mod h1:/j6NAhSk8iQ723BGAUyoAcn7SlD7s15Dp9Nd/SfeaFQ= 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= 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.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 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= 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 h1:mBffYraMEf7aa0sB+NuKnuCy8qI/9Bughn8dC2Gu5r0=
golang.org/x/tools v0.35.0/go.mod h1:NKdj5HkL/73byiZSJjqJgKn3ep7KjFkBOkR/Hps3VPw= golang.org/x/tools v0.35.0/go.mod h1:NKdj5HkL/73byiZSJjqJgKn3ep7KjFkBOkR/Hps3VPw=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=

View file

@ -24,6 +24,13 @@ func processImage(fileName string) string {
return dst[4:] return dst[4:]
} }
templ AvifImage(srcPath, alt string) { templ AvifImage(srcPath, alt string, lazy bool, priority bool) {
<img src={ processImage(srcPath) } alt={ alt }/> <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> <a href="wyroby.html" class="btn">Zobacz naszą ofertę</a>
</div> </div>
<div> <div>
@AvifImage("001.avif", "tort") @AvifImage("001.avif", "tort", false, true)
</div> </div>
</header> </header>
<section id="about-us"> <section id="about-us">
<div> <div>
<div> <div>
<div> <div>
@AvifImage("002.avif", "kawiarnia") @AvifImage("002.avif", "kawiarnia", true, false)
</div> </div>
<div> <div>
@AvifImage("003.avif", "kawiarnia") @AvifImage("003.avif", "kawiarnia", true, false)
</div> </div>
</div> </div>
<div> <div>
@ -42,7 +42,7 @@ templ Index() {
<h2>Nasze Wyroby</h2> <h2>Nasze Wyroby</h2>
<div class="card"> <div class="card">
<div> <div>
@AvifImage("004.avif", "tort") @AvifImage("004.avif", "tort", true, false)
</div> </div>
<div> <div>
<h3>Torty Okazjonalne</h3> <h3>Torty Okazjonalne</h3>
@ -69,7 +69,7 @@ templ Index() {
</div> </div>
<div class="card"> <div class="card">
<div> <div>
@AvifImage("005.avif", "ciasto") @AvifImage("005.avif", "ciasto", true, false)
</div> </div>
<div class="greener"> <div class="greener">
<h3>Domowe Ciasta</h3> <h3>Domowe Ciasta</h3>
@ -96,7 +96,7 @@ templ Index() {
</div> </div>
<div class="card"> <div class="card">
<div> <div>
@AvifImage("006.avif", "slodkosci") @AvifImage("006.avif", "slodkosci", true, false)
</div> </div>
<div> <div>
<h3>Słodkie Stoły</h3> <h3>Słodkie Stoły</h3>
@ -174,10 +174,10 @@ templ Index() {
</div> </div>
<div> <div>
<div> <div>
@AvifImage("007.avif", "kawiarnia") @AvifImage("007.avif", "kawiarnia", true, false)
</div> </div>
<div> <div>
@AvifImage("008.avif", "ciasto i kawa") @AvifImage("008.avif", "ciasto i kawa", true, false)
</div> </div>
</div> </div>
</div> </div>

View file

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

View file

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

View file

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

View file

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

View file

@ -171,7 +171,7 @@ templ WyrobyTorty() {
<header> <header>
<h2>Torty Musowe</h2> <h2>Torty Musowe</h2>
<div> <div>
@AvifImage("010.avif", "tort musowy") @AvifImage("010.avif", "tort musowy", true, false)
</div> </div>
</header> </header>
<div> <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 () { (function () {
var grid = document.getElementById("portfolio-grid"); window.initPortfolio = function () {
if (!grid) return; var old = document.getElementById('lightbox')
if (old) old.remove()
var items = Array.from(grid.children); var grid = document.getElementById('portfolio-grid')
if (!items.length) return; if (!grid) return
var lb = document.createElement("div"); var items = Array.from(grid.children)
lb.id = "lightbox"; if (!items.length) return
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);
var lbImg = lb.querySelector("img"); var lb = document.createElement('div')
var current = -1; 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)
function open(idx) { var lbImg = lb.querySelector('img')
current = idx; var current = -1
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() { function open(idx) {
lb.classList.remove("open"); current = idx
current = -1; var img = items[current].querySelector('img')
} lbImg.removeAttribute('src')
lbImg.alt = ''
function prev() { lb.classList.add('open')
if (current < 0) return; lbImg.src = img.src
open((current - 1 + items.length) % items.length); lbImg.alt = img.alt
}
function next() {
if (current < 0) return;
open((current + 1) % items.length);
}
items.forEach(function (item, 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();
});
document.addEventListener("keydown", function (e) {
if (!lb.classList.contains("open")) return;
if (e.key === "Escape") {
close();
e.preventDefault();
} }
if (e.key === "ArrowLeft") {
prev(); function close() {
e.preventDefault(); lb.classList.remove('open')
current = -1
} }
if (e.key === "ArrowRight") {
next(); function prev() {
e.preventDefault(); if (current < 0) return
open((current - 1 + items.length) % items.length)
} }
});
lb.addEventListener("click", function (e) { function next() {
if (e.target === lb) close(); if (current < 0) return
}); open((current + 1) % items.length)
}
var touchStartX = 0; items.forEach(function (item, i) {
var touchStartY = 0; item.addEventListener('click', function () { open(i) })
lb.addEventListener( })
"touchstart",
function (e) {
touchStartX = e.changedTouches[0].screenX;
touchStartY = e.changedTouches[0].screenY;
},
{ passive: true },
);
lb.addEventListener( lb.querySelector('#lb-close').addEventListener('click', close)
"touchend", lb.querySelector('#lb-prev').addEventListener('click', function (e) {
function (e) { e.stopPropagation()
var dx = e.changedTouches[0].screenX - touchStartX; prev()
var dy = e.changedTouches[0].screenY - touchStartY; })
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() }
})
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 })
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 (Math.abs(dx) > Math.abs(dy) && Math.abs(dx) > 50) {
if (dx < 0) next(); if (dx < 0) next()
else prev(); 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 //go:generate go tool templ generate templates
import ( import (
"bytes"
"context" "context"
"fmt" "fmt"
"io" "io"
@ -15,20 +16,57 @@ import (
"piekarnikgosi/utils" "piekarnikgosi/utils"
"sort" "sort"
"strings" "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 { type ComponentRenderer interface {
Render(ctx context.Context, w io.Writer) error Render(ctx context.Context, w io.Writer) error
} }
func saveFile(name string, comp ComponentRenderer) { var minifier = sync.OnceValue(func() *minify.M {
file, err := os.Create("www/" + name + ".html") 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 { if err != nil {
log.Fatal(err) 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 { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
@ -117,7 +155,8 @@ func mergeStyles() {
bundledCSS = append(bundledCSS, '\n') 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 { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
@ -125,9 +164,12 @@ func mergeStyles() {
func ensureDir() { func ensureDir() {
log.Println("creating directories") log.Println("creating directories")
err := os.MkdirAll("www/images", 0777) os.MkdirAll("www/images", 0777)
if err != nil { entries, _ := os.ReadDir("www")
log.Fatal(err) 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/favicon.avif", "www/images/favicon.avif")
mustCopy("img/logo.svg", "www/images/logo.svg") mustCopy("img/logo.svg", "www/images/logo.svg")
mustCopy("js/portfolio.js", "www/portfolio.js") 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 { if len(os.Args) > 1 {
return return
} }