Freeframe

Documentation

Summit

v1.0.3Updated Oct 17, 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 Copenhagen (14–16 April 2027) with 12 speakers and 35 sessions. This is the Astro 7 + TypeScript + Tailwind CSS v4 version, version 1.0.2. It builds to static HTML files with no UI framework and can be hosted anywhere. See it live at freeframe.dev/templates/summit.

Quick start#

Requirements: Node.js 24 or newer and pnpm (npm works too).

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

With npm: npm install, then npm run dev. Add ?now=2027-04-15T10:45 to any page's URL to preview it as if the event were live (see Live schedule state).

Command What it does
pnpm dev Development server at http://localhost:4321 with hot reload
pnpm build Static site in dist/
pnpm preview Serve dist/ locally after a build
pnpm check astro check: type-checks .astro files and TypeScript
pnpm typecheck Generate Astro's types, then type-check with tsc (TypeScript 7)
pnpm lint ESLint (TypeScript and Astro rules)
pnpm build:demo Build for /demos/astro/summit into demo/ (only used for the demo on freeframe.dev)

Pages#

Every file in src/pages/ becomes one page.

URL File What's on it
/ src/pages/index.astro Hero with live countdown, topics strip, highlights, featured speakers, count-up stats, schedule preview, sponsor wall, testimonial, venue preview, newsletter
/speakers src/pages/speakers.astro Track filter and pop-up speaker profiles
/speaker src/pages/speaker.astro One speaker (/speaker?id=hana-kim): bio, links, sessions, "More speakers"
/schedule src/pages/schedule.astro Day tabs, track filter, starred sessions in "My schedule", "On now" / "Up next" during the event
/tickets src/pages/tickets.astro Four passes with early-bird prices, quantity steppers, order summary with team discount, comparison, refund FAQ, demo checkout
/venue src/pages/venue.astro Venue details, illustrated map, travel options, partner hotels, accessibility
/sponsors src/pages/sponsors.astro Sponsor tiers and packages, sponsorship enquiry form
404 src/pages/404.astro Not-found page (most hosts show 404.html automatically)

Project structure#

summit/
├─ src/
│  ├─ pages/                 one .astro file per page (see above)
│  ├─ layouts/
│  │  └─ Layout.astro        <head> (title, meta, Open Graph, fonts), skip link, header, footer, toast, scripts
│  ├─ components/
│  │  ├─ SiteHeader.astro    logo, desktop navigation, tickets button, full-screen mobile menu
│  │  └─ SiteFooter.astro    footer links and copyright
│  ├─ data/
│  │  └─ summit.ts           tracks, days, speakers, sessions, ticket prices, early-bird date, team discount
│  ├─ lib/
│  │  └─ site.ts             site name, theme colour and the url() helper for links
│  ├─ scripts/
│  │  ├─ main.ts             countdown, speakers, schedule and stars, tickets, forms, reveals, menu
│  │  └─ back-to-top.ts      floating "Back to top" button
│  └─ styles/
│     └─ global.css          Tailwind theme (colours, fonts), track tags and component classes
├─ public/img/               speakers/<id>.svg portraits, favicon, og-image.png (and its SVG source)
├─ astro.config.mjs          site URL, base path, output format, Tailwind plugin
├─ eslint.config.mjs, tsconfig.json
├─ package.json, pnpm-workspace.yaml, pnpm-lock.yaml
└─ README.md, DOCS.md, CHANGELOG.md, template.json

Pages are rendered to HTML at build time. The two files in src/scripts/ are the only JavaScript sent to the browser, plus one inline line in the layout that swaps the no-js class on <html> for js before the page paints. Layout.astro imports both scripts, and Astro bundles them (with the data they import from src/data/summit.ts) into one small module.

Customising#

Brand and colours#

The palette and fonts are tokens in the @theme block at the top of src/styles/global.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 and accents

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

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

Track colours (.tag-design, .tag-engineering, .tag-ai, ...) are component classes in global.css. If you change --color-night, also change site.themeColor in src/lib/site.ts, which fills the theme-color meta tag.

Typography#

Headings use Space Grotesk (--font-display) and text uses Inter (--font-sans), loaded from Google Fonts by a <link> in src/layouts/Layout.astro. To change them, replace that link and the two tokens. To self-host, put the font files in public/fonts/, add @font-face rules to global.css and remove the <link> and the two preconnect tags.

Content#

Page text lives in each src/pages/*.astro file; the title and description are the title and description props on <Layout>. Event data lives in src/data/summit.ts, which is typed, so pnpm check 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 14 Apr", title: "Workshop 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)
earlyBirdEnds When early-bird pricing ends: "2026-12-31T23:59:59+01:00"
keepDemoCurrent true in the demo: once the edition is over, every date moves on a year (see Demo dates). Set it to false when you enter your real dates
teamDiscount { minTickets: 5, percent: 15 }
tickets { id, name, price, early } per ticket (Ticket)

What reads which source:

Content Comes from
Speaker cards on /speakers and / The page markup
Speaker pop-up, /speaker?id=…, "More speakers" speakers and sessions
Default /speaker (no ?id=) The page markup, written for the first speaker in speakers
Schedule sessions on /schedule The page markup (<li data-session="…"> items)
Valid starred sessions, a speaker's talks sessions
Ticket prices and early-bird prices tickets (price and early per ticket id)
Early-bird end date and team discount earlyBirdEnds, teamDiscount
Countdown target data-countdown on the countdown in index.astro

Adding a session means adding it to sessions and adding a schedule item to the right day panel in src/pages/schedule.astro, with the same id and timing attributes:

html
<li class="session grid gap-4 p-5 sm:grid-cols-[8.5rem_1fr_auto] sm:p-6"
    data-session="s36" data-track="ai" data-day="thu"
    data-start="2027-04-15T16:00" data-end="2027-04-15T16:40">
  <!-- copy the inner markup of an existing session: time, title, speakers, room, star button -->
</li>
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" },

Copy the inner markup from a neighbouring session so it keeps its star button (data-star="s36") and .live-badge. data-start / data-end are local event times without a time zone.

Adding a speaker means an entry in speakers, a square portrait at public/img/speakers/<id>.svg, and a card in speakers.astro (a list item with data-track and a button with data-speaker="<id>").

Demo dates#

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

  • days (dates and labels), earlyBirdEnds, the countdown target and every data-start and data-end;
  • data-edition text: dates ("14–16 April 2027"), year ("2027"), year-1, year+1, yy ("’27") and yy-plain ("27", in the hotel code);
  • data-date="2027-04-01" text, written as dmy ("1 April 2027", the default), dm ("1 March") or dmon ("31 Dec") with data-date-format;
  • data-day-label="wed" (the schedule's day tabs), the page title and aria-labels that contain the year.

Before the edition is over nothing is changed, so a preview such as ?now=2027-04-15T10:45 still shows the live schedule. Set keepDemoCurrent to false once you enter your real dates. Elements with data-early-only (the "Early-bird tickets on sale" pill and the first sentence of the tickets intro) are removed once early-bird pricing has ended, whatever the setting.

Time zone#

Event times are written in Copenhagen summer time. eventTime() in src/scripts/main.ts appends +02:00 to every time, and the schedule's "today" check uses the same offset. For an event elsewhere, change both (for example to -04:00 and -4 * 3600 * 1000 for New York in summer), plus the note at the top of src/data/summit.ts.

Images and artwork#

Speaker portraits are illustrated SVGs in public/img/speakers/<speaker-id>.svg (square). Replace them with square photos using the same file names, or change speakerImg() in src/scripts/main.ts (and the <img> tags in the pages) if you use .jpg. Write image paths with url() in .astro files so they work under a base path:

html
<img src={url("/img/speakers/hana-kim.svg")} alt="" width="400" height="400" />

Other art is inline SVG in the pages. 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.

  • src/components/SiteHeader.astro is the header and full-screen mobile menu. A link gets aria-current="page" (and its highlighted style) when the page's current prop matches it, for example current="schedule"; speaker.astro passes current="speakers".
  • src/components/SiteFooter.astro is the footer.

To add a page, create src/pages/workshops.astro:

html
---
import Layout from "../layouts/Layout.astro";
---

<Layout title="Workshops — Summit 2027" description="Hands-on workshops on 14 April." current="workshops" page="workshops">
  <main id="main">
    <section class="container-x pt-36 pb-20">
      <h1 class="display text-6xl">Workshops</h1>
    </section>
  </main>
</Layout>

Each page provides its own <main id="main"> (the skip link's target); page sets data-page on <body>. Then add the link to the desktop and mobile menus in SiteHeader.astro (with the same aria-current={(current === "workshops") ? "page" : undefined} pattern) and to the footer. Always write internal links as href={url("/workshops")}.

Features#

All interactive code is in src/scripts/main.ts, one commented block per feature. Each block looks for its elements by data-* attribute and does nothing if they're missing. HTML strings built in the script (speaker pop-ups, "My schedule", the order summary) use Tailwind classes too; Tailwind scans src/, so classes you add there are generated automatically.

Countdown#

The element with data-countdown="2027-04-14T09:00:00+02:00" counts down days, hours, minutes and seconds into its [data-unit="days"], "hours", "minutes" and "seconds" children every second. It has role="timer" and stops at zero.

Speakers filter and pop-up profiles#

On /speakers, buttons with data-filter (all or a track id) show speakers whose list item has a matching data-track and update [data-count-label]. Clicking a speaker opens a native <dialog> ([data-speaker-modal]) built from speakers: portrait, role, bio, every session they appear in and a "Full profile" link to /speaker?id=<id>. Esc, the close button or clicking outside closes it, and focus returns to the speaker you opened.

Speaker pages#

/speaker is built for the first speaker in speakers. With ?id=<speaker-id> (any letter case: ?id=Mei-Tanaka works) the script swaps in that speaker's portrait, bio, links and session cards in the same layout, with the same reveal animation, and updates the page title. Unknown ids (and names like constructor) keep the first speaker. "More speakers" ([data-more-speakers]) lists four others.

Schedule, tracks and My schedule#

/schedule has a tablist of days ([data-day-tab], including a "My schedule" tab with id mine) with Left/Right, Home and End keys. The current tab, chosen by click or key, is kept in the URL hash (/schedule#fri), and during the event the current day (in event time) opens automatically.

  • Track buttons (data-track-filter) show one track; breaks and socials (general) stay visible so each day still reads as a timeline. An empty state ([data-filter-empty]) appears when a day has nothing in that track.
  • Star buttons (data-star, aria-pressed) add sessions to My schedule, grouped by day, with a count in [data-star-count] and a toast. Their label follows the state ("Add “…” to my schedule", or "Remove “…” from my schedule" once starred, also for stars restored from the browser). 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 note ([data-live-note]). Preview it any time with ?now=: /schedule?now=2027-04-15T10:45. The same parameter also drives the countdown and early-bird pricing, which is handy for testing.

Tickets and order summary#

Each ticket card on /tickets (data-ticket="<id>") has −/+ steppers (data-step, 0–20 each). Until earlyBirdEnds the early price is used and the full price is shown struck through; afterwards the early-bird labels are removed. The summary lists each line, the subtotal, the team discount (15% from five tickets, from teamDiscount), a hint showing how many more tickets unlock it, and the total. Continue to checkout opens the checkout form.

Prices come from tickets in src/data/summit.ts, matched to each card by its data-ticket id:

ts
export const tickets: Ticket[] = [
  { id: "workshop", name: "Workshop day", price: 340, early: 290 },
  { id: "conference", name: "Conference pass", price: 690, early: 490 },
  // ...
];

The script writes the current price (data-price-now) and the full price (data-price-was) into each card, so the order totals always match the data. When early equals price the struck-through price and early-bird label are removed. The prices typed into tickets.astro are only shown if JavaScript doesn't run; update them, and the comparison table, to match. A card whose id isn't in tickets is hidden.

Scroll reveals and count-up stats#

Elements with reveal fade in when scrolled into view; numbers with data-count-to (and an optional data-suffix) count up once when visible. The gradient blobs drift slowly. All motion stops with prefers-reduced-motion, and content stays visible without JavaScript.

Mobile menu#

The menu button (data-menu-toggle) opens the full-screen #mobile-menu, locks page scrolling, makes the header solid while open, moves focus to the first link, updates aria-expanded and its label, and closes with Esc (focus returns to the button) or when the window reaches the lg breakpoint.

Back to top button#

src/scripts/back-to-top.ts adds a .back-to-top button that appears after you scroll down about one screen. It scrolls back up (instantly with reduced motion), moves keyboard focus to #main and lifts above toasts (such as "Added to my schedule"). 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 the file, its import in Layout.astro and the .back-to-top rules at the end of global.css.

Forms#

Form (data-form) Page Fields
checkout /tickets first, last, email, company, country, payment (demo choice), terms
sponsor /sponsors name, email, company, tier, message
newsletter / email

Forms marked data-validate check required fields, email format, minimum lengths and required checkboxes. Errors appear next to each field (data-error-for), fields get aria-invalid, and the first problem is focused. On success the form's [data-success] element is shown and a summit:submitted event fires; the ticket page uses it to show a confirmation with the order total that says plainly no order was placed and no payment was taken.

The demo forms don't send anything. In the "forms" block of src/scripts/main.ts, find the comment "To send for real", make the submit listener async (e) => { ... } and post the data before showing success:

ts
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
$opt("[data-success]", form)?.classList.remove("hidden");
form.reset();
document.dispatchEvent(new CustomEvent("summit:submitted", { detail: { form: form.dataset.form } }));

To run your own server code instead, add an Astro adapter (for example @astrojs/node, @astrojs/netlify or @astrojs/vercel) and an API endpoint such as src/pages/api/sponsor.ts with export const prerender = false, then post to it.

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 with their embed, or send the selected quantities to their checkout from the Continue to checkout button.

Saved data (localStorage)#

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

On load the stored value is validated: anything that isn't an array of known, unique session ids (from sessions) 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 src/scripts/main.ts (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 passes title and description to <Layout>. The layout turns them into <title>, the meta description, og:title, og:description and a summary_large_image Twitter card.

  • The canonical URL, og:url and the absolute og:image URL are built from site in astro.config.mjs, which reads SITE_URL (default https://summit.example.com). Set it to your domain when you build:

    sh
    SITE_URL=https://example.com pnpm build

    Canonical URLs have no .html (https://example.com/schedule).

  • /speaker updates its title for the speaker in ?id= in the browser. If speaker pages matter for search, generate one static page per speaker with a dynamic route such as src/pages/speakers/[id].astro using getStaticPaths() over speakers.

  • The 404 page passes noindex, which adds <meta name="robots" content="noindex"> and leaves out the canonical URL.

  • Add Event structured data (name, dates, location, offers) as JSON-LD in index.astro, and a sitemap with @astrojs/sitemap.

Accessibility#

Built in:

  • Skip link, labelled landmarks, one <h1> per page and visible lime focus rings.
  • Day tabs follow the tabs pattern; 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 colour alone.
  • Toasts are announced with aria-live.
  • Motion (reveals, drifting gradients, 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 build        # static site in dist/
pnpm preview      # preview dist/ locally

dist/ is plain HTML, CSS and JavaScript. Pages are written as schedule.html rather than schedule/index.html (build.format: "file" in astro.config.mjs), and links point to /schedule without a trailing slash. Netlify, Cloudflare Pages and GitHub Pages serve /schedule from schedule.html automatically; Vercel does with cleanUrls (below).

  • Netlify / Cloudflare Pages: build command pnpm build, publish directory dist.
  • Vercel: import the repository; it detects Astro and static output needs no adapter. Add a vercel.json with { "cleanUrls": true } so /schedule serves schedule.html.
  • GitHub Pages: use the official Astro GitHub Action. For a project site (username.github.io/repo), build with DEMO_BASE_PATH=/repo.
  • Any static host: upload the contents of dist/. If the host doesn't map /schedule to schedule.html, change build.format to "directory" in astro.config.mjs.

To serve the site from a sub-folder, set DEMO_BASE_PATH when building, e.g. DEMO_BASE_PATH=/summit pnpm build. It becomes Astro's base; every link written with url() and every link the script builds (speaker pages and portraits) includes it.

demo/ (if present) is the build used for the freeframe.dev demo; you can delete it.

TypeScript 7 setup#

package.json installs two TypeScript packages on purpose. @typescript/native is TypeScript 7 and provides the tsc that pnpm typecheck runs. typescript is an alias for @typescript/typescript6, the TypeScript 6 API that astro check and typescript-eslint load. When those tools support TypeScript 7, replace both with a single typescript@7. tsconfig.json extends Astro's strict preset.

Troubleshooting#

  • A new session has no star or never shows "On now". Copy the full inner markup of an existing session (star button and .live-badge), and give it data-start / data-end.
  • A starred session disappears after reload. Its id must also exist in sessions in src/data/summit.ts.
  • A speaker pop-up doesn't open. The button's data-speaker must match an id in speakers.
  • A ticket card disappeared. Its data-ticket id must match an id in tickets.
  • Times are an hour or more off. Change the +02:00 offset in eventTime() (and the "today" offset next to it) to your event's offset.
  • /schedule shows a 404 on my host. The host doesn't map extensionless URLs to .html files. Set build.format: "directory" in astro.config.mjs and rebuild.
  • Forms "send" but nothing arrives. Connect them as described in Forms.
  • pnpm install skips a build script. pnpm-workspace.yaml lists the packages allowed to run install scripts (esbuild, sharp, @tailwindcss/oxide); add any new one there.

License#

MIT. The code, the SVG speaker portraits and all other included artwork are free for personal and commercial use; the download includes the license text in LICENSE.txt. 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