Skip to content

Public pages

This page shows how an App that signs people in greets visitors who have not: a landing page, and, where you allow it, records they can read. Preview.

app.ts
import { applyTo, commandForm, commandRef, contact, defineApp, faq, field, hero, landing, signIn, step, steps } from 'tablewalk/app';
const applyForFinancing = commandRef('applyForFinancing', {
version: 1,
input: { applicant_name: field.text({ label: 'Full name', max: 120 }) },
});
export default defineApp({
id: 'ignition',
name: 'Ignition',
sources: { lending: { connection: 'loans', default: true } },
resources: {
lead: { access: 'write', owner: 'tenant' },
dealer: { access: 'write', owner: 'platform' },
},
auth: {
providers: ['email'], signUp: 'invite',
rows: 'tenant', tenancy: { label: 'Dealer', profile: 'dealer', platform: { label: 'Lender' } },
roles: { dealer_staff: { realm: 'tenant', read: ['lead'] } },
},
views: {
leads: { kind: 'list', table: 'lead', label: 'Leads', realm: 'tenant' },
finance: commandForm('Apply for financing', applyForFinancing, [step('About you', ['applicant_name'])], { public: true }),
welcome: landing(
hero('Finance your next car', 'Apply in about five minutes with the quotation from your dealer.', [
applyTo('finance', 'Apply for financing'),
signIn('Staff sign in'),
]),
[
steps('How it works', [
{ title: 'Apply', text: 'You get a reference as soon as you send it.' },
{ title: 'A reviewer decides', text: 'You hear about next steps by email.' },
]),
faq('Questions', [
{ question: 'Is Ignition a real lender?', answer: 'No. It is a demonstration lender with synthetic data.' },
]),
contact('Contact us', [
{ label: '[email protected]', href: 'mailto:[email protected]' },
]),
],
{ label: 'Ignition — car financing', footer: { text: 'A fictional lender.' } },
),
},
home: [{ view: 'welcome', signedOut: true }, { view: 'leads' }],
nav: ['leads'],
});

A landing is a view of kind landing. An App has at most one, it needs auth, and a { view, signedOut: true } rule in home must open it. The public form it links to is a public command form. The whole of Ignition’s landing is examples/apps/ignition/landing.ts.

Address Shows
/{app}, signed out The landing (the sign-in screen when there is none)
/{app}, signed in The App, on its signed-in home
/{app}/welcome The landing, signed in or not, to preview it
/{app}/sign-in The sign-in screen
/{app}/form/{key} A public form

hero(headline, text?, actions, options?) is required: one heading, a line of text, and one to three calls to action — applyTo(formKey, label) or signIn(label). Up to twelve sections follow:

Helper Holds
features(title?, items) 1–6 { title, text } points
steps(title?, items) 1–6 numbered steps
faq(title?, items) 1–12 { question, answer } disclosures
text(title?, text) Up to 2,000 characters of prose
callToAction(title, text?, actions) A closing prompt with 1–3 actions
contact(title?, items) 1–8 { label, href } links
embed(view, { limit, sort }) A cards list view’s records (needs auth.public)
search(view, filters, opts) A search panel over a list’s filters (needs auth.public)

Everything is plain text, never HTML or Markdown, and every string has a length limit checked at load. A link is https://, mailto: or tel:; anything else is refused. landing()’s third argument takes the browser label and a footer of text and up to eight links.

Without auth.public a landing reads no data: it is served from the App definition alone, and GET /api/landing is, with public forms and who-am-I, the only API a signed-out visitor reaches.

auth.public lets signed-out visitors read named resources, read-only:

auth: {
roles: { staff: { read: true } },
public: {
read: ['vehicle', 'vehicle_photo', 'dealer', 'site_image'],
fields: { dealer: { read: ['id', 'name', 'city', 'phone', 'about'] } },
},
},

Each resource is named, within the App’s read ceiling, and fields narrows columns as a role’s grant does. Everything else still needs a session. The grant is listed in authority.lock, and it needs roles and rows: 'all'. See signed-out reads.

With it, the landing is drawn inside the App — its menu, its look and a Sign in link — and it can show rows:

  • a call to action may open a list or dashboard whose tables the public reads;
  • embed(view) draws 1–12 records of a cards list;
  • search(view, filters) opens that list with exactly the conditions its filters write, so the result’s address is the search;
  • hero(…, { image, layout, search, highlights }) adds a photo (a public file resource’s row), layout: 'split' | 'overlay', a search panel inside the hero, and up to four short highlights.

Signed-out visitors see public record pages as a storefront: no record-kind label, no Back, no Workbench furniture, no history.

slug: true on a resource follows a public record’s id with its name: /forecourt/vehicles/1-2024-toyota-corolla-le. Only the id is read; any other spelling answers with a 301 to the current one, and the page carries a real <title> and a canonical link. Signed-in readers see /forecourt/vehicles/1. See addresses.

--check-app --preview-features prints preview: landing at the landing view. Check the page signed out at /{app} and signed in at /{app}/welcome, at phone and desk widths. Forecourt’s front page (examples/apps/forecourt/landing.ts) uses every option above.