feat: add source_url config option for footer source link

Thread source_url through: config.ServerConfig → Handler.sourceURL
→ PageData.SourceURL → template footer. Footer only shows Source
link when source_url is set.
This commit is contained in:
Franz Kafka 2026-03-22 08:34:20 +00:00
parent bb0b97820b
commit 805e7ffdc2
6 changed files with 18 additions and 9 deletions

View file

@ -77,7 +77,7 @@ func main() {
acSvc := autocomplete.NewService(cfg.Upstream.URL, cfg.HTTPTimeout()) acSvc := autocomplete.NewService(cfg.Upstream.URL, cfg.HTTPTimeout())
h := httpapi.NewHandler(svc, acSvc.Suggestions) h := httpapi.NewHandler(svc, acSvc.Suggestions, cfg.Server.SourceURL)
mux := http.NewServeMux() mux := http.NewServeMux()
mux.HandleFunc("/", h.Index) mux.HandleFunc("/", h.Index)

View file

@ -14,6 +14,11 @@ http_timeout = "10s"
# Example: "https://search.example.com" # Example: "https://search.example.com"
base_url = "" base_url = ""
# Link to the source code (shown in footer as "Source" link)
# Defaults to the upstream kafka repo if not set.
# Example: "https://git.example.com/my-kafka-fork"
source_url = ""
[upstream] [upstream]
# URL of an upstream metasearch instance for unported engines (env: UPSTREAM_SEARXNG_URL) # URL of an upstream metasearch instance for unported engines (env: UPSTREAM_SEARXNG_URL)
# Leave empty to run without an upstream proxy. # Leave empty to run without an upstream proxy.

View file

@ -40,7 +40,8 @@ type Config struct {
type ServerConfig struct { type ServerConfig struct {
Port int `toml:"port"` Port int `toml:"port"`
HTTPTimeout string `toml:"http_timeout"` HTTPTimeout string `toml:"http_timeout"`
BaseURL string `toml:"base_url"` // Public URL for OpenSearch XML (e.g. "https://search.example.com") BaseURL string `toml:"base_url"` // Public URL for OpenSearch XML (e.g. "https://search.example.com")
SourceURL string `toml:"source_url"` // Link to the source code (e.g. "https://git.example.com/fork/kafka")
} }
type UpstreamConfig struct { type UpstreamConfig struct {

View file

@ -30,12 +30,14 @@ import (
type Handler struct { type Handler struct {
searchSvc *search.Service searchSvc *search.Service
autocompleteSvc func(ctx context.Context, query string) ([]string, error) autocompleteSvc func(ctx context.Context, query string) ([]string, error)
sourceURL string
} }
func NewHandler(searchSvc *search.Service, autocompleteSuggestions func(ctx context.Context, query string) ([]string, error)) *Handler { func NewHandler(searchSvc *search.Service, autocompleteSuggestions func(ctx context.Context, query string) ([]string, error), sourceURL string) *Handler {
return &Handler{ return &Handler{
searchSvc: searchSvc, searchSvc: searchSvc,
autocompleteSvc: autocompleteSuggestions, autocompleteSvc: autocompleteSuggestions,
sourceURL: sourceURL,
} }
} }
@ -51,7 +53,7 @@ func (h *Handler) Index(w http.ResponseWriter, r *http.Request) {
http.NotFound(w, r) http.NotFound(w, r)
return return
} }
if err := views.RenderIndex(w); err != nil { if err := views.RenderIndex(w, h.sourceURL); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
} }
} }
@ -80,7 +82,7 @@ func (h *Handler) Search(w http.ResponseWriter, r *http.Request) {
if err != nil { if err != nil {
// For HTML, render error on the results page. // For HTML, render error on the results page.
if req.Format == contracts.FormatHTML || r.FormValue("format") == "html" { if req.Format == contracts.FormatHTML || r.FormValue("format") == "html" {
pd := views.PageData{Query: r.FormValue("q")} pd := views.PageData{SourceURL: h.sourceURL, Query: r.FormValue("q")}
if views.IsHTMXRequest(r) { if views.IsHTMXRequest(r) {
views.RenderSearchFragment(w, pd) views.RenderSearchFragment(w, pd)
} else { } else {
@ -95,7 +97,7 @@ func (h *Handler) Search(w http.ResponseWriter, r *http.Request) {
resp, err := h.searchSvc.Search(r.Context(), req) resp, err := h.searchSvc.Search(r.Context(), req)
if err != nil { if err != nil {
if req.Format == contracts.FormatHTML { if req.Format == contracts.FormatHTML {
pd := views.PageData{Query: req.Query} pd := views.PageData{SourceURL: h.sourceURL, Query: req.Query}
if views.IsHTMXRequest(r) { if views.IsHTMXRequest(r) {
views.RenderSearchFragment(w, pd) views.RenderSearchFragment(w, pd)
} else { } else {

View file

@ -35,7 +35,7 @@
</main> </main>
<footer> <footer>
<p>Powered by <a href="https://git.ashisgreat.xyz/penal-colony/kafka">kafka</a> — a privacy-respecting, open metasearch engine · <a href="https://git.ashisgreat.xyz/penal-colony/kafka">Source</a> · <a href="https://www.gnu.org/licenses/agpl-3.0.html">AGPLv3</a></p> <p>Powered by <a href="https://git.ashisgreat.xyz/penal-colony/kafka">kafka</a> — a privacy-respecting, open metasearch engine{{if .SourceURL}} · <a href="{{.SourceURL}}">Source</a>{{end}} · <a href="https://www.gnu.org/licenses/agpl-3.0.html">AGPLv3</a></p>
</footer> </footer>
<script src="/static/js/settings.js"></script> <script src="/static/js/settings.js"></script>

View file

@ -35,6 +35,7 @@ var staticFS embed.FS
// PageData holds all data passed to templates. // PageData holds all data passed to templates.
type PageData struct { type PageData struct {
SourceURL string
Query string Query string
Pageno int Pageno int
PrevPage int PrevPage int
@ -187,9 +188,9 @@ func FromResponse(resp contracts.SearchResponse, query string, pageno int) PageD
} }
// RenderIndex renders the homepage (search box only). // RenderIndex renders the homepage (search box only).
func RenderIndex(w http.ResponseWriter) error { func RenderIndex(w http.ResponseWriter, sourceURL string) error {
w.Header().Set("Content-Type", "text/html; charset=utf-8") w.Header().Set("Content-Type", "text/html; charset=utf-8")
return tmplIndex.ExecuteTemplate(w, "base", PageData{ShowHeader: true}) return tmplIndex.ExecuteTemplate(w, "base", PageData{ShowHeader: true, SourceURL: sourceURL})
} }
// RenderSearch renders the full search results page (with base layout). // RenderSearch renders the full search results page (with base layout).