in this article we will do the following
- update homepage content in umbraco and add new property
- create a service and interface to return data from umbraco and add it to DI
- create a controller to use that service
- create custom hook to fetch data from umbraco API
Updating our current homepage
- navigate to /umbraco and login to dashboard
- click on settings/document types > homepage
- click on add property > enter name as image, then click on select editor and select image cropper
- click on save to save the changes
- then click on settings (check image below) and select models builder to update homepage model so we can use it our service
- then click on Content, update the content and click Save and publish
current folder structure without ClientApp,
├├─> MyCustomUmbracoProject
│ ├─> .vscode
│ │ ├─> launch.json
│ │ ├─> tasks.json
│ ├─> appsettings.json
│ ├─> ClientApp //to be covered in bit
│ ├─> Controllers
│ │ ├─> HomeController.cs
│ │ ├─> ContentController.cs
│ ├─> Interfaces
│ ├─> IContentService.cs
│ ├─> Models
│ │ ├─> GenericResult.cs
│ │ ├─> HomepageDTO.cs
│ ├─> Services
│ ├─> ContentService.cs
│ ├─> umbraco
│ ├─> models
│ │ ├─> Homepage.generated.cs
add Interfaces, Services Folder and create the following
- under Models add 2 classes HomepageDTO, GenericResult ```cs
namespace MyCustomUmbracoProject.Models
{
public class GenericResult
{
public T Data { get; set; }
public bool Success { get; set; }
public string Message { get; set; } = null;
public string Error { get; set; } = null;
public IEnumerable ErrorMessages { get; set; } = Enumerable.Empty();
}
}
```cs
namespace MyCustomUmbracoProject.Models
{
public class HomepageDTO
{
public string? Title { get; set; }
public string? ImageUrl { get; set; }
}
}
- create a interface named IContentService ```cs
using MyCustomUmbracoProject.Models;
namespace MyCustomUmbracoProject.Interfaces
{
public interface IContentService
{
GenericResult GetHomeContent();
}
}
- create a Service named ContentService
```cs
using MyCustomUmbracoProject.Interfaces;
using MyCustomUmbracoProject.Models;
using Umbraco.Cms.Web.Common;
using ContentModels = Umbraco.Cms.Web.Common.PublishedModels;
namespace MyCustomUmbracoProject.Services
{
public class ContentService : IContentService
{
private UmbracoHelper _umbracoHelper;
private readonly ILogger<ContentService> _logger;
public ContentService(ILogger<ContentService> logger, UmbracoHelper umbracoHelper)
{
_logger = logger;
_umbracoHelper = umbracoHelper;
}
public GenericResult<HomepageDTO> GetHomeContent()
{
GenericResult<HomepageDTO> result = new GenericResult<HomepageDTO>();
try
{
var model = this._umbracoHelper?.ContentAtRoot()?.DescendantsOrSelf<ContentModels.Homepage>().FirstOrDefault() ?? null;
result.Data = new HomepageDTO()
{
Title = model?.Title ?? "",
ImageUrl = model?.Image?.Src ?? ""
};
result.Success = true;
result.Message = "Content Fetched Successfully";
}
catch (System.Exception ex)
{
result.Success = false;
result.Message = "Something went wrong";
result.Error = ex.Message;
this._logger.LogError(ex, "error while getting data for HomeContent");
}
return result;
}
}
}
- inject the service and interface in DI in Startup.cs
public void ConfigureServices(IServiceCollection services)
{
//Rest of code
services.AddScoped<IContentService, ContentService>();
}
- create controller named Content Controller and inject content service into constructor
using Microsoft.AspNetCore.Mvc;
using MyCustomUmbracoProject.Models;
using MyCustomUmbracoProject.Interfaces;
namespace MyCustomUmbracoProject
{
[Route("api/[controller]")]
public class ContentController : ControllerBase
{
private IContentService _contentService;
public ContentController(IContentService contentService)
{
_contentService = contentService;
}
[HttpGet]
[Route("home-content")]
public ActionResult<HomepageDTO> FetchHomeContent()
{
var result = this._contentService.GetHomeContent();
return Ok(result);
}
}
}
now for the ClientApp
├─> MyCustomUmbracoProject
│ ├─> ClientApp
│ │ ├─> src
│ │ │ ├─> components
│ │ │ ├─> Spinner.tsx
│ │ │ ├─> hooks
│ │ │ ├─> use-fetch.ts
│ │ │ ├─> models
│ │ │ │─> generic-result.ts
│ │ │ ├─> home-page.ts
create use-fetch.ts hook into hooks folder
import { useCallback, useEffect, useReducer, useRef } from 'react'
interface State<T> {
response?: T
error?: Error
loading: boolean;
runQuery: (params?: Record<string, any>) => void;
}
interface FetchOptions<T> {
url?: string;
options?: RequestInit;
runOnFirstRender?: boolean;
}
type Cache<T> = { [url: string]: T }
// discriminated union type
type Action<T> =
| { type: 'loading' }
| { type: 'fetched'; payload: T }
| { type: 'error'; payload: Error }
const useFetch = <T = unknown>({ url, options, runOnFirstRender = true }: FetchOptions<T>): State<T> => {
const cache = useRef<Cache<T>>({})
// Used to prevent state update if the component is unmounted
const cancelRequest = useRef<boolean>(false)
const initialState: State<T> = {
error: undefined,
response: undefined,
loading: false,
runQuery: () => (params?: Record<string, any> | undefined): void => {
}
}
// Keep state logic separated
const fetchReducer = (state: State<T>, action: Action<T>): State<T> => {
switch (action.type) {
case 'loading':
return { ...initialState, loading: true }
case 'fetched':
return { ...initialState, response: action.payload, loading: false }
case 'error':
return { ...initialState, error: action.payload, loading: false }
default:
return state
}
}
const [state, dispatch] = useReducer(fetchReducer, initialState)
const runQuery = useCallback((params?: Record<string, any>) => {
if (url) {
fetchData(url, params)
}
}, [url])
const fetchData = async (url: string, params?: Record<string, any>) => {
dispatch({ type: 'loading' })
// If a cache exists for this url, return it
if (cache.current[url]) {
dispatch({ type: 'fetched', payload: cache.current[url] })
return
}
try {
const response = await fetch(url, options)
if (!response.ok) {
throw new Error(response.statusText ? response.statusText : response.status.toString())
}
const data = (await response.json()) as T
cache.current[url] = data
if (cancelRequest.current) return
dispatch({ type: 'fetched', payload: data })
} catch (error) {
if (cancelRequest.current) return
dispatch({ type: 'error', payload: error as Error })
}
}
useEffect(() => {
// Do nothing if the url is not given
if (!url) return
cancelRequest.current = false
if (runOnFirstRender) fetchData(url, options)
// Use the cleanup function for avoiding a possibly...
// ...state update after the component was unmounted
return () => {
cancelRequest.current = true
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [url, runOnFirstRender])
return { response: state.response, loading: state.loading, error: state.error, runQuery: runQuery }
}
export default useFetch
-update App.tsx
import { Route, HashRouter as Router, Routes } from "react-router-dom";
import "./App.css";
import reactLogo from "./assets/react.svg";
import viteLogo from "./assets/vite.png";
import { About } from "./pages/About";
import { ContactUs } from "./pages/Contact";
import { Home } from "./pages/Home";
import { NotFound } from "./pages/NotFound";
export const App = () => {
return (
<>
<nav>
<h2 className="title">Vite + React + Umbraco</h2>
<ul>
<li>
<a href="/">Home </a>
</li>
<li>
<a href="#/about">About</a>
</li>
<li>
<a href="#/contact">Contact</a>
</li>
</ul>
</nav>
<div className="App">
<div>
<a href="https://vitejs.dev" target="_blank">
<img src={viteLogo} className="logo" alt="Vite logo" />
</a>
<a href="https://reactjs.org" target="_blank">
<img src={reactLogo} className="logo react" alt="React logo" />
</a>
<a href="https://reactjs.org" target="_blank">
<img
src="https://user-images.githubusercontent.com/6791648/60256231-6e710c00-98d1-11e9-8120-06eecbdb858e.png"
className="umbraco logo"
alt="umbraco logo"
/>
</a>
</div>
<Router>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/contact" element={<ContactUs />} />
<Route path="*" element={<NotFound />} />
</Routes>
</Router>
<div className="card"></div>
</div>
</>
);
};
- update app.css
* {
padding: 0;
margin: 0;
}
#root {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
text-align: center;
}
.logo {
height: 6em;
padding: 1.5em;
will-change: filter;
transition: filter 300ms;
}
.logo:hover {
filter: drop-shadow(0 0 2em #646cffaa);
}
.logo.react:hover {
filter: drop-shadow(0 0 2em #61dafbaa);
}
@keyframes logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
@media (prefers-reduced-motion: no-preference) {
a:nth-of-type(2) .logo {
animation: logo-spin infinite 20s linear;
}
}
.error {
color: red;
}
/* nav start */
nav {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem 2rem;
background: #cfd8dc;
}
nav ul {
display: flex;
list-style: none;
}
nav li {
padding-left: 1rem;
}
nav a {
text-decoration: none;
color: #0d47a1;
}
/*
Extra small devices (phones, 600px and down)
*/
@media only screen and (max-width: 600px) {
nav {
flex-direction: column;
}
nav ul {
flex-direction: column;
padding-top: 0.5rem;
}
nav li {
padding: 0.5rem 0;
}
}
.title {
color: #242424;
}
/* To center the spinner*/
.pos-center {
position: fixed;
top: calc(50% - 40px);
left: calc(50% - 40px);
}
.loader {
border: 10px solid #f3f3f3;
border-top: 10px solid #3498db;
border-radius: 50%;
width: 80px;
height: 80px;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.center {
display: flex;
justify-content: center;
align-items: center;
}
-update Home.tsx
import useFetch from "../hooks/use-fetch";
import { HomePage } from "../models/home-page";
import { GenericResult } from "../models/generic-result";
import Spinner from "../components/Spinner";
export const Home = () => {
const {
response: { data, success, error: errorMsg } = {
data: undefined,
success: false,
error: undefined,
},
error,
loading,
} = useFetch<GenericResult<HomePage>>({ url: "api/content/home-content" });
return (
<>
<Spinner loading={loading} />
{error && <p className="error">error ...{error?.message}</p>}
{!loading && !success && <p className="error">error ...{errorMsg}</p>}
{success && data && (
<div>
title : {data.title}
<div>
{data.imageUrl && (
<img
title={data.title}
src={</span><span class="p">${</span><span class="nx">data</span><span class="p">.</span><span class="nx">imageUrl</span><span class="p">}</span><span class="s2">
}
width="auto"
height="200px"
/>
)}
</div>
</div>
)}
</>
);
};
Final Output
let me know if you're interested in content like,
if would like to continue this series also let me know..
Socials:
LinkedIn: https://www.linkedin.com/in/mohammedzaky
GitHub: https://github.com/mozaky
Top comments (3)
An excellent resource for developers looking to explore the integration of these technologies! This article strikes a balance between being informative and concise, making it accessible to developers at various skill levels. Thank you for sharing! 🙌
thank you 🙏for your feedback
Thank you for this article. I assume that you would replace your content service with the Content Delivery API that Umbraco ships OOTB? This is a great guide for getting started!