feat(abi): add keyed page and render response contracts

This commit is contained in:
Alex Dunmow 2026-08-19 21:50:28 +08:00
parent 301f286c2f
commit fb58679b6f
22 changed files with 890 additions and 233 deletions

View File

@ -770,6 +770,7 @@ message ProvisionerEnsureSettingResponse {}
// PageSeed mirrors plugin.PageConfig. // PageSeed mirrors plugin.PageConfig.
message PageSeed { message PageSeed {
string slug = 1; string slug = 1;
// Legacy slug-based parent reference. Prefer parent_key for new page seeds.
string parent_slug = 2; string parent_slug = 2;
string title = 3; string title = 3;
string template_key = 4; string template_key = 4;
@ -779,6 +780,16 @@ message PageSeed {
string detail_slug_field = 8; string detail_slug_field = 8;
bool reconcile_blocks = 9; bool reconcile_blocks = 9;
bool reconcile_template = 10; bool reconcile_template = 10;
// Stable identity scoped to the declaring plugin.
string key = 11;
// Stable identity of a parent page declared by the same plugin.
string parent_key = 12;
// Applied only when the page is created; later reconciliation does not move
// an existing page when this value changes.
string mount_path = 13;
// Shallowly add absent top-level defaults to matching plugin-owned block
// assignments while preserving all present values, including empty values.
bool merge_missing_block_content = 14;
} }
message ProvisionerEnsurePageRequest { message ProvisionerEnsurePageRequest {
@ -802,6 +813,10 @@ message ProvisionerEnsureMenuItemRequest {
string url = 3; string url = 3;
string page_slug = 4; string page_slug = 4;
int32 sort_order = 5; int32 sort_order = 5;
// Stable plugin/page-key target. When set, these identify the provisioned
// page independently of its current slug.
string page_plugin = 6;
string page_key = 7;
} }
message ProvisionerEnsureMenuItemResponse {} message ProvisionerEnsureMenuItemResponse {}

View File

@ -44,6 +44,23 @@ message PoweredBlock {
string template = 1; string template = 1;
// JSON encoding of the template data map (map[string]any). // JSON encoding of the template data map (map[string]any).
bytes data_json = 2; bytes data_json = 2;
// Requested HTTP response status. Before writing headers, the host accepts
// only 200599, uses the highest status class, and keeps the first result
// encountered within that class.
int32 status_code = 3;
// Structured document metadata merged by the host into the page head. The
// first non-empty value for each field wins in deterministic render order.
DocumentMetadata metadata = 4;
}
// DocumentMetadata is the safe, shared subset of document head data a plugin
// may provide. The host validates and escapes every value before rendering.
message DocumentMetadata {
string title = 1;
string description = 2;
string canonical_url = 3;
string image_url = 4;
string robots = 5;
} }
// RenderTemplateRequest renders one template (templates.TemplateFunc). // RenderTemplateRequest renders one template (templates.TemplateFunc).
@ -76,6 +93,11 @@ message TemplateDocument {
// Extra <html>-element attributes (e.g. data-theme). A "lang" key here is // Extra <html>-element attributes (e.g. data-theme). A "lang" key here is
// ignored the lang field wins. // ignored the lang field wins.
map<string, string> html_attrs = 6; map<string, string> html_attrs = 6;
// Requested HTTP response status. The host applies the same validation and
// precedence rules as PoweredBlock.status_code.
int32 status_code = 7;
// Structured document metadata merged by the host into the page head.
DocumentMetadata metadata = 8;
} }
// RenderTagRequest invokes a plugin-declared template tag (HOOK_RENDER_TAG). // RenderTagRequest invokes a plugin-declared template tag (HOOK_RENDER_TAG).
@ -181,6 +203,10 @@ message PageContext {
string title = 3; string title = 3;
string post_type = 4; // "page", "post", "master", "system" string post_type = 4; // "page", "post", "master", "system"
string status = 5; // "published", "draft", "scheduled" string status = 5; // "published", "draft", "scheduled"
// Slug derived from the resolved page hierarchy.
string derived_slug = 6;
// Effective mount path after host inheritance and normalization.
string mount_path = 7;
} }
// PostContext mirrors blocks.PostContext. // PostContext mirrors blocks.PostContext.
@ -276,4 +302,8 @@ message BlockContext {
bytes detail_row_data_json = 36; bytes detail_row_data_json = 36;
string blog_index_url = 37; string blog_index_url = 37;
string category_page_url = 38; string category_page_url = 38;
// Slug derived from the resolved page hierarchy.
string derived_slug = 39;
// Effective mount path after host inheritance and normalization.
string mount_path = 40;
} }

View File

@ -6668,19 +6668,30 @@ func (*ProvisionerEnsureSettingResponse) Descriptor() ([]byte, []int) {
// PageSeed mirrors plugin.PageConfig. // PageSeed mirrors plugin.PageConfig.
type PageSeed struct { type PageSeed struct {
state protoimpl.MessageState `protogen:"open.v1"` state protoimpl.MessageState `protogen:"open.v1"`
Slug string `protobuf:"bytes,1,opt,name=slug,proto3" json:"slug,omitempty"` Slug string `protobuf:"bytes,1,opt,name=slug,proto3" json:"slug,omitempty"`
ParentSlug string `protobuf:"bytes,2,opt,name=parent_slug,json=parentSlug,proto3" json:"parent_slug,omitempty"` // Legacy slug-based parent reference. Prefer parent_key for new page seeds.
Title string `protobuf:"bytes,3,opt,name=title,proto3" json:"title,omitempty"` ParentSlug string `protobuf:"bytes,2,opt,name=parent_slug,json=parentSlug,proto3" json:"parent_slug,omitempty"`
TemplateKey string `protobuf:"bytes,4,opt,name=template_key,json=templateKey,proto3" json:"template_key,omitempty"` Title string `protobuf:"bytes,3,opt,name=title,proto3" json:"title,omitempty"`
Blocks []*PageBlock `protobuf:"bytes,5,rep,name=blocks,proto3" json:"blocks,omitempty"` TemplateKey string `protobuf:"bytes,4,opt,name=template_key,json=templateKey,proto3" json:"template_key,omitempty"`
DetailSourceType string `protobuf:"bytes,6,opt,name=detail_source_type,json=detailSourceType,proto3" json:"detail_source_type,omitempty"` Blocks []*PageBlock `protobuf:"bytes,5,rep,name=blocks,proto3" json:"blocks,omitempty"`
DetailSourceKey string `protobuf:"bytes,7,opt,name=detail_source_key,json=detailSourceKey,proto3" json:"detail_source_key,omitempty"` DetailSourceType string `protobuf:"bytes,6,opt,name=detail_source_type,json=detailSourceType,proto3" json:"detail_source_type,omitempty"`
DetailSlugField string `protobuf:"bytes,8,opt,name=detail_slug_field,json=detailSlugField,proto3" json:"detail_slug_field,omitempty"` DetailSourceKey string `protobuf:"bytes,7,opt,name=detail_source_key,json=detailSourceKey,proto3" json:"detail_source_key,omitempty"`
ReconcileBlocks bool `protobuf:"varint,9,opt,name=reconcile_blocks,json=reconcileBlocks,proto3" json:"reconcile_blocks,omitempty"` DetailSlugField string `protobuf:"bytes,8,opt,name=detail_slug_field,json=detailSlugField,proto3" json:"detail_slug_field,omitempty"`
ReconcileTemplate bool `protobuf:"varint,10,opt,name=reconcile_template,json=reconcileTemplate,proto3" json:"reconcile_template,omitempty"` ReconcileBlocks bool `protobuf:"varint,9,opt,name=reconcile_blocks,json=reconcileBlocks,proto3" json:"reconcile_blocks,omitempty"`
unknownFields protoimpl.UnknownFields ReconcileTemplate bool `protobuf:"varint,10,opt,name=reconcile_template,json=reconcileTemplate,proto3" json:"reconcile_template,omitempty"`
sizeCache protoimpl.SizeCache // Stable identity scoped to the declaring plugin.
Key string `protobuf:"bytes,11,opt,name=key,proto3" json:"key,omitempty"`
// Stable identity of a parent page declared by the same plugin.
ParentKey string `protobuf:"bytes,12,opt,name=parent_key,json=parentKey,proto3" json:"parent_key,omitempty"`
// Applied only when the page is created; later reconciliation does not move
// an existing page when this value changes.
MountPath string `protobuf:"bytes,13,opt,name=mount_path,json=mountPath,proto3" json:"mount_path,omitempty"`
// Shallowly add absent top-level defaults to matching plugin-owned block
// assignments while preserving all present values, including empty values.
MergeMissingBlockContent bool `protobuf:"varint,14,opt,name=merge_missing_block_content,json=mergeMissingBlockContent,proto3" json:"merge_missing_block_content,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
} }
func (x *PageSeed) Reset() { func (x *PageSeed) Reset() {
@ -6783,6 +6794,34 @@ func (x *PageSeed) GetReconcileTemplate() bool {
return false return false
} }
func (x *PageSeed) GetKey() string {
if x != nil {
return x.Key
}
return ""
}
func (x *PageSeed) GetParentKey() string {
if x != nil {
return x.ParentKey
}
return ""
}
func (x *PageSeed) GetMountPath() string {
if x != nil {
return x.MountPath
}
return ""
}
func (x *PageSeed) GetMergeMissingBlockContent() bool {
if x != nil {
return x.MergeMissingBlockContent
}
return false
}
type ProvisionerEnsurePageRequest struct { type ProvisionerEnsurePageRequest struct {
state protoimpl.MessageState `protogen:"open.v1"` state protoimpl.MessageState `protogen:"open.v1"`
Page *PageSeed `protobuf:"bytes,1,opt,name=page,proto3" json:"page,omitempty"` Page *PageSeed `protobuf:"bytes,1,opt,name=page,proto3" json:"page,omitempty"`
@ -6947,12 +6986,16 @@ func (*ProvisionerOverrideSiteSettingsResponse) Descriptor() ([]byte, []int) {
// ProvisionerEnsureMenuItemRequest mirrors plugin.MenuItemConfig under a // ProvisionerEnsureMenuItemRequest mirrors plugin.MenuItemConfig under a
// named menu. // named menu.
type ProvisionerEnsureMenuItemRequest struct { type ProvisionerEnsureMenuItemRequest struct {
state protoimpl.MessageState `protogen:"open.v1"` state protoimpl.MessageState `protogen:"open.v1"`
MenuName string `protobuf:"bytes,1,opt,name=menu_name,json=menuName,proto3" json:"menu_name,omitempty"` MenuName string `protobuf:"bytes,1,opt,name=menu_name,json=menuName,proto3" json:"menu_name,omitempty"`
Label string `protobuf:"bytes,2,opt,name=label,proto3" json:"label,omitempty"` Label string `protobuf:"bytes,2,opt,name=label,proto3" json:"label,omitempty"`
Url string `protobuf:"bytes,3,opt,name=url,proto3" json:"url,omitempty"` Url string `protobuf:"bytes,3,opt,name=url,proto3" json:"url,omitempty"`
PageSlug string `protobuf:"bytes,4,opt,name=page_slug,json=pageSlug,proto3" json:"page_slug,omitempty"` PageSlug string `protobuf:"bytes,4,opt,name=page_slug,json=pageSlug,proto3" json:"page_slug,omitempty"`
SortOrder int32 `protobuf:"varint,5,opt,name=sort_order,json=sortOrder,proto3" json:"sort_order,omitempty"` SortOrder int32 `protobuf:"varint,5,opt,name=sort_order,json=sortOrder,proto3" json:"sort_order,omitempty"`
// Stable plugin/page-key target. When set, these identify the provisioned
// page independently of its current slug.
PagePlugin string `protobuf:"bytes,6,opt,name=page_plugin,json=pagePlugin,proto3" json:"page_plugin,omitempty"`
PageKey string `protobuf:"bytes,7,opt,name=page_key,json=pageKey,proto3" json:"page_key,omitempty"`
unknownFields protoimpl.UnknownFields unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache sizeCache protoimpl.SizeCache
} }
@ -7022,6 +7065,20 @@ func (x *ProvisionerEnsureMenuItemRequest) GetSortOrder() int32 {
return 0 return 0
} }
func (x *ProvisionerEnsureMenuItemRequest) GetPagePlugin() string {
if x != nil {
return x.PagePlugin
}
return ""
}
func (x *ProvisionerEnsureMenuItemRequest) GetPageKey() string {
if x != nil {
return x.PageKey
}
return ""
}
type ProvisionerEnsureMenuItemResponse struct { type ProvisionerEnsureMenuItemResponse struct {
state protoimpl.MessageState `protogen:"open.v1"` state protoimpl.MessageState `protogen:"open.v1"`
unknownFields protoimpl.UnknownFields unknownFields protoimpl.UnknownFields
@ -8786,7 +8843,7 @@ const file_v1_capability_proto_rawDesc = "" +
"\x1fProvisionerEnsureSettingRequest\x12\x10\n" + "\x1fProvisionerEnsureSettingRequest\x12\x10\n" +
"\x03key\x18\x01 \x01(\tR\x03key\x12,\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12,\n" +
"\x12default_value_json\x18\x02 \x01(\fR\x10defaultValueJson\"\"\n" + "\x12default_value_json\x18\x02 \x01(\fR\x10defaultValueJson\"\"\n" +
" ProvisionerEnsureSettingResponse\"\x83\x03\n" + " ProvisionerEnsureSettingResponse\"\x92\x04\n" +
"\bPageSeed\x12\x12\n" + "\bPageSeed\x12\x12\n" +
"\x04slug\x18\x01 \x01(\tR\x04slug\x12\x1f\n" + "\x04slug\x18\x01 \x01(\tR\x04slug\x12\x1f\n" +
"\vparent_slug\x18\x02 \x01(\tR\n" + "\vparent_slug\x18\x02 \x01(\tR\n" +
@ -8799,20 +8856,29 @@ const file_v1_capability_proto_rawDesc = "" +
"\x11detail_slug_field\x18\b \x01(\tR\x0fdetailSlugField\x12)\n" + "\x11detail_slug_field\x18\b \x01(\tR\x0fdetailSlugField\x12)\n" +
"\x10reconcile_blocks\x18\t \x01(\bR\x0freconcileBlocks\x12-\n" + "\x10reconcile_blocks\x18\t \x01(\bR\x0freconcileBlocks\x12-\n" +
"\x12reconcile_template\x18\n" + "\x12reconcile_template\x18\n" +
" \x01(\bR\x11reconcileTemplate\"D\n" + " \x01(\bR\x11reconcileTemplate\x12\x10\n" +
"\x03key\x18\v \x01(\tR\x03key\x12\x1d\n" +
"\n" +
"parent_key\x18\f \x01(\tR\tparentKey\x12\x1d\n" +
"\n" +
"mount_path\x18\r \x01(\tR\tmountPath\x12=\n" +
"\x1bmerge_missing_block_content\x18\x0e \x01(\bR\x18mergeMissingBlockContent\"D\n" +
"\x1cProvisionerEnsurePageRequest\x12$\n" + "\x1cProvisionerEnsurePageRequest\x12$\n" +
"\x04page\x18\x01 \x01(\v2\x10.abi.v1.PageSeedR\x04page\"\x1f\n" + "\x04page\x18\x01 \x01(\v2\x10.abi.v1.PageSeedR\x04page\"\x1f\n" +
"\x1dProvisionerEnsurePageResponse\"O\n" + "\x1dProvisionerEnsurePageResponse\"O\n" +
"&ProvisionerOverrideSiteSettingsRequest\x12%\n" + "&ProvisionerOverrideSiteSettingsRequest\x12%\n" +
"\x0eoverrides_json\x18\x01 \x01(\fR\roverridesJson\")\n" + "\x0eoverrides_json\x18\x01 \x01(\fR\roverridesJson\")\n" +
"'ProvisionerOverrideSiteSettingsResponse\"\xa3\x01\n" + "'ProvisionerOverrideSiteSettingsResponse\"\xdf\x01\n" +
" ProvisionerEnsureMenuItemRequest\x12\x1b\n" + " ProvisionerEnsureMenuItemRequest\x12\x1b\n" +
"\tmenu_name\x18\x01 \x01(\tR\bmenuName\x12\x14\n" + "\tmenu_name\x18\x01 \x01(\tR\bmenuName\x12\x14\n" +
"\x05label\x18\x02 \x01(\tR\x05label\x12\x10\n" + "\x05label\x18\x02 \x01(\tR\x05label\x12\x10\n" +
"\x03url\x18\x03 \x01(\tR\x03url\x12\x1b\n" + "\x03url\x18\x03 \x01(\tR\x03url\x12\x1b\n" +
"\tpage_slug\x18\x04 \x01(\tR\bpageSlug\x12\x1d\n" + "\tpage_slug\x18\x04 \x01(\tR\bpageSlug\x12\x1d\n" +
"\n" + "\n" +
"sort_order\x18\x05 \x01(\x05R\tsortOrder\"#\n" + "sort_order\x18\x05 \x01(\x05R\tsortOrder\x12\x1f\n" +
"\vpage_plugin\x18\x06 \x01(\tR\n" +
"pagePlugin\x12\x19\n" +
"\bpage_key\x18\a \x01(\tR\apageKey\"#\n" +
"!ProvisionerEnsureMenuItemResponse\"\x87\x01\n" + "!ProvisionerEnsureMenuItemResponse\"\x87\x01\n" +
")ProvisionerRegisterEmbeddingConfigRequest\x12\x1b\n" + ")ProvisionerRegisterEmbeddingConfigRequest\x12\x1b\n" +
"\ttable_key\x18\x01 \x01(\tR\btableKey\x12#\n" + "\ttable_key\x18\x01 \x01(\tR\btableKey\x12#\n" +

View File

@ -161,7 +161,14 @@ type PoweredBlock struct {
// ninjatpl/pongo2 template source the host renders host-side. // ninjatpl/pongo2 template source the host renders host-side.
Template string `protobuf:"bytes,1,opt,name=template,proto3" json:"template,omitempty"` Template string `protobuf:"bytes,1,opt,name=template,proto3" json:"template,omitempty"`
// JSON encoding of the template data map (map[string]any). // JSON encoding of the template data map (map[string]any).
DataJson []byte `protobuf:"bytes,2,opt,name=data_json,json=dataJson,proto3" json:"data_json,omitempty"` DataJson []byte `protobuf:"bytes,2,opt,name=data_json,json=dataJson,proto3" json:"data_json,omitempty"`
// Requested HTTP response status. Before writing headers, the host accepts
// only 200599, uses the highest status class, and keeps the first result
// encountered within that class.
StatusCode int32 `protobuf:"varint,3,opt,name=status_code,json=statusCode,proto3" json:"status_code,omitempty"`
// Structured document metadata merged by the host into the page head. The
// first non-empty value for each field wins in deterministic render order.
Metadata *DocumentMetadata `protobuf:"bytes,4,opt,name=metadata,proto3" json:"metadata,omitempty"`
unknownFields protoimpl.UnknownFields unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache sizeCache protoimpl.SizeCache
} }
@ -210,6 +217,98 @@ func (x *PoweredBlock) GetDataJson() []byte {
return nil return nil
} }
func (x *PoweredBlock) GetStatusCode() int32 {
if x != nil {
return x.StatusCode
}
return 0
}
func (x *PoweredBlock) GetMetadata() *DocumentMetadata {
if x != nil {
return x.Metadata
}
return nil
}
// DocumentMetadata is the safe, shared subset of document head data a plugin
// may provide. The host validates and escapes every value before rendering.
type DocumentMetadata struct {
state protoimpl.MessageState `protogen:"open.v1"`
Title string `protobuf:"bytes,1,opt,name=title,proto3" json:"title,omitempty"`
Description string `protobuf:"bytes,2,opt,name=description,proto3" json:"description,omitempty"`
CanonicalUrl string `protobuf:"bytes,3,opt,name=canonical_url,json=canonicalUrl,proto3" json:"canonical_url,omitempty"`
ImageUrl string `protobuf:"bytes,4,opt,name=image_url,json=imageUrl,proto3" json:"image_url,omitempty"`
Robots string `protobuf:"bytes,5,opt,name=robots,proto3" json:"robots,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *DocumentMetadata) Reset() {
*x = DocumentMetadata{}
mi := &file_v1_render_proto_msgTypes[3]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *DocumentMetadata) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*DocumentMetadata) ProtoMessage() {}
func (x *DocumentMetadata) ProtoReflect() protoreflect.Message {
mi := &file_v1_render_proto_msgTypes[3]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use DocumentMetadata.ProtoReflect.Descriptor instead.
func (*DocumentMetadata) Descriptor() ([]byte, []int) {
return file_v1_render_proto_rawDescGZIP(), []int{3}
}
func (x *DocumentMetadata) GetTitle() string {
if x != nil {
return x.Title
}
return ""
}
func (x *DocumentMetadata) GetDescription() string {
if x != nil {
return x.Description
}
return ""
}
func (x *DocumentMetadata) GetCanonicalUrl() string {
if x != nil {
return x.CanonicalUrl
}
return ""
}
func (x *DocumentMetadata) GetImageUrl() string {
if x != nil {
return x.ImageUrl
}
return ""
}
func (x *DocumentMetadata) GetRobots() string {
if x != nil {
return x.Robots
}
return ""
}
// RenderTemplateRequest renders one template (templates.TemplateFunc). // RenderTemplateRequest renders one template (templates.TemplateFunc).
type RenderTemplateRequest struct { type RenderTemplateRequest struct {
state protoimpl.MessageState `protogen:"open.v1"` state protoimpl.MessageState `protogen:"open.v1"`
@ -223,7 +322,7 @@ type RenderTemplateRequest struct {
func (x *RenderTemplateRequest) Reset() { func (x *RenderTemplateRequest) Reset() {
*x = RenderTemplateRequest{} *x = RenderTemplateRequest{}
mi := &file_v1_render_proto_msgTypes[3] mi := &file_v1_render_proto_msgTypes[4]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@ -235,7 +334,7 @@ func (x *RenderTemplateRequest) String() string {
func (*RenderTemplateRequest) ProtoMessage() {} func (*RenderTemplateRequest) ProtoMessage() {}
func (x *RenderTemplateRequest) ProtoReflect() protoreflect.Message { func (x *RenderTemplateRequest) ProtoReflect() protoreflect.Message {
mi := &file_v1_render_proto_msgTypes[3] mi := &file_v1_render_proto_msgTypes[4]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@ -248,7 +347,7 @@ func (x *RenderTemplateRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use RenderTemplateRequest.ProtoReflect.Descriptor instead. // Deprecated: Use RenderTemplateRequest.ProtoReflect.Descriptor instead.
func (*RenderTemplateRequest) Descriptor() ([]byte, []int) { func (*RenderTemplateRequest) Descriptor() ([]byte, []int) {
return file_v1_render_proto_rawDescGZIP(), []int{3} return file_v1_render_proto_rawDescGZIP(), []int{4}
} }
func (x *RenderTemplateRequest) GetTemplateKey() string { func (x *RenderTemplateRequest) GetTemplateKey() string {
@ -281,7 +380,7 @@ type RenderTemplateResponse struct {
func (x *RenderTemplateResponse) Reset() { func (x *RenderTemplateResponse) Reset() {
*x = RenderTemplateResponse{} *x = RenderTemplateResponse{}
mi := &file_v1_render_proto_msgTypes[4] mi := &file_v1_render_proto_msgTypes[5]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@ -293,7 +392,7 @@ func (x *RenderTemplateResponse) String() string {
func (*RenderTemplateResponse) ProtoMessage() {} func (*RenderTemplateResponse) ProtoMessage() {}
func (x *RenderTemplateResponse) ProtoReflect() protoreflect.Message { func (x *RenderTemplateResponse) ProtoReflect() protoreflect.Message {
mi := &file_v1_render_proto_msgTypes[4] mi := &file_v1_render_proto_msgTypes[5]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@ -306,7 +405,7 @@ func (x *RenderTemplateResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use RenderTemplateResponse.ProtoReflect.Descriptor instead. // Deprecated: Use RenderTemplateResponse.ProtoReflect.Descriptor instead.
func (*RenderTemplateResponse) Descriptor() ([]byte, []int) { func (*RenderTemplateResponse) Descriptor() ([]byte, []int) {
return file_v1_render_proto_rawDescGZIP(), []int{4} return file_v1_render_proto_rawDescGZIP(), []int{5}
} }
func (x *RenderTemplateResponse) GetDocument() *TemplateDocument { func (x *RenderTemplateResponse) GetDocument() *TemplateDocument {
@ -329,14 +428,19 @@ type TemplateDocument struct {
BodyEndExtraHtml []byte `protobuf:"bytes,5,opt,name=body_end_extra_html,json=bodyEndExtraHtml,proto3" json:"body_end_extra_html,omitempty"` // appended before </body>, after bn.BodyEnd BodyEndExtraHtml []byte `protobuf:"bytes,5,opt,name=body_end_extra_html,json=bodyEndExtraHtml,proto3" json:"body_end_extra_html,omitempty"` // appended before </body>, after bn.BodyEnd
// Extra <html>-element attributes (e.g. data-theme). A "lang" key here is // Extra <html>-element attributes (e.g. data-theme). A "lang" key here is
// ignored — the lang field wins. // ignored — the lang field wins.
HtmlAttrs map[string]string `protobuf:"bytes,6,rep,name=html_attrs,json=htmlAttrs,proto3" json:"html_attrs,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` HtmlAttrs map[string]string `protobuf:"bytes,6,rep,name=html_attrs,json=htmlAttrs,proto3" json:"html_attrs,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"`
// Requested HTTP response status. The host applies the same validation and
// precedence rules as PoweredBlock.status_code.
StatusCode int32 `protobuf:"varint,7,opt,name=status_code,json=statusCode,proto3" json:"status_code,omitempty"`
// Structured document metadata merged by the host into the page head.
Metadata *DocumentMetadata `protobuf:"bytes,8,opt,name=metadata,proto3" json:"metadata,omitempty"`
unknownFields protoimpl.UnknownFields unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache sizeCache protoimpl.SizeCache
} }
func (x *TemplateDocument) Reset() { func (x *TemplateDocument) Reset() {
*x = TemplateDocument{} *x = TemplateDocument{}
mi := &file_v1_render_proto_msgTypes[5] mi := &file_v1_render_proto_msgTypes[6]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@ -348,7 +452,7 @@ func (x *TemplateDocument) String() string {
func (*TemplateDocument) ProtoMessage() {} func (*TemplateDocument) ProtoMessage() {}
func (x *TemplateDocument) ProtoReflect() protoreflect.Message { func (x *TemplateDocument) ProtoReflect() protoreflect.Message {
mi := &file_v1_render_proto_msgTypes[5] mi := &file_v1_render_proto_msgTypes[6]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@ -361,7 +465,7 @@ func (x *TemplateDocument) ProtoReflect() protoreflect.Message {
// Deprecated: Use TemplateDocument.ProtoReflect.Descriptor instead. // Deprecated: Use TemplateDocument.ProtoReflect.Descriptor instead.
func (*TemplateDocument) Descriptor() ([]byte, []int) { func (*TemplateDocument) Descriptor() ([]byte, []int) {
return file_v1_render_proto_rawDescGZIP(), []int{5} return file_v1_render_proto_rawDescGZIP(), []int{6}
} }
func (x *TemplateDocument) GetBodyHtml() []byte { func (x *TemplateDocument) GetBodyHtml() []byte {
@ -406,6 +510,20 @@ func (x *TemplateDocument) GetHtmlAttrs() map[string]string {
return nil return nil
} }
func (x *TemplateDocument) GetStatusCode() int32 {
if x != nil {
return x.StatusCode
}
return 0
}
func (x *TemplateDocument) GetMetadata() *DocumentMetadata {
if x != nil {
return x.Metadata
}
return nil
}
// RenderTagRequest invokes a plugin-declared template tag (HOOK_RENDER_TAG). // RenderTagRequest invokes a plugin-declared template tag (HOOK_RENDER_TAG).
// The host wires one pongo2 tag per manifest.declared_tags name; when its // The host wires one pongo2 tag per manifest.declared_tags name; when its
// engine hits that tag while rendering a powered block, it calls back here. // engine hits that tag while rendering a powered block, it calls back here.
@ -424,7 +542,7 @@ type RenderTagRequest struct {
func (x *RenderTagRequest) Reset() { func (x *RenderTagRequest) Reset() {
*x = RenderTagRequest{} *x = RenderTagRequest{}
mi := &file_v1_render_proto_msgTypes[6] mi := &file_v1_render_proto_msgTypes[7]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@ -436,7 +554,7 @@ func (x *RenderTagRequest) String() string {
func (*RenderTagRequest) ProtoMessage() {} func (*RenderTagRequest) ProtoMessage() {}
func (x *RenderTagRequest) ProtoReflect() protoreflect.Message { func (x *RenderTagRequest) ProtoReflect() protoreflect.Message {
mi := &file_v1_render_proto_msgTypes[6] mi := &file_v1_render_proto_msgTypes[7]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@ -449,7 +567,7 @@ func (x *RenderTagRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use RenderTagRequest.ProtoReflect.Descriptor instead. // Deprecated: Use RenderTagRequest.ProtoReflect.Descriptor instead.
func (*RenderTagRequest) Descriptor() ([]byte, []int) { func (*RenderTagRequest) Descriptor() ([]byte, []int) {
return file_v1_render_proto_rawDescGZIP(), []int{6} return file_v1_render_proto_rawDescGZIP(), []int{7}
} }
func (x *RenderTagRequest) GetTagName() string { func (x *RenderTagRequest) GetTagName() string {
@ -486,7 +604,7 @@ type RenderTagResponse struct {
func (x *RenderTagResponse) Reset() { func (x *RenderTagResponse) Reset() {
*x = RenderTagResponse{} *x = RenderTagResponse{}
mi := &file_v1_render_proto_msgTypes[7] mi := &file_v1_render_proto_msgTypes[8]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@ -498,7 +616,7 @@ func (x *RenderTagResponse) String() string {
func (*RenderTagResponse) ProtoMessage() {} func (*RenderTagResponse) ProtoMessage() {}
func (x *RenderTagResponse) ProtoReflect() protoreflect.Message { func (x *RenderTagResponse) ProtoReflect() protoreflect.Message {
mi := &file_v1_render_proto_msgTypes[7] mi := &file_v1_render_proto_msgTypes[8]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@ -511,7 +629,7 @@ func (x *RenderTagResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use RenderTagResponse.ProtoReflect.Descriptor instead. // Deprecated: Use RenderTagResponse.ProtoReflect.Descriptor instead.
func (*RenderTagResponse) Descriptor() ([]byte, []int) { func (*RenderTagResponse) Descriptor() ([]byte, []int) {
return file_v1_render_proto_rawDescGZIP(), []int{7} return file_v1_render_proto_rawDescGZIP(), []int{8}
} }
func (x *RenderTagResponse) GetHtml() string { func (x *RenderTagResponse) GetHtml() string {
@ -544,7 +662,7 @@ type ApplyFilterRequest struct {
func (x *ApplyFilterRequest) Reset() { func (x *ApplyFilterRequest) Reset() {
*x = ApplyFilterRequest{} *x = ApplyFilterRequest{}
mi := &file_v1_render_proto_msgTypes[8] mi := &file_v1_render_proto_msgTypes[9]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@ -556,7 +674,7 @@ func (x *ApplyFilterRequest) String() string {
func (*ApplyFilterRequest) ProtoMessage() {} func (*ApplyFilterRequest) ProtoMessage() {}
func (x *ApplyFilterRequest) ProtoReflect() protoreflect.Message { func (x *ApplyFilterRequest) ProtoReflect() protoreflect.Message {
mi := &file_v1_render_proto_msgTypes[8] mi := &file_v1_render_proto_msgTypes[9]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@ -569,7 +687,7 @@ func (x *ApplyFilterRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use ApplyFilterRequest.ProtoReflect.Descriptor instead. // Deprecated: Use ApplyFilterRequest.ProtoReflect.Descriptor instead.
func (*ApplyFilterRequest) Descriptor() ([]byte, []int) { func (*ApplyFilterRequest) Descriptor() ([]byte, []int) {
return file_v1_render_proto_rawDescGZIP(), []int{8} return file_v1_render_proto_rawDescGZIP(), []int{9}
} }
func (x *ApplyFilterRequest) GetFilterName() string { func (x *ApplyFilterRequest) GetFilterName() string {
@ -605,7 +723,7 @@ type ApplyFilterResponse struct {
func (x *ApplyFilterResponse) Reset() { func (x *ApplyFilterResponse) Reset() {
*x = ApplyFilterResponse{} *x = ApplyFilterResponse{}
mi := &file_v1_render_proto_msgTypes[9] mi := &file_v1_render_proto_msgTypes[10]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@ -617,7 +735,7 @@ func (x *ApplyFilterResponse) String() string {
func (*ApplyFilterResponse) ProtoMessage() {} func (*ApplyFilterResponse) ProtoMessage() {}
func (x *ApplyFilterResponse) ProtoReflect() protoreflect.Message { func (x *ApplyFilterResponse) ProtoReflect() protoreflect.Message {
mi := &file_v1_render_proto_msgTypes[9] mi := &file_v1_render_proto_msgTypes[10]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@ -630,7 +748,7 @@ func (x *ApplyFilterResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use ApplyFilterResponse.ProtoReflect.Descriptor instead. // Deprecated: Use ApplyFilterResponse.ProtoReflect.Descriptor instead.
func (*ApplyFilterResponse) Descriptor() ([]byte, []int) { func (*ApplyFilterResponse) Descriptor() ([]byte, []int) {
return file_v1_render_proto_rawDescGZIP(), []int{9} return file_v1_render_proto_rawDescGZIP(), []int{10}
} }
func (x *ApplyFilterResponse) GetOutput() string { func (x *ApplyFilterResponse) GetOutput() string {
@ -694,7 +812,7 @@ type RenderContext struct {
func (x *RenderContext) Reset() { func (x *RenderContext) Reset() {
*x = RenderContext{} *x = RenderContext{}
mi := &file_v1_render_proto_msgTypes[10] mi := &file_v1_render_proto_msgTypes[11]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@ -706,7 +824,7 @@ func (x *RenderContext) String() string {
func (*RenderContext) ProtoMessage() {} func (*RenderContext) ProtoMessage() {}
func (x *RenderContext) ProtoReflect() protoreflect.Message { func (x *RenderContext) ProtoReflect() protoreflect.Message {
mi := &file_v1_render_proto_msgTypes[10] mi := &file_v1_render_proto_msgTypes[11]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@ -719,7 +837,7 @@ func (x *RenderContext) ProtoReflect() protoreflect.Message {
// Deprecated: Use RenderContext.ProtoReflect.Descriptor instead. // Deprecated: Use RenderContext.ProtoReflect.Descriptor instead.
func (*RenderContext) Descriptor() ([]byte, []int) { func (*RenderContext) Descriptor() ([]byte, []int) {
return file_v1_render_proto_rawDescGZIP(), []int{10} return file_v1_render_proto_rawDescGZIP(), []int{11}
} }
func (x *RenderContext) GetRequest() *RequestInfo { func (x *RenderContext) GetRequest() *RequestInfo {
@ -868,7 +986,7 @@ type RequestInfo struct {
func (x *RequestInfo) Reset() { func (x *RequestInfo) Reset() {
*x = RequestInfo{} *x = RequestInfo{}
mi := &file_v1_render_proto_msgTypes[11] mi := &file_v1_render_proto_msgTypes[12]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@ -880,7 +998,7 @@ func (x *RequestInfo) String() string {
func (*RequestInfo) ProtoMessage() {} func (*RequestInfo) ProtoMessage() {}
func (x *RequestInfo) ProtoReflect() protoreflect.Message { func (x *RequestInfo) ProtoReflect() protoreflect.Message {
mi := &file_v1_render_proto_msgTypes[11] mi := &file_v1_render_proto_msgTypes[12]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@ -893,7 +1011,7 @@ func (x *RequestInfo) ProtoReflect() protoreflect.Message {
// Deprecated: Use RequestInfo.ProtoReflect.Descriptor instead. // Deprecated: Use RequestInfo.ProtoReflect.Descriptor instead.
func (*RequestInfo) Descriptor() ([]byte, []int) { func (*RequestInfo) Descriptor() ([]byte, []int) {
return file_v1_render_proto_rawDescGZIP(), []int{11} return file_v1_render_proto_rawDescGZIP(), []int{12}
} }
func (x *RequestInfo) GetMethod() string { func (x *RequestInfo) GetMethod() string {
@ -968,19 +1086,23 @@ func (x *RequestInfo) GetUserAgent() string {
// PageContext mirrors blocks.PageContext. // PageContext mirrors blocks.PageContext.
type PageContext struct { type PageContext struct {
state protoimpl.MessageState `protogen:"open.v1"` state protoimpl.MessageState `protogen:"open.v1"`
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` // UUID Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` // UUID
Slug string `protobuf:"bytes,2,opt,name=slug,proto3" json:"slug,omitempty"` Slug string `protobuf:"bytes,2,opt,name=slug,proto3" json:"slug,omitempty"`
Title string `protobuf:"bytes,3,opt,name=title,proto3" json:"title,omitempty"` Title string `protobuf:"bytes,3,opt,name=title,proto3" json:"title,omitempty"`
PostType string `protobuf:"bytes,4,opt,name=post_type,json=postType,proto3" json:"post_type,omitempty"` // "page", "post", "master", "system" PostType string `protobuf:"bytes,4,opt,name=post_type,json=postType,proto3" json:"post_type,omitempty"` // "page", "post", "master", "system"
Status string `protobuf:"bytes,5,opt,name=status,proto3" json:"status,omitempty"` // "published", "draft", "scheduled" Status string `protobuf:"bytes,5,opt,name=status,proto3" json:"status,omitempty"` // "published", "draft", "scheduled"
// Slug derived from the resolved page hierarchy.
DerivedSlug string `protobuf:"bytes,6,opt,name=derived_slug,json=derivedSlug,proto3" json:"derived_slug,omitempty"`
// Effective mount path after host inheritance and normalization.
MountPath string `protobuf:"bytes,7,opt,name=mount_path,json=mountPath,proto3" json:"mount_path,omitempty"`
unknownFields protoimpl.UnknownFields unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache sizeCache protoimpl.SizeCache
} }
func (x *PageContext) Reset() { func (x *PageContext) Reset() {
*x = PageContext{} *x = PageContext{}
mi := &file_v1_render_proto_msgTypes[12] mi := &file_v1_render_proto_msgTypes[13]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@ -992,7 +1114,7 @@ func (x *PageContext) String() string {
func (*PageContext) ProtoMessage() {} func (*PageContext) ProtoMessage() {}
func (x *PageContext) ProtoReflect() protoreflect.Message { func (x *PageContext) ProtoReflect() protoreflect.Message {
mi := &file_v1_render_proto_msgTypes[12] mi := &file_v1_render_proto_msgTypes[13]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@ -1005,7 +1127,7 @@ func (x *PageContext) ProtoReflect() protoreflect.Message {
// Deprecated: Use PageContext.ProtoReflect.Descriptor instead. // Deprecated: Use PageContext.ProtoReflect.Descriptor instead.
func (*PageContext) Descriptor() ([]byte, []int) { func (*PageContext) Descriptor() ([]byte, []int) {
return file_v1_render_proto_rawDescGZIP(), []int{12} return file_v1_render_proto_rawDescGZIP(), []int{13}
} }
func (x *PageContext) GetId() string { func (x *PageContext) GetId() string {
@ -1043,6 +1165,20 @@ func (x *PageContext) GetStatus() string {
return "" return ""
} }
func (x *PageContext) GetDerivedSlug() string {
if x != nil {
return x.DerivedSlug
}
return ""
}
func (x *PageContext) GetMountPath() string {
if x != nil {
return x.MountPath
}
return ""
}
// PostContext mirrors blocks.PostContext. // PostContext mirrors blocks.PostContext.
type PostContext struct { type PostContext struct {
state protoimpl.MessageState `protogen:"open.v1"` state protoimpl.MessageState `protogen:"open.v1"`
@ -1061,7 +1197,7 @@ type PostContext struct {
func (x *PostContext) Reset() { func (x *PostContext) Reset() {
*x = PostContext{} *x = PostContext{}
mi := &file_v1_render_proto_msgTypes[13] mi := &file_v1_render_proto_msgTypes[14]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@ -1073,7 +1209,7 @@ func (x *PostContext) String() string {
func (*PostContext) ProtoMessage() {} func (*PostContext) ProtoMessage() {}
func (x *PostContext) ProtoReflect() protoreflect.Message { func (x *PostContext) ProtoReflect() protoreflect.Message {
mi := &file_v1_render_proto_msgTypes[13] mi := &file_v1_render_proto_msgTypes[14]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@ -1086,7 +1222,7 @@ func (x *PostContext) ProtoReflect() protoreflect.Message {
// Deprecated: Use PostContext.ProtoReflect.Descriptor instead. // Deprecated: Use PostContext.ProtoReflect.Descriptor instead.
func (*PostContext) Descriptor() ([]byte, []int) { func (*PostContext) Descriptor() ([]byte, []int) {
return file_v1_render_proto_rawDescGZIP(), []int{13} return file_v1_render_proto_rawDescGZIP(), []int{14}
} }
func (x *PostContext) GetId() string { func (x *PostContext) GetId() string {
@ -1166,7 +1302,7 @@ type AuthorContext struct {
func (x *AuthorContext) Reset() { func (x *AuthorContext) Reset() {
*x = AuthorContext{} *x = AuthorContext{}
mi := &file_v1_render_proto_msgTypes[14] mi := &file_v1_render_proto_msgTypes[15]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@ -1178,7 +1314,7 @@ func (x *AuthorContext) String() string {
func (*AuthorContext) ProtoMessage() {} func (*AuthorContext) ProtoMessage() {}
func (x *AuthorContext) ProtoReflect() protoreflect.Message { func (x *AuthorContext) ProtoReflect() protoreflect.Message {
mi := &file_v1_render_proto_msgTypes[14] mi := &file_v1_render_proto_msgTypes[15]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@ -1191,7 +1327,7 @@ func (x *AuthorContext) ProtoReflect() protoreflect.Message {
// Deprecated: Use AuthorContext.ProtoReflect.Descriptor instead. // Deprecated: Use AuthorContext.ProtoReflect.Descriptor instead.
func (*AuthorContext) Descriptor() ([]byte, []int) { func (*AuthorContext) Descriptor() ([]byte, []int) {
return file_v1_render_proto_rawDescGZIP(), []int{14} return file_v1_render_proto_rawDescGZIP(), []int{15}
} }
func (x *AuthorContext) GetId() string { func (x *AuthorContext) GetId() string {
@ -1241,7 +1377,7 @@ type CategoryContext struct {
func (x *CategoryContext) Reset() { func (x *CategoryContext) Reset() {
*x = CategoryContext{} *x = CategoryContext{}
mi := &file_v1_render_proto_msgTypes[15] mi := &file_v1_render_proto_msgTypes[16]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@ -1253,7 +1389,7 @@ func (x *CategoryContext) String() string {
func (*CategoryContext) ProtoMessage() {} func (*CategoryContext) ProtoMessage() {}
func (x *CategoryContext) ProtoReflect() protoreflect.Message { func (x *CategoryContext) ProtoReflect() protoreflect.Message {
mi := &file_v1_render_proto_msgTypes[15] mi := &file_v1_render_proto_msgTypes[16]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@ -1266,7 +1402,7 @@ func (x *CategoryContext) ProtoReflect() protoreflect.Message {
// Deprecated: Use CategoryContext.ProtoReflect.Descriptor instead. // Deprecated: Use CategoryContext.ProtoReflect.Descriptor instead.
func (*CategoryContext) Descriptor() ([]byte, []int) { func (*CategoryContext) Descriptor() ([]byte, []int) {
return file_v1_render_proto_rawDescGZIP(), []int{15} return file_v1_render_proto_rawDescGZIP(), []int{16}
} }
func (x *CategoryContext) GetId() string { func (x *CategoryContext) GetId() string {
@ -1302,7 +1438,7 @@ type MasterPageContext struct {
func (x *MasterPageContext) Reset() { func (x *MasterPageContext) Reset() {
*x = MasterPageContext{} *x = MasterPageContext{}
mi := &file_v1_render_proto_msgTypes[16] mi := &file_v1_render_proto_msgTypes[17]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@ -1314,7 +1450,7 @@ func (x *MasterPageContext) String() string {
func (*MasterPageContext) ProtoMessage() {} func (*MasterPageContext) ProtoMessage() {}
func (x *MasterPageContext) ProtoReflect() protoreflect.Message { func (x *MasterPageContext) ProtoReflect() protoreflect.Message {
mi := &file_v1_render_proto_msgTypes[16] mi := &file_v1_render_proto_msgTypes[17]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@ -1327,7 +1463,7 @@ func (x *MasterPageContext) ProtoReflect() protoreflect.Message {
// Deprecated: Use MasterPageContext.ProtoReflect.Descriptor instead. // Deprecated: Use MasterPageContext.ProtoReflect.Descriptor instead.
func (*MasterPageContext) Descriptor() ([]byte, []int) { func (*MasterPageContext) Descriptor() ([]byte, []int) {
return file_v1_render_proto_rawDescGZIP(), []int{16} return file_v1_render_proto_rawDescGZIP(), []int{17}
} }
func (x *MasterPageContext) GetId() string { func (x *MasterPageContext) GetId() string {
@ -1364,7 +1500,7 @@ type HumanProofBanner struct {
func (x *HumanProofBanner) Reset() { func (x *HumanProofBanner) Reset() {
*x = HumanProofBanner{} *x = HumanProofBanner{}
mi := &file_v1_render_proto_msgTypes[17] mi := &file_v1_render_proto_msgTypes[18]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@ -1376,7 +1512,7 @@ func (x *HumanProofBanner) String() string {
func (*HumanProofBanner) ProtoMessage() {} func (*HumanProofBanner) ProtoMessage() {}
func (x *HumanProofBanner) ProtoReflect() protoreflect.Message { func (x *HumanProofBanner) ProtoReflect() protoreflect.Message {
mi := &file_v1_render_proto_msgTypes[17] mi := &file_v1_render_proto_msgTypes[18]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@ -1389,7 +1525,7 @@ func (x *HumanProofBanner) ProtoReflect() protoreflect.Message {
// Deprecated: Use HumanProofBanner.ProtoReflect.Descriptor instead. // Deprecated: Use HumanProofBanner.ProtoReflect.Descriptor instead.
func (*HumanProofBanner) Descriptor() ([]byte, []int) { func (*HumanProofBanner) Descriptor() ([]byte, []int) {
return file_v1_render_proto_rawDescGZIP(), []int{17} return file_v1_render_proto_rawDescGZIP(), []int{18}
} }
func (x *HumanProofBanner) GetActiveTimeMinutes() int32 { func (x *HumanProofBanner) GetActiveTimeMinutes() int32 {
@ -1433,7 +1569,7 @@ type DetailRow struct {
func (x *DetailRow) Reset() { func (x *DetailRow) Reset() {
*x = DetailRow{} *x = DetailRow{}
mi := &file_v1_render_proto_msgTypes[18] mi := &file_v1_render_proto_msgTypes[19]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@ -1445,7 +1581,7 @@ func (x *DetailRow) String() string {
func (*DetailRow) ProtoMessage() {} func (*DetailRow) ProtoMessage() {}
func (x *DetailRow) ProtoReflect() protoreflect.Message { func (x *DetailRow) ProtoReflect() protoreflect.Message {
mi := &file_v1_render_proto_msgTypes[18] mi := &file_v1_render_proto_msgTypes[19]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@ -1458,7 +1594,7 @@ func (x *DetailRow) ProtoReflect() protoreflect.Message {
// Deprecated: Use DetailRow.ProtoReflect.Descriptor instead. // Deprecated: Use DetailRow.ProtoReflect.Descriptor instead.
func (*DetailRow) Descriptor() ([]byte, []int) { func (*DetailRow) Descriptor() ([]byte, []int) {
return file_v1_render_proto_rawDescGZIP(), []int{18} return file_v1_render_proto_rawDescGZIP(), []int{19}
} }
func (x *DetailRow) GetTableId() string { func (x *DetailRow) GetTableId() string {
@ -1524,13 +1660,17 @@ type BlockContext struct {
DetailRowDataJson []byte `protobuf:"bytes,36,opt,name=detail_row_data_json,json=detailRowDataJson,proto3" json:"detail_row_data_json,omitempty"` DetailRowDataJson []byte `protobuf:"bytes,36,opt,name=detail_row_data_json,json=detailRowDataJson,proto3" json:"detail_row_data_json,omitempty"`
BlogIndexUrl string `protobuf:"bytes,37,opt,name=blog_index_url,json=blogIndexUrl,proto3" json:"blog_index_url,omitempty"` BlogIndexUrl string `protobuf:"bytes,37,opt,name=blog_index_url,json=blogIndexUrl,proto3" json:"blog_index_url,omitempty"`
CategoryPageUrl string `protobuf:"bytes,38,opt,name=category_page_url,json=categoryPageUrl,proto3" json:"category_page_url,omitempty"` CategoryPageUrl string `protobuf:"bytes,38,opt,name=category_page_url,json=categoryPageUrl,proto3" json:"category_page_url,omitempty"`
unknownFields protoimpl.UnknownFields // Slug derived from the resolved page hierarchy.
sizeCache protoimpl.SizeCache DerivedSlug string `protobuf:"bytes,39,opt,name=derived_slug,json=derivedSlug,proto3" json:"derived_slug,omitempty"`
// Effective mount path after host inheritance and normalization.
MountPath string `protobuf:"bytes,40,opt,name=mount_path,json=mountPath,proto3" json:"mount_path,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
} }
func (x *BlockContext) Reset() { func (x *BlockContext) Reset() {
*x = BlockContext{} *x = BlockContext{}
mi := &file_v1_render_proto_msgTypes[19] mi := &file_v1_render_proto_msgTypes[20]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@ -1542,7 +1682,7 @@ func (x *BlockContext) String() string {
func (*BlockContext) ProtoMessage() {} func (*BlockContext) ProtoMessage() {}
func (x *BlockContext) ProtoReflect() protoreflect.Message { func (x *BlockContext) ProtoReflect() protoreflect.Message {
mi := &file_v1_render_proto_msgTypes[19] mi := &file_v1_render_proto_msgTypes[20]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@ -1555,7 +1695,7 @@ func (x *BlockContext) ProtoReflect() protoreflect.Message {
// Deprecated: Use BlockContext.ProtoReflect.Descriptor instead. // Deprecated: Use BlockContext.ProtoReflect.Descriptor instead.
func (*BlockContext) Descriptor() ([]byte, []int) { func (*BlockContext) Descriptor() ([]byte, []int) {
return file_v1_render_proto_rawDescGZIP(), []int{19} return file_v1_render_proto_rawDescGZIP(), []int{20}
} }
func (x *BlockContext) GetUrl() string { func (x *BlockContext) GetUrl() string {
@ -1824,6 +1964,20 @@ func (x *BlockContext) GetCategoryPageUrl() string {
return "" return ""
} }
func (x *BlockContext) GetDerivedSlug() string {
if x != nil {
return x.DerivedSlug
}
return ""
}
func (x *BlockContext) GetMountPath() string {
if x != nil {
return x.MountPath
}
return ""
}
var File_v1_render_proto protoreflect.FileDescriptor var File_v1_render_proto protoreflect.FileDescriptor
const file_v1_render_proto_rawDesc = "" + const file_v1_render_proto_rawDesc = "" +
@ -1835,16 +1989,25 @@ const file_v1_render_proto_rawDesc = "" +
"\x0erender_context\x18\x03 \x01(\v2\x15.abi.v1.RenderContextR\rrenderContext\"Y\n" + "\x0erender_context\x18\x03 \x01(\v2\x15.abi.v1.RenderContextR\rrenderContext\"Y\n" +
"\x13RenderBlockResponse\x12\x12\n" + "\x13RenderBlockResponse\x12\x12\n" +
"\x04html\x18\x01 \x01(\tR\x04html\x12.\n" + "\x04html\x18\x01 \x01(\tR\x04html\x12.\n" +
"\apowered\x18\x02 \x01(\v2\x14.abi.v1.PoweredBlockR\apowered\"G\n" + "\apowered\x18\x02 \x01(\v2\x14.abi.v1.PoweredBlockR\apowered\"\x9e\x01\n" +
"\fPoweredBlock\x12\x1a\n" + "\fPoweredBlock\x12\x1a\n" +
"\btemplate\x18\x01 \x01(\tR\btemplate\x12\x1b\n" + "\btemplate\x18\x01 \x01(\tR\btemplate\x12\x1b\n" +
"\tdata_json\x18\x02 \x01(\fR\bdataJson\"\x93\x01\n" + "\tdata_json\x18\x02 \x01(\fR\bdataJson\x12\x1f\n" +
"\vstatus_code\x18\x03 \x01(\x05R\n" +
"statusCode\x124\n" +
"\bmetadata\x18\x04 \x01(\v2\x18.abi.v1.DocumentMetadataR\bmetadata\"\xa4\x01\n" +
"\x10DocumentMetadata\x12\x14\n" +
"\x05title\x18\x01 \x01(\tR\x05title\x12 \n" +
"\vdescription\x18\x02 \x01(\tR\vdescription\x12#\n" +
"\rcanonical_url\x18\x03 \x01(\tR\fcanonicalUrl\x12\x1b\n" +
"\timage_url\x18\x04 \x01(\tR\bimageUrl\x12\x16\n" +
"\x06robots\x18\x05 \x01(\tR\x06robots\"\x93\x01\n" +
"\x15RenderTemplateRequest\x12!\n" + "\x15RenderTemplateRequest\x12!\n" +
"\ftemplate_key\x18\x01 \x01(\tR\vtemplateKey\x12\x19\n" + "\ftemplate_key\x18\x01 \x01(\tR\vtemplateKey\x12\x19\n" +
"\bdoc_json\x18\x02 \x01(\fR\adocJson\x12<\n" + "\bdoc_json\x18\x02 \x01(\fR\adocJson\x12<\n" +
"\x0erender_context\x18\x03 \x01(\v2\x15.abi.v1.RenderContextR\rrenderContext\"T\n" + "\x0erender_context\x18\x03 \x01(\v2\x15.abi.v1.RenderContextR\rrenderContext\"T\n" +
"\x16RenderTemplateResponse\x124\n" + "\x16RenderTemplateResponse\x124\n" +
"\bdocument\x18\x02 \x01(\v2\x18.abi.v1.TemplateDocumentR\bdocumentJ\x04\b\x01\x10\x02\"\xbf\x02\n" + "\bdocument\x18\x02 \x01(\v2\x18.abi.v1.TemplateDocumentR\bdocumentJ\x04\b\x01\x10\x02\"\x96\x03\n" +
"\x10TemplateDocument\x12\x1b\n" + "\x10TemplateDocument\x12\x1b\n" +
"\tbody_html\x18\x01 \x01(\fR\bbodyHtml\x12\x1d\n" + "\tbody_html\x18\x01 \x01(\fR\bbodyHtml\x12\x1d\n" +
"\n" + "\n" +
@ -1853,7 +2016,10 @@ const file_v1_render_proto_rawDesc = "" +
"\x0fhead_extra_html\x18\x04 \x01(\fR\rheadExtraHtml\x12-\n" + "\x0fhead_extra_html\x18\x04 \x01(\fR\rheadExtraHtml\x12-\n" +
"\x13body_end_extra_html\x18\x05 \x01(\fR\x10bodyEndExtraHtml\x12F\n" + "\x13body_end_extra_html\x18\x05 \x01(\fR\x10bodyEndExtraHtml\x12F\n" +
"\n" + "\n" +
"html_attrs\x18\x06 \x03(\v2'.abi.v1.TemplateDocument.HtmlAttrsEntryR\thtmlAttrs\x1a<\n" + "html_attrs\x18\x06 \x03(\v2'.abi.v1.TemplateDocument.HtmlAttrsEntryR\thtmlAttrs\x12\x1f\n" +
"\vstatus_code\x18\a \x01(\x05R\n" +
"statusCode\x124\n" +
"\bmetadata\x18\b \x01(\v2\x18.abi.v1.DocumentMetadataR\bmetadata\x1a<\n" +
"\x0eHtmlAttrsEntry\x12\x10\n" + "\x0eHtmlAttrsEntry\x12\x10\n" +
"\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" +
"\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"\x88\x01\n" + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"\x88\x01\n" +
@ -1916,13 +2082,16 @@ const file_v1_render_proto_rawDesc = "" +
"\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\x1a:\n" + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\x1a:\n" +
"\fCookiesEntry\x12\x10\n" + "\fCookiesEntry\x12\x10\n" +
"\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" +
"\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"|\n" + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"\xbe\x01\n" +
"\vPageContext\x12\x0e\n" + "\vPageContext\x12\x0e\n" +
"\x02id\x18\x01 \x01(\tR\x02id\x12\x12\n" + "\x02id\x18\x01 \x01(\tR\x02id\x12\x12\n" +
"\x04slug\x18\x02 \x01(\tR\x04slug\x12\x14\n" + "\x04slug\x18\x02 \x01(\tR\x04slug\x12\x14\n" +
"\x05title\x18\x03 \x01(\tR\x05title\x12\x1b\n" + "\x05title\x18\x03 \x01(\tR\x05title\x12\x1b\n" +
"\tpost_type\x18\x04 \x01(\tR\bpostType\x12\x16\n" + "\tpost_type\x18\x04 \x01(\tR\bpostType\x12\x16\n" +
"\x06status\x18\x05 \x01(\tR\x06status\"\xaf\x02\n" + "\x06status\x18\x05 \x01(\tR\x06status\x12!\n" +
"\fderived_slug\x18\x06 \x01(\tR\vderivedSlug\x12\x1d\n" +
"\n" +
"mount_path\x18\a \x01(\tR\tmountPath\"\xaf\x02\n" +
"\vPostContext\x12\x0e\n" + "\vPostContext\x12\x0e\n" +
"\x02id\x18\x01 \x01(\tR\x02id\x12\x12\n" + "\x02id\x18\x01 \x01(\tR\x02id\x12\x12\n" +
"\x04slug\x18\x02 \x01(\tR\x04slug\x12\x14\n" + "\x04slug\x18\x02 \x01(\tR\x04slug\x12\x14\n" +
@ -1957,7 +2126,7 @@ const file_v1_render_proto_rawDesc = "" +
"\tDetailRow\x12\x19\n" + "\tDetailRow\x12\x19\n" +
"\btable_id\x18\x01 \x01(\tR\atableId\x12\x15\n" + "\btable_id\x18\x01 \x01(\tR\atableId\x12\x15\n" +
"\x06row_id\x18\x02 \x01(\tR\x05rowId\x12\x1b\n" + "\x06row_id\x18\x02 \x01(\tR\x05rowId\x12\x1b\n" +
"\tdata_json\x18\x03 \x01(\fR\bdataJson\"\x85\f\n" + "\tdata_json\x18\x03 \x01(\fR\bdataJson\"\xc7\f\n" +
"\fBlockContext\x12\x10\n" + "\fBlockContext\x12\x10\n" +
"\x03url\x18\x01 \x01(\tR\x03url\x12\x12\n" + "\x03url\x18\x01 \x01(\tR\x03url\x12\x12\n" +
"\x04path\x18\x02 \x01(\tR\x04path\x12\x12\n" + "\x04path\x18\x02 \x01(\tR\x04path\x12\x12\n" +
@ -2001,7 +2170,10 @@ const file_v1_render_proto_rawDesc = "" +
"\x0fdetail_table_id\x18# \x01(\tR\rdetailTableId\x12/\n" + "\x0fdetail_table_id\x18# \x01(\tR\rdetailTableId\x12/\n" +
"\x14detail_row_data_json\x18$ \x01(\fR\x11detailRowDataJson\x12$\n" + "\x14detail_row_data_json\x18$ \x01(\fR\x11detailRowDataJson\x12$\n" +
"\x0eblog_index_url\x18% \x01(\tR\fblogIndexUrl\x12*\n" + "\x0eblog_index_url\x18% \x01(\tR\fblogIndexUrl\x12*\n" +
"\x11category_page_url\x18& \x01(\tR\x0fcategoryPageUrl\x1a8\n" + "\x11category_page_url\x18& \x01(\tR\x0fcategoryPageUrl\x12!\n" +
"\fderived_slug\x18' \x01(\tR\vderivedSlug\x12\x1d\n" +
"\n" +
"mount_path\x18( \x01(\tR\tmountPath\x1a8\n" +
"\n" + "\n" +
"QueryEntry\x12\x10\n" + "QueryEntry\x12\x10\n" +
"\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" +
@ -2025,66 +2197,69 @@ func file_v1_render_proto_rawDescGZIP() []byte {
return file_v1_render_proto_rawDescData return file_v1_render_proto_rawDescData
} }
var file_v1_render_proto_msgTypes = make([]protoimpl.MessageInfo, 27) var file_v1_render_proto_msgTypes = make([]protoimpl.MessageInfo, 28)
var file_v1_render_proto_goTypes = []any{ var file_v1_render_proto_goTypes = []any{
(*RenderBlockRequest)(nil), // 0: abi.v1.RenderBlockRequest (*RenderBlockRequest)(nil), // 0: abi.v1.RenderBlockRequest
(*RenderBlockResponse)(nil), // 1: abi.v1.RenderBlockResponse (*RenderBlockResponse)(nil), // 1: abi.v1.RenderBlockResponse
(*PoweredBlock)(nil), // 2: abi.v1.PoweredBlock (*PoweredBlock)(nil), // 2: abi.v1.PoweredBlock
(*RenderTemplateRequest)(nil), // 3: abi.v1.RenderTemplateRequest (*DocumentMetadata)(nil), // 3: abi.v1.DocumentMetadata
(*RenderTemplateResponse)(nil), // 4: abi.v1.RenderTemplateResponse (*RenderTemplateRequest)(nil), // 4: abi.v1.RenderTemplateRequest
(*TemplateDocument)(nil), // 5: abi.v1.TemplateDocument (*RenderTemplateResponse)(nil), // 5: abi.v1.RenderTemplateResponse
(*RenderTagRequest)(nil), // 6: abi.v1.RenderTagRequest (*TemplateDocument)(nil), // 6: abi.v1.TemplateDocument
(*RenderTagResponse)(nil), // 7: abi.v1.RenderTagResponse (*RenderTagRequest)(nil), // 7: abi.v1.RenderTagRequest
(*ApplyFilterRequest)(nil), // 8: abi.v1.ApplyFilterRequest (*RenderTagResponse)(nil), // 8: abi.v1.RenderTagResponse
(*ApplyFilterResponse)(nil), // 9: abi.v1.ApplyFilterResponse (*ApplyFilterRequest)(nil), // 9: abi.v1.ApplyFilterRequest
(*RenderContext)(nil), // 10: abi.v1.RenderContext (*ApplyFilterResponse)(nil), // 10: abi.v1.ApplyFilterResponse
(*RequestInfo)(nil), // 11: abi.v1.RequestInfo (*RenderContext)(nil), // 11: abi.v1.RenderContext
(*PageContext)(nil), // 12: abi.v1.PageContext (*RequestInfo)(nil), // 12: abi.v1.RequestInfo
(*PostContext)(nil), // 13: abi.v1.PostContext (*PageContext)(nil), // 13: abi.v1.PageContext
(*AuthorContext)(nil), // 14: abi.v1.AuthorContext (*PostContext)(nil), // 14: abi.v1.PostContext
(*CategoryContext)(nil), // 15: abi.v1.CategoryContext (*AuthorContext)(nil), // 15: abi.v1.AuthorContext
(*MasterPageContext)(nil), // 16: abi.v1.MasterPageContext (*CategoryContext)(nil), // 16: abi.v1.CategoryContext
(*HumanProofBanner)(nil), // 17: abi.v1.HumanProofBanner (*MasterPageContext)(nil), // 17: abi.v1.MasterPageContext
(*DetailRow)(nil), // 18: abi.v1.DetailRow (*HumanProofBanner)(nil), // 18: abi.v1.HumanProofBanner
(*BlockContext)(nil), // 19: abi.v1.BlockContext (*DetailRow)(nil), // 19: abi.v1.DetailRow
nil, // 20: abi.v1.TemplateDocument.HtmlAttrsEntry (*BlockContext)(nil), // 20: abi.v1.BlockContext
nil, // 21: abi.v1.RenderContext.InjectedSlotsEntry nil, // 21: abi.v1.TemplateDocument.HtmlAttrsEntry
nil, // 22: abi.v1.RequestInfo.HeadersEntry nil, // 22: abi.v1.RenderContext.InjectedSlotsEntry
nil, // 23: abi.v1.RequestInfo.CookiesEntry nil, // 23: abi.v1.RequestInfo.HeadersEntry
nil, // 24: abi.v1.BlockContext.QueryEntry nil, // 24: abi.v1.RequestInfo.CookiesEntry
nil, // 25: abi.v1.BlockContext.CookiesEntry nil, // 25: abi.v1.BlockContext.QueryEntry
nil, // 26: abi.v1.BlockContext.HeadersEntry nil, // 26: abi.v1.BlockContext.CookiesEntry
(*timestamppb.Timestamp)(nil), // 27: google.protobuf.Timestamp nil, // 27: abi.v1.BlockContext.HeadersEntry
(*timestamppb.Timestamp)(nil), // 28: google.protobuf.Timestamp
} }
var file_v1_render_proto_depIdxs = []int32{ var file_v1_render_proto_depIdxs = []int32{
10, // 0: abi.v1.RenderBlockRequest.render_context:type_name -> abi.v1.RenderContext 11, // 0: abi.v1.RenderBlockRequest.render_context:type_name -> abi.v1.RenderContext
2, // 1: abi.v1.RenderBlockResponse.powered:type_name -> abi.v1.PoweredBlock 2, // 1: abi.v1.RenderBlockResponse.powered:type_name -> abi.v1.PoweredBlock
10, // 2: abi.v1.RenderTemplateRequest.render_context:type_name -> abi.v1.RenderContext 3, // 2: abi.v1.PoweredBlock.metadata:type_name -> abi.v1.DocumentMetadata
5, // 3: abi.v1.RenderTemplateResponse.document:type_name -> abi.v1.TemplateDocument 11, // 3: abi.v1.RenderTemplateRequest.render_context:type_name -> abi.v1.RenderContext
20, // 4: abi.v1.TemplateDocument.html_attrs:type_name -> abi.v1.TemplateDocument.HtmlAttrsEntry 6, // 4: abi.v1.RenderTemplateResponse.document:type_name -> abi.v1.TemplateDocument
10, // 5: abi.v1.RenderTagRequest.render_context:type_name -> abi.v1.RenderContext 21, // 5: abi.v1.TemplateDocument.html_attrs:type_name -> abi.v1.TemplateDocument.HtmlAttrsEntry
11, // 6: abi.v1.RenderContext.request:type_name -> abi.v1.RequestInfo 3, // 6: abi.v1.TemplateDocument.metadata:type_name -> abi.v1.DocumentMetadata
19, // 7: abi.v1.RenderContext.block_context:type_name -> abi.v1.BlockContext 11, // 7: abi.v1.RenderTagRequest.render_context:type_name -> abi.v1.RenderContext
12, // 8: abi.v1.RenderContext.page:type_name -> abi.v1.PageContext 12, // 8: abi.v1.RenderContext.request:type_name -> abi.v1.RequestInfo
13, // 9: abi.v1.RenderContext.post:type_name -> abi.v1.PostContext 20, // 9: abi.v1.RenderContext.block_context:type_name -> abi.v1.BlockContext
14, // 10: abi.v1.RenderContext.author:type_name -> abi.v1.AuthorContext 13, // 10: abi.v1.RenderContext.page:type_name -> abi.v1.PageContext
15, // 11: abi.v1.RenderContext.category:type_name -> abi.v1.CategoryContext 14, // 11: abi.v1.RenderContext.post:type_name -> abi.v1.PostContext
16, // 12: abi.v1.RenderContext.master_page:type_name -> abi.v1.MasterPageContext 15, // 12: abi.v1.RenderContext.author:type_name -> abi.v1.AuthorContext
21, // 13: abi.v1.RenderContext.injected_slots:type_name -> abi.v1.RenderContext.InjectedSlotsEntry 16, // 13: abi.v1.RenderContext.category:type_name -> abi.v1.CategoryContext
17, // 14: abi.v1.RenderContext.human_proof_banner:type_name -> abi.v1.HumanProofBanner 17, // 14: abi.v1.RenderContext.master_page:type_name -> abi.v1.MasterPageContext
18, // 15: abi.v1.RenderContext.detail_row:type_name -> abi.v1.DetailRow 22, // 15: abi.v1.RenderContext.injected_slots:type_name -> abi.v1.RenderContext.InjectedSlotsEntry
22, // 16: abi.v1.RequestInfo.headers:type_name -> abi.v1.RequestInfo.HeadersEntry 18, // 16: abi.v1.RenderContext.human_proof_banner:type_name -> abi.v1.HumanProofBanner
23, // 17: abi.v1.RequestInfo.cookies:type_name -> abi.v1.RequestInfo.CookiesEntry 19, // 17: abi.v1.RenderContext.detail_row:type_name -> abi.v1.DetailRow
27, // 18: abi.v1.PostContext.published_at:type_name -> google.protobuf.Timestamp 23, // 18: abi.v1.RequestInfo.headers:type_name -> abi.v1.RequestInfo.HeadersEntry
27, // 19: abi.v1.BlockContext.now:type_name -> google.protobuf.Timestamp 24, // 19: abi.v1.RequestInfo.cookies:type_name -> abi.v1.RequestInfo.CookiesEntry
24, // 20: abi.v1.BlockContext.query:type_name -> abi.v1.BlockContext.QueryEntry 28, // 20: abi.v1.PostContext.published_at:type_name -> google.protobuf.Timestamp
25, // 21: abi.v1.BlockContext.cookies:type_name -> abi.v1.BlockContext.CookiesEntry 28, // 21: abi.v1.BlockContext.now:type_name -> google.protobuf.Timestamp
26, // 22: abi.v1.BlockContext.headers:type_name -> abi.v1.BlockContext.HeadersEntry 25, // 22: abi.v1.BlockContext.query:type_name -> abi.v1.BlockContext.QueryEntry
23, // [23:23] is the sub-list for method output_type 26, // 23: abi.v1.BlockContext.cookies:type_name -> abi.v1.BlockContext.CookiesEntry
23, // [23:23] is the sub-list for method input_type 27, // 24: abi.v1.BlockContext.headers:type_name -> abi.v1.BlockContext.HeadersEntry
23, // [23:23] is the sub-list for extension type_name 25, // [25:25] is the sub-list for method output_type
23, // [23:23] is the sub-list for extension extendee 25, // [25:25] is the sub-list for method input_type
0, // [0:23] is the sub-list for field type_name 25, // [25:25] is the sub-list for extension type_name
25, // [25:25] is the sub-list for extension extendee
0, // [0:25] is the sub-list for field type_name
} }
func init() { file_v1_render_proto_init() } func init() { file_v1_render_proto_init() }
@ -2098,7 +2273,7 @@ func file_v1_render_proto_init() {
GoPackagePath: reflect.TypeOf(x{}).PkgPath(), GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_v1_render_proto_rawDesc), len(file_v1_render_proto_rawDesc)), RawDescriptor: unsafe.Slice(unsafe.StringData(file_v1_render_proto_rawDesc), len(file_v1_render_proto_rawDesc)),
NumEnums: 0, NumEnums: 0,
NumMessages: 27, NumMessages: 28,
NumExtensions: 0, NumExtensions: 0,
NumServices: 0, NumServices: 0,
}, },

View File

@ -13,11 +13,13 @@ import (
// PageContext provides page information to blocks via context. // PageContext provides page information to blocks via context.
// The CMS populates this from its internal db.Page type. // The CMS populates this from its internal db.Page type.
type PageContext struct { type PageContext struct {
ID uuid.UUID ID uuid.UUID
Slug string Slug string
Title string DerivedSlug string // resolved route from the page hierarchy
PostType string // "page", "post", "master", "system" MountPath string // effective host-resolved mount prefix
Status string // "published", "draft", "scheduled" Title string
PostType string // "page", "post", "master", "system"
Status string // "published", "draft", "scheduled"
} }
// PostContext provides blog post information to blocks via context. // PostContext provides blog post information to blocks via context.
@ -368,44 +370,46 @@ func RenderSlot(ctx context.Context) string {
// BlockContext provides contextual information available to all block templates. // BlockContext provides contextual information available to all block templates.
type BlockContext struct { type BlockContext struct {
URL string `pongo2:"url"` URL string `pongo2:"url"`
Path string `pongo2:"path"` Path string `pongo2:"path"`
Slug string `pongo2:"slug"` Slug string `pongo2:"slug"`
PageID string `pongo2:"pageId"` DerivedSlug string `pongo2:"derivedSlug"`
PageTitle string `pongo2:"pageTitle"` MountPath string `pongo2:"mountPath"`
TemplateKey string `pongo2:"templateKey"` PageID string `pongo2:"pageId"`
IsEditor bool `pongo2:"isEditor"` PageTitle string `pongo2:"pageTitle"`
Timestamp int64 `pongo2:"timestamp"` TemplateKey string `pongo2:"templateKey"`
IsLoggedIn bool `pongo2:"isLoggedIn"` IsEditor bool `pongo2:"isEditor"`
UserID string `pongo2:"userId"` Timestamp int64 `pongo2:"timestamp"`
UserEmail string `pongo2:"userEmail"` IsLoggedIn bool `pongo2:"isLoggedIn"`
UserRole string `pongo2:"userRole"` UserID string `pongo2:"userId"`
Method string `pongo2:"method"` UserEmail string `pongo2:"userEmail"`
Host string `pongo2:"host"` UserRole string `pongo2:"userRole"`
Query map[string]string `pongo2:"query"` Method string `pongo2:"method"`
Referrer string `pongo2:"referrer"` Host string `pongo2:"host"`
UserAgent string `pongo2:"userAgent"` Query map[string]string `pongo2:"query"`
IP string `pongo2:"ip"` Referrer string `pongo2:"referrer"`
Cookies map[string]string `pongo2:"cookies"` UserAgent string `pongo2:"userAgent"`
Headers map[string]string `pongo2:"headers"` IP string `pongo2:"ip"`
Country string `pongo2:"country"` Cookies map[string]string `pongo2:"cookies"`
City string `pongo2:"city"` Headers map[string]string `pongo2:"headers"`
Timezone string `pongo2:"timezone"` Country string `pongo2:"country"`
Now time.Time `pongo2:"now"` City string `pongo2:"city"`
CurrentAuthor map[string]any `pongo2:"currentAuthor"` Timezone string `pongo2:"timezone"`
CurrentPost map[string]any `pongo2:"currentPost"` Now time.Time `pongo2:"now"`
CurrentCategory map[string]any `pongo2:"currentCategory"` CurrentAuthor map[string]any `pongo2:"currentAuthor"`
Site map[string]any `pongo2:"site"` CurrentPost map[string]any `pongo2:"currentPost"`
IsPublicLoggedIn bool `pongo2:"isPublicLoggedIn"` CurrentCategory map[string]any `pongo2:"currentCategory"`
PublicUserID string `pongo2:"publicUserId"` Site map[string]any `pongo2:"site"`
PublicUsername string `pongo2:"publicUsername"` IsPublicLoggedIn bool `pongo2:"isPublicLoggedIn"`
PublicDisplayName string `pongo2:"publicDisplayName"` PublicUserID string `pongo2:"publicUserId"`
PublicEmailVerified bool `pongo2:"publicEmailVerified"` PublicUsername string `pongo2:"publicUsername"`
DetailRowID string `pongo2:"detailRowId"` PublicDisplayName string `pongo2:"publicDisplayName"`
DetailTableID string `pongo2:"detailTableId"` PublicEmailVerified bool `pongo2:"publicEmailVerified"`
DetailRowData map[string]any `pongo2:"detailRowData"` DetailRowID string `pongo2:"detailRowId"`
BlogIndexURL string `pongo2:"blogIndexUrl"` DetailTableID string `pongo2:"detailTableId"`
CategoryPageURL string `pongo2:"categoryPageUrl"` DetailRowData map[string]any `pongo2:"detailRowData"`
BlogIndexURL string `pongo2:"blogIndexUrl"`
CategoryPageURL string `pongo2:"categoryPageUrl"`
} }
type blockContextKey struct{} type blockContextKey struct{}
@ -425,6 +429,7 @@ func GetBlockContext(ctx context.Context) *BlockContext {
func (bc *BlockContext) ToMap() map[string]any { func (bc *BlockContext) ToMap() map[string]any {
return map[string]any{ return map[string]any{
"url": bc.URL, "path": bc.Path, "slug": bc.Slug, "url": bc.URL, "path": bc.Path, "slug": bc.Slug,
"derivedSlug": bc.DerivedSlug, "mountPath": bc.MountPath,
"pageId": bc.PageID, "pageTitle": bc.PageTitle, "pageId": bc.PageID, "pageTitle": bc.PageTitle,
"templateKey": bc.TemplateKey, "isEditor": bc.IsEditor, "templateKey": bc.TemplateKey, "isEditor": bc.IsEditor,
"timestamp": bc.Timestamp, "now": bc.Now, "timestamp": bc.Timestamp, "now": bc.Now,
@ -437,12 +442,12 @@ func (bc *BlockContext) ToMap() map[string]any {
"currentAuthor": bc.CurrentAuthor, "currentPost": bc.CurrentPost, "currentAuthor": bc.CurrentAuthor, "currentPost": bc.CurrentPost,
"currentCategory": bc.CurrentCategory, "site": bc.Site, "currentCategory": bc.CurrentCategory, "site": bc.Site,
"isPublicLoggedIn": bc.IsPublicLoggedIn, "isPublicLoggedIn": bc.IsPublicLoggedIn,
"publicUserId": bc.PublicUserID, "publicUsername": bc.PublicUsername, "publicUserId": bc.PublicUserID, "publicUsername": bc.PublicUsername,
"publicDisplayName": bc.PublicDisplayName, "publicDisplayName": bc.PublicDisplayName,
"publicEmailVerified": bc.PublicEmailVerified, "publicEmailVerified": bc.PublicEmailVerified,
"detailRowId": bc.DetailRowID, "detailTableId": bc.DetailTableID, "detailRowId": bc.DetailRowID, "detailTableId": bc.DetailTableID,
"detailRowData": bc.DetailRowData, "detailRowData": bc.DetailRowData,
"blogIndexUrl": bc.BlogIndexURL, "categoryPageUrl": bc.CategoryPageURL, "blogIndexUrl": bc.BlogIndexURL, "categoryPageUrl": bc.CategoryPageURL,
} }
} }

View File

@ -3,6 +3,8 @@ package blocks
import ( import (
"encoding/json" "encoding/json"
"strings" "strings"
"git.dev.alexdunmow.com/block/pluginsdk/document"
) )
// poweredBlockSentinel prefixes a PoweredBlock marker string. It uses NUL // poweredBlockSentinel prefixes a PoweredBlock marker string. It uses NUL
@ -16,8 +18,17 @@ const poweredBlockSentinel = "\x00bn:powered\x00"
// and forwards it as abiv1.PoweredBlock so the HOST renders the template with // and forwards it as abiv1.PoweredBlock so the HOST renders the template with
// pongo2 — after the block-invoke has returned, keeping the guest free. // pongo2 — after the block-invoke has returned, keeping the guest free.
type PoweredResult struct { type PoweredResult struct {
Template string `json:"template"` Template string `json:"template"`
Data map[string]any `json:"data"` Data map[string]any `json:"data"`
StatusCode int32 `json:"statusCode,omitempty"`
Metadata *document.Metadata `json:"metadata,omitempty"`
}
// PoweredOptions controls host-owned response data for a powered block.
// StatusCode is transported as declared; the host accepts only 200599.
type PoweredOptions struct {
StatusCode int32
Metadata *document.Metadata
} }
// PoweredBlock marks a block's return value as "powered": instead of final // PoweredBlock marks a block's return value as "powered": instead of final
@ -37,11 +48,28 @@ type PoweredResult struct {
// hits ({% mytag %} / |myfilter) is a fresh RENDER_TAG / APPLY_FILTER invoke, // hits ({% mytag %} / |myfilter) is a fresh RENDER_TAG / APPLY_FILTER invoke,
// never a re-entrant one. // never a re-entrant one.
func PoweredBlock(template string, data map[string]any) string { func PoweredBlock(template string, data map[string]any) string {
payload, err := json.Marshal(PoweredResult{Template: template, Data: data}) return poweredBlockMarker(PoweredResult{Template: template, Data: data})
}
// PoweredBlockWithOptions is PoweredBlock with structured document metadata
// and an optional HTTP response status for the host to accumulate before it
// writes response headers.
func PoweredBlockWithOptions(template string, data map[string]any, options PoweredOptions) string {
return poweredBlockMarker(PoweredResult{
Template: template,
Data: data,
StatusCode: options.StatusCode,
Metadata: options.Metadata,
})
}
func poweredBlockMarker(result PoweredResult) string {
payload, err := json.Marshal(result)
if err != nil { if err != nil {
// A non-serializable data map is a programming error; fall back to an // A non-serializable data map is a programming error; fall back to an
// empty-data powered result so the template still renders. // empty-data result while preserving response options.
payload, _ = json.Marshal(PoweredResult{Template: template}) result.Data = nil
payload, _ = json.Marshal(result)
} }
return poweredBlockSentinel + string(payload) return poweredBlockSentinel + string(payload)
} }

View File

@ -1,6 +1,10 @@
package blocks package blocks
import "testing" import (
"testing"
"git.dev.alexdunmow.com/block/pluginsdk/document"
)
func TestPoweredBlockRoundTrip(t *testing.T) { func TestPoweredBlockRoundTrip(t *testing.T) {
marker := PoweredBlock("<p>{{ x }}</p>", map[string]any{"x": "y"}) marker := PoweredBlock("<p>{{ x }}</p>", map[string]any{"x": "y"})
@ -16,6 +20,47 @@ func TestPoweredBlockRoundTrip(t *testing.T) {
} }
} }
func TestPoweredBlockWithOptionsRoundTrip(t *testing.T) {
metadata := &document.Metadata{
Title: "Missing article",
Description: "The requested article was not found.",
CanonicalURL: "https://example.com/docs/missing",
ImageURL: "https://example.com/media/missing.png",
Robots: "noindex,nofollow",
}
marker := PoweredBlockWithOptions("<h1>{{ title }}</h1>", map[string]any{"title": "Missing"}, PoweredOptions{
StatusCode: 404,
Metadata: metadata,
})
got, ok := DecodePoweredBlock(marker)
if !ok {
t.Fatal("DecodePoweredBlock did not recognize its own marker")
}
if got.StatusCode != 404 {
t.Errorf("status code = %d, want 404", got.StatusCode)
}
if got.Metadata == nil || *got.Metadata != *metadata {
t.Errorf("metadata = %+v, want %+v", got.Metadata, metadata)
}
}
func TestPoweredBlockWithOptionsPreservesOptionsWhenDataCannotMarshal(t *testing.T) {
marker := PoweredBlockWithOptions("<h1>fallback</h1>", map[string]any{"bad": make(chan int)}, PoweredOptions{
StatusCode: 503,
Metadata: &document.Metadata{Title: "Unavailable"},
})
got, ok := DecodePoweredBlock(marker)
if !ok {
t.Fatal("DecodePoweredBlock did not recognize fallback marker")
}
if got.Data != nil {
t.Errorf("data = %#v, want nil fallback", got.Data)
}
if got.StatusCode != 503 || got.Metadata == nil || got.Metadata.Title != "Unavailable" {
t.Errorf("options were not preserved: %+v", got)
}
}
func TestDecodePoweredBlockRejectsPlainHTML(t *testing.T) { func TestDecodePoweredBlockRejectsPlainHTML(t *testing.T) {
if _, ok := DecodePoweredBlock("<p>hello</p>"); ok { if _, ok := DecodePoweredBlock("<p>hello</p>"); ok {
t.Error("plain HTML must not decode as a powered block") t.Error("plain HTML must not decode as a powered block")

View File

@ -0,0 +1,58 @@
# Provisioned pages use stable keys and render results carry safe response data
Provisioned pages were identified only by slug. Slugs are customer-facing
content and can change when an administrator renames a page, changes its
hierarchy, or mounts a plugin below a customer-specific route. A later plugin
load could therefore fail to find its page, recreate it, or attach children
and menu items to a stale location.
Decision: a page declaration may carry a plugin-scoped `Key`. `ParentKey`
references another declaration from the same plugin, while menu items use the
pair `PagePlugin` and `PageKey` so they can target a provisioned page across
plugin boundaries. The existing `ParentSlug` field remains an explicit legacy
input, but keyed references are the stable identity for new declarations.
`MountPath` is create-only. The host applies it while creating the page but
does not relocate an existing customer page during later reconciliation.
`MergeMissingBlockContent` enables a shallow default merge for matching
plugin-owned block assignments: only absent top-level keys are added, and
present values such as an empty string, zero, false, or null are preserved.
The render ABI exposes both `DerivedSlug` and `MountPath` on page and block
contexts. `DerivedSlug` is computed from the resolved hierarchy. `MountPath`
is the effective, normalized value after host inheritance, rather than the raw
stored declaration. Guests consume these values and do not reimplement host
routing rules.
Powered blocks and template documents may also request an HTTP status and a
shared `DocumentMetadata` value. Metadata is deliberately limited to title,
description, canonical URL, image URL, and robots directives. The host
validates status codes and metadata, escapes values, and accumulates status
before writing headers. Only 200599 is accepted; the highest status class
wins, with the first result retained within a class. For metadata, the first
non-empty value for each field wins in deterministic render order, after which
page/document defaults fill gaps. Arbitrary meta/link maps were rejected
because they would create a second unvalidated head-injection path; advanced
trusted templates already have the explicit `HeadExtra` surface.
Appending fields to the v1 protobuf messages was chosen over renumbering or a
Go-only helper because `abi/proto/v1` is the language-neutral contract. Slug
fallbacks remain data in that contract; the SDK does not silently synthesize
keys from mutable slugs.
Consequences:
- Plugin updates can find the same provisioned page after customer route
changes without taking ownership of the customer's chosen slug.
- Parent and menu references survive page moves and renames.
- Changing a declaration's mount path does not unexpectedly move a live page.
- Plugins can add newly introduced block defaults without overwriting customer
configuration.
- Native and WASM render code receive the same host-resolved routing values.
- Wiki-like powered blocks can return correct not-found status and structured
metadata without rendering or injecting the document envelope themselves.
Keywords: PageConfig.Key, ParentKey, ParentSlug, MountPath,
MergeMissingBlockContent, MenuItemConfig.PagePlugin, PageKey, DerivedSlug,
PageSeed, RenderContext, PoweredBlockWithOptions, DocumentMetadata,
TemplateDocument, stable identity, page provisioning

14
document/metadata.go Normal file
View File

@ -0,0 +1,14 @@
// Package document defines structured page-response metadata shared by block
// and template render results.
package document
// Metadata is the safe document-head subset a plugin may ask the host to
// render. The host remains responsible for validating URLs and robots values
// and for escaping every value before it is written into HTML.
type Metadata struct {
Title string `json:"title,omitempty"`
Description string `json:"description,omitempty"`
CanonicalURL string `json:"canonicalUrl,omitempty"`
ImageURL string `json:"imageUrl,omitempty"`
Robots string `json:"robots,omitempty"`
}

View File

@ -41,8 +41,24 @@ type DataTableConfig struct {
// PageConfig defines a page to provision. // PageConfig defines a page to provision.
type PageConfig struct { type PageConfig struct {
Slug string // Key is the stable, plugin-scoped identity used to find the page across
ParentSlug string // slug and hierarchy changes.
Key string
Slug string
// ParentKey identifies a parent provisioned by the same plugin.
ParentKey string
// ParentSlug is the legacy slug-based parent reference. New declarations
// should use ParentKey so moving or renaming the parent does not break the
// relationship.
ParentSlug string
// MountPath is applied when the page is first created. Reconciliation does
// not move an existing page when this value later changes.
MountPath string
Title string Title string
TemplateKey string TemplateKey string
Blocks []PageBlockConfig Blocks []PageBlockConfig
@ -51,6 +67,10 @@ type PageConfig struct {
DetailSlugField string DetailSlugField string
ReconcileBlocks bool ReconcileBlocks bool
ReconcileTemplate bool ReconcileTemplate bool
// MergeMissingBlockContent shallowly adds absent top-level default keys to
// matching plugin-owned block assignments. Present values, including empty
// strings, zero, false, and null, are preserved.
MergeMissingBlockContent bool
} }
// PageBlockConfig defines a block within a provisioned page. // PageBlockConfig defines a block within a provisioned page.
@ -72,10 +92,15 @@ type EmbeddingConfigDef struct {
// MenuItemConfig defines a menu item to provision. // MenuItemConfig defines a menu item to provision.
type MenuItemConfig struct { type MenuItemConfig struct {
Label string Label string
URL string URL string
PageSlug string PageSlug string
SortOrder int32 // PagePlugin and PageKey identify a provisioned page independently of its
// current slug. PagePlugin permits a menu declared by one plugin to target
// a page owned by another.
PagePlugin string
PageKey string
SortOrder int32
} }
// EmbedConfig defines an embeddable component template. // EmbedConfig defines an embeddable component template.

View File

@ -790,7 +790,9 @@ func TestCapabilityRoundTrip(t *testing.T) {
resp: &abiv1.ProvisionerEnsurePageResponse{}, resp: &abiv1.ProvisionerEnsurePageResponse{},
run: func(t *testing.T, cs plugin.CoreServices) { run: func(t *testing.T, cs plugin.CoreServices) {
err := cs.Provisioner.EnsurePage(plugin.PageConfig{ err := cs.Provisioner.EnsurePage(plugin.PageConfig{
Slug: "/", Title: "Home", TemplateKey: "homepage", ReconcileBlocks: true, Key: "home", Slug: "/", ParentKey: "site", ParentSlug: "/legacy-site",
MountPath: "/knowledge", Title: "Home", TemplateKey: "homepage",
ReconcileBlocks: true, MergeMissingBlockContent: true,
Blocks: []plugin.PageBlockConfig{ Blocks: []plugin.PageBlockConfig{
{BlockKey: "html", Title: "Hero", Content: map[string]any{"x": float64(1)}, Slot: "main", SortOrder: 1}, {BlockKey: "html", Title: "Hero", Content: map[string]any{"x": float64(1)}, Slot: "main", SortOrder: 1},
}, },
@ -813,7 +815,9 @@ func TestCapabilityRoundTrip(t *testing.T) {
name: "provisioner_ensure_menu_item", wantMethod: "provisioner.ensure_menu_item", name: "provisioner_ensure_menu_item", wantMethod: "provisioner.ensure_menu_item",
resp: &abiv1.ProvisionerEnsureMenuItemResponse{}, resp: &abiv1.ProvisionerEnsureMenuItemResponse{},
run: func(t *testing.T, cs plugin.CoreServices) { run: func(t *testing.T, cs plugin.CoreServices) {
err := cs.Provisioner.EnsureMenuItem("main", plugin.MenuItemConfig{Label: "Docs", PageSlug: "/docs", SortOrder: 3}) err := cs.Provisioner.EnsureMenuItem("main", plugin.MenuItemConfig{
Label: "Docs", PageSlug: "/docs", PagePlugin: "wiki", PageKey: "index", SortOrder: 3,
})
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -1008,6 +1012,41 @@ func TestCapabilityRoundTrip(t *testing.T) {
} }
} }
func TestProvisionerPageContractGolden(t *testing.T) {
raw, err := os.ReadFile(goldenPath("provisioner_ensure_page_req"))
if err != nil {
t.Fatal(err)
}
req := &abiv1.ProvisionerEnsurePageRequest{}
if err := proto.Unmarshal(raw, req); err != nil {
t.Fatal(err)
}
page := req.GetPage()
if page == nil {
t.Fatal("page is nil")
}
if page.GetKey() != "home" || page.GetSlug() != "/" ||
page.GetParentKey() != "site" || page.GetParentSlug() != "/legacy-site" ||
page.GetMountPath() != "/knowledge" || !page.GetMergeMissingBlockContent() {
t.Errorf("page seed = %+v", page)
}
}
func TestProvisionerMenuPageReferenceContractGolden(t *testing.T) {
raw, err := os.ReadFile(goldenPath("provisioner_ensure_menu_item_req"))
if err != nil {
t.Fatal(err)
}
req := &abiv1.ProvisionerEnsureMenuItemRequest{}
if err := proto.Unmarshal(raw, req); err != nil {
t.Fatal(err)
}
if req.GetMenuName() != "main" || req.GetPageSlug() != "/docs" ||
req.GetPagePlugin() != "wiki" || req.GetPageKey() != "index" {
t.Errorf("menu request = %+v", req)
}
}
// fakeAbiErr is a transport error carrying an ABI code, like wasmguest.HostError. // fakeAbiErr is a transport error carrying an ABI code, like wasmguest.HostError.
type fakeAbiErr struct { type fakeAbiErr struct {
code abiv1.AbiErrorCode code abiv1.AbiErrorCode

View File

@ -53,15 +53,19 @@ func (s *provisionerStub) EnsureSetting(key string, defaultValue any) error {
func (s *provisionerStub) EnsurePage(config plugin.PageConfig) error { func (s *provisionerStub) EnsurePage(config plugin.PageConfig) error {
seed := &abiv1.PageSeed{ seed := &abiv1.PageSeed{
Slug: config.Slug, Key: config.Key,
ParentSlug: config.ParentSlug, Slug: config.Slug,
Title: config.Title, ParentKey: config.ParentKey,
TemplateKey: config.TemplateKey, ParentSlug: config.ParentSlug,
DetailSourceType: config.DetailSourceType, MountPath: config.MountPath,
DetailSourceKey: config.DetailSourceKey, Title: config.Title,
DetailSlugField: config.DetailSlugField, TemplateKey: config.TemplateKey,
ReconcileBlocks: config.ReconcileBlocks, DetailSourceType: config.DetailSourceType,
ReconcileTemplate: config.ReconcileTemplate, DetailSourceKey: config.DetailSourceKey,
DetailSlugField: config.DetailSlugField,
ReconcileBlocks: config.ReconcileBlocks,
ReconcileTemplate: config.ReconcileTemplate,
MergeMissingBlockContent: config.MergeMissingBlockContent,
} }
for _, b := range config.Blocks { for _, b := range config.Blocks {
contentJSON, err := json.Marshal(b.Content) contentJSON, err := json.Marshal(b.Content)
@ -92,11 +96,13 @@ func (s *provisionerStub) OverrideSiteSettings(overrides map[string]any) error {
func (s *provisionerStub) EnsureMenuItem(menuName string, config plugin.MenuItemConfig) error { func (s *provisionerStub) EnsureMenuItem(menuName string, config plugin.MenuItemConfig) error {
req := &abiv1.ProvisionerEnsureMenuItemRequest{ req := &abiv1.ProvisionerEnsureMenuItemRequest{
MenuName: menuName, MenuName: menuName,
Label: config.Label, Label: config.Label,
Url: config.URL, Url: config.URL,
PageSlug: config.PageSlug, PageSlug: config.PageSlug,
SortOrder: config.SortOrder, PagePlugin: config.PagePlugin,
PageKey: config.PageKey,
SortOrder: config.SortOrder,
} }
return s.invoke(context.Background(), "ensure_menu_item", req, &abiv1.ProvisionerEnsureMenuItemResponse{}) return s.invoke(context.Background(), "ensure_menu_item", req, &abiv1.ProvisionerEnsureMenuItemResponse{})
} }

View File

@ -1,2 +1,2 @@
mainDocs"/docs( mainDocs"/docs(2wiki:index

View File

@ -1,4 +1,5 @@
4 \
/Home"homepage* / /legacy-siteHome"homepage*
htmlHero{"x":1}*main0H htmlHero{"x":1}*main0HZhomebsitej
/knowledgep

View File

@ -37,11 +37,13 @@ func contextFromRenderContext(ctx context.Context, rc *abiv1.RenderContext) cont
} }
if p := rc.GetPage(); p != nil { if p := rc.GetPage(); p != nil {
ctx = blocks.WithCurrentPage(ctx, &blocks.PageContext{ ctx = blocks.WithCurrentPage(ctx, &blocks.PageContext{
ID: parseUUID(p.GetId()), ID: parseUUID(p.GetId()),
Slug: p.GetSlug(), Slug: p.GetSlug(),
Title: p.GetTitle(), DerivedSlug: p.GetDerivedSlug(),
PostType: p.GetPostType(), MountPath: p.GetMountPath(),
Status: p.GetStatus(), Title: p.GetTitle(),
PostType: p.GetPostType(),
Status: p.GetStatus(),
}) })
} }
if p := rc.GetPost(); p != nil { if p := rc.GetPost(); p != nil {
@ -159,6 +161,8 @@ func blockContextFromProto(pb *abiv1.BlockContext) *blocks.BlockContext {
URL: pb.GetUrl(), URL: pb.GetUrl(),
Path: pb.GetPath(), Path: pb.GetPath(),
Slug: pb.GetSlug(), Slug: pb.GetSlug(),
DerivedSlug: pb.GetDerivedSlug(),
MountPath: pb.GetMountPath(),
PageID: pb.GetPageId(), PageID: pb.GetPageId(),
PageTitle: pb.GetPageTitle(), PageTitle: pb.GetPageTitle(),
TemplateKey: pb.GetTemplateKey(), TemplateKey: pb.GetTemplateKey(),

View File

@ -0,0 +1,51 @@
package wasmguest
import (
"context"
"testing"
abiv1 "git.dev.alexdunmow.com/block/pluginsdk/abi/v1"
"git.dev.alexdunmow.com/block/pluginsdk/blocks"
"github.com/google/uuid"
)
func TestContextFromRenderContextCarriesResolvedPagePaths(t *testing.T) {
pageID := uuid.MustParse("5f0c5f3f-3f1e-4a3a-9b6e-2f4f6a6d7e8f")
ctx := contextFromRenderContext(context.Background(), &abiv1.RenderContext{
Page: &abiv1.PageContext{
Id: pageID.String(),
Slug: "/article",
DerivedSlug: "/knowledge/article",
MountPath: "/knowledge",
Title: "Article",
PostType: "page",
Status: "published",
},
BlockContext: &abiv1.BlockContext{
Slug: "/article",
DerivedSlug: "/knowledge/article",
MountPath: "/knowledge",
},
})
page := blocks.GetCurrentPage(ctx)
if page == nil {
t.Fatal("current page is nil")
}
if page.ID != pageID || page.Slug != "/article" ||
page.DerivedSlug != "/knowledge/article" || page.MountPath != "/knowledge" {
t.Errorf("page context = %+v", page)
}
block := blocks.GetBlockContext(ctx)
if block == nil {
t.Fatal("block context is nil")
}
if block.Slug != "/article" || block.DerivedSlug != "/knowledge/article" || block.MountPath != "/knowledge" {
t.Errorf("block context = %+v", block)
}
data := block.ToMap()
if data["derivedSlug"] != "/knowledge/article" || data["mountPath"] != "/knowledge" {
t.Errorf("block template map = %#v", data)
}
}

View File

@ -14,6 +14,7 @@ import (
abiv1 "git.dev.alexdunmow.com/block/pluginsdk/abi/v1" abiv1 "git.dev.alexdunmow.com/block/pluginsdk/abi/v1"
"git.dev.alexdunmow.com/block/pluginsdk/ai" "git.dev.alexdunmow.com/block/pluginsdk/ai"
"git.dev.alexdunmow.com/block/pluginsdk/blocks" "git.dev.alexdunmow.com/block/pluginsdk/blocks"
"git.dev.alexdunmow.com/block/pluginsdk/document"
"git.dev.alexdunmow.com/block/pluginsdk/plugin" "git.dev.alexdunmow.com/block/pluginsdk/plugin"
"git.dev.alexdunmow.com/block/pluginsdk/plugin/wasmguest/bnwasm" "git.dev.alexdunmow.com/block/pluginsdk/plugin/wasmguest/bnwasm"
"git.dev.alexdunmow.com/block/pluginsdk/plugin/wasmguest/caps" "git.dev.alexdunmow.com/block/pluginsdk/plugin/wasmguest/caps"
@ -253,7 +254,12 @@ func (g *guest) renderBlock(ctx context.Context, payload []byte) (proto.Message,
return nil, internalError("marshal powered block data: " + err.Error()) return nil, internalError("marshal powered block data: " + err.Error())
} }
return &abiv1.RenderBlockResponse{ return &abiv1.RenderBlockResponse{
Powered: &abiv1.PoweredBlock{Template: pr.Template, DataJson: dataJSON}, Powered: &abiv1.PoweredBlock{
Template: pr.Template,
DataJson: dataJSON,
StatusCode: pr.StatusCode,
Metadata: documentMetadataToProto(pr.Metadata),
},
}, nil }, nil
} }
return &abiv1.RenderBlockResponse{Html: out}, nil return &abiv1.RenderBlockResponse{Html: out}, nil
@ -364,6 +370,8 @@ func (g *guest) renderTemplate(ctx context.Context, payload []byte) (proto.Messa
tdoc.HtmlAttrs = pd.HTMLAttrs tdoc.HtmlAttrs = pd.HTMLAttrs
tdoc.HeadExtraHtml = head tdoc.HeadExtraHtml = head
tdoc.BodyEndExtraHtml = end tdoc.BodyEndExtraHtml = end
tdoc.StatusCode = pd.StatusCode
tdoc.Metadata = documentMetadataToProto(pd.Metadata)
} else { } else {
body, err := renderPart(component) body, err := renderPart(component)
if err != nil { if err != nil {
@ -374,6 +382,19 @@ func (g *guest) renderTemplate(ctx context.Context, payload []byte) (proto.Messa
return &abiv1.RenderTemplateResponse{Document: tdoc}, nil return &abiv1.RenderTemplateResponse{Document: tdoc}, nil
} }
func documentMetadataToProto(metadata *document.Metadata) *abiv1.DocumentMetadata {
if metadata == nil {
return nil
}
return &abiv1.DocumentMetadata{
Title: metadata.Title,
Description: metadata.Description,
CanonicalUrl: metadata.CanonicalURL,
ImageUrl: metadata.ImageURL,
Robots: metadata.Robots,
}
}
func (g *guest) handleHTTP(ctx context.Context, payload []byte) (proto.Message, *abiv1.AbiError) { func (g *guest) handleHTTP(ctx context.Context, payload []byte) (proto.Message, *abiv1.AbiError) {
if g.reg.HTTPHandler == nil { if g.reg.HTTPHandler == nil {
return nil, &abiv1.AbiError{ return nil, &abiv1.AbiError{

View File

@ -10,6 +10,7 @@ import (
abiv1 "git.dev.alexdunmow.com/block/pluginsdk/abi/v1" abiv1 "git.dev.alexdunmow.com/block/pluginsdk/abi/v1"
"git.dev.alexdunmow.com/block/pluginsdk/blocks" "git.dev.alexdunmow.com/block/pluginsdk/blocks"
"git.dev.alexdunmow.com/block/pluginsdk/document"
"git.dev.alexdunmow.com/block/pluginsdk/plugin" "git.dev.alexdunmow.com/block/pluginsdk/plugin"
"git.dev.alexdunmow.com/block/pluginsdk/templates" "git.dev.alexdunmow.com/block/pluginsdk/templates"
"google.golang.org/protobuf/proto" "google.golang.org/protobuf/proto"
@ -383,6 +384,14 @@ func TestRenderTemplatePageDocumentFillsEnvelope(t *testing.T) {
HTMLAttrs: map[string]string{"data-theme": "x"}, HTMLAttrs: map[string]string{"data-theme": "x"},
HeadExtra: textComponent(`<meta name="e">`), HeadExtra: textComponent(`<meta name="e">`),
BodyEndExtra: textComponent("<script>1</script>"), BodyEndExtra: textComponent("<script>1</script>"),
StatusCode: 201,
Metadata: &document.Metadata{
Title: "Page title",
Description: "Page description",
CanonicalURL: "https://example.com/page",
ImageURL: "https://example.com/page.png",
Robots: "index,follow",
},
} }
}) })
}, },
@ -419,6 +428,17 @@ func TestRenderTemplatePageDocumentFillsEnvelope(t *testing.T) {
if string(doc.GetBodyEndExtraHtml()) != "<script>1</script>" { if string(doc.GetBodyEndExtraHtml()) != "<script>1</script>" {
t.Errorf("body_end_extra_html = %q", doc.GetBodyEndExtraHtml()) t.Errorf("body_end_extra_html = %q", doc.GetBodyEndExtraHtml())
} }
if doc.GetStatusCode() != 201 {
t.Errorf("status_code = %d, want 201", doc.GetStatusCode())
}
metadata := doc.GetMetadata()
if metadata == nil || metadata.GetTitle() != "Page title" ||
metadata.GetDescription() != "Page description" ||
metadata.GetCanonicalUrl() != "https://example.com/page" ||
metadata.GetImageUrl() != "https://example.com/page.png" ||
metadata.GetRobots() != "index,follow" {
t.Errorf("metadata = %+v", metadata)
}
} }
func TestJobHook(t *testing.T) { func TestJobHook(t *testing.T) {

View File

@ -8,6 +8,7 @@ import (
abiv1 "git.dev.alexdunmow.com/block/pluginsdk/abi/v1" abiv1 "git.dev.alexdunmow.com/block/pluginsdk/abi/v1"
"git.dev.alexdunmow.com/block/pluginsdk/blocks" "git.dev.alexdunmow.com/block/pluginsdk/blocks"
"git.dev.alexdunmow.com/block/pluginsdk/document"
"git.dev.alexdunmow.com/block/pluginsdk/plugin" "git.dev.alexdunmow.com/block/pluginsdk/plugin"
"git.dev.alexdunmow.com/block/pluginsdk/templates" "git.dev.alexdunmow.com/block/pluginsdk/templates"
"google.golang.org/protobuf/proto" "google.golang.org/protobuf/proto"
@ -25,7 +26,20 @@ func tagFilterRegistration() plugin.PluginRegistration {
func(ctx context.Context, content map[string]any) string { func(ctx context.Context, content map[string]any) string {
title, _ := content["title"].(string) title, _ := content["title"].(string)
// Powered: return template + data; the host renders it. // Powered: return template + data; the host renders it.
return blocks.PoweredBlock("<h1>{{ title }}</h1>", map[string]any{"title": title}) return blocks.PoweredBlockWithOptions(
"<h1>{{ title }}</h1>",
map[string]any{"title": title},
blocks.PoweredOptions{
StatusCode: 404,
Metadata: &document.Metadata{
Title: "Missing",
Description: "Missing article",
CanonicalURL: "https://example.com/missing",
ImageURL: "https://example.com/missing.png",
Robots: "noindex,nofollow",
},
},
)
}) })
blocks.RegisterTag("greet", func(args map[string]any, rctx blocks.RenderContext) (string, error) { blocks.RegisterTag("greet", func(args map[string]any, rctx blocks.RenderContext) (string, error) {
who, _ := args["who"].(string) who, _ := args["who"].(string)
@ -111,6 +125,17 @@ func TestPoweredBlockRoundTrip(t *testing.T) {
if !strings.Contains(string(p.GetDataJson()), `"title":"Welcome"`) { if !strings.Contains(string(p.GetDataJson()), `"title":"Welcome"`) {
t.Errorf("data_json = %s", p.GetDataJson()) t.Errorf("data_json = %s", p.GetDataJson())
} }
if p.GetStatusCode() != 404 {
t.Errorf("status_code = %d, want 404", p.GetStatusCode())
}
metadata := p.GetMetadata()
if metadata == nil || metadata.GetTitle() != "Missing" ||
metadata.GetDescription() != "Missing article" ||
metadata.GetCanonicalUrl() != "https://example.com/missing" ||
metadata.GetImageUrl() != "https://example.com/missing.png" ||
metadata.GetRobots() != "noindex,nofollow" {
t.Errorf("metadata = %+v", metadata)
}
} }
func TestRenderTagRoundTrip(t *testing.T) { func TestRenderTagRoundTrip(t *testing.T) {

View File

@ -10,6 +10,7 @@ import (
"strings" "strings"
"git.dev.alexdunmow.com/block/pluginsdk/blocks" "git.dev.alexdunmow.com/block/pluginsdk/blocks"
"git.dev.alexdunmow.com/block/pluginsdk/document"
"git.dev.alexdunmow.com/block/pluginsdk/plugin" "git.dev.alexdunmow.com/block/pluginsdk/plugin"
"git.dev.alexdunmow.com/block/pluginsdk/templates" "git.dev.alexdunmow.com/block/pluginsdk/templates"
) )
@ -45,7 +46,20 @@ var Registration = plugin.PluginRegistration{
if powered, _ := content["powered"].(bool); powered { if powered, _ := content["powered"].(bool); powered {
// Powered path: hand the host a template + data instead of final // Powered path: hand the host a template + data instead of final
// HTML, so the host renders it (pongo2) after this call returns. // HTML, so the host renders it (pongo2) after this call returns.
return blocks.PoweredBlock("<p>hello {{ name }}</p>", map[string]any{"name": name}) return blocks.PoweredBlockWithOptions(
"<p>hello {{ name }}</p>",
map[string]any{"name": name},
blocks.PoweredOptions{
StatusCode: 404,
Metadata: &document.Metadata{
Title: "Missing greeting",
Description: "The greeting was not found.",
CanonicalURL: "https://example.com/greetings/missing",
ImageURL: "https://example.com/greetings/missing.png",
Robots: "noindex,nofollow",
},
},
)
} }
return fmt.Sprintf("<p>hello %s</p>", name) return fmt.Sprintf("<p>hello %s</p>", name)
}) })

View File

@ -241,6 +241,17 @@ func TestWasmFixtureTagFilterPoweredRoundTrip(t *testing.T) {
if !strings.Contains(string(p.GetDataJson()), `"name":"wazero"`) { if !strings.Contains(string(p.GetDataJson()), `"name":"wazero"`) {
t.Errorf("powered data_json = %s", p.GetDataJson()) t.Errorf("powered data_json = %s", p.GetDataJson())
} }
if p.GetStatusCode() != 404 {
t.Errorf("powered status_code = %d, want 404", p.GetStatusCode())
}
metadata := p.GetMetadata()
if metadata == nil || metadata.GetTitle() != "Missing greeting" ||
metadata.GetDescription() != "The greeting was not found." ||
metadata.GetCanonicalUrl() != "https://example.com/greetings/missing" ||
metadata.GetImageUrl() != "https://example.com/greetings/missing.png" ||
metadata.GetRobots() != "noindex,nofollow" {
t.Errorf("powered metadata = %+v", metadata)
}
// HOOK_RENDER_TAG dispatches to the registered tag fn (fresh invoke — // HOOK_RENDER_TAG dispatches to the registered tag fn (fresh invoke —
// no re-entrancy; RENDER_BLOCK already returned). // no re-entrancy; RENDER_BLOCK already returned).

View File

@ -3,6 +3,8 @@ package templates
import ( import (
"context" "context"
"io" "io"
"git.dev.alexdunmow.com/block/pluginsdk/document"
) )
// PageDocument is a full-page template's contribution to the document. The // PageDocument is a full-page template's contribution to the document. The
@ -17,6 +19,8 @@ type PageDocument struct {
HTMLAttrs map[string]string // extra <html> attributes (e.g. data-theme) HTMLAttrs map[string]string // extra <html> attributes (e.g. data-theme)
HeadExtra HTMLComponent // appended inside <head> after bn.Head HeadExtra HTMLComponent // appended inside <head> after bn.Head
BodyEndExtra HTMLComponent // appended before </body> after bn.BodyEnd BodyEndExtra HTMLComponent // appended before </body> after bn.BodyEnd
StatusCode int32 // requested HTTP status; host accepts 200599
Metadata *document.Metadata
} }
func (p *PageDocument) Render(ctx context.Context, w io.Writer) error { func (p *PageDocument) Render(ctx context.Context, w io.Writer) error {