diff --git a/back_office/components/document_specification.go b/back_office/components/document_specification.go
index 06178a1b24eb0e90fce7c0aeeb24211e4cb0686f..69dea8d44964ea3955046ed1776af0adeffb6189 100644
--- a/back_office/components/document_specification.go
+++ b/back_office/components/document_specification.go
@@ -56,7 +56,7 @@ func DocumentSpecification(doc models.Document) g.Node {
 				h.P(g.Text("Status: "), h.Strong(g.Text(doc.Status.String()), h.Class("font-mono"))),
 			),
 			h.Div(
-				h.Class("col-span-1 rounded shadow-sm bg-white border border-gray-200 p-2 px-4"),
+				h.Class("col-span-1 rounded shadow-sm bg-white border border-gray-200 p-2 px-4 truncate"),
 				h.P(g.Text("Cache Hash: "), h.Strong(g.Text(doc.CacheHash), h.Class("font-mono"))),
 			),
 			h.Div(
diff --git a/back_office/documents_controller.go b/back_office/documents_controller.go
index 26f0d702475902db0971e89fb4675c2cd5dd3411..ab6c23c63d9cc02f6ed9c54e0969bf8c053e0aa2 100644
--- a/back_office/documents_controller.go
+++ b/back_office/documents_controller.go
@@ -25,6 +25,7 @@ import (
 type DocumentsController struct {
 	Logger         *zap.Logger
 	DocumentRepo   *repos.DocumentRepo
+	MediumRepo     *repos.MediumRepo
 	SessionManager *scs.SessionManager
 	View           *DocumentsView
 }
@@ -219,7 +220,7 @@ func (c *DocumentsController) view(ctx echo.Context) error {
 	}
 
 	document.BakedContent = c.DocumentRepo.ReplaceMagicBlocks(document.BakedContent)
-
+	document.BakedContent = c.MediumRepo.ReplaceMagicBlocks(document.BakedContent)
 	return ctx.HTML(http.StatusOK, c.View.Show(ctx, DocumentsViewShowParams{Document: document}))
 }
 
@@ -306,6 +307,7 @@ type NewDocumentsControllerParams struct {
 	Logger         *zap.Logger
 	View           *DocumentsView
 	DocumentRepo   *repos.DocumentRepo
+	MediumRepo     *repos.MediumRepo
 	SessionManager *scs.SessionManager
 }
 
@@ -320,6 +322,7 @@ func NewDocumentsController(p NewDocumentsControllerParams) NewDocumentsControll
 			Logger:         p.Logger,
 			View:           p.View,
 			DocumentRepo:   p.DocumentRepo,
+			MediumRepo:     p.MediumRepo,
 			SessionManager: p.SessionManager,
 		},
 	}
diff --git a/back_office/documents_view.go b/back_office/documents_view.go
index 124303203408cc9ac16b25a1a24e6d3907d53930..8ceb7c20986dab79f9ebca074e18cbdaa488d06d 100644
--- a/back_office/documents_view.go
+++ b/back_office/documents_view.go
@@ -157,7 +157,7 @@ func (v *DocumentsView) form(p DocumentsViewFormParams) g.Node {
 				h.Class("my-2"),
 				h.Summary(g.Text("Frontmatter/special blocks reference")),
 				h.Pre(
-					g.Text("---\ntoc: true\nsubdocuments_disabled: false\nredirect: https://example.com\ndescription: |\n  This is a description\n---\n\n{{latest:tag:N}} - will be replaced with an unordered list of latest N posts under `tag`\n\nFigure:\n\n!---\n[![](http://localhost:8080/!m/10/2137.jpg?width=69&height=669&fill=center)](http://host/!m/10/2137.jpg)\n!---\nFigure: caption for the figure."),
+					g.Text("---\ntoc: true\nsubdocuments_disabled: false\nredirect: https://example.com\ndescription: |\n  This is a description\n---\n\n{{latest:tag:N}} - will be replaced with an unordered list of latest N posts under `tag`\n{{gallery:slug}} - will be replaced with a gallery of images from the collection `slug`\n\nFigure:\n\n!---\n[![](http://localhost:8080/!m/10/2137.jpg?width=69&height=669&fill=center)](http://host/!m/10/2137.jpg)\n!---\nFigure: caption for the figure."),
 					h.Class("text-xs text-gray-500"),
 				),
 				h.P(g.Text("For more fancy stuff, see "), h.A(g.Text("Mmark"), h.Href("https://mmark.miek.nl/post/syntax/")), g.Text(" for more information."), h.Class("text-xs text-gray-500 mt-3")),
diff --git a/back_office/media_collection_create_form.go b/back_office/media_collection_create_form.go
index 554fa2541da1d9547951326e0ec0747bbdf841ef..5b9ca525c060e19f6775b691aa3ceb8e8a08f059 100644
--- a/back_office/media_collection_create_form.go
+++ b/back_office/media_collection_create_form.go
@@ -8,7 +8,7 @@ package back_office
 import "kita.gawa.moe/paweljw/vellum/repos"
 
 type mediaCollectionCreateForm struct {
-	Name string `form:"name" validate:"required,min=3,max=255,lowercase,printascii"`
+	Name string `form:"name" validate:"required,min=3,max=255,lowercase,printascii,excludesall= !"`
 }
 
 func (f *mediaCollectionCreateForm) ToParams() repos.CreateMediaCollectionParams {
diff --git a/commons/image.go b/commons/image.go
index d32fb00d9ab9e901b2d83b422aa218aeabc418af..32479293b491652ff8fc50d5a3755a646b9d5034 100644
--- a/commons/image.go
+++ b/commons/image.go
@@ -90,7 +90,7 @@ func (o *ImageOptions) ToPathSuffix() string {
 }
 
 func (o *ImageOptions) isValid() bool {
-	return o.Width > 0 && o.Height > 0
+	return (o.Width > 0 && o.Height > 0) || (o.Width == 0 && o.Height == 0)
 }
 
 func NewImageOptions(width, height, fit, fill, basePath string) (*ImageOptions, error) {
diff --git a/commons/markdown.go b/commons/markdown.go
index 6e4787e28293f2e40ceeb532c0cd25dc7759ab97..413e0ecd03cad7d71509b887a1eff28da51a40b7 100644
--- a/commons/markdown.go
+++ b/commons/markdown.go
@@ -142,8 +142,14 @@ func ExtractDescription(html string) string {
 		html = parts[0]
 	}
 
+	// Replace latest-tag and gallery tags with empty strings
+	re := regexp.MustCompile(`{{latest:[^}]+}}`)
+	html = re.ReplaceAllString(html, "")
+	re = regexp.MustCompile(`{{gallery:[^}]+}}`)
+	html = re.ReplaceAllString(html, "")
+
 	// Remove HTML tags using a regular expression
-	re := regexp.MustCompile("<[^>]*>")
+	re = regexp.MustCompile("<[^>]*>")
 	html = re.ReplaceAllString(html, "")
 
 	// Trim whitespace and limit to reasonable length
diff --git a/frontend/archives_controller.go b/frontend/archives_controller.go
index 6d208b8c6a5a4530120eab625ebea0beab330a64..c8015b6c4c2bb0ba7c5f1e6aa8a4cc3d1b9996c5 100644
--- a/frontend/archives_controller.go
+++ b/frontend/archives_controller.go
@@ -100,7 +100,7 @@ func buildGroupedByYear(docs []models.Document) map[uint]ArchivesYearGroup {
 		if _, exists := yearGroups[year].Months[month]; !exists {
 			yearGroups[year].Months[month] = ArchivesMonthGroup{
 				Month: month,
-				Docs:  []models.Document{doc},
+				Docs:  []models.Document{},
 			}
 		}
 
diff --git a/frontend/documents_controller.go b/frontend/documents_controller.go
index 9067a314654b3c070a3c24158abf070d5d8a1586..692e4f9c7f94e159073ed581d872c9cbb166a03f 100644
--- a/frontend/documents_controller.go
+++ b/frontend/documents_controller.go
@@ -25,6 +25,7 @@ type DocumentsController struct {
 	View           *DocumentView
 	Logger         *zap.Logger
 	DocumentRepo   *repos.DocumentRepo
+	MediumRepo     *repos.MediumRepo
 	SessionManager *scs.SessionManager
 }
 
@@ -89,7 +90,7 @@ func (h *DocumentsController) view(ctx echo.Context) error {
 
 	// Replace magic blocks in the format {{latest:tag:N}} with the latest N documents for the tag
 	doc.BakedContent = h.DocumentRepo.ReplaceMagicBlocks(doc.BakedContent)
-
+	doc.BakedContent = h.MediumRepo.ReplaceMagicBlocks(doc.BakedContent)
 	// If we're still here, cache is stale, so we need to render the document, checking for raw or baked
 	if ctx.QueryParam("raw") != "" {
 		return ctx.String(200, doc.Content)
@@ -126,6 +127,7 @@ type NewDocumentsControllerParams struct {
 	View           *DocumentView
 	Logger         *zap.Logger
 	DocumentRepo   *repos.DocumentRepo
+	MediumRepo     *repos.MediumRepo
 	SessionManager *scs.SessionManager
 }
 
@@ -140,6 +142,7 @@ func NewDocumentsController(p NewDocumentsControllerParams) NewDocumentsControll
 			View:           p.View,
 			Logger:         p.Logger,
 			DocumentRepo:   p.DocumentRepo,
+			MediumRepo:     p.MediumRepo,
 			SessionManager: p.SessionManager,
 		},
 	}
diff --git a/frontend/media_controller.go b/frontend/media_controller.go
index b2816d53aba24b801a247caeccca89a9776445c5..654f69f1037dab2210276efee040278b5686cfb5 100644
--- a/frontend/media_controller.go
+++ b/frontend/media_controller.go
@@ -6,6 +6,7 @@
 package frontend
 
 import (
+	"crypto/md5"
 	"fmt"
 	"net/http"
 	"strings"
@@ -43,6 +44,10 @@ func (c *MediaController) show(ctx echo.Context) error {
 		fmt.Sprintf("%s/%s", c.Config.StoragePath, medium.StoragePath),
 	)
 
+	if err != nil {
+		return ctx.String(http.StatusBadRequest, "Invalid image options: "+err.Error())
+	}
+
 	if imageOptions.VersionDoesntExist() {
 		referer := ctx.Request().Referer()
 		if referer == "" || !strings.HasPrefix(referer, c.Config.HttpHost) {
@@ -62,6 +67,16 @@ func (c *MediaController) show(ctx echo.Context) error {
 	if !medium.IsRenderable() || ctx.QueryParam("download") != "" {
 		return ctx.Attachment(fullPath, medium.Filename)
 	}
+
+	etag := fmt.Sprintf("%x", md5.Sum([]byte(fullPath)))
+	ctx.Response().Header().Set("Etag", etag)
+	ctx.Response().Header().Set("Cache-Control", "public, max-age=0, must-revalidate")
+	if match := ctx.Request().Header.Get("If-None-Match"); match != "" {
+		if strings.Contains(match, etag) {
+			return ctx.NoContent(http.StatusNotModified)
+		}
+	}
+
 	return ctx.File(fullPath)
 }
 
diff --git a/repos/medium_repo.go b/repos/medium_repo.go
index 29f5574fd98262a84b1d0c7599e9aa3b29c26ca4..b73d247b135f84455bba62e1ffccfdae2d447dfd 100644
--- a/repos/medium_repo.go
+++ b/repos/medium_repo.go
@@ -9,6 +9,8 @@ import (
 	"errors"
 	"fmt"
 	"os"
+	"regexp"
+	"strings"
 
 	"go.uber.org/fx"
 	"kita.gawa.moe/paweljw/vellum/config"
@@ -66,6 +68,33 @@ func (r *MediumRepo) Delete(collection *models.MediumCollection) error {
 	return nil
 }
 
+func (r *MediumRepo) ReplaceMagicBlocks(content string) string {
+	re := regexp.MustCompile(`{{gallery:([^}]+)}}`)
+	content = re.ReplaceAllStringFunc(content, func(match string) string {
+		parts := re.FindStringSubmatch(match)
+		collection, err := r.GetCollectionByName(parts[1])
+		if err != nil {
+			return "<p class=\"latest-for-tag-error\">Failed to get latest documents for tag " + parts[1] + ". Sorry!</p>"
+		}
+		items := []string{}
+		for _, medium := range collection.Mediums {
+			fullPath := fmt.Sprintf("%s/%d/%s", config.MEDIA_CONTROLLER_ROOT, medium.ID, medium.Filename)
+			small := fmt.Sprintf("%s?width=682&height=384&fill=center", fullPath)
+			items = append(
+				items,
+				fmt.Sprintf(
+					"<div class=\"gallery-item\"><a href=\"%s\" target=\"_blank\"><img src=\"%s\" alt=\"%s\" class=\"gallery-image\"></a></div>",
+					fullPath,
+					small,
+					medium.Filename,
+				),
+			)
+		}
+		return "<div class=\"gallery\">" + strings.Join(items, "\n") + "</div>"
+	})
+	return content
+}
+
 type CreateMediaCollectionParams struct {
 	Name string
 }
diff --git a/src/tailwind.css b/src/tailwind.css
index 41a5f51e6c7b799bf1b8cf2c848a04bdbd27f970..ef2d28c0c30e6d2248573d6cb46aad2476f0e73e 100644
--- a/src/tailwind.css
+++ b/src/tailwind.css
@@ -241,4 +241,8 @@ body.mathjax-loaded:not(.no-js) .math.display {
 
 .rendered-markdown figure figcaption {
   @apply text-sm text-gray-500 mb-0 italic;
+}
+
+.rendered-markdown .gallery {
+  @apply grid grid-cols-1 md:grid-cols-3 lg:grid-cols-4 gap-4;
 }
\ No newline at end of file