mirror of
https://github.com/usememos/memos.git
synced 2026-01-02 22:45:08 +08:00
Merge 2741a1cac7 into 96a91ebff0
This commit is contained in:
commit
8a19d64aea
4 changed files with 136 additions and 34 deletions
|
|
@ -3,14 +3,12 @@ package frontend
|
|||
import (
|
||||
"context"
|
||||
"embed"
|
||||
"errors"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/labstack/echo/v4/middleware"
|
||||
|
||||
"github.com/usememos/memos/internal/profile"
|
||||
"github.com/usememos/memos/internal/util"
|
||||
"github.com/usememos/memos/store"
|
||||
)
|
||||
|
||||
|
|
@ -29,41 +27,65 @@ func NewFrontendService(profile *profile.Profile, store *store.Store) *FrontendS
|
|||
}
|
||||
}
|
||||
|
||||
func (*FrontendService) Serve(_ context.Context, e *echo.Echo) {
|
||||
skipper := func(c echo.Context) bool {
|
||||
// Skip API routes.
|
||||
if util.HasPrefixes(c.Path(), "/api", "/memos.api.v1") {
|
||||
return true
|
||||
}
|
||||
// For index.html and root path, set no-cache headers to prevent browser caching
|
||||
// This prevents sensitive data from being accessible via browser back button after logout
|
||||
if c.Path() == "/" || c.Path() == "/index.html" {
|
||||
c.Response().Header().Set(echo.HeaderCacheControl, "no-cache, no-store, must-revalidate")
|
||||
c.Response().Header().Set("Pragma", "no-cache")
|
||||
c.Response().Header().Set("Expires", "0")
|
||||
return false
|
||||
func (*FrontendService) Serve(_ context.Context, e *echo.Echo) error {
|
||||
fs, err := fs.Sub(embeddedFiles, "dist")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
idx, err := parseFSTemplate(fs, "index.html")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
htmlMeta := map[string]string{
|
||||
"viewport": "width=device-width, initial-scale=1, user-scalable=no",
|
||||
}
|
||||
static := echo.StaticDirectoryHandler(fs, false)
|
||||
index := templateHandler(idx, templateConfig{
|
||||
MetaData: htmlMeta,
|
||||
})
|
||||
exploreFeedTitle := func(_ echo.Context) string {
|
||||
return "Public Memos"
|
||||
}
|
||||
userFeedTitle := func(c echo.Context) string {
|
||||
u := c.Param("username")
|
||||
|
||||
return u + " Memos"
|
||||
}
|
||||
assets := func(c echo.Context) error {
|
||||
p := c.Request().URL.Path
|
||||
if p == "/" || p == "/index.html" {
|
||||
// do not serve index.html from the filesystem
|
||||
// but serve it as rendered template instead
|
||||
return index(c)
|
||||
}
|
||||
|
||||
// Set Cache-Control header for static assets.
|
||||
// Since Vite generates content-hashed filenames (e.g., index-BtVjejZf.js),
|
||||
// we can cache aggressively but use immutable to prevent revalidation checks.
|
||||
// For frequently redeployed instances, use shorter max-age (1 hour) to avoid
|
||||
// serving stale assets after redeployment.
|
||||
c.Response().Header().Set(echo.HeaderCacheControl, "public, max-age=3600, immutable") // 1 hour
|
||||
return false
|
||||
}
|
||||
if err := static(c); err == nil || !errors.Is(err, echo.ErrNotFound) {
|
||||
return err
|
||||
}
|
||||
|
||||
// Route to serve the main app with HTML5 fallback for SPA behavior.
|
||||
e.Use(middleware.StaticWithConfig(middleware.StaticConfig{
|
||||
Filesystem: getFileSystem("dist"),
|
||||
HTML5: true, // Enable fallback to index.html
|
||||
Skipper: skipper,
|
||||
// fallback to the index document, assuming it is a SPA route
|
||||
return index(c)
|
||||
}
|
||||
e.GET("/", index)
|
||||
e.GET("/*", assets)
|
||||
e.GET("/explore", templateHandler(idx, templateConfig{
|
||||
MetaData: htmlMeta,
|
||||
InjectFeedURL: true,
|
||||
ResolveFeedTitle: exploreFeedTitle,
|
||||
}))
|
||||
e.GET("/u/:username", templateHandler(idx, templateConfig{
|
||||
MetaData: htmlMeta,
|
||||
InjectFeedURL: true,
|
||||
ResolveFeedTitle: userFeedTitle,
|
||||
}))
|
||||
}
|
||||
|
||||
func getFileSystem(path string) http.FileSystem {
|
||||
fs, err := fs.Sub(embeddedFiles, path)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return http.FS(fs)
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
71
server/router/frontend/template.go
Normal file
71
server/router/frontend/template.go
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
package frontend
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"text/template"
|
||||
|
||||
echo "github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
var templateFuncs = template.FuncMap{
|
||||
"default": templateFuncDefault,
|
||||
}
|
||||
|
||||
type templateData struct {
|
||||
FeedURL string
|
||||
FeedTitle string
|
||||
Title string
|
||||
MetaData map[string]string
|
||||
}
|
||||
|
||||
type templateConfig struct {
|
||||
InjectFeedURL bool
|
||||
ResolveFeedTitle func(c echo.Context) string
|
||||
MetaData map[string]string
|
||||
}
|
||||
|
||||
func templateHandler(tpl *template.Template, cfg templateConfig) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
data := &templateData{
|
||||
Title: "Memos",
|
||||
MetaData: cfg.MetaData,
|
||||
}
|
||||
|
||||
if cfg.InjectFeedURL {
|
||||
if cfg.ResolveFeedTitle != nil {
|
||||
data.FeedTitle = cfg.ResolveFeedTitle(c)
|
||||
}
|
||||
|
||||
data.FeedURL = c.Request().URL.JoinPath("rss.xml").String()
|
||||
}
|
||||
|
||||
header := c.Response().Header()
|
||||
if header.Get(echo.HeaderContentType) == "" {
|
||||
header.Set(echo.HeaderContentType, echo.MIMETextHTMLCharsetUTF8)
|
||||
}
|
||||
|
||||
// Prevent sensitive data from being accessible via browser back button after logout
|
||||
header.Set(echo.HeaderCacheControl, "no-cache, no-store, must-revalidate")
|
||||
header.Set("Pragma", "no-cache")
|
||||
header.Set("Expires", "0")
|
||||
|
||||
if err := tpl.Execute(c.Response().Writer, data); err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "unable to render template").SetInternal(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func parseFSTemplate(root fs.FS, file string) (*template.Template, error) {
|
||||
return template.New(file).Funcs(templateFuncs).ParseFS(root, file)
|
||||
}
|
||||
|
||||
func templateFuncDefault(fallback, value string) string {
|
||||
if value != "" {
|
||||
return value
|
||||
}
|
||||
|
||||
return fallback
|
||||
}
|
||||
|
|
@ -63,7 +63,9 @@ func NewServer(ctx context.Context, profile *profile.Profile, store *store.Store
|
|||
})
|
||||
|
||||
// Serve frontend static files.
|
||||
frontend.NewFrontendService(profile, store).Serve(ctx, echoServer)
|
||||
if err := frontend.NewFrontendService(profile, store).Serve(ctx, echoServer); err != nil {
|
||||
return nil, errors.Wrap(err, "unable to set up frontend service")
|
||||
}
|
||||
|
||||
rootGroup := echoServer.Group("")
|
||||
|
||||
|
|
|
|||
|
|
@ -8,9 +8,16 @@
|
|||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
|
||||
<link rel="icon" type="image/webp" href="/logo.webp" />
|
||||
<link rel="manifest" href="/site.webmanifest" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no" />
|
||||
<!-- memos.metadata.head -->
|
||||
<title>Memos</title>
|
||||
<!-- {{ printf "memos.metadata.head %s>" "--" }}
|
||||
{{- with .FeedURL }}
|
||||
<link rel="alternate" type="application/rss+xml" href="{{ . }}" title="{{ $.FeedTitle | default $.Title | html }}" />
|
||||
{{ end }}
|
||||
|
||||
{{- range $name, $content := .MetaData }}
|
||||
<meta name="{{ $name | html }}" content="{{ $content | html }}" />
|
||||
{{ end }}
|
||||
{{ printf "<%s" "!--" }} -->
|
||||
<title>{{ .Title | html }}</title>
|
||||
</head>
|
||||
<body class="text-base w-full min-h-svh">
|
||||
<div id="root" class="relative w-full min-h-full"></div>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue