Overview#
Solstice is a sunny, poster-style website for an outdoor food and music festival: a live countdown, a filterable lineup with artist pop-ups, a multi-stage schedule where visitors build their own plan (with clash warnings and a calendar export), a ticket shop with early-bird prices and a group discount, a food village with dietary filters and a map, and a visitor guide with an interactive site map and a searchable FAQ. The demo event is Solstice 2027, a fictional three-day festival at Birch Bluff Farm, Traverse City, Michigan (Friday – Sunday, June 25–27, 2027) with 40 acts on four stages and 16 food and drink vendors. This is the Nuxt version (1.0.0): Nuxt 4, Vue 3, TypeScript and Tailwind CSS v4, generated as a static site. An HTML + Tailwind version of the template is also available.
Live demo: freeframe.dev/templates/solstice
Quick start#
Requirements: Node.js 24 and pnpm (corepack enable pnpm).
pnpm install
pnpm dev # http://localhost:3000| 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/solstice into demo/ (the freeframe.dev demo; you can delete it) |
Add ?now=2027-06-25T21:30 to any URL to preview the site at a given festival time (see
Previewing a date).
Pages#
| Route | File | Notable sections and features |
|---|---|---|
/ |
app/pages/index.vue |
Poster hero with countdown and early-bird sticker, scrolling tape, poster-style lineup by day, "What's on" tiles, ticket preview, count-up numbers, FAQ teaser, visitor guide card, lineup alerts sign-up |
/lineup |
app/pages/lineup.vue |
Day, stage and genre filters (kept in the URL), "My plan only", artist cards with generated artwork, artist pop-up |
/schedule |
app/pages/schedule.vue |
Day tabs, grid and list views, "My plan only", My plan panel with clashes, .ics export and clear, opening times |
/tickets |
app/pages/tickets.vue |
Day, weekend and add-on rows with steppers, order summary with fees and group discount, three-step demo checkout, good-to-know cards |
/food |
app/pages/food.vue |
Food/sweet/drink and dietary filters, search, vendor cards, food village map with numbered pins |
/info |
app/pages/info.vue |
Site map with layer toggles and a text version, getting there, accessibility, families, what to bring, searchable FAQ |
/contact |
app/pages/contact.vue |
Contact form that routes volunteer, vendor, press and visitor inquiries, direct contacts |
| 404 | app/error.vue |
Error page with links back (generated as 404.html) |
Project structure#
solstice/
├── app/
│ ├── app.vue the layout and the current page (restarted when a link opens the page again)
│ ├── error.vue 404 page
│ ├── router.options.ts writes query strings the way the HTML version does
│ ├── layouts/default.vue skip link, header, footer, toast, back to top; sets the demo-date shift
│ ├── pages/ index, lineup, schedule, tickets, food, info, contact (one .vue per page)
│ ├── components/
│ │ ├── SiteHeader.vue header, "My plan" pill and mobile menu
│ │ ├── SiteFooter.vue footer
│ │ ├── LineUp.vue lineup filters, artist cards and the artist pop-up
│ │ ├── ScheduleBoard.vue schedule tabs, grid and list views, My plan panel and .ics export
│ │ ├── TicketShop.vue ticket rows, order summary and the demo checkout
│ │ ├── FoodVillage.vue food filters, vendor cards and the village map
│ │ ├── FaqSearch.vue searchable FAQ (/info)
│ │ ├── ContactForm.vue contact form with topic routing
│ │ ├── AlertsForm.vue lineup alerts sign-up (home)
│ │ ├── GateCountdown.vue, CountUp.vue countdown and count-up numbers
│ │ ├── ArtistArt.vue generated poster artwork for each artist
│ │ ├── PlanButton.vue the sun buttons that add a set to "My plan"
│ │ └── FieldError.vue, SunIcon.vue, WarnIcon.vue, BackToTop.vue
│ ├── composables/
│ │ ├── useEdition.ts demo-date shift, useFestivalNow(), useEarlyBird()
│ │ ├── useValidation.ts inline form validation
│ │ └── usePageSeo.ts, useCurrentPage.ts, useAsset.ts
│ ├── data/
│ │ ├── festival.ts days, stages, genres, artists, diets, vendors, tickets, add-ons and ticket rules
│ │ ├── faq.ts the FAQ on /info
│ │ └── nav.ts menu items
│ ├── utils/
│ │ ├── festival.ts festival time, prices (money()), set times, clash detection, makeRef()
│ │ ├── edition.ts demo dates: editionShift(), edition()
│ │ ├── plan.ts "My plan" in localStorage
│ │ ├── safeStorage.ts localStorage with an in-memory fallback
│ │ └── toast.ts, aria.ts
│ ├── plugins/
│ │ ├── plan.client.ts loads "My plan" and keeps tabs in sync
│ │ ├── reveal.client.ts scroll reveals
│ │ ├── same-page.client.ts links to the page already showing restart it, like a page load
│ │ ├── keep-hash.client.ts keeps the #set-… hash as written
│ │ └── stable-build.server.ts reproducible builds
│ └── assets/
│ ├── css/main.css Tailwind source: fonts, design tokens (@theme) and component classes
│ └── fonts/ self-hosted fonts and their licenses
├── public/img/ favicon.svg, hero-sun.svg, hero-hills.svg, bunting.svg, og-image.svg, og-image.png
├── nuxt.config.ts base path, head defaults, SITE_URL, prerendered routes, Tailwind
├── package.json, pnpm-workspace.yaml, tsconfig.json, eslint.config.mjs
├── template.json template metadata used by freeframe.dev
├── README.md, CHANGELOG.md
└── DOCS.md this guideCustomizing#
Brand and colors#
All colors are tokens in the @theme block of app/assets/css/main.css. Tailwind turns each one into utilities
(bg-sun, text-tomato, border-ink...), and the component classes use them through CSS variables.
| Token | Default | Used for |
|---|---|---|
--color-cream |
#fff4de |
Page background |
--color-paper |
#fffbf2 |
Cards, fields, buttons |
--color-ink |
#24122e |
Text, outlines, hard shadows, footer |
--color-muted |
#654757 |
Secondary text |
--color-sun / --color-sun-soft |
#ffc53d / #ffe08a |
Main accent, planned sets, hover states |
--color-tangerine |
#ff8a2b |
Orchard stage, accents |
--color-tomato |
#c73a1e |
Primary buttons, clash warnings, eyebrows |
--color-sea, --color-sky, --color-leaf, --color-peach |
Section colors, stages, dietary tags |
To change the main accent, edit the token:
@theme {
--color-tomato: #b4235a; /* raspberry buttons and warnings instead of tomato red */
}Keep white text on --color-tomato at 4.5:1 contrast or more. The SVG files in public/img/, the inline maps
and the generated artwork (app/components/ArtistArt.vue) use hex colors directly; search for the old hex value
to change them too. theme-color is set in nuxt.config.ts.
The "poster" look comes from a few component classes in main.css: .poster (thick ink outline and a hard
offset shadow), .poster-sm, .poster-hover, .btn with .btn-primary, .btn-sun, .btn-ink and
.btn-ghost, .chip, .tag, .display (condensed uppercase headings) and .wordmark.
Typography#
Three fonts are self-hosted from app/assets/fonts/ and declared with @font-face at the top of main.css.
They are the same Latin files Google Fonts serves the HTML version (SIL Open Font License in
app/assets/fonts/*-OFL.txt):
| Font | Token | Used for |
|---|---|---|
| Bricolage Grotesque (wdth 75–100, wght 400–800) | --font-display |
Headings via .display and .h-section, numbers |
| DM Sans (wght 400–800) | --font-sans |
Body text |
| Shrikhand | --font-script |
The "Solstice" wordmark (.wordmark) |
To swap a font, put its .woff2 file in app/assets/fonts/, change the matching @font-face rule and the token
in @theme. .display uses font-stretch: 75%; if your new display font has no width axis, remove that line
(and font-stretch from its @font-face).
Content#
Festival content lives in app/data/festival.ts:
| Export | What it holds |
|---|---|
KEEP_DEMO_CURRENT |
true in the demo: once the festival is over, every date moves on a year (see Demo dates). Set it to false when you enter your real dates |
GATES_OPEN, UTC_OFFSET |
Opening time (the home countdown) and the festival's UTC offset (-04:00 for Eastern Daylight Time) |
DAYS |
id, label, short, date, gates, curfew |
STAGES |
id, name, blurb (order = schedule column order) |
GENRES |
Genre id → label |
ARTISTS |
One entry per act and set (see below) |
DIETS, VENDORS |
Dietary labels and the food village vendors |
TICKETS, ADDONS |
Ticket types and extras with prices |
EARLY_BIRD_ENDS, GROUP_SIZE, GROUP_DISCOUNT, FEE, MAX_PER_TYPE, CURRENCY |
Ticket rules |
Add an artist by adding an object to ARTISTS. Times are local festival time, 24-hour; the pages show them on
the 12-hour clock:
{ id: "paper-moons", name: "Paper Moons", day: "sat", stage: "orchard", start: "19:15", end: "20:00",
genre: "folk", tier: 3, from: "Ann Arbor",
bio: "Three-part harmonies and a borrowed double bass.", art: ["waves", "#8fd0ee", "#24122e"] },idmust be unique; it is used in URLs (/lineup?artist=paper-moons), the plan and the calendar.tieris 1 for headliners (red "Headliner" badge, listed first), 2 for special guests, 3 for the rest.artis[motif, background, foreground]. Motifs:sun,waves,rings,stripes,burst,hills.- Sets should finish by midnight; the schedule grid spans from the earliest start to the latest end of each day.
The lineup and schedule are drawn from this data automatically. The poster-style lineup on the home page
(app/pages/index.vue, "Three days. Forty acts.") is written out by hand so you can set the billing exactly:
update it when you change headliners.
Add a vendor to VENDORS:
{ id: "pie-minister", name: "Pie Minister", kind: "food", cuisine: "Savory pies",
dish: "Steak or mushroom pies with mashed potatoes", price: "$12–$16",
diets: ["vegetarian"], pin: [62, 44] },kind is food, sweet or drink (pin color and the "Show" filter). diets uses the keys of DIETS.
pin is the position on the food village map in percent (x, y). Vendor numbers follow the array order.
Ticket prices are in TICKETS (price and early, in dollars) and ADDONS (price). Every price on the site
comes from there. The rows on /tickets are listed in SECTIONS at the top of app/components/TicketShop.vue
(with the color and icon of each row's badge): add a row there when you add a ticket type.
The FAQ on /info is in app/data/faq.ts: groups with questions (q) and answers (a, a list of text,
{ href, text } links and { date } dates). Other copy is in the page files.
Images and artwork#
There are no photos. All artwork is SVG:
public/img/hero-sun.svg(600×600) andhero-hills.svg(1440×300) are reused on the home page, inner page headers and the 404 page. The sun's rays turn slowly; the animation is inside the SVG and stops for visitors who prefer reduced motion.bunting.svgruns along the top of the home and visitor pages. Files inpublic/are linked with the base path throughuseAsset()(asset('img/hero-sun.svg')).- Artist artwork is generated by
app/components/ArtistArt.vuefrom each artist'sartsetting. To use photos instead, render an<img>in its place inLineUp.vue; keep a square (1:1) image for cards. - The site map (
app/pages/info.vue) and the food village map (app/components/FoodVillage.vue) are inline SVG. Food pins are drawn fromVENDORS[].pin. public/img/og-image.svgis the editable source of the social image; export it toog-image.pngat 1200×630 after editing (most social sites don't display SVG).public/img/favicon.svgis the favicon; the same mark is drawn inSiteHeader.vueandSiteFooter.vue.
Navigation, header and footer#
The header is app/components/SiteHeader.vue and reads its menu from app/data/nav.ts: NAV is the desktop
menu and MOBILE_NAV adds Home and Tickets for the mobile menu. To add a page, create app/pages/<route>.vue,
add its route to routes in nuxt.config.ts, add it to NAV and link it from app/components/SiteFooter.vue.
The current page is marked with aria-current="page" (useCurrentPage()). The "My plan" pill and every
[data-plan-count] show how many sets are saved.
Features#
Countdown#
<GateCountdown /> (app/components/GateCountdown.vue) on the home page shows days, hours, minutes and seconds
until GATES_OPEN. When it reaches zero, the cells are hidden and "The gates are open" message appears. It has
role="timer". The generated HTML shows 000 00 00 00; the browser fills in the real values once the page has
mounted, so there is no hydration mismatch.
Early-bird prices#
EARLY_BIRD_ENDS decides which price is used everywhere. useEarlyBird() in app/composables/useEdition.ts
is true before that moment (and always in the generated HTML). Pages show the current price with
ticketPrice(t, early) ([data-price-of]), the full price struck through while early-bird prices apply
([data-full-of]), and elements marked data-early-only (such as the hero sticker) or data-early-hide are
hidden with :hidden="!early" or :hidden="early".
Lineup filters and artist pop-up#
app/components/LineUp.vue. Day buttons ([data-day-filter]), the stage and genre selects (#filter-stage,
#filter-genre) and "My plan only" filter the cards; the result count is announced through an aria-live
region. Filters are written back to the URL with history.replaceState, so
/lineup?day=sat&stage=meadow&genre=indie can be shared or bookmarked; values that don't exist are dropped.
/lineup?artist=<id> opens that artist's pop-up on load.
Clicking an artist name opens a native <dialog> with the artwork, stage, set time, bio, a clash warning if
relevant, "Add to my plan" and "See in schedule". Esc, the close button or a click on the backdrop closes it,
and focus returns to the card. While it is open, the toast is drawn inside it so it stays visible.
My plan and clash detection#
Every sun button (PlanButton, [data-plan-toggle="<artist id>"]) adds or removes a set from the visitor's
plan, uses aria-pressed and shows a toast. The plan lives in app/utils/plan.ts (planState,
togglePlan(), savePlan()), is saved in localStorage (see Saved data), shared by
all pages and synced between open tabs (app/plugins/plan.client.ts). clashes() in app/utils/festival.ts
marks two planned sets as clashing when they are on the same day and their times overlap.
Schedule#
app/components/ScheduleBoard.vue shows one day at a time with keyboard-friendly tabs (Left/Right, Home/End).
The day is kept in the URL (/schedule?day=sun); without it, the page opens on the current festival day or
Friday. "Grid" draws the four stages as columns on an hour scale (scrolls sideways on small screens); "List"
shows the day's sets in time order. List is the default below 768px. Planned sets are yellow and clashes get a
red striped background and a "Clash" badge. Links like /schedule?day=fri#set-halcyon-bloom scroll to and
highlight one set.
The My plan panel lists saved sets by day with their clashes, and has two buttons:
- Add to calendar (.ics) downloads
solstice-2027-my-plan.icswith one event per set (times in UTC, location and description included). It works with Apple, Google and Outlook calendars. - Clear plan needs two clicks within three seconds, so a stray tap can't wipe the plan.
To change the hour height of the grid, edit --hour on .tt in main.css.
Tickets and checkout#
app/components/TicketShop.vue. Steppers change quantities from 0 to MAX_PER_TYPE. The order summary lists
every line, then:
- Group discount:
GROUP_DISCOUNT(10%) off the ticket subtotal when an order hasGROUP_SIZE(6) or more tickets. Add-ons are not discounted. A hint shows how many more tickets unlock it. - Service fee:
FEE($4.50) per ticket; add-ons have no fee. - Rules (
problem): an RV site needs a Weekend + camping or Family weekend ticket per RV, and a bell tent needs at least one of those tickets. The summary explains the problem and disables "Continue".
The cart is saved on the device. "Continue to details" shows step 2, a validated form (name, email, ZIP code,
two required checkboxes); a valid submit shows step 3, "Order … is confirmed", with an order reference (SO-
and four characters, from makeRef() in app/utils/festival.ts; use your ticketing provider's own reference
instead if it returns one), the total and a note that the e-tickets are on their way by email, hides the order
summary so the confirmation spans the full width (data-step="done" on [data-checkout-layout]), and empties
the cart. Nothing is charged or sent. To sell real tickets, link the "Continue" button to your ticketing
provider, or send the cart to your own checkout in onValid().
Food village filters and map#
app/components/FoodVillage.vue. "Show" buttons filter by food, sweet or drink; dietary chips can be combined
and vendors must match all of them; the search box matches name, cuisine and dish. Matching pins on the map
stay bright, others fade and leave the tab order. "Show on map" highlights a vendor's pin; clicking a pin (or
pressing Enter or Space on it) highlights the vendor's card. /food?diet=vegan,gluten-free preselects dietary
filters.
Site map layers#
Each layer of the map on /info is a <g class="map-layer" data-layer-group="..."> (stages, food,
toilets (the Restrooms layer), firstaid, water, access). The buttons with the same data-layer toggle it
(toggleLayer() in app/pages/info.vue, with aria-pressed), and "Show all" turns every layer back on. Each
symbol has a <title> tooltip, and "Read the map as a list" gives the same information as text. To add a
symbol, put a new <g> with a <title> inside the right layer group in app/pages/info.vue; to add a layer,
add its name to LAYERS there too.
FAQ search#
app/components/FaqSearch.vue filters the questions from app/data/faq.ts as you type (every word must
match), opens the matches, highlights the words with <mark>, hides empty groups and announces the count.
Clearing the search closes the answers it opened.
Contact routing#
The topic radios in app/components/ContactForm.vue show the matching [data-topic-panel] (volunteer, vendor,
press, general), disable the fields of the others (so they are neither validated nor submitted) and say where
the message goes. /contact?topic=trader preselects a topic. Team names, addresses and reply times are in
ROUTES at the top of the file.
Scroll reveals, tape and back to top#
Elements with .reveal fade in as they scroll into view (app/plugins/reveal.client.ts), and everything above
them is revealed too, so fast jumps never leave gaps. Don't put .reveal on an element that also has a
:class binding: Vue would drop the is-in class the next time it updates. The home page tape scrolls
continuously and pauses on hover. Numbers marked with CountUp count up when they scroll into view. A
back-to-top button (app/components/BackToTop.vue) appears after one screen of scrolling; it lifts above the
toast and steps aside for the sticky order summary (data-back-to-top-hide). All motion is switched off for
visitors who prefer reduced motion.
Demo dates#
The demo is written for June 25–27, 2027. While KEEP_DEMO_CURRENT in app/data/festival.ts is true and that
edition is over (after the last day's curfew), every date moves to the same week of the next year, by whole
weeks so weekdays never change (Friday, June 25, 2027 becomes Friday, June 23, 2028), so the countdown,
early-bird prices, schedule and calendar export always point at an upcoming festival. The year of the edition
follows.
editionShift() in app/utils/edition.ts works out the shift; app/layouts/default.vue sets it once the page
has mounted, and useEdition() gives every component the shifted values: ed.year, ed.dates(),
ed.datesLong(), ed.date("2027-01-31"), ed.dayLabel("fri") and ed.text(), which moves dates such as
"June 1, 2027" or "June 19–24" inside a sentence and swaps the edition years. The page title follows too. The
generated HTML shows the dates as written, so there is no hydration mismatch. Before the edition is over nothing
changes, so ?now=2027-06-26T21:40 still previews the festival weekend. Set KEEP_DEMO_CURRENT = false once
you enter your real dates.
Previewing a date#
Add ?now=YYYY-MM-DDTHH:MM (festival local time) to any page to pretend it is that moment, for example
/tickets?now=2027-03-01T10:00 to see full prices after the early bird ends, or
/schedule?now=2027-06-26T15:00 to open on Saturday. It is read by festivalNow() in app/utils/festival.ts.
Forms#
Forms use useValidation() from app/composables/useValidation.ts: errors appear under each field
(<FieldError>, linked with aria-describedby), aria-invalid is set, and focus moves to the first problem.
Errors show when you leave a filled-in field and update as you type once shown. A group of checkboxes or radios
is a <fieldset data-group-required="message">; a data-error attribute on a field replaces its default
message.
| Form | File | Validates |
|---|---|---|
| Lineup alerts | app/components/AlertsForm.vue |
Email, at least one alert type |
| Checkout | app/components/TicketShop.vue |
Name, email, ZIP code pattern (5 digits, optional +4), age and terms checkboxes |
| Contact | app/components/ContactForm.vue |
Topic, name, email, topic fields, message of 20+ characters |
Each form finishes with an in-world confirmation. The lineup alerts ask subscribers to look out for a
confirmation email. The contact form says which team the message reached, repeats that team's reply time from
ROUTES in ContactForm.vue (the same times the page shows before sending) and shows a reference (SO- and
four characters, made in the browser by makeRef(); use your form service's own reference instead if it
returns one). Change the reply times in ROUTES if yours differ.
The forms don't send anything until you connect them. To connect a form service such as Formspree, send the
data in the form's onSubmit function, where the comment marks the spot:
// in onSubmit, after `if (!(await v.validate(el))) return;`
const res = await fetch("https://formspree.io/f/your-id", {
method: "POST",
body: new FormData(el),
headers: { Accept: "application/json" },
});
if (!res.ok) return; // show an error message instead of the confirmationWith pnpm build (a Node.js server build) you can post to a server route of your own instead, for example
server/api/contact.post.ts with export default defineEventHandler(async (event) => { const data = await readFormData(event); ... }). Show the confirmation only when the service answers OK.
Saved data (localStorage)#
| Key | Shape | Validation |
|---|---|---|
solstice-plan |
Array of artist ids, e.g. ["halcyon-bloom","kemi-sunday"] |
Non-strings, duplicates and unknown ids are dropped and storage is repaired |
solstice-cart |
{ "tickets": { "camping": 2 }, "addons": { "parking": 1 } } |
Unknown ids are dropped, quantities are clamped to 0–MAX_PER_TYPE and the clean cart is saved back; removed when the cart is empty |
Both are read in the browser after the page has mounted, so the generated HTML (and the first render) shows an
empty plan and cart and the saved values appear straight after. safeStorage (app/utils/safeStorage.ts) keeps
a copy in memory when storage is blocked or full, so everything still works for the current page. Visitors can
reset by clearing the plan on the schedule or the site data in their browser; in code, call
safeStorage.remove("solstice-plan").
SEO and sharing#
-
Each page calls
usePageSeo({ title, description, page })(app/composables/usePageSeo.ts), which sets<title>, the meta description,og:title,og:description,og:type,og:site_name,og:image, asummary_large_imageTwitter card, a canonical link anddata-pageon<body>. The dates and year in the title follow the demo dates. -
The canonical URL,
og:urland the absoluteog:imageURL start withruntimeConfig.public.siteUrlinnuxt.config.ts, which readsSITE_URL(defaulthttps://solstice.example). Set it when you build:shSITE_URL=https://www.yourfestival.com pnpm generate -
The 404 page passes
noindex:<meta name="robots" content="noindex">and no canonical URL. -
For richer search results you can add
schema.org/MusicEventorFestivalstructured data (name, dates, location, offers) as JSON-LD withuseHead()on the home page, and a sitemap.
Accessibility#
- Skip link, landmarks, one
<h1>per page and a logical heading order. - Visible focus outlines and WCAG AA color contrast.
- Mobile menu with
aria-expanded; it focuses its first link, locks the page scroll and closes with Esc. - Filters and toggles use
aria-pressed; day tabs use the tabs pattern; the artist pop-up is a native<dialog>that returns focus; counts and totals are announced witharia-live. - The schedule has a list view, each grid column is labeled with its stage, and the site map has a text version. Food map pins are buttons you can reach with Tab.
- Reduced motion turns off reveals, the tape, the turning sun, count-ups and hover lifts.
When editing, keep one <h1> per page, give every new field a <label>, and use PlanButton for sun buttons so
their labels stay up to date.
Building and deploying#
pnpm generate # static site in .output/public/
pnpm preview # serve it locallynuxt generate prerenders every route listed in routes in nuxt.config.ts as <page>.html (lineup.html,
linked as /lineup), 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 /lineup to lineup.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:
DEMO_BASE_PATH=/solstice pnpm generatepnpm 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#
- The lineup or schedule shows nothing for a new artist. Check
day,stage(must match a stageid) andHH:MMtimes in 24-hour format;pnpm typecheckcatches unknown days, stages and genres. - Prices show the full price before the early-bird date. Check
EARLY_BIRD_ENDS(ISO date with a time zone) and remove any?now=from the URL. - The calendar file has the wrong times. Set
UTC_OFFSETto your festival's offset during the event (-04:00for Eastern Daylight Time,-07:00for Pacific Daylight Time). - The dates moved to next year.
KEEP_DEMO_CURRENTistrueand the edition inDAYSis over; set it tofalse(or enter your next dates). - A new page shows the 404 page after
pnpm generate. Add its route toroutesinnuxt.config.ts. /lineupshows a 404 on my host. The host doesn't map extensionless URLs to.htmlfiles; add that rewrite (for example Vercel's"cleanUrls": true).- The social preview shows no image. Build with
SITE_URLset to your domain. pnpm lintfails right after cloning. It needs.nuxt/, whichpnpm installcreates (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. Bricolage
Grotesque, DM Sans and Shrikhand are licensed under the SIL Open Font License (app/assets/fonts/*-OFL.txt).
Solstice 2027, its artists and vendors are fictional.