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 bytesDescription#
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 PAGESee also#
- head: a page's own title, description, canonical address and structured data, and its 301.
- publishing a REST API (app.Apis): publishing functions and entities as a JSON API for another program.
- routes and pages: how a page gets its address.
- moved addresses (app.Redirects):
app.Redirects, for an address that moved. - byte[]: the
byte[]a served function returns for binary content.