diff --git a/internal/server/api.go b/internal/server/api.go
index 59ce9a1..588d93d 100644
--- a/internal/server/api.go
+++ b/internal/server/api.go
@@ -6,8 +6,8 @@ import (
"strings"
"github.com/go-chi/chi/v5"
- "github.com/writekitapp/writekit/internal/auth"
- "github.com/writekitapp/writekit/internal/tenant"
+ "writekit/internal/auth"
+ "writekit/internal/tenant"
)
func (s *Server) publicAPIRoutes() chi.Router {
diff --git a/internal/server/blog.go b/internal/server/blog.go
index da86848..a0e1d02 100644
--- a/internal/server/blog.go
+++ b/internal/server/blog.go
@@ -16,13 +16,13 @@ import (
"time"
"github.com/go-chi/chi/v5"
- "github.com/writekitapp/writekit/internal/auth"
- "github.com/writekitapp/writekit/internal/build/assets"
- "github.com/writekitapp/writekit/internal/build/templates"
- "github.com/writekitapp/writekit/internal/config"
- "github.com/writekitapp/writekit/internal/markdown"
- "github.com/writekitapp/writekit/internal/tenant"
- "github.com/writekitapp/writekit/studio"
+ "writekit/internal/auth"
+ "writekit/internal/tenant/assets"
+ "writekit/internal/tenant/templates"
+ "writekit/internal/config"
+ "writekit/internal/markdown"
+ "writekit/internal/tenant"
+ "writekit/frontends/studio"
)
func (s *Server) serveBlog(w http.ResponseWriter, r *http.Request, subdomain string) {
@@ -59,12 +59,14 @@ func (s *Server) serveBlog(w http.ResponseWriter, r *http.Request, subdomain str
r = r.WithContext(ctx)
mux := chi.NewRouter()
+ mux.Use(auth.OptionalSessionMiddleware(s.database))
mux.Get("/", s.blogHome)
mux.Get("/posts", s.blogList)
mux.Get("/posts/{slug}", s.blogPost)
mux.Handle("/static/*", http.StripPrefix("/static/", assets.Handler()))
+ mux.Handle("/@owner-tools/*", http.HandlerFunc(s.serveOwnerTools))
mux.Route("/api/studio", func(r chi.Router) {
r.Use(demoAwareSessionMiddleware(s.database))
@@ -638,8 +640,20 @@ func computeETag(data []byte) string {
}
func (s *Server) servePreRendered(w http.ResponseWriter, r *http.Request, html []byte, etag, cacheControl string) {
+ tenantID, _ := r.Context().Value(tenantIDKey).(string)
+ modified := false
+
if demoInfo := GetDemoInfo(r); demoInfo.IsDemo {
html = s.injectDemoBanner(html, demoInfo.ExpiresAt)
+ modified = true
+ }
+
+ if s.isOwner(r, tenantID) {
+ html = s.injectOwnerTools(html)
+ modified = true
+ }
+
+ if modified {
etag = computeETag(html)
}
@@ -701,3 +715,49 @@ func timeOrZero(t *time.Time) time.Time {
}
return *t
}
+
+func (s *Server) isOwner(r *http.Request, tenantID string) bool {
+ if GetDemoInfo(r).IsDemo {
+ return true
+ }
+ userID := auth.GetUserID(r)
+ if userID == "" {
+ return false
+ }
+ isOwner, err := s.database.IsUserTenantOwner(r.Context(), userID, tenantID)
+ if err != nil {
+ return false
+ }
+ return isOwner
+}
+
+func (s *Server) injectOwnerTools(html []byte) []byte {
+ var script string
+ if s.ownerToolsURL != "" {
+ script = ``
+ } else {
+ script = ``
+ }
+ return bytes.Replace(html, []byte("
-
-
-
-
-
- Blog Hosting for Developers / 2025
- Your Words,
Your Platform
- Spin up a beautiful, fast blog in seconds. SQLite-powered, markdown-native, infinitely customizable.
-
-
-
{{DEMO_MINUTES}} minute demo. Create a real blog instead.
+
+
+
+
WriteKit — Blogging Platform for Developers
+
+
+
+
+
+
+
+
+
+
"), []byte(script+""), 1)
+}
+
+func (s *Server) serveOwnerTools(w http.ResponseWriter, r *http.Request) {
+ if s.ownerToolsURL == "" {
+ http.NotFound(w, r)
+ return
+ }
+
+ target, err := url.Parse(s.ownerToolsURL)
+ if err != nil {
+ http.Error(w, "internal error", http.StatusInternalServerError)
+ return
+ }
+
+ proxy := httputil.NewSingleHostReverseProxy(target)
+ proxy.Director = func(req *http.Request) {
+ req.URL.Scheme = target.Scheme
+ req.URL.Host = target.Host
+ req.Host = target.Host
+ }
+ proxy.ServeHTTP(w, r)
+}
diff --git a/internal/server/build.go b/internal/server/build.go
index ed1c846..a7d47a5 100644
--- a/internal/server/build.go
+++ b/internal/server/build.go
@@ -8,9 +8,9 @@ import (
"sync"
"time"
- "github.com/writekitapp/writekit/internal/build/templates"
- "github.com/writekitapp/writekit/internal/markdown"
- "github.com/writekitapp/writekit/internal/tenant"
+ "writekit/internal/tenant/templates"
+ "writekit/internal/markdown"
+ "writekit/internal/tenant"
)
type renderedPage struct {
@@ -39,14 +39,16 @@ func (s *Server) rebuildSite(ctx context.Context, tenantID string, db *sql.DB, h
codeTheme := getSettingOr(settings, "code_theme", "github")
fontKey := getSettingOr(settings, "font", "system")
isDemo := getSettingOr(settings, "is_demo", "") == "true"
+ codeThemeCSS, _ := markdown.GenerateChromaCSS(codeTheme)
pageData := templates.PageData{
- SiteName: siteName,
- Year: time.Now().Year(),
- FontURL: templates.GetFontURL(fontKey),
- FontFamily: templates.GetFontFamily(fontKey),
- Settings: settingsToMap(settings),
- NoIndex: isDemo,
+ SiteName: siteName,
+ Year: time.Now().Year(),
+ FontURL: templates.GetFontURL(fontKey),
+ FontFamily: templates.GetFontFamily(fontKey),
+ Settings: settingsToMap(settings),
+ NoIndex: isDemo,
+ CodeThemeCSS: template.CSS(codeThemeCSS),
}
var pages []renderedPage
diff --git a/internal/server/demo.go b/internal/server/demo.go
index 8a4d855..18b7766 100644
--- a/internal/server/demo.go
+++ b/internal/server/demo.go
@@ -8,9 +8,9 @@ import (
"strconv"
"time"
- "github.com/writekitapp/writekit/internal/auth"
- "github.com/writekitapp/writekit/internal/db"
- "github.com/writekitapp/writekit/internal/tenant"
+ "writekit/internal/auth"
+ "writekit/internal/db"
+ "writekit/internal/tenant"
)
type ctxKey string
diff --git a/internal/server/platform.go b/internal/server/platform.go
index 78597d8..3766721 100644
--- a/internal/server/platform.go
+++ b/internal/server/platform.go
@@ -11,7 +11,7 @@ import (
"regexp"
"strings"
- "github.com/writekitapp/writekit/internal/auth"
+ "writekit/internal/auth"
)
//go:embed templates/*.html
diff --git a/internal/server/ratelimit.go b/internal/server/ratelimit.go
index f58e6f8..da095a7 100644
--- a/internal/server/ratelimit.go
+++ b/internal/server/ratelimit.go
@@ -5,7 +5,7 @@ import (
"sync"
"time"
- "github.com/writekitapp/writekit/internal/config"
+ "writekit/internal/config"
)
type bucket struct {
diff --git a/internal/server/reader.go b/internal/server/reader.go
index 270579a..251e143 100644
--- a/internal/server/reader.go
+++ b/internal/server/reader.go
@@ -13,7 +13,7 @@ import (
"time"
"github.com/go-chi/chi/v5"
- "github.com/writekitapp/writekit/internal/tenant"
+ "writekit/internal/tenant"
)
func (s *Server) readerRoutes() chi.Router {
diff --git a/internal/server/server.go b/internal/server/server.go
index d35d1e6..9545773 100644
--- a/internal/server/server.go
+++ b/internal/server/server.go
@@ -11,26 +11,27 @@ import (
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
- "github.com/writekitapp/writekit/internal/auth"
- "github.com/writekitapp/writekit/internal/cloudflare"
- "github.com/writekitapp/writekit/internal/db"
- "github.com/writekitapp/writekit/internal/imaginary"
- "github.com/writekitapp/writekit/internal/storage"
- "github.com/writekitapp/writekit/internal/tenant"
+ "writekit/internal/auth"
+ "writekit/internal/cloudflare"
+ "writekit/internal/db"
+ "writekit/internal/imaginary"
+ "writekit/internal/storage"
+ "writekit/internal/tenant"
)
type Server struct {
- router chi.Router
- database *db.DB
- tenantPool *tenant.Pool
- tenantCache *tenant.Cache
- storage storage.Client
- imaginary *imaginary.Client
- cloudflare *cloudflare.Client
- rateLimiter *RateLimiter
- domain string
- jarvisURL string
- stopCleanup chan struct{}
+ router chi.Router
+ database *db.DB
+ tenantPool *tenant.Pool
+ tenantCache *tenant.Cache
+ storage storage.Client
+ imaginary *imaginary.Client
+ cloudflare *cloudflare.Client
+ rateLimiter *RateLimiter
+ domain string
+ jarvisURL string
+ ownerToolsURL string
+ stopCleanup chan struct{}
}
func New(database *db.DB, pool *tenant.Pool, cache *tenant.Cache, storageClient storage.Client) *Server {
@@ -44,6 +45,8 @@ func New(database *db.DB, pool *tenant.Pool, cache *tenant.Cache, storageClient
jarvisURL = "http://localhost:8090"
}
+ ownerToolsURL := os.Getenv("OWNER_TOOLS_URL")
+
var imgClient *imaginary.Client
if url := os.Getenv("IMAGINARY_URL"); url != "" {
imgClient = imaginary.New(url)
@@ -52,17 +55,18 @@ func New(database *db.DB, pool *tenant.Pool, cache *tenant.Cache, storageClient
cfClient := cloudflare.NewClient()
s := &Server{
- router: chi.NewRouter(),
- database: database,
- tenantPool: pool,
- tenantCache: cache,
- storage: storageClient,
- imaginary: imgClient,
- cloudflare: cfClient,
- rateLimiter: NewRateLimiter(),
- domain: domain,
- jarvisURL: jarvisURL,
- stopCleanup: make(chan struct{}),
+ router: chi.NewRouter(),
+ database: database,
+ tenantPool: pool,
+ tenantCache: cache,
+ storage: storageClient,
+ imaginary: imgClient,
+ cloudflare: cfClient,
+ rateLimiter: NewRateLimiter(),
+ domain: domain,
+ jarvisURL: jarvisURL,
+ ownerToolsURL: ownerToolsURL,
+ stopCleanup: make(chan struct{}),
}
s.router.Use(middleware.Logger)
diff --git a/internal/server/studio.go b/internal/server/studio.go
index 3ff11e2..89a05fc 100644
--- a/internal/server/studio.go
+++ b/internal/server/studio.go
@@ -15,11 +15,11 @@ import (
"github.com/go-chi/chi/v5"
"github.com/google/uuid"
"github.com/gorilla/websocket"
- "github.com/writekitapp/writekit/internal/config"
- "github.com/writekitapp/writekit/internal/db"
- "github.com/writekitapp/writekit/internal/imaginary"
- "github.com/writekitapp/writekit/internal/markdown"
- "github.com/writekitapp/writekit/internal/tenant"
+ "writekit/internal/config"
+ "writekit/internal/db"
+ "writekit/internal/imaginary"
+ "writekit/internal/markdown"
+ "writekit/internal/tenant"
)
func renderContent(q *tenant.Queries, r *http.Request, content string) string {
@@ -52,6 +52,7 @@ func (s *Server) studioRoutes() chi.Router {
r.Get("/settings", s.getSettings)
r.Put("/settings", s.updateSettings)
+ r.Get("/settings/schema", s.getSettingsSchema)
r.Get("/interaction-config", s.getStudioInteractionConfig)
r.Put("/interaction-config", s.updateInteractionConfig)
@@ -93,9 +94,8 @@ func (s *Server) studioRoutes() chi.Router {
r.Get("/sdk", s.getSDK)
r.Get("/lsp", s.proxyLSP)
- // Code themes
+ // Code theme CSS
r.Get("/code-theme.css", s.codeThemeCSS)
- r.Get("/code-themes", s.listCodeThemes)
// Plugin testing
r.Post("/plugins/test", s.testPlugin)
@@ -818,6 +818,89 @@ func (s *Server) updateSettings(w http.ResponseWriter, r *http.Request) {
jsonResponse(w, http.StatusOK, map[string]bool{"success": true})
}
+type settingOption struct {
+ Value string `json:"value"`
+ Label string `json:"label"`
+}
+
+type settingDefinition struct {
+ Key string `json:"key"`
+ Type string `json:"type"`
+ Label string `json:"label"`
+ Options []settingOption `json:"options,omitempty"`
+ Default string `json:"default,omitempty"`
+}
+
+func (s *Server) getSettingsSchema(w http.ResponseWriter, r *http.Request) {
+ schema := []settingDefinition{
+ {
+ Key: "accent_color",
+ Type: "color",
+ Label: "Accent Color",
+ Default: "#10b981",
+ },
+ {
+ Key: "font",
+ Type: "select",
+ Label: "Font",
+ Options: []settingOption{
+ {Value: "system", Label: "System Default"},
+ {Value: "inter", Label: "Inter"},
+ {Value: "georgia", Label: "Georgia"},
+ {Value: "merriweather", Label: "Merriweather"},
+ {Value: "source-serif", Label: "Source Serif"},
+ {Value: "jetbrains-mono", Label: "JetBrains Mono"},
+ },
+ Default: "system",
+ },
+ {
+ Key: "code_theme",
+ Type: "select",
+ Label: "Code Theme",
+ Options: []settingOption{
+ {Value: "github", Label: "GitHub Light"},
+ {Value: "github-dark", Label: "GitHub Dark"},
+ {Value: "vs", Label: "VS Light"},
+ {Value: "xcode", Label: "Xcode Light"},
+ {Value: "xcode-dark", Label: "Xcode Dark"},
+ {Value: "solarized-light", Label: "Solarized Light"},
+ {Value: "solarized-dark", Label: "Solarized Dark"},
+ {Value: "gruvbox-light", Label: "Gruvbox Light"},
+ {Value: "gruvbox", Label: "Gruvbox Dark"},
+ {Value: "nord", Label: "Nord"},
+ {Value: "onedark", Label: "One Dark"},
+ {Value: "dracula", Label: "Dracula"},
+ {Value: "monokai", Label: "Monokai"},
+ },
+ Default: "github",
+ },
+ {
+ Key: "layout",
+ Type: "select",
+ Label: "Layout",
+ Options: []settingOption{
+ {Value: "default", Label: "Classic"},
+ {Value: "minimal", Label: "Minimal"},
+ {Value: "magazine", Label: "Magazine"},
+ },
+ Default: "default",
+ },
+ {
+ Key: "compactness",
+ Type: "select",
+ Label: "Density",
+ Options: []settingOption{
+ {Value: "compact", Label: "Compact"},
+ {Value: "cozy", Label: "Cozy"},
+ {Value: "spacious", Label: "Spacious"},
+ },
+ Default: "cozy",
+ },
+ }
+
+ jsonResponse(w, http.StatusOK, schema)
+}
+
func (s *Server) listAssets(w http.ResponseWriter, r *http.Request) {
tenantID := r.Context().Value(tenantIDKey).(string)
@@ -1789,11 +1872,6 @@ func (s *Server) codeThemeCSS(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(css))
}
-func (s *Server) listCodeThemes(w http.ResponseWriter, r *http.Request) {
- themes := markdown.ListThemes()
- jsonResponse(w, http.StatusOK, themes)
-}
-
func contains(slice []string, item string) bool {
for _, s := range slice {
if s == item {
diff --git a/internal/server/sync.go b/internal/server/sync.go
index 78e6418..e750d56 100644
--- a/internal/server/sync.go
+++ b/internal/server/sync.go
@@ -5,7 +5,7 @@ import (
"log"
"time"
- "github.com/writekitapp/writekit/internal/tenant"
+ "writekit/internal/tenant"
)
func (s *Server) StartAnalyticsSync() {
diff --git a/internal/server/templates/index.html b/internal/server/templates/index.html
index 9250a17..5a06591 100644
--- a/internal/server/templates/index.html
+++ b/internal/server/templates/index.html
@@ -1,417 +1,604 @@
-
-
-
-
WriteKit — Full Blogging Platform. Lightweight. Yours.
-
-
-
-
-
-
-
-