Osy#betaRun it on Osyrin: osyrin.com

Reference / API

[Serve]

[Serve("/sitemap.xml", ContentType = "application/xml")] [AllowAnonymous] string Sitemap() { … }

Gives a function an address on the app's own origin. A GET at that address answers with the function's return value as the whole response body — a `string` as UTF-8 text, a `byte[]` as its bytes — with a Content-Type inferred from the address or named. It is how an app publishes `/sitemap.xml`, `/robots.txt`, a feed, or anything else a machine fetches by a well-known address.

stable2 examples compiled by CIapihttpseorouting

Summary#

Some things an app publishes are not pages. A search engine reads /sitemap.xml and /robots.txt, a feed reader reads a feed, and each of them fetches a fixed address on the app's own host. [Serve] gives a function one of those addresses. The function reads the app's data like any other and returns the body.

[Serve("/robots.txt")]
[AllowAnonymous]
string Robots() => "User-agent: *\nSitemap: https://shop.example.com/sitemap.xml\n";

Signature#

[Serve("<path>")]                                   // Content-Type inferred from the path's extension
[Serve("<path>", ContentType = "<media type>")]     // …or named
string  Name(<one parameter per {segment}>) { … }   // the body, served as UTF-8 text
byte[]  Name(<one parameter per {segment}>) { … }   // the body, served as those bytes

Description#

The address#

The path is on the app's own origin — one leading /, no query, no fragment. A {name} segment binds the function parameter called name, exactly as a page's route does, and every parameter has to be one: a GET has nothing else to give it. A segment is a whole parameter or none, so /feeds/{slug} is fine and /feeds/{slug}.xml is refused.

Two things may not share an address, and the compiler says which ones collided: two served functions, or a served function and a page. Addresses the platform answers on every app are refused, because the function would never be reached: /api/…, /_osy/…, /oauth/…, /zone/…, /.well-known/oauth-…, /health, and the favicon addresses, which are the app's icon.

A path ending in an extension, like /sitemap.xml, is the ordinary case, and it works for pages too: a page route whose parameter holds a dot (/p/v1.2) is served the page.

The body and its Content-Type#

The function returns the whole body: a string is served as UTF-8 text and a byte[] as its bytes. Any other return type is refused, because a served address has no envelope around it — what the function returns is exactly what arrives.

Without ContentType, the type follows the extension. .xml is application/xml, .txt is text/plain, .json is application/json, and there are also .webmanifest, .csv, .rss, .atom, .ics, .md, .svg, .png, .jpg, .webp, .gif, .ico and .pdf. Text types carry charset=utf-8. An address with no known extension is text/plain for a string and application/octet-stream for bytes.

text/html is refused: an HTML document is a page, and a page is a component with [Route("/…")], which gets the app's head, theme and security for free.

Who it runs as#

It runs as the caller. A crawler is always signed out, so a function a crawler must reach is [AllowAnonymous], like any function reachable without a sign-in. Without it, a signed-out caller is answered 401 and a signed-in one runs it as themselves. Either way it reads only what that caller may read, because the function's data reads are ordinary reads.

A declared refusal inside the function maps to its status: a denial is 403, not-found is 404, a validation refusal is 400. A fault is 500, with a reference to the log line and nothing else.

Caching#

Every response carries an ETag computed from its body, and Cache-Control: no-cache. A client or crawler that sends the ETag back is answered 304 Not Modified without the body when nothing changed. So a sitemap can be read as often as a crawler likes, and it is always current.

Examples#

A sitemap from the app's own rows#

entity Product {
  [MaxLength(200)] string Slug;
  security { allow read when IsAnonymous || IsAuthenticated; }
}

[Serve("/sitemap.xml")]
[AllowAnonymous]
string Sitemap() {
  var xml = "<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\">";
  foreach (var p in Product.OrderBy(p => p.Slug).ToList()) {
    xml = xml + "<url><loc>https://shop.example.com/p/" + p.Slug + "</loc></url>";
  }
  return xml + "</urlset>";
}

[Serve("/robots.txt")]
[AllowAnonymous]
string Robots() { return "User-agent: *\nSitemap: https://shop.example.com/sitemap.xml\n"; }

[Route("/p/{slug}")]
[AllowAnonymous]
[Title("Product")]
component ProductPage(string slug) { render { Text(slug); } }

An address with a parameter, and bytes#

[Serve("/feeds/{slug}", ContentType = "application/json")]
[AllowAnonymous]
string Feed(string slug) { return "{\"slug\":\"" + slug + "\"}"; }

[Serve("/pixel.png")]
[AllowAnonymous]
byte[] Pixel() {
  return Convert.FromBase64String("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=");
}

What the compiler refuses#

[Serve("/api/export")]   string A() { … }                   // an address the platform answers on every app
[Serve("/p/{id}")]       string B(string id) { … }          // the page 'ProductPage' is routed at "/p/{slug}"
[Serve("/count")]        int C() { … }                      // a served function returns `string` or `byte[]`
[Serve("/m/{a}")]        string D(string b) { … }           // `{a}` binds nothing, and `b` has no segment
[Serve("/h", ContentType = "text/html")] string E() { … }   // an HTML document is a PAGE

See also#

Related

publishing a REST API (app.Apis)

`app.Apis` publishes your app to a third party over HTTP. You never write a controller or an endpoint handler — you…

head

A routed page's document head — its title, description, canonical address, share image, structured data, whether it may…

routes and pages

How a component becomes a page: it declares a route with `[Route("/catalog/{slug}")]`, and navigating to a matching…

byte[]

Raw bytes — a file's contents, a digest, a key. Write one as C# does, `new byte[] { 0x89, 0x50 }`, or as a collection…