diff --git a/cmd/redirectord/redirectord.go b/cmd/redirectord/redirectord.go
index 40328916a15178f95f473d38249f7268d789ff77..a1b2024a20740af8f4adc368863df45c116e9de1 100644
--- a/cmd/redirectord/redirectord.go
+++ b/cmd/redirectord/redirectord.go
@@ -25,6 +25,7 @@ var (
 	templateDir = flag.String("template-dir", "/usr/share/autoradio/htdocs/templates", "HTML templates directory")
 	lbPolicy    = flag.String("lb-policy", "listeners_available,listeners_score,weighted", "Load balancing rules specification (see godoc documentation for details)")
 	nameservers = flag.String("nameservers", "", "Comma-separated list of name servers (not IPs) for the zone specified in --domain")
+	redirectMap = flag.String("redirect-map", "", "File containing a list of source path / target redirects, space-separated, one per line")
 
 	// Default DNS TTL (seconds).
 	dnsTtl = 5
@@ -78,9 +79,15 @@ func main() {
 	dnsRed := fe.NewDNSRedirector(client, *domain, *publicIPs, dnsTtl, ns)
 	dnsRed.Start(fmt.Sprintf(":%d", *dnsPort))
 
-	red, err := fe.NewHTTPRedirector(client, *domain, *lbPolicy, *staticDir, *templateDir)
+	httpRed, err := fe.NewHTTPRedirector(client, *domain, *lbPolicy, *staticDir, *templateDir)
 	if err != nil {
 		log.Fatal(err)
 	}
-	red.Run(fmt.Sprintf(":%d", *httpPort))
+	if *redirectMap != "" {
+		if err := httpRed.LoadStaticRedirects(*redirectMap); err != nil {
+			// An error loading the redirect map should not be fatal.
+			log.Printf("Warning: could not load static redirect map: %v", err)
+		}
+	}
+	httpRed.Run(fmt.Sprintf(":%d", *httpPort))
 }
diff --git a/fe/http.go b/fe/http.go
index a37a4fe9bad6475b0ac623fadc643d536e430e93..837aaf1dbb49fc78390ff7f8edf48f5d6c95db99 100644
--- a/fe/http.go
+++ b/fe/http.go
@@ -1,7 +1,9 @@
 package fe
 
 import (
+	"bufio"
 	"bytes"
+	"errors"
 	"flag"
 	"fmt"
 	"html/template"
@@ -10,6 +12,7 @@ import (
 	"net"
 	"net/http"
 	"net/url"
+	"os"
 	"path/filepath"
 	"strconv"
 	"strings"
@@ -99,6 +102,7 @@ type HTTPRedirector struct {
 	lb        *autoradioLoadBalancer
 	client    *autoradio.Client
 	template  *template.Template
+	redirects map[string]string
 }
 
 // NewHTTPRedirector creates a new HTTP redirector.
@@ -181,7 +185,7 @@ func (h *HTTPRedirector) withMount(f func(*autoradio.Mount, http.ResponseWriter,
 		mountPath := strings.TrimSuffix(r.URL.Path, ".m3u")
 		mount, err := h.client.GetMount(mountPath)
 		if err != nil {
-			http.Error(w, "Not Found", http.StatusNotFound)
+			http.NotFound(w, r)
 			return
 		}
 		f(mount, w, r)
@@ -212,20 +216,7 @@ func (h *HTTPRedirector) serveRelay(mount *autoradio.Mount, w http.ResponseWrite
 	if strings.HasSuffix(r.URL.Path, ".m3u") {
 		h.serveM3U(mount, w, r)
 	} else {
-		targetURL := streamURL(relayAddr, mount.Name)
-
-		// Firefox apparently caches redirects regardless of
-		// the status code, so we have to add some quite
-		// aggressive cache-busting headers. We serve a status
-		// code of 307 to HTTP/1.1 clients, 302 otherwise.
-		w.Header().Set("Cache-Control", "max-age=0,no-cache,no-store")
-		w.Header().Set("Pragma", "no-cache")
-		w.Header().Set("Expires", "-1")
-		code := 302
-		if r.ProtoMinor == 1 {
-			code = 307
-		}
-		http.Redirect(w, r, targetURL, code)
+		sendRedirect(w, r, streamURL(relayAddr, mount.Name))
 	}
 }
 
@@ -349,6 +340,8 @@ func (h *HTTPRedirector) createHandler() http.Handler {
 	mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
 		switch {
 		case r.Method == "SOURCE" || r.Method == "PUT":
+			// Icecast 2.4 started supporting the PUT
+			// method for sources, along the old SOURCE.
 			sourceHandler.ServeHTTP(w, r)
 		case r.URL.Path == "" || r.URL.Path == "/":
 			statusPageHandler.ServeHTTP(w, r)
@@ -357,8 +350,55 @@ func (h *HTTPRedirector) createHandler() http.Handler {
 		}
 	})
 
-	// Instrument the resulting HTTP handler.
-	return logHandler(trackRequestsHandler(mux))
+	// If a redirect map is present, run the redirect handler in
+	// front of everything.
+	var rooth http.Handler = mux
+	if h.redirects != nil {
+		rooth = h.redirectHandler(rooth)
+	}
+
+	// Instrument the resulting HTTP handler, and map global
+	// redirects.
+	return logHandler(trackRequestsHandler(rooth))
+}
+
+// Create a handler that will detect requests matching our redirect
+// map, and will serve a redirect for them.
+func (h *HTTPRedirector) redirectHandler(wrap http.Handler) http.Handler {
+	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		if target, ok := h.redirects[r.URL.Path]; ok {
+			sendRedirect(w, r, target)
+			return
+		}
+		wrap.ServeHTTP(w, r)
+	})
+}
+
+func parseRedirects(r io.Reader) (map[string]string, error) {
+	out := make(map[string]string)
+	s := bufio.NewScanner(r)
+	for s.Scan() {
+		line := s.Text()
+		if line == "" || strings.HasPrefix(line, "#") {
+			continue
+		}
+		fields := strings.Fields(line)
+		if len(fields) != 2 {
+			return nil, errors.New("syntax error")
+		}
+		out[fields[0]] = fields[1]
+	}
+	return out, nil
+}
+
+func (h *HTTPRedirector) LoadStaticRedirects(filename string) error {
+	f, err := os.Open(filename)
+	if err != nil {
+		return err
+	}
+	defer f.Close()
+	h.redirects, err = parseRedirects(f)
+	return err
 }
 
 // Run starts the HTTP server on the given addr. Does not return.
@@ -382,3 +422,18 @@ func addDefaultHeaders(w http.ResponseWriter) {
 	w.Header().Set("Expires", "-1")
 	w.Header().Set("Cache-Control", "no-store")
 }
+
+func sendRedirect(w http.ResponseWriter, r *http.Request, target string) {
+	// Firefox apparently caches redirects regardless of
+	// the status code, so we have to add some quite
+	// aggressive cache-busting headers. We serve a status
+	// code of 307 to HTTP/1.1 clients, 302 otherwise.
+	w.Header().Set("Cache-Control", "max-age=0,no-cache,no-store")
+	w.Header().Set("Pragma", "no-cache")
+	w.Header().Set("Expires", "-1")
+	code := http.StatusFound
+	if r.ProtoMinor == 1 {
+		code = http.StatusTemporaryRedirect
+	}
+	http.Redirect(w, r, target, code)
+}