Build classic and block WordPress themes from scratch: the template hierarchy, The Loop, the_title() and the_content(), template parts, menus, the Customizer, child themes and theme.json.
Lessons
Two templates handle visitors who are looking for something: search.php shows the results of the site search, and 404.php handles URLs that do not exist — and both should help the visitor take a next step rather than leaving them stuck.
<?php
get_header();
?>
<main id="main" class="site-main">
<header class="page-header">
<h1 class="page-title">
<?php
printf(
/* translators: %s: the search query */
esc_html__( 'Search results for: %s', 'gdc-starter' ),
'<span>' . esc_html( get_search_query() ) . '</span>'
);
?>
</h1>
</header>
<?php if ( have_posts() ) : ?>
<?php while ( have_posts() ) : the_post(); ?>
<?php get_template_part( 'template-parts/content', 'search' ); ?>
<?php endwhile; ?>
<?php the_posts_pagination(); ?>
<?php else : ?>
<p><?php esc_html_e( 'Nothing matched your search. Try different keywords.', 'gdc-starter' ); ?></p>
<?php get_search_form(); ?>
<?php endif; ?>
</main>
<?php
get_footer();Watch out: The search query is typed by the visitor, so it is untrusted.get_search_query()escapes it by default, but always wrap it inesc_html()when printing — a search for<script>must show as text.
Every call to get_search_form() — in your templates, in the Search widget — uses searchform.php if your theme has one:
<?php $unique_id = wp_unique_id( 'search-form-' ); ?>
<form role="search" method="get" class="search-form" action="<?php echo esc_url( home_url( '/' ) ); ?>">
<label for="<?php echo esc_attr( $unique_id ); ?>" class="screen-reader-text">
<?php esc_html_e( 'Search for:', 'gdc-starter' ); ?>
</label>
<input type="search"
id="<?php echo esc_attr( $unique_id ); ?>"
class="search-field"
name="s"
value="<?php echo get_search_query(); ?>"
placeholder="<?php esc_attr_e( 'Search…', 'gdc-starter' ); ?>">
<button type="submit" class="search-submit"><?php esc_html_e( 'Search', 'gdc-starter' ); ?></button>
</form>s — that is the query variable WordPress searches with.wp_unique_id() gives each form on the page a different id, so two search forms never share one label.get_search_query() with no argument is already escaped for use in an attribute.<?php
get_header();
?>
<main id="main" class="site-main error-404">
<header class="page-header">
<h1 class="page-title"><?php esc_html_e( 'That page can’t be found.', 'gdc-starter' ); ?></h1>
</header>
<div class="page-content">
<p><?php esc_html_e( 'It may have moved, or the link may be wrong. Try a search, or start from one of these:', 'gdc-starter' ); ?></p>
<?php get_search_form(); ?>
<h2><?php esc_html_e( 'Recent posts', 'gdc-starter' ); ?></h2>
<ul>
<?php
wp_get_archives(
array(
'type' => 'postbypost',
'limit' => 5,
)
);
?>
</ul>
</div>
</main>
<?php
get_footer();Tip: WordPress already sends a real 404 status code with this template, so search engines drop the dead URL. Do not redirect every 404 to the home page — that tells search engines the broken URL is valid.
search.php and search for a word, then for <b>test</b>, and confirm it prints as text.searchform.php and check the search widget uses it./this-does-not-exist/ and confirm your 404.php shows.In accordance with Section 6 & Section 7 of the DPDP Act 2023, third-party analytics and marketing trackers are strictly gated behind your affirmative consent. You may withdraw consent at any time under Section 6(4).
Essential for session authentication (ci_session), Cross-Site Request Forgery security (csrf_cookie_name), and Cloudflare DDoS/bot prevention. Cannot be disabled.
Gated under DPDP Section 7. Allows Google Analytics 4 (Google Consent Mode v2) to measure aggregate, anonymized tool usage.
Enables browser local storage to preserve your theme selection (dark/light mode) and code editor layout between visits.
Used to display developer sponsorships and native tool partner notices.