274 lines
6.5 KiB
Go
274 lines
6.5 KiB
Go
// A Dagger module for building and deploying a Zola static website
|
|
//
|
|
// This module provides modular functions to:
|
|
// - Build the Zola website
|
|
// - Create a Docker image serving the site with nginx
|
|
// - Deploy to S3 (future)
|
|
|
|
package main
|
|
|
|
import (
|
|
"context"
|
|
"dagger/blog/internal/dagger"
|
|
"fmt"
|
|
"strings"
|
|
)
|
|
|
|
const (
|
|
// Zola version to use for building
|
|
ZolaImage = "ghcr.io/getzola/zola:v0.21.0"
|
|
// Nginx version to use for serving
|
|
NginxImage = "nginx:1.27-alpine3.20-slim"
|
|
// Build output directory
|
|
BuildOutputDir = "public"
|
|
// Lychee version for link checking
|
|
LycheeImage = "lycheeverse/lychee:0.22-alpine"
|
|
// htmltest version for HTML validation
|
|
HtmltestImage = "wjdp/htmltest:v0.17.0"
|
|
)
|
|
|
|
type Blog struct{}
|
|
|
|
// Build builds the Zola website and returns the public directory
|
|
//
|
|
// Args:
|
|
// - source: The source directory containing the Zola site
|
|
//
|
|
// Returns:
|
|
// - A Directory containing the built website
|
|
func (m *Blog) Build(
|
|
ctx context.Context,
|
|
// The source directory containing the Zola site
|
|
// +defaultPath="/"
|
|
// +ignore=[".git", ".dagger", "public"]
|
|
source *dagger.Directory,
|
|
) *dagger.Directory {
|
|
return dag.Container().
|
|
From(ZolaImage).
|
|
WithMountedDirectory("/site", source).
|
|
WithWorkdir("/site").
|
|
WithExec([]string{"zola", "build", "--output-dir", BuildOutputDir, "--minify"}).
|
|
Directory(BuildOutputDir)
|
|
}
|
|
|
|
// prepareNginx prepares an nginx container with configuration
|
|
func (m *Blog) prepareNginx() *dagger.Container {
|
|
nginxConfig := `server {
|
|
listen 80;
|
|
listen [::]:80;
|
|
server_name _;
|
|
|
|
root /usr/share/nginx/html;
|
|
index index.html;
|
|
|
|
# Enable gzip compression
|
|
gzip on;
|
|
gzip_vary on;
|
|
gzip_min_length 1024;
|
|
gzip_types text/plain text/css text/xml text/javascript application/javascript application/xml+rss application/json;
|
|
|
|
location / {
|
|
try_files $uri $uri/ =404;
|
|
}
|
|
|
|
# Cache static assets
|
|
location ~* \.(jpg|jpeg|png|gif|ico|css|js|svg|woff|woff2|ttf|eot)$ {
|
|
expires 1y;
|
|
add_header Cache-Control "public, immutable";
|
|
}
|
|
}`
|
|
|
|
return dag.Container().
|
|
From(NginxImage).
|
|
WithNewFile("/etc/nginx/conf.d/default.conf", nginxConfig).
|
|
WithExposedPort(80)
|
|
}
|
|
|
|
// BuildContainer creates a Docker container image with nginx serving the built website
|
|
//
|
|
// Args:
|
|
// - source: The source directory containing the Zola site
|
|
//
|
|
// Returns:
|
|
// - A Container ready to serve the website
|
|
func (m *Blog) BuildContainer(
|
|
ctx context.Context,
|
|
// The source directory containing the Zola site
|
|
// +defaultPath="/"
|
|
// +ignore=[".git", ".dagger", "public"]
|
|
source *dagger.Directory,
|
|
) *dagger.Container {
|
|
builtSite := m.Build(ctx, source)
|
|
nginxContainer := m.prepareNginx()
|
|
|
|
return nginxContainer.WithDirectory("/usr/share/nginx/html", builtSite)
|
|
}
|
|
|
|
// getDefaultTag returns a tag based on the latest git commit date
|
|
func (m *Blog) getDefaultTag(
|
|
ctx context.Context,
|
|
source *dagger.Directory,
|
|
) (string, error) {
|
|
// Get the commit date in YYYYMMDDHHMMSS format
|
|
output, err := dag.Container().
|
|
From("alpine/git:latest").
|
|
WithMountedDirectory("/repo", source).
|
|
WithWorkdir("/repo").
|
|
WithExec([]string{"git", "log", "-1", "--format=%cd", "--date=format:%Y%m%d%H%M%S"}).
|
|
Stdout(ctx)
|
|
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return strings.TrimSpace(output), nil
|
|
}
|
|
|
|
// Publish publishes the container to a registry
|
|
//
|
|
// Args:
|
|
// - source: The source directory containing the Zola site
|
|
// - registry: The registry address (e.g., "ghcr.io/username/blog")
|
|
// - tag: The tag to use (defaults to git commit date in YYYYMMDDHHMMSS format)
|
|
//
|
|
// Returns:
|
|
// - The published image reference
|
|
func (m *Blog) Publish(
|
|
ctx context.Context,
|
|
// The source directory containing the Zola site
|
|
// +defaultPath="/"
|
|
// +ignore=[".git", ".dagger", "public"]
|
|
source *dagger.Directory,
|
|
// The registry address
|
|
registry string,
|
|
// +optional
|
|
tag string,
|
|
) (string, error) {
|
|
if tag == "" {
|
|
defaultTag, err := m.getDefaultTag(ctx, source)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
tag = defaultTag
|
|
}
|
|
|
|
container := m.BuildContainer(ctx, source)
|
|
address := registry + ":" + tag
|
|
|
|
return container.Publish(ctx, address)
|
|
}
|
|
|
|
// checkLinks validates all links in the built site
|
|
func (m *Blog) checkLinks(
|
|
ctx context.Context,
|
|
builtSite *dagger.Directory,
|
|
) error {
|
|
_, err := dag.Container().
|
|
From(LycheeImage).
|
|
WithMountedDirectory("/site", builtSite).
|
|
WithWorkdir("/site").
|
|
WithExec([]string{
|
|
"lychee",
|
|
"--base-url", "https://enoent.fr",
|
|
"--remap", "https://enoent\\.fr/ file:///site/",
|
|
"--offline",
|
|
"--no-progress",
|
|
"/site",
|
|
}).
|
|
Sync(ctx)
|
|
|
|
return err
|
|
}
|
|
|
|
// checkHTML validates HTML structure
|
|
func (m *Blog) checkHTML(
|
|
ctx context.Context,
|
|
builtSite *dagger.Directory,
|
|
) error {
|
|
// htmltest configuration
|
|
// Enable comprehensive HTML validation
|
|
htmltestConfig := `DirectoryPath: /site
|
|
CheckDoctype: true
|
|
CheckAnchors: true
|
|
CheckLinks: true
|
|
CheckImages: true
|
|
CheckScripts: true
|
|
CheckFavicon: false
|
|
CheckMetaRefresh: true
|
|
CheckMetaDescription: true
|
|
EnforceHTTPS: false
|
|
IgnoreInternalEmptyHash: true
|
|
IgnoreDirectoryMissingTrailingSlash: true
|
|
CheckExternal: false
|
|
IgnoreURLs:
|
|
- "^https://enoent\\.fr/"`
|
|
|
|
_, err := dag.Container().
|
|
From(HtmltestImage).
|
|
WithMountedDirectory("/site", builtSite).
|
|
WithWorkdir("/test").
|
|
WithNewFile(".htmltest.yml", htmltestConfig).
|
|
WithExec([]string{"htmltest"}).
|
|
Sync(ctx)
|
|
|
|
return err
|
|
}
|
|
|
|
// Check runs all quality checks on the built site
|
|
//
|
|
// Args:
|
|
// - source: The source directory containing the Zola site
|
|
//
|
|
// Returns:
|
|
// - An error if any check fails
|
|
func (m *Blog) Check(
|
|
ctx context.Context,
|
|
// The source directory containing the Zola site
|
|
// +defaultPath="/"
|
|
// +ignore=[".git", ".dagger", "public"]
|
|
source *dagger.Directory,
|
|
) error {
|
|
// Build the site first
|
|
builtSite := m.Build(ctx, source)
|
|
|
|
type checkResult struct {
|
|
name string
|
|
err error
|
|
}
|
|
|
|
checks := []struct {
|
|
name string
|
|
fn func(context.Context, *dagger.Directory) error
|
|
}{
|
|
{"Link Check", m.checkLinks},
|
|
{"HTML Validation", m.checkHTML},
|
|
}
|
|
|
|
results := make(chan checkResult, len(checks))
|
|
|
|
for _, check := range checks {
|
|
check := check
|
|
go func() {
|
|
results <- checkResult{
|
|
name: check.name,
|
|
err: check.fn(ctx, builtSite),
|
|
}
|
|
}()
|
|
}
|
|
|
|
// Collect results
|
|
var failures []string
|
|
for i := 0; i < len(checks); i++ {
|
|
result := <-results
|
|
if result.err != nil {
|
|
failures = append(failures, fmt.Sprintf("%s failed: %v", result.name, result.err))
|
|
}
|
|
}
|
|
|
|
// Return combined error if any checks failed
|
|
if len(failures) > 0 {
|
|
return fmt.Errorf("checks failed:\n%s", strings.Join(failures, "\n"))
|
|
}
|
|
|
|
return nil
|
|
}
|