Overview#
Atlas is a bold, magazine-style personal-brand website for an independent real estate agent or realtor. The demo
content is for Delia Quintero, a fictional Nashville agent who trained as an architect and works in six
neighborhoods. It has filterable active and sold listings, listing pages with illustrated room galleries and open houses
you can add to a calendar, a three-step home value estimate, a seller net sheet, a buyer affordability calculator, an
interactive neighborhood map and an online consultation booking flow. Gloock headlines and a cobalt, persimmon and
butter palette give it an editorial look, and every house and room picture is an SVG drawn from data, so there are no
photos to license. This is the Nuxt version: Nuxt 4, Vue 3 single-file components, TypeScript and Tailwind CSS v4,
generated as a static site, with no other dependencies. This guide covers version 1.0.0 (see template.json and
CHANGELOG.md). Live demo: freeframe.dev/templates/atlas.
Quick start#
Requirements: Node.js 24 and pnpm.
pnpm install
pnpm devOpen http://localhost:3000. Pages reload as you edit. Other scripts:
| Script | What it does |
|---|---|
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 lint |
ESLint |
pnpm typecheck |
nuxt prepare and vue-tsc |
Pages#
| Route | File | Purpose and notable features |
|---|---|---|
/ |
app/pages/index.vue |
Hero with portrait and "I'm buying" / "I'm selling" paths, instant estimate teaser, three featured listings, count-up stats, testimonial carousel, neighborhood teaser with map, booking call to action |
/listings |
app/pages/listings.vue |
Status tabs (for sale, under contract, sold, all), neighborhood, bedroom and price filters with live counts, sort, empty state, URL state, off-market call to action |
/listing |
app/pages/listing.vue |
One page for every home, chosen by ?id=: illustrated gallery, key facts, description, highlights, open houses with .ics downloads, tour request form, similar homes, not-found state |
/sell |
app/pages/sell.vue |
Three-step home value estimate (#valuation), seller net proceeds sheet (#net-sheet), four-week selling plan |
/buy |
app/pages/buy.vue |
Tour-notes sketch, six-step buyer guide, affordability calculator (#afford) with donut chart, FAQ accordion |
/neighborhoods |
app/pages/neighborhoods.vue |
Interactive SVG map with an info panel, six guide cards with statistics and links to filtered listings |
/about |
app/pages/about.vue |
Story and portrait, sold-history chart, timeline, three principles, credentials, personal facts |
/contact |
app/pages/contact.vue |
Consult types, day strip, time slots, details form, live summary, confirmation with .ics download |
| 404 | app/error.vue |
Map with a bobbing pin and links back into the site (noindex) |
Project structure#
atlas/
app/
app.vue <NuxtLayout><NuxtPage>, keyed so a link to the page you're on starts it afresh
error.vue 404 page (generated as 404.html)
router.options.ts query strings written like the HTML version
layouts/default.vue skip link, header, page, footer, back to top
pages/ one file per page (index, listings, listing, sell, buy, neighborhoods, about, contact)
components/
SiteHeader.vue "Open this weekend" strip, sticky header, current page, mobile menu
SiteFooter.vue footer with contact details, links and the Monthly Market Memo signup
LogoMark.vue the DQ house monogram
HouseArt.vue a listing's house or room illustration
ListingCard.vue a listing card
ListingsBrowser.vue tabs, filters, sort, results and URL state
ListingGallery.vue listing gallery
TourForm.vue tour request form
EstimateTeaser.vue home page instant estimate
ReviewCarousel.vue testimonial carousel
ValuationWizard.vue three-step home value estimate
NetSheet.vue seller net sheet
AffordabilityCalculator.vue affordability calculator
FaqList.vue buyer FAQ accordion
BookingFlow.vue consultation booking
FieldError.vue a field's inline error message
BackToTop.vue back-to-top button
composables/
useValidation.ts form validation
usePageSeo.ts per-page title, description, canonical and Open Graph
useCountUp.ts count-up numbers on the home page
useAsset.ts files in public/ with the base path
useMounted.ts, useYear.ts browser-only values (clock, URL) without hydration mismatches
data/
agent.ts AGENT: name, phone, email, office, time zone
areas.ts AREAS: the six neighborhoods, their statistics and guide text
listings.ts LISTINGS, STATUS_LABEL, PRICE_STEPS, SORTS, GALLERY
valuation.ts VALUATION (home value factors) and AFFORD (28/36 rule)
booking.ts CONSULT_TYPES and AVAILABILITY
content.ts NAV, MOBILE_NAV, TESTIMONIALS, FAQ
map.ts MAP_SHAPES: the interactive map's neighborhood shapes
utils/
art.ts house and room drawings (SVG markup from a listing's colors)
estimate.ts estimate(): the home value range
afford.ts affordability(): the 28/36 rule
listingState.ts listings filters, sorting and URL parsing
time.ts Nashville time, next open house, .ics files
format.ts money, moneyShort, num, dates, sold percentage
ref.ts makeRef(): confirmation references
plugins/
reveal.client.ts scroll reveals
same-page.client.ts links to the page you're on reload it, like the HTML version
keep-hash.client.ts keeps the #hash exactly as opened (the map reads it)
stable-build.server.ts reproducible builds
assets/css/main.css Tailwind CSS v4: fonts, @theme tokens and component classes
assets/fonts/ Gloock and Albert Sans woff2 files, OFL.txt
public/img/
favicon.svg the DQ house monogram
delia-portrait.svg illustrated portrait (480 x 600)
og-image.svg, og-image.png social sharing image (1200 x 630)
nuxt.config.ts base path, head defaults, SITE_URL, prerendered routes, Tailwind
template.json gallery metadataCustomizing#
Brand and colors#
All design tokens live in the @theme block at the top of app/assets/css/main.css. Each token becomes Tailwind
utilities (bg-cobalt-600, text-persimmon-700, border-sand-200 and so on).
| Token group | Used for |
|---|---|
--color-paper, --color-sand-50 to --color-sand-300 |
Page background, soft panels, borders |
--color-ink-400 to --color-ink-950 |
Text, the footer and dark sections |
--color-cobalt-50 to --color-cobalt-900 |
The brand blue: primary buttons, links, the estimate and calculator panels |
--color-persimmon-50 to --color-persimmon-700 |
The warm accent: highlighted words, the "selling" path, section numbers |
--color-butter-100 to --color-butter-400 |
Highlights on dark backgrounds, the "under contract" pill |
--color-moss-100 to --color-moss-700 |
Parks and gardens in the art, "sold" results |
To change the brand blue, replace the cobalt scale:
@theme {
--color-cobalt-600: #0f766e;
--color-cobalt-700: #0d5f59;
--color-cobalt-800: #0b4a46;
}Use persimmon-600 or darker behind white text, persimmon-700 for small persimmon text on paper, and ink-600 or
darker for small gray text. Reusable classes such as .btn, .btn-primary, .card, .kicker, .display, .hl,
.swash, .input, .tab, .seg, .slot and .listing-card are in the @layer components block of the same file.
The illustrations use hex colors directly: each listing's art colors in app/data/listings.ts, the GLASS,
LAWN, TREE and TREE_DARK constants in app/utils/art.ts, the map in app/data/map.ts and the page templates and the SVG files
in public/img/. If you change the palette, search for the old hex values (for example #2742cc and #e4552b).
Typography#
The fonts are self-hosted from app/assets/fonts/ with @font-face rules at the top of app/assets/css/main.css:
the exact woff2 files Google Fonts serves to the HTML version, so text sets identically. Gloock
(--font-display, one weight) is used for headlines, prices and big numbers through .display and font-display;
Albert Sans (--font-sans, variable, 400 to 800) is the body face. Each family has a latin and a latin-ext file
split by unicode range. The faces use their plain family names, so the SVG text in the logo, map, chart and footer
wordmark can name "Gloock" and "Albert Sans" directly.
To swap a font, put its woff2 files in app/assets/fonts/, update the @font-face rules, and change
--font-display or --font-sans in the @theme block. Gloock has no italic, so emphasis in headlines uses the .hl
color and the hand-drawn .swash underline instead.
Content#
Page copy is in each page's template in app/pages/. The data behind the interactive parts lives in app/data/:
| Export | File | What it holds |
|---|---|---|
AGENT |
agent.ts |
Name, site name, phone, email, office and time zone (America/Chicago) |
AREAS |
areas.ts |
The six neighborhoods: id, name, ppsf (price per square foot), median, dom (days on market), yoy (one-year change), and the guide card text |
LISTINGS |
listings.ts |
Every home, for sale, under contract or sold |
VALUATION, AFFORD |
valuation.ts |
Home value factors and the affordability rule |
CONSULT_TYPES, AVAILABILITY |
booking.ts |
Bookable consultations, weekly hours, notice, closed dates |
NAV, MOBILE_NAV, TESTIMONIALS, FAQ |
content.ts |
Menus, home page reviews and the buyer FAQ |
MAP_SHAPES |
map.ts |
The interactive map's neighborhood shapes and labels |
A listing looks like this (shortened):
{ id: "utah-4512", status: "active", featured: true, address: "4512 Utah Ave", area: "sylvan-park", zip: "37209",
price: 925000, beds: 3, baths: 2, sqft: 1780, lot: "0.21 acres", year: 1928, type: "Single-family",
style: "Craftsman bungalow", dom: 3, listed: "2026-10-12",
headline: "A Sylvan Park bungalow with the original built-ins",
description: ["First paragraph.", "Second paragraph."],
features: ["Original built-ins and fireplace", "Deep back deck"],
openHouses: [["sat", "13:00", "15:00"]],
art: { style: "bungalow", sky: "#e3ead9", body: "#8fa97a", roof: "#435a39", trim: "#f6f2ea", door: "#a13517" } },To add a home, copy an object in LISTINGS and change it (TypeScript checks the shape):
idmust be unique; it is the?id=of the listing page.statusisactive,pendingorsold. Sold homes also needsold: { price, date }(dateasYYYY-MM-DD).areamust be anidfromAREAS.featured: trueputs a home for sale on the home page (the first three are shown).listed(andsold.date) drive the "Newest" sort.openHousesis a list of[weekday, start, end]in Nashville time, with weekdayssuntosatand 24-hour times. Each one is shown on its next date, so they never go stale. Only homes for sale show open houses.art.stylepicks the house drawing (see Images and artwork).
Listing cards, listing pages, the comparable sales in the estimate, the map counts and the guide buttons all read this array, so nothing else needs updating. The headline numbers (238 homes, 8 days, 101.6%, $171M) appear on the home, sell and about pages; keep them in step when you change them.
Images and artwork#
Everything is SVG:
- Houses.
HOUSESinapp/utils/art.tsdraws a 400 x 300 (4:3) house from a listing'sartobject;<HouseArt>(app/components/HouseArt.vue) wraps it in an<svg>. The styles arevictorian,farmhouse,bungalow,townhome,modern,ranch,foursquare,tudorandcondo; the colors aresky,body,roof,trimanddoor. The cards are rendered when the site is generated, so the drawings are in the HTML. - Rooms. The listing gallery shows the house plus five rooms (
living,kitchen,bedroom,bath,yard) drawn byROOMSand tinted from the same colors (<HouseArt :kind="...">). The order and captions are inGALLERYinapp/data/listings.ts. - Portrait.
public/img/delia-portrait.svg(480 x 600, 4:5) is used on the home, about and listing pages. Replace it with a 4:5 photo; the.archclass rounds the top into an arch. - Map. The stylized map (not to scale) is markup in
app/pages/neighborhoods.vue,app/pages/index.vueandapp/error.vue; the interactive areas on/neighborhoodscome fromMAP_SHAPESinapp/data/map.ts(polygon, fill and label position).
To use real photos for listings, add a photo field to Listing and render an <img> (with useAsset() for files in
public/) instead of <HouseArt> in ListingCard.vue and ListingGallery.vue. Keep a 4:3 aspect ratio so cards
line up.
The social image is public/img/og-image.png (1200 x 630); og-image.svg is its editable source. Export the SVG to
PNG after editing it. The favicon is public/img/favicon.svg.
Navigation, header and footer#
The desktop menu is NAV and the mobile menu MOBILE_NAV in app/data/content.ts; SiteHeader.vue marks the
current page with aria-current="page" (a listing page counts as Listings). The footer's link lists are EXPLORE
and AREA_LINKS at the top of SiteFooter.vue. The "Open this weekend" strip is at the top of SiteHeader.vue:
change its text and /listing?id= link, or delete that <div>. To add a page, create app/pages/<name>.vue and add
its route to routes in nuxt.config.ts.
Features#
Listings filters#
ListingsBrowser.vue runs the listings page; the filter logic is in app/utils/listingState.ts.
- Status tabs are buttons with
data-status(active,pending,sold,all) andaria-pressed. - Filters: neighborhood (
area), minimum bedrooms (beds: 1 to 4), minimum and maximum price (min,max, fromPRICE_STEPS), and sort (newest,price-asc,price-desc,size). Sold homes are filtered and sorted by their sold price. Picking a minimum above the maximum clears the other one. - Live counts: each tab and each neighborhood option shows how many homes you would see if you picked it.
- URL state: the state is written to the address bar with
history.replaceState, for example/listings?status=sold&area=east-nashville&beds=3. Defaults (status=active,sort=newest) are left out and unknown values (?status=banana,?min=7) are dropped (parseListingState()). - Empty state (
[data-empty]) appears when nothing matches, with a Clear filters button that keeps the status and sort and moves focus to the neighborhood filter.
The component reads the URL once the page has loaded, so the generated HTML shows the default view, homes for sale,
newest first. A link to /listings from a filtered listings page (the menu, the footer) opens it afresh, like a page
load (app/plugins/same-page.client.ts). The result count is an aria-live region.
Listing page#
app/pages/listing.vue reads ?id=. Without an id it shows the first listing; with an unknown one it shows "That
listing isn't on my desk" with its own <h1>. The tab title becomes "address, neighborhood — Delia Quintero Real
Estate", set in the browser because a static site can't know the id at build time. If listings matter for search,
consider one prerendered route per home instead (app/pages/listing/[id].vue, with each /listing/<id> added to
routes in nuxt.config.ts).
The gallery (ListingGallery.vue) has previous and next buttons, thumbnails with aria-current, and responds to the left and
right arrow keys when focused. Sold homes show the sold price, the percentage of asking and a "Want one like this?"
panel instead of the tour form; homes under contract say so in the open-house box.
Open houses and calendar files#
nextOpenHouse() in app/utils/time.ts turns each [weekday, start, end] into its next real date in
AGENT.timeZone, skipping one that has already ended today. Times are shown in Central Time, with the visitor's own
time added when they are in another time zone. Because this depends on the clock, open houses are filled in by the
browser after the page loads. "Add to calendar" builds an iCalendar file with buildIcs() (times in UTC, so every
calendar app places them correctly; 11:00 Central on a Saturday in October is DTSTART:...T160000Z) and downloads
it with downloadIcs().
Home value estimate#
The home page teaser (EstimateTeaser.vue) and the three-step wizard on /sell (ValuationWizard.vue) share
estimate() in app/utils/estimate.ts:
value = sqft x area ppsf x type factor x condition factor x (1 + bed and bath adjustment) x (1 + extras)
range = value plus or minus the condition's spread, rounded to $5,000The factors are in VALUATION. For example, an updated 1,780 sq ft house in Sylvan Park comes out at $880,000 to
$970,000. The teaser links to /sell?area=...&sqft=...#valuation, which fills in the wizard. Each step validates
before moving on and moves focus to the next step's heading. The result shows where the estimate sits against the
neighborhood median and lists the agent's sold homes in that area. "Use in the net sheet" copies the midpoint into
the net sheet and scrolls to it.
Seller net sheet#
NetSheet.vue recalculates on every keystroke: sale price minus commission (percent), closing costs (percent),
mortgage payoff and repairs or credits (the defaults net $376,050). The colored bar shows each part as a share of the
price, and a warning appears if costs exceed the price. Money fields (data-money) get thousands separators when you
leave them.
Affordability calculator#
AffordabilityCalculator.vue applies the 28/36 rule from AFFORD: housing costs up to 28% of gross monthly income, and housing
plus debts up to 36%. affordability() in app/utils/afford.ts finds the highest price whose monthly cost (principal and interest, property
tax, insurance, mortgage insurance below 20% down, HOA dues) fits both limits, the component draws an SVG donut of the
payment and links to /listings?max= with the highest price step at or below the result. With the defaults that is
$601,000 at $3,843 a month, linking to homes under $600K. Change FRONT_END, BACK_END, PMI_RATE or PMI_BELOW to
use other assumptions.
Neighborhood map#
On /neighborhoods, app/pages/neighborhoods.vue draws the map and the info panel. Hovering or focusing an area highlights its
guide card ([data-area-card]) and fills the panel; hovering or focusing a card highlights its area. Each area is a
link to its guide, so Tab and Enter work. Selecting an area (or arriving with a hash such as #east-nashville) keeps it
highlighted. Counts of homes for sale come from LISTINGS; the statistics and guide text from AREAS.
To add a neighborhood, add it to AREAS (the guide card appears automatically) and add its shape to MAP_SHAPES.
Consultation booking#
BookingFlow.vue on /contact runs the booking form:
- Consult type: radio cards for
CONSULT_TYPES(buyer,seller,value,relocation).?type=sellerpreselects one, and&listing=<id>fills in the message. - Day: a strip of the next
AVAILABILITY.daysShowndays that have a free slot, skippingcloseddates and days withouthours. - Time: slots every
slotStepminutes inside the day's windows, long enough for the chosen consult and at leastminNoticeHoursfrom now, all in Nashville time. The summary adds the visitor's local time when it differs. - Details: name, email, optional phone, timeline and message.
The summary panel updates live. On submit, missing choices are reported inline and focus moves to the first problem
(the day strip when no day is picked). The confirmation repeats the booking, shows a short reference (DQ- and
four characters, from makeRef() in app/utils/ref.ts) and offers an .ics download that carries the same reference;
"Book another" starts again. demoBusyShare marks a fixed share of slots as taken (the same ones on every load) so the demo
looks realistic; set it to 0 when connected to a real calendar. The days and times depend on the clock, so they are
filled in by the browser.
Carousel, stats, reveals and back to top#
- Testimonials are a native scroll-snap track with previous and next buttons and a "1 / 6" status
(
ReviewCarousel.vue). Swipe, trackpad and keyboard scrolling work. - Count-up stats animate elements with
data-count(and optionaldata-decimals) once they scroll into view (useCountUp()inapp/composables/useCountUp.ts). - Scroll reveals (
app/plugins/reveal.client.ts) fade in.revealelements. Revealing an element also reveals everything above it, so jumping with the End key or an anchor never leaves hidden content. - FAQ buttons toggle their panel and
aria-expanded(FaqList.vue). - Back to top (
BackToTop.vue) appears after a screen of scrolling; remove it fromapp/layouts/default.vueand delete the.back-to-topstyles inmain.cssto drop it.
Animations, smooth scrolling and transitions are turned off for visitors who prefer reduced motion.
Forms#
Every form validates in the browser and shows an in-world confirmation: the newsletter asks the subscriber to look out
for a confirmation email, and the tour request and the booking show a short reference made in the browser (DQ- and
four characters, from makeRef() in app/utils/ref.ts; if your form service or booking tool returns its own reference,
show that instead). The tour confirmation promises an email "within a few hours" and the booking confirmation "within
one business day", matching what the pages already say; change them to match how you actually reply. Nothing is sent
or booked until you connect the forms.
Validation is the useValidation() composable in app/composables/useValidation.ts. A field is checked when it has
data-validate; data-message replaces the "empty" message and the wording is in fieldMessage():
data-validate |
Rule |
|---|---|
required |
Must be filled |
email |
A valid email address (required when the field has required) |
phone-optional |
Blank, or at least 7 digits |
address |
At least 5 characters |
sqft |
Between VALUATION.minSqft and VALUATION.maxSqft square feet |
future-date |
Today or later |
choice |
One radio of the group is chosen (on the group's first radio) |
Errors appear under the field (or the group), set aria-invalid, are linked with aria-describedby, focus the first
problem and clear as the visitor fixes them.
| Form | File | Validates |
|---|---|---|
| Newsletter | app/components/SiteFooter.vue |
|
| Tour request | app/components/TourForm.vue |
Date (today or later), name, email, optional phone |
| Home value wizard | app/components/ValuationWizard.vue |
Address, neighborhood, square feet |
| Booking | app/components/BookingFlow.vue |
Consult type, day, time, name, email, optional phone |
Connecting to Formspree#
Create a form at Formspree, then in subscribe() in SiteFooter.vue or submit() in TourForm.vue (search for
"Connect a real endpoint here") send the data before showing the success state:
async function submit() {
if (!v.validate(form.value)) return;
const res = await fetch("https://formspree.io/f/your-id", {
method: "POST",
body: new FormData(form.value!),
headers: { Accept: "application/json" },
});
if (!res.ok) return alert("Sorry, that didn’t send. Please email hello@deliaquintero.example.");
sent.value = true;
}Every field has a name, so FormData picks them all up; the tour form also sends the listing address in a hidden
listing field.
With a server build (pnpm build, see Building and deploying) you can post to your own
server route instead: server/api/tour.post.ts with export default defineEventHandler(async (event) => { const body = await readFormData(event); ... }), and fetch("/api/tour", ...) from the form.
Connecting a booking tool#
The booking flow is a front end only. Either replace <BookingFlow /> in app/pages/contact.vue with your Cal.com or
Calendly embed, or keep it and send the request in submit() in BookingFlow.vue (the block marked "Connect a real
booking tool"): post type, slot.start.toISOString() and the details fields to your form service or server. Set
AVAILABILITY.demoBusyShare to 0 either way, and load real busy times from your calendar if you keep the flow.
SEO and sharing#
Each page calls usePageSeo({ title, description }) (app/composables/usePageSeo.ts), which sets the exact
<title> and description copied from the HTML version, a canonical link, Open Graph tags (og:image is
public/img/og-image.png) and a Twitter card. The listing page changes its title in the browser; the 404 page passes
noindex: true. Social networks need absolute URLs, so set your domain when building; it becomes
runtimeConfig.public.siteUrl in nuxt.config.ts:
SITE_URL=https://www.your-domain.com pnpm generateConsider adding a sitemap.xml and robots.txt to public/, and RealEstateAgent structured data (schema.org
JSON-LD, with useHead()) with your real name, office and phone. Real estate sites usually must show license and
brokerage details and fair-housing wording: the footer has placeholders (license no. 000000, Larkspur & Main Realty);
replace them with your own and check your state's advertising rules.
Accessibility#
Built in:
- A skip link, landmarks (
header,nav,main,footer), one<h1>per page (the listing not-found state has its own) and a logical heading order. - Visible focus outlines (cobalt on light, butter on dark backgrounds) and WCAG AA contrast for text.
- The mobile menu button uses
aria-expandedandaria-controls; Escape closes the menu and returns focus. - Status tabs and day buttons use
aria-pressed, bedroom and consult choices are native radios; result counts, the estimate teaser, calculator results and the booking summary arearia-liveregions. - The map areas are links with descriptive labels; illustrations are labeled (
role="img") or hidden from screen readers when decorative. Booked time slots are announced as "booked" rather than offered. - Motion is reduced for visitors who prefer it.
When you edit, keep one <h1> per page, give every new input a <label>, and keep the data-* attributes the
components use for validation and state.
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 (listings.html,
linked as /listings), 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 /listings to listings.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=/atlas 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.
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#
| Problem | Fix |
|---|---|
| A listing page says "That listing isn't on my desk" | The ?id= doesn't match any id in LISTINGS. Check the spelling. |
| A new listing doesn't appear | Its status must be active, pending or sold and its area an id in AREAS; pnpm typecheck reports shape errors. Homes for sale only show on the default tab. |
| Open houses show the wrong day or time | Weekdays are sun to sat, times are 24-hour Nashville time, and the zone is AGENT.timeZone (an IANA name such as America/Chicago). |
| No booking days appear | Check AVAILABILITY.hours (day 0 is Sunday) and closed, and that minNoticeHours isn't larger than your windows. |
| A filtered listings link opens unfiltered | The values must be valid: an AREAS id for area, a PRICE_STEPS value for min and max, 1 to 4 for beds. |
| The social preview has no image | Build with SITE_URL set to your domain so og:image is an absolute URL. |
| A new page shows the 404 page on the live site | Add its route to routes in nuxt.config.ts so nuxt generate writes its HTML file. |
| "Hydration mismatch" warnings in the console after an edit | Something reads the clock, the URL or window while rendering. Read it in onMounted() instead, as the listing and booking components do. |
| Content stays invisible after editing | .reveal elements are hidden until app/plugins/reveal.client.ts reveals them; check the browser console for errors. |
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. Gloock and Albert
Sans are under the SIL Open Font License 1.1 (app/assets/fonts/OFL.txt). The agent, brokerage, homes, prices
and clients are fictional.