about summary refs log tree commit diff
path: root/bskyweb
diff options
context:
space:
mode:
Diffstat (limited to 'bskyweb')
-rw-r--r--bskyweb/.gitignore5
-rw-r--r--bskyweb/cmd/bskyweb/mailmodo.go68
-rw-r--r--bskyweb/cmd/bskyweb/main.go68
-rw-r--r--bskyweb/cmd/bskyweb/renderer.go82
-rw-r--r--bskyweb/cmd/bskyweb/server.go108
-rw-r--r--bskyweb/go.mod2
-rw-r--r--bskyweb/go.sum6
-rw-r--r--bskyweb/static.go6
-rw-r--r--bskyweb/static/js/.gitkeep1
-rw-r--r--bskyweb/templates.go6
-rw-r--r--bskyweb/templates/scripts.html2
11 files changed, 270 insertions, 84 deletions
diff --git a/bskyweb/.gitignore b/bskyweb/.gitignore
index 7994736da..4dc17cf0f 100644
--- a/bskyweb/.gitignore
+++ b/bskyweb/.gitignore
@@ -1,3 +1,4 @@
-/static/bundle.web.js
-/bskyweb
 .env
+
+# Don't check in the binary.
+/bskyweb
diff --git a/bskyweb/cmd/bskyweb/mailmodo.go b/bskyweb/cmd/bskyweb/mailmodo.go
new file mode 100644
index 000000000..67ee6a2d6
--- /dev/null
+++ b/bskyweb/cmd/bskyweb/mailmodo.go
@@ -0,0 +1,68 @@
+package main
+
+import (
+	"bytes"
+	"context"
+	"crypto/sha256"
+	"encoding/json"
+	"fmt"
+	"net/http"
+	"time"
+)
+
+type Mailmodo struct {
+	httpClient *http.Client
+	APIKey     string
+	BaseURL    string
+}
+
+func NewMailmodo(apiKey string) *Mailmodo {
+	return &Mailmodo{
+		APIKey:     apiKey,
+		BaseURL:    "https://api.mailmodo.com/api/v1",
+		httpClient: &http.Client{},
+	}
+}
+
+func (m *Mailmodo) request(ctx context.Context, httpMethod string, apiMethod string, data any) error {
+	endpoint := fmt.Sprintf("%s/%s", m.BaseURL, apiMethod)
+	js, err := json.Marshal(data)
+	if err != nil {
+		return fmt.Errorf("Mailmodo JSON encoding failed: %w", err)
+	}
+	req, err := http.NewRequestWithContext(ctx, httpMethod, endpoint, bytes.NewBuffer(js))
+	if err != nil {
+		return fmt.Errorf("Mailmodo HTTP creating request %s %s failed: %w", httpMethod, apiMethod, err)
+	}
+	req.Header.Set("mmApiKey", m.APIKey)
+	req.Header.Set("Content-Type", "application/json")
+
+	res, err := m.httpClient.Do(req)
+	if err != nil {
+		return fmt.Errorf("Mailmodo HTTP making request %s %s failed: %w", httpMethod, apiMethod, err)
+	}
+	defer res.Body.Close()
+
+	status := struct {
+		Success bool   `json:"success"`
+		Message string `json:"message"`
+	}{}
+	if err := json.NewDecoder(res.Body).Decode(&status); err != nil {
+		return fmt.Errorf("Mailmodo HTTP parsing response %s %s failed: %w", httpMethod, apiMethod, err)
+	}
+	if !status.Success {
+		return fmt.Errorf("Mailmodo API response %s %s failed: %s", httpMethod, apiMethod, status.Message)
+	}
+	return nil
+}
+
+func (m *Mailmodo) AddToList(ctx context.Context, listName, email string) error {
+	return m.request(ctx, "POST", "addToList", map[string]any{
+		"listName": listName,
+		"email":    email,
+		"data": map[string]any{
+			"email_hashed": fmt.Sprintf("%x", sha256.Sum256([]byte(email))),
+		},
+		"created_at": time.Now().UTC().Format(time.RFC3339),
+	})
+}
diff --git a/bskyweb/cmd/bskyweb/main.go b/bskyweb/cmd/bskyweb/main.go
index 4b691b686..fd1429902 100644
--- a/bskyweb/cmd/bskyweb/main.go
+++ b/bskyweb/cmd/bskyweb/main.go
@@ -35,33 +35,57 @@ func run(args []string) {
 		Usage: "web server for bsky.app web app (SPA)",
 	}
 
-	app.Flags = []cli.Flag{
-		&cli.StringFlag{
-			Name:    "pds-host",
-			Usage:   "method, hostname, and port of PDS instance",
-			Value:   "http://localhost:4849",
-			EnvVars: []string{"ATP_PDS_HOST"},
-		},
-		&cli.StringFlag{
-			Name:     "handle",
-			Usage:    "for PDS login",
-			Required: true,
-			EnvVars:  []string{"ATP_AUTH_HANDLE"},
-		},
-		&cli.StringFlag{
-			Name:     "password",
-			Usage:    "for PDS login",
-			Required: true,
-			EnvVars:  []string{"ATP_AUTH_PASSWORD"},
-		},
-		// TODO: local IP/port to bind on
-	}
-
 	app.Commands = []*cli.Command{
 		&cli.Command{
 			Name:   "serve",
 			Usage:  "run the server",
 			Action: serve,
+			Flags: []cli.Flag{
+				&cli.StringFlag{
+					Name:    "pds-host",
+					Usage:   "method, hostname, and port of PDS instance",
+					Value:   "http://localhost:4849",
+					EnvVars: []string{"ATP_PDS_HOST"},
+				},
+				&cli.StringFlag{
+					Name:     "handle",
+					Usage:    "for PDS login",
+					Required: true,
+					EnvVars:  []string{"ATP_AUTH_HANDLE"},
+				},
+				&cli.StringFlag{
+					Name:     "password",
+					Usage:    "for PDS login",
+					Required: true,
+					EnvVars:  []string{"ATP_AUTH_PASSWORD"},
+				},
+				&cli.StringFlag{
+					Name:     "mailmodo-api-key",
+					Usage:    "Mailmodo API key",
+					Required: false,
+					EnvVars:  []string{"MAILMODO_API_KEY"},
+				},
+				&cli.StringFlag{
+					Name:     "mailmodo-list-name",
+					Usage:    "Mailmodo contact list to add email addresses to",
+					Required: false,
+					EnvVars:  []string{"MAILMODO_LIST_NAME"},
+				},
+				&cli.StringFlag{
+					Name:     "http-address",
+					Usage:    "Specify the local IP/port to bind to",
+					Required: false,
+					Value:    ":8100",
+					EnvVars:  []string{"HTTP_ADDRESS"},
+				},
+				&cli.BoolFlag{
+					Name:     "debug",
+					Usage:    "Enable debug mode",
+					Value:    false,
+					Required: false,
+					EnvVars:  []string{"DEBUG"},
+				},
+			},
 		},
 	}
 	app.RunAndExitOnError()
diff --git a/bskyweb/cmd/bskyweb/renderer.go b/bskyweb/cmd/bskyweb/renderer.go
new file mode 100644
index 000000000..4bf8b80c5
--- /dev/null
+++ b/bskyweb/cmd/bskyweb/renderer.go
@@ -0,0 +1,82 @@
+package main
+
+import (
+	"bytes"
+	"embed"
+	"errors"
+	"fmt"
+	"io"
+	"path/filepath"
+
+	"github.com/flosch/pongo2/v6"
+	"github.com/labstack/echo/v4"
+)
+
+type RendererLoader struct {
+	prefix string
+	fs     *embed.FS
+}
+
+func NewRendererLoader(prefix string, fs *embed.FS) pongo2.TemplateLoader {
+	return &RendererLoader{
+		prefix: prefix,
+		fs:     fs,
+	}
+}
+func (l *RendererLoader) Abs(_, name string) string {
+	// TODO: remove this workaround
+	// Figure out why this method is being called
+	// twice on template names resulting in a failure to resolve
+	// the template name.
+	if filepath.HasPrefix(name, l.prefix) {
+		return name
+	}
+	return filepath.Join(l.prefix, name)
+}
+
+func (l *RendererLoader) Get(path string) (io.Reader, error) {
+	b, err := l.fs.ReadFile(path)
+	if err != nil {
+		return nil, fmt.Errorf("reading template %q failed: %w", path, err)
+	}
+	return bytes.NewReader(b), nil
+}
+
+type Renderer struct {
+	TemplateSet *pongo2.TemplateSet
+	Debug       bool
+}
+
+func NewRenderer(prefix string, fs *embed.FS, debug bool) *Renderer {
+	return &Renderer{
+		TemplateSet: pongo2.NewSet(prefix, NewRendererLoader(prefix, fs)),
+		Debug:       debug,
+	}
+}
+
+func (r Renderer) Render(w io.Writer, name string, data interface{}, c echo.Context) error {
+	var ctx pongo2.Context
+
+	if data != nil {
+		var ok bool
+		ctx, ok = data.(pongo2.Context)
+		if !ok {
+			return errors.New("no pongo2.Context data was passed")
+		}
+	}
+
+	var t *pongo2.Template
+	var err error
+
+	if r.Debug {
+		t, err = pongo2.FromFile(name)
+	} else {
+		t, err = r.TemplateSet.FromFile(name)
+	}
+
+	if err != nil {
+		return err
+	}
+
+	return t.ExecuteWriter(ctx, w)
+}
diff --git a/bskyweb/cmd/bskyweb/server.go b/bskyweb/cmd/bskyweb/server.go
index 7ae0fb54f..efa5f2057 100644
--- a/bskyweb/cmd/bskyweb/server.go
+++ b/bskyweb/cmd/bskyweb/server.go
@@ -2,15 +2,17 @@ package main
 
 import (
 	"context"
-	"errors"
 	"fmt"
-	"io"
+	"io/fs"
 	"net/http"
+	"os"
+	"strings"
 
 	comatproto "github.com/bluesky-social/indigo/api/atproto"
 	appbsky "github.com/bluesky-social/indigo/api/bsky"
 	cliutil "github.com/bluesky-social/indigo/cmd/gosky/util"
 	"github.com/bluesky-social/indigo/xrpc"
+	"github.com/bluesky-social/social-app/bskyweb"
 
 	"github.com/flosch/pongo2/v6"
 	"github.com/labstack/echo/v4"
@@ -18,60 +20,35 @@ import (
 	"github.com/urfave/cli/v2"
 )
 
-// TODO: embed templates in executable
-
-type Renderer struct {
-	Debug bool
-}
-
-func (r Renderer) Render(w io.Writer, name string, data interface{}, c echo.Context) error {
-
-	var ctx pongo2.Context
-
-	if data != nil {
-		var ok bool
-		ctx, ok = data.(pongo2.Context)
-
-		if !ok {
-			return errors.New("no pongo2.Context data was passed...")
-		}
-	}
-
-	var t *pongo2.Template
-	var err error
-
-	if r.Debug {
-		t, err = pongo2.FromFile(name)
-	} else {
-		t, err = pongo2.FromCache(name)
-	}
-
-	if err != nil {
-		return err
-	}
-
-	return t.ExecuteWriter(ctx, w)
-}
-
 type Server struct {
 	xrpcc *xrpc.Client
 }
 
 func serve(cctx *cli.Context) error {
+	debug := cctx.Bool("debug")
+	httpAddress := cctx.String("http-address")
+	pdsHost := cctx.String("pds-host")
+	atpHandle := cctx.String("handle")
+	atpPassword := cctx.String("password")
+	mailmodoAPIKey := cctx.String("mailmodo-api-key")
+	mailmodoListName := cctx.String("mailmodo-list-name")
+
+	// Mailmodo client.
+	mailmodo := NewMailmodo(mailmodoAPIKey)
 
 	// create a new session
 	// TODO: does this work with no auth at all?
 	xrpcc := &xrpc.Client{
 		Client: cliutil.NewHttpClient(),
-		Host:   cctx.String("pds-host"),
+		Host:   pdsHost,
 		Auth: &xrpc.AuthInfo{
-			Handle: cctx.String("handle"),
+			Handle: atpHandle,
 		},
 	}
 
 	auth, err := comatproto.SessionCreate(context.TODO(), xrpcc, &comatproto.SessionCreate_Input{
 		Identifier: &xrpcc.Auth.Handle,
-		Password:   cctx.String("password"),
+		Password:   atpPassword,
 	})
 	if err != nil {
 		return err
@@ -83,19 +60,32 @@ func serve(cctx *cli.Context) error {
 
 	server := Server{xrpcc}
 
+	staticHandler := http.FileServer(func() http.FileSystem {
+		if debug {
+			return http.FS(os.DirFS("static"))
+		}
+		fsys, err := fs.Sub(bskyweb.StaticFS, "static")
+		if err != nil {
+			log.Fatal(err)
+		}
+		return http.FS(fsys)
+	}())
+
 	e := echo.New()
 	e.HideBanner = true
 	e.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{
+		// Don't log requests for static content.
+		Skipper: func(c echo.Context) bool {
+			return strings.HasPrefix(c.Request().URL.Path, "/static")
+		},
 		Format: "method=${method} path=${uri} status=${status} latency=${latency_human}\n",
 	}))
-	e.Renderer = Renderer{Debug: true}
+	e.Renderer = NewRenderer("templates/", &bskyweb.TemplateFS, debug)
 	e.HTTPErrorHandler = customHTTPErrorHandler
 
 	// configure routes
-	e.File("/robots.txt", "static/robots.txt")
-	e.Static("/static", "static")
-	e.Static("/static/js", "../web-build/static/js")
-
+	e.GET("/robots.txt", echo.WrapHandler(staticHandler))
+	e.GET("/static/*", echo.WrapHandler(http.StripPrefix("/static/", staticHandler)))
 	e.GET("/", server.WebHome)
 
 	// generic routes
@@ -118,9 +108,17 @@ func serve(cctx *cli.Context) error {
 	e.GET("/profile/:handle/post/:rkey/downvoted-by", server.WebGeneric)
 	e.GET("/profile/:handle/post/:rkey/reposted-by", server.WebGeneric)
 
-	bind := "localhost:8100"
-	log.Infof("starting server bind=%s", bind)
-	return e.Start(bind)
+	// Mailmodo
+	e.POST("/waitlist", func(c echo.Context) error {
+		email := strings.TrimSpace(c.FormValue("email"))
+		if err := mailmodo.AddToList(c.Request().Context(), mailmodoListName, email); err != nil {
+			return err
+		}
+		return c.JSON(http.StatusOK, map[string]bool{"success": true})
+	})
+
+	log.Infof("starting server address=%s", httpAddress)
+	return e.Start(httpAddress)
 }
 
 func customHTTPErrorHandler(err error, c echo.Context) {
@@ -132,18 +130,18 @@ func customHTTPErrorHandler(err error, c echo.Context) {
 	data := pongo2.Context{
 		"statusCode": code,
 	}
-	c.Render(code, "templates/error.html", data)
+	c.Render(code, "error.html", data)
 }
 
 // handler for endpoint that have no specific server-side handling
 func (srv *Server) WebGeneric(c echo.Context) error {
 	data := pongo2.Context{}
-	return c.Render(http.StatusOK, "templates/base.html", data)
+	return c.Render(http.StatusOK, "base.html", data)
 }
 
 func (srv *Server) WebHome(c echo.Context) error {
 	data := pongo2.Context{}
-	return c.Render(http.StatusOK, "templates/home.html", data)
+	return c.Render(http.StatusOK, "home.html", data)
 }
 
 func (srv *Server) WebPost(c echo.Context) error {
@@ -152,7 +150,7 @@ func (srv *Server) WebPost(c echo.Context) error {
 	rkey := c.Param("rkey")
 	// sanity check argument
 	if len(handle) > 4 && len(handle) < 128 && len(rkey) > 0 {
-		ctx := context.TODO()
+		ctx := c.Request().Context()
 		// requires two fetches: first fetch profile (!)
 		pv, err := appbsky.ActorGetProfile(ctx, srv.xrpcc, handle)
 		if err != nil {
@@ -172,7 +170,7 @@ func (srv *Server) WebPost(c echo.Context) error {
 		}
 
 	}
-	return c.Render(http.StatusOK, "templates/post.html", data)
+	return c.Render(http.StatusOK, "post.html", data)
 }
 
 func (srv *Server) WebProfile(c echo.Context) error {
@@ -180,7 +178,7 @@ func (srv *Server) WebProfile(c echo.Context) error {
 	handle := c.Param("handle")
 	// sanity check argument
 	if len(handle) > 4 && len(handle) < 128 {
-		ctx := context.TODO()
+		ctx := c.Request().Context()
 		pv, err := appbsky.ActorGetProfile(ctx, srv.xrpcc, handle)
 		if err != nil {
 			log.Warnf("failed to fetch handle: %s\t%v", handle, err)
@@ -189,5 +187,5 @@ func (srv *Server) WebProfile(c echo.Context) error {
 		}
 	}
 
-	return c.Render(http.StatusOK, "templates/profile.html", data)
+	return c.Render(http.StatusOK, "profile.html", data)
 }
diff --git a/bskyweb/go.mod b/bskyweb/go.mod
index dc7af5723..f6e03f4fd 100644
--- a/bskyweb/go.mod
+++ b/bskyweb/go.mod
@@ -4,6 +4,7 @@ go 1.19
 
 require (
 	github.com/bluesky-social/indigo v0.0.0-20230307000525-294e33e70185
+	github.com/flosch/pongo2/v6 v6.0.0
 	github.com/ipfs/go-log v1.0.5
 	github.com/joho/godotenv v1.5.1
 	github.com/labstack/echo/v4 v4.10.2
@@ -13,7 +14,6 @@ require (
 require (
 	github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
 	github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 // indirect
-	github.com/flosch/pongo2/v6 v6.0.0 // indirect
 	github.com/go-logr/logr v1.2.3 // indirect
 	github.com/go-logr/stdr v1.2.2 // indirect
 	github.com/goccy/go-json v0.10.0 // indirect
diff --git a/bskyweb/go.sum b/bskyweb/go.sum
index df9d87dc6..1f4bfa907 100644
--- a/bskyweb/go.sum
+++ b/bskyweb/go.sum
@@ -1,8 +1,6 @@
 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
 github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
 github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
-github.com/bluesky-social/indigo v0.0.0-20230306194356-5958f14d5152 h1:7fHM+tQHJN5lsMU8FvV4bNuWpD0Dd+pAUSuoLYdcYIQ=
-github.com/bluesky-social/indigo v0.0.0-20230306194356-5958f14d5152/go.mod h1:xy2hI4NMC6fgUefSJcCst6E0yo9Xbfd97aF27lgHyHE=
 github.com/bluesky-social/indigo v0.0.0-20230307000525-294e33e70185 h1:WnaOpRFWE8Tmw0IeXEEthsqBZtNG6/niokmWANv/aEU=
 github.com/bluesky-social/indigo v0.0.0-20230307000525-294e33e70185/go.mod h1:xy2hI4NMC6fgUefSJcCst6E0yo9Xbfd97aF27lgHyHE=
 github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
@@ -64,9 +62,11 @@ github.com/klauspost/cpuid/v2 v2.2.3 h1:sxCkb+qR91z4vsqw4vGGZlDgPz3G7gjaLyK3V8y7
 github.com/klauspost/cpuid/v2 v2.2.3/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
 github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
 github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
+github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
 github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
 github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
 github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
+github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
 github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
 github.com/labstack/echo/v4 v4.10.2 h1:n1jAhnq/elIFTHr1EYpiYtyKgx4RW9ccVgkqByZaN2M=
 github.com/labstack/echo/v4 v4.10.2/go.mod h1:OEyqf2//K1DFdE57vw2DRgWY0M7s65IVQO2FzvI4J5k=
@@ -129,6 +129,7 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
+github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k=
 github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
 github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
 github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
@@ -266,6 +267,7 @@ golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3j
 golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
 gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
 gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
diff --git a/bskyweb/static.go b/bskyweb/static.go
new file mode 100644
index 000000000..a67d189f5
--- /dev/null
+++ b/bskyweb/static.go
@@ -0,0 +1,6 @@
+package bskyweb
+
+import "embed"
+
+//go:embed static/*
+var StaticFS embed.FS
diff --git a/bskyweb/static/js/.gitkeep b/bskyweb/static/js/.gitkeep
new file mode 100644
index 000000000..57404ad76
--- /dev/null
+++ b/bskyweb/static/js/.gitkeep
@@ -0,0 +1 @@
+NOOP
diff --git a/bskyweb/templates.go b/bskyweb/templates.go
new file mode 100644
index 000000000..ce3fa29af
--- /dev/null
+++ b/bskyweb/templates.go
@@ -0,0 +1,6 @@
+package bskyweb
+
+import "embed"
+
+//go:embed templates/*
+var TemplateFS embed.FS
diff --git a/bskyweb/templates/scripts.html b/bskyweb/templates/scripts.html
deleted file mode 100644
index 30826bfb9..000000000
--- a/bskyweb/templates/scripts.html
+++ /dev/null
@@ -1,2 +0,0 @@
-<script defer="defer" src="/static/js/412.e47ad7b9.js"></script>
-<script defer="defer" src="/static/js/main.f526ceaa.js"></script>
\ No newline at end of file