Freeframe

Documentation

Summit

v1.0.1Updated Nov 11, 2026A bold conference website with speakers, a starrable schedule and ticket checkout.

Overview#

Summit is a vibrant website for a conference or event: a live countdown, speakers with pop-up profiles, a three-day schedule with tracks and a personal "My schedule", ticket selection with a team discount, the venue and sponsors. The demo event is Summit 2027, a fictional design, engineering and AI conference in Bellevue, WA (April 14–16, 2027) with 12 speakers and 35 sessions. Speakers, sessions, days and prices live in typed data files. This is the Nuxt version (1.0.0): Nuxt 4, Vue 3, TypeScript and Tailwind CSS v4, generated as a static site.

Live demo: freeframe.dev/templates/summit

Quick start#

Requirements: Node.js 24 and pnpm (corepack enable pnpm).

sh
pnpm install
pnpm dev          # http://localhost:3000

Add ?now=2027-04-15T10:45 to any page's URL to see it as if the event were live (see Live schedule state).

Command What it does
pnpm dev Development server at http://localhost:3000
pnpm generate Static site in .output/public/
pnpm preview Serve the last build locally
pnpm build Node.js server build in .output/ (pages still prerendered)
pnpm typecheck nuxt prepare, then vue-tsc
pnpm lint ESLint (the flat config from @nuxt/eslint)
pnpm build:demo Build for /demos/nuxt/summit into demo/ (the freeframe.dev demo; you can delete it)

Pages#

Route File Notable sections and features
/ app/pages/index.vue Hero with live countdown, topics marquee, highlights, featured speakers, count-up stats, "A day at Summit", partner wall, testimonial, venue preview, newsletter sign-up
/speakers app/pages/speakers.vue Track filter and pop-up speaker profiles (SpeakerGrid.vue)
/speaker?id=<id> app/pages/speaker.vue One speaker: portrait, bio, links, session cards, "More speakers"
/schedule app/pages/schedule.vue Day tabs, track filter, stars and "My schedule", "On now" / "Up next" during the event (ScheduleBoard.vue)
/tickets app/pages/tickets.vue Four passes with early-bird prices, steppers, order summary with team discount, checkout (TicketShop.vue), comparison table, FAQ
/venue app/pages/venue.vue Venue facts, illustrated map, travel, partner hotels, accessibility
/sponsors app/pages/sponsors.vue Sponsor tiers, packages, sponsorship inquiry form
404 app/error.vue Not-found page, generated as 404.html

Project structure#

summit/
├─ app/
│  ├─ app.vue                     <NuxtLayout><NuxtPage /></NuxtLayout>
│  ├─ error.vue                   the 404 page
│  ├─ router.options.ts           writes query strings like the HTML version (?id=mei-tanaka)
│  ├─ layouts/default.vue         skip link, header, page, footer, toast, back-to-top button; sets the demo-date shift
│  ├─ pages/                      one .vue file per page (see above)
│  ├─ components/
│  │  ├─ SiteHeader.vue           logo, main menu, tickets button, full-screen phone menu
│  │  ├─ SiteFooter.vue           footer links, contact and copyright year
│  │  ├─ PageHero.vue             the title band of the inner pages
│  │  ├─ EventCountdown.vue       days, hours, minutes and seconds to the doors opening
│  │  ├─ CountUp.vue              numbers that count up when scrolled into view
│  │  ├─ SpeakerGrid.vue          track filter, speaker tiles and the speaker <dialog>
│  │  ├─ SpeakerTile.vue          a speaker's portrait, track, name and role (link or button)
│  │  ├─ SessionCard.vue          one session: time, room, tags, title, speakers, star, live badge
│  │  ├─ ScheduleBoard.vue        day tabs, track filter, stars, My schedule, live highlights
│  │  ├─ TicketShop.vue           ticket cards, order summary, checkout and confirmation
│  │  ├─ NewsletterForm.vue       home page sign-up
│  │  ├─ SponsorForm.vue          sponsorship inquiry
│  │  ├─ FieldError.vue           a field's inline error message
│  │  ├─ TrackTag.vue, SponsorLogo.vue, AppIcon.vue, DatedText.vue
│  │  ├─ VenueArt.vue, VenueMap.vue   the venue illustration and map (inline SVG)
│  │  └─ BackToTop.vue            floating "Back to top" button
│  ├─ composables/
│  │  ├─ useEdition.ts            useEdition(), useEventNow(), useEarlyBird(): dates, years and the clock
│  │  ├─ useFormValidation.ts     inline validation for the three forms
│  │  ├─ useCurrentPage.ts        aria-current for menu links
│  │  └─ usePageSeo.ts            title, description, canonical and Open Graph tags
│  ├─ data/
│  │  ├─ summit.ts                tracks, days, speakers, sessions, ticket prices, early-bird date, team discount
│  │  ├─ tickets.ts               ticket cards, comparison table, FAQ, checkout countries
│  │  ├─ sponsors.ts              sponsor tiers, home page partners, sponsorship packages
│  │  ├─ venue.ts                 venue facts, travel, hotels, accessibility list
│  │  ├─ home.ts                  marquee topics, featured speakers, home agenda, countdown target
│  │  ├─ nav.ts                   header, phone menu and footer links
│  │  └─ icons.ts                 the line icons
│  ├─ utils/                      event times, demo dates, safeStorage, toast, dollars() and makeRef()
│  ├─ plugins/                    scroll reveals, the URL hash, reproducible builds
│  └─ assets/
│     ├─ css/main.css             Tailwind theme (colors, fonts) and component classes
│     └─ fonts/                   Inter and Space Grotesk (woff2) with their OFL license texts
├─ public/img/                    speakers/<id>.svg portraits, favicon, og-image.png (and its SVG source)
├─ nuxt.config.ts                 base path, head defaults, SITE_URL, prerendered routes, Tailwind
├─ eslint.config.mjs, tsconfig.json
├─ package.json, pnpm-workspace.yaml, pnpm-lock.yaml
└─ README.md, DOCS.md, CHANGELOG.md

Customizing#

Brand and colors#

The palette and fonts are tokens in the @theme block of app/assets/css/main.css:

Token Used for
--color-night, --color-night-2, --color-panel Dark backgrounds and cards
--color-edge Borders
--color-ink, --color-haze Text and secondary text
--color-violet, --color-magenta, --color-flame The gradient used for headlines, buttons and highlights
--color-lime Focus rings, "On now" badges, selected items
--color-sky "Up next" badges

Change the three gradient colors to restyle the whole event; pnpm dev picks it up instantly:

css
@theme {
  --color-violet: #2563eb;
  --color-magenta: #06b6d4;
  --color-flame: #22c55e;
}

Track colors (.tag-design, .tag-engineering, .tag-ai, ...) are component classes further down in main.css. If you change --color-night, also change the theme-color meta tag in nuxt.config.ts.

Typography#

Headings use Space Grotesk (--font-display) and text uses Inter (--font-sans). Both are self-hosted: the woff2 files are in app/assets/fonts/ and declared with @font-face at the top of main.css. To use other fonts, put their files in app/assets/fonts/, change the @font-face rules and the two tokens.

Content#

Page text is in each app/pages/*.vue file. Event data lives in app/data/summit.ts, which is typed, so pnpm typecheck catches a misspelt track or a missing field:

Export Contents
TRACKS Track ids and labels: design, engineering, ai, leadership, general
DAYS { id: "wed", date: "2027-04-14", label: "Wed, Apr 14", title: "Workshop day" } (Day)
SPEAKERS { id, name, role, company, city, track, bio, twitter, site } (Speaker)
SESSIONS { id, day, start, end, title, speakers: [ids], track, room, level, kind } (Session)
EARLY_BIRD_ENDS When early-bird pricing ends: "2026-12-31T23:59:59-08:00"
KEEP_DEMO_CURRENT true in the demo: once the edition is over, every date moves on a year (see Demo dates)
TEAM_DISCOUNT { minTickets: 5, percent: 15 }
TICKET_PRICES { id, name, price, early } per ticket (TicketPrice)

Everything is rendered from these: the speaker tiles and dialog, the speaker pages, every schedule (the full schedule, a speaker's sessions and the home page agenda in AGENDA in app/data/home.ts), and the ticket prices. Adding a session is one entry in SESSIONS; it appears on its day, with a star, on its speakers' pages and in their dialogs:

ts
{ id: "s36", day: "thu", start: "16:00", end: "16:40", title: "Agents in the wild", speakers: ["sam-wu"], track: "ai", room: "Hall B", level: "Intermediate", kind: "talk" },

start and end are 24-hour local event times and show on a 12-hour clock ("4:00 – 4:40 PM"). kind is talk, keynote, workshop, panel (each with its tag), break or social (no star, dashed card). Keep session ids stable: starred sessions are saved by id.

Adding a speaker is an entry in SPEAKERS plus a square portrait at public/img/speakers/<id>.svg. The home page shows the speakers listed in FEATURED_SPEAKERS in app/data/home.ts.

Ticket cards (name, description, features, "Most popular") are in TICKETS in app/data/tickets.ts, with the comparison table (COMPARE), the FAQ (TICKETS_FAQ) and the checkout's COUNTRIES. Sponsors and packages are in app/data/sponsors.ts; hotels, travel and the accessibility list in app/data/venue.ts.

Demo dates#

The demo is written for April 14–16, 2027. While KEEP_DEMO_CURRENT in app/data/summit.ts is true and that edition is over (the event-local date is after the last of DAYS), every date moves to the same week of the next year, by whole weeks so weekdays never change (Wed, April 14, 2027 becomes Wed, April 12, 2028), so the countdown, ticket prices, schedule and "live" state always point at an upcoming conference. The edition year follows.

The shift is worked out in app/utils/edition.ts and set by app/layouts/default.vue once the page has mounted (the generated HTML always shows the written dates). Components read it with useEdition(), which gives the edition's year, dates ("April 14–16, 2027"), yy ("’27"), yyPlain ("27"), label(day), date(iso, format), iso() for event times, text() for text containing the year, and earlyBirdEnds:

html
<p>© <span data-edition="year">{{ ed.year }}</span> Summit Conference LLC.</p>
<span data-date="2027-03-01" data-date-format="dm">{{ ed.date("2027-03-01", "dm") }}</span>

Formats are dmy ("April 1, 2027", the default), dm ("March 1") and dmon ("Dec 31"). In TICKETS_FAQ, write a date as [2027-02-01] and DatedText.vue shows it the same way. Before the edition is over nothing changes, so a preview such as ?now=2027-04-15T10:45 still shows the live schedule. Set KEEP_DEMO_CURRENT to false once you enter your real dates. The "Early-bird tickets on sale" pill and the first sentence of the tickets intro disappear once early-bird pricing has ended (useEarlyBird()), whatever the setting.

Time zone#

Event times are Pacific daylight time (UTC-7 in April). eventTime() in app/utils/event.ts appends EVENT_OFFSET (-07:00) to every time, and eventDate() uses the same offset for the schedule's "today". For an event elsewhere, change both (for example -04:00 and -4 * 3600 * 1000 for New York in summer), plus the note at the top of app/data/summit.ts and the schedule intro.

Images and artwork#

Speaker portraits are illustrated SVGs in public/img/speakers/<speaker-id>.svg (square). Replace them with square photos using the same names, or change the .svg in SpeakerTile.vue, SessionCard.vue, SpeakerGrid.vue and speaker.vue if you use .jpg. Files in public/ are written with the base path:

html
<img :src="`${app.baseURL}img/speakers/${speaker.id}.svg`" alt="" width="400" height="400">

The venue illustration (VenueArt.vue) and map (VenueMap.vue) are inline SVG; sponsor logos are placeholder wordmarks (SponsorLogo.vue, a shape from LogoShape plus the name). The favicon is public/img/favicon.svg; the social image is public/img/og-image.png (1200×630), exported from public/img/og-image.svg.

Menu links are in app/data/nav.ts: NAV (header), MOBILE_NAV (the phone menu, which adds Home and Tickets), FOOTER_EVENT and FOOTER_INFO. SiteHeader.vue marks the current section with aria-current="page" through useCurrentPage() (a speaker's page counts as Speakers). To add a page, create app/pages/workshops.vue:

html
<script setup lang="ts">
usePageSeo({ title: "Workshops — Summit 2027", description: "Hands-on workshops on April 14." });
</script>

<template>
  <main id="main">
    <PageHero eyebrow="Workshops">Hands-on <span class="gradient-text">workshops</span><template #intro>Six small groups.</template></PageHero>
  </main>
</template>

Then add "/workshops" to routes in nuxt.config.ts (every page is listed there to be prerendered) and the link to NAV. Each page has its own <main id="main">, the skip link's target. Internal links are <NuxtLink>s; links that must never be marked as the current page pass :aria-current="undefined".

Features#

Every feature reads its content from app/data/ and keeps the HTML version's data-* attributes, so CSS and tests can find the same elements.

Countdown#

EventCountdown.vue (on the home page, with DOORS_OPEN from app/data/home.ts) shows days, hours, minutes and seconds until the doors open, ticking every second and stopping at zero. It has role="timer". The generated HTML shows zeros until the browser fills it in.

Speakers filter and pop-up profiles#

On /speakers, the track buttons (aria-pressed) show the speakers of one track and update the count, which is announced politely. Clicking a speaker opens a native <dialog> with their portrait, role, bio, every session they appear in and a Full profile link. Esc, the close button or a click outside the box closes it, and focus returns to the speaker you opened.

Speaker pages#

/speaker?id=<speaker-id> (any letter case: ?id=Mei-Tanaka works) shows that speaker, with the same reveal animation, and puts their name in the page title. The generated speaker.html is written for the first speaker in SPEAKERS; the id is read once the page has loaded, and a missing or unknown id (or one like constructor) keeps the first speaker. "More speakers" lists four others. Following a link to another speaker shows them from the top of the page.

Schedule, tracks and My schedule#

/schedule has a tablist of days plus My schedule, with Left/Right, Home and End keys. The chosen day is kept in the URL hash (/schedule#fri), so a reload or a shared link opens it; during the event the current day (in event time) opens automatically.

  • Track buttons show one track; breaks and socials (the general track) stay visible so each day still reads as a timeline. A message appears when a day has nothing in that track.
  • Star buttons (aria-pressed) add sessions to My schedule, grouped by day, with a count on the tab and a toast. Their label says what a press does ("Add “…” to my schedule", or "Remove “…” from my schedule" once starred). Stars are saved in the browser (see Saved data).

Live schedule state#

Every minute the schedule marks the session that is on now (.is-now, "● On now") and the next one within three hours (.is-next, "Up next"), and shows the "Live now" note. Preview it any time with ?now=: /schedule?now=2027-04-15T10:45. The same parameter drives the countdown, early-bird pricing and the demo dates (eventNow() in app/utils/event.ts).

Tickets and order summary#

Each ticket card on /tickets has −/+ steppers (0–20 each). Until EARLY_BIRD_ENDS the early price is used and the full price is shown struck through with an "Early bird" badge; afterwards they go. The summary lists each line, the subtotal, the team discount (15% from five tickets, from TEAM_DISCOUNT), a hint showing how many more tickets unlock it, and the total. Continue to checkout opens the checkout form below. Prices come from TICKET_PRICES, matched to the cards in TICKETS by id:

ts
export const TICKET_PRICES: TicketPrice[] = [
  {id:"workshop",name:"Workshop day",price:349,early:299},
  {id:"conference",name:"Conference pass",price:799,early:599},
  // ...
];

When early equals price the struck-through price and badge are left out. A card without a price is not shown. Update the comparison table (COMPARE) when you change what a pass includes.

Scroll reveals and count-up stats#

Elements with reveal fade in when scrolled into view (app/plugins/reveal.client.ts), together with everything above them. CountUp.vue counts its number up once when visible. The gradient blobs drift and the topics marquee scrolls (it pauses on hover). All motion stops with prefers-reduced-motion, and content stays visible without JavaScript (they are only hidden once a script in nuxt.config.ts has added the js class to <html>, before the page paints).

Mobile menu#

Below 1024px the menu button opens the full-screen #mobile-menu: it locks page scrolling, makes the header solid, moves focus to the first link, updates aria-expanded and its label, and closes with Esc (focus returns to the button), on a link, or when the window reaches the lg breakpoint.

Back to top button#

BackToTop.vue (added to the end of <body> by the layout) appears after you scroll down about one screen. It scrolls back up (instantly with reduced motion), moves keyboard focus to #main and lifts above the toast. Mark your own fixed UI with data-back-to-top-avoid (lift above it) or data-back-to-top-hide (hide while covering it). To remove it, delete <BackToTop /> from the layout and the .back-to-top rules at the end of main.css.

Forms#

Form (data-form) Component Fields
newsletter NewsletterForm.vue (home) email
checkout TicketShop.vue (tickets) first, last, email, company (optional), country, payment (card or invoice), terms
sponsor SponsorForm.vue (sponsors) company, name, email, tier, message (at least 20 characters)

useFormValidation() checks required fields, email format, minimum lengths and required checkboxes with the browser's own rules. Errors appear under each field (<FieldError>, id="<form>-<field>-error"), fields get aria-invalid and aria-describedby, and the first problem is focused. A filled-in field is checked when you leave it, and an error updates as you type. On success the newsletter and sponsor forms show their message (the sponsor one with a reference like SU-7K2Q from makeRef()), and the ticket page shows an "Order placed" confirmation with the reference and order total.

The forms don't send anything until you connect them. Each form's submit function has a "To send for real" comment. For example, in SponsorForm.vue, make submit async and post the data to a form service such as Formspree before showing the message:

ts
async function submit(e: Event) {
  const form = e.target as HTMLFormElement;
  if (!v.validate(form)) return;
  const res = await fetch("https://formspree.io/f/your-id", {
    method: "POST",
    body: new FormData(form),
    headers: { Accept: "application/json" },
  });
  if (!res.ok) return; // show an error message here
  reference.value = makeRef("SU");
  done.value = true;
  form.reset();
}

To run your own server code instead, add a server route such as server/api/sponsor.post.ts, deploy with pnpm build (a Node.js server) and post to /api/sponsor.

For real ticket sales, don't collect payment details in this form. Use a ticketing or payment provider (Tito, Eventbrite, Stripe Checkout...): replace the checkout section in TicketShop.vue with their embed, or send the quantities (qty) to their checkout from toCheckout(). The confirmation texts (deck within two business days, a payment link by email) are examples; change them to match what really happens.

Saved data (localStorage)#

Key Shape Purpose
summit-2027-stars Array of session ids, e.g. ["s2", "s14"] Sessions starred for My schedule

ScheduleBoard.vue reads it once the page has loaded, through safeStorage (app/utils/safeStorage.ts). Anything that isn't an array of known, unique session ids is dropped and storage is repaired; unreadable JSON counts as no stars. If storage is blocked, starring still works for the current page. For a new edition, change STAR_KEY in ScheduleBoard.vue (for example summit-2028-stars) so old stars don't carry over. Reset with localStorage.removeItem("summit-2027-stars") in the browser console.

SEO and sharing#

  • Each page calls usePageSeo({ title, description }), which sets <title>, the meta description, og:title, og:description, og:type, og:site_name, og:image, a summary_large_image Twitter card and a canonical link. The year in the title follows the demo dates.

  • The canonical URL, og:url and the absolute og:image URL start with runtimeConfig.public.siteUrl in nuxt.config.ts, which reads SITE_URL (default https://summit.example). Set it when you build:

    sh
    SITE_URL=https://example.com pnpm generate
  • /speaker puts the speaker from ?id= in the title in the browser. If speaker pages matter for search, add a page per speaker (app/pages/speakers/[id].vue) and list each route in routes in nuxt.config.ts.

  • The 404 page passes noindex: <meta name="robots" content="noindex"> and no canonical URL.

  • Consider Event structured data (name, dates, location, offers) as JSON-LD with useHead() on the home page, and a sitemap.

Accessibility#

Built in:

  • Skip link, labeled landmarks, one <h1> per page and visible lime focus rings.
  • Day tabs follow the tabs pattern (roving tabindex, arrow keys); filters and stars use aria-pressed.
  • Speaker profiles use a native <dialog> with focus return; the countdown has role="timer".
  • Track tags use text labels, not color alone; decorative icons are hidden from screen readers.
  • The toast and the speaker count are announced with aria-live; form errors are linked with aria-describedby.
  • Motion (reveals, drifting gradients, the marquee, counters, smooth scrolling) respects prefers-reduced-motion.

When editing, keep gradient text large and on dark backgrounds for contrast, label new form fields, and give portraits empty alt text only where the speaker's name is already next to them.

Building and deploying#

sh
pnpm generate     # static site in .output/public/
pnpm preview      # serve it locally

nuxt generate prerenders every route listed in routes in nuxt.config.ts as <page>.html (schedule.html, linked as /schedule), plus 404.html from app/error.vue. Next to each page is a folder with its _payload.json, which Nuxt loads when you move between pages. Upload .output/public/ to any static host (Netlify, Cloudflare Pages, GitHub Pages, S3, Vercel). Hosts that don't map /schedule to schedule.html on their own need that rewrite (Vercel: "cleanUrls": true); most serve 404.html for unknown URLs.

To serve the site from a sub-folder, set DEMO_BASE_PATH, which becomes Nuxt's app.baseURL:

sh
DEMO_BASE_PATH=/summit pnpm generate

pnpm build makes a Node.js server build instead (run it with node .output/server/index.mjs); the pages are still prerendered, and you can add server routes. demo/ is the freeframe.dev demo build and can be deleted.

TypeScript setup#

pnpm typecheck runs vue-tsc against the configs Nuxt writes to .nuxt/ (tsconfig.json references them). typescript is aliased to @typescript/typescript6, the TypeScript 6 API that vue-tsc and typescript-eslint load; replace it with typescript@7 once they support it.

Troubleshooting#

  • A starred session disappears after reload. Its id must still exist in SESSIONS; unknown ids are dropped.
  • A new page shows the 404 page after pnpm generate. Add its route to routes in nuxt.config.ts.
  • A ticket card disappeared. Its id in TICKETS must match an id in TICKET_PRICES.
  • Times are an hour or more off. Change EVENT_OFFSET and the offset in eventDate() in app/utils/event.ts to your event's offset.
  • The dates moved to next year. KEEP_DEMO_CURRENT is true and the edition in DAYS is over; set it to false (or enter your next dates).
  • /schedule shows a 404 on my host. The host doesn't map extensionless URLs to .html files; add that rewrite (for example Vercel's "cleanUrls": true).
  • Forms "send" but nothing arrives. Connect them as described in Forms.
  • pnpm lint fails right after cloning. It needs .nuxt/, which pnpm install creates (nuxt prepare).

License#

Freeframe Commercial License: use it in unlimited websites and apps for yourself and your clients; it may not be resold or redistributed as a template. The full terms, with your licensee details, are in LICENSE.txt and at https://freeframe.dev/license#commercial. Third-party components keep their own licenses. Space Grotesk and Inter are licensed under the SIL Open Font License. Summit 2027, its speakers and sponsors are fictional.

Search documentation

Search the guides and every template's documentation