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}} + + + + + + + + +