nuxt-x-restaurants
nuxt-x-restaurants
Restaurant directory layer for Nuxt 4. Provides 55 auto-imported XRD-prefixed components, 11 composables for API integration, filtering, analytics, and menu cards, plus 25 auto-registered routes. Extends nuxt-x-marketing for shared marketing components (header, footer, schema, cards).
What the consumer writes (quickstart)
The ideal consumer is an extends entry, an app/app.config.ts, and one env var — every page, route, layout, and component listed below ships with the layer, including app.vue.
1. Install the layer and its companion layers
npm install @xenterprises/nuxt-x-restaurants @xenterprises/nuxt-x-marketing @xenterprises/nuxt-x-cards
2. nuxt.config.ts — extends + API endpoint
export default defineNuxtConfig({
extends: ['@xenterprises/nuxt-x-restaurants'],
runtimeConfig: {
public: {
apiEndpoint: process.env.NUXT_PUBLIC_API_ENDPOINT,
},
},
})
3. .env
NUXT_PUBLIC_API_ENDPOINT=https://api.yoursite.com
4. app/app.config.ts — site identity
The file must live in app/ (Nuxt 4 srcDir) — an app.config.ts at the project root silently yields only the layer defaults.
export default defineAppConfig({
xRestaurants: {
name: 'My Restaurant Directory',
domain: 'mysite.com',
logo: '/logos/my-logo.webp',
},
})
Done — your directory is live at /, /restaurants, /restaurants/:slug, /cuisines, /neighborhoods, /articles, and /features. Customize the homepage, filters, menu card, and brand via xRestaurants in app/app.config.ts (see Configuration).
Local development without an API (mock backend)
useDomain() builds URLs as ${apiEndpoint}/public/.... Setting apiEndpoint to an empty string turns those into relative requests that hit your own Nuxt server — so you can develop against a mock backend by dropping Nitro routes into server/routes/public/**. This is exactly how the layer's .playground stays self-contained:
// nuxt.config.ts
runtimeConfig: {
public: {
// Empty base URL → the layer calls the app's own mock API routes
// (server/routes/public/**) with relative paths.
apiEndpoint: '',
},
},
The playground's mock (/.playground/server/routes/public/**) implements a working subset you can copy:
server/routes/public/domains/[domain]/info.get.tsserver/routes/public/domains/[domain]/restaurants.get.ts(plusfeatured,new,random,top-ratedvariants)server/routes/public/restaurants/[slug].get.tsserver/routes/public/analytics/track.post.tsserver/utils/fixtures.ts— shared fixture data
Endpoints the mock doesn't implement (menu, reviews, photos, search, …) return null via useDomain()'s error handling, and pages render their empty states — the site still runs.
JSON-LD site identity
useSchema() / useSiteMeta() read site identity from appConfig.xSchema.siteUrl / siteName / siteLogo — the config namespace of the companion @xenterprises/nuxt-x-schema layer. Since v0.10.0 the layer extends nuxt-x-schema directly (its XSchema* components are used by the home/detail/features pages), so OG URLs and JSON-LD site fields render out of the box — set xSchema in app/app.config.ts for your site's values:
// app/app.config.ts
export default defineAppConfig({
xSchema: {
siteUrl: 'https://mysite.com',
siteName: 'My Restaurant Directory',
siteLogo: 'https://mysite.com/logos/my-logo.webp',
},
})
Yelp-Class Features
The listing page (/restaurants) is designed to feel Yelp-class out of the box:
- Open Now indicator — Every card and the profile hero show a live "Open Now" / "Closed" badge that updates with the current local time. An "Open Now" filter narrows the list.
- Three view modes — Toggle between Grid (default cards), List (text-dense rows for browsing more results), and Map (split layout with an embedded city map + scrollable list).
- Sort options — Recommended, Distance (when geolocation is granted), Highest rated, Price (low to high), Most reviewed.
- Distance from user — "1.2 mi" displayed on cards when geolocation is granted; selecting "Distance" in the sort dropdown triggers the permission prompt if not already granted.
- Today's hours status — "Open until 10 PM" or "Closed · Opens 5 PM" displayed in the profile hero.
- URL-synced filters — All filters (cuisine, location, price, rating, amenities, dietary, open-now, vibes, sort) sync to query params so views are shareable.
Yelp-Class Tier 2 Features
The profile and filter surfaces also got Tier 2 upgrades:
- Vibe filter — A multi-dimensional filter combining Good For (Date Night, Family, Business, Groups, Kids) + Atmosphere (Casual, Trendy, Intimate, Upscale) + Noise Level (Quiet, Moderate, Lively). 12 standardized ids in
useVibes(). Auto-hides options not present in the data. - Hours graph —
XRDProfileHoursnow renders a visual weekly bar chart with today highlighted and a current-time marker dot on today's row. Falls back gracefully when hours data is sparse. - Similar Restaurants section — Every profile page ends with
XRDProfileSimilar— up to 4 related restaurants ranked by shared cuisine (+10 each), neighborhood (+5), and price (+2). - Owner responses —
XRDProfileReviewsItemnow displays business-owner replies in a distinct quoted block with an "Owner" badge, schema.orgReview.responseshape supported (string or{ text, date, author }).
Tier 3 — Discovery & SEO
The directory also generates SEO-optimized landing pages for every cuisine and neighborhood:
- Cuisine landing pages (
/cuisines/[slug]) — One page per cuisine type, e.g./cuisines/italian,/cuisines/japanese. Auto-generated from restaurant data — no manual pages. Cards link back to the cuisine page via cuisine badges. - Neighborhood landing pages (
/neighborhoods/[slug]) — Same pattern, filtered byaddressLocality/neighborhood. - Cover photo carousel —
XRDProfileHeroupgraded to a multi-image carousel with prev/next arrows, photo N/M counter, and a thumbnail strip for direct navigation. - Popular in your area —
XRDProfilePopularInAreaat the bottom of profile pages shows top-rated same-neighborhood restaurants.
Tier 4 — Small components & CTAs
The profile surface also got small standalone components:
- Call / Directions / Reservation CTAs — Already wired in
XRDProfileSidebar: phone (tel:link), directions (deep link to Google Maps via ContactInfo), reservation button (reservationUrlfrom API). - "Mentioned by reviewers" tags —
XRDProfileMentionedTermsshows top-8 terms by frequency from review comments usinguseMentionedTerms(word-frequency + stopword filtering). No NLP needed.
Polish Round 1 — Production-readiness
- Index pages —
/cuisinesand/neighborhoodsindex pages list every available cuisine/neighborhood with restaurant counts and links to detail pages. Card-grid UX. - Schema.org
ItemListJSON-LD — Cuisine and neighborhood detail pages emitItemListschema (top 50 items) for SEO rich results. - Dynamic sitemap —
server/api/__sitemap__/urls.tsenumerates ALL cuisine + neighborhood URLs from restaurant data at build time. Major SEO multiplier — every cuisine and every neighborhood in the directory is now indexable. - Loading skeletons —
XRDRestaurantCardSkeleton(matches card dimensions, no CLS) used in/restaurants,/cuisines/[slug],/neighborhoods/[slug]while data fetches. - Error states —
UAlertshown on cuisine/neighborhood pages whenuseAsyncDatafails, replacing the silent empty grid.
What This Layer Provides
Components (prefix: XRD)
- Layout / Shell —
XRDNavbar,XRDFooter,XRDComingSoon - Homepage (13) —
XRDHomePageHero,XRDHomePageFeaturedRestaurants,XRDHomePageNewRestaurants,XRDHomePagePopularThisWeek,XRDHomePageSpecialOffers,XRDHomePageLatestStories,XRDHomePageLocalDiningGuide,XRDHomePageShowcase,XRDHomePageGrid,XRDHomePageStats,XRDHomePageTestimonials,XRDHomePageFAQ,XRDHomePageFeatures - Filters (10) —
XRDFilters,XRDFiltersAsideFilters,XRDFiltersActiveFilters,XRDFiltersCuisineFilter,XRDFiltersPriceFilter,XRDFiltersRatingFilter,XRDFiltersLocationFilter,XRDFiltersAmenitiesFilter,XRDFiltersDietaryFilter,XRDFiltersVibeFilter - Profile (26) —
XRDProfileHero,XRDProfileCard,XRDProfileListItem,XRDProfileBreadcrumb,XRDProfileNavigation,XRDProfileAbout,XRDProfileDescription,XRDProfileContact,XRDProfileContactInfo,XRDProfileHours,XRDProfileMap,XRDProfileCuisine,XRDProfileDietary,XRDProfileAmenities,XRDProfileMenuSection,XRDProfileMenuItem,XRDProfileReviews,XRDProfileReviewsItem,XRDProfileSidebar,XRDProfileSocial,XRDProfileFAQ,XRDProfilePromotions,XRDProfileNewsletter,XRDProfileSimilar,XRDProfilePopularInArea,XRDProfileMentionedTerms - Utility —
XRDStarRating,XRDMenuCard,XRDRestaurantCardSkeleton
Composables
useDomain()— API endpoint management for restaurant datauseRestaurantFilters()— restaurant listing filter state with URL sync (cuisine, location, price, rating, amenities, dietary, open-now, sort)useFilters()— generic filter logic with custom handler registrationuseMenuCard()— menu card configuration fromapp.config.tsuseRestaurant(slug)— individual restaurant data fetchinguseAnalytics()— client-side analytics event trackinguseOpenNow(hours)— whether a restaurant is currently open, plus closing/next-opening timeuseGeolocation()— browser geolocation + Haversine distance helpersuseVibes()— standardized vibe ids for Good For / Atmosphere / Noise filtersuseMentionedTerms(reviews)— extract top-N most-mentioned terms from reviews (word-frequency + stopwords)useSchema()— Schema.org JSON-LD composables (Article, Review, Product, ItemList, BreadcrumbList) plususeSiteMetafor title/description/OG/Twitter Card boilerplate
Pages (Auto-Registered)
| Route | Description |
|---|---|
/ | Homepage with hero, featured, popular, new, stories, showcase, FAQ. |
/restaurants | Listing with sidebar filters and pagination. |
/restaurants/:slug | Profile layout (data provider for child pages). |
/restaurants/:slug/ | Profile overview (about, FAQ, contact). |
/restaurants/:slug/menu | Restaurant menu with dietary accommodations. |
/restaurants/:slug/reviews | Reviews display. |
/restaurants/:slug/photos | Photo gallery. |
/restaurants/:slug/articles | Related articles. |
/articles | Blog/articles listing (Nuxt Content). |
/articles/* | Article detail (Nuxt Content catch-all articles/[...slug].vue). |
/features | Features listing (Nuxt Content). |
/features/* | Feature detail (Nuxt Content catch-all features/[...slug].vue). |
/cuisines | Cuisine index — every cuisine with restaurant counts. |
/cuisines/:slug | Cuisine landing page with ItemList JSON-LD. |
/neighborhoods | Neighborhood index — every neighborhood with restaurant counts. |
/neighborhoods/:slug | Neighborhood landing page with ItemList JSON-LD. |
/popular | Popular restaurants listing. |
/popular/:slug | 301 redirect → /restaurants/:slug. |
/explore | 301 redirect → /restaurants. |
/about | About the directory. |
/contact | Contact page. |
/list-restaurant | Restaurant submission landing page. |
/privacy | Privacy policy (Nuxt Content). |
/terms | Terms of service (Nuxt Content). |
/sitemap | HTML sitemap. |
Composable Reference
useDomain()
Builds typed API endpoint methods against ${apiEndpoint}/public/domains/${xRestaurants.domain} (per-restaurant endpoints resolve directly against ${apiEndpoint}/public/restaurants/${slug}). Returns: domain, siteName, siteUrl, getInfo(), getRestaurants(params), getRestaurantsByCuisine(), getRestaurantsByNeighborhood(), getRestaurantsByPrice(), getRestaurantBySlug(), getRestaurantMenuBySlug(), getRestaurantReviewsBySlug(), getRestaurantPhotosBySlug(), getNewestRestaurants(limit), getRestaurantsFeatured(), getRestaurantsNew(), getRestaurantsRandom(), getRestaurantsSearch(q), getRestaurantsTopRated(), getSitemap(), suggestRestaurant(body).
useRestaurantFilters()
Restaurant listing filter state with SSR-safe URL sync (cuisines, locations, prices, rating, amenities, dietary query params). State is shared via useState across the listing, filter panel, and active-filter chips. Returns: selectedCuisines, selectedLocations, selectedRatingMin, selectedPriceLevels, selectedAmenities, selectedDietaryOptions, filteredRestaurants, totalCount, pending, availableCuisines, availableLocations, availableAmenities, hasActiveFilters, activeFilterCount, initializeFromUrl(), updateUrl(), toggleCuisine(), toggleLocation(), togglePriceLevel(), toggleAmenity(), toggleDietaryOption(), setRating(), clearAllFilters(), refresh().
useFilters()
Low-level generic filter composable. Reads group config from appConfig.xRestaurants.filters and exposes config, activeFilters, hasActiveFilters, activeFiltersCount, isMobileFiltersOpen, updateFilter(), toggleCheckbox(), isCheckboxSelected(), clearAllFilters(), filterItems(items), registerFilterHandler(id, handler).
useMenuCard()
Reads appConfig.xRestaurants.menuCard and exposes config, defaultDietaryTags, getDietaryTag(id), getItemTags(item), formatPrice(price, currency). Used internally by XRDMenuCard and XRDProfileMenuItem.
useRestaurant(slug)
Per-restaurant fetch helpers: getRestaurant(), getRestaurantSchema(), getRestaurantReviews(), getRestaurantFAQs(), getRestaurantMenu(), getRestaurantPhotos(). Each wraps useAsyncData so results are cached per slug.
useAnalytics()
Client-side analytics tracking. Posts events to ${apiEndpoint}/public/analytics/track. Returns trackEvent(restaurantId, eventType, eventData?, source?) plus convenience wrappers: trackPageView, trackClickWebsite, trackClickPhone, trackClickMap, trackFaqView, trackSearchImpression, trackSearchClick. Silently no-ops on the server and on errors so analytics never break the UI.
Configuration (app.config.ts)
export default defineAppConfig({
xRestaurants: {
name: "My Restaurant Directory",
domain: "mysite.com",
logo: "/logos/my-logo.webp",
navbar: {
links: [
{ label: "Explore", to: "/restaurants" },
{ label: "Articles", to: "/articles" },
],
},
homePage: {
hero: {
title: "Find the Best Dining",
description: "Explore top-rated restaurants...",
backgroundImage: "https://...",
searchButtonLabel: "Search",
cuisinePlaceholder: "Cuisine",
locationPlaceholder: "Neighborhood",
},
featured: {
title: "Featured Places",
description: "Hand-picked selections.",
viewAllLink: "/restaurants",
viewAllLabel: "View All",
viewButtonLabel: "View",
},
latestStories: {
title: "Latest Food Stories",
description: "...",
readMoreLabel: "Read Article",
},
popular: {
title: "Popular This Week",
viewDetailsLabel: "View Details",
},
localGuide: {
title: "Your Guide to Local Dining",
paragraphs: [],
},
faq: {
title: "Frequently Asked Questions",
items: [{ label: "...", content: "..." }],
},
// Every section also takes `enabled: false` to hide it:
// hero, featured, popular, newRestaurants, localGuide,
// latestStories, showcase, faq.
},
comingSoon: {
enabled: false,
title: "",
description: "",
backgroundImage: "",
launchDate: "",
},
menuCard: {
showImage: true,
showDescription: true,
showTags: true,
showSpiceLevel: false,
showCalories: false,
showPrepTime: false,
imageAspect: "video", // 'square' | 'video' | 'wide'
variant: "default", // 'default' | 'compact' | 'horizontal'
dietaryTags: [
{ id: "vegetarian", label: "Vegetarian", icon: "i-heroicons-leaf", color: "green" },
],
},
filters: {
enabled: true, // false hides the /restaurants filters aside
title: "Filters",
groups: [],
showClearAll: true,
mobileBreakpoint: "lg", // 'sm' | 'md' | 'lg' | 'xl'
// Optional: directory aside sections — subset, order, labels,
// default-open. Omit for the full default set.
// sections: [
// { id: "cuisine", label: "Cuisine", defaultOpen: true },
// { id: "price" }, // location | cuisine | price | amenities | dietary | vibe
// ],
},
analytics: {
enabled: true, // false disables all useAnalytics tracking
},
},
});
See config/index.md for the full schema.
Opting Out of Defaults
Everything the layer renders can be turned off or replaced without forking:
- Pages — Nuxt page shadowing: a file at the same path in your own
app/pages/wins over the layer's (e.g. yourapp/pages/about.vuereplaces the default about page). - Homepage sections —
xRestaurants.homePage.<section>.enabled: falsehides a section (hero,featured,popular,newRestaurants,localGuide,latestStories,showcase,faq). - Directory filters aside —
xRestaurants.filters.enabled: falsehides it on/restaurants;xRestaurants.filters.sectionscontrols which sections render, their order, labels, and default-open state. - Analytics —
xRestaurants.analytics.enabled: falsedisables alluseAnalytics()tracking. - Components/layouts — component shadowing works the same way: your own
app/components/X/RD/...orapp/layouts/default.vuetakes precedence.
Environment Variables
| Variable | Required | Description |
|---|---|---|
NUXT_PUBLIC_API_ENDPOINT | Yes | Base URL for the restaurant API (e.g. https://api.example.com). |
NUXT_PUBLIC_SITE_URL | No | Public site URL used in meta tags and schema. |
NUXT_PUBLIC_SITE_NAME | No | Site name used in meta tags. |
NUXT_SITE_URL | No | Site URL for @nuxtjs/seo. |
NUXT_SITE_NAME | No | Site name for @nuxtjs/seo. |
How It Works
The layer extends nuxt-x-marketing for shared marketing components (header, footer, schema, cards) and adds restaurant-specific functionality:
- API Integration:
useDomain()builds API URLs fromruntimeConfig.public.apiEndpoint+ the domain configured inapp.config.ts. Per-domain collection endpoints live under${apiEndpoint}/public/domains/${domain}; per-restaurant detail endpoints live under${apiEndpoint}/public/restaurants/${slug}. All read methods returnnullon any error (network, 404, 500) — pages can writeif (!result)instead of try/catch boilerplate. The one exception issuggestRestaurant, which throws so/list-restaurantcan show the API's user-facing error message. - Client-Side Filtering:
useRestaurantFilters()manages filter state viauseState(SSR-safe) with URL query parameter sync (cuisines,locations,prices,rating,amenities,dietary). It also exposes derived option lists (availableCuisines,availableLocations,availableAmenities) andfilteredRestaurants/totalCountfrom the API. The lower-leveluseFilters()composable powers the menu filter groups with custom-handler support. - Profile Pages: The
restaurants/[slug].vuelayout fetches restaurant data once viauseAsyncDataandprovides it to child pages via Vue'sprovide/inject. JSON-LDLocalBusinessschema is injected automatically. - Analytics:
useAnalytics()posts page-view, click, and search events client-side. All failures are swallowed so analytics never break the UI. - Content Integration: Articles, features, privacy, and terms use Nuxt Content v3 with markdown files in
content/articles/,content/features/,content/misc/, etc. The homepageXRDHomePageLocalDiningGuidereadscontent/misc/homepage-seo, andXRDHomePageFAQreadscontent/misc/faqs. - Coming Soon Mode: When
appConfig.xRestaurants.comingSoon.enabledistrue(or the configured domain isn't found by the API), the homepage rendersXRDComingSoonusing thebarelayout (no navbar/footer wrapper). - Components: All 55 components live under
app/components/X/RD/and are auto-imported by Nuxt with theXRDprefix convention. They use Nuxt UI v4 primitives.
AI Context
package: "@xenterprises/nuxt-x-restaurants"
version: "0.10.0"
type: nuxt-layer
prefix: XRD
peer-dependencies:
- "@nuxt/ui >= 4.6.1"
- "@nuxt/content >= 3.13.0"
- "@nuxtjs/seo >= 3.3.0"
- "@xenterprises/nuxt-x-marketing >= 1.1.1"
- "@xenterprises/nuxt-x-cards >= 0.2.2"
- embla-carousel
extends:
- "@xenterprises/nuxt-x-marketing"
use-when: >
Building a restaurant directory site with Nuxt 4. Provides 55 XRD-prefixed
components for listing, filtering, and profile pages, 11 composables for API
integration, filtering, analytics, schema.org JSON-LD, open-now status,
geolocation, vibes, and mentioned-terms extraction (useDomain,
useRestaurantFilters, useFilters, useMenuCard, useRestaurant, useAnalytics,
useOpenNow, useGeolocation, useVibes, useMentionedTerms, useSchema), and
25 auto-registered routes including restaurant profiles, cuisine and
neighborhood landing pages, articles, features, and coming-soon mode.
Listing page supports Grid / List / Map view modes, Open Now filter, sort
by distance/rating/price/reviews, and URL-synced filter state. Schema.org
JSON-LD (Article, Review, Product, BreadcrumbList, ItemList) is emitted
via the useSchema composable on every page that needs it. Extends
nuxt-x-marketing for shared layout components. Requires
NUXT_PUBLIC_API_ENDPOINT to be set.
