Restaurant for WooCommerce
Turn WooCommerce products into a one-page restaurant ordering menu with dish popups, quantity steppers, a Veg / Non-veg filter and a slide-in basket. Orders use your normal WooCommerce checkout.
Version 1.2 WooCommerce plugin Free plugin Requires WordPress 6.3 Requires PHP 7.4 Tested up to 7.1 Updated 6 Oct 2026
1. Overview
Restaurant for WooCommerce turns your WooCommerce products into a one-page ordering menu. You put the shortcode [wxp_restaurant] on a page. Your product categories become the sections of the menu and your products become the dishes. Customers search, filter, add dishes, choose sizes or other variations in a popup, and go to the normal WooCommerce cart and checkout from a slide-in basket, without leaving the page.
The plugin keeps no menu of its own. Names, photos, prices (sale prices included), short descriptions, stock, categories and their order all come from your WooCommerce products. The only thing it adds to a product is one tick box, Is Non-Vegetarian. Orders are ordinary WooCommerce orders, so your payment methods, shipping methods (delivery or local pickup), coupons, taxes and order emails keep working as they do today.
It suits restaurants, cafés, takeaways, pizzerias, bakeries, food trucks and cloud kitchens that already sell through WooCommerce. A paid companion, Advanced Restaurant for WooCommerce, adds extras, opening hours and delivery slots; see How it relates to Advanced Restaurant for WooCommerce.
How it works
- You put your dishes in WooCommerce product categories (for example Starters, Mains, Drinks).
- You add
[wxp_restaurant]to a page. When the page loads, the plugin lists the categories that have published products, in WooCommerce's category order, and the products of each category in WooCommerce's product order. - A customer taps Add on a simple dish, or opens the dish popup to choose a variation. The plugin sends the request to WooCommerce's own AJAX endpoint (
?wc-ajax=rfw_add_to_cart), WooCommerce validates it like any add-to-cart, and the basket on the page is refreshed with the result. - The customer opens the basket and taps Checkout or View cart. From there everything is standard WooCommerce.
At a glance
One page, one shortcode
All categories and dishes on one page, with a category list that follows the scroll. Limit and order the sections with categories="starters,mains".
Quick ordering
A one-tap Add button that becomes a quantity stepper, a dish popup for variations, and a slide-in basket with quantity controls and a subtotal.
Search and Veg filter
Live search in dish names and short descriptions, plus a Veg only or Non-veg only filter driven by the Is Non-Vegetarian product tick box.
Your WooCommerce rules
Stock limits, "Sold individually", "Hide out of stock items", product visibility and other plugins' add-to-cart validation all apply to orders placed from the menu.
Phone first, accessible
Real buttons, keyboard use, focus handling in the popup and basket, reduced-motion support, and a menu script that needs no jQuery and no icon font (your theme may still load jQuery).
Nothing to configure
There is no settings screen. You change colours with CSS variables and the layout with theme template overrides.
What it does not do
- It has no settings. WooCommerce → Restaurant is a help screen, not a settings screen (see The WooCommerce → Restaurant screen).
- It does not offer extras, add-ons or kitchen notes, opening hours, an "Open now" or "Closed" status, or delivery day and time selection. Those are in the paid plugin.
- It does not stop customers ordering at any time of day. Orders are accepted whenever WooCommerce accepts them.
- It does not change the cart page, the checkout page or emails. Orders from the menu look exactly like any other order.
- It does not create order screens for a kitchen, table or QR ordering, printing, or order-status pages.
- It is a shortcode only. It has no block, widget, REST route or WP-CLI command.
- Search looks only in the dish name and the short description. It does not search the long description, categories, SKUs or tags.
How it relates to Advanced Restaurant for WooCommerce
Advanced Restaurant for WooCommerce is the paid companion. Its plugin header calls it "Restaurant for WooCommerce Pro". It replaces this plugin rather than extending it:
- Both can be activated, but only one works. WordPress lets you activate both. When the paid plugin is loaded, this plugin does not start. It registers no shortcode, no menu item and no scripts. The paid plugin provides the same
[wxp_restaurant]shortcode, so your menu pages keep working. - A notice tells you. On the Plugins screen, administrators who can activate plugins see: "Restaurant for WooCommerce Pro is active, so the free Restaurant for WooCommerce plugin is not used. You can deactivate it." You can then deactivate and delete this plugin.
- The check happens when plugins load. This plugin tests for the paid plugin's class or function after all plugins have loaded (
plugins_loaded, priority 20), so the order in which the two are loaded does not matter. A network-activated paid plugin on a multisite network counts too. - The menu changes when you switch. The paid plugin has its own page layout, templates and scripts. CSS variables and theme template overrides written for this plugin do not apply to it. Product data is shared: the Is Non-Vegetarian flag is stored under the same meta key in both.
The tables below compare what each plugin's code does. The paid plugin is described in full in its own documentation.
| Topic | Restaurant for WooCommerce 1.2 (this plugin) | Advanced Restaurant for WooCommerce 1.5 |
|---|---|---|
| Shortcode | [wxp_restaurant] with categories | The same shortcode and attribute |
| Veg filter and search | Always on | Each can be switched off in the settings |
| Adding a dish | Add button that becomes a quantity stepper | Add button; each click adds one |
| Dish popup | Opens for any dish (photo, description, variations) | Opens only for dishes with options (variations or PPOM fields) |
| Variations | Option buttons; unavailable and out-of-stock combinations are disabled | Drop-downs; the variation's stock is not checked in the popup |
| Extras and add-ons (PPOM for WooCommerce) | Not supported | Select, radio, checkbox, text and textarea fields, with prices |
| Basket | Drawer with quantity steppers, line totals and subtotal | Side panel with a remove button per line and Cart and Checkout buttons |
| Opening hours, header and closed messages | No | Yes |
| Delivery day and time at checkout | No | Yes, classic checkout and Checkout block |
| Colours | CSS variables you add yourself | Twelve colour settings |
| Settings screen | None (a help screen) | Yes |
| Products hidden from the catalog | Not listed | Listed |
| "Hide out of stock items" (WooCommerce setting) | Followed | Not followed; out-of-stock dishes show "Sold out" |
| Order of dishes | WooCommerce sort order, then title | Newest first (WooCommerce's default product query order) |
| Scripts | One script, no jQuery | jQuery, Fancybox and WooCommerce's BlockUI |
2. Requirements
| Item | Requirement | Notes |
|---|---|---|
| WordPress | 6.3 or later | Tested up to 7.1. From the plugin header. |
| PHP | 7.4 or later | From the plugin header. The changelog for 1.2 says it was tested with PHP 8.2 to 8.5. |
| WooCommerce | 8.0 or later | Tested up to 11.1. The header carries Requires Plugins: woocommerce. WordPress 6.5 and later uses that header to ask for WooCommerce at activation (support was added in WordPress 6.5.0, checked in the WordPress 7.1.2 source). On WordPress 6.3 and 6.4 the header is ignored. |
| When WooCommerce is missing | The plugin does nothing | It starts only if the WooCommerce class exists. It shows no notice of its own. Because no shortcode is registered, WordPress prints [wxp_restaurant] as plain text on the page. |
| Theme | Any | The menu adapts to the width of its container. The readme says it was tested with Storefront and the Twenty Twenty-Five block theme. For block themes, the plugin loads its own style and script when the shortcode is rendered. |
| HPOS and Cart/Checkout blocks | Declared compatible | The plugin declares compatibility with custom_order_tables and cart_checkout_blocks. It adds nothing to the cart page, the checkout page or orders, apart from showing options that the paid plugin created (see Options from the paid plugin in the cart). |
| Browser | A current browser | The popup and the basket use the HTML dialog element; the layout uses CSS container queries and :has(). If a browser cannot open a dialog as a modal, the script opens it by setting its open attribute. |
| Server | Nothing special | No extension, cron job or outgoing connection is needed. The plugin makes no external requests. |
3. Installation
- Make sure WooCommerce is installed and active.
- Open Plugins → Add Plugin, search for "Restaurant for WooCommerce", then click Install Now and Activate. The menu label is the one in WordPress 7.1.2; older versions word it slightly differently. You can also upload the plugin folder to
wp-content/plugins/and activate it on the Plugins screen. - Put your dishes in product categories and tick Is Non-Vegetarian on meat and fish dishes (see The Is Non-Vegetarian flag).
- Add
[wxp_restaurant]to a page with the Shortcode block and publish it.
After activation, a Getting started link appears under the plugin name on the Plugins screen. It opens WooCommerce → Restaurant.
What activation creates
Nothing. The plugin creates no database table, option, user role, page, cron event or upload folder, and registers no activation hook. The only data it ever writes is a product meta value, _is_nonveg, when you save a product in the product editor.
Updating
There is nothing to migrate. Update from the Plugins screen as usual. Version 1.2 redesigned the menu, so if you wrote your own CSS against the markup of version 1.1, check your menu page after updating. The readme's upgrade notice says the same.
Deactivating and deleting
The plugin has no uninstall.php and no deactivation hook. Deactivating or deleting it removes no data. The _is_nonveg value stays on your products, so the flags are still there if you reinstall the plugin or switch to the paid plugin. A page that contains the shortcode shows it as plain text once the plugin is gone.
Multisite
The plugin has no multisite-specific code, apart from one check: it counts the paid plugin as active when that plugin is network-activated, and then stays idle. It stores no per-site or network options.
4. Quick start
- Open Products → Categories and check that each menu section is a category, in the order you want (drag to reorder). Child categories are listed after their parent.
- Edit each dish. Add a photo, a price and a short description. Tick Is Non-Vegetarian next to the product type for meat and fish.
- Create a page, add a Shortcode block, type
[wxp_restaurant]and publish. To show only some categories, use[wxp_restaurant categories="starters,mains,drinks"]with category slugs. - Open the page, add a dish and open the basket. Check that Checkout takes you to your WooCommerce checkout.
- Open WooCommerce → Restaurant to see how many categories and dishes the menu lists and which pages contain the shortcode.
5. Features
The shortcode and which dishes it lists
Put the shortcode in the content of a page or post. It prints the menu where it stands. You can use it more than once on a page; each menu gets its own element ID (wxp-menu-1, wxp-menu-2, ...) and all menus on the page share one basket. In the admin area (for example in some page builders' editors) the shortcode prints nothing.
| Shortcode | Result |
|---|---|
[wxp_restaurant] | Every product category that has published products. |
[wxp_restaurant categories="starters,mains,drinks"] | Only these category slugs, in this order. Slugs are cleaned with sanitize_title(); a slug that matches no category is ignored. Since 1.2. |
The shortcode also accepts a menu attribute (default lunch). It changes nothing on the page. It is only passed on to the wxp_restaurant_menu_category_args filter as its $menu argument.
Which categories are listed
- Without the
categoriesattribute, the plugin asks WordPress for product categories that are not empty. In WooCommerce 11.1.2, product categories come back in the order you set under Products → Categories. The plugin then arranges them parent first, then that parent's children, and so on. In the wide layout (see below) child categories are indented in the category list, with three indent levels at most. - With the attribute, categories appear in the order you list them, all at the same level, whether or not they are parents or children.
- A category appears only if at least one of its products can be listed (see below). A section without any listed dish is dropped.
- The number beside each category name and section title is the number of dishes listed in that section. It changes while the customer searches or filters.
- If the category has a description, it is shown under the section title.
Which dishes are listed
- Only products with the status Published. Drafts, private and scheduled products are not listed.
- A product whose catalog visibility is Hidden is not listed. Products set to "Shop only" or "Search results only" are listed. The code excludes only the value
hidden. - If WooCommerce → Settings → Products → Inventory → Hide out of stock items from the catalog is on, out-of-stock products are not listed. Products that are in stock or on backorder are listed. With the setting off, out-of-stock dishes stay on the menu and are marked Sold out.
- A dish in a child category is listed in the child's section only, not repeated in its parent's section.
- Dishes follow the manual order you set with the Sorting link above the product list (Products → All Products → Sorting; WooCommerce's
menu_order), then alphabetically by title. There is no limit on the number of dishes per section. - If nothing can be listed, the page shows: "There is nothing on the menu yet. Please check back soon."
The menu page
The menu has a toolbar (search and Veg filter), a category list, and one section per category.
Category list
- Narrow menus (below 760px wide): a sticky bar of swipeable category chips at the top of the menu. The chip of the section in view is highlighted and scrolled into view.
- Wide menus (760px and above): a sticky column on the left, 180 to 230px wide, with each category and its count. The width that counts is the width of the menu's own container, not of the browser window, so the same page can use either layout depending on your theme's content width.
- Clicking a category scrolls smoothly to its section (jumps at once if the visitor has asked for reduced motion). While scrolling, the active category follows the section in view. At the very bottom of the page the last section is active.
- While a search or filter is active, categories with no matching dish disappear from the list.
- The sticky bar sits at the top of the window, or below the WordPress toolbar when it shows (32px, or 46px up to 782px wide, and 0 up to 600px wide).
Dish rows
Each dish shows a diet mark (green for vegetarian, red for non-vegetarian), the name, the price as WooCommerce prints it (sale prices and variable-price ranges included), the short description, the product photo if it has one, and the order control. On menus 1,180px wide or more, dishes are laid out in two columns.
- Clicking the name or the photo opens the dish popup.
- Simple and variable dishes that can be bought show Add. Variable dishes also show the note Customizable.
- External products, grouped products and other product types (for example subscriptions or bundles) are not ordered from the menu. They show a button instead. External products use the product's own button text, link to the product's external address (the "Product URL" of the product) and open it in a new tab (
noopener noreferrer nofollow). Other types use the product's add-to-cart text and link to the product page. - A dish that is out of stock shows the badge Sold out. A dish that is in stock but cannot be bought (for example a simple product without a price) shows Unavailable. A variable product with no buyable variation shows Sold out.
Search and Veg filter
- The search box filters as you type. It matches the dish name and the short description, ignoring case and accents. Pressing Esc in the box clears it; so does the clear button.
- Veg only and Non-veg only are two tick boxes that work like radio buttons: ticking one unticks the other. Search and filter combine.
- Sections and category entries with no match are hidden. If nothing matches, the page shows "No dishes match your search." and a button, Show the whole menu, that clears the search and the filter.
- The browser may restore text typed before a reload. The script re-applies it when the page loads.
There is no setting to switch the search or the filter off in this plugin. You can hide them with your own CSS (for example .wxp-search { display: none; } and .wxp-diet-filter { display: none; }), or use the paid plugin, which has settings for both.
The Is Non-Vegetarian flag
Edit a product and tick Is Non-Vegetarian in the row of tick boxes next to the product type (where Virtual and Downloadable are). Its help text is "Check if item is Non-Vegetarian." The tick box shows for simple and variable products only. Every product that is not ticked is shown with the vegetarian mark, including product types that do not show the tick box.
- The value is stored in the product meta
_is_nonvegasyesorno. - The plugin saves it only when you save a product from the classic product editor (it hooks
woocommerce_admin_process_product_object). Quick edit, bulk edit, the REST API and imports leave the value as it is. Before 1.2, those saves reset it to "no". - To set it in an import or a script, write the meta key
_is_nonvegwith the valueyes.
Adding dishes: Add button and quantity stepper
On a simple dish, Add adds one to the basket without leaving the page. Once the dish is in the basket, the button turns into a stepper (a minus button, the quantity, a plus button).
- Plus adds one more. It is disabled when the product allows only one per order ("Sold individually") or when the quantity in the basket reached the product's maximum purchasable quantity.
- Minus removes one. At a quantity of 1 the minus button becomes a bin icon, and tapping it removes the dish from the basket.
- The quantity shown on a dish is the total of that product in the basket, including all its variations. If the same dish is in the basket on several lines (for example two sizes), the minus button does not guess which line to reduce. It shows "This item is in your basket more than once. Change the quantity in the basket." and opens the basket.
- Tapping Add (or plus) on a variable dish opens the popup, because a variation has to be chosen.
- Requests from the menu run one after another, so quick repeated taps are handled in order. While a request runs, the control shows a busy state.
- After a successful add, a message appears at the bottom of the screen ("Added to your basket.") with a View basket button. Failures show the reason in a red message for about five seconds.
The dish popup
The popup is a native dialog element, so Esc closes it and focus returns to the control that opened it. Clicking outside the panel or the close button also closes it. On narrow screens it opens as a sheet from the bottom.
- It shows the photo (the product's single-product image), the diet mark, name, price and the short description (the same text as in the dish row; the long description is not shown).
- For a variable dish, each attribute is a group of option buttons (radio buttons) labelled with the attribute name. Values that are the product's default attributes are preselected, and an attribute with a single value is preselected.
- While the customer chooses, option buttons that would lead to a combination that cannot be bought (out of stock, not purchasable, or not existing) are disabled, and the selection is cleared if it becomes invalid. When all attributes are chosen, the price shows the variation's price and the photo switches to the variation's photo if it has one. If the chosen combination is unavailable, the popup says "Sorry, this combination is unavailable. Please choose another one."
- A quantity stepper (minimum 1, maximum the variation's or product's purchasable maximum) sits in the footer next to the button Add to basket, which shows the line total (unit price times quantity, formatted with your WooCommerce currency settings). If options are missing, tapping the button shows "Please choose an option for each choice."
- On success the popup closes and the "Added to your basket." message appears. On failure the message appears inside the popup.
- For external, grouped and other linked products, the popup footer shows the link button; for sold-out and unavailable dishes it shows the badge.
The basket
The basket is printed once, in the page footer, whenever a menu is on the page. It has two parts.
Floating basket button
- Empty basket: a round button with the basket icon (on wide screens it reads "Your basket").
- With items: the item count (total quantity), the subtotal and "View basket". On screens up to 640px wide it becomes a full-width bar at the bottom of the screen.
- On screens up to 767px wide, if the page has Storefront's handheld footer bar, the button is lifted 69px so it does not cover the bar. For other fixed footer bars, set
--wxp-basket-lift(see Customising colours and layout).
Basket drawer
- Each line shows the thumbnail, the name, the cart item data (for example the chosen variation), the unit price, a quantity stepper and the line total. The plus button is disabled for "Sold individually" products and when the maximum is reached.
- The footer shows the Subtotal (WooCommerce's cart subtotal, with your tax display setting), the note "Delivery and any discounts are calculated at checkout.", and the buttons Checkout and View cart. They go to the WooCommerce checkout and cart pages.
- An empty basket shows "Your basket is empty" and "Add something tasty from the menu."
- Changing a quantity to 0 removes the line. Errors (for example not enough stock) appear as a message at the bottom of the screen.
- The drawer is a modal dialog: the page behind it does not scroll while it is open.
After every cart change the script refreshes the basket button and drawer from the server's response, then asks the theme to refresh its own mini cart. It triggers the jQuery event wc_fragment_refresh on the page body (if jQuery is on the page) and dispatches the wc-blocks_added_to_cart event on the body. WooCommerce's block scripts listen for that event (found in the WooCommerce 11.1.2 block scripts, including the one used by the Mini-Cart block).
What WooCommerce still checks
The menu is a front end for WooCommerce's cart. Every change goes through the server, where:
- The product must exist and be published, and must be a simple product, or a variable product with a valid variation that belongs to it. Otherwise the customer sees "This item is not available." or "Please choose an option for each choice."
- For "Any ..." attributes (the variation accepts any value), the value the customer chose is sent along and WooCommerce validates it against the product.
- Other plugins' checks on
woocommerce_add_to_cart_validation(minimum and maximum quantity plugins, bookings, and so on) run before the item is added, with the same arguments as on a product page. Checks onwoocommerce_update_cart_validationrun when a quantity changes. Messages those plugins add as WooCommerce error notices are shown to the customer, with WooCommerce's "View cart" button removed. - WooCommerce's own cart checks run when the item is added: purchasable, in stock, enough stock, sold individually.
- When a quantity is raised in the basket, the plugin checks "Sold individually" ("You can only have 1 %s in your basket.") and stock for products that manage stock without backorders ("Sorry, only N of X are available."). The count includes other basket lines that draw on the same stock.
- Prices are recalculated by WooCommerce on every change, so coupons and tax rules apply as on the cart page.
Security token and page caching
Requests from the menu carry a WordPress nonce (action wooexperts-menu, sent as the check parameter) that is printed into the page. A WordPress nonce is valid for between 12 and 24 hours by default (the nonce_life filter; checked in WordPress 7.1.2). If a page served from a cache carries an expired token, the server answers with the message "Your session has expired. Please reload the page and try again." and a fresh token, and the script repeats the request once with the new token. The customer normally sees nothing.
The cart state on the page (the basket button, the drawer and the quantities on the dish rows) is printed when the page is generated. The script does not ask the server for the cart when the page loads; it updates the state after the first change. A cache that serves one stored copy to visitors who already have items in their basket therefore shows the cached state until they change something. See Troubleshooting.
Customising colours and layout
Colours, radii and shadows are CSS custom properties on the .wxp-root class. The menu, the popup, the basket button, the basket drawer and the message all carry that class, so one rule changes them all. Add your rule under Appearance → Customize → Additional CSS (or Appearance → Editor → Styles → Additional CSS with a block theme):
.wxp-root {
--wxp-accent: #0f766e;
--wxp-accent-hover: #115e59;
--wxp-accent-soft: #e6f4f2;
}
| Variable | Default | Used for |
|---|---|---|
--wxp-accent | #c2410c | Main accent: primary buttons (Checkout, Add to basket), the Add button, the quantity stepper, the active category, selected option buttons, the basket count badge, the basket bar on phones, the sale price in the popup and focus outlines. |
--wxp-accent-hover | #9a3412 | Primary buttons on hover. |
--wxp-accent-contrast | #fff | Text on primary buttons and other accent backgrounds. |
--wxp-accent-soft | #fff1e8 | Soft background of the active category and of selected option buttons, and the glow around the search box when it has focus. |
--wxp-text | #1c1917 | Main text colour. |
--wxp-muted | #6b635c | Secondary text. |
--wxp-border | #e9e4df | Light borders and dividers. |
--wxp-border-strong | #cfc6bd | Stronger borders. |
--wxp-surface | #fff | Panel and popup background. |
--wxp-surface-2 | #f8f5f2 | Secondary background. |
--wxp-nav-bg | rgba(255, 255, 255, .94) | Background of the sticky category bar. |
--wxp-veg | #15803d | Vegetarian mark. |
--wxp-nonveg | #b42318 | Non-vegetarian mark. |
--wxp-error | #b42318 | Error messages. |
--wxp-radius | 16px | Large corner radius. |
--wxp-radius-sm | 10px | Small corner radius. |
--wxp-pill | 999px | Fully rounded corners (chips, buttons). |
--wxp-shadow | 0 1px 2px rgba(28, 25, 23, .06), 0 6px 20px rgba(28, 25, 23, .08) | Card shadow. |
--wxp-sticky-top | 0px | Distance from the top of the window where the category bar sticks. For visitors without the WordPress toolbar, your own .wxp-root { --wxp-sticky-top: 80px; } works. For logged-in users who see the toolbar, the plugin sets 32px (46px up to 782px wide; 0px up to 600px wide) with the more specific selector body.admin-bar .wxp-root, which wins over .wxp-root. |
--wxp-basket-lift | 0px | Raises the floating basket button and the message above a fixed footer bar. Set it on body, for example body { --wxp-basket-lift: 60px; }. |
Template overrides
Since 1.2, you can copy templates/menu.php or templates/basket.php from the plugin folder to your-theme/restaurant-for-woocommerce/ and edit the copy. WooCommerce looks for your-theme/restaurant-for-woocommerce/menu.php and, as a fallback, your-theme/menu.php (child theme first, then parent theme; checked in WooCommerce 11.1.2). A file called menu.php or basket.php in the root of your theme therefore replaces the plugin's template too.
menu.phpprints the menu. It receives$items(products per category slug),$cats(per slug:id,name,count,description,depth),$instance(1 for the first menu on the page) and$cart(the cart state).basket.phpprints the floating button and the drawer. It receives no variables.dashboard.php(the admin screen) is included directly and cannot be overridden.- The bundled script looks for the class names and data attributes used by the templates (
data-wxp-menu,data-product,.wxp-item,.wxp-add,.wxp-drawerand so on). Keep them when you edit a copy.
Sticky positioning in themes
Sticky elements stop working inside a parent that has overflow: hidden, which several themes use. To keep the category list sticky, the menu script looks at the menu's ancestors and, for any that hide overflow, sets the inline style overflow: clip (which clips the same way without breaking sticky). It does this in the visitor's browser on every page with a menu. If a theme element misbehaves, that is the first place to look.
The WooCommerce → Restaurant screen
Open WooCommerce → Restaurant (menu slug wxp-restaurant, for users who can manage_woocommerce). The screen, titled "Restaurant for WooCommerce" with the version number, has no form and saves nothing.
| Card | What it shows |
|---|---|
| Getting started | Three steps (add your dishes as products, mark meat and fish dishes, add the menu to a page), the two shortcodes with a Copy button each, and a note on how dishes and categories are ordered. The Copy button writes the shortcode to the clipboard and reads "Copied" for 1.5 seconds. It needs the browser's clipboard feature; without it the button does nothing. |
| Your menu | The number of categories and dishes the shortcode lists (counted the same way as on the menu page) and the number of pages that contain the shortcode (at most 10). Below that, the pages themselves with View and Edit links; drafts and private items carry their status. The list is found by searching pages and posts (published, draft or private) for the text [wxp_restaurant, sorted by title, and shows at most 10. A menu added by a theme template or a page builder that does not store the shortcode text in the content is not found. |
| Help / FAQ | Short answers about missing dishes, colours, layout, the free and paid plugin, and where to get help. |
| Pro version | A list of what the paid plugin adds and a Get Pro Version button (also at the top of the screen), linking to the product page on wpexpertshub.com. |
The screen's own stylesheet and script load only on this screen.
Options from the paid plugin in the cart
If a cart contains items that were added by the paid plugin with options (cart item data under the key wxp-menu), this plugin still shows those options on the cart item, adds the options' prices to the item price, and copies the options to the order line item as item meta. This covers customers whose cart was created while the paid plugin was active. This plugin never creates such options itself.
- The cart shows each option group as a name and value list. Options with a price show "(+price)" after the name.
- The price is added to the item every time WooCommerce loads the cart from the session and again before totals are calculated. The code guards against adding it twice to the same product object.
- The plugin also removes any cart item data row whose name is empty and whose value (or display text) is empty, for every cart item, whichever plugin added it (it filters
woocommerce_get_item_data).
Accessibility and translation
- Controls are real buttons and links. The popup and the basket are modal dialogs with labels. Stepper quantities are announced as they change. Keyboard users keep focus in the right place when the basket is redrawn.
- Animations are switched off for visitors who prefer reduced motion.
- Text is translatable with the text domain
restaurant-for-woocommerce. The plugin folder contains no translation files. Strings that the scripts show are passed in from PHP, so they translate too.
6. Developer reference
Shortcode
| Attribute | Default | Meaning |
|---|---|---|
categories | empty | Comma-separated product category slugs. Order is kept. |
menu | lunch | Passed to wxp_restaurant_menu_category_args only. |
Filters
The plugin fires three filters of its own. It fires no actions.
| Filter | Parameters | When |
|---|---|---|
wxp_restaurant_menu_product_args | $args (array), $slug (category slug), $atts (shortcode attributes) | Before the products of one section are queried with wc_get_products(). Default $args: status = publish, limit = -1, orderby = menu_order ascending then title ascending, a tax_query on the category's term ID with include_children false, and, when "Hide out of stock items" is on, stock_status = instock and onbackorder. Products whose catalog visibility is hidden are removed after your filter runs. |
wxp_restaurant_menu_category_args | $args (array), $menu (string), $atts (array) | Before the categories are fetched with get_terms(). Default $args: taxonomy = product_cat, hide_empty = true; with the categories attribute also slug (array) and orderby = slug__in. |
wxp_restaurant_menu_item_data | $data (array), $product (WC_Product) | After the data for one dish is built, before the template uses it. Keys: id, type, name, diet (veg or non-veg), orderable, quick (true when the dish is ordered from the menu), variable, status (empty, unavailable or soldout), image, link, link_text, and json (the array that becomes the row's data-product attribute, with id, name, diet, price, price_html, variable, orderable, max, image, options). |
Exclude two products from every section:
add_filter( 'wxp_restaurant_menu_product_args', function ( $args, $slug, $atts ) {
$args['exclude'] = array( 123, 456 ); // product IDs
return $args;
}, 10, 3 );
Leave one category out of every menu (term ID 15):
add_filter( 'wxp_restaurant_menu_category_args', function ( $args, $menu, $atts ) {
$args['exclude'] = array( 15 ); // term ID
return $args;
}, 10, 3 );
Mark dishes tagged "fish" as non-vegetarian without touching the product. Both the row and the popup read the diet, so set it in both places:
add_filter( 'wxp_restaurant_menu_item_data', function ( $data, $product ) {
if ( has_term( 'fish', 'product_tag', $product->get_id() ) ) {
$data['diet'] = 'non-veg';
$data['json']['diet'] = 'non-veg';
}
return $data;
}, 10, 2 );
AJAX endpoints
Requests go to WooCommerce's AJAX endpoint, ?wc-ajax=rfw_<action>, as POST. They are available to logged-out visitors. Every handler verifies the nonce (action wooexperts-menu, POST parameter check) before it changes the cart.
| Endpoint | POST parameters | What it does |
|---|---|---|
rfw_add_to_cart | data: a URL-encoded string with product (ID), quantity (default 1), variation (variation ID, 0 for none) and opts[attribute_pa_size]=value for "Any" attributes | Adds an item. Returns key (the cart item key) on success. |
rfw_update_item | key (cart item key), qty (0 removes the line) | Changes a basket line's quantity. |
rfw_remove_item | id (cart item key) | Removes a line. The bundled script does not call it (it uses rfw_update_item with quantity 0); it exists for compatibility. |
Every response is JSON with these keys:
res: true or false.message: the text to show the customer.fragments:div.wxp-drawer-content(the drawer's HTML) andbutton.wxp-basket-btn(the floating button). The script replaces the elements that match each selector.cart:count,subtotal(plain text) andproducts(product ID toqtyandkeys, the cart item keys).- On a nonce failure:
code=nonceand a newnonce.
The older endpoint ?wexperts-ajax=add_to_cart (or remove_item, update_item) still works, for pages cached with the version 1.1 script. It runs on template_redirect at priority 0, defines DOING_AJAX and sends no-cache headers.
An example from a page that has a menu (the script's wxpmenu object exists only there):
fetch( wxpmenu.ajax_url.replace( '%%endpoint%%', 'rfw_add_to_cart' ), {
method: 'POST',
credentials: 'same-origin',
headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' },
body: new URLSearchParams( { data: 'product=123&quantity=1', check: wxpmenu.nonce } ).toString()
} ).then( function ( r ) { return r.json(); } ).then( console.log );
JavaScript configuration and events
The script assets/js/menu.js (handle restaurant-script, loaded in the footer with defer, no dependencies) receives a global object wxpmenu printed by wp_localize_script(). The style handle is restaurant-style. Both are registered on every front page and enqueued only when the shortcode is rendered.
| Key | Content |
|---|---|
ajax_url, ajax_prefix | WooCommerce's endpoint with the placeholder %%endpoint%%, and the prefix rfw_. |
nonce | The nonce for wooexperts-menu. |
cart_url, checkout_url | WooCommerce cart and checkout page URLs. |
price | symbol, format, decimals, decimal, thousand: your WooCommerce currency settings, used to format totals in the popup. |
i18n | The translated strings used by the script. |
wxp_ajax, wxp_ajax_url, wxp_ajax_prefix, wxp_legacy_param, wxp_nonce | Kept for scripts cached from version 1.1. |
After each cart change the script triggers wc_fragment_refresh on document.body (jQuery event, only if jQuery exists) and dispatches the DOM event wc-blocks_added_to_cart on document.body.
PHP classes and functions
The plugin does not declare a public PHP API. These are the helpers its own templates call.
wxp_restaurant()returns the main object (classWooExperts_Restaurant). Itsget_options( $product )returns the variation data for the popup.WooExperts_Load_Restaurant(inclasses/load-restaurant.php) registers the shortcode and the product tick box.get_menu_categories(),get_menu_items(),get_item_data()andesc_data()build the data formenu.php.RFW_WExperts_AJAX(inclasses/ajax.php) handles the endpoints and builds the basket HTML:get_cart_state(),get_basket_button_html(),get_sidecart_html(),stepper_html()andicon().wxp_restaurant_pro_is_active()returns true when the paid plugin's classWooExperts_Restaurant_Proor functionwxp_restaurant_pro()exists.- Constants:
WXP_RESTAURANT_FILE(the plugin's main file) andWXP_RESTAURANT_VER(1.2).
WordPress and WooCommerce hooks the plugin uses
| Hook | Priority | Purpose |
|---|---|---|
plugins_loaded | 20 | Starts the plugin if WooCommerce is present and the paid plugin is not active; otherwise adds the Plugins screen notice. |
before_woocommerce_init | 10 | Declares HPOS and Cart/Checkout blocks compatibility. |
init | 10 (and 0) | Registers the shortcode and the other hooks of this table; priority 0 defines DOING_AJAX for the legacy endpoint. |
admin_menu | 10 | Adds WooCommerce → Restaurant. |
admin_notices | 10 | Only when the paid plugin is active: the Plugins screen notice that this plugin is not used. |
plugin_action_links_<plugin> | 10 | Adds the Getting started link first in the plugin's row. |
wp_enqueue_scripts | 10 | Registers the style and script. |
admin_enqueue_scripts | 999 | Loads the admin style and script on WooCommerce → Restaurant only. |
wp_footer | 5 | Prints the basket (once per page, only when a menu was rendered). |
template_redirect | 0 | Handles the legacy ?wexperts-ajax= endpoint. |
wc_ajax_rfw_add_to_cart, wc_ajax_rfw_update_item, wc_ajax_rfw_remove_item | 10 | The AJAX handlers. |
product_type_options | 10 | Adds the Is Non-Vegetarian tick box. |
woocommerce_admin_process_product_object | 10 | Saves the tick box. |
woocommerce_get_item_data | 10 | Shows wxp-menu options on cart items and removes item data rows with an empty name and an empty value or display text. The filter's result is changed for every cart item. |
woocommerce_cart_loaded_from_session | 999 | Adds stored option prices to cart items. |
woocommerce_before_calculate_totals | 20 | Same, before totals are calculated. |
woocommerce_checkout_create_order_line_item | 10 | Copies stored options to the order line item. |
The plugin calls these WooCommerce filters and actions itself, so other plugins' callbacks run: woocommerce_add_to_cart_validation, woocommerce_update_cart_validation, woocommerce_ajax_added_to_cart, woocommerce_cart_item_product, woocommerce_widget_cart_item_visible, woocommerce_cart_item_name, woocommerce_cart_item_thumbnail, woocommerce_cart_item_price and woocommerce_cart_item_subtotal.
Data the plugin reads and writes
| Item | Where | Meaning |
|---|---|---|
_is_nonveg | Product meta | yes or no. Written by the product editor; read to choose the diet mark. |
wxp-menu | Cart item data (WooCommerce session) | Read only. Options created by the paid plugin: a list of title and options (each with name and price). |
| Item meta on order lines | Order item meta | One entry per wxp-menu group: the group's title as the key and the chosen option names, comma separated, as the value. Only exists for carts created by the paid plugin. |
woocommerce_hide_out_of_stock_items | WooCommerce option | Read to decide whether out-of-stock dishes are listed. |
The plugin has no options of its own, no custom tables, no user meta, no transients, no cron events, no roles, no custom capabilities and no REST routes. The only capability it checks is manage_woocommerce for the admin screen.
File layout
restaurant-for-woocommerce.php: header, bootstrap, the free/paid check, admin menu, variation data.classes/load-restaurant.php: shortcode, categories, products, item data, script registration, cart-item handling.classes/ajax.php: AJAX handlers and basket HTML.templates/menu.php,templates/basket.php,templates/dashboard.php.assets/js/menu.js,assets/js/admin.js,assets/css/front.css,assets/css/admin.css.readme.txt.
7. Privacy
| Topic | What the plugin does |
|---|---|
| Personal data stored by the plugin | None. It stores no customer data, no IP addresses and no logs. Its only stored value is the diet flag on products (_is_nonveg). |
| Orders and carts | Handled entirely by WooCommerce. What a customer adds is held in WooCommerce's cart session and ends up in WooCommerce orders, under WooCommerce's own privacy settings, retention and exporters. |
| Cookies | The plugin sets none. When a customer adds an item, WooCommerce itself sets its cart cookies (for example wp_woocommerce_session_*, woocommerce_items_in_cart and woocommerce_cart_hash; names checked in WooCommerce 11.1.2). The script uses no localStorage or sessionStorage. |
| Data sent anywhere | Nothing. The plugin makes no outgoing request and loads no external font, script or image. The menu's AJAX requests go to your own site. |
| Retention and clean-up | The plugin keeps nothing that needs clean-up. Deactivating or deleting it removes no data; the _is_nonveg product values remain. |
| Privacy policy text, exporter, eraser | The plugin adds none, because it holds no personal data. |
| Admin screen | Shows counts of your own categories, dishes and pages. It sends nothing out. The Pro version button is a plain link. |
8. Troubleshooting
| Problem | Likely cause and fix |
|---|---|
The page shows [wxp_restaurant] as text. | The plugin is not running: it is deactivated, or WooCommerce is not active (the plugin starts only when WooCommerce is loaded). Activate WooCommerce and the plugin. Check that the shortcode is typed with straight square brackets in a Shortcode block. |
| "There is nothing on the menu yet. Please check back soon." | No category has a listable product. Check that the products are published, in the categories you expect (or that the slugs in categories="..." are right), not set to catalog visibility "Hidden", and not out of stock while "Hide out of stock items" is on. |
| A dish is missing. | It is not published, its catalog visibility is Hidden, or it is out of stock with "Hide out of stock items" on. It may also be in a child category, in which case it is listed in the child's own section. |
| A category is missing. | It has no listable products, or you used the categories attribute and its slug is not in the list. Use the slug (shown in Products → Categories), not the name. |
| Tapping Add does nothing, or shows "Something went wrong. Please try again." | The script could not read a JSON answer. Look for a security plugin, firewall or cache rule that blocks or caches requests to ?wc-ajax=rfw_add_to_cart, a JavaScript error from the theme on the page, or a script optimiser that moved or removed the plugin's script. Open the browser's network tab: the response should be JSON. |
| "Your session has expired. Please reload the page and try again." | The page carried an expired token. The script retries once with a fresh token, so you see this only if that retry fails too. Reload the page, and exclude the menu page from page caching or keep the cache lifetime at 12 hours or less (a token is valid for 12 to 24 hours). |
| The basket button or the quantities on the menu show an old state. | The cart state is printed into the page when it is generated, and the page is served from a cache. Exclude the menu page from the cache for visitors who have a WooCommerce cart (cookie woocommerce_items_in_cart), or for everyone. |
| "Please choose an option for each choice." | A variable dish needs a value for every attribute. Choose one in each group in the popup. If an option is greyed out, no buyable variation has that value together with your other choices. |
| A variation is missing from the popup. | Variations without a price, disabled variations and (with "Hide out of stock items" on) out-of-stock variations are not offered. WooCommerce decides which variations are available. |
| The plus button is greyed out. | The product is "Sold individually", or the basket already holds the product's maximum purchasable quantity (from its stock settings). |
| "Sorry, only N of X are available." or "You can only have 1 X in your basket." | Stock or "Sold individually" limits. Change the stock settings on the product, or tell the customer. |
| A dish shows the wrong vegetarian mark. | The flag is read from the product meta _is_nonveg (yes means non-vegetarian). Tick or untick Is Non-Vegetarian in the product editor. Quick edit, bulk edit, imports and the REST API do not change it. |
| The floating basket covers my theme's mobile footer bar or a cookie banner. | Set body { --wxp-basket-lift: 60px; } (the height of the bar) in Additional CSS. Storefront's handheld footer bar is handled automatically. |
| The category list is not sticky, or sits under my theme's header. | Sticky positioning needs the menu's parents not to clip overflow. The script switches overflow: hidden to overflow: clip on parents. For a fixed theme header, set --wxp-sticky-top to the header's height (for example .wxp-root { --wxp-sticky-top: 80px; }). That works for visitors; logged-in users who see the WordPress toolbar get the plugin's own toolbar offset instead, because its selector (body.admin-bar .wxp-root) is more specific. |
| My colour change has no effect. | Set the variables on .wxp-root (not body, except --wxp-basket-lift), and clear any cache or CSS optimiser that serves an old copy of your Additional CSS. |
| My template override is not used. | The copy must be at your-theme/restaurant-for-woocommerce/menu.php (or basket.php) in the active theme, or in its parent theme. Check the file name and folder, and clear object or page caches. |
| The theme's own mini cart does not update. | After a change, the script triggers the jQuery event wc_fragment_refresh and the wc-blocks_added_to_cart event. A mini cart that does not listen to either will update on the next page load. |
| The menu looks different after updating to 1.2. | Version 1.2 is a new design. Custom CSS written for the old markup no longer matches. Remove it and use the CSS variables described under Customising colours and layout. |
| A notice on the Plugins screen says the free plugin is not used. | The paid plugin is active. Deactivate and delete this plugin, as the notice says. |
9. FAQ
How do I mark a dish as non-vegetarian?
Edit the product and tick Is Non-Vegetarian next to the product type. All other dishes show the vegetarian mark.
How do customers pay, and how do they get their food?
The menu uses the normal WooCommerce checkout, so customers can pay with any payment method you have enabled, including cash on delivery. Use WooCommerce shipping methods for delivery and "Local pickup" for collection. Choosing a delivery day and time slot is a feature of the paid plugin.
Can customers choose extras such as toppings?
Choices that are product variations (sizes, crusts, spice levels) work in this plugin, and the price updates in the popup. Paid extras such as toppings, sides and notes (PPOM for WooCommerce fields) are a feature of the paid plugin.
Can I show only some categories?
Yes. [wxp_restaurant categories="starters,mains,drinks"] shows only these category slugs, in this order. You can put different menus on different pages.
In which order are dishes and categories shown?
Categories follow the order under Products → Categories, with child categories right after their parent. Dishes follow the manual order set with the Sorting link above the product list (Products → All Products → Sorting), then alphabetically by title. Dishes in a child category are listed in that category's own section.
Why is a dish or category missing?
Only published products whose catalog visibility is not "Hidden" are listed, and a category is shown when at least one of its products is listed. With "Hide out of stock items" on in WooCommerce, out-of-stock dishes are hidden too.
Does it work with my theme?
It is designed for any theme and adapts to the width of its container. The readme says it was tested with Storefront and the Twenty Twenty-Five block theme.
Does it work with page caching?
Partly. An expired security token in a cached page is replaced automatically and the request retried. The cart state printed in the page is not refreshed on load, so exclude the menu page from caching for visitors with a cart if you see an old basket.
Can I change the colours?
Yes. Set the CSS variables on .wxp-root in Additional CSS. See Customising colours and layout for the full list.
Can I change the layout?
Copy templates/menu.php or templates/basket.php to your-theme/restaurant-for-woocommerce/ and edit the copy.
Can I use the free and the paid plugin together?
They can both be active, but when the paid plugin is active this one stays idle. Deactivate and delete it. The shortcode is the same, so your menu pages keep working. See How it relates to Advanced Restaurant for WooCommerce.
Does a customer have to log in or create an account?
The menu itself does not ask for it. The AJAX endpoints work for visitors who are not logged in. Whether checkout needs an account depends on your WooCommerce settings.
Can I turn off the search box or the Veg filter?
Not with a setting. You can hide them with CSS (the classes are .wxp-search and .wxp-diet-filter), or use the paid plugin, which has settings for both.
Where can I get help?
See WooCommerce → Restaurant in your dashboard, open a topic in the WordPress.org support forum, or email support@wpexpertshub.com.
10. Changelog
1.2 - 29/09/2026
- New - Redesigned menu: dish rows with photos, one-tap Add buttons that become quantity steppers, category chips on phones, sticky category list on desktop, live search in names and descriptions.
- New - Dish popup (native dialog, no jQuery plugins) with option buttons for variations, disabled out-of-stock combinations, default attributes, variation photo and price, quantity and live total.
- New - Basket drawer with quantity steppers, line totals, subtotal, Checkout and View cart; floating basket button with item count and total (bar at the bottom on phones).
- New - Shortcode attribute categories="slug1,slug2".
- New - Nested categories are listed parent first, then their children; dishes of a child category are no longer repeated in the parent's section.
- New - Dishes follow the product sort order, and products hidden from the catalog or out of stock (with "Hide out of stock items") are not listed.
- New - External, grouped and other product types link to their product page; sold-out and unavailable dishes are marked.
- New - Templates can be overridden in your-theme/restaurant-for-woocommerce/; colours can be changed with CSS variables.
- New - WooCommerce > Restaurant screen with getting started steps, copyable shortcodes, pages using the menu and FAQ.
- New - Declared compatibility with HPOS and Cart/Checkout blocks; header mini carts refresh after changes from the menu.
- Fix - Only 10 items per category were shown (default query limit); all published dishes are now listed.
- Fix - "Any" variations with custom attributes of more than one word (for example "Extra Hot") could not be added to the basket.
- Fix - Stock limits, "Sold individually" and other plugins' add-to-cart validation are applied to menu orders and quantity changes; WooCommerce error messages are shown to the customer.
- Fix - Pages served from a full-page cache with an expired security token now retry automatically instead of failing.
- Fix - Menu styles and scripts were not loaded with block themes.
- Fix - Fatal error when the add-to-cart request contained a missing or invalid product.
- Fix - "Is Non-Vegetarian" flag was reset to "no" whenever a product was saved outside the product editor (quick edit, REST API, imports).
- Fix - Add-to-cart failures (out of stock, expired session, network error) left the menu blocked.
- Fix - The plugin also loaded next to the Pro version on multisite when WooCommerce was network-activated.
- Fix - The plugin screen hid all admin notices.
- Fix - PHP 8.2+ deprecations; all output escaped; Plugin Check clean.
- Tweak - Removed fancybox, the icon font and the BlockUI dependency (smaller, faster pages).
- Tweak - AJAX requests use the WooCommerce AJAX endpoint (the old endpoint still works).
- Tweak - Text domain changed to restaurant-for-woocommerce; tested with WordPress 7.1, WooCommerce 11.1 and PHP 8.2 - 8.5.
1.1 - 30/07/2022
- Tweak - Minor fix and compatibility check.
1.0 - 04/05/2021
- Initial release.
11. Support
For help, see WooCommerce → Restaurant in your dashboard, open a topic in the WordPress.org support forum, or email support@wpexpertshub.com.
When you write, include:
- The plugin version (1.2 or the version you run), your WordPress version and your PHP version.
- Your WooCommerce version and the name of your theme.
- The address of the page with the menu, and the exact shortcode you used.
- What you expected, what happened, and what you already tried. A screenshot of any message and the browser console output help.