repos
/ orchard main

orchard

mirror

Every site I host, in one repo, along with the Cloudflare Tunnel and Caddy that front them. It's all Go, Vite, and SQLite, and it runs on a desktop at home with nothing listening on an inbound port.

blogbuncaddycloudflare-tunneldockergogolanghomelabhtml-templatemonorepoself-hostedseosqlitestatic-sitetypstuptime-monitoringviteweb-analytics

3.4 KB · 118 lines · Go Raw History
  1package main
  2
  3import (
  4	"database/sql"
  5	"fmt"
  6	"html/template"
  7	"log/slog"
  8	"net/http"
  9	"time"
 10)
 11
 12// PageData is everything every template needs. A struct rather than a map:
 13// html/template errors on a missing field but renders nothing for a missing key.
 14type PageData struct {
 15	Title         string
 16	Description   string
 17	Path          string
 18	Canonical     string
 19	Staging       bool
 20	Analytics     bool
 21	AnalyticsID   string
 22	Authenticated bool
 23	Year          int
 24	BaseURL       string
 25	SourceURL     string
 26	SiteName      string
 27	AuthorName    string
 28
 29	OGImage    string
 30	JSONLD     template.JS
 31	Script     string
 32	Styles     []string
 33	PageScript string
 34	PageStyles []string
 35
 36	// Login
 37	Next  string
 38	Error string
 39
 40	// Home
 41	TotalChecks     int64
 42	TotalProperties int64
 43	FirstCheckAt    string
 44
 45	// Properties list and dashboard
 46	Properties    []*PropertyView
 47	Query         string
 48	Property      *PropertyView
 49	InsightGroups []InsightGroup
 50	ResponseTimes []ResponseTimePoint
 51	StatusCodes   []LabelCount
 52	UptimeSlices  []LabelPercent
 53	GeneratedAt   string
 54}
 55
 56func (s *site) home(w http.ResponseWriter, r *http.Request) {
 57	if s.auth.Authenticated(r) {
 58		http.Redirect(w, r, "/properties", http.StatusSeeOther)
 59		return
 60	}
 61
 62	data := s.page(r, "Self-hosted uptime monitoring",
 63		"Self-hosted uptime monitoring with public status pages, response time history, "+
 64			"Lighthouse audits and crawl findings.")
 65
 66	var firstCheck sql.NullInt64
 67	err := s.db.QueryRowContext(r.Context(),
 68		`SELECT (SELECT COUNT(*) FROM checks),
 69		        (SELECT COUNT(*) FROM properties),
 70		        (SELECT MIN(created_at) FROM checks)`).
 71		Scan(&data.TotalChecks, &data.TotalProperties, &firstCheck)
 72	if err != nil {
 73		// These numbers are decoration, so an error renders zeros rather than a 500.
 74		slog.Info(fmt.Sprintf("home totals: %v", err))
 75	}
 76	if firstCheck.Valid {
 77		data.FirstCheckAt = time.UnixMilli(firstCheck.Int64).UTC().Format("Jan 2, 2006")
 78	}
 79
 80	data.PageScript = s.pagesScript
 81	data.PageStyles = s.pagesStyles
 82	s.renderer.Render(w, http.StatusOK, "home.html", data)
 83}
 84
 85func favicon(w http.ResponseWriter, r *http.Request) {
 86	w.Header().Set("Content-Type", "image/svg+xml")
 87	w.Header().Set("Cache-Control", "public, max-age=86400")
 88	_, _ = w.Write([]byte(faviconSVG))
 89}
 90
 91const faviconSVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
 92<polyline points="2,34 18,34 24,28 30,14 36,52 42,20 48,34 62,34" fill="none" stroke="#6b9e78" stroke-width="6" stroke-linejoin="round" stroke-linecap="round"/>
 93<circle cx="30" cy="14" r="3.5" fill="#c9a84c"/>
 94</svg>`
 95
 96// robots refuses the crawl on staging. Both this and the noindex meta tag in
 97// base.html are needed: a crawler obeying robots.txt never fetches the tag.
 98func robots(w http.ResponseWriter, r *http.Request) {
 99	w.Header().Set("Content-Type", "text/plain; charset=utf-8")
100	if Staging {
101		_, _ = w.Write([]byte("User-agent: *\nDisallow: /\n"))
102		return
103	}
104	_, _ = fmt.Fprintf(w, "User-agent: *\nAllow: /\nSitemap: %s/sitemap.xml\n", baseURL)
105}
106
107// sitemap lists the one public page. Property dashboards stay out even when
108// public: a status page is handed to somebody, not searched for.
109func sitemap(w http.ResponseWriter, r *http.Request) {
110	w.Header().Set("Content-Type", "application/xml; charset=utf-8")
111	today := time.Now().UTC().Format("2006-01-02")
112	_, _ = fmt.Fprintf(w, `<?xml version="1.0" encoding="UTF-8"?>
113<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
114  <url><loc>%s/</loc><lastmod>%s</lastmod></url>
115</urlset>
116`, baseURL, today)
117}