feat(middleware): add support for markdown content negotiation and rendering
- Implemented content negotiation for markdown files based on URL suffix and Accept header. - Introduced a new RenderRaw method to serve raw markdown content, bypassing HTML rendering. - Updated PageConfig to include a handler for markdown output, enhancing flexibility in content delivery.
This commit is contained in:
parent
99ce3ea456
commit
413fd71841
4 changed files with 157 additions and 9 deletions
|
|
@ -63,6 +63,14 @@ func withStaticFileServer(c *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Content negotiation: .md suffix or Accept: text/markdown
|
||||||
|
if strings.HasSuffix(c.Request.URL.Path, ".md") {
|
||||||
|
c.Set("content_type", "markdown")
|
||||||
|
c.Request.URL.Path = strings.TrimSuffix(c.Request.URL.Path, ".md")
|
||||||
|
} else if strings.Contains(c.GetHeader("Accept"), "text/markdown") {
|
||||||
|
c.Set("content_type", "markdown")
|
||||||
|
}
|
||||||
|
|
||||||
// Rewrite
|
// Rewrite
|
||||||
for _, rewrite := range rewriteRules {
|
for _, rewrite := range rewriteRules {
|
||||||
// log.Debug("Rewrite: %s => %s", c.Request.URL.Path, rewrite.Replacement)
|
// log.Debug("Rewrite: %s => %s", c.Request.URL.Path, rewrite.Replacement)
|
||||||
|
|
@ -90,6 +98,18 @@ func withStaticFileServer(c *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Content negotiation: serve raw markdown instead of HTML
|
||||||
|
if ct, exists := c.Get("content_type"); exists && ct == "markdown" {
|
||||||
|
raw, contentType, code, err := r.RenderRaw("markdown")
|
||||||
|
if err != nil {
|
||||||
|
c.AbortWithStatusJSON(code, gin.H{"code": code, "message": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.Data(code, contentType, []byte(raw))
|
||||||
|
c.Done()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
html, code, err := r.Render()
|
html, code, err := r.Render()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if code == 301 || code == 302 {
|
if code == 301 || code == 302 {
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ import (
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
jsoniter "github.com/json-iterator/go"
|
jsoniter "github.com/json-iterator/go"
|
||||||
"github.com/yaoapp/gou/application"
|
"github.com/yaoapp/gou/application"
|
||||||
|
"github.com/yaoapp/gou/process"
|
||||||
"github.com/yaoapp/kun/exception"
|
"github.com/yaoapp/kun/exception"
|
||||||
"github.com/yaoapp/kun/log"
|
"github.com/yaoapp/kun/log"
|
||||||
"github.com/yaoapp/yao/sui/core"
|
"github.com/yaoapp/yao/sui/core"
|
||||||
|
|
@ -194,6 +195,122 @@ func (r *Request) Render() (string, int, error) {
|
||||||
return html, 200, nil
|
return html, 200, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RenderRaw serves alternative content types (e.g. markdown) by invoking the
|
||||||
|
// handler declared in the page config, skipping data scripts and HTML rendering.
|
||||||
|
// "method" calls the page's backend.ts function; "process" calls a global Yao process.
|
||||||
|
func (r *Request) RenderRaw(kind string) (string, string, int, error) {
|
||||||
|
|
||||||
|
// Load or build cache (same as Render)
|
||||||
|
var c *core.Cache = nil
|
||||||
|
if !r.Request.DisableCache() {
|
||||||
|
c = core.GetCache(r.File)
|
||||||
|
}
|
||||||
|
if c == nil {
|
||||||
|
var status int
|
||||||
|
var err error
|
||||||
|
c, status, err = r.MakeCache()
|
||||||
|
if err != nil {
|
||||||
|
return "", "", status, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Guard
|
||||||
|
code, err := r.Guard(c)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", code, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse config to find the handler
|
||||||
|
if c.Config == "" {
|
||||||
|
return "", "", 404, fmt.Errorf("page does not support %s output", kind)
|
||||||
|
}
|
||||||
|
|
||||||
|
var conf core.PageConfig
|
||||||
|
if err := jsoniter.UnmarshalFromString(c.Config, &conf); err != nil {
|
||||||
|
return "", "", 500, fmt.Errorf("config parse error: %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve the handler from config by kind
|
||||||
|
var handler *core.PageProcess
|
||||||
|
switch kind {
|
||||||
|
case "markdown":
|
||||||
|
handler = conf.Markdown
|
||||||
|
}
|
||||||
|
|
||||||
|
if handler == nil || (handler.Method == "" && handler.Process == "") {
|
||||||
|
return "", "", 404, fmt.Errorf("page does not support %s output", kind)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve arguments
|
||||||
|
args := make([]interface{}, len(handler.In))
|
||||||
|
for i, expr := range handler.In {
|
||||||
|
args[i] = r.resolveArg(expr)
|
||||||
|
}
|
||||||
|
|
||||||
|
var result interface{}
|
||||||
|
|
||||||
|
if handler.Method != "" {
|
||||||
|
// Call backend.ts function via the page's compiled script
|
||||||
|
if c.Script == nil {
|
||||||
|
return "", "", 500, fmt.Errorf("page has no backend script")
|
||||||
|
}
|
||||||
|
r.Request.Script = c.Script
|
||||||
|
result, err = c.Script.Call(r.Request, handler.Method, args...)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", 500, fmt.Errorf("backend script error: %s", err.Error())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Fallback: call a global Yao process
|
||||||
|
p, err := process.Of(handler.Process, args...)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", 500, fmt.Errorf("process error: %s", err.Error())
|
||||||
|
}
|
||||||
|
result, err = p.Exec()
|
||||||
|
if err != nil {
|
||||||
|
return "", "", 500, fmt.Errorf("process exec error: %s", err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
content := ""
|
||||||
|
switch v := result.(type) {
|
||||||
|
case string:
|
||||||
|
content = v
|
||||||
|
case []byte:
|
||||||
|
content = string(v)
|
||||||
|
default:
|
||||||
|
return "", "", 500, fmt.Errorf("handler must return string, got %T", result)
|
||||||
|
}
|
||||||
|
|
||||||
|
contentType := "text/markdown; charset=utf-8"
|
||||||
|
return content, contentType, 200, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// resolveArg replaces $param.*, $query.* placeholders with actual request values.
|
||||||
|
func (r *Request) resolveArg(expr string) interface{} {
|
||||||
|
if strings.HasPrefix(expr, "$param.") {
|
||||||
|
key := expr[7:]
|
||||||
|
if val, ok := r.Request.Params[key]; ok {
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(expr, "$query.") {
|
||||||
|
key := expr[7:]
|
||||||
|
if r.Request.Query.Has(key) {
|
||||||
|
return r.Request.Query.Get(key)
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(expr, "$header.") {
|
||||||
|
key := expr[8:]
|
||||||
|
if r.Request.Headers.Has(key) {
|
||||||
|
return r.Request.Headers.Get(key)
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return expr
|
||||||
|
}
|
||||||
|
|
||||||
// MakeCache is the cache for the page API.
|
// MakeCache is the cache for the page API.
|
||||||
func (r *Request) MakeCache() (*core.Cache, int, error) {
|
func (r *Request) MakeCache() (*core.Cache, int, error) {
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -72,6 +72,7 @@ func (page *Page) ExportConfig() string {
|
||||||
"dataCache": page.Config.DataCache,
|
"dataCache": page.Config.DataCache,
|
||||||
"api": page.Config.API,
|
"api": page.Config.API,
|
||||||
"root": page.Root,
|
"root": page.Root,
|
||||||
|
"markdown": page.Config.Markdown,
|
||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
||||||
|
|
@ -375,15 +375,25 @@ type PageConfig struct {
|
||||||
|
|
||||||
// PageSetting is the struct for the page setting
|
// PageSetting is the struct for the page setting
|
||||||
type PageSetting struct {
|
type PageSetting struct {
|
||||||
Title string `json:"title,omitempty"`
|
Title string `json:"title,omitempty"`
|
||||||
Guard string `json:"guard,omitempty"`
|
Guard string `json:"guard,omitempty"`
|
||||||
CacheStore string `json:"cacheStore,omitempty"`
|
CacheStore string `json:"cacheStore,omitempty"`
|
||||||
Cache int `json:"cache,omitempty"`
|
Cache int `json:"cache,omitempty"`
|
||||||
Root string `json:"root,omitempty"`
|
Root string `json:"root,omitempty"`
|
||||||
DataCache int `json:"dataCache,omitempty"`
|
DataCache int `json:"dataCache,omitempty"`
|
||||||
Description string `json:"description,omitempty"`
|
Description string `json:"description,omitempty"`
|
||||||
SEO *PageSEO `json:"seo,omitempty"`
|
SEO *PageSEO `json:"seo,omitempty"`
|
||||||
API *PageAPI `json:"api,omitempty"`
|
API *PageAPI `json:"api,omitempty"`
|
||||||
|
Markdown *PageProcess `json:"markdown,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// PageProcess binds a handler to a content negotiation output.
|
||||||
|
// "method" calls the page's own backend.ts function (preferred);
|
||||||
|
// "process" calls a global Yao process as fallback.
|
||||||
|
type PageProcess struct {
|
||||||
|
Method string `json:"method,omitempty"`
|
||||||
|
Process string `json:"process,omitempty"`
|
||||||
|
In []string `json:"in,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// PageConfigRendered is the struct for the page config rendered
|
// PageConfigRendered is the struct for the page config rendered
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue