Osy#betaRun it on Osyrin: osyrin.com

Reference / UI

The PageBuilder kit — the desk staff arrange the site at

`Osysharp.PageBuilder` is the desk where the people who run a site arrange its [[ui-blocks|regions]]: add, move, fill in and take out blocks beside a live preview at phone and desk width, keep it as a draft, and publish. The site comes in VERSIONS — the everyday one and, say, Christmas — each with its own arrangements and look, live now or on a schedule, and a member of staff can preview the whole site as a version would look on a date before anyone else sees it.

stable3 examples compiled by CIuiblockscontentkit

Source on GitHub: Osysharp.PageBuilder — github.com/osysharp/pagebuilder ↗

Summary#

Put PagesDesk() on a staff page and SiteLook() in the layout, grant staff the tables, and the regions the app's pages declare are theirs to arrange. The desk is in English and Swedish, and staff write each block's words in every language the app speaks.

// app.osy
use Osysharp.Blocks;
use Osysharp.PageBuilder@1;

Description#

Putting the desk in an app#

Three things: the desk on a page only staff reach, SiteLook() in the layout every public page uses, and the grants.

app Greenhouse {
  use Osysharp.Ui;
  use Osysharp.Storage;
  use Osysharp.Blocks;
  use Osysharp.PageBuilder@1;
  model "model/**/*.osy";
}

using Osysharp.Blocks;
using Osysharp.PageBuilder;

theme Greenhouse {
  Colors {
    Leaf = Modes.Of(light: "#2F6B3A", christmas: "#8C1C13");
    Pot = Modes.Of(light: "#F3EDE4", christmas: "#FBF7EF");
  }
}

[Role] public enum AppRole { Authenticator, Staff }

[Principal]
public entity Member {
  [Required, Unique, MaxLength(200)] string Email;
  security { allow read when IsAuthenticated where Id == user.Id; }
}

[RoleGrant] public entity RoleGrant {
  [Required] Member Member;
  [Required] AppRole Role;
  security { allow read when IsAuthenticated; }
}

policy IsStaff => RoleGrant.Any(g => g.Member == user && g.Role == AppRole.Staff);

policy ArrangesPages => IsStaff;   // Osysharp.Blocks' one question: who arranges the site

/// A headline on the leaf-green ground.
[Block("Headline")]
component Headline([Label("Headline"), MaxLength(80)] string title, Color ground = Colors.Pot) {
  render { Stack(p: 8, bg: ground) { Text(title, fontSize: FontSize.Title, color: Colors.Leaf); } }
}

// Every public page sits in this layout — SiteLook() dresses it in the live version's look, and shows staff the
// banner while they preview.
[Layout]
[Composable]
component Site() {
  render { Stack(gap: 0) { SiteLook(); Outlet; } }
}

[Route("/")]
[Layout(Site)]
[AllowAnonymous]
component Front() {
  render { Region("front", allow: [Headline]) { Headline(title: "Plants for dark flats"); } }
}

[Route("/staff/pages")]
[Authorize(IsStaff)]
component StaffPages() {
  render { Box(p: 6) { PagesDesk(); } }
}

What does staff do at the desk?#

The desk lists the site's versions and every region the code declares, with the page it is on and whether it is still as the code has it, arranged, or has a draft waiting. Arranging a region opens it beside the page itself, at phone width or desk width:

  • Add a block from those the region allows — each drawn small in the site's own look, with its name and what it is for ([[ui-blocks#thumbnails]]) — Up/Down to move one, Take out to remove it;
  • a block's fields, each with the editor its type chose ([[ui-blocks#params]]) — words in the language chosen at the top, so a Swedish reader gets Swedish words; a Markdown field is written in the Markdown editor (the one a newsletter is written in), and a Color field offers the theme's colours by name ([[ui-blocks#colours]]);
  • When it shows — a block of its own can be dated (a sale banner from the 1st to the 21st), and is gone from the morning of its last date;
  • Publish — the draft becomes what customers see while its version is live. Throw the draft away goes back to what is published; Back to the code's own to the default the code writes.

The site's frame — the menu, the strip, the footer#

Regions in the LAYOUT and the app's SiteMenu(…) (The site's frame — a menu, a strip and a footer staff change) are listed first, under Around every page. A frame region is arranged like any other, previewed on the front page. The MENU opens as a list instead of blocks: each entry's words and where it goes (Change, with its dates), Hide/Show, Move in/Move out (under the entry above), Up/Down, Take out, and Add an entry — an address that is neither a page of the site nor an https:// one is refused by name, and an outside address is marked Another site. Publish, throw away and back to the code's own work as for a region; the page beside it shows the draft menu as it changes.

Arranging a mail#

A mail's [[ui-email#region|region]] — the words an order confirmation greets with, a photograph or a quote beside them — is listed under What each mail says and arranged the same way, in the same versions of the site. Beside its blocks the desk shows the MAIL, as the mail renderer writes it and as the region's sample: renders it for something real (the latest order), with this member of staff's draft in it:

  • Phone — the mail at a phone's width; Wide — in a reading pane 720 pixels wide; Plain text — the text twin a client that shows no HTML reads. Each is rendered again after every change.
  • Add a block draws each mail block small, as mail.
  • A block the mail must keep (require:) says Stays and has no Take out.
  • Words naming a placeholder the mail does not fill are refused where they are written, naming the ones it offers.

A mail rendered while its sender previews says it is a preview (RenderedEmail.IsPreview), and is not sent — so a member of staff looking at the Christmas draft can mark an order sent without mailing the draft to its customer: the shop's outbox refuses it, and says to stop previewing first.

Nothing a customer sees changes until Publish: every edit goes to a DRAFT, and a draft is visible only in a preview.

Versions of the site — the everyday one, Christmas#

The site starts with one version, Everyday, which customers see whenever no other version is live. New version copies the one on screen — every arrangement, as drafts — so Christmas starts as Everyday and changes from there.

A version other than Everyday is live between two dates (Live from, Live until — the first day it is not), or now with Make it live now. Two versions cannot be live over the same days: a schedule that overlaps another is refused, naming it. A region a version never arranged shows what Everyday shows there.

Each version can wear one of the theme's variants — the christmas: values of a Modes.Of(…) ([[ui-theming#variants]]). The theme declares them; the desk only offers what is there. The live version's variant is on the page the server sends — <html data-variant="christmas"> — so the very first paint is already the Christmas look, never the everyday one for a moment first. (A staff member's preview is applied once the page knows who they are.)

A version's look — the whole design#

An app that installs LOOKS (Looks — a site's whole design, picked per version) — five shop designs, say — shows them on each version's card: Look and a button per look, the one it wears marked, with the look's line under it. Choosing another look first lists what that look has no design for — a block only the current look draws, a type the new one does not, a region it has no place for — under What will not come along, with Switch to … and Keep …; nothing moves until staff choose. A switch keeps every arrangement: the same words and photographs, drawn in the new look's design, and anything left behind comes back with a look that draws it.

A new version copies its look with everything else, so a redesign is drafted in a copy, previewed as a whole site and switched on a date like any other version. The regions listed are the version's look's — what each allows, its default — and the Add a block list draws each block small in that look (its design, its theme), whatever the live site wears.

Seeing the site as it will be — the preview#

Preview a version as of a date, with or without its drafts, and then open the site: every page of it answers as that version would on that date — its arrangements, its dated blocks, its look — for this browser only, with a banner saying so and Back to the live site. Nobody else sees it, and the link cannot be sent: the preview belongs to the signed-in staff member AND the browser they started it in.

A preview moves what the site SHOWS, never the clock: nothing that runs on a date runs early. And it is read-only — code that does real work asks SitePreviewing() and refuses, as the shop kit's checkout does:

using Osysharp.Blocks;

[AllowAnonymous]
public string Reserve(string plant) {
  if (SitePreviewing()) { throw new Exception("You are previewing the site — nothing is reserved until you go back to the live site."); }
  return $"{plant} is set aside for you.";
}

SiteDate() answers the date the site is being looked at — today, or the previewed date — for a block that shows something by date itself. The shop's own sales go by the same rule (Blocks and regions — pages the staff arrange' one rule for dated content, ShowsAt): a markdown can be scheduled from one date until another, a customer never sees it before it starts, and a preview as of a date inside it shows its prices and its sale shelf.

Doing it from code — versions and arrangements#

Everything the desk does is a function of Osysharp.Blocks, for a test, an import or a scheduled job:

functionwhat it does
MainVersion()the everyday version
DuplicateVersion(id, name)a copy of a version, its arrangements as drafts — the new version's id
ScheduleVersion(id, from, until) · MakeMainVersion(id)put a version live between two dates · make it the everyday one
SetThemeVariant(id, name)the variant a version wears ("" for none)
EditRegion(version, region, key)the draft of a region (made from what is published, or the code's default)
AddBlock(draft, block, position) · MoveBlock · RemoveBlockarrange a draft; a block is named by its full name
SaveBlockValue(placed, language, value) · DateBlock(placed, from, until)fill in a field · date a block
PublishRegion(draft) · DiscardDraft(draft) · ResetRegion(version, region, key)publish · throw away · back to the code's default
StartSitePreview(version, at, withDrafts) · StopSitePreview()preview for this browser · back to the live site
using Osysharp.Blocks;

[Authorize(IsStaff)]
public Guid PrepareChristmas() {
  var christmas = DuplicateVersion(MainVersion().Id, "Christmas");
  SetThemeVariant(christmas, "christmas");
  var draft = EditRegion(christmas, "front", "");
  var headline = PlacedBlock.Where(b => b.Arrangement.Id == draft).OrderBy(b => b.Position).First();
  SaveBlockValue(headline.Id, "en", new BlockValue { Name = "title", Text = "Wreaths, and trees that last" });
  PublishRegion(draft);
  ScheduleVersion(christmas, new DateTime(2026, 11, 28), new DateTime(2026, 12, 27));
  return christmas;
}

How the desk shows a page beside its fields — PageFrame#

The desk shows the page beside the fields in a PageFrame("/c/christmas", title: "…", w: …, h: …) — an atom that frames a page of the SAME app, and nothing else: an address outside the app, a javascript: or a //host path is not framed. It is how the desk shows the phone width and the desk width of the real page, and it is free for any staff page of your own.

See also#

Related

Blocks and regions — pages the staff arrange

A `[Block]` is a component staff can place on a page: its parameters are the fields they fill in, and each parameter's…

[Email] — mail rendered from data

An `[Email]` component is a mail template. It is rendered on the server — from any function, a workflow step included —…

theme tokens

A `theme` block names your app's design tokens — colors, spacing, radii, and more — as reusable values. A token can…