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
Template tags such as <?php the_title(); ?> and <?php the_content(); ?> are WordPress functions that print one piece of the current post — and understanding exactly what each one does to your content is what makes a template correct rather than merely working.
<?php while ( have_posts() ) : the_post(); ?>
<article id="post-<?php the_ID(); ?>" <?php post_class(); ?>>
<h1 class="entry-title"><?php the_title(); ?></h1>
<div class="entry-content">
<?php the_content(); ?>
</div>
</article>
<?php endwhile; ?>Both must run inside the Loop, after the_post(). That call is what tells them which post is "current".
the_title() takes three optional parameters: text to print before the title, text to print after it, and whether to echo.
<?php
// the_title( $before, $after, $display )
// Wrap the title in a heading — nothing is printed if the title is empty.
the_title( '<h1 class="entry-title">', '</h1>' );
// Make the title a link on archive pages.
the_title(
sprintf( '<h2 class="entry-title"><a href="%s" rel="bookmark">', esc_url( get_permalink() ) ),
'</a></h2>'
);Tip: Passing the wrapper as$beforeand$afteris better than hard-coding<h1>around the call. If a post has no title, WordPress prints nothing at all — no empty<h1></h1>that screen readers would announce.
Behind the scenes, the_title() runs the title through the the_title filter. Plugins use that filter, and WordPress itself uses it to add "Private:" or "Protected:" in front of restricted posts. That is why you call the function rather than reading $post->post_title directly.
the_content() does far more than print text. It runs the stored post content through the the_content filter, which in turn:
[gallery] becomes a gallery.<p> tags around paragraphs with wpautop() for content written in the classic editor.srcset and lazy-loading attributes to images.Watch out: If you print$post->post_contentdirectly, none of that happens. Shortcodes show as raw[gallery]text and blocks show as HTML comments.
It also handles the More tag. On archive pages, content above <!--more--> is shown with a "Continue reading" link. Customise the link text with the first parameter:
<?php
the_content(
sprintf(
/* translators: %s: post title, only visible to screen readers */
esc_html__( 'Continue reading %s', 'gdc-starter' ),
'<span class="screen-reader-text">' . esc_html( get_the_title() ) . '</span>'
)
);And when a post is split into pages with the Page Break block, the_content() shows only the current page. Add wp_link_pages() right after it so readers can reach the other pages:
<div class="entry-content">
<?php
the_content();
wp_link_pages(
array(
'before' => '<nav class="page-links">' . esc_html__( 'Pages:', 'gdc-starter' ),
'after' => '</nav>',
)
);
?>
</div>Almost every template tag comes in two versions. This is the most important pattern in this lesson.
| Prints (echoes) | Returns a value | Use the return version when |
|---|---|---|
the_title() | get_the_title() | You need the title inside an attribute, a string, or a condition. |
the_permalink() | get_permalink() | You are building a URL or passing it to another function. |
the_excerpt() | get_the_excerpt() | You want to trim or test the excerpt first. |
the_ID() | get_the_ID() | You are passing the ID to another function. |
the_content() | apply_filters( 'the_content', get_the_content() ) | Rarely — get_the_content() alone skips the filters above. |
Here is the difference in practice. The first line is wrong; the second is correct:
<!-- Wrong: the_title() echoes immediately, so the title appears
BEFORE the <img> tag and the alt attribute is empty. -->
<img src="cover.jpg" alt="<?php echo the_title(); ?>">
<!-- Right: get_the_title() returns the string, escaped for an attribute. -->
<img src="cover.jpg" alt="<?php echo esc_attr( get_the_title() ); ?>">Functions that echo — the_title(), the_content(), the_permalink() — handle their own output safely for their usual context. Functions that return hand you raw data, and you must escape it for where it is going:
| Output is going into | Escape with |
|---|---|
| Normal HTML text | esc_html( get_the_title() ) |
| An HTML attribute | esc_attr( get_the_title() ) |
A URL attribute (href, src) | esc_url( get_permalink() ) |
Lesson 25 covers escaping in depth. The habit to build now: **every echo gets an escaping function**.
| Tag | Prints |
|---|---|
the_ID() | The post ID, e.g. 42. |
the_permalink() | The post URL. |
the_excerpt() | The manual excerpt, or the first 55 words with HTML removed. |
post_class() | class="..." with status, type, format and category classes. |
body_class() | class="..." for <body>, describing the whole page. |
edit_post_link() | An "Edit" link, shown only to users allowed to edit the post. |
The get_ versions accept a post ID or post object, so they work anywhere — in a sidebar, a widget, or a function:
<?php
$about_page = get_page_by_path( 'about' );
if ( $about_page ) {
printf(
'<a class="about-link" href="%1$s">%2$s</a>',
esc_url( get_permalink( $about_page ) ),
esc_html( get_the_title( $about_page ) )
);
}<?php echo the_title(); ?>. The echo is redundant — the_title() already prints and returns nothing.get_the_content() and printing it, which skips shortcodes and blocks.the_title() before the_post() has run, which prints the title of the wrong post or none.single.php with the_title() wrapped through its $before and $after parameters and the_content() followed by wp_link_pages().[gallery] shortcode. Print it once with the_content() and once with echo $post->post_content; and compare.alt attribute from get_the_title() and confirm it is escaped with esc_attr().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.