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.
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.mdComponents, 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:
@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, theposterutility,.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:
{
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:
{ 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:
{ 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'sart:bg(paper color),aandb(two inks),ink(title color) andmotif(weather,glass,river,signal,moons,bus,sunorgrid). The drawing code iscoverInner()inapp/utils/art.ts. Passlabel=""when the cover is decorative. To use real artwork, render an<img>of your square artwork (at least 1200×1200) inapp/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.svg1200×800,member-*.svg400×500), shown withuseAsset()so they work under a base path. Swap them for photos with the same aspect ratios and keep thealttext accurate. - Social image:
public/img/og-image.svgis the source andpublic/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 innuxt.config.ts.
Navigation, header and footer#
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 getsaria-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()inplayer.tsis 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):
{ 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:
<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:
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.
Links to the page you are on#
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:
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:
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.
SITE_URL=https://www.marigoldstatic.example pnpm generateThe 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), oneh1per 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-pressedand 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 inaria-keyshortcuts. - The cart is a modal dialog with focus trapping and Esc; the press bios use the ARIA tabs pattern; the menu
uses
aria-expandedand Esc. - Forms have real labels, inline errors linked with
aria-describedbyand success boxes that receive focus. prefers-reduced-motionturns 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):
DEMO_BASE_PATH=/band SITE_URL=https://www.marigoldstatic.example pnpm generateAdd 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 inpublic/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.keepDemoDatesFreshon while you build the site. - Copy buttons say "Select the text and copy it". The clipboard API needs
https://orlocalhost. pnpm lintsays it can't find.nuxt/eslint.config.mjs. Runpnpm install(orpnpm typecheck) once;nuxt preparewrites it./music?release=...works locally but 404s on my host. The host must servemusic.htmlfor/music. Most do; on others add a rewrite.- A new page 404s after
pnpm generate. Add its route toroutesinnuxt.config.ts. - The bag shows an item I deleted from
PRODUCTS. It doesn't:readCart()drops it. If the count looks wrong, cleartempo-cartin 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.