{{ pageTitle }}

{{ recordCount }}
{{ addButtonLabel }}
🎹
{{ teachers.length }}
Teachers
{{ teachersMissingPhoto ? teachersMissingPhoto + ' need photo' : 'All have photos' }}
📅
{{ events.length }}
Events
{{ nextEventDays }}
📄
{{ pages.length }}
Pages
{{ pagesInNav }} in nav
🏛️
{{ boardMembers.length }}
Board Members
{{ boardMissingBio ? boardMissingBio + ' need bio' : 'All bios complete' }}
🔐
{{ adminUsers.length }}
Admin Users
Next Event
{{ nextEvent.daysAway === 0 ? '!' : nextEvent.daysAway }}
{{ nextEvent.daysAway === 0 ? 'Today' : nextEvent.daysAway === 1 ? 'day away' : 'days away' }}
{{ nextEvent.title }}
{{ nextEvent.date }}
{{ nextEvent.presenter }}
{{ nextEvent.location.split(',')[0] }}

Season complete — all events have passed.

{{ seasonLabel }}

No events added yet.

{{ e.shortDate }} {{ e.title }}
Needs Attention
{{ teachersMissingPhoto }} teacher{{ teachersMissingPhoto !== 1 ? 's' : '' }} missing photo All teacher photos uploaded
{{ teachersMissingAreas }} teacher{{ teachersMissingAreas !== 1 ? 's' : '' }} missing service areas All teachers have service areas
{{ boardMissingPhoto }} board member{{ boardMissingPhoto !== 1 ? 's' : '' }} missing photo All board photos uploaded
{{ boardMissingBio }} board member{{ boardMissingBio !== 1 ? 's' : '' }} missing bio All board bios complete
Quick Actions
Add New Teacher Add New Event Add New Page View API Endpoints
{{ props.row.lastName }}, {{ props.row.firstName }} {{ props.row.email }} {{ props.row.phone }} {{ (Array.isArray(props.row.areasServed) ? props.row.areasServed.join(', ') : props.row.areasServed) || '—' }}
{{ props.row.name }} {{ props.row.title || '—' }}
{{ showPastEvents ? 'Hide Past Events' : 'Show Past Events' }}
{{ props.row.date }} {{ props.row.title }}
{{ props.row.time }} · {{ props.row.location || 'TBD' }}
{{ props.row.presenter || '—' }}
Saving… ★ = Featured  ·  Drag rows to reorder

{{ pageSearch ? 'No pages match your search.' : 'No pages yet. Click "Add Page" to get started.' }}

/
Sections {{ activePage.sections.length }}

No sections yet

Add one below or use AI to build the page

{{ sectionTypeLabel(section.type) }} {{ sectionPreview(section) }}
{{ si + 1 }}

Select a section to edit

Click any section on the left to open its editor here

Add a Section
Choose the type of content block to add to your page
Hero Banner
Full-width top section with headline, supporting text, image, and a call-to-action button
Text Content
Rich text editor for paragraphs, headings, bold text, lists, and links
Image + Text
Photo or illustration side-by-side with a text block — great for visual storytelling
Card Grid
2 or 3-column grid of highlight cards, each with an icon, title, and description
Link List
Organized list of links, documents, PDFs, or external resources with descriptions
CTA Banner
Prominent call-to-action with a bold headline, body paragraph, and a button
AI Page Assistant AI
{{ props.row.email }} {{ props.row.displayName || '—' }} {{ props.row.role || 'admin' }} {{ props.row.createdAt || '—' }}
API Reference IPTA Headless CMS · v2

The IPTA CMS exposes a REST API served via Firebase Cloud Functions. Use it to build a fully custom public website — fetch teachers, events, pages, and navigation without any CMS login. All data is JSON.

Base URL
https://ipta-cms.web.app
Content-Type
application/json
Authentication: All GET endpoints are publicly accessible — no API key or login required. Use them freely from any public website. POST, PUT, and DELETE operations require a Firebase Auth ID token passed as Authorization: Bearer <token>.
Teachers Public Read

The teacher directory. Each teacher record includes contact info, the geographic service areas they cover, and a photo URL. Use the area or zip query params to power a "Find a Teacher Near Me" feature.

GET/api/teachersReturns all teachers as an array
GET/api/teachers/:idReturns a single teacher object
GET/api/teachers?area=CarmelFilter by service area name
GET/api/teachers?zip=46032Filter by zip code served
POST/api/teachersAuth required
PUT/api/teachers/:idAuth required
DEL/api/teachers/:idAuth required
Teacher Object Fields
FieldTypeDescription
idstringFirestore document ID. Unique identifier for this teacher.
firstNamestringTeacher's first name.
lastNamestringTeacher's last name.
emailstringContact email address.
phonestringContact phone number.
websitestringTeacher's personal or studio website URL. Always includes https://.
photostringFirebase Storage download URL for their headshot. May be empty string if not uploaded.
areasServedstring[]Array of geographic area names they serve (e.g. ["Carmel", "Fishers"]).
zipcodesServedstring[]Array of zip code strings automatically derived from areasServed. Use this for zip-based lookups.
createdAtstringISO 8601 timestamp of when the record was created.
updatedAtstringISO 8601 timestamp of last update.
{ "id": "abc123", "firstName": "Caroline", "lastName": "Ahn", "email": "caroline@example.com", "phone": "317-555-1234", "website": "https://carolineahn.com", "photo": "https://firebasestorage.googleapis.com/...", "areasServed": ["Carmel", "Fishers"], "zipcodesServed": ["46032", "46033", "46037", "46038"], "createdAt": "2024-09-01T14:22:00.000Z", "updatedAt": "2025-01-15T09:04:00.000Z" }
Events Public Read

Program events and workshops. Dates are stored as human-readable strings (e.g. "Friday, April 12, 2024") rather than timestamps, so display them as-is. Use ?upcoming=true to get events sorted with the most recently added first.

GET/api/eventsAll events, unordered
GET/api/events?upcoming=trueAll events, sorted by createdAt desc
GET/api/events/:idSingle event object
POST/api/eventsAuth required
PUT/api/events/:idAuth required
DEL/api/events/:idAuth required
Event Object Fields
FieldTypeDescription
idstringFirestore document ID.
titlestringEvent title or name.
datestringHuman-readable date string, e.g. "Friday, April 12, 2024". Parse with new Date(event.date) for sorting.
timestringTime range string, e.g. "10:00 AM – 11:30 AM".
locationstringVenue name and/or address.
presenterstringName of the presenter or performer.
descriptionstringPlain text description of the event.
biostringPresenter biography, plain text.
createdAtstringISO 8601 timestamp.
{ "id": "evt456", "title": "Early Childhood Piano Workshop", "date": "Saturday, March 8, 2025", "time": "9:00 AM - 12:00 PM", "location": "Butler University, Jordan Hall", "presenter": "Dr. Susan Payne", "description": "An interactive workshop exploring...", "bio": "Dr. Payne is a professor of music education...", "createdAt": "2025-01-10T16:30:00.000Z" }
Pages & Page Sections Public Read

CMS-managed pages. Each page has a slug (the URL path) and a sections array that defines the full content of the page. Your public site should fetch a page by slug, then render each section in order based on its type.

GET/api/pagesAll pages (includes sections)
GET/api/pages?slug=aboutGet page by URL slug — use this for routing
GET/api/pages/:idGet page by Firestore ID
POST/api/pagesAuth required
PUT/api/pages/:idAuth required
DEL/api/pages/:idAuth required
Tip for routing: Fetch /api/pages?slug=about returns an array — take the first element. To build a site router, fetch all pages on load, index them by slug, then look up the current URL path to find the matching page.
Page Object Fields
FieldTypeDescription
idstringFirestore document ID.
titlestringPage title, for use in <title> tags and headings.
slugstringURL-safe identifier. Match against the current URL path to load the right page (e.g. "about", "membership").
sectionsSection[]Ordered array of content sections. Render these in order. See section types below.
updatedAtstringISO 8601 timestamp — useful for cache invalidation.
Section Object (universal fields)
FieldTypeDescription
idstringUnique section ID within the page.
typestringSection type — determines which renderer to use. See types below.
ordernumber1-based render order. Sections are already sorted in the API response.
bgStylestringBackground color hint: "" = white, "grey" = light warm grey (#f0eeea), "blue" = brand blue (#5cc0ed), "dark" = dark navy (#1a1a2e). Apply to the section's outer wrapper.
contentobjectSection-specific content fields. Shape depends on type. See below.
Section Types & Content Schemas
hero — Full-width banner section
content fieldTypeDescription
headlinestringLarge primary heading text.
subtextstringSupporting paragraph beneath the headline.
ctaTextstringCall-to-action button label. Empty string if no button.
ctaUrlstringButton link target. Can be relative (/about) or absolute.
imageUrlstringFirebase Storage URL for the hero background/side image. May be empty.
imageAltstringAlt text for the image.
imageAnchorstringCSS object-position value controlling the image focal point. One of: top left, top center, top right, center left, center center, center right, bottom left, bottom center, bottom right. Defaults to center center when absent.

Render the image with object-fit: cover; object-position: {imageAnchor} so the focal point stays visible at any container size.

text — Rich text content block
content fieldTypeDescription
markdownstringHTML string. Render with innerHTML or v-html. Contains <h2>, <h3>, <p>, <strong>, <em>, <ul>/<li>, <a> tags.
Despite the field name, the value is HTML, not Markdown syntax. Always render it as raw HTML. Sanitize with DOMPurify if accepting untrusted content.
image-text — Side-by-side image and rich text
content fieldTypeDescription
markdownstringHTML string — same as text section above. Render with innerHTML.
imageUrlstringFirebase Storage URL for the image.
imageAltstringAlt text for the image.
imagePositionstring"left" or "right" — which side the image appears on in a two-column layout.
card-grid — Grid of highlight cards
content fieldTypeDescription
headlinestringOptional section heading displayed above the grid.
columnsnumber2 or 3 — number of columns in the grid.
showIconsbooleanWhether card emoji icons should be displayed.
cardsCard[]Array of card objects (see below).
Card Object
fieldTypeDescription
titlestringCard heading.
textstringCard body text (plain text).
iconstringEmoji icon character, e.g. "🎹". May be empty.
imageUrlstringOptional card image URL.
ctaTextstringOptional button label.
ctaUrlstringOptional button link.
link-list — Organized list of links or downloads
content fieldTypeDescription
headlinestringOptional section heading.
linksLink[]Array of link objects.
Link Object
fieldTypeDescription
labelstringVisible link text.
urlstringLink destination. May be a relative path, absolute URL, or asset path like /assets/docs/file.pdf.
descriptionstringOptional short description shown beneath the link.
iconstringOptional MDI icon name (without the mdi- prefix), e.g. "download".
cta-banner — Call-to-action banner
content fieldTypeDescription
headlinestringBold banner heading.
bodystringSupporting paragraph text (plain text).
ctaTextstringButton label.
ctaUrlstringButton link target.
Full Page Response Example
{ "id": "page789", "title": "About IPTA", "slug": "about", "updatedAt": "2025-03-01T12:00:00.000Z", "sections": [ { "id": "s1", "type": "hero", "order": 1, "bgStyle": "blue", "content": { "headline": "Indiana Piano Teachers Association", "subtext": "Supporting piano education across Indiana since 1935.", "ctaText": "Find a Teacher", "ctaUrl": "/teachers", "imageUrl": "https://firebasestorage.googleapis.com/...", "imageAlt": "Piano keys close-up" } }, { "id": "s2", "type": "text", "order": 2, "bgStyle": "", "content": { "markdown": "<h2>Our Mission</h2><p>IPTA exists to advance piano...</p>" } } ] }
Site Config & Navigation Public Read

Returns the global site configuration, including the navigation structure. Use this to build your site's nav menu — it tells you which pages are "featured" (in the primary nav), in what order, and what their URL slugs are.

GET/api/site-configReturns site config with nav array
PUT/api/site-configAuth required
Nav Item Fields (within the nav array)
FieldTypeDescription
pageIdstringThe page's Firestore ID. Use to cross-reference with the /api/pages response.
slugstringURL slug for this nav item — use as the link href (e.g. /about).
featuredbooleanIf true, this page appears in the primary navigation. If false, it's a secondary/footer page.
navOrdernumberSort order for the nav. Sort ascending by this field to render the menu in the correct sequence.
{ "id": "main", "nav": [ { "pageId": "page789", "slug": "about", "featured": true, "navOrder": 1 }, { "pageId": "page012", "slug": "membership", "featured": true, "navOrder": 2 }, { "pageId": "page345", "slug": "competition", "featured": true, "navOrder": 3 }, { "pageId": "page678", "slug": "resources", "featured": false, "navOrder": 4 } ] }
Board Members Public Read

IPTA board member directory. Use to build an "About / Leadership" page section.

GET/api/board-membersReturns all board members
GET/api/board-members/:idSingle board member
POST/api/board-membersAuth required
PUT/api/board-members/:idAuth required
DEL/api/board-members/:idAuth required
Board Member Object Fields
FieldTypeDescription
idstringFirestore document ID.
namestringFull name.
titlestringBoard title or role (e.g. "President", "Treasurer").
biostringShort biography, plain text.
photostringFirebase Storage URL for their headshot. May be empty.
Quick Start: Fetching Data in JavaScript

No library or authentication needed for read operations. Here's a minimal example of loading a page by slug and rendering its sections:

// Fetch the navigation to build your menu const { nav } = await fetch('https://ipta-cms.web.app/api/site-config').then(r => r.json()); const featuredNav = nav.filter(n => n.featured).sort((a, b) => a.navOrder - b.navOrder); // Fetch a page by its URL slug const slug = window.location.pathname.replace('/', '') || 'home'; const pages = await fetch(`https://ipta-cms.web.app/api/pages?slug=${slug}`).then(r => r.json()); const page = pages[0]; // slug is unique // Render each section based on its type page.sections.forEach(section => { switch (section.type) { case 'hero': renderHero(section.content, section.bgStyle); break; case 'text': renderText(section.content, section.bgStyle); break; case 'image-text': renderImageText(section.content, section.bgStyle); break; case 'card-grid': renderCardGrid(section.content, section.bgStyle); break; case 'link-list': renderLinkList(section.content, section.bgStyle); break; case 'cta-banner': renderCtaBanner(section.content, section.bgStyle); break; } }); // bgStyle → background color map const bgColors = { '': '#ffffff', grey: '#f0eeea', blue: '#5cc0ed', dark: '#1a1a2e' };