Experience the powerful AI writing right inside WordPress
Show stunning before-and-after transformations with image sliders.
Improve user engagement by showing estimated reading time.
Written by Mahmuda Akter Isha
Showcase Designs Using Before After Slider.
Yes. A traditional WordPress website is primarily server-side rendered. When a visitor requests a page, WordPress processes PHP code, performs the necessary database queries, applies theme templates and plugin hooks, and generates HTML on the server. The browser then receives that HTML and displays it.
WordPress server-side rendering can sound like a modern feature that must be added to a website. In reality, traditional WordPress has always relied heavily on server-side rendering.
When someone visits a standard WordPress page, the server runs PHP, retrieves information from the database, processes the active theme and plugins, generates the page’s HTML, and sends that HTML to the visitor’s browser.
However, the term “WordPress SSR” can also refer to two more specialized development approaches:
Each approach has different use cases, benefits, performance considerations, and development requirements.
This guide explains how server-side rendering works in WordPress, how it compares with other rendering methods, how to create a server-rendered Gutenberg block, and when headless WordPress SSR might be appropriate.
Server-side rendering, commonly called SSR, is a rendering method in which a server generates a page’s initial HTML before sending it to the visitor’s browser.
A simplified SSR request looks like this:
This differs from client-side rendering, where the browser may initially receive a basic HTML shell and then use JavaScript to request data and construct the page.
Because the important content can already be present in the initial HTML response, SSR can make pages easier to display without depending entirely on client-side JavaScript.
However, SSR does not automatically make a website fast. Server processing, database queries, API calls, hosting resources, caching, JavaScript, images, and third-party scripts all influence the final performance.
A traditional WordPress page request generally follows this process.
The visitor opens a page such as:
https://example.com/blog/wordpress-ssr/
The browser sends an HTTP request to the server hosting the WordPress website.
The server starts the WordPress loading process. WordPress loads its core files, configuration, active plugins, theme functions, and registered hooks.
WordPress analyzes the requested URL and uses its query system to determine whether the visitor is requesting a post, page, category archive, search page, product, custom post type, or another resource.
WordPress retrieves the content, settings, metadata, menu information, user permissions, and other data required to build the requested page.
Plugins and themes may perform additional database queries during this stage.
Depending on the theme, WordPress uses either PHP template files or block templates to determine the structure of the page.
A classic theme might use files such as:
single.php page.php archive.php header.php footer.php
A block theme may use HTML templates, template parts, patterns, and dynamic blocks.
WordPress combines the retrieved content with the selected templates. Dynamic blocks and PHP functions generate their output at this stage.
Plugins and themes can modify the content, metadata, scripts, styles, menus, and final HTML through WordPress actions and filters.
The completed HTML document is sent to the visitor. The browser parses the document and loads the associated CSS, JavaScript, fonts, images, and other assets.
This process is why standard WordPress can be described as a server-rendered content management system.
WordPress SSR development can be divided into three major categories.
Traditional rendering is the default approach used by most WordPress websites.
WordPress runs on the server, retrieves content from its database, processes the active theme and plugins, and produces the final HTML for each request.
This approach is suitable for:
Traditional WordPress development normally offers the simplest deployment process because the content-management system and presentation layer operate within the same application.
Page caching can prevent PHP and database operations from running for every public visit. When a cached version exists, the server may return the previously generated HTML instead of rebuilding the page.
A Gutenberg block can be static or dynamic.
A static block saves its HTML structure in the WordPress database when the post is saved. WordPress retrieves that saved markup when displaying the page.
A dynamic block generates its frontend markup on the server when the page is requested.
Dynamic blocks are especially useful when the output must reflect current information, such as:
WordPress supports two primary methods for creating dynamic blocks:
render_callback
register_block_type()
render
block.json
The PHP rendering process receives the block attributes, saved content, and block instance.
In a headless architecture, WordPress manages the content but does not directly control the public-facing frontend.
A separate frontend application retrieves WordPress content through an API, such as:
The frontend may be built with:
The frontend framework then decides whether each page should be generated at request time, generated during a build, cached, revalidated, streamed, or rendered in the browser.
Current Next.js applications use Server Components by default in the App Router. Routes can be prerendered or dynamically rendered depending on their data and request-time requirements.
Understanding the differences between rendering methods can help you select the right architecture.
No single method is best for every website.
A news homepage might need dynamic rendering because its content changes frequently. A documentation page may be better suited to static generation. A user dashboard may require request-time rendering, while interactive filters may use client-side rendering after the first page loads.
Modern websites frequently combine several rendering techniques.
WordPress server-side rendering offers several practical advantages for website performance, accessibility, content delivery, and development flexibility. By generating important page content on the server before sending it to the browser, WordPress can provide a more reliable initial experience while supporting dynamic data, progressive enhancement, and search engine visibility.
Important headings, paragraphs, links, navigation, product information, and other content can be included in the server’s initial response.
This reduces the website’s dependence on JavaScript for displaying essential information.
A server-rendered page can provide a functional foundation before optional JavaScript features are loaded.
Forms, navigation, links, and content can remain accessible while JavaScript adds enhancements such as filters, live search, sliders, pop-ups, or asynchronous updates.
Google can process JavaScript, but server rendering or prerendering still helps users and crawlers receive meaningful content sooner. Google also notes that not every bot can run JavaScript.
Dynamic WordPress blocks can display current information without requiring editors to open and update every post containing the block.
For example, a Latest Posts block can automatically display a newly published article.
A dynamic block’s HTML is generated by its server-side PHP template. Updating that template can update every instance of the block across the website.
This can be easier to maintain than modifying saved HTML in hundreds of posts.
Server-rendered components can use:
The visitor’s device does not need to build all initial page content from raw API data. This can be helpful for users on slower devices, although the total benefit depends on how much JavaScript the website loads afterward.
WordPress server-side rendering provides strong control over how content is generated and delivered, but it also introduces several technical challenges. Server processing, database queries, caching requirements, hosting limitations, and complex frontend integrations can affect performance and scalability. Understanding these limitations helps developers decide when SSR is appropriate and how to optimize it effectively.
An uncached request may require PHP execution, database queries, plugin processing, API requests, and template rendering.
High-traffic websites may need stronger hosting, caching, database optimization, or load balancing.
SSR does not automatically reduce Time to First Byte.
If the server must perform expensive operations before returning HTML, the browser may wait longer for the first response. A slow database, overloaded server, external API, or poorly optimized plugin can increase TTFB.
Dynamic content creates difficult caching decisions.
A website may need different responses for:
Incorrect caching can show stale or inappropriate information.
A server-rendered JavaScript application may still need to download and execute JavaScript before its interactive components become usable.
Sending HTML from the server does not eliminate the cost of a large JavaScript bundle.
A headless WordPress setup introduces another application, hosting environment, deployment workflow, caching layer, and potential source of errors.
Editors may also need custom preview and publishing workflows.
Some WordPress plugins assume that the active WordPress theme controls the frontend. These plugins may not work automatically in a headless frontend.
Forms, SEO metadata, redirects, search, memberships, authentication, previews, and WooCommerce features may need additional integration.
The following example creates a dynamic Latest Posts block.
Editors can select how many posts to display, while PHP generates the final list on the server.
A simple plugin structure might look like this:
codecanel-latest-posts/ ├── codecanel-latest-posts.php ├── src/ │ └── latest-posts/ │ ├── block.json │ ├── edit.js │ ├── index.js │ ├── render.php │ └── style.scss ├── build/ └── package.json
For a real project, the official @wordpress/create-block package can generate much of the initial structure.
@wordpress/create-block
Create the main plugin file:
<?php /** * Plugin Name: CodeCanel Latest Posts Block * Description: Displays a configurable list of recent WordPress posts. * Version: 1.0.0 * Author: CodeCanel * Text Domain: codecanel */ if ( ! defined( 'ABSPATH' ) ) { exit; } /** * Registers the dynamic block. */ function codecanel_register_latest_posts_block() { register_block_type( __DIR__ . '/build/latest-posts' ); } add_action( 'init', 'codecanel_register_latest_posts_block' );
Using the generated build directory ensures that WordPress loads the compiled JavaScript, CSS, metadata, and PHP rendering file.
Create the following block metadata:
{ "$schema": "https://schemas.wp.org/trunk/block.json", "apiVersion": 3, "name": "codecanel/latest-posts", "version": "1.0.0", "title": "Latest Posts", "category": "widgets", "icon": "list-view", "description": "Displays a server-rendered list of recent posts.", "keywords": ["posts", "recent posts", "latest posts"], "textdomain": "codecanel", "attributes": { "numberOfPosts": { "type": "number", "default": 5 } }, "editorScript": "file:./index.js", "style": "file:./style-index.css", "render": "file:./render.php" }
WordPress recommends block.json as the canonical way to register block metadata. The current Block API version is 3, and the render property can identify the PHP file used to generate frontend output.
Create index.js:
index.js
import { registerBlockType } from '@wordpress/blocks'; import Edit from './edit'; import './style.scss'; registerBlockType('codecanel/latest-posts', { edit: Edit, save: () => null, });
Returning null from save means WordPress stores the block delimiter and attributes instead of fixed frontend HTML.
null
save
The PHP rendering file will control the frontend output.
Create edit.js:
edit.js
import { InspectorControls, useBlockProps, } from '@wordpress/block-editor'; import { Notice, PanelBody, RangeControl, Spinner, } from '@wordpress/components'; import { RawHTML } from '@wordpress/element'; import { useServerSideRender } from '@wordpress/server-side-render'; export default function Edit({ attributes, setAttributes }) { const { numberOfPosts } = attributes; const blockProps = useBlockProps(); const { content, status, error } = useServerSideRender({ block: 'codecanel/latest-posts', attributes, }); return ( <> <InspectorControls> <PanelBody title="Latest Posts Settings"> <RangeControl label="Number of posts" value={numberOfPosts} onChange={(value) => setAttributes({ numberOfPosts: value, }) } min={1} max={10} /> </PanelBody> </InspectorControls> <div {...blockProps}> {status === 'loading' && <Spinner />} {status === 'error' && ( <Notice status="error" isDismissible={false}> {error?.message || 'The latest posts preview could not be loaded.'} </Notice> )} {status === 'success' && ( <RawHTML>{content}</RawHTML> )} </div> </> ); }
The useServerSideRender hook retrieves a server-rendered block preview, manages loading and error states, and debounces repeated requests. It can use GET or POST depending on the request payload.
useServerSideRender
Create render.php:
render.php
<?php /** * Server-side rendering for the Latest Posts block. * * Available variables: * * @var array $attributes Block attributes. * @var string $content Saved block content. * @var WP_Block $block Current block instance. */ if ( ! defined( 'ABSPATH' ) ) { exit; } $number_of_posts = isset( $attributes['numberOfPosts'] ) ? absint( $attributes['numberOfPosts'] ) : 5; $number_of_posts = max( 1, min( 10, $number_of_posts ) ); $latest_posts_query = new WP_Query( array( 'post_type' => 'post', 'post_status' => 'publish', 'posts_per_page' => $number_of_posts, 'ignore_sticky_posts' => true, 'no_found_rows' => true, 'update_post_meta_cache' => false, 'update_post_term_cache' => false, ) ); ?> <div <?php echo get_block_wrapper_attributes( array( 'class' => 'codecanel-latest-posts', ) ); ?> > <?php if ( $latest_posts_query->have_posts() ) : ?> <ul class="codecanel-latest-posts__list"> <?php while ( $latest_posts_query->have_posts() ) : ?> <?php $latest_posts_query->the_post(); ?> <li class="codecanel-latest-posts__item"> <a class="codecanel-latest-posts__link" href="<?php echo esc_url( get_permalink() ); ?>" > <?php echo esc_html( get_the_title() ); ?> </a> </li> <?php endwhile; ?> </ul> <?php wp_reset_postdata(); ?> <?php else : ?> <p class="codecanel-latest-posts__empty"> <?php echo esc_html__( 'No published posts were found.', 'codecanel' ); ?> </p> <?php endif; ?> </div>
This implementation:
Because render.php can run for every instance of the block, avoid declaring reusable functions or classes directly inside the file. Shared logic should be placed elsewhere and called from the rendering template.
Create style.scss:
style.scss
.wp-block-codecanel-latest-posts { padding: 1.25rem; border: 1px solid currentColor; border-radius: 0.5rem; } .codecanel-latest-posts__list { margin: 0; padding-left: 1.25rem; } .codecanel-latest-posts__item + .codecanel-latest-posts__item { margin-top: 0.5rem; } .codecanel-latest-posts__link { font-weight: 600; }
Use theme-compatible values rather than hardcoded branding colors when the block must work across different WordPress themes.
Run the appropriate build commands:
npm install npm run build
Activate the plugin, insert the Latest Posts block, change the number of posts, and test both the editor preview and frontend output.
Test the block under the following conditions:
Both methods generate dynamic block output in PHP.
A callback-based version might look like this:
function codecanel_render_dynamic_message_block( $attributes ) { $message = isset( $attributes['message'] ) ? sanitize_text_field( $attributes['message'] ) : __( 'Hello from WordPress.', 'codecanel' ); return sprintf( '<div %1$s>%2$s</div>', get_block_wrapper_attributes(), esc_html( $message ) ); } function codecanel_register_dynamic_message_block() { register_block_type( __DIR__ . '/build/dynamic-message', array( 'render_callback' => 'codecanel_render_dynamic_message_block', ) ); } add_action( 'init', 'codecanel_register_dynamic_message_block' );
WordPress officially supports both rendering methods.
ServerSideRender
The ServerSideRender component and useServerSideRender hook are useful when the editor preview must reuse existing PHP rendering logic.
However, they should not automatically become the data layer for every new block.
A dedicated REST endpoint or WordPress data store may provide a better editor experience when the block requires:
Use server-rendered previews when reusing PHP output provides a meaningful development or consistency benefit.
A headless website separates the WordPress backend from the public frontend.
WordPress handles:
Next.js handles:
A simplified Next.js App Router page could retrieve a post by its WordPress slug.
import { notFound } from 'next/navigation'; type WordPressPost = { id: number; slug: string; title: { rendered: string; }; content: { rendered: string; }; }; async function getPost(slug: string): Promise<WordPressPost | null> { const apiBaseUrl = process.env.WORDPRESS_API_URL; if (!apiBaseUrl) { throw new Error('WORDPRESS_API_URL is not configured.'); } const requestUrl = `${apiBaseUrl}/wp-json/wp/v2/posts` + `?slug=${encodeURIComponent(slug)}`; const response = await fetch(requestUrl, { cache: 'no-store', }); if (!response.ok) { throw new Error( `WordPress request failed with status ${response.status}.` ); } const posts: WordPressPost[] = await response.json(); return posts[0] ?? null; } export default async function BlogPostPage({ params, }: { params: Promise<{ slug: string }>; }) { const { slug } = await params; const post = await getPost(slug); if (!post) { notFound(); } return ( <main> <article> <h1 dangerouslySetInnerHTML={{ __html: post.title.rendered, }} /> <div dangerouslySetInnerHTML={{ __html: post.content.rendered, }} /> </article> </main> ); }
The example uses request-time fetching. In a production project, you must also plan:
Next.js App Router pages and layouts are Server Components by default. Server rendering can happen during prerendering or dynamically at request time, depending on the route and data requirements.
Traditional WordPress and headless WordPress SSR both generate content on the server, but they use different architectures. Traditional WordPress manages the content, templates, plugins, and frontend within one system, while a headless setup uses WordPress as the content backend and a separate framework to build and render the public website.
The right option depends on your project’s flexibility, performance, integration, maintenance, and development requirements.
Headless WordPress is not automatically faster or more modern in a way that benefits every business.
For an ordinary blog, company website, portfolio, or WooCommerce store, traditional WordPress may be easier to build, maintain, optimize, and hand over.
Headless development makes more sense when the project has clear requirements that traditional WordPress cannot efficiently satisfy.
The WordPress Interactivity API provides a standard approach for adding interactive frontend behavior to blocks.
It allows WordPress to generate the initial, state-aware HTML on the server. Client-side JavaScript can then handle later interactions without requiring a full page reload.
The process can combine:
To mark a block as interactive, its block.json can include:
{ "supports": { "interactivity": true } }
A block can then use WordPress interactivity directives in its server-rendered markup.
The Interactivity API was introduced in WordPress 6.5 and is used by WordPress core blocks. WordPress processes supported directives on the server so that the initial HTML already contains the appropriate state before client-side behavior takes over.
This approach is useful for features such as:
It provides WordPress developers with a native path for building interactive blocks without replacing WordPress’s server-rendering foundation.
Caching is not a separate type of server-side rendering.
It is an optimization layer that can store the result of expensive operations so that WordPress does not repeat them unnecessarily.
Full-page caching stores the completed HTML response for a page.
Instead of running WordPress, querying the database, and generating the HTML for every visitor, the server can return the cached HTML.
This is especially effective for public pages that show the same content to every visitor.
Object caching stores the results of expensive database operations.
Persistent object-cache systems such as Redis or Memcached can preserve cached objects between separate page requests.
Browser caching allows static files such as CSS, JavaScript, images, and fonts to remain on the visitor’s device for a defined period.
A content delivery network can store cacheable content at geographically distributed locations.
This reduces the distance between the visitor and the cached resource.
Opcode caching stores compiled PHP instructions so the server does not need to repeatedly parse and compile the same PHP files.
Developers can cache expensive query results or API responses with the WordPress Transients API, object cache, or another controlled caching layer.
Every caching strategy needs a plan for removing stale data.
A cache may need to be cleared when:
Caching should be planned as part of the rendering architecture rather than installed as an afterthought.
SSR can support SEO, but it does not guarantee higher rankings.
Server rendering can ensure that important content, headings, links, metadata, and structured information are available in the initial HTML response.
This can reduce the website’s dependence on JavaScript rendering and make content easier for different crawlers to process.
Google can render JavaScript, but Google still recommends server-side rendering, static rendering, or hydration instead of using dynamic rendering as a permanent workaround. Dynamic rendering refers to serving one version to bots and a different version to users.
Search visibility still depends on factors such as:
A slow, thin, or irrelevant server-rendered page will not outrank a useful page simply because it uses SSR.
Server-side rendering affects performance, but it is only one part of the full page-loading process.
Largest Contentful Paint measures how quickly the main visible content appears.
SSR may help make the main content available earlier, but LCP can still be delayed by:
Interaction to Next Paint measures responsiveness.
A server-rendered page may still have poor responsiveness if it loads and executes excessive JavaScript or performs expensive work after a user interaction.
Cumulative Layout Shift measures visual stability.
SSR does not prevent layout shifts caused by:
Google’s recommended “good” thresholds are an LCP within 2.5 seconds, an INP below 200 milliseconds, and a CLS below 0.1.
TTFB measures how long it takes for the browser to begin receiving the server response.
Server-side rendering can increase TTFB when the server performs expensive work. Improve it through:
Measure the complete experience rather than assuming that one rendering method will solve every performance issue.
WordPress SSR issues often come from slow server processing, inefficient database queries, caching conflicts, block-rendering errors, or differences between server and client output. Identifying the underlying cause helps developers improve performance, maintain consistent content, and prevent errors in both traditional and headless WordPress environments.
Server rendering handles data that may come from block attributes, post content, user input, APIs, or the database.
Follow these practices:
Use the appropriate WordPress sanitization function before storing or processing values.
Examples include:
sanitize_text_field() sanitize_email() absint() sanitize_key() esc_url_raw()
Escape values according to where they appear.
esc_html() esc_attr() esc_url() wp_kses_post()
Do not assume that block attributes always contain the expected type or range.
Convert and limit numeric values before using them in queries.
Any server endpoint that exposes private, draft, account, or administrative data must verify the user’s capabilities.
Use WordPress nonces for requests that perform protected actions. A nonce should not replace capability checks.
Do not expose private API keys or backend credentials in client-side JavaScript.
Only render trusted HTML. Sanitize content when it can include untrusted or externally supplied markup.
Use traditional WordPress rendering when:
Use dynamic Gutenberg blocks when:
Use headless WordPress SSR when:
Use static generation when:
Use client-side rendering selectively when:
WordPress server-side rendering is not one single technology.
It can refer to the traditional PHP rendering used by ordinary WordPress websites, the dynamic PHP rendering used by Gutenberg blocks, or a separate server-rendered frontend connected to a headless WordPress backend.
For most WordPress websites, traditional rendering combined with effective page caching, optimized database queries, strong hosting, and selective JavaScript is a practical solution.
Dynamic Gutenberg blocks are valuable when content must remain current or when developers need centralized control over block markup.
Headless WordPress SSR provides greater frontend flexibility, but that flexibility comes with additional development, deployment, caching, preview, SEO, and maintenance responsibilities.
The best approach is not the one with the newest terminology. It is the architecture that delivers accurate content, reliable performance, maintainable code, and a smooth experience for both website visitors and content editors.
Yes. Traditional WordPress uses PHP and database queries to generate HTML on the server before sending it to the browser.
Gutenberg server-side rendering means that a block’s frontend HTML is generated by PHP when WordPress renders the page. The block can use a render_callback or a render.php file.
A dynamic block generates its output at page-rendering time instead of relying entirely on HTML stored when the editor saved the post.
Use a dynamic block when its content needs to reflect current data, user information, query results, changing metadata, or centrally maintained markup.
No. SSR can make content available in the initial HTML, but rankings still depend on relevance, quality, authority, technical accessibility, and many other signals.
Not automatically. Generating HTML on every request can increase TTFB. Caching, efficient queries, fast hosting, and server optimization are usually required.
No. Caching is an optimization that stores previously generated data or HTML. It can be used with server-side rendering, static generation, APIs, and other architectures.
It depends on the page. SSR is helpful for initial content, while client-side rendering can be useful for highly interactive behavior after the page loads.
A static block saves its frontend markup in the database. A dynamic block uses PHP to generate its frontend markup when the page is rendered.
Use render.php for template-oriented block markup registered through block.json. Use render_callback when a reusable PHP function or programmatic registration is more appropriate.
This page was last edited on 20 July 2026, at 6:08 pm
Your email address will not be published. Required fields are marked *
Comment *
Name *
Email *
Website
Save my name, email, and website in this browser for the next time I comment.
How many people work in your company?Less than 1010-5050-250250+
By proceeding, you agree to our Privacy Policy