Freeframe

Documentation

Tempo

v1.0.2Updated Nov 13, 2026A bold, poster-printed website for a band or musician, with a real track player, tour dates, a merch store and a press kit.

Overview#

Tempo is a website template for a band or solo musician, built for Marigold Static, a fictional four-piece indie band from Lincoln, Nebraska (Isla Carrick, Reuben Oyelaran, Fenna de Wit and Callum Shiach) whose second album, Weather for Two, is about to come out on the fictional Lantern House Records. The site has a home page with a release countdown and pre-save, a discography with a working track player, tour dates, a merch store with a cart, an about page, an electronic press kit (EPK) and a contact form that routes messages by topic. This is the Nuxt version: Nuxt 4, Vue 3 single-file components, TypeScript and Tailwind CSS v4, generated as a static site. The look is a printed gig poster: warm paper, black ink, fluorescent pink, cobalt and sun-yellow "riso" inks, condensed Big Shoulders Display headlines and SVG artwork throughout. The player lives in the layout, so whatever is playing keeps playing while you move between pages. This guide covers version 1.0.2. See it live at freeframe.dev/templates/tempo.

Quick start#

Requires Node.js 24 or newer and pnpm.

sh
pnpm install      # also runs `nuxt prepare` (types and the ESLint config in .nuxt/)
pnpm dev          # http://localhost:3000
Command What it does
pnpm dev Development server with hot reload
pnpm generate Static site in .output/public/ (every page prerendered, plus 404.html)
pnpm build A Node.js server build in .output/ (pages are still prerendered)
pnpm preview Serves the last build
pnpm typecheck vue-tsc with Nuxt's strict settings
pnpm lint ESLint (flat config from @nuxt/eslint)

Pages#

Route File Purpose and notable features
/ app/pages/index.vue New-release hero with countdown, pre-save and "Play Glasshouse" (#presave), platform links, city ticker, the new album's track list, click-to-play video poster, next four shows, merch preview, press quote, Static Club sign-up (#join)
/music app/pages/music.vue Release stats, album/EP/single filter with counts, release grid, release detail (#release) with track list, credits and previous/next links. /music?type=ep&release=night-bus-hymns#release
/tour app/pages/tour.vue Region filter and country select, show list with status chips, calendar downloads, past shows, "notify me" form (#notify), FAQ. /tour?region=europe&country=Germany, /tour?notify=paris#notify
/merch app/pages/merch.vue Category filter (/merch?cat=apparel), product cards with color and size options (#p-<product id>), shipping and sizing
/about app/pages/about.vue Group illustration, story and stats, band members, timeline, press quotes
/press app/pages/press.vue Fact sheet, short/medium/long bio tabs with copy, downloads (#downloads), tech rider, press quotes, contacts (#contacts)
/contact app/pages/contact.vue Topic-routed form (/contact?topic=booking), direct contacts, mailing address
404 app/error.vue "Not on the record" page with links back into the site, generated as 404.html

The announcement bar, header, footer, player bar, cart drawer, toast and back-to-top button are in app/layouts/default.vue, so they are on every page.

Project structure#

tempo/
  nuxt.config.ts               base path, head defaults (fonts are in CSS), SITE_URL, prerendered routes
  app/app.vue                  <NuxtLayout><NuxtPage /></NuxtLayout>, with the page key that restarts a page
  app/layouts/default.vue      skip link, "New album" bar, header, footer, player bar, cart drawer, toast
  app/pages/                   one file per page (index, music, tour, merch, about, press, contact)
  app/error.vue                the 404 page
  app/router.options.ts        query strings written the way the HTML version writes them
  app/components/              SiteHeader, SiteFooter, PlayerBar, TrackList, PlayReleaseButton, ReleaseCover,
                               ProductArt, ProductCard, CartDrawer, ShowRow, VideoPoster, JoinForm,
                               NotifyForm, ContactForm, BackToTop and small icons
  app/composables/             useClock (today and the ticking clock), useDemoDates, useValidatedForm,
                               usePageSeo, useCurrentPage, useAsset
  app/data/band.ts             BAND settings, NAV, PLATFORMS and CONTACT_TOPICS
  app/data/releases.ts         RELEASES (with their tracks) and TYPES
  app/data/tour.ts             REGIONS, TOUR, PAST_SHOWS and ticketUrl()
  app/data/products.ts         PRODUCTS and MERCH_CATS
  app/data/press.ts            FACTS, BIOS, RIDER and CONTACTS for the press kit
  app/utils/player.ts          the player engine (one shared <audio> element) and playFrom()
  app/utils/synth.ts           track previews synthesized in the browser
  app/utils/art.ts             album covers and merch mock-ups drawn as SVG markup
  app/utils/cart.ts            the bag (localStorage) and the drawer state
  app/utils/dates.ts           BUILD_DAY, demo dates, release dates, upcoming and past shows
  app/utils/forms.ts           validation messages and helpers
  app/utils/download.ts        file downloads, clipboard and .ics calendar files
  app/utils/format.ts          money(), clock(), doorsTime(), makeRef() and other helpers
  app/utils/safeStorage.ts     localStorage with an in-memory fallback, readJSON(), STORAGE_KEYS
  app/utils/toast.ts           toast()
  app/plugins/                 clock, saved bag, scroll reveals, same-page links, stable builds
  app/assets/css/main.css      Tailwind CSS: @font-face rules, @theme design tokens and component classes
  app/assets/fonts/            woff2 font files and their license (OFL.txt)
  public/img/                  favicon, band and member illustrations, og-image.png and og-image.svg
  README.md, DOCS.md, CHANGELOG.md

Components, composables and everything exported from app/utils/ are auto-imported by Nuxt; data files are imported by path (import { RELEASES } from "~/data/releases").

Customizing#

Brand and colors#

All design tokens live in the @theme block in app/assets/css/main.css:

Token Default Used for
--color-paper #f4efe6 Page background
--color-paper-2 #ebe4d6 Alternate sections
--color-card #fbf8f2 Cards, inputs, the cart drawer
--color-ink #15130f Text, borders, dark sections, hard shadows
--color-soft / --color-subtle #4b463d / #686155 Secondary text and captions on paper
--color-fog / --color-haze #bdb5a6 / #9c9485 Secondary text and captions on ink
--color-pink #ff4d94 Brand fill: primary buttons, chips, highlights (with ink text)
--color-pink-deep #c4155f Pink text and links on paper
--color-cobalt #2b3fd6 Second ink: the fan-club section, focus rings
--color-sun #ffcf33 Third ink: highlights, stickers, success boxes

To rebrand, change the inks:

css
@theme {
  --color-pink: #ff7a1a;      /* fills, used with ink text */
  --color-pink-deep: #b34700; /* the same color dark enough for text on paper */
}

Keep --color-pink-deep at a 4.5:1 contrast with the paper. A few SVGs (the logo mark in app/components/LogoMark.vue, the hero vinyl in app/pages/index.vue, the video poster, the 404 record and the artwork in app/utils/art.ts) use hex values directly; search for #ff4d94 to update them too.

Dark sections are marked with the on-ink class. Inside them buttons, links, meta text and form fields switch to their light-on-dark versions automatically. The browser theme color is set in nuxt.config.ts.

Typography#

The fonts are self-hosted from app/assets/fonts/ with @font-face rules at the top of app/assets/css/main.css. They are the same woff2 files the HTML version loads from Google Fonts (latin and latin-ext subsets), so text sets at exactly the same width:

  • Big Shoulders Display (700–900) for the condensed poster headlines (--font-display, the poster utility, .h-hero, .h-page, .h-section).
  • Familjen Grotesk (400–700, upright and italic) for body text (--font-sans).
  • Space Mono (400 and 700) for labels, dates and prices (--font-mono, .meta, .eyebrow, .chip).

Each face uses its real family name, so the --font-* tokens and the SVG artwork refer to them by name. To swap a font, put its woff2 files in app/assets/fonts/, change the matching @font-face rules and the --font-* token, and search app/utils/art.ts for font-family.

Content#

Page copy is written in each page's template. Everything that is listed, filtered or played lives in typed data files in app/data/:

Constant File What it holds
BAND band.ts Name, site URL, email, currency and locale for prices and dates, freeShippingFrom, shipping, keepDemoDatesFresh
NAV, PLATFORMS, CONTACT_TOPICS band.ts Menu items, streaming links, contact form topics with who each one goes to and the reply time
RELEASES, TYPES releases.ts Every release, newest first: id, type (album, ep, single), title, date, label, cat, art, merch, blurb, credits, tracks
REGIONS, TOUR, PAST_SHOWS tour.ts Tour regions and shows: id, date, time (doors, 24-hour; 19:00 shows as 7:00 PM), city, country, region, venue, status, onSale, note, support, optional tickets
PRODUCTS, MERCH_CATS products.ts Merch: id, name, cat, kind, price, badge, desc, optional release, colours, sizes, soldOut
FACTS, BIOS, RIDER, CONTACTS press.ts The press kit's fact sheet, bios (*text* is set in italics), tech rider lines and contact cards

Add a release by putting a new entry at the start of the DATA list in app/data/releases.ts:

ts
{
  id: "bluestem-ep", type: "ep", title: "Bluestem", date: "2027-05-07", label: "Lantern House Records", cat: "LHR034",
  art: { bg: "#c9cff7", a: "#ff4d94", b: "#15130f", ink: "#15130f", motif: "moons" },
  blurb: "Four songs recorded in a grain elevator.", credits: "Produced by Ailsa Braid.",
  tracks: [{ title: "Bluestem", len: "3:40", early: true }, { title: "Storm Cellar", len: "4:05" }],
},

The first release is the "new release" the home page counts down to (the hero copy in app/pages/index.vue names Weather for Two; update it with the release). A release dated in the future shows "Out <date>"; only tracks with early: true can be played before release day. Track ids are generated as <release id>-<track number> (for example weather-for-two-2).

Add a show to TOUR:

ts
{ id: "oslo", date: "2026-12-13", time: "20:00", city: "Oslo", country: "Norway", region: "europe",
  venue: "Kulturhuset Sal 2", status: "on-sale", support: "Twin Ferries", tickets: "https://tickets.example/oslo" },

status is on-sale, low (shows "Last tickets"), sold-out (shows a waitlist link) or soon (shows "On sale <onSale>" and a reminder link). Without tickets, ticketUrl() builds a placeholder address. Shows dated before today move to "Past shows" automatically, above the older PAST_SHOWS. Region and country counts are worked out from the data.

Add a product to PRODUCTS:

ts
{ id: "beanie", name: "Static beanie", cat: "apparel", kind: "tee", price: 20,
  desc: "Ribbed knit with a woven label.", colours: [{ name: "Pink", hex: "#ff4d94" }],
  sizes: ["One size"] },

kind picks the SVG mock-up (lp, cd, cassette, tee, hoodie, poster, tote or pins); lp and cd use the cover of the release you set. Products with more than one color get swatches; products with sizes require a size before they can be added. Sizes listed in soldOut are crossed out and disabled.

Images and artwork#

  • Album covers are drawn by <ReleaseCover> from each release's art: bg (paper color), a and b (two inks), ink (title color) and motif (weather, glass, river, signal, moons, bus, sun or grid). The drawing code is coverInner() in app/utils/art.ts. Pass label="" when the cover is decorative. To use real artwork, render an <img> of your square artwork (at least 1200×1200) in app/components/ReleaseCover.vue.
  • Merch mock-ups are drawn by <ProductArt> (productArtInner()) for the selected color, in a 4:5 frame. Replace them with 4:5 product photos the same way.
  • Band and member portraits are SVG files in public/img/ (band.svg 1200×800, member-*.svg 400×500), shown with useAsset() so they work under a base path. Swap them for photos with the same aspect ratios and keep the alt text accurate.
  • Social image: public/img/og-image.svg is the source and public/img/og-image.png (1200×630) is what every page uses. Export a new PNG after editing the SVG.
  • Favicon: public/img/favicon.svg, linked in nuxt.config.ts.

The header and mobile menu are app/components/SiteHeader.vue (menu items from NAV in app/data/band.ts; the mobile menu adds Home), the footer is app/components/SiteFooter.vue, and the "New album" bar above the header is in app/layouts/default.vue. To add a page, create app/pages/<route>.vue with a <main id="main"> root and a usePageSeo() call, add the route to routes in nuxt.config.ts, then add it to NAV and the footer. The current menu item gets aria-current="page" from useCurrentPage().

Features#

The player#

app/utils/player.ts holds one shared <audio> element outside the pages, so playback carries on while Nuxt swaps pages. A queue is the playable tracks of one release. Components read playerState (the loaded track cur, playing, muted, position t and length d) and call Player.toggle(), next(), prev(), seek(), toggleMute() and close().

  • <TrackList release-id> draws a release's track list. Each number turns into play/pause; the current row gets aria-current="true" and a "Now playing" label with an equalizer, and unreleased tracks are disabled with "Out <date>".
  • <PlayReleaseButton release-id track-id? label> plays a release from its first playable track (or the given track) and becomes "Pause" while it plays. playFrom() in player.ts is what both call.
  • <PlayerBar> slides up once something is loaded, with previous, play/pause, next, a seek slider, mute and close, and announces the new track in a polite live region.

Previews without audio files: app/utils/synth.ts synthesizes a 30-second piece for each track (a pad, bass, arpeggio and drums in a key, tempo and chord progression picked from the track id), encodes it as a WAV file and plays it with the <audio> element. To play your real audio, put the files in public/audio/ and give the track a src (the base path is added for you):

ts
{ title: "Glasshouse", len: "3:41", early: true, src: "/audio/glasshouse-preview.mp3" },

Remove the "30-second preview" labels in TrackList.vue and the "Preview" chip in PlayerBar.vue if you play full tracks. The player also sets Media Session metadata, so lock screens and media keys work.

Keyboard: all controls are buttons and the seek slider takes arrow keys. Space or K plays and pauses, Shift+N and Shift+P skip tracks (ignored while typing or when a button or link has focus); the handler is in PlayerBar.vue.

New release countdown and pre-save#

The home page reads the first release in RELEASES. Before its date the hero shows the "soon" content (the countdown, the pre-save button and "Play Glasshouse"); from release day it shows the "out" content instead. The switch happens by itself when the countdown reaches zero. The pre-save button remembers its state in localStorage (tempo-presave); point it at your pre-save service by replacing it with a link, or call that service in togglePresave() in app/pages/index.vue.

Keeping the demo current#

Everything that depends on today reads useToday() and useNowSeconds() (app/composables/useClock.ts). The generated HTML is rendered for BUILD_DAY in app/utils/dates.ts (October 14, 2026, the day the copy describes), and app/plugins/clock.client.ts switches to the browser's own date once the page has loaded and ticks once a second, so the page hydrates cleanly and is still right on any later day.

BAND.keepDemoDatesFresh moves the whole tour, and the new album (the release with campaign: true), forward to the same weeks of the next year once every tour date has passed, by whole weeks so weekdays never change (Friday, November 13, 2026 becomes Friday, November 12, 2027). The demo then has an album on the way and upcoming shows again. Set it to false on your real site. demoShift(), releaseDate(), tourOn() and tourYears() in app/utils/dates.ts work it out for a day; a show marked status: "soon" goes on sale on its onSale date.

Text written in the pages follows the same dates through useDemoDates() (app/composables/useDemoDates.ts): the banner ("Weather for Two is out November 13. ", then "is out now"), the release date in the hero, the "Pre-save" buttons, "Hear the first single" and "Pre-order the LP" (which read "Listen", "Listen to the album", "Listen now" and "Buy the LP" once the album is out, from RELEASE_CTA), the tour years, the about timeline, the press kit's dates (fix()) and the home and tour page titles. Products with the "Pre-order" badge show "Out now" once the album is out. The copyright year in the footer is updated in the browser too.

Video poster#

<VideoPoster> shows an SVG poster and loads nothing until someone presses play. Pass your embed address and the button swaps the poster for an iframe:

html
<VideoPoster embed="https://www.youtube-nocookie.com/embed/VIDEO_ID" title="Glasshouse (official video)" release-id="glasshouse" />

With embed empty (the demo, in app/pages/index.vue), pressing play plays the preview of release-id and animates the poster's bars.

Discography#

app/pages/music.vue renders the release grid and detail from RELEASES. The filter buttons update the grid, the live count and ?type=; any release link shows that release in the detail section, moves focus to its title and updates ?release= (with history.replaceState, so no new history entry). Without ?release= the newest album that is out is shown. "Copy link" copies a link straight to the release (#release).

Tour dates and calendar files#

app/pages/tour.vue lists upcoming shows. The region buttons and country select filter the list and keep ?region= and ?country= in the address; an empty state offers a reset. Each row is a <ShowRow>; its "Calendar" button downloads an .ics file built by icsFor() in app/utils/download.ts with the doors time, venue, support act and ticket link. The home page shows the next four with the same row (compact). /tour?notify=<show id> fills in the tour-alert form's city and country.

Merch and cart#

app/pages/merch.vue renders a <ProductCard> per product. Choosing a color redraws the mock-up; "Add to bag" checks that a size is chosen, adds the item (merging identical color and size) and opens the cart drawer. The bag lives in app/utils/cart.ts (cartLines, cart.add(), step(), remove(), clear()), and the header shows its count.

<CartDrawer> is a modal dialog: focus moves into it and is kept there, Esc or the backdrop closes it and focus returns to the button that opened it. It has quantity steppers (1–10), remove buttons, the subtotal, a free-shipping meter based on BAND.freeShippingFrom and a shipping line based on BAND.shipping.

Checkout. "Checkout" shows an order summary and a shipping note; "Place order" empties the bag and shows "Order placed" with an order reference (MS- and four characters, made in the browser by makeRef()). Nothing is charged until you connect a store: to sell for real, replace placeOrder() in app/components/CartDrawer.vue with a redirect to your store, for example a Shopify cart permalink or a Stripe Payment Link:

ts
function placeOrder() {
  window.location.href = "https://your-shop.example/cart/" + cartLines.value.map((l) => `${l.id}:${l.qty}`).join(",");
}

Press kit#

The bio tabs on app/pages/press.vue follow the ARIA tabs pattern (arrow keys, Home and End move between Short, Medium and Long). "Copy this bio" copies the visible bio as plain paragraphs and the word count follows the tab. The download buttons generate files in the browser (save()): cover saves the new album's cover as a 2000×2000 SVG, logo saves #logo-art, bio saves all three bios as text, facts saves every [data-fact] row of the fact sheet, and rider saves #rider-text. The band illustration is a plain download link. The "Copy" chips copy an email address.

Header, menu, reveals and back to top#

The header gets a border once you scroll. On screens under 1024 px the menu button opens a full-height menu (aria-expanded, Esc closes it and returns focus, opening a page closes it). Elements with .reveal fade in as they scroll into view (app/plugins/reveal.client.ts; revealing one also reveals everything above it); with reduced motion they are visible right away and the ticker, equalizers and vinyl stop moving. <BackToTop> appears after one screen, lifts itself above the player bar and toasts (data-back-to-top-avoid), and moves focus to the main content; delete it from app/layouts/default.vue and the .back-to-top styles to drop it.

In the HTML version every link loads a page. Here links are client-side, and the music, tour and merch pages keep their filters in the address without telling the router. app/plugins/same-page.client.ts makes a link to the page already showing (the menu link to the page you are on, a Waitlist link on the tour page) start that page afresh at the linked address, scrolled to the top or to its #hash, by raising the page key in app/app.vue. Links that only jump to a section of the page (#notify) work as usual.

Forms#

Form Component Page Validates
#join-form (Static Club) <JoinForm> Home Region, email, consent checkbox
#notify-form (tour alerts) <NotifyForm> Tour Email, town or city, country, travel distance
#contact-form <ContactForm> Contact Topic, name, email, message (30+ characters) and the topic's own fields

Each form uses useValidatedForm() from app/composables/useValidatedForm.ts. Fields stay uncontrolled and are checked with the browser's own constraint validation (required, type="email", minlength, min, pattern) on submit and as they change; errors appear under each field (linked with aria-describedby) and focus moves to the first problem. Field names in messages come from data-name; checkbox and radio messages from data-required-msg, date and pattern messages from data-min-msg and data-pattern-msg (message() in app/utils/forms.ts). On success the form is swapped for its success box, which takes focus.

Contact routing: choosing a topic shows that topic's fields (data-topic-fields) and says who the message goes to, from CONTACT_TOPICS. Booking is routed by the "Where is the show?" select: North America goes to the North American agent, everything else to the international agent. Topic fields are only required while their topic is selected; dates can't be in the past.

The forms don't send anything until you connect them. Submissions end in onValid() in app/composables/useValidatedForm.ts. To use Formspree, post the form there before showing the success box:

ts
async function onValid(el: HTMLFormElement) {
  const res = await fetch("https://formspree.io/f/your-form-id", {
    method: "POST",
    body: new FormData(el),
    headers: { Accept: "application/json" },
  });
  if (!res.ok) return toast("Sorry, that didn’t send. Please email us instead.");
  // ...then the existing lines that set `sent` and focus the success box
}

Use a different endpoint per form by checking el.id. Every field has a name, so FormData carries all values. For the fan-club and tour-alert forms you can post to your mailing list provider's form endpoint instead. If you deploy with a Node.js server (pnpm build), you can post to your own Nitro route instead, for example server/api/contact.post.ts:

ts
export default defineEventHandler(async (event) => {
  const data = await readFormData(event);
  // send an email, save to a database...
  return { ok: true, topic: data.get("topic") };
});

Success messages: the fan-club and tour-alert boxes ask the visitor to look out for a confirmation email (the usual double opt-in). The contact box says who the message went to, repeats that topic's reply time from CONTACT_TOPICS and shows a reference (MS- and four characters from makeRef(); use your form service's own reference instead if it returns one). The merch topic's order number field accepts these references as well as numeric ones like MS-10482.

Saved data (localStorage)#

Key Shape Notes
tempo-cart [{ "id": "glasshouse-tee", "colour": "Pink", "size": "L", "qty": 2 }] Read by readCart() in app/utils/cart.ts, which drops unknown products, colors or sizes, sold-out sizes and bad quantities, and caps quantities at 10
tempo-presave true or false The pre-save button's state (app/pages/index.vue)

The keys are in STORAGE_KEYS (app/utils/safeStorage.ts). Values are read with readJSON(), which falls back to defaults on missing or broken data, and only in the browser after the page has loaded (app/plugins/cart.client.ts), so the generated HTML never depends on them. When storage is blocked (a sandboxed frame, strict privacy settings) safeStorage keeps the values in memory for the visit. To reset, run localStorage.removeItem("tempo-cart") (or "tempo-presave") in the browser console, or clear the site's data. "Place order" also empties the bag.

SEO and sharing#

Each page calls usePageSeo({ title, description }) (app/composables/usePageSeo.ts), which sets the title, description, canonical URL, Open Graph and Twitter card tags. URLs are absolute: set your domain with SITE_URL when building.

sh
SITE_URL=https://www.marigoldstatic.example pnpm generate

The social image is public/img/og-image.png. Consider adding a sitemap (for example with the @nuxtjs/sitemap module or a public/sitemap.xml), a public/robots.txt, and MusicGroup and MusicEvent JSON-LD built from BAND and TOUR with useHead() for richer search results. The 404 page is noindex.

Accessibility#

  • Skip link, landmarks (header, nav, main, footer), one h1 per page and a logical heading order.
  • Visible focus rings (cobalt on paper, yellow on ink), AA contrast for text in both the paper and ink sections, aria-current="page" on the current menu item.
  • Filters are toggle buttons with aria-pressed and live result counts; the player announces the new track; track buttons say "Play" or "Pause" with the track name, and the player's buttons list their shortcuts in aria-keyshortcuts.
  • The cart is a modal dialog with focus trapping and Esc; the press bios use the ARIA tabs pattern; the menu uses aria-expanded and Esc.
  • Forms have real labels, inline errors linked with aria-describedby and success boxes that receive focus.
  • prefers-reduced-motion turns off reveals, the ticker, equalizers, the spinning vinyl and smooth scrolling.

When editing, keep one h1 per page, give new inputs a <label> and an error element in their [data-field] box, keep alt text on portraits, and check the contrast of any new color pairs.

Building and deploying#

pnpm generate writes the static site to .output/public/: one .html file per page (music.html, tour.html...), 404.html, a _payload.json per page for client-side navigation, and the assets in _nuxt/. Upload that folder to any static host: Netlify, Vercel, Cloudflare Pages, GitHub Pages or your own server. Most hosts serve music.html at /music. Audio files can live in public/audio/.

To serve the site from a sub-folder, set DEMO_BASE_PATH when building (the live demo uses /demos/nuxt/tempo):

sh
DEMO_BASE_PATH=/band SITE_URL=https://www.marigoldstatic.example pnpm generate

Add new pages to routes in nuxt.config.ts so they are prerendered. Rebuild when your release date passes so the generated HTML shows the "out now" state before JavaScript runs (the browser switches on its own either way), and update BUILD_DAY in app/utils/dates.ts to the day your copy describes. For server features (API routes, per-request rendering), run pnpm build and deploy .output/ to a host that runs Node.js.

TypeScript setup#

tsconfig.json only references the configs Nuxt writes to .nuxt/ when you run pnpm install (or nuxt prepare). If your editor reports missing types, run pnpm install again.

Troubleshooting#

  • No sound when I press play. Check the tab isn't muted and the player's mute button isn't on. If you added a src, check the path (files in public/ are served from /, plus the base path if you set one) and that the file is a format the browser plays (MP3, AAC or Ogg). Errors show a toast.
  • The tour list is empty. Every date is in the past. Add new shows, or leave BAND.keepDemoDatesFresh on while you build the site.
  • Copy buttons say "Select the text and copy it". The clipboard API needs https:// or localhost.
  • pnpm lint says it can't find .nuxt/eslint.config.mjs. Run pnpm install (or pnpm typecheck) once; nuxt prepare writes it.
  • /music?release=... works locally but 404s on my host. The host must serve music.html for /music. Most do; on others add a rewrite.
  • A new page 404s after pnpm generate. Add its route to routes in nuxt.config.ts.
  • The bag shows an item I deleted from PRODUCTS. It doesn't: readCart() drops it. If the count looks wrong, clear tempo-cart in localStorage.
  • Fonts look different in the downloaded cover SVG. SVG files use the fonts installed on the viewer's computer; convert the text to outlines in a design tool if you need exact type.

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. Big Shoulders Display, Familjen Grotesk and Space Mono are under the SIL Open Font License 1.1 (app/assets/fonts/OFL.txt). Marigold Static, Lantern House Records and every person, venue and business named in the template are fictional.

Search documentation

Search the guides and every template's documentation