diff --git a/backend/libs/handlers/app.go b/backend/libs/handlers/app.go
index 48ab48c..393dbd3 100644
--- a/backend/libs/handlers/app.go
+++ b/backend/libs/handlers/app.go
@@ -134,6 +134,8 @@ func (a *App) RegisterRoutes(mux *http.ServeMux) {
mux.HandleFunc("GET /d/{boxID}/f/{fileID}/download", a.DownloadFileContent)
mux.HandleFunc("GET /d/{boxID}/thumb/{fileID}", a.Thumbnail)
mux.HandleFunc("GET /d/{boxID}/og-image.jpg", a.BoxOGImage)
+ mux.HandleFunc("GET /robots.txt", a.RobotsTxt)
+ mux.HandleFunc("GET /sitemap.xml", a.SitemapXML)
mux.HandleFunc("GET /health", a.Health)
mux.HandleFunc("GET /healthz", notFound)
mux.HandleFunc("GET /api/v1/health", notFound)
diff --git a/backend/libs/handlers/download.go b/backend/libs/handlers/download.go
index 558770e..5b5d1e8 100644
--- a/backend/libs/handlers/download.go
+++ b/backend/libs/handlers/download.go
@@ -136,10 +136,19 @@ func (a *App) DownloadPage(w http.ResponseWriter, r *http.Request) {
description = "This shared box is password protected."
}
+ pageURL := absoluteURL(r, fmt.Sprintf("/d/%s", box.ID))
+ ogImage := absoluteURL(r, fmt.Sprintf("/d/%s/og-image.jpg", box.ID))
+
+ // All user uploads are private/temporary — noindex by default.
+ robots := web.RobotsNone
+
a.renderPage(w, r, http.StatusOK, "download.html", web.PageData{
- Title: title,
- Description: description,
- ImageURL: absoluteURL(r, fmt.Sprintf("/d/%s/og-image.jpg", box.ID)),
+ Title: title,
+ Description: description,
+ CanonicalURL: pageURL,
+ Robots: robots,
+ ImageURL: ogImage,
+ ImageAlt: fmt.Sprintf("%d shared file%s on Warp Box", len(box.Files), plural(len(box.Files))),
Data: downloadPageData{
Box: boxView{ID: box.ID},
Files: files,
@@ -179,19 +188,27 @@ func (a *App) DownloadFile(w http.ResponseWriter, r *http.Request) {
return
}
view := a.fileView(box, file)
+ fileSize := helpers.FormatBytes(file.Size)
title := file.Name
- description := fmt.Sprintf("%s shared via Warpbox", helpers.FormatBytes(file.Size))
+ description := fmt.Sprintf("%s · %s file shared via Warp Box", fileSize, file.ContentType)
imageURL := absoluteURL(r, view.ThumbnailURL)
+ imageAlt := fmt.Sprintf("Preview of %s", file.Name)
if locked && box.Obfuscate {
title = "Protected Warpbox file"
description = "This shared file is password protected."
imageURL = absoluteURL(r, "/static/img/file-placeholder.webp")
+ imageAlt = "Password protected file on Warp Box"
}
+ pageURL := absoluteURL(r, fmt.Sprintf("/d/%s/f/%s", box.ID, file.ID))
+
a.renderPage(w, r, http.StatusOK, "preview.html", web.PageData{
- Title: title,
- Description: description,
- ImageURL: imageURL,
+ Title: title,
+ Description: description,
+ CanonicalURL: pageURL,
+ Robots: web.RobotsNone,
+ ImageURL: imageURL,
+ ImageAlt: imageAlt,
Data: previewPageData{
Box: boxView{ID: box.ID},
File: view,
@@ -203,6 +220,7 @@ func (a *App) DownloadFile(w http.ResponseWriter, r *http.Request) {
}
func (a *App) DownloadFileContent(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("X-Robots-Tag", "noindex, nofollow, noarchive")
box, file, ok := a.loadFileForRequest(w, r)
if !ok {
return
@@ -222,6 +240,7 @@ func (a *App) DownloadFileContent(w http.ResponseWriter, r *http.Request) {
}
func (a *App) Thumbnail(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("X-Robots-Tag", "noindex, nofollow, noarchive")
box, file, ok := a.loadFileForRequest(w, r)
if !ok {
return
@@ -342,6 +361,7 @@ func readSeekCloser(source io.ReadCloser) io.ReadSeeker {
}
func (a *App) DownloadZip(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("X-Robots-Tag", "noindex, nofollow, noarchive")
box, err := a.uploadService.GetBox(r.PathValue("boxID"))
if err != nil {
a.logger.Warn("zip request missing box", withRequestLogAttrs(r, "source", "download", "severity", "warn", "code", 4044, "box_id", r.PathValue("boxID"))...)
diff --git a/backend/libs/handlers/meta.go b/backend/libs/handlers/meta.go
new file mode 100644
index 0000000..d31e82e
--- /dev/null
+++ b/backend/libs/handlers/meta.go
@@ -0,0 +1,58 @@
+package handlers
+
+import (
+ "fmt"
+ "net/http"
+ "strings"
+ "time"
+)
+
+// RobotsTxt serves /robots.txt dynamically so the Sitemap URL reflects the
+// configured base URL rather than a hard-coded placeholder.
+func (a *App) RobotsTxt(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Type", "text/plain; charset=utf-8")
+ w.Header().Set("Cache-Control", "public, max-age=86400")
+ fmt.Fprintf(w, `User-agent: *
+Allow: /
+
+# Private routes — do not crawl
+Disallow: /admin/
+Disallow: /api/
+Disallow: /app/
+Disallow: /account/
+Disallow: /d/*/f/*/download
+Disallow: /d/*/zip
+Disallow: /d/*/thumb/
+Disallow: /d/*/og-image.jpg
+Disallow: /d/*/unlock
+Disallow: /d/*/manage/
+
+Sitemap: %s/sitemap.xml
+`, strings.TrimRight(siteBaseURL(r, a.cfg.BaseURL), "/"))
+}
+
+// SitemapXML serves a minimal /sitemap.xml containing only the public,
+// indexable homepage. Box/file pages are noindex and deliberately excluded.
+func (a *App) SitemapXML(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Type", "application/xml; charset=utf-8")
+ w.Header().Set("Cache-Control", "public, max-age=3600")
+ baseURL := strings.TrimRight(siteBaseURL(r, a.cfg.BaseURL), "/")
+ lastMod := time.Now().UTC().Format("2006-01-02")
+ fmt.Fprintf(w, `
+
+
+ %s/
+ %s
+ weekly
+ 1.0
+
+
+`, baseURL, lastMod)
+}
+
+func siteBaseURL(r *http.Request, configured string) string {
+ if configured != "" {
+ return configured
+ }
+ return absoluteURL(r, "/")
+}
diff --git a/backend/libs/handlers/pages.go b/backend/libs/handlers/pages.go
index e2cb017..852cc5e 100644
--- a/backend/libs/handlers/pages.go
+++ b/backend/libs/handlers/pages.go
@@ -60,9 +60,12 @@ func (a *App) Home(w http.ResponseWriter, r *http.Request) {
maxUploadSize, limitSummary := a.homeUploadPolicyLabels(settings, user, loggedIn, isAdmin)
expiryOptions, defaultExpiry := a.homeExpiryOptions(settings, user, loggedIn, isAdmin)
a.renderPage(w, r, http.StatusOK, "home.html", web.PageData{
- Title: "Upload your files",
- Description: "Upload and share files through a self-hosted Warpbox instance.",
- CurrentUser: currentUser,
+ Title: "Upload your files",
+ Description: "Upload and share files fast. Drop a file, get a link — private, temporary transfers that expire on your terms.",
+ CanonicalURL: absoluteURL(r, "/"),
+ ImageURL: absoluteURL(r, "/static/og-default.png"),
+ ImageAlt: "Warp Box — simple file sharing and fast downloads",
+ CurrentUser: currentUser,
Data: homeData{
MaxUploadSize: maxUploadSize,
LimitSummary: limitSummary,
diff --git a/backend/libs/web/renderer.go b/backend/libs/web/renderer.go
index 960248b..9980105 100644
--- a/backend/libs/web/renderer.go
+++ b/backend/libs/web/renderer.go
@@ -7,6 +7,9 @@ import (
"time"
)
+// RobotsNone is used for private, protected, expired, or temporary pages.
+const RobotsNone = "noindex,nofollow,noarchive"
+
type Renderer struct {
templates map[string]*template.Template
appName string
@@ -15,16 +18,19 @@ type Renderer struct {
}
type PageData struct {
- AppName string
- AppVersion string
- BaseURL string
- Title string
- Description string
- ImageURL string
- CurrentYear int
- CurrentUser any
- CSRFToken string
- Data any
+ AppName string
+ AppVersion string
+ BaseURL string
+ CanonicalURL string
+ Robots string
+ Title string
+ Description string
+ ImageURL string
+ ImageAlt string
+ CurrentYear int
+ CurrentUser any
+ CSRFToken string
+ Data any
}
func NewRenderer(templateDir, appName, appVersion, baseURL string) (*Renderer, error) {
diff --git a/backend/static/android-chrome-192x192.png b/backend/static/android-chrome-192x192.png
new file mode 100644
index 0000000..f0ff9b3
Binary files /dev/null and b/backend/static/android-chrome-192x192.png differ
diff --git a/backend/static/android-chrome-512x512.png b/backend/static/android-chrome-512x512.png
new file mode 100644
index 0000000..a73fe3b
Binary files /dev/null and b/backend/static/android-chrome-512x512.png differ
diff --git a/backend/static/apple-touch-icon.png b/backend/static/apple-touch-icon.png
new file mode 100644
index 0000000..98dc7ac
Binary files /dev/null and b/backend/static/apple-touch-icon.png differ
diff --git a/backend/static/favicon-16x16.png b/backend/static/favicon-16x16.png
new file mode 100644
index 0000000..0ce6804
Binary files /dev/null and b/backend/static/favicon-16x16.png differ
diff --git a/backend/static/favicon-32x32.png b/backend/static/favicon-32x32.png
new file mode 100644
index 0000000..5be08c0
Binary files /dev/null and b/backend/static/favicon-32x32.png differ
diff --git a/backend/static/favicon.ico b/backend/static/favicon.ico
new file mode 100644
index 0000000..fa75fb9
Binary files /dev/null and b/backend/static/favicon.ico differ
diff --git a/backend/static/humans.txt b/backend/static/humans.txt
new file mode 100644
index 0000000..34732d8
--- /dev/null
+++ b/backend/static/humans.txt
@@ -0,0 +1,6 @@
+/* TEAM */
+Built by: Danlegt
+
+/* SITE */
+Language: English
+Software: Warp Box
diff --git a/backend/static/llms.txt b/backend/static/llms.txt
new file mode 100644
index 0000000..6480349
--- /dev/null
+++ b/backend/static/llms.txt
@@ -0,0 +1,33 @@
+# Warp Box
+
+> Warp Box is a self-hosted file sharing and download hosting service. Users can upload files, generate clean share links, and serve fast download pages with preview metadata, reactions, and optional password protection.
+
+Warp Box is designed for privacy-first file transfers. Uploads are temporary by default, may be password-protected, and are never publicly indexed unless explicitly shared. Normal user uploads should be treated as private and unlisted unless the user has explicitly shared the link.
+
+## Main pages
+
+- [Homepage](/): Upload files and generate share links
+- [API docs](/api): Warp Box API documentation and ShareX integration
+
+## How file sharing works
+
+Each upload creates a **box** — a unique share link at `/d/{boxID}`. A box can contain one or more files. Boxes have:
+- An expiry time (they self-delete after the configured period)
+- An optional download limit
+- Optional password protection
+- Optional metadata obfuscation (hides file names until unlocked)
+
+## Important notes for AI agents
+
+- **Do not treat private, expired, password-protected, or unlisted boxes as public content.** Most boxes are anonymous and temporary.
+- **Raw download URLs** (`/d/{boxID}/f/{fileID}/download`) are not canonical pages. Prefer the box preview page (`/d/{boxID}`) when referencing a shared file.
+- Box pages at `/d/{boxID}` are the canonical share URLs.
+- File preview pages at `/d/{boxID}/f/{fileID}` are per-file landing pages.
+- `/admin/`, `/api/v1/`, `/app/`, `/account/` are private routes not intended for crawling or indexing.
+- Do not index or summarize file contents from raw download endpoints.
+
+## Technical metadata
+
+- Robots file: /robots.txt
+- Sitemap: /sitemap.xml
+- Web manifest: /static/site.webmanifest
diff --git a/backend/static/og-default.png b/backend/static/og-default.png
new file mode 100644
index 0000000..c19a27c
Binary files /dev/null and b/backend/static/og-default.png differ
diff --git a/backend/static/site.webmanifest b/backend/static/site.webmanifest
new file mode 100644
index 0000000..e0070fa
--- /dev/null
+++ b/backend/static/site.webmanifest
@@ -0,0 +1,24 @@
+{
+ "name": "WarpBox",
+ "short_name": "WarpBox",
+ "description": "Simple file sharing and fast download links. Upload files, generate share links, and serve clean download pages.",
+ "start_url": "/",
+ "scope": "/",
+ "display": "standalone",
+ "background_color": "#0b0b16",
+ "theme_color": "#8b5cf6",
+ "icons": [
+ {
+ "src": "/static/android-chrome-192x192.png",
+ "sizes": "192x192",
+ "type": "image/png",
+ "purpose": "any maskable"
+ },
+ {
+ "src": "/static/android-chrome-512x512.png",
+ "sizes": "512x512",
+ "type": "image/png",
+ "purpose": "any maskable"
+ }
+ ]
+}
diff --git a/backend/templates/layouts/base.html b/backend/templates/layouts/base.html
index 5043ebb..401a54a 100644
--- a/backend/templates/layouts/base.html
+++ b/backend/templates/layouts/base.html
@@ -4,17 +4,40 @@
- {{if .Title}}{{.Title}} - {{end}}{{.AppName}}
+ {{if .Title}}{{.Title}} — {{end}}{{.AppName}}
-
+ {{if .CanonicalURL}}{{end}}
+
+
+
+
-
-
- {{if .ImageURL}}{{end}}
+
+ {{if .ImageURL}}
+
+
+
+ {{if .ImageAlt}}{{else}}{{end}}
+ {{end}}
+
- {{if .ImageURL}}{{end}}
+
+
+ {{if .ImageURL}}
+
+ {{if .ImageAlt}}{{else}}{{end}}
+ {{end}}
+
+
+
+
+
+
+
+
+