renderx

package
v0.7.3 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 16, 2026 License: MIT Imports: 31 Imported by: 0

Documentation

Index

Constants

View Source
const (
	DefaultMemPages = 1024
	DefaultTTL      = time.Hour
)
View Source
const (
	DefaultMemoryLimit = 512 << 20

	// WasmShadowStackSize is the physical C shadow stack baked into the
	// bundled quickjs.wasm at build time (wasm-ld -z stack-size=8MiB; see
	// internal/qjs/csrc/README.md). Exhausting it traps the wasm module with
	// an uncatchable "out of bounds memory access" and leaves the instance
	// unusable — QuickJS's own catchable guard must trip first, which is why
	// MaxStackSize is validated against this ceiling minus headroom.
	WasmShadowStackSize = 8 << 20

	// MaxLogicalStackSize is the largest accepted PoolOptions.MaxStackSize:
	// the shadow stack minus a 1MiB headroom, so a guard trip on the deepest
	// legal budget still leaves real stack below it and the physical wall
	// stays unreachable by recursion.
	MaxLogicalStackSize = WasmShadowStackSize - 1<<20

	// DefaultMaxStackSize is the QuickJS execution-stack cap applied when
	// PoolOptions.MaxStackSize is unset. QuickJS checks it on every nested
	// call and raises a CATCHABLE "Maximum call stack size exceeded" that
	// leaves the instance healthy (measured capacities: ~3.1k frames of
	// simple recursion at this budget, ~8k at MaxLogicalStackSize). History:
	// the pre-v0.17.0 engine silently disabled this guard under wasi, so the
	// only visible wall was the binary's 64KiB default shadow stack — the
	// original #15/#16 "phantom depth ceiling". Rebuilt wasm (csrc/, pinned
	// quickjs-ng v0.17.0) restores the guard; see ADR 0015.
	DefaultMaxStackSize = 1 << 20

	DefaultRenderTimeout = 3 * time.Second
)
View Source
const (
	DefaultCacheControl = "max-age=60, stale-while-revalidate=300"

	DefaultPoisonTTL = 10 * time.Minute
)
View Source
const (
	TierEmbed   = "embed"
	TierCache   = "cache"
	TierRender  = "render"
	TierStale   = "stale"
	TierSPA     = "spa"
	TierAsset   = "asset"
	TierError   = "error"
	TierRobots  = "robots"
	TierSitemap = "sitemap"
)

Tier values, most-specific-first in the serving order: embedded prerender artifact, layered-cache hit, on-the-spot render (miss path), stale copy on render failure, SPA shell fallback (index.html routes incl. the 404 form), static asset passthrough, product error page. The SEO companions carry their own values so traffic middleware can count crawler fetches of them.

View Source
const DefaultIndexNowEndpoint = "https://api.indexnow.org/indexnow"
View Source
const DefaultPrelude = `` /* 5289-byte string literal not displayed */
View Source
const HeaderTier = "X-Renderx-Tier"

HeaderTier names the response header carrying which serving layer produced the response — one of the Tier* values below. It exists for traffic observation (metrics.TrafficConfig consumes it: "did the crawler get cache or SSR?") and for debugging ("why was this request rendered?"). Readers must keep in mind that a CDN caching the HTML replays the header of the origin response that first populated the cache.

View Source
const MaxPropsBytes = 512 << 10
View Source
const OutputVersion = "rv1"

Variables

View Source
var (
	ErrRenderTimeout = errors.New("renderx: render timeout")

	ErrPoolClosed = errors.New("renderx: engine pool closed")
)
View Source
var (
	ErrEmptyRenderOutput = errors.New("renderx: empty render output")

	ErrMissingTitle = errors.New("renderx: head directives emitted but no <title>")
)
View Source
var (
	ErrPatternNotFound = errors.New("renderx: pattern not registered")

	ErrPatternMismatch = errors.New("renderx: registered pattern not in route table")

	ErrMissingPaths = errors.New("renderx: static param route missing Paths")

	ErrInvalidParam = errors.New("renderx: invalid path param")

	ErrInvalidDirective = errors.New("renderx: invalid directive payload")

	ErrInvalidHeadTag = errors.New("renderx: invalid head tag or attr name")

	ErrInvalidCacheKey = errors.New("renderx: invalid cache key")

	ErrBuildIDMismatch = errors.New("renderx: cache entry buildId mismatch")

	ErrPropsTooLarge = errors.New("renderx: props exceed size limit")

	ErrPageNotFound = errors.New("renderx: page not found")
)

Functions

func AffectedPatterns

func AffectedPatterns(pattern string) []string

func BuildSSRBundle

func BuildSSRBundle(opts BuildOptions) ([]byte, error)

func DefaultPoolSize

func DefaultPoolSize() int

func DefaultSSRAliases

func DefaultSSRAliases(webDir, packagesDir string) map[string]string

func ExpandPath

func ExpandPath(pattern string, params Params) (string, bool)

func Handler

func Handler(deps HandlerDeps) http.Handler

func IndexNow

func IndexNow(ctx context.Context, deps IndexNowDeps, urls []string) error

func Invalidate

func Invalidate(ctx context.Context, pattern string, params Params) error

func PropsSHA256

func PropsSHA256(propsJSON []byte) string

func Reconcile

func Reconcile(routes []RouteSpec) error

func ReconcileEngine

func ReconcileEngine(ctx context.Context, r Renderer) error

func Refresh

func Refresh(ctx context.Context, pattern string, params Params) error

func RegisterDep

func RegisterDep(pagePattern, dependsOnPattern string)

func RegisterLoader

func RegisterLoader(pattern string, fn LoaderFn)

func RegisterPaths

func RegisterPaths(pattern string, fn PathsFn)

func RegisterService

func RegisterService(s *Service)

func RenderDocument

func RenderDocument(doc Document) ([]byte, error)

func ResetForTest

func ResetForTest()

func RobotsHandler added in v0.7.2

func RobotsHandler(opts RobotsOptions) http.Handler

RobotsHandler serves /robots.txt: one "User-agent: *" group with the configured Disallow prefixes, the extra verbatim lines, and the Sitemap pointer. Options are validated and serialized once at construction — a malformed option panics at wiring time, never serving a silently wrong file.

func SitemapHandler

func SitemapHandler(routes []RouteSpec, baseURL string, opts SitemapOptions) http.Handler

SitemapHandler serves /sitemap.xml from the route table + Paths registry. baseURL must be an absolute http(s) origin (e.g. "https://example.com") — a malformed value panics at construction time; mount the route only when an origin is actually known (see example wiring for the env-driven pattern).

func ValidParamValue

func ValidParamValue(v string) bool

func ValidateParams

func ValidateParams(p Params) error

func ViteBuildID

func ViteBuildID(data []byte) string

Types

type BuildOptions

type BuildOptions struct {
	EntryPoints []string

	AbsWorkingDir string

	NodePaths []string

	Alias map[string]string
}

type CacheConfig

type CacheConfig struct {
	BuildID  string
	MemPages int
	TTL      time.Duration
	Disk     *storagex.LocalBackend
}

type CacheEntry

type CacheEntry struct {
	HTML        []byte    `json:"html"`
	PropsSHA256 string    `json:"props_sha256"`
	BuiltAt     time.Time `json:"built_at"`
	BuildID     string    `json:"build_id"`
}

func RenderOnce added in v0.4.0

func RenderOnce(ctx context.Context, r Renderer, pageID, location string, props json.RawMessage, lang string, assets ViteAssets) (CacheEntry, error)

RenderOnce 把单个页面渲染成完整 HTML 文档——不经过缓存与 HTTP 层的 一次性渲染入口,预渲染 / handler 未命中 / 缓存回填共用此组装。 预览(草稿渲染)、邮件合成、调试等场景直接调用即可,语义与预渲染管线 完全一致(props 规范化、空输出校验、head 指令与 <title> 校验)。

type DevHandler

type DevHandler struct {
	// contains filtered or unexported fields
}

func NewDevHandler

func NewDevHandler(deps DevHandlerDeps) (*DevHandler, error)

func (*DevHandler) Close

func (d *DevHandler) Close()

func (*DevHandler) ServeHTTP

func (d *DevHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)

type DevHandlerDeps

type DevHandlerDeps struct {
	BuildOpts BuildOptions

	PoolOpts PoolOptions

	NewRenderer func(bundle []byte, opts PoolOptions) (Renderer, error)

	Cache *LayeredCache

	Routes []RouteSpec

	BuildID string

	Assets ViteAssets
	Lang   string

	SPA http.Handler

	Registry *Registry

	Interval time.Duration

	Warn func(format string, args ...any)
}

type Directive

type Directive struct {
	Kind    string          `json:"kind"`
	Payload json.RawMessage `json:"payload,omitempty"`
	Seq     int             `json:"seq"`
}

type DirectiveSink

type DirectiveSink struct {
	// contains filtered or unexported fields
}

func NewSink

func NewSink() *DirectiveSink

func (*DirectiveSink) Append

func (s *DirectiveSink) Append(kind string, payload json.RawMessage) Directive

func (*DirectiveSink) Directives

func (s *DirectiveSink) Directives() []Directive

func (*DirectiveSink) Truncated

func (s *DirectiveSink) Truncated() bool

type Document

type Document struct {
	Lang          string
	Head          []HeadEntry
	CSS           []string
	BodyHTML      string
	PropsJSON     []byte
	PageID        string
	HydrateScript string
}

type EnginePool

type EnginePool struct {
	// contains filtered or unexported fields
}

func NewEnginePool

func NewEnginePool(bundle []byte, opts PoolOptions) (*EnginePool, error)

func (*EnginePool) Close

func (p *EnginePool) Close()

func (*EnginePool) Render

func (p *EnginePool) Render(ctx context.Context, pageID, location string, props json.RawMessage) (RenderResult, error)

func (*EnginePool) Routes

func (p *EnginePool) Routes(ctx context.Context) ([]RouteSpec, error)

type HandlerDeps

type HandlerDeps struct {
	PrerenderFS fs.FS

	SPA http.Handler

	Cache *LayeredCache

	Renderer Renderer

	Routes []RouteSpec

	BuildID string

	CacheControl string

	ErrorPage func(http.ResponseWriter, *http.Request, error)

	Warn func(format string, args ...any)

	Assets ViteAssets

	Lang string

	Registry *Registry

	PoisonTTL time.Duration
}

type HeadEntry

type HeadEntry struct {
	Tag      string            `json:"tag"`
	Attrs    map[string]string `json:"attrs,omitempty"`
	Children string            `json:"children,omitempty"`
}

func HeadEntries

func HeadEntries(dirs []Directive) ([]HeadEntry, error)

type IndexNowDeps

type IndexNowDeps struct {
	Endpoint string

	Key string

	Host string

	Client *http.Client
}

type LayeredCache

type LayeredCache struct {
	// contains filtered or unexported fields
}

func NewLayeredCache

func NewLayeredCache(cfg CacheConfig) (*LayeredCache, error)

func (*LayeredCache) Delete

func (c *LayeredCache) Delete(ctx context.Context, paths ...string) error

func (*LayeredCache) EvictMatching

func (c *LayeredCache) EvictMatching(ctx context.Context, match func(key string) bool) (int, error)

func (*LayeredCache) Get

func (c *LayeredCache) Get(ctx context.Context, path string) (CacheEntry, bool)

func (*LayeredCache) GetOrLoad

func (c *LayeredCache) GetOrLoad(ctx context.Context, path string, load func(context.Context) (CacheEntry, error)) (CacheEntry, error)

func (*LayeredCache) GetOrLoadHit added in v0.7.2

func (c *LayeredCache) GetOrLoadHit(ctx context.Context, path string, load func(context.Context) (CacheEntry, error)) (CacheEntry, bool, error)

GetOrLoadHit is GetOrLoad plus a hit report: hit=true when an existing cache entry (memory or disk) was served — including the singleflight replay path, where a concurrent Put landed after this request's first check and the load never ran. hit=false when the request was answered through the load path (this request's own load, or a coincident one it joined as a singleflight waiter). The X-Renderx-Tier response header is built on it.

func (*LayeredCache) GetStale

func (c *LayeredCache) GetStale(ctx context.Context, path string) (CacheEntry, bool)

func (*LayeredCache) Keys

func (c *LayeredCache) Keys() []string

func (*LayeredCache) PurgeForeign

func (c *LayeredCache) PurgeForeign(ctx context.Context, buildID string) error

func (*LayeredCache) Put

func (c *LayeredCache) Put(ctx context.Context, path string, ent CacheEntry) error

type LoaderFn

type LoaderFn func(ctx context.Context, p Params) (any, error)

type Mode

type Mode string
const (
	ModeStatic Mode = "static"

	ModeCSR Mode = "csr"
)

type PagePath

type PagePath struct {
	Params map[string]string `json:"params,omitempty"`
}

type Params

type Params map[string]string

func Match

func Match(pattern, path string) (Params, bool)

type PathsFn

type PathsFn func(ctx context.Context) ([]PagePath, error)

type PoolOptions

type PoolOptions struct {
	Size int

	MemoryLimit int

	// MaxStackSize caps the QuickJS execution stack in bytes. QuickJS's
	// guard trips BEFORE the wasm module's physical shadow stack and raises
	// a catchable "Maximum call stack size exceeded" — the instance stays
	// healthy — so this knob trades depth capacity against per-render stack
	// budget. Values above MaxLogicalStackSize are rejected at construction:
	// beyond it recursion could reach the physical wall (uncatchable trap,
	// instance retired). 0 keeps DefaultMaxStackSize (1MiB).
	MaxStackSize int

	RenderTimeout time.Duration

	Prelude string
}

type PrerenderDeps

type PrerenderDeps struct {
	Renderer Renderer

	OutDir string

	Routes []RouteSpec

	Warm []string

	BuildID string

	ViteAssets ViteAssets

	Lang string

	Concurrency int

	Registry *Registry
}

type PrerenderManifest

type PrerenderManifest struct {
	BuildID     string                   `json:"buildId"`
	GeneratedAt time.Time                `json:"generatedAt"`
	Pages       []PrerenderManifestEntry `json:"pages"`
}

func Prerender

func Prerender(ctx context.Context, deps PrerenderDeps) (PrerenderManifest, error)

type PrerenderManifestEntry

type PrerenderManifestEntry struct {
	Path        string    `json:"path"`
	PageID      string    `json:"pageId"`
	PropsSHA256 string    `json:"propsSha256"`
	BuildID     string    `json:"buildId"`
	BuiltAt     time.Time `json:"builtAt"`
	File        string    `json:"file"`
}

type Registry

type Registry struct {
	// contains filtered or unexported fields
}

func NewRegistry

func NewRegistry() *Registry

func (*Registry) AffectedPatterns

func (r *Registry) AffectedPatterns(pattern string) []string

func (*Registry) LookupLoader

func (r *Registry) LookupLoader(pattern string) (LoaderFn, error)

func (*Registry) LookupPaths

func (r *Registry) LookupPaths(pattern string) (PathsFn, error)

func (*Registry) Reconcile

func (r *Registry) Reconcile(routes []RouteSpec) error

func (*Registry) RegisterDep

func (r *Registry) RegisterDep(pagePattern, dependsOnPattern string)

func (*Registry) RegisterLoader

func (r *Registry) RegisterLoader(pattern string, fn LoaderFn)

func (*Registry) RegisterPaths

func (r *Registry) RegisterPaths(pattern string, fn PathsFn)

type RenderResult

type RenderResult struct {
	HTML string
	Sink *DirectiveSink
}

type Renderer

type Renderer interface {
	Routes(ctx context.Context) ([]RouteSpec, error)

	Render(ctx context.Context, pageID, location string, props json.RawMessage) (RenderResult, error)
}

type RobotsOptions added in v0.7.2

type RobotsOptions struct {
	// SitemapURL is emitted as a "Sitemap:" line when non-empty (pass an
	// absolute URL, e.g. "https://example.com/sitemap.xml").
	SitemapURL string

	// Disallow lists path prefixes emitted as "Disallow:" lines under the
	// default "User-agent: *" group (e.g. "/api/", "/admin").
	Disallow []string

	// Extra holds verbatim lines appended after the default block, for more
	// elaborate rules (further user-agent groups, Allow exceptions). Blank
	// lines are skipped.
	Extra []string
}

RobotsOptions configures RobotsHandler.

type RouteSpec

type RouteSpec struct {
	Path   string `json:"path"`
	PageID string `json:"pageId"`
	Render Mode   `json:"render"`
}

type SEOSignals added in v0.7.2

type SEOSignals struct {
	Title       string
	Description string
	Canonical   string

	// CanonicalCount counts link[rel=canonical] elements (>1 = the page emits
	// duplicates). The SSR path does not dedupe head entries — one page, one
	// canonical is a product-side rule, and build-time audits warn on it.
	CanonicalCount int
}

SEOSignals is the head-level SEO signal set AuditSEO extracts from a rendered document. Empty strings mean the signal is absent.

func AuditSEO added in v0.7.2

func AuditSEO(doc []byte) SEOSignals

AuditSEO extracts the title / meta description / canonical link from a rendered document. It understands the serialization this package's template emits (lowercase tags, double-quoted attributes) — it audits our own output, not arbitrary HTML. Values come back HTML-unescaped. Build chains use it to report SEO coverage of prerendered pages (cmd/render does).

type Service

type Service struct {
	Cache    *LayeredCache
	Renderer Renderer
	Routes   []RouteSpec
	BuildID  string
	Assets   ViteAssets
	Lang     string
	Registry *Registry

	Warn func(format string, args ...any)

	RefreshTimeout time.Duration
}

func (*Service) Invalidate

func (s *Service) Invalidate(ctx context.Context, pattern string, params Params) error

func (*Service) Refresh

func (s *Service) Refresh(ctx context.Context, pattern string, params Params) error

type SitemapOptions

type SitemapOptions struct {
	Filter func(route RouteSpec, location string, params Params) bool

	Registry *Registry
}

type ViteAssets

type ViteAssets struct {
	CSS []string
	JS  []string
}

func ParseViteManifest

func ParseViteManifest(data []byte, entryKey string) (ViteAssets, error)

Directories

Path Synopsis
internal
qjs

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL