npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@cremadesign/menu-schema-generator

v1.0.0

Published

Browser-side class that reads a restaurant page's menu from the live DOM and attaches it to the page's schema.org Restaurant as a nested Menu (hasMenu).

Readme

Menu Schema Generator

A reusable, browser-side ES6 class that reads a restaurant page's menu out of the live DOM and attaches it to that page's schema.org Restaurant as a nested Menu (hasMenu). No build step, no dependencies, no per-site configuration.

It finds the menu region by content density, reads sections and items by structure rather than by fixed tag names (so it survives an h4 becoming an h3), and writes a JSON-LD <script> — creating one, or merging into an existing Restaurant. A bar page is modeled as a department (BarOrPub).

Features

  • Zero-config — works across very different menu markups (h4 + p, h3 + p, p > strong + em, ul > li, <br>-separated price lines) with no options.
  • Structure-based parsing — density-scored content root, relative-heading sectioning, role-based item detection; resilient to tag changes.
  • Prices → Offers — auto-detects the page's price separator; single prices and labeled multi-prices (Glass/Bottle, Cup/Bowl, "14 (shrimp), 15 (steak)") become Offers. Items without prices are still emitted.
  • Bar as department — a bar/landing page is detected from the URL and nested under Restaurant.department (BarOrPub).
  • Idempotent — safe to call repeatedly; merges into an existing Restaurant in place without clobbering sibling fields, or injects a marked <script>.
  • Fail-closed — never throws into the page, never injects a partial result; returns null on a no-op.

Usage

Include the script and call build() once the DOM is ready:

<script src="MenuSchemaGenerator.js"></script>
<script>
	new MenuSchemaGenerator().build();
</script>

That single call is all most pages need. The helper below adds DOM-ready timing and a debug flag read from the URL.

Drop-in initializer

// Reads ?debug=true|1|on (or a bare ?debug) from the current URL. The site
// decides the environment; the generator just receives the flag.
const isDebug = (() => {
	const value = new URLSearchParams(location.search).get('debug');
	return value === '' || value === 'true' || value === '1' || value === 'on';
})();

// Runs the generator once the DOM is parsed.
function initMenuSchema(options = {}) {
	const start = () => new MenuSchemaGenerator({ debug: isDebug, ...options }).build();
	if (document.readyState === 'loading') {
		document.addEventListener('DOMContentLoaded', start);
	} else {
		start();
	}
}

Debug detection lives in the host page, not the generator. The flag is read fresh per URL — to keep diagnostics on across a multi-page menu, append ?debug=true to each page.

Per-site examples

The same initMenuSchema() call handles every layout below with zero config.

All locations on one page, split into tabs — section tabs become MenuSections; an existing Restaurant schema on the page is matched and its hasMenu is filled in. A "landing"/bar page is detected from the URL and modeled as a department (BarOrPub).

initMenuSchema();
// Force/deny the department with { department: 'BarOrPub' } or { department: false }.

Menu spread across separate pages (one category per page) — each page yields one MenuSection under a generic Menu, and every page shares the same @id (the site origin) so the pages resolve to one restaurant. A wine page nests sub-sections (Rosé/White/Red) and emits Glass/Bottle offers automatically; a router page or an empty page is a no-op.

initMenuSchema();

A restaurant with an in-restaurant bar — the dining menu and the /bar page are separate pages; the bar page is detected from the URL and emitted as department: BarOrPub. Pages without prices still emit valid MenuItems (without offers).

initMenuSchema();

A single menu page — tabbed sections with strong/em item markup parse the same as every other site.

initMenuSchema();

Optional overrides (rarely needed)

initMenuSchema({
	restaurantName: 'Georgia Blue',
	restaurantUrl: 'https://georgiablue.net',
	schemaId: 'georgia-blue-madison',
	mergeStrategy: 'preserve',
	currency: 'USD'
});

Running code after the schema is attached

onComplete runs after every build() with { schema, injected, script, diagnostics }: the Restaurant object (or null on a no-op), whether a menu was written, the affected <script> element, and the debug diagnostics. A throwing callback is caught and never breaks the page.

initMenuSchema({
	onComplete({ schema, injected, script, diagnostics }) {
		if (!injected) {
			return; // no parseable menu on this page — nothing was written
		}
		// The schema was attached; react to it here.
		script.id = 'menu-schema';
		if (diagnostics) {
			console.table(diagnostics);
		}
	}
});

API

const generator = new MenuSchemaGenerator(options);
const restaurant = generator.build(); // the Restaurant object, or null on a no-op

build() parses the page, injects/updates the JSON-LD <script>, returns the Restaurant object (or null), and fires onComplete.

Options

All optional.

| Option | Purpose | Default | |---|---|---| | restaurantName | Override the derived restaurant name | og:site_name → cleaned <title> | | restaurantUrl | Override the canonical URL used for @id/url | <link rel=canonical>og:urllocation.origin | | menuName | Set Menu.name | omitted | | schemaId | id of an existing JSON-LD script to merge into | none → match by @type | | department | 'BarOrPub' to force a department, false to deny | auto-detected from URL | | mergeStrategy | 'replace' or 'preserve' for an existing structured hasMenu | 'replace' | | currency | priceCurrency for offers | 'USD' | | rootSelector | Force the content root | density detection | | ignoreSelector | CSS selector of subtrees to skip | DEFAULT_IGNORE_SELECTOR | | containerPattern | RegExp of wrapper tags to recurse into | DEFAULT_CONTAINER_PATTERN | | debug | Console warnings + a diagnostics object on the result | false | | onComplete | Callback run after every build() (see above) | none |

ignoreSelector and containerPattern default to the static DEFAULT_* fields at the top of the class.

Output

{
  "@context": "https://schema.org",
  "@type": "Restaurant",
  "@id": "https://example.com",
  "url": "https://example.com",
  "name": "Example",
  "hasMenu": {
    "@type": "Menu",
    "hasMenuSection": [
      {
        "@type": "MenuSection",
        "name": "Appetizers",
        "hasMenuItem": [
          {
            "@type": "MenuItem",
            "name": "Turnip Green Bites",
            "description": "…",
            "offers": { "@type": "Offer", "price": "13", "priceCurrency": "USD" }
          }
        ]
      }
    ]
  }
}

For a bar page, the Menu hangs off department: Restaurant → department: { "@type": "BarOrPub", "hasMenu": Menu }.

Notes

  • Browser-only ES6. Runs in the browser against the live DOM; no Node runtime is required.
  • @id is the canonical origin of the restaurant, so multiple pages of the same site resolve to one entity. It needs a <link rel="canonical">, og:url, or a real location.origin — a file:// load falls back to the file URL.
  • schema.org keys are emitted verbatim (Menu, MenuSection, MenuItem, Offer, hasMenu, …).