Skip to content
PlaceFull

Plain embeds — contract v1

The second way to put PlaceFull on a studio's own website (build-plan/182). The first is the booking frame (?embed=1, build-plan/174): the whole experience, in an iframe. This one is the opposite trade: plain semantic HTML rendered into the studio's own page, which takes the site's fonts and colours, styled by a small CSS layer the studio picks, writes, or has an agent write. It lists what can be booked and when; **every time is a link to the hosted PlaceFull page**, where booking and paying happen.

This file is the public contract. CSS written against it keeps working: everything named here is frozen for the life of v1 (test/unit/plain-embed-v1.test.ts holds the default output).

The code a studio pastes

<script src="https://placefull.ai/embed/v1.js" async></script>
<placefull-schedule location="ash-street-yoga">
  <a href="https://placefull.ai/ash-street-yoga">Book a class</a>
</placefull-schedule>

The link inside the tag is what shows until the script runs, and what stays if it never does. The same code works in a page's HTML (WordPress, Squarespace, hand-built), as a Wix **Custom Element** (tag name + script URL; paid Wix plan with a connected domain), and inside Wix's Embed HTML box (there the box is Wix's own frame, so the site's CSS does not reach it — the theme carries the look, and links open in a new tab).

Tags

TagShowsAttributes
<placefull-schedule>The next 14 days of classes, by day; each row books that datelocation
<placefull-classes>Every published class, with a link to its pagelocation
<placefull-appointments>Every published appointmentlocation
<placefull-rentals>Every published rentallocation
<placefull-listings>Every published listing listed under one heading on the studio's page — Parties, Workshops — any kind (build-plan/291); every listing with no grouplocation, group
<placefull-times>One class, appointment or rental: its upcoming times, paged, with a date jumplocation, listing
<placefull-walkins>Live walk-in spots right now — status, detail, Booklocation; optional listing, book, hide-when, target

Every tag also takes theme (below). Unknown attributes are ignored. A tag whose location or listing is not found keeps its fallback content.

Markup

All hooks are data-pf attributes. We ship no class names and no ids. Text is the storefront's own words (spot, Full — join waitlist, Appointment, Rental).

Lists — classes, appointments, rentals

<div data-pf="list" data-pf-kind="class" data-pf-v="1">
  <ul data-pf="items">
    <li data-pf="item">
      <h3 data-pf="name"><a data-pf="link" href="https://placefull.ai/ash-street-yoga/slow-flow">Slow Flow</a></h3>
      <p data-pf="meta">
        <span data-pf="next">Tuesdays · 6:00 PM</span>
        <span data-pf="duration">60 min</span>
        <span data-pf="price">$22</span>
      </p>
      <p data-pf="description">An unhurried hour…</p>
    </li>
  </ul>
</div>

data-pf-kind is class, appointment or rental, or group from <placefull-listings> (the same items, in the storefront's order). group matches the heading a listing is *Listed under* — its own word, else the studio's word for its kind — in either form and any case: group="Parties" and group="party" are the same group. A meta span is omitted when there is nothing to say (an unpriced listing has no price), and the whole meta line when every span is. A room carries one more span after the price — <span data-pf="mode">per person</span> or whole space — because its price means nothing without it. description is plain text, at most a couple of lines. With nothing published the section holds one <p data-pf="empty"> and no list.

Schedule

<div data-pf="schedule" data-pf-v="1">
  <div data-pf="day">
    <h3 data-pf="day-name"><time datetime="2026-09-21">Mon, Sep 21</time></h3>
    <ul data-pf="items">
      <li data-pf="item">
        <time data-pf="time" datetime="2026-09-21T23:30:00.000Z">5:30 PM</time>
        <a data-pf="link" href="https://placefull.ai/ash-street-yoga/slow-flow">Slow Flow</a>
        <span data-pf="duration">60 min</span>
        <span data-pf="instructor">Sam</span>
        <span data-pf="price">$22</span>
        <span data-pf="spots">4 spots left</span>
        <a data-pf="book" href="https://placefull.ai/ash-street-yoga/slow-flow?time=…"><span data-pf="label">Book</span></a>
      </li>
    </ul>
  </div>
</div>

The nearest days read Today and Tomorrow; the datetime is always the date. A full date carries data-pf-full on its item and its book link reads Waitlist. A studio that cannot take bookings right now gets Details links with no time.

Times

<div data-pf="times" data-pf-v="1">
  <div data-pf="head">
    <h3 data-pf="name">Slow Flow</h3>
    <p data-pf="meta"><span data-pf="price">$22</span> <span data-pf="duration">60 min</span></p>
  </div>
  <form data-pf="jump" method="get" action="…/embed/v1/ash-street-yoga/times/slow-flow">
    <label>Jump to a date <input type="date" name="from" min="2026-09-20"></label>
    <button type="submit"><span data-pf="label">Show</span></button>
  </form>
  <div data-pf="day">
    <h4 data-pf="day-name"><time datetime="2026-09-21">Mon, Sep 21</time></h4>
    <ul data-pf="items">
      <li data-pf="item">
        <a data-pf="book" href="https://placefull.ai/ash-street-yoga/slow-flow?time=…"><time datetime="2026-09-21T23:30:00.000Z">5:30 PM</time></a>
        <span data-pf="spots">4 spots left</span>
      </li>
    </ul>
  </div>
  <p data-pf="pages"><a data-pf="page" rel="next" href="…/times/slow-flow?after=2026-09-24T…">Later times</a></p>
</div>

A page is whole days, up to about two dozen times. Later times and the date jump swap the section in place (the script intercepts them). No times in the window: one <p data-pf="empty"> and the jump form.

Walk-ins

<div data-pf="walkins" data-pf-v="1">
  <span data-pf="status" data-pf-state="open" data-pf-level="plenty" data-pf-spots="14" aria-live="polite">
    <span data-pf="label">Walk-ins welcome</span>
    <span data-pf="detail">plenty of room · 14 spots open now</span>
  </span>
  <a data-pf="book" href="https://placefull.ai/ash-street-yoga?ref=…"><span data-pf="label">Book</span></a>
</div>

data-pf-state is open | busy | full | closed. data-pf-level carries the raw fill band when open (plenty | filling_up | nearly_full | full). Spots are the sum of open seats across walk-in rooms (published per-person space offerings), with a last-entry rule: a room whose stay would run past closing contributes 0. Optional attributes on the tag:

AttributeEffect
listing="slug[,slug]"Count only rooms that host those walk-in offerings. A slug that matches none keeps the fallback and logs one console.warn — never "0 open". A room shared by two listed offerings is counted once.
book="slug"Book → goes to that listing (with ref).
hide-when="closed|full|off"Render nothing when the state matches (no reserved height). Off / unknown / no capacity / paused always collapse; closed and full still render unless listed here.
target="_self|_blank"Sets the Book link's target.

The script refreshes every 30 seconds (the fragment is cached for 15s — never poll faster than the cache), pauses while the tab is hidden, and runs at once when it becomes visible. It dispatches placefull:walkins on the element on first paint and whenever the state changes: { state, level, spotsOpen, until, listing }. Status text sits in aria-live="polite"; unchanged refreshes do not rewrite the DOM, so they are not announced.

<script src="https://placefull.ai/embed/v1.js" async></script>
<placefull-walkins location="ash-street-yoga" listing="pyop,clay" book="pyop">
  <a href="https://placefull.ai/ash-street-yoga">Walk-in availability at Ash Street Yoga</a>
</placefull-walkins>
placefull-walkins { --pf-accent: #5b7a5b; --pf-status-open: #5b7a5b; }
placefull-walkins [data-pf="book"] { text-transform: uppercase; }

Themes

theme="clean" is the default: spacing and list layout only; fonts and colours are the site's. theme="card" draws each item as a card. theme="studio" is card in the studio's own Look (Your page → Branding: accent, ground, type, corners). theme="none" loads no CSS at all — not a theme, and not a location's own stylesheet (below).

Our CSS is weaker than yours. A rule we ship outweighs a site's bare element rule (ul {}, a {}, button {} — nearly every site has them, and they would wreck a list or a button) and nothing else: any selector you write on purpose — a data-pf hook, a class, an id — wins without !important, and the variables are set at no weight at all, so placefull-schedule { --pf-accent: … } always takes. A Book button's words are their own element (label, or the time) so they keep --pf-accent-text even where a site colours every link. A theme styles only the tags that ask for it, so one page can hold tags in different themes. Wrappers are plain <div>s on purpose — a site's section {}, header {} or nav {} rule has nothing of ours to land on — while headings, lists, <time>, links and the form stay semantic, and take your site's heading and list styles gladly.

Themes are driven by these variables — set them on the tag or any parent:

VariableDefault
--pf-fontinherit
--pf-textcurrentColor
--pf-mutedcolor-mix(in srgb, currentColor 62%, transparent)
--pf-accentyour site's link or text colour (studio: the studio's accent)
--pf-accent-textCanvas (studio: the accent's text colour)
--pf-linecolor-mix(in srgb, currentColor 16%, transparent)
--pf-cardtransparent (card, studio: a ground)
--pf-radius8px
--pf-gap0.75rem
--pf-status-opengreen (walk-ins status dot)
--pf-status-busyamber
--pf-status-fullred
placefull-schedule { --pf-accent: #7a2e2e; --pf-radius: 0; }
placefull-schedule [data-pf="book"] { text-transform: uppercase; }

Your stylesheet

A location can keep its own CSS in PlaceFull (Your page → Embed → Customize), and every tag of that location wears it, on every site it is pasted on — nothing to paste again. It is served exactly as saved, with a minute's cache, so a Save reaches the site within a minute:

https://placefull.ai/embed/v1/{location}/custom.css

The script links it for you: once per location however many tags and scripts the page holds, after the theme's sheet (so at equal weight yours wins), and only when a fragment answers with X-Placefull-Css: 1. A location that saved none costs no extra request, and its tags render exactly as before. A fragment waits for the sheet — 1.5 seconds at most — before it shows, so the first paint is already styled. The sheet is overrides on whichever theme the tag wears; a tag with theme="none" never asks for it. PlaceFull's own pages — the listing, checkout, the receipt — never load it.

Every rule starts with one of the six tags. The sheet loads into your website's page, so that is what keeps it to the tags. Nesting inside a tag rule is fine — it stays inside the tag:

placefull-schedule, placefull-times { --pf-accent: #5b7a5b; --pf-radius: 999px; }
placefull-schedule [data-pf="day-name"] { font-variant: small-caps; }
placefull-schedule [data-pf="item"] {
  border-bottom: 1px solid var(--pf-line);
  & [data-pf="book"]:hover { opacity: 0.85; }
}

Save refuses a sheet with any of these, and says which line (Line 18 — this rule would reach your whole website. Start it with the tag.):

  • A rule that does not start with a tag: a bare a {}, :root, *, a hook on its own ([data-pf="book"] {}), or a comma list with one bare part.
  • A + or ~ straight after the tag (placefull-schedule ~ p): it reaches the page beside the tag. Further in is fine ([data-pf="item"] + [data-pf="item"]). A nested selector may not start with +, ~, & + or & ~, nor hold & anywhere but the start (:not(&)).
  • @import and @font-face (the tags already wear your site's fonts), and every at-rule but @media, @supports and @container (@keyframes and @layer included); the rules inside those three are held to the same rule.
  • An address that is not https:// — in url(), image-set() or src().
  • expression(), behavior: and -moz-binding; braces that do not match; more than 32 KB.

!important saves, with a warning: it is never needed. A rule that starts with the tag already outweighs the theme, and at a tie your sheet comes after it.

A rule names the tag, not the location: to aim at one location where two share a page, add the attribute (placefull-schedule[location="ash-street-yoga"] …). The same CSS works in your site's own stylesheet; saved here, it follows the tags to every site they are on. Customize offers six starters to begin from — Plain (every variable written out and commented, so it changes nothing), Cards, Table, Pills, Dark and Classic — and what you start from is yours to edit.

What v1 promises

  • The tag names, attributes, data-pf values, elements and nesting shown above, variable names and theme names do not change or disappear.
  • Additions are opt-in. New content arrives behind a new attribute or as a new tag; a snippet pasted today renders the same markup for as long as v1 is served.
  • A change that would rename or remove anything ships as /embed/v2.js beside v1.
  • Write CSS against data-pf hooks, not against position (:nth-child, > chains): hooks are the contract, incidental order is not.

Coming back

A booking link carries where the customer came from (ref), and the hosted page offers ← Back to {site} through checkout and on the receipt, in place of the studio's mark in the header, so it is the one way back on the page. It is offered only when that address is on the studio's own website (the Website on Your page) — never for an arbitrary site.

A plain link from your site needs no ref: the page reads where the customer came from (the browser's Referer, which is usually just your site's address) and offers the way back when that is your website. A tap takes them back to the page they were on, through the tab's own history, whenever that page is still behind this one; otherwise the link goes to your site's front page, or to the page ref names when you add one.

Sizing the booking frame

Not this contract — the other embed, ?embed=1 in an iframe — but it belongs where a developer looks. Paste the frame's snippet with its <script src="https://placefull.ai/embed.js" async> and nothing below is your problem: that script sizes every iframe[data-placefull] to the page inside it and keeps the frame in view. Size the frame yourself and you are reading these messages, so here is all of one:

{ type: 'placefull:resize', height: 1170, navigated: true }
  • height — the document's height in CSS pixels. Set the iframe to it.
  • navigated — present, and true, only on a page's first message. It is how you know the customer moved (schedule → listing → checkout) rather than the same page growing.

Two rules a host has to keep, both learned from a real site:

  • Check the origin. Take messages from https://placefull.ai and ignore every other sender.
  • Scroll the frame back into view on navigated. A customer who taps Book at the end of a long schedule is scrolled far past the frame's top; the page that replaces it is shorter, so without this they are left looking at empty ground under it.
addEventListener('message', (e) => {
  if (e.origin !== 'https://placefull.ai') return;
  const d = e.data;
  if (!d || d.type !== 'placefull:resize' || !(d.height >= 0)) return;
  for (const f of document.querySelectorAll('iframe[data-placefull]')) {
    if (f.contentWindow !== e.source) continue;
    f.style.height = d.height + 'px';
    if (d.navigated === true && d.height > 0 && f.getBoundingClientRect().top < 0) {
      f.scrollIntoView({ block: 'start' });
    }
  }
});

Height 0 is intentional: the walk-ins bar (?embed=live) collapses when off. The snippet still reserves 48px until the first resize message arrives. The frame sends nothing else, and reads nothing the host sends.

For developers and agents

The fragments are plain URLs, CORS-open, cacheable for a minute, and can be fetched server-side and printed into a page (better for search engines than script-inserted content):

GET https://placefull.ai/embed/v1/{location}/schedule
GET https://placefull.ai/embed/v1/{location}/classes | appointments | rentals
GET https://placefull.ai/embed/v1/{location}/times/{listing}?from=YYYY-MM-DD | ?after={ISO}
GET https://placefull.ai/embed/v1/{location}/walkins?listing=slug,slug&book=slug&target=_blank
GET https://placefull.ai/embed/v1/themes/{clean|card}.css
GET https://placefull.ai/embed/v1/{location}/theme.css        (studio)
GET https://placefull.ai/embed/v1/{location}/custom.css       (the location's own stylesheet)

A fragment of a location with its own stylesheet answers with X-Placefull-Css: 1 (exposed to a cross-origin fetch): a page that prints fragments server-side reads it to know whether to print the custom.css link itself, after the theme's. custom.css answers an empty comment when nothing is saved, and 404 for a location we cannot find.

Walk-ins answers 204 when the bar must collapse (switch off, paused, no capacity), 404 with X-Placefull-Walkins: unknown_listing when listing= matches nothing (keep the fallback), and 200 with the fragment otherwise. Cache is 15 seconds.