Octov0.4.2
Guides

Serving HTML

Render HTML pages from templates and parse form submissions.

Flows answer with JSON by default, but an HTTP flow can serve any content type. This guide renders an HTML page from a template and parses a form submission — both sides of Octo's raw-content mode, based on samples/raw-content-demo.

Raw content in one paragraph

Flows are JSON by default; a block puts the message into raw-content mode to serve something else. In that mode the body carries {contentType, rawData} and the HTTP source writes rawData verbatim with contentType as the Content-Type header, instead of JSON-encoding the body. You enter raw mode with a producing block — template-resource or set-payload, both with rawBody: true — not by hand-building the object. See Raw Content and Streaming for the full model.

Declare a template resource

Templates are files declared under resources.templates and referenced by their as alias. A template embeds {{ CEL }} placeholders evaluated over the current message — body, vars, env, and now are all in scope.

samples/raw-content-demo/config.yaml (excerpt)
resources:
  templates:
    - resource: templates/page.tmpl
      as: page
samples/raw-content-demo/templates/page.tmpl
<!doctype html>
<html>
  <head><title>Octo raw content</title></head>
  <body>
    <h1>Hello, {{ vars.name }}!</h1>
    <p>This page was rendered by a template-resource block and served as
      <code>text/html</code> from a raw-content body — no JSON in sight.</p>
  </body>
</html>

Serve the page

The flow is one block: template-resource renders the template against the message and, with rawBody: true, writes the result as a raw-content body. The HTTP source then serves it with the declared content type.

samples/raw-content-demo/config.yaml (excerpt)
- name: page
  source:
    connector: api
    type: http
    settings:
      path: /page/{name}          # {name} -> vars.name
  process:
    - type: template-resource
      settings:
        id: page
        rawBody: true
        contentType: text/html; charset=utf-8

Run it and request a page:

octo run --config samples/raw-content-demo
curl -i localhost:8080/page/World
# HTTP/1.1 200 OK
# Content-Type: text/html; charset=utf-8
#
# <!doctype html> ... <h1>Hello, World!</h1> ...

template-resource takes four settings: id (the template's alias, required), rawBody, contentType (required when rawBody is on), and target — set target to render into a variable instead of the body, for example to build an email fragment mid-flow. rawBody applies to the body only, so leave target empty when serving pages.

You don't need a template file for small responses. set-payload with rawBody: true, a contentType, and a string value (for example value: '"<h1>hi</h1>"') produces a raw-content body inline — handy for terse error pages, as in An AI Web App End to End. The value must evaluate to a string when rawBody is set.

Accept a form submission

Raw content works inbound too. Setting rawBody: true on the HTTP source accepts any content type and stores the request as {contentType, rawData} instead of parsing JSON. The CEL function fromFormData then parses a urlencoded form string into a structured object:

samples/raw-content-demo/config.yaml (excerpt)
- name: submit
  source:
    connector: api
    type: http
    settings:
      path: /submit
      rawBody: true               # accept any content type, store as raw
  process:
    - type: set-payload
      settings:
        value: 'fromFormData(body.rawData)'

Post a form to it:

curl -s -X POST localhost:8080/submit \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  -d 'name=Ann&city=Berlin'
# -> {"city":"Berlin","name":"Ann"}

The body arrives as {"contentType": "application/x-www-form-urlencoded", "rawData": "name=Ann&city=Berlin"}; fromFormData(body.rawData) turns it back into JSON, which the flow returns as a normal JSON response. A single-valued field parses to a string, so body.name and body.city read naturally in later blocks. (The inverse, toFormData, serializes an object into a form string for outbound calls.)

Putting it together: serve a form, handle its POST

Combine the two flows and you have a complete round trip with no frontend stack: a GET flow renders a template containing <form method="POST" action="/submit">, and the POST flow parses the submission with fromFormData, validates or stores it, and responds — with JSON, or with another rendered template for a classic confirmation page.

Where to go next

On this page