Site integration: getPage(), admin bar, translations
Tapestry gives StudioCMS sites a Drupal-style admin bar on the public site for logged-in editors, plus a page loader that keeps draft pages private. Both work for every StudioCMS page type, not just Tapestry pages.
Decision record: ADR 0009.
What editors get
Section titled “What editors get”When a logged-in editor (or admin or owner) views any page on the site, a dark bar appears at the top:
| Item | Goes to |
|---|---|
| Dashboard | the StudioCMS dashboard |
| Content | the content list |
| Add page | the create-page screen |
| Status badge + page title | (status only): Draft (StudioCMS draft page), and for Tapestry pages Not published yet, Unpublished changes or Published |
| Preview draft / View published | Tapestry pages with unpublished changes: switch between the draft (?tapestry-preview) and the live version |
| Edit page | this page’s editor in the dashboard |
| your name, Log out | StudioCMS’s logout |
In the other direction, the Tapestry editor’s canvas bar has a View page button (next to reload) that opens the saved page in a new tab.
Anonymous visitors get nothing: no bar markup, no CSS, no JavaScript, and no extra database work.
Setup (two small changes to your site)
Section titled “Setup (two small changes to your site)”1. Load pages with getPage()
Section titled “1. Load pages with getPage()”---import { StudioCMSRenderer } from 'studiocms:renderer';import { getPage } from '@nascencestudio/tapestry/page';import Layout from '../layouts/Layout.astro';
const result = await getPage(Astro);if (!result) return new Response(null, { status: 404 });const { page, viewer, publishing } = result;---<Layout title={page.title} viewer={viewer} page={page} publishing={publishing}> <StudioCMSRenderer data={page} /></Layout>⚠️ Don’t use
SDKCoreJs.GET.page.bySlug()directly in public routes. It doesn’t filter drafts, so a draft page would be served to anyone who guesses its URL. (StudioCMS’s own@studiocms/blogroute has this problem as of 0.5.0.)
getPage(Astro, { slug? }):
- Uses
Astro.params.slug(or'index') unless you passslug. - Returns
nullif the page doesn’t exist, or if it’s a draft and the viewer isn’t an editor. Both should be a 404, so drafts don’t reveal that they exist. - Sets response headers:
Cache-Control: private, no-storeon every response rendered for an editor (it contains the admin bar, and possibly draft content).X-Robots-Tag: noindex, nofollowon drafts.
- Turns on canvas mode when an editor requests the page with
?tapestry-canvas(the editor’s visual canvas does this). It’s ignored for everyone else. The canvas only works on routes that usegetPage(). - For Tapestry pages, picks the version to render (ADR 0011):
visitors always get the published version, and a page that was never
published returns
nullfor them (404). Editors get the published version too, unless they add?tapestry-preview(or are in the canvas): then they get the unpublished draft, withnoindex. - Returns
{ page, viewer, publishing? }.vieweris{ isLoggedIn, permissionLevel, user }.publishing(Tapestry pages only) is{ status, publishedAt, previewing }, wherestatusis'unpublished' | 'published' | 'changed'. Pass it to the admin bar.
To check the session yourself (e.g. on a custom route), use
getViewer(Astro) from the same module.
2. Add the bar to your layout
Section titled “2. Add the bar to your layout”---import AdminBar from '@nascencestudio/tapestry/AdminBar.astro';const { viewer, page, publishing } = Astro.props;---<body> <AdminBar viewer={viewer} page={page} publishing={publishing} /> <slot /></body>Put it first in <body>; it’s position: sticky at the top. page is optional
(omit it on pages that aren’t StudioCMS pages, and the page-specific items disappear).
publishing is optional too; without it, Tapestry pages show only the basic badge.
3. Turn off StudioCMS’s corner menu (recommended)
Section titled “3. Turn off StudioCMS’s corner menu (recommended)”StudioCMS injects its own logged-in “Quick Tools” corner menu, an ~18 KB script, into every public page for every visitor. With the admin bar you don’t need it:
export default defineStudioCMSConfig({ features: { injectQuickActionsMenu: false, // must be under `features`; a top-level key is silently ignored }, // ...});In the playground this cut public-page JavaScript from 18,714 bytes to 174 bytes.
4. Markdown pages: add their CSS where it’s needed
Section titled “4. Markdown pages: add their CSS where it’s needed”The Markdown plugin is patched so it no longer adds its CSS to every page (known issue #33). If your site has Markdown pages, inline it in the layout for those pages only:
---import markdownCss from 'studiocms:md/styles-inline';const { page } = Astro.props; // from getPage()---<head> {page?.package === 'studiocms/markdown' && <style is:inline set:html={markdownCss} />}</head>5. A “not found” page of your own
Section titled “5. A “not found” page of your own”Without one, StudioCMS serves its own 404, which renders the whole dashboard layout
(~190 KB) for visitors (known issue #34). Add src/pages/404.astro with your site’s
layout, and stop StudioCMS from injecting its page (otherwise the routes collide):
features: { dashboardConfig: { inject404Route: false } },getPage() returning nothing → return new Response(null, { status: 404 }) then shows
your page.
6. Optional: translations
Section titled “6. Optional: translations”With tapestry({ languages: ['en', 'fr'] }), editors can create a French version of any
Tapestry page (its own page, slug fr/<slug>). getPage() then also returns the page’s
language and the translations the viewer may see:
---const { page, language, translations = [] } = result;const versions = translations.length > 1 ? translations : [];---<html lang={language?.code ?? 'en'}> <head> {versions.map((t) => <link rel="alternate" hreflang={t.lang} href={new URL(t.path, Astro.url).href} />)} </head> <body> <nav aria-label="Languages"> {versions.map((t) => t.current ? <span aria-current="page" lang={t.lang}>{t.label}</span> : <a href={t.path} hreflang={t.lang} lang={t.lang}>{t.label}</a>)} </nav>Your catch-all route ([...slug].astro) already serves /fr/about. Page links inside
Tapestry content follow the page’s language. See ADR 0032.
7. Optional: templates (shared header and footer)
Section titled “7. Optional: templates (shared header and footer)”Admins build headers and footers once (Dashboard → Plugins → Tapestry → Templates) and combine them into templates; editors choose a page’s template in the editor. Place the regions in your layout, around the page content:
---import Region from '@nascencestudio/tapestry/Region.astro';const { result } = Astro.props; // getPage()'s result---<body> <Region name="header" page={result} /> <slot /> <Region name="footer" page={result} /></body>Pages of other types (Markdown) get the default template’s header and footer. See ADR 0041.
8. Optional: page URL pattern
Section titled “8. Optional: page URL pattern”The editor’s “View page” button builds the URL from the page slug with
pageUrlPattern (default /{slug}; the index slug maps to /):
tapestry({ components, pageUrlPattern: '/{slug}' })Listing public pages
Section titled “Listing public pages”listPublicPages() (from @nascencestudio/tapestry/page) returns every page visitors can see
right now: never StudioCMS drafts, Tapestry pages only once they’re published (scheduled versions
once their time has come), with each page’s path, title, description, page type, last change,
language and visible translations. Use it for sitemaps, feeds or a site map page; the
tapestry-seo plugin builds its sitemap from it
(ADR 0039).
import { listPublicPages } from '@nascencestudio/tapestry/page';
const pages = await listPublicPages(); // [{ path: '/about', title: 'About', updatedAt: '…', translations: […] }, …]How it works
Section titled “How it works”- Session check. StudioCMS only resolves the logged-in user for dashboard
and API routes.
getViewer()calls StudioCMS’s ownUser.getUserData()(fromstudiocms:auth/lib), the same code the dashboard uses. With noauth_sessioncookie it returns immediately; with a cookie it validates the session (one database lookup). Invalid or forged cookies are treated as anonymous, and any error fails closed. - Rendering.
AdminBar.astrorenders only whenisEditor(viewer)is true. Its CSS is an inline<style>emitted together with the bar, so nothing reaches anonymous visitors.all: initialplus ID-anchored selectors (#tapestry-adminbar) keep site styles from leaking in. Site rules using IDs or!importantcould still override it. - Links come from StudioCMS’s route map, so a customized dashboard path still works.
Caveats
Section titled “Caveats”- Shared caches / CDNs. Editor responses are
private, no-store, so they’re never stored. Anonymous responses carry no cache headers from Tapestry; if a CDN caches them, editors may see the cached (bar-less) version until they bypass it. We don’t addVary: Cookie, because that would defeat caching for everyone. - Content Security Policy. The bar’s inline
<style>needsstyle-src 'unsafe-inline'or a nonce, for editors only. A nonce option can be added if needed. - The bar is shown to
editor,adminandownerlevels.visitoraccounts don’t see it and can’t see drafts.
Source: docs/admin-bar.md (view on GitHub)