Build a React Frontend With Umbraco Including Server Side Rendering

In this article we set up Umbraco with a React front end and Server side rendering to improve SEO for search engines.



Recently I wanted to try if Umbraco CMS could be integrated with a React front end while remaining SEO friendly. The solution has to include server-side rendering (SSR) if we want to make it as easy as possible for web crawlers to index our web pages. This code was part of a Proof of Concept at Authority.

I stumbled across different sources related to SSR and Umbraco such as this article or ReactJS .NET, but the first one has a missing repo, and the other one has not been updated since June 2020, which means that it does not support React 18 as of today when writing this post.

After a few trials and errors, I could get something pretty clean working, so I thought I’d share with you the implementation I adopted.



Solution overview


The solution is composed of three different parts:

  • Umbraco Web application
  • Server-side rendering application (Express — Node.js)
  • React application




Concretely, the first request is made to an Umbraco RenderMvcController involving SSR from the Express server, and all subsequent requests are made from the React application to various Umbraco ApiControllers.

Docker-compose to run the app, but the possibility to debug locally.





The first request is made an Umbraco RenderMvcController. By default, all document types in the Umbraco project share the same template, thus all requests to Umbraco pages will use the same function i.e. IndexAsync from within the SsrDataController.


public class SsrDataController : RenderControllerBase
    public SsrDataController(
        ILogger<SsrDataController> logger,
        ICompositeViewEngine compositeViewEngine,
        IUmbracoContextAccessor umbracoContextAccessor) : base(logger, compositeViewEngine, umbracoContextAccessor)

    public sealed override IActionResult Index() => throw new NotImplementedException();

    public async Task<IActionResult> IndexAsync()
        if (CurrentPage == null)
            return CurrentTemplate(CurrentPage);

        var content = await Mediator.Send(new GetSsrDataQuery(CurrentPage));

        return CurrentTemplate(content);


From there, the controller retrieves the model through a call to the Mediator in order to retrieve the model needed for our SSR.


protected override object? Handle(GetSsrDataQuery request)
    var content = request.Content;

    if(content == null)
        return null;

    return content.ContentType.Alias.ToPascalCase() switch
        nameof(ContentModels.Page) => (content as ContentModels.Page)?.GetPage(_mapper),
        nameof(ContentModels.NewsItem) => (content as ContentModels.NewsItem)?.GetNewsItem(_mapper),
        _ => null


Once the model is retrieved, it is injected in the resulting template Index.cshtml and uses Master.cshtml template.


@addTagHelper "*, Microsoft.AspNetCore.Mvc.TagHelpers"
    Layout = null;
@await Html.PartialAsync("_ViewImports")
<!DOCTYPE html>
<html lang="en">
    @await Html.PartialAsync("_Head")
        <noscript>You need to enable JavaScript to run this app.</noscript>
        <div id="root" asp-prerender-module="App" asp-prerender-data="@Model"> <!-- TagHelper is taking the model as input -->
                window.data = @Html.Raw(Json.Serialize(Model));


The TagHelper (asp-prerender-data) added to the root of our React web app, will in turn make a POST request to our Express server with the model as payload.



const router = express.Router();

router.use('^/$', (req, res, next) => {
  const component = ReactDOMServer.renderToString(
    React.createElement(App, { data: req.body })


server.listen(PORT, () => {
  console.log(`Server running on http://localhost:${PORT}`)


Our App component is instantiated server side and provided with all the data necessary to render the page (injected via props).


export default function App(props: { data?: any | undefined }) { // our payload is injected here via props
  const data = props?.data
    <div className="App">
        <NavBar />
        <Switch data={data}/>


Once the App component of our React application is rendered, it is returned as a string to our Umbraco application, that will finally inject it in our view, and return that view to the user.


If you inspect the code source of the page, you will then see that the body of the page is present and prepopulated with data.


All subsequent requests from the React app are using other endpoints directed towards various Umbraco API controllers.


NB: When a component is pre-rendered, useEffect hooks are not run, hence no requests are made at that time.


NB2: If your application contains routing, make sure to use StaticRouter instead of BrowserRouter, otherwise ReactDOMServer will fail to render your component. That is the reason why the root of my application is wrapped with a Router.tsx component that will determine which router type to choose.


function Router(props: { children: any }) {
  return typeof document === 'undefined'
    ? (
      <StaticRouter location={'/'}>
    : (





The Umbraco applications is divided into three types of document types:

  • Collections

  • Components

  • Page types The page types are pretty simple and mainly contain properties:

  • Metadata

  • Block List of sections > Containers > Blocks





React application


The React application is set up via Create-React-App and is fairly basic with a Navigation bar, the main body, and a few block types. Nothing fancy.

The only requirement here is that the React app should obviously be aware of the response type returned by some of the Umbraco ApiControllers.





All the code is available in this GitHub repo, and the README provides all the information necessary to build and test the solution.



Going further


Only a single request to retrieve the body of the page is made in this example, which is enough to retrieve the different sections and blocks of the page. Though other requests would be needed to retrieve the data from the navigation items, or subsequent requests made by other components after they are loaded for example.

I have not included that part here for simplification purposes.





The request for initiating the prerendering is inspired by this repo.