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:
Max 2026-04-02 18:55:36 +08:00
parent 99ce3ea456
commit 413fd71841
4 changed files with 157 additions and 9 deletions

View file

@ -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 {

View file

@ -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) {

View file

@ -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 {

View file

@ -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