X Enterprises

nuxt-x-restaurants

Restaurant directory Nuxt layer — 55 components for listing, filtering, and profile pages, 11 composables for API integration, filtering, analytics, schema.org JSON-LD, and menu cards, plus 25 auto-registered routes.

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.ts
  • server/routes/public/domains/[domain]/restaurants.get.ts (plus featured, new, random, top-rated variants)
  • server/routes/public/restaurants/[slug].get.ts
  • server/routes/public/analytics/track.post.ts
  • server/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 graphXRDProfileHours now 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 responsesXRDProfileReviewsItem now displays business-owner replies in a distinct quoted block with an "Owner" badge, schema.org Review.response shape 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 by addressLocality / neighborhood.
  • Cover photo carouselXRDProfileHero upgraded to a multi-image carousel with prev/next arrows, photo N/M counter, and a thumbnail strip for direct navigation.
  • Popular in your areaXRDProfilePopularInArea at 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 (reservationUrl from API).
  • "Mentioned by reviewers" tagsXRDProfileMentionedTerms shows top-8 terms by frequency from review comments using useMentionedTerms (word-frequency + stopword filtering). No NLP needed.

Polish Round 1 — Production-readiness

  • Index pages/cuisines and /neighborhoods index pages list every available cuisine/neighborhood with restaurant counts and links to detail pages. Card-grid UX.
  • Schema.org ItemList JSON-LD — Cuisine and neighborhood detail pages emit ItemList schema (top 50 items) for SEO rich results.
  • Dynamic sitemapserver/api/__sitemap__/urls.ts enumerates 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 skeletonsXRDRestaurantCardSkeleton (matches card dimensions, no CLS) used in /restaurants, /cuisines/[slug], /neighborhoods/[slug] while data fetches.
  • Error statesUAlert shown on cuisine/neighborhood pages when useAsyncData fails, replacing the silent empty grid.

What This Layer Provides

Components (prefix: XRD)

  • Layout / ShellXRDNavbar, 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
  • UtilityXRDStarRating, XRDMenuCard, XRDRestaurantCardSkeleton

Composables

  • useDomain() — API endpoint management for restaurant data
  • useRestaurantFilters() — restaurant listing filter state with URL sync (cuisine, location, price, rating, amenities, dietary, open-now, sort)
  • useFilters() — generic filter logic with custom handler registration
  • useMenuCard() — menu card configuration from app.config.ts
  • useRestaurant(slug) — individual restaurant data fetching
  • useAnalytics() — client-side analytics event tracking
  • useOpenNow(hours) — whether a restaurant is currently open, plus closing/next-opening time
  • useGeolocation() — browser geolocation + Haversine distance helpers
  • useVibes() — standardized vibe ids for Good For / Atmosphere / Noise filters
  • useMentionedTerms(reviews) — extract top-N most-mentioned terms from reviews (word-frequency + stopwords)
  • useSchema() — Schema.org JSON-LD composables (Article, Review, Product, ItemList, BreadcrumbList) plus useSiteMeta for title/description/OG/Twitter Card boilerplate

Pages (Auto-Registered)

RouteDescription
/Homepage with hero, featured, popular, new, stories, showcase, FAQ.
/restaurantsListing with sidebar filters and pagination.
/restaurants/:slugProfile layout (data provider for child pages).
/restaurants/:slug/Profile overview (about, FAQ, contact).
/restaurants/:slug/menuRestaurant menu with dietary accommodations.
/restaurants/:slug/reviewsReviews display.
/restaurants/:slug/photosPhoto gallery.
/restaurants/:slug/articlesRelated articles.
/articlesBlog/articles listing (Nuxt Content).
/articles/*Article detail (Nuxt Content catch-all articles/[...slug].vue).
/featuresFeatures listing (Nuxt Content).
/features/*Feature detail (Nuxt Content catch-all features/[...slug].vue).
/cuisinesCuisine index — every cuisine with restaurant counts.
/cuisines/:slugCuisine landing page with ItemList JSON-LD.
/neighborhoodsNeighborhood index — every neighborhood with restaurant counts.
/neighborhoods/:slugNeighborhood landing page with ItemList JSON-LD.
/popularPopular restaurants listing.
/popular/:slug301 redirect/restaurants/:slug.
/explore301 redirect/restaurants.
/aboutAbout the directory.
/contactContact page.
/list-restaurantRestaurant submission landing page.
/privacyPrivacy policy (Nuxt Content).
/termsTerms of service (Nuxt Content).
/sitemapHTML 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. your app/pages/about.vue replaces the default about page).
  • Homepage sectionsxRestaurants.homePage.<section>.enabled: false hides a section (hero, featured, popular, newRestaurants, localGuide, latestStories, showcase, faq).
  • Directory filters asidexRestaurants.filters.enabled: false hides it on /restaurants; xRestaurants.filters.sections controls which sections render, their order, labels, and default-open state.
  • AnalyticsxRestaurants.analytics.enabled: false disables all useAnalytics() tracking.
  • Components/layouts — component shadowing works the same way: your own app/components/X/RD/... or app/layouts/default.vue takes precedence.

Environment Variables

VariableRequiredDescription
NUXT_PUBLIC_API_ENDPOINTYesBase URL for the restaurant API (e.g. https://api.example.com).
NUXT_PUBLIC_SITE_URLNoPublic site URL used in meta tags and schema.
NUXT_PUBLIC_SITE_NAMENoSite name used in meta tags.
NUXT_SITE_URLNoSite URL for @nuxtjs/seo.
NUXT_SITE_NAMENoSite 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:

  1. API Integration: useDomain() builds API URLs from runtimeConfig.public.apiEndpoint + the domain configured in app.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 return null on any error (network, 404, 500) — pages can write if (!result) instead of try/catch boilerplate. The one exception is suggestRestaurant, which throws so /list-restaurant can show the API's user-facing error message.
  2. Client-Side Filtering: useRestaurantFilters() manages filter state via useState (SSR-safe) with URL query parameter sync (cuisines, locations, prices, rating, amenities, dietary). It also exposes derived option lists (availableCuisines, availableLocations, availableAmenities) and filteredRestaurants / totalCount from the API. The lower-level useFilters() composable powers the menu filter groups with custom-handler support.
  3. Profile Pages: The restaurants/[slug].vue layout fetches restaurant data once via useAsyncData and provides it to child pages via Vue's provide/inject. JSON-LD LocalBusiness schema is injected automatically.
  4. Analytics: useAnalytics() posts page-view, click, and search events client-side. All failures are swallowed so analytics never break the UI.
  5. Content Integration: Articles, features, privacy, and terms use Nuxt Content v3 with markdown files in content/articles/, content/features/, content/misc/, etc. The homepage XRDHomePageLocalDiningGuide reads content/misc/homepage-seo, and XRDHomePageFAQ reads content/misc/faqs.
  6. Coming Soon Mode: When appConfig.xRestaurants.comingSoon.enabled is true (or the configured domain isn't found by the API), the homepage renders XRDComingSoon using the bare layout (no navbar/footer wrapper).
  7. Components: All 55 components live under app/components/X/RD/ and are auto-imported by Nuxt with the XRD prefix 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.
Copyright © 2026