Himanshu SharmaSoftware engineer
Let’s talk

WordPress · Lesson 45 of 45

Project: a small business site

Build a small bakery website with a block theme, pages, a menu plugin using a custom post type and shortcode, then review it before launch.

30 minSecurity, performance and projectUpdated 23 October 2026

By the end of this lessonCombine what the course covered, theme, content, plugin and checks, to build and review a working site that you could hand to a client.

The brief

A neighbourhood bakery, Daily Bread, needs a small website:

  • a home page with a welcome message and opening hours;
  • a menu page that lists breads and pastries with prices, which the owner updates herself;
  • an About page and a Contact page;
  • a clean, fast design that works on phones.

This project uses what the course taught: a block theme with theme.json, pages and navigation, a plugin with a custom post type, post meta and a shortcode, then security, speed and launch checks. Work through it step by step. Everything is written so that you can repeat it with WP-CLI on any site.

A rule that shapes the structure: design lives in the theme, features live in a plugin. If the bakery changes its look in two years, the menu data and the shortcode stay.

Step 1: the theme

The theme is a small block theme with a fixed header (site title and a list of pages), a footer, and templates for pages, single posts and the fallback.

css
/*
Theme Name: Daily Bread
Description: A warm, simple block theme for a neighbourhood bakery.
Version: 1.0.0
Requires at least: 6.6
Requires PHP: 7.4
License: GNU General Public License v2 or later
Text Domain: daily-bread
*/
json
{
	"$schema": "https://schemas.wp.org/trunk/theme.json",
	"version": 3,
	"settings": {
		"appearanceTools": true,
		"layout": { "contentSize": "680px", "wideSize": "1100px" },
		"color": {
			"palette": [
				{ "slug": "crust", "name": "Crust", "color": "#8a4b14" },
				{ "slug": "flour", "name": "Flour", "color": "#fbf7f0" },
				{ "slug": "ink", "name": "Ink", "color": "#2b2118" }
			]
		},
		"typography": {
			"fontFamilies": [
				{ "slug": "serif", "name": "Serif", "fontFamily": "Georgia, 'Times New Roman', serif" },
				{ "slug": "system", "name": "System", "fontFamily": "system-ui, -apple-system, 'Segoe UI', sans-serif" }
			]
		}
	},
	"styles": {
		"color": { "background": "var(--wp--preset--color--flour)", "text": "var(--wp--preset--color--ink)" },
		"typography": { "fontFamily": "var(--wp--preset--font-family--system)", "lineHeight": "1.6" },
		"elements": {
			"heading": { "typography": { "fontFamily": "var(--wp--preset--font-family--serif)" } },
			"link": { "color": { "text": "var(--wp--preset--color--crust)" } }
		}
	}
}

The header uses the Page List block, which shows your published pages as links, so the navigation updates itself when a page is added:

html
<!-- wp:group {"tagName":"header","layout":{"type":"flex","justifyContent":"space-between"},"style":{"spacing":{"padding":{"top":"1rem","bottom":"1rem"}}}} -->
<header class="wp-block-group" style="padding-top:1rem;padding-bottom:1rem">
	<!-- wp:site-title {"level":0,"isLink":true} /-->
	<!-- wp:page-list /-->
</header>
<!-- /wp:group -->
html
<!-- wp:group {"tagName":"footer","layout":{"type":"constrained"}} -->
<footer class="wp-block-group">
	<!-- wp:paragraph {"fontSize":"small"} -->
	<p class="has-small-font-size">Daily Bread, 12 High Street. Open Tuesday to Sunday from 7am.</p>
	<!-- /wp:paragraph -->
</footer>
<!-- /wp:group -->

Pages and posts show their title and content, and the fallback lists posts:

html
<!-- wp:template-part {"slug":"header","tagName":"header"} /-->

<!-- wp:group {"tagName":"main","layout":{"type":"constrained"}} -->
<main class="wp-block-group">
	<!-- wp:post-title {"level":1} /-->
	<!-- wp:post-content {"layout":{"type":"constrained"}} /-->
</main>
<!-- /wp:group -->

<!-- wp:template-part {"slug":"footer","tagName":"footer"} /-->
html
<!-- wp:template-part {"slug":"header","tagName":"header"} /-->

<!-- wp:group {"tagName":"main","layout":{"type":"constrained"}} -->
<main class="wp-block-group">
	<!-- wp:query {"query":{"inherit":true}} -->
	<div class="wp-block-query">
		<!-- wp:post-template -->
			<!-- wp:post-title {"isLink":true} /-->
			<!-- wp:post-excerpt /-->
		<!-- /wp:post-template -->
		<!-- wp:query-no-results -->
			<!-- wp:paragraph --><p>Nothing found.</p><!-- /wp:paragraph -->
		<!-- /wp:query-no-results -->
	</div>
	<!-- /wp:query -->
</main>
<!-- /wp:group -->

<!-- wp:template-part {"slug":"footer","tagName":"footer"} /-->
bash
wp theme activate daily-bread
result
Success: Switched to 'Daily Bread' theme.

Step 2: the menu plugin

The menu items are content that belongs to the business, so they go in a plugin. Each item has a title, a description (the editor), and a price stored as post meta. A shortcode, [menu_items], lists them.

php
<?php
/**
 * Plugin Name: Daily Bread Menu
 * Description: Menu items with prices, and a [menu_items] shortcode.
 * Version:     1.0.0
 * Text Domain: daily-bread-menu
 */
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

function daily_bread_register_types() {
	register_post_type( 'menu_item', [
		'labels'       => [ 'name' => 'Menu items', 'singular_name' => 'Menu item', 'add_new_item' => 'Add menu item' ],
		'public'       => true,
		'has_archive'  => false,
		'menu_icon'    => 'dashicons-carrot',
		'supports'     => [ 'title', 'editor', 'thumbnail' ],
		'show_in_rest' => true,
		'rewrite'      => [ 'slug' => 'menu-item' ],
	] );

	register_post_meta( 'menu_item', '_price', [
		'type'              => 'number',
		'single'            => true,
		'default'           => 0,
		'sanitize_callback' => fn( $value ) => max( 0, round( (float) $value, 2 ) ),
		'show_in_rest'      => true,
	] );

	register_taxonomy( 'menu_section', [ 'menu_item' ], [
		'labels'       => [ 'name' => 'Sections', 'singular_name' => 'Section' ],
		'hierarchical' => true,
		'show_in_rest' => true,
	] );
}
add_action( 'init', 'daily_bread_register_types' );

register_activation_hook( __FILE__, function () {
	// Register first, then rebuild the address rules once.
	daily_bread_register_types();
	flush_rewrite_rules();
} );

/**
 * [menu_items section="breads"]  lists the items, optionally of one section.
 */
add_shortcode( 'menu_items', function ( $atts ) {
	$atts = shortcode_atts( [ 'section' => '' ], $atts, 'menu_items' );

	$args = [
		'post_type'      => 'menu_item',
		'posts_per_page' => 50,
		'orderby'        => 'title',
		'order'          => 'ASC',
		'no_found_rows'  => true,
	];
	if ( '' !== $atts['section'] ) {
		$args['tax_query'] = [ [ 'taxonomy' => 'menu_section', 'field' => 'slug', 'terms' => sanitize_title( $atts['section'] ) ] ];
	}

	$items = get_posts( $args );
	if ( ! $items ) {
		return '';
	}

	$html = '<ul class="menu-items">';
	foreach ( $items as $item ) {
		$price = (float) get_post_meta( $item->ID, '_price', true );
		$html .= sprintf(
			'<li><span class="menu-name">%s</span> <span class="menu-price">£%s</span></li>',
			esc_html( get_the_title( $item ) ),
			esc_html( number_format( $price, 2 ) )
		);
	}
	return $html . '</ul>';
} );
bash
wp plugin activate daily-bread-menu
result
Plugin 'daily-bread-menu' activated.
Success: Activated 1 of 1 plugins.

Step 3: content with WP-CLI

Creating the content from the command line means you can run the same commands on a staging site or the live one. First the sections and menu items, with prices:

bash
wp term create menu_section Breads --slug=breads
wp term create menu_section Pastries --slug=pastries
wp post create --post_type=menu_item --post_title="Sourdough loaf" --post_status=publish --porcelain
wp post create --post_type=menu_item --post_title="Rye bread" --post_status=publish --porcelain
wp post create --post_type=menu_item --post_title="Croissant" --post_status=publish --porcelain
result
Success: Created menu_section 2.
Success: Created menu_section 3.
4
5
6
php
<?php
$prices   = [ 'Sourdough loaf' => 4.5, 'Rye bread' => 4.0, 'Croissant' => 2.25 ];
$sections = [ 'Sourdough loaf' => 'breads', 'Rye bread' => 'breads', 'Croissant' => 'pastries' ];

foreach ( $prices as $title => $price ) {
	$item = get_posts( [ 'post_type' => 'menu_item', 'title' => $title, 'numberposts' => 1 ] )[0];
	update_post_meta( $item->ID, '_price', $price );
	wp_set_object_terms( $item->ID, $sections[ $title ], 'menu_section' );
	echo $item->post_title, ' £', number_format( get_post_meta( $item->ID, '_price', true ), 2 ), ' in ', $sections[ $title ], "\n";
}
result
Sourdough loaf £4.50 in breads
Rye bread £4.00 in breads
Croissant £2.25 in pastries

Now the pages, with the shortcode on the menu page, and the settings to make Home the front page:

bash
wp post create --post_type=page --post_title="Home" --post_name=home --post_status=publish --post_content="<!-- wp:paragraph --><p>Fresh bread and pastries, baked every morning. Open Tuesday to Sunday from 7am.</p><!-- /wp:paragraph -->" --porcelain
wp post create --post_type=page --post_title="Our menu" --post_name=menu --post_status=publish --post_content="[menu_items]" --porcelain
wp post create --post_type=page --post_title="About" --post_name=about --post_status=publish --post_content="<!-- wp:paragraph --><p>We have baked on High Street since opening day.</p><!-- /wp:paragraph -->" --porcelain
wp post create --post_type=page --post_title="Contact" --post_name=contact --post_status=publish --post_content="<!-- wp:paragraph --><p>12 High Street. Telephone: 01234 567890 (example number).</p><!-- /wp:paragraph -->" --porcelain
result
7
8
9
10
bash
wp option update show_on_front page
wp option update page_on_front 7
wp option update blogname "Daily Bread"
wp option update blogdescription "Baked fresh every morning"
wp rewrite structure "/%postname%/"
result
Success: Updated 'show_on_front' option.
Success: Updated 'page_on_front' option.
Success: Updated 'blogname' option.
Success: Updated 'blogdescription' option.
Success: Rewrite structure set.
Success: Rewrite rules flushed.

(The Home page has ID 7 here: WordPress's sample content uses IDs 1 to 3, and the three menu items took 4 to 6. On your own site, use the ID that wp post create printed for Home.)

Step 4: check the result

Visit the pages the way a visitor would:

http
GET /
result
Home
http
GET /menu/
result
Our menu
http
GET /menu/
result
class="menu-name">Croissant
class="menu-price">£2.25
class="menu-name">Rye bread
class="menu-price">£4.00
class="menu-name">Sourdough loaf
class="menu-price">£4.50
http
GET /contact/
result
<title>Contact &#8211; Daily Bread

Open the front page in a browser too: look at the header links, the colours from theme.json, and the layout at phone width. The shortcode prints escaped values and a formatted price, and the menu page lists the items alphabetically.

Step 5: review before launch

Use this checklist, built from the whole course:

Content and design

  • Every page has a clear title, and the navigation lists the pages visitors need.
  • The front page tells visitors what, where and when in the first screen.
  • Images (when added) are sized for the page and have alt text.

Code

  • Features are in the plugin, not the theme; every file starts with the ABSPATH check; every function and option is prefixed.
  • Everything printed is escaped, input is sanitised, and write actions check a nonce and a capability (Writing secure WordPress code).
  • No test code, WP_DEBUG display or sample accounts remain.

Security and backups

  • Strong admin passwords, two-factor authentication, only the users that are needed, DISALLOW_FILE_EDIT on (Hardening, updates and backups).
  • An automatic backup of files and database, stored off the server, and a restore that you have tested.

Speed

Launch

  • Move the site with the steps in Moving a site from local to live: export the database, copy the files, search-replace the address, flush permalinks.
  • Check the live site on a phone, test the forms, set up the search engine verification and uptime monitoring.

Common mistakes

  • Putting the business data in the theme, so the menu disappears when the design changes.
  • Hard-coding the front page ID instead of reading it from what wp post create printed.
  • Printing prices and titles without escaping in the shortcode.
  • Skipping the launch checklist, especially the backup and the restore test.
  • Building everything on the live site instead of a local or staging copy.

Extensions to try

  1. Add a Menu custom block (a dynamic block) that does what the shortcode does, with the section as an attribute.
  2. Add an opening hours option on a settings page and print it in the footer.
  3. Add an endpoint /wp-json/daily-bread/v1/menu that returns the items as JSON (custom endpoints).
  4. Cache the shortcode's output for an hour with a transient, and clear it when a menu item is saved.
  5. Create a child theme and change the colours without touching the parent (child themes).

Exercise

The owner wants a "Today's special" line on the front page that she changes herself, without editing code or pages. Which feature would you build, and where does the code go?

Show one possible answer

Add a setting in the plugin: a settings page under Settings with one text field, saved in an option with a sanitise callback (see Options and settings pages). Add a shortcode [todays_special] (or a block) that prints esc_html( get_option( 'daily_bread_special' ) ), and put it on the Home page. The code goes in the plugin, because it is a feature of the business, not of the design.

Key points

  • The project builds Daily Bread, a bakery site: a block theme with theme.json, pages and a menu, a plugin with a custom post type and a shortcode, and a launch checklist.
  • Keep design in the theme and features in a plugin, so the client can change one without losing the other.
  • Content is created and configured with WP-CLI, so the build can be repeated on a staging site and on the live site.
  • Before launch: check the pages, escaping and permissions, speed, backups and the move from local to live.

Check yourself

1. Why is the menu (the list of products) created in a plugin rather than the theme?
2. Which setting makes the 'Home' page the front page?
3. What should the shortcode do with the price it prints?
4. Which is the best order before launch?

Final exam and certificate

Finish all 45 lessons to unlock a 15-question exam on WordPress. Pass it and you get a certificate with a public link you can share.

Sign in to take the exam