{"id":27793,"date":"2022-03-21T19:09:30","date_gmt":"2022-03-21T19:09:30","guid":{"rendered":"http:\/\/Adding-Custom-Branding-to-a-Redwood-App"},"modified":"2022-03-21T19:09:30","modified_gmt":"2022-03-21T19:09:30","slug":"adding-custom-branding-to-a-redwood-app","status":"publish","type":"post","link":"https:\/\/cloudinary.com\/blog\/guest_post\/adding-custom-branding-to-a-redwood-app\/","title":{"rendered":"Adding Custom Branding to a Redwood App"},"content":{"rendered":"<div class=\"wp-block-cloudinary-markdown \"><p>Many organizations want some ability to use a service to handle some of their functionality and customize the interface users are shown. This includes things like the names they see displayed, data they want to be shown, or some images they want to see. Giving them the ability to add their own branding is one way to add value to your own products.<\/p>\n<p>In this Redwood tutorial, we\u2019ll make an app that will change formats depending on what user is associated with the page.<\/p>\n<h2>Create the Redwood app<\/h2>\n<p>First thing we need to do is spin up a new app. In a terminal, run:<\/p>\n<pre class=\"js-syntax-highlighted\"><span><code class=\"hljs shcb-wrap-lines\">yarn create redwood-app branding\n<\/code><\/span><\/pre>\n<p>When this is done, you\u2019ll have a bunch of new files and folders in a <code>branding<\/code> directory. The main folders we\u2019ll be working in are the <code>api<\/code> and <code>web<\/code> folders. We\u2019ll start with some work in the <code>api<\/code> folder first.<\/p>\n<h2>Setting up the model<\/h2>\n<p>Building our app by making the models for the database schema works really well in Redwood. I usually like to start here because it\u2019s one way you can start thinking about your business logic from the beginning.<\/p>\n<p>We\u2019ll be using a Postgres database. Here are <a href=\"https:\/\/www.postgresql.org\/download\/\">the docs to get Postgres installed locally<\/a>. Let\u2019s start by updating the <code>.env<\/code> file with a connection string for your local instance. Uncomment the <code>DATABASE_URL<\/code> line and update the value. It might look something like this.<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-1\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript shcb-wrap-lines\">DATABASE_URL=postgres:<span class=\"hljs-comment\">\/\/admin:password@localhost:5432\/branding<\/span>\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-1\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n<p>Now we can go to <code>api &gt; db<\/code> and open the <code>schema.prisam<\/code> file. This is where we\u2019ll add our models. One thing we need to do is update the <code>provider<\/code> value at the top to <code>postgresql<\/code> instead of <code>sqlite<\/code>. Next, you can delete the existing example model and add these.<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-2\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript shcb-wrap-lines\">model User {\n  id     Int      @id @<span class=\"hljs-keyword\">default<\/span>(autoincrement())\n  name   <span class=\"hljs-built_in\">String<\/span>\n  info   Info&#91;]\n  image  Image&#91;]\n  layout Layout&#91;]\n}\n\nmodel Image {\n  id     Int    @id @<span class=\"hljs-keyword\">default<\/span>(autoincrement())\n  name   <span class=\"hljs-built_in\">String<\/span>\n  url    <span class=\"hljs-built_in\">String<\/span>\n  user   User   @relation(fields: &#91;userId], <span class=\"hljs-attr\">references<\/span>: &#91;id])\n  userId Int\n}\n\nmodel Info {\n  id        Int      @id @<span class=\"hljs-keyword\">default<\/span>(autoincrement())\n  balance   Float\n  lastLogin DateTime\n  endDate   DateTime\n  user      User     @relation(fields: &#91;userId], <span class=\"hljs-attr\">references<\/span>: &#91;id])\n  userId    Int\n}\n\nmodel Layout {\n  id           Int    @id @<span class=\"hljs-keyword\">default<\/span>(autoincrement())\n  name         <span class=\"hljs-built_in\">String<\/span>\n  dataLocation <span class=\"hljs-built_in\">String<\/span>\n  imageUrl     <span class=\"hljs-built_in\">String<\/span>\n  user         User   @relation(fields: &#91;userId], <span class=\"hljs-attr\">references<\/span>: &#91;id])\n  userId       Int\n}\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-2\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n<p>Usually, when you have relationships between tables like we have here, it\u2019s a good idea to seed your database with some initial values. You\u2019ll see this pretty often with apps that have dropdown menus or pre-defined user roles.<\/p>\n<p>We\u2019ll be adding our own seed data in the <code>seed.js<\/code> file. You can open that and delete all of the commented-out code in the <code>main<\/code> function and replace it with this.<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-3\" data-shcb-language-name=\"CSS\" data-shcb-language-slug=\"css\"><span><code class=\"hljs language-css shcb-wrap-lines\"><span class=\"hljs-selector-tag\">await<\/span> <span class=\"hljs-selector-tag\">db<\/span><span class=\"hljs-selector-class\">.user<\/span><span class=\"hljs-selector-class\">.create<\/span>({\n  <span class=\"hljs-attribute\">data<\/span>: { name: <span class=\"hljs-string\">'Nimothy'<\/span> },\n})\n\n<span class=\"hljs-selector-tag\">await<\/span> <span class=\"hljs-selector-tag\">db<\/span><span class=\"hljs-selector-class\">.image<\/span><span class=\"hljs-selector-class\">.create<\/span>({\n  <span class=\"hljs-attribute\">data<\/span>: {\n    name: <span class=\"hljs-string\">'Nimothy Profile'<\/span>,\n    url: <span class=\"hljs-string\">'https:\/\/res.cloudinary.com\/milecia\/image\/upload\/v1606580774\/fish-vegetables.jpg'<\/span>,\n    userId: <span class=\"hljs-number\">1<\/span>,\n  },\n})\n\n<span class=\"hljs-selector-tag\">await<\/span> <span class=\"hljs-selector-tag\">db<\/span><span class=\"hljs-selector-class\">.info<\/span><span class=\"hljs-selector-class\">.create<\/span>({\n  <span class=\"hljs-attribute\">data<\/span>: {\n    balance: <span class=\"hljs-number\">7.89<\/span>,\n    lastLogin: new <span class=\"hljs-built_in\">Date<\/span>(),\n    endDate: new <span class=\"hljs-built_in\">Date<\/span>(),\n    userId: <span class=\"hljs-number\">1<\/span>,\n  },\n})\n\n<span class=\"hljs-selector-tag\">await<\/span> <span class=\"hljs-selector-tag\">db<\/span><span class=\"hljs-selector-class\">.layout<\/span><span class=\"hljs-selector-class\">.create<\/span>({\n  <span class=\"hljs-attribute\">data<\/span>: {\n    name: <span class=\"hljs-string\">'MidLeft'<\/span>,\n    dataLocation: <span class=\"hljs-string\">'mid-left'<\/span>,\n    imageUrl:\n      <span class=\"hljs-string\">'https:\/\/res.cloudinary.com\/milecia\/image\/upload\/v1606580774\/fish-vegetables.jpg'<\/span>,\n    userId: <span class=\"hljs-number\">1<\/span>,\n  },\n})\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-3\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">CSS<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">css<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n<h3>Run migration<\/h3>\n<p>With our models and seed data in place, we can migrate the database with this command:<\/p>\n<pre class=\"js-syntax-highlighted\"><span><code class=\"hljs shcb-wrap-lines\">yarn rw prisma migrate dev\n<\/code><\/span><\/pre>\n<p>That will add the tables and columns with the defined relationships to your Postgres instance. To seed the database, we\u2019ll need to run:<\/p>\n<pre class=\"js-syntax-highlighted\"><span><code class=\"hljs shcb-wrap-lines\">yarn rw prisma db seed\n<\/code><\/span><\/pre>\n<p>This will add the placeholder data we created in <code>seed.js<\/code> so that the relationships between tables and columns are met and don\u2019t cause errors with our app.<\/p>\n<p>Since we\u2019ve run the migration and seeding, we can move on to the back-end and front-end.<\/p>\n<h2>Making the back-end and front-end<\/h2>\n<p>We\u2019re going to make the functionality to add new layouts and new users to the app for now so that we can show how things update for the user. We\u2019ll also be adding a special page to show how these updates would actually affect users.<\/p>\n<p>For the sake of this project, we\u2019re going to assume that adding new users and layouts is admin functionality that users of the app won\u2019t be able to see. Later on, we\u2019ll add the user view that applies the custom branding.<\/p>\n<p>Adding the ability to create and update users and layouts only takes a couple of commands in Redwood. Let\u2019s start by making the users functionality with this:<\/p>\n<pre class=\"js-syntax-highlighted\"><span><code class=\"hljs shcb-wrap-lines\">yarn rw g scaffold user\n<\/code><\/span><\/pre>\n<p>This will generate the back-end GraphQL types and resolvers as well as adding new components to the front-end. We\u2019ll run this command one more time for the layouts functionality:<\/p>\n<pre class=\"js-syntax-highlighted\"><span><code class=\"hljs shcb-wrap-lines\">yarn rw g scaffold layout\n<\/code><\/span><\/pre>\n<p>You can take a look at the code Redwood generated to make all of this work on the front-end by going through the <code>web &gt; src<\/code> directory. There are new files under <code>components<\/code>, <code>layouts<\/code>, and <code>pages<\/code>, plus <code>Routes.js<\/code> has been updated. All of the new files you see were created by that <code>scaffold<\/code> command for those two models.<\/p>\n<p>The back-end code that supports new user and layout creation and the edit and delete functionality can be found in the <code>api &gt; src<\/code> directory. You\u2019ll see new files under <code>graphql<\/code> and <code>services<\/code> that hold the GraphQL types and resolvers that make all of the CRUD work and persists the data.<\/p>\n<p>Now we have the CRUD for the front-end and back-end for these two models. You can run the <code>scaffold<\/code> command to create the CRUD for the other models, but we don\u2019t actually need it. What we <em>do<\/em> need are the types for those models. We can generate those with a couple of Redwood commands:<\/p>\n<pre class=\"js-syntax-highlighted\"><span><code class=\"hljs shcb-wrap-lines\">yarn rw g sdl info\nyarn rw g sdl image\n<\/code><\/span><\/pre>\n<p>The <code>sdl<\/code> generator makes all of the GraphQL types and a resolver for the specified model. If you check out <code>api &gt; src &gt; graphql<\/code>, you\u2019ll see the new types that were generated for info and images. Then if you look in <code>api &gt; src &gt; service<\/code>, you\u2019ll see that a resolver has been made to handle a query for us for both info and images.<\/p>\n<p>The reason we\u2019re adding these types is so the user types reference these so we need them to be available, even if we aren\u2019t adding the front-end piece.<\/p>\n<h3>Running the updated app<\/h3>\n<p>If you run your app with <code>yarn rw dev<\/code> and navigate to <code>localhost:8910\/users<\/code>, you\u2019ll see a table and buttons for different ways to interact with the data. You should see something similar to this:<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/res.cloudinary.com\/jesse-thisdot\/image\/upload\/c_limit,w_2000\/f_auto\/q_auto\/v1627041441\/e-603fc55d218a650069f5228b\/kkzg6tzgiga2jdbpx9mm.png\" alt=\"users table\" loading=\"lazy\" class=\"c-transformed-asset\"  width=\"2000\" height=\"957\"\/><\/p>\n<p>Go ahead and add a new user by clicking the \u201cNew User\u201d button. This will open the form like this:<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/res.cloudinary.com\/jesse-thisdot\/image\/upload\/c_limit,w_2000\/f_auto\/q_auto\/v1627041426\/e-603fc55d218a650069f5228b\/dzfe2pj48gskppaxa2nl.png\" alt=\"new user form\" loading=\"lazy\" class=\"c-transformed-asset\"  width=\"2000\" height=\"825\"\/><\/p>\n<p>Now you can add a new layout for this new user by going to <code>localhost:8910\/layouts<\/code> and clicking the \u201cNew Layout\u201d button. It\u2019ll bring up this form:<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/res.cloudinary.com\/jesse-thisdot\/image\/upload\/c_limit,w_2000\/f_auto\/q_auto\/v1627041413\/e-603fc55d218a650069f5228b\/vcbn7kdin9nm45t1ekg0.png\" alt=\"new layout form\" loading=\"lazy\" class=\"c-transformed-asset\"  width=\"2000\" height=\"1017\"\/><\/p>\n<h2>Show the user their custom view<\/h2>\n<p>Now that we\u2019ve got the core functionality together to create users and associate layouts with them, we can create the custom view that they will see. To do that, we\u2019ll use Redwood to generate a page that will load a specific user\u2019s layout. Make a new page with this command:<\/p>\n<pre class=\"js-syntax-highlighted\"><span><code class=\"hljs shcb-wrap-lines\">yarn rw g page option\n<\/code><\/span><\/pre>\n<p>This will add a new page to the <code>web &gt; src &gt; pages<\/code> directory and it will update the <code>Routes.js<\/code> file with a new <code>\/option<\/code> route. If you navigate to <code>localhost:8910\/option<\/code>, you\u2019ll see this:<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/res.cloudinary.com\/jesse-thisdot\/image\/upload\/c_limit,w_2000\/f_auto\/q_auto\/v1627041396\/e-603fc55d218a650069f5228b\/vwrmwymhxmgspzhgc2sx.png\" alt=\"option page\" loading=\"lazy\" class=\"c-transformed-asset\"  width=\"2000\" height=\"760\"\/><\/p>\n<p>We need to update this page to show the user\u2019s layout by pulling some data from the back-end.<\/p>\n<h3>Querying for the user layout<\/h3>\n<p>In the <code>web &gt; src &gt; pages &gt; OptionPage<\/code> directory, open the <code>OptionPage.js<\/code> file and add the following import to get your GraphQL query ready.<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-4\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript shcb-wrap-lines\"><span class=\"hljs-keyword\">import<\/span> { useQuery } <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">'@redwoodjs\/web'<\/span>\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-4\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n<p>Then at the bottom of the file, right above the export statement, add this code for the query.<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-5\" data-shcb-language-name=\"PHP\" data-shcb-language-slug=\"php\"><span><code class=\"hljs language-php shcb-wrap-lines\"><span class=\"hljs-keyword\">const<\/span> LAYOUT = gql`\n  query layout($id: Int!) {\n    layout(id: $id) {\n      id\n      name\n      dataLocation\n      imageUrl\n      userId\n    }\n  }\n`\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-5\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">PHP<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">php<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n<p>This will give us a specific layout based on the id we pass to the query. We\u2019ll be manually setting this id to mimic what we might get from a prop from a different component. We\u2019ll add the variable for the id in our query hook. This will be added inside of the <code>OptionPage<\/code> component:<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-6\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript shcb-wrap-lines\"><span class=\"hljs-keyword\">const<\/span> { loading, data } = useQuery(LAYOUT, {\n  <span class=\"hljs-attr\">variables<\/span>: { <span class=\"hljs-attr\">id<\/span>: <span class=\"hljs-number\">1<\/span> },\n})\n\n<span class=\"hljs-keyword\">if<\/span> (loading) {\n  <span class=\"hljs-keyword\">return<\/span> <span class=\"xml\"><span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">div<\/span>&gt;<\/span>Loading...<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">div<\/span>&gt;<\/span><\/span>\n}\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-6\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n<p>We\u2019re using the <code>useQuery<\/code> hook to execute the query we made earlier and we\u2019re manually setting the id of the layout we want to use. Then we\u2019re checking the load status for the data and rendering an indicator that the page is loading the content so that the user doesn\u2019t see an error before the fetch finishes.<\/p>\n<p>The last thing we\u2019ll do is update the elements to show in the layout format we currently have loaded.<\/p>\n<h3>Updating the page<\/h3>\n<p>To show the right layout, we\u2019re going to install the <code>styled-components<\/code> package. That way we\u2019ll be able to pass props to update the layout based on the user viewing the page. So in the <code>web<\/code> directory in your terminal, run:<\/p>\n<pre class=\"js-syntax-highlighted\"><span><code class=\"hljs shcb-wrap-lines\">yarn add styled-components\n<\/code><\/span><\/pre>\n<p>Now we\u2019re going to import that package in the <code>OptionPage.js<\/code> file.<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-7\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript shcb-wrap-lines\"><span class=\"hljs-keyword\">import<\/span> styled <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">'styled-components'<\/span>\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-7\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n<p>Then we need to add a new styled component to handle the location of the image based on that user layout. We\u2019ll add this right above the <code>OptionPage<\/code> component.<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-8\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript shcb-wrap-lines\"><span class=\"hljs-keyword\">const<\/span> Img = styled.img<span class=\"hljs-string\">`\n  display: block;\n  position: absolute;\n  top: <span class=\"hljs-subst\">${(props) =&gt; (props.dataLocation === <span class=\"hljs-string\">'mid-left'<\/span> ? <span class=\"hljs-string\">'35%'<\/span> : <span class=\"hljs-string\">'unset'<\/span>)}<\/span>;\n  right: <span class=\"hljs-subst\">${(props) =&gt; (props.dataLocation === <span class=\"hljs-string\">'mid-left'<\/span> ? <span class=\"hljs-string\">'unset'<\/span> : <span class=\"hljs-string\">'0'<\/span>)}<\/span>;\n  width: 360px;\n`<\/span>\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-8\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n<p>We\u2019re doing a simple update of the image location with an absolute position setup. This will let the image move independently of the other elements on the page so that the user sees it in the place they\u2019ve selected. We\u2019re passing in the <code>dataLocation<\/code> value as a prop.<\/p>\n<h3>Cleaning things up<\/h3>\n<p>Just a few finishing touches and we\u2019ll have this layout working. First, we need to add the <code>Img<\/code> to <code>OptionPage<\/code>. We\u2019ll delete the existing <code>Link<\/code> from the return statement and add this image instead.<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-9\" data-shcb-language-name=\"HTML, XML\" data-shcb-language-slug=\"xml\"><span><code class=\"hljs language-xml shcb-wrap-lines\"><span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">Img<\/span> <span class=\"hljs-attr\">src<\/span>=<span class=\"hljs-string\">{data.layout.imageUrl}<\/span> <span class=\"hljs-attr\">dataLocation<\/span>=<span class=\"hljs-string\">{data.layout.dataLocation}<\/span> \/&gt;<\/span>\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-9\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">HTML, XML<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">xml<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n<p>We\u2019ll also add a little line to show the name of the current layout. This will go below the description of the file location.<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-10\" data-shcb-language-name=\"HTML, XML\" data-shcb-language-slug=\"xml\"><span><code class=\"hljs language-xml shcb-wrap-lines\"><span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">p<\/span>&gt;<\/span>{data.layout.name}<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">p<\/span>&gt;<\/span>\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-10\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">HTML, XML<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">xml<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n<p>That\u2019s it! We\u2019ve finished up this app. Now if you run the app with <code>yarn rw dev<\/code>, you should see something similar to this.<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/res.cloudinary.com\/jesse-thisdot\/image\/upload\/c_limit,w_2000\/f_auto\/q_auto\/v1627041358\/e-603fc55d218a650069f5228b\/zcw2tdqrwyur6bpzryv4.png\" alt=\"user one layout\" loading=\"lazy\" class=\"c-transformed-asset\"  width=\"2000\" height=\"1135\"\/><\/p>\n<p>If you update the <code>id<\/code> in the query variable to <code>2<\/code> and reload the browser, you\u2019ll see something like this.<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/res.cloudinary.com\/jesse-thisdot\/image\/upload\/c_limit,w_2000\/f_auto\/q_auto\/v1627041339\/e-603fc55d218a650069f5228b\/nhs5ar1x8kyeppxzparf.png\" alt=\"user two layout\" loading=\"lazy\" class=\"c-transformed-asset\"  width=\"2000\" height=\"1133\"\/><\/p>\n<h2>Finished code<\/h2>\n<p>If you want to check out the complete code, you can check it out in the <a href=\"https:\/\/github.com\/flippedcoder\/blog-examples\/tree\/main\/custom-app-branding\"><code>custom-app-branding<\/code> folder of this repo<\/a>. You can also check out the front-end in <a href=\"https:\/\/codesandbox.io\/s\/new-sound-pp7z3\">this Code Sandbox<\/a>.<\/p>\n<\/div>\n  \n  <div class=\"wp-block-cloudinary-code-sandbox \">\n    <iframe\n      src=\"https:\/\/codesandbox.io\/embed\/new-sound-pp7z3?theme=dark&amp;codemirror=1&amp;highlights=&amp;editorsize=50&amp;fontsize=14&amp;expanddevtools=0&amp;hidedevtools=0&amp;eslint=0&amp;forcerefresh=0&amp;hidenavigation=0&amp;initialpath=%2F&amp;module=&amp;moduleview=0&amp;previewwindow=&amp;view=&amp;runonclick=1\"\n      height=\"500\"\n      style=\"width: 100%;\"\n      title=\"new-sound-pp7z3\"\n      loading=\"lazy\"\n      allow=\"accelerometer; ambient-light-sensor; camera; encrypted-media; geolocation; gyroscope; hid; microphone; midi; payment; usb; vr; xr-spatial-tracking\"\n      sandbox=\"allow-forms allow-modals allow-popups allow-presentation allow-same-origin allow-scripts\"\n    ><\/iframe>\n  <\/div>\n\n  <div class=\"wp-block-cloudinary-markdown \"><h2>Conclusion<\/h2>\n<p>If you\u2019re interested in a deeper dive on how Redwood handles scaffolding or the general way it creates files for you, make sure to go through <a href=\"https:\/\/redwoodjs.com\/docs\/introduction\">their docs<\/a>.<\/p>\n<\/div>","protected":false},"excerpt":{"rendered":"","protected":false},"author":41,"featured_media":27794,"comment_status":"","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"_cloudinary_featured_overwrite":false,"footnotes":""},"categories":[1],"tags":[134,370,177,246,371],"class_list":["post-27793","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-uncategorized","tag-guest-post","tag-image","tag-javascript","tag-react","tag-under-review"],"acf":[],"yoast_head":"<!-- This site is optimized with the Yoast SEO Premium plugin v25.6 (Yoast SEO v26.9) - https:\/\/yoast.com\/product\/yoast-seo-premium-wordpress\/ -->\n<title>Adding Custom Branding to a Redwood App<\/title>\n<meta name=\"description\" content=\"You might end up working on a project with multiple users who want different layouts for their dashboards. Users typically like to have some level of customization over the applications they use and we&#039;re going to cover how to do that with Redwood.\" \/>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/cloudinary.com\/blog\/guest_post\/adding-custom-branding-to-a-redwood-app\/\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"Adding Custom Branding to a Redwood App\" \/>\n<meta property=\"og:description\" content=\"You might end up working on a project with multiple users who want different layouts for their dashboards. Users typically like to have some level of customization over the applications they use and we&#039;re going to cover how to do that with Redwood.\" \/>\n<meta property=\"og:url\" content=\"https:\/\/cloudinary.com\/blog\/guest_post\/adding-custom-branding-to-a-redwood-app\/\" \/>\n<meta property=\"og:site_name\" content=\"Cloudinary Blog\" \/>\n<meta property=\"article:published_time\" content=\"2022-03-21T19:09:30+00:00\" \/>\n<meta property=\"og:image\" content=\"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1681926258\/Web_Assets\/blog\/194c0c513d6a0dd48c62c7a349777056fdec389b-5460x3642-1_27794aacac\/194c0c513d6a0dd48c62c7a349777056fdec389b-5460x3642-1_27794aacac.jpg?_i=AA\" \/>\n\t<meta property=\"og:image:width\" content=\"5460\" \/>\n\t<meta property=\"og:image:height\" content=\"3642\" \/>\n\t<meta property=\"og:image:type\" content=\"image\/jpeg\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\/\/schema.org\",\"@graph\":[{\"@type\":\"NewsArticle\",\"@id\":\"https:\/\/cloudinary.com\/blog\/guest_post\/adding-custom-branding-to-a-redwood-app\/#article\",\"isPartOf\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/guest_post\/adding-custom-branding-to-a-redwood-app\/\"},\"author\":{\"name\":\"\",\"@id\":\"\"},\"headline\":\"Adding Custom Branding to a Redwood App\",\"datePublished\":\"2022-03-21T19:09:30+00:00\",\"mainEntityOfPage\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/guest_post\/adding-custom-branding-to-a-redwood-app\/\"},\"wordCount\":7,\"publisher\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/#organization\"},\"image\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/guest_post\/adding-custom-branding-to-a-redwood-app\/#primaryimage\"},\"thumbnailUrl\":\"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1681926258\/Web_Assets\/blog\/194c0c513d6a0dd48c62c7a349777056fdec389b-5460x3642-1_27794aacac\/194c0c513d6a0dd48c62c7a349777056fdec389b-5460x3642-1_27794aacac.jpg?_i=AA\",\"keywords\":[\"Guest Post\",\"Image\",\"Javascript\",\"React\",\"Under Review\"],\"inLanguage\":\"en-US\",\"copyrightYear\":\"2022\",\"copyrightHolder\":{\"@id\":\"https:\/\/cloudinary.com\/#organization\"}},{\"@type\":\"WebPage\",\"@id\":\"https:\/\/cloudinary.com\/blog\/guest_post\/adding-custom-branding-to-a-redwood-app\/\",\"url\":\"https:\/\/cloudinary.com\/blog\/guest_post\/adding-custom-branding-to-a-redwood-app\/\",\"name\":\"Adding Custom Branding to a Redwood App\",\"isPartOf\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/#website\"},\"primaryImageOfPage\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/guest_post\/adding-custom-branding-to-a-redwood-app\/#primaryimage\"},\"image\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/guest_post\/adding-custom-branding-to-a-redwood-app\/#primaryimage\"},\"thumbnailUrl\":\"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1681926258\/Web_Assets\/blog\/194c0c513d6a0dd48c62c7a349777056fdec389b-5460x3642-1_27794aacac\/194c0c513d6a0dd48c62c7a349777056fdec389b-5460x3642-1_27794aacac.jpg?_i=AA\",\"datePublished\":\"2022-03-21T19:09:30+00:00\",\"description\":\"You might end up working on a project with multiple users who want different layouts for their dashboards. Users typically like to have some level of customization over the applications they use and we're going to cover how to do that with Redwood.\",\"breadcrumb\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/guest_post\/adding-custom-branding-to-a-redwood-app\/#breadcrumb\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\/\/cloudinary.com\/blog\/guest_post\/adding-custom-branding-to-a-redwood-app\/\"]}]},{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\/\/cloudinary.com\/blog\/guest_post\/adding-custom-branding-to-a-redwood-app\/#primaryimage\",\"url\":\"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1681926258\/Web_Assets\/blog\/194c0c513d6a0dd48c62c7a349777056fdec389b-5460x3642-1_27794aacac\/194c0c513d6a0dd48c62c7a349777056fdec389b-5460x3642-1_27794aacac.jpg?_i=AA\",\"contentUrl\":\"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1681926258\/Web_Assets\/blog\/194c0c513d6a0dd48c62c7a349777056fdec389b-5460x3642-1_27794aacac\/194c0c513d6a0dd48c62c7a349777056fdec389b-5460x3642-1_27794aacac.jpg?_i=AA\",\"width\":5460,\"height\":3642},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\/\/cloudinary.com\/blog\/guest_post\/adding-custom-branding-to-a-redwood-app\/#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\/\/cloudinary.com\/blog\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"Adding Custom Branding to a Redwood App\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\/\/cloudinary.com\/blog\/#website\",\"url\":\"https:\/\/cloudinary.com\/blog\/\",\"name\":\"Cloudinary Blog\",\"description\":\"\",\"publisher\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/#organization\"},\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\/\/cloudinary.com\/blog\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"en-US\"},{\"@type\":\"Organization\",\"@id\":\"https:\/\/cloudinary.com\/blog\/#organization\",\"name\":\"Cloudinary Blog\",\"url\":\"https:\/\/cloudinary.com\/blog\/\",\"logo\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\/\/cloudinary.com\/blog\/#\/schema\/logo\/image\/\",\"url\":\"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1649718331\/Web_Assets\/blog\/cloudinary_logo_for_white_bg_1937437aa7_19374666c7_193742f877\/cloudinary_logo_for_white_bg_1937437aa7_19374666c7_193742f877.png?_i=AA\",\"contentUrl\":\"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1649718331\/Web_Assets\/blog\/cloudinary_logo_for_white_bg_1937437aa7_19374666c7_193742f877\/cloudinary_logo_for_white_bg_1937437aa7_19374666c7_193742f877.png?_i=AA\",\"width\":312,\"height\":60,\"caption\":\"Cloudinary Blog\"},\"image\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/#\/schema\/logo\/image\/\"}},{\"@type\":\"Person\",\"@id\":\"\"}]}<\/script>\n<!-- \/ Yoast SEO Premium plugin. -->","yoast_head_json":{"title":"Adding Custom Branding to a Redwood App","description":"You might end up working on a project with multiple users who want different layouts for their dashboards. Users typically like to have some level of customization over the applications they use and we're going to cover how to do that with Redwood.","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/cloudinary.com\/blog\/guest_post\/adding-custom-branding-to-a-redwood-app\/","og_locale":"en_US","og_type":"article","og_title":"Adding Custom Branding to a Redwood App","og_description":"You might end up working on a project with multiple users who want different layouts for their dashboards. Users typically like to have some level of customization over the applications they use and we're going to cover how to do that with Redwood.","og_url":"https:\/\/cloudinary.com\/blog\/guest_post\/adding-custom-branding-to-a-redwood-app\/","og_site_name":"Cloudinary Blog","article_published_time":"2022-03-21T19:09:30+00:00","og_image":[{"width":5460,"height":3642,"url":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1681926258\/Web_Assets\/blog\/194c0c513d6a0dd48c62c7a349777056fdec389b-5460x3642-1_27794aacac\/194c0c513d6a0dd48c62c7a349777056fdec389b-5460x3642-1_27794aacac.jpg?_i=AA","type":"image\/jpeg"}],"twitter_card":"summary_large_image","schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"NewsArticle","@id":"https:\/\/cloudinary.com\/blog\/guest_post\/adding-custom-branding-to-a-redwood-app\/#article","isPartOf":{"@id":"https:\/\/cloudinary.com\/blog\/guest_post\/adding-custom-branding-to-a-redwood-app\/"},"author":{"name":"","@id":""},"headline":"Adding Custom Branding to a Redwood App","datePublished":"2022-03-21T19:09:30+00:00","mainEntityOfPage":{"@id":"https:\/\/cloudinary.com\/blog\/guest_post\/adding-custom-branding-to-a-redwood-app\/"},"wordCount":7,"publisher":{"@id":"https:\/\/cloudinary.com\/blog\/#organization"},"image":{"@id":"https:\/\/cloudinary.com\/blog\/guest_post\/adding-custom-branding-to-a-redwood-app\/#primaryimage"},"thumbnailUrl":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1681926258\/Web_Assets\/blog\/194c0c513d6a0dd48c62c7a349777056fdec389b-5460x3642-1_27794aacac\/194c0c513d6a0dd48c62c7a349777056fdec389b-5460x3642-1_27794aacac.jpg?_i=AA","keywords":["Guest Post","Image","Javascript","React","Under Review"],"inLanguage":"en-US","copyrightYear":"2022","copyrightHolder":{"@id":"https:\/\/cloudinary.com\/#organization"}},{"@type":"WebPage","@id":"https:\/\/cloudinary.com\/blog\/guest_post\/adding-custom-branding-to-a-redwood-app\/","url":"https:\/\/cloudinary.com\/blog\/guest_post\/adding-custom-branding-to-a-redwood-app\/","name":"Adding Custom Branding to a Redwood App","isPartOf":{"@id":"https:\/\/cloudinary.com\/blog\/#website"},"primaryImageOfPage":{"@id":"https:\/\/cloudinary.com\/blog\/guest_post\/adding-custom-branding-to-a-redwood-app\/#primaryimage"},"image":{"@id":"https:\/\/cloudinary.com\/blog\/guest_post\/adding-custom-branding-to-a-redwood-app\/#primaryimage"},"thumbnailUrl":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1681926258\/Web_Assets\/blog\/194c0c513d6a0dd48c62c7a349777056fdec389b-5460x3642-1_27794aacac\/194c0c513d6a0dd48c62c7a349777056fdec389b-5460x3642-1_27794aacac.jpg?_i=AA","datePublished":"2022-03-21T19:09:30+00:00","description":"You might end up working on a project with multiple users who want different layouts for their dashboards. Users typically like to have some level of customization over the applications they use and we're going to cover how to do that with Redwood.","breadcrumb":{"@id":"https:\/\/cloudinary.com\/blog\/guest_post\/adding-custom-branding-to-a-redwood-app\/#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/cloudinary.com\/blog\/guest_post\/adding-custom-branding-to-a-redwood-app\/"]}]},{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/cloudinary.com\/blog\/guest_post\/adding-custom-branding-to-a-redwood-app\/#primaryimage","url":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1681926258\/Web_Assets\/blog\/194c0c513d6a0dd48c62c7a349777056fdec389b-5460x3642-1_27794aacac\/194c0c513d6a0dd48c62c7a349777056fdec389b-5460x3642-1_27794aacac.jpg?_i=AA","contentUrl":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1681926258\/Web_Assets\/blog\/194c0c513d6a0dd48c62c7a349777056fdec389b-5460x3642-1_27794aacac\/194c0c513d6a0dd48c62c7a349777056fdec389b-5460x3642-1_27794aacac.jpg?_i=AA","width":5460,"height":3642},{"@type":"BreadcrumbList","@id":"https:\/\/cloudinary.com\/blog\/guest_post\/adding-custom-branding-to-a-redwood-app\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/cloudinary.com\/blog\/"},{"@type":"ListItem","position":2,"name":"Adding Custom Branding to a Redwood App"}]},{"@type":"WebSite","@id":"https:\/\/cloudinary.com\/blog\/#website","url":"https:\/\/cloudinary.com\/blog\/","name":"Cloudinary Blog","description":"","publisher":{"@id":"https:\/\/cloudinary.com\/blog\/#organization"},"potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/cloudinary.com\/blog\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"en-US"},{"@type":"Organization","@id":"https:\/\/cloudinary.com\/blog\/#organization","name":"Cloudinary Blog","url":"https:\/\/cloudinary.com\/blog\/","logo":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/cloudinary.com\/blog\/#\/schema\/logo\/image\/","url":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1649718331\/Web_Assets\/blog\/cloudinary_logo_for_white_bg_1937437aa7_19374666c7_193742f877\/cloudinary_logo_for_white_bg_1937437aa7_19374666c7_193742f877.png?_i=AA","contentUrl":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1649718331\/Web_Assets\/blog\/cloudinary_logo_for_white_bg_1937437aa7_19374666c7_193742f877\/cloudinary_logo_for_white_bg_1937437aa7_19374666c7_193742f877.png?_i=AA","width":312,"height":60,"caption":"Cloudinary Blog"},"image":{"@id":"https:\/\/cloudinary.com\/blog\/#\/schema\/logo\/image\/"}},{"@type":"Person","@id":""}]}},"jetpack_featured_media_url":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1681926258\/Web_Assets\/blog\/194c0c513d6a0dd48c62c7a349777056fdec389b-5460x3642-1_27794aacac\/194c0c513d6a0dd48c62c7a349777056fdec389b-5460x3642-1_27794aacac.jpg?_i=AA","_links":{"self":[{"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/posts\/27793","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/users\/41"}],"replies":[{"embeddable":true,"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/comments?post=27793"}],"version-history":[{"count":0,"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/posts\/27793\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/media\/27794"}],"wp:attachment":[{"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/media?parent=27793"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/categories?post=27793"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/tags?post=27793"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}