{"id":28197,"date":"2022-03-23T22:59:01","date_gmt":"2022-03-23T22:59:01","guid":{"rendered":"http:\/\/Making-a-Media-Library-with-Redwood"},"modified":"2022-03-23T22:59:01","modified_gmt":"2022-03-23T22:59:01","slug":"making-a-media-library-with-redwood","status":"publish","type":"post","link":"https:\/\/cloudinary.com\/blog\/guest_post\/making-a-media-library-with-redwood\/","title":{"rendered":"Making a Media Library"},"content":{"rendered":"<div class=\"wp-block-cloudinary-markdown \"><p>You might have a blog or a site that specializes in giving users video and image content. You can make yourself a media library to manage all of your content and keep it relevant to your users. That way you can see the exact data you need and you can arrange your images and videos in ways that don\u2019t affect users.<\/p>\n<p>In this tutorial, we\u2019ll make a small media library to handle videos and images for us. You\u2019ll be able to upload the videos and images to Cloudinary and then show them in your own library. We\u2019ll be storing references to that content in our own Postgres database that we could use to reference images in other places.<\/p>\n<h2>Setting up a Cloudinary account<\/h2>\n<p>The first thing we\u2019ll do is set up a Cloudinary account. You can <a href=\"https:\/\/cloudinary.com\/signup\">sign up for a free account here<\/a>.<\/p>\n<p>Then you\u2019ll have a single place to upload all of your users\u2019 media content. That way you don\u2019t have to worry about storing all of your content and you can just fetch and upload your videos.<\/p>\n<h2>Setting up the Redwood app<\/h2>\n<p>Now you can go to a terminal and we\u2019ll make our new Redwood app with the following command.<\/p>\n<p><code>yarn create redwood-app media-library<\/code><\/p>\n<p>When this finishes, you\u2019ll have a full-stack app that just needs a little updating. In the <code>api<\/code> folder you\u2019ll find all of the code to handle your database and GraphQL back-end. In the <code>web<\/code> folder you\u2019ll find the code for the React front-end. We\u2019ll start with the database schema because Redwood uses this to handle a lot of work for us.<\/p>\n<h3>Making the models<\/h3>\n<p>Open <code>schema.prisma<\/code> in the <code>api &gt; db<\/code> directory. Make sure to update your <code>provider<\/code> to <code>postgresql<\/code> instead of <code>sqlite<\/code> since we\u2019ll using a Postgres database. Take a moment to update the <code>.env<\/code> file in the root of the project. You\u2019ll need to uncomment the <code>DATABASE_URL<\/code> value and update it to your Postgres connection string. It might look like this.<\/p>\n<p><code>DATABASE_URL=postgres:\/\/postgres:admin@localhost:5432\/media_library<\/code><\/p>\n<p>If you don\u2019t have Postgres installed locally, <a href=\"https:\/\/www.postgresql.org\/download\/\">you can download it here<\/a> and get your connection string from pgAdmin once the installation is finished.<\/p>\n<p>You can close <code>.env<\/code> now and go back to <code>schema.prisma<\/code> because we need to add our models. You can delete the example model in the file and add the following ones.<\/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\">model Video {\n  id       Int    @id @<span class=\"hljs-keyword\">default<\/span>(autoincrement())\n  name     <span class=\"hljs-built_in\">String<\/span>\n  duration Float\n  url      <span class=\"hljs-built_in\">String<\/span>\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}\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>We have the database schema ready so we can run a quick migration to set up the database.<\/p>\n<p><code>yarn rw prisma migrate dev<\/code><\/p>\n<p>This will create the tables with the columns and constraints we defined in the models. Just to have some data to look at, we\u2019ll seed our database.<\/p>\n<h3>Seeding the database<\/h3>\n<p>Inside of <code>api &gt; db<\/code>, open <code>seed.js<\/code> and delete all of the commented out code in the <code>main<\/code> function. We\u2019ll be adding our own seed calls with the following code.<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-2\" 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\">.video<\/span><span class=\"hljs-selector-class\">.create<\/span>({\n  <span class=\"hljs-attribute\">data<\/span>: { name: <span class=\"hljs-string\">'Beth_vid'<\/span>, duration: <span class=\"hljs-number\">765.34<\/span>, url: <span class=\"hljs-string\">'example.com'<\/span> },\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>: { name: <span class=\"hljs-string\">'Beth_img'<\/span>, url: <span class=\"hljs-string\">'example.com\/beth.jpg'<\/span> },\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\">CSS<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">css<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n<p>Now you can run this command to seed your database.<\/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>With your fresh data applied, we can move on to the best part of Redwood.<\/p>\n<h2>Using Redwood to generate the front-end and back-end<\/h2>\n<p>Now we\u2019ll use my favorite Redwood command to generate the CRUD to work with videos for the front-end and back-end of this project.<\/p>\n<p><code>yarn rw g scaffold video<\/code><\/p>\n<p>This one command generates a React front-end to handle everything we need to add video records to the database. If you look in <code>api &gt; db &gt; src &gt; graphql<\/code>, you\u2019ll see a new sdl file that contains all of the types for our queries and mutations for handling videos. In <code>api &gt; db &gt; src &gt; services<\/code>, you\u2019ll see a new <code>videos<\/code> directory. This folder has all of the resolvers to handle the database updates. It also has a test that you can add on to.<\/p>\n<p>On the front-end of the app, take a look at <code>web &gt; src &gt; pages<\/code>, you\u2019ll see a lot of new directories for videos. These contain pages that show different aspects of the CRUD functionality.<\/p>\n<p>Take a look in <code>web &gt; src &gt; components<\/code> and you\u2019ll see even more directories for videos. These directories contain the files that handle the data on the front-end and the form we could use to handle adding videos to the database.<\/p>\n<p>We\u2019re going to run the <code>scaffold<\/code> command one more time to generate all of the files for images.<\/p>\n<pre class=\"js-syntax-highlighted\"><span><code class=\"hljs shcb-wrap-lines\">yarn rw g scaffold image\n<\/code><\/span><\/pre>\n<p>This is everything we need to start working with the Cloudinary upload widget.<\/p>\n<h2>Adding the Cloudinary upload widget<\/h2>\n<p>We\u2019re not actually going to use the form to add videos to the database. We\u2019re going to do that automatically after we upload the videos to Cloudinary. To start with, we\u2019ll add the Cloudinary widget to the <code>NewVideo<\/code> component. We can use a package to add this widget so we\u2019ll install that in the <code>web<\/code> directory first.<\/p>\n<pre class=\"js-syntax-highlighted\"><span><code class=\"hljs shcb-wrap-lines\">yarn add react-cloudinary-upload-widget\n<\/code><\/span><\/pre>\n<p>Let\u2019s go to <code>web &gt; src &gt; components &gt; NewVideo<\/code> and edit the file there. This is where the edit form for the video gets pulled in. We\u2019ll add the import for the upload widget components we need along with the other imports.<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-3\" 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> { WidgetLoader, Widget } <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">'react-cloudinary-upload-widget'<\/span>\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-3\"><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\u2019ll add the uploader widget components. In the code right above the <code>VideoForm<\/code>, add this code.<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-4\" data-shcb-language-name=\"PHP\" data-shcb-language-slug=\"php\"><span><code class=\"hljs language-php shcb-wrap-lines\">&lt;WidgetLoader \/&gt;\n&lt;Widget\n  sources={&#91;<span class=\"hljs-string\">'local'<\/span>, <span class=\"hljs-string\">'camera'<\/span>]}\n  cloudName={process.env.CLOUD_NAME}\n  uploadPreset={process.env.UPLOAD_PRESET}\n  buttonText={<span class=\"hljs-string\">'Open'<\/span>}\n  style={{\n    color: <span class=\"hljs-string\">'white'<\/span>,\n    border: <span class=\"hljs-string\">'none'<\/span>,\n    width: <span class=\"hljs-string\">'120px'<\/span>,\n    backgroundColor: <span class=\"hljs-string\">'green'<\/span>,\n    borderRadius: <span class=\"hljs-string\">'4px'<\/span>,\n    height: <span class=\"hljs-string\">'25px'<\/span>,\n  }}\n  folder={<span class=\"hljs-string\">'test0'<\/span>}\n  onSuccess={successCallBack}\n\/&gt;\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-4\"><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>While you can check out what each of these props does for the widget in the <a href=\"https:\/\/github.com\/bubbaspaarx\/react-cloudinary-upload-widget#readme\">README in the repo<\/a>, there are a few we need to highlight. The <code>CLOUD_NAME<\/code> and <code>UPLOAD_PRESET<\/code> need to be defined in your <code>.env<\/code>.<\/p>\n<h3>Making an upload preset in the Cloudinary console<\/h3>\n<p>You\u2019ll need the values for these two fields and you\u2019ll get those from your Cloudinary console. The cloud name is on the dashboard as soon as you log in.<\/p>\n<p><img decoding=\"async\" src=\"cloud_name.png\" alt=\"cloud name\" loading=\"lazy\" class=\"c-transformed-asset\" \/><\/p>\n<p>You\u2019ll need to go to the <a href=\"https:\/\/cloudinary.com\/console\/settings\/upload\">settings page for uploads<\/a> and create a new unsigned upload preset. Your upload setting page will look 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\/v1625837578\/e-603fc55d218a650069f5228b\/vyiyxaahjdaxbqpt1zio.png\" alt=\"upload settings\" loading=\"lazy\" class=\"c-transformed-asset\"  width=\"2000\" height=\"889\"\/><\/p>\n<p>If you scroll down the page a bit, you\u2019ll see the \u201cUpload presets\u201d section. Click on \u201cAdd upload preset\u201d to create a new unsigned preset. You be taken to a page that looks like this and the only thing you need to do is change \u201cSigned\u201d to \u201cUnsigned\u201d and save that change.<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/res.cloudinary.com\/jesse-thisdot\/image\/upload\/c_limit,w_2000\/f_auto\/q_auto\/v1625837557\/e-603fc55d218a650069f5228b\/p0y8npxjslxqqv2cuqgk.png\" alt=\"preset settings\" loading=\"lazy\" class=\"c-transformed-asset\"  width=\"2000\" height=\"821\"\/><\/p>\n<p>The value you see for \u201cUpload preset name\u201d is what you will need to set for your <code>UPLOAD_PRESET<\/code> in the <code>.env<\/code>. With these two values, you\u2019re <code>.env<\/code> should look similar to this.<\/p>\n<pre class=\"js-syntax-highlighted\"><span><code class=\"hljs shcb-wrap-lines\">CLOUD_NAME=test0\nUPLOAD_PRESET=rftg435ebtre4\n<\/code><\/span><\/pre>\n<h3>Making the success callback<\/h3>\n<p>Now that we have those values in place, we need to define the callback for a successful video upload. This is where we\u2019ll get the URL to store in the database and where we\u2019ll make that GraphQL call now. You can delete the form now!<\/p>\n<p>The success callback will look like this. It\u2019ll go right above the <code>return<\/code> statement.<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-5\" 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> successCallBack = <span class=\"hljs-function\">(<span class=\"hljs-params\">results<\/span>) =&gt;<\/span> {\n  <span class=\"hljs-keyword\">const<\/span> videoInfo = results.info\n  <span class=\"hljs-keyword\">const<\/span> input = {\n    <span class=\"hljs-attr\">name<\/span>: videoInfo.original_filename,\n    <span class=\"hljs-attr\">duration<\/span>: videoInfo.duration,\n    <span class=\"hljs-attr\">url<\/span>: videoInfo.url,\n  }\n  createVideo({ <span class=\"hljs-attr\">variables<\/span>: { input } })\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\">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>When you run the app with <code>yarn rw dev<\/code> and go to <code>http:\/\/localhost:8910\/videos\/new<\/code> in your browser, you should see a new button above the form that says \u201cOpen\u201d. This opens the Cloudinary upload widget.<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/res.cloudinary.com\/jesse-thisdot\/image\/upload\/c_limit,w_2000\/f_auto\/q_auto\/v1625837461\/e-603fc55d218a650069f5228b\/crvkstwqwqpmmpl8y85a.png\" alt=\"upload button\" loading=\"lazy\" class=\"c-transformed-asset\"  width=\"2000\" height=\"750\"\/><\/p>\n<p><img decoding=\"async\" src=\"https:\/\/res.cloudinary.com\/jesse-thisdot\/image\/upload\/c_limit,w_2000\/f_auto\/q_auto\/v1625837490\/e-603fc55d218a650069f5228b\/g85drai4anpv5gtjm62x.png\" alt=\"upload widget\" loading=\"lazy\" class=\"c-transformed-asset\"  width=\"2000\" height=\"1089\"\/><\/p>\n<p>Upload a few videos and you\u2019ll see you get redirected to the main videos page that has a table with references to all your videos.<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/res.cloudinary.com\/jesse-thisdot\/image\/upload\/c_limit,w_2000\/f_auto\/q_auto\/v1625837507\/e-603fc55d218a650069f5228b\/utyqalhfdskofvzblktx.png\" alt=\"video table\" loading=\"lazy\" class=\"c-transformed-asset\"  width=\"2000\" height=\"782\"\/><\/p>\n<p>All that\u2019s left now is to show the videos on this page in a library format!<\/p>\n<h2>Showing the media<\/h2>\n<p>Now you\u2019ll need to go to <code>web &gt; src &gt; components &gt; Videos<\/code> and open <code>Video.js<\/code>. This is the file that has the table we see with our video info listed. We\u2019re going to keep the table and add the videos above it.<\/p>\n<p>Since this component already has all of the video data fetched, we\u2019re going to add another <code>.map<\/code> over the videos and create new elements. Add this code right above the table element.<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-6\" 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\">div<\/span>\n  <span class=\"hljs-attr\">style<\/span>=<span class=\"hljs-string\">{{<\/span>\n    <span class=\"hljs-attr\">display:<\/span> '<span class=\"hljs-attr\">flex<\/span>',\n    <span class=\"hljs-attr\">flexWrap:<\/span> '<span class=\"hljs-attr\">wrap<\/span>',\n    <span class=\"hljs-attr\">justifyContent:<\/span> '<span class=\"hljs-attr\">space-evenly<\/span>',\n    <span class=\"hljs-attr\">marginBottom:<\/span> '<span class=\"hljs-attr\">24px<\/span>',\n  }}\n&gt;<\/span>\n  {videos.map((video) =&gt; (\n    <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">video<\/span> <span class=\"hljs-attr\">width<\/span>=<span class=\"hljs-string\">\"320\"<\/span> <span class=\"hljs-attr\">height<\/span>=<span class=\"hljs-string\">\"240\"<\/span> <span class=\"hljs-attr\">controls<\/span>&gt;<\/span>\n      <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">source<\/span> <span class=\"hljs-attr\">src<\/span>=<span class=\"hljs-string\">{video.url}<\/span> <span class=\"hljs-attr\">type<\/span>=<span class=\"hljs-string\">\"video\/mp4\"<\/span> \/&gt;<\/span>\n      Your browser does not support the video tag.\n    <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">video<\/span>&gt;<\/span>\n  ))}\n<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">div<\/span>&gt;<\/span>\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-6\"><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>If you reload your page, you should see something similar to this, but with your videos.<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/res.cloudinary.com\/jesse-thisdot\/image\/upload\/c_limit,w_2000\/f_auto\/q_auto\/v1625837530\/e-603fc55d218a650069f5228b\/cpstw0fbzbeypf4bazx1.png\" alt=\"video library\" loading=\"lazy\" class=\"c-transformed-asset\"  width=\"2000\" height=\"964\"\/><\/p>\n<p>We\u2019re done! Now you have a fully functional media library. You can customize how content is displayed and how users interact with it if you add a little more imagination to what we started. You could add this functionality to the <code>Image<\/code> components we didn\u2019t cover. (It\u2019s why I put them in this guide.)<\/p>\n<h2>Finished code<\/h2>\n<p>You can check out the full code in this GitHub repo in the <code>media-library<\/code> directory. Or you can check out the front-end in this <a href=\"https:\/\/codesandbox.io\/s\/beautiful-shape-n5gjs\">Code Sandbox<\/a>.<\/p>\n<\/div>\n  \n  <div class=\"wp-block-cloudinary-code-sandbox \">\n    <iframe\n      src=\"https:\/\/codesandbox.io\/embed\/beautiful-shape-n5gjs?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=\"beautiful-shape-n5gjs\"\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 \"><p><em>Note: You won\u2019t be able to see everything in the Code Sandbox because it doesn\u2019t support monorepos like Redwood.<\/em><\/p>\n<h2>Conclusion<\/h2>\n<p>Storing media references to your own database can give you the flexibility to monitor changes over time. They can also act as a buffer against people trying to access your Cloudinary account. Some performance aspects might also make media references in your database useful.<\/p>\n<p>Regardless of what your reasons are, now you know how to handle this use case quickly with the help of Redwood.<\/p>\n<\/div>","protected":false},"excerpt":{"rendered":"","protected":false},"author":41,"featured_media":28198,"comment_status":"","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"_cloudinary_featured_overwrite":false,"footnotes":""},"categories":[1],"tags":[134,246,371,303],"class_list":["post-28197","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-uncategorized","tag-guest-post","tag-react","tag-under-review","tag-video"],"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>Making a Media Library<\/title>\n<meta name=\"description\" content=\"Having a customizable media library can help you keep track of relevant content and manage settings around it. In this post, we&#039;ll create a media library that can be extended to cover a lot of different needs.\" \/>\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\/making-a-media-library-with-redwood\/\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"Making a Media Library\" \/>\n<meta property=\"og:description\" content=\"Having a customizable media library can help you keep track of relevant content and manage settings around it. In this post, we&#039;ll create a media library that can be extended to cover a lot of different needs.\" \/>\n<meta property=\"og:url\" content=\"https:\/\/cloudinary.com\/blog\/guest_post\/making-a-media-library-with-redwood\/\" \/>\n<meta property=\"og:site_name\" content=\"Cloudinary Blog\" \/>\n<meta property=\"article:published_time\" content=\"2022-03-23T22:59:01+00:00\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:image\" content=\"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1681925138\/Web_Assets\/blog\/cb1ca04df335d4cd75a8d49433a2a5c3612f418b-5957x3493-1_281980dcd2\/cb1ca04df335d4cd75a8d49433a2a5c3612f418b-5957x3493-1_281980dcd2.jpg?_i=AA\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\/\/schema.org\",\"@graph\":[{\"@type\":\"NewsArticle\",\"@id\":\"https:\/\/cloudinary.com\/blog\/guest_post\/making-a-media-library-with-redwood\/#article\",\"isPartOf\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/guest_post\/making-a-media-library-with-redwood\/\"},\"author\":{\"name\":\"\",\"@id\":\"\"},\"headline\":\"Making a Media Library\",\"datePublished\":\"2022-03-23T22:59:01+00:00\",\"mainEntityOfPage\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/guest_post\/making-a-media-library-with-redwood\/\"},\"wordCount\":4,\"publisher\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/#organization\"},\"image\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/guest_post\/making-a-media-library-with-redwood\/#primaryimage\"},\"thumbnailUrl\":\"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1681925138\/Web_Assets\/blog\/cb1ca04df335d4cd75a8d49433a2a5c3612f418b-5957x3493-1_281980dcd2\/cb1ca04df335d4cd75a8d49433a2a5c3612f418b-5957x3493-1_281980dcd2.jpg?_i=AA\",\"keywords\":[\"Guest Post\",\"React\",\"Under Review\",\"Video\"],\"inLanguage\":\"en-US\",\"copyrightYear\":\"2022\",\"copyrightHolder\":{\"@id\":\"https:\/\/cloudinary.com\/#organization\"}},{\"@type\":\"WebPage\",\"@id\":\"https:\/\/cloudinary.com\/blog\/guest_post\/making-a-media-library-with-redwood\/\",\"url\":\"https:\/\/cloudinary.com\/blog\/guest_post\/making-a-media-library-with-redwood\/\",\"name\":\"Making a Media Library\",\"isPartOf\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/#website\"},\"primaryImageOfPage\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/guest_post\/making-a-media-library-with-redwood\/#primaryimage\"},\"image\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/guest_post\/making-a-media-library-with-redwood\/#primaryimage\"},\"thumbnailUrl\":\"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1681925138\/Web_Assets\/blog\/cb1ca04df335d4cd75a8d49433a2a5c3612f418b-5957x3493-1_281980dcd2\/cb1ca04df335d4cd75a8d49433a2a5c3612f418b-5957x3493-1_281980dcd2.jpg?_i=AA\",\"datePublished\":\"2022-03-23T22:59:01+00:00\",\"description\":\"Having a customizable media library can help you keep track of relevant content and manage settings around it. In this post, we'll create a media library that can be extended to cover a lot of different needs.\",\"breadcrumb\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/guest_post\/making-a-media-library-with-redwood\/#breadcrumb\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\/\/cloudinary.com\/blog\/guest_post\/making-a-media-library-with-redwood\/\"]}]},{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\/\/cloudinary.com\/blog\/guest_post\/making-a-media-library-with-redwood\/#primaryimage\",\"url\":\"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1681925138\/Web_Assets\/blog\/cb1ca04df335d4cd75a8d49433a2a5c3612f418b-5957x3493-1_281980dcd2\/cb1ca04df335d4cd75a8d49433a2a5c3612f418b-5957x3493-1_281980dcd2.jpg?_i=AA\",\"contentUrl\":\"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1681925138\/Web_Assets\/blog\/cb1ca04df335d4cd75a8d49433a2a5c3612f418b-5957x3493-1_281980dcd2\/cb1ca04df335d4cd75a8d49433a2a5c3612f418b-5957x3493-1_281980dcd2.jpg?_i=AA\",\"width\":5957,\"height\":3493},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\/\/cloudinary.com\/blog\/guest_post\/making-a-media-library-with-redwood\/#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\/\/cloudinary.com\/blog\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"Making a Media Library\"}]},{\"@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":"Making a Media Library","description":"Having a customizable media library can help you keep track of relevant content and manage settings around it. In this post, we'll create a media library that can be extended to cover a lot of different needs.","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\/making-a-media-library-with-redwood\/","og_locale":"en_US","og_type":"article","og_title":"Making a Media Library","og_description":"Having a customizable media library can help you keep track of relevant content and manage settings around it. In this post, we'll create a media library that can be extended to cover a lot of different needs.","og_url":"https:\/\/cloudinary.com\/blog\/guest_post\/making-a-media-library-with-redwood\/","og_site_name":"Cloudinary Blog","article_published_time":"2022-03-23T22:59:01+00:00","twitter_card":"summary_large_image","twitter_image":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1681925138\/Web_Assets\/blog\/cb1ca04df335d4cd75a8d49433a2a5c3612f418b-5957x3493-1_281980dcd2\/cb1ca04df335d4cd75a8d49433a2a5c3612f418b-5957x3493-1_281980dcd2.jpg?_i=AA","schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"NewsArticle","@id":"https:\/\/cloudinary.com\/blog\/guest_post\/making-a-media-library-with-redwood\/#article","isPartOf":{"@id":"https:\/\/cloudinary.com\/blog\/guest_post\/making-a-media-library-with-redwood\/"},"author":{"name":"","@id":""},"headline":"Making a Media Library","datePublished":"2022-03-23T22:59:01+00:00","mainEntityOfPage":{"@id":"https:\/\/cloudinary.com\/blog\/guest_post\/making-a-media-library-with-redwood\/"},"wordCount":4,"publisher":{"@id":"https:\/\/cloudinary.com\/blog\/#organization"},"image":{"@id":"https:\/\/cloudinary.com\/blog\/guest_post\/making-a-media-library-with-redwood\/#primaryimage"},"thumbnailUrl":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1681925138\/Web_Assets\/blog\/cb1ca04df335d4cd75a8d49433a2a5c3612f418b-5957x3493-1_281980dcd2\/cb1ca04df335d4cd75a8d49433a2a5c3612f418b-5957x3493-1_281980dcd2.jpg?_i=AA","keywords":["Guest Post","React","Under Review","Video"],"inLanguage":"en-US","copyrightYear":"2022","copyrightHolder":{"@id":"https:\/\/cloudinary.com\/#organization"}},{"@type":"WebPage","@id":"https:\/\/cloudinary.com\/blog\/guest_post\/making-a-media-library-with-redwood\/","url":"https:\/\/cloudinary.com\/blog\/guest_post\/making-a-media-library-with-redwood\/","name":"Making a Media Library","isPartOf":{"@id":"https:\/\/cloudinary.com\/blog\/#website"},"primaryImageOfPage":{"@id":"https:\/\/cloudinary.com\/blog\/guest_post\/making-a-media-library-with-redwood\/#primaryimage"},"image":{"@id":"https:\/\/cloudinary.com\/blog\/guest_post\/making-a-media-library-with-redwood\/#primaryimage"},"thumbnailUrl":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1681925138\/Web_Assets\/blog\/cb1ca04df335d4cd75a8d49433a2a5c3612f418b-5957x3493-1_281980dcd2\/cb1ca04df335d4cd75a8d49433a2a5c3612f418b-5957x3493-1_281980dcd2.jpg?_i=AA","datePublished":"2022-03-23T22:59:01+00:00","description":"Having a customizable media library can help you keep track of relevant content and manage settings around it. In this post, we'll create a media library that can be extended to cover a lot of different needs.","breadcrumb":{"@id":"https:\/\/cloudinary.com\/blog\/guest_post\/making-a-media-library-with-redwood\/#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/cloudinary.com\/blog\/guest_post\/making-a-media-library-with-redwood\/"]}]},{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/cloudinary.com\/blog\/guest_post\/making-a-media-library-with-redwood\/#primaryimage","url":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1681925138\/Web_Assets\/blog\/cb1ca04df335d4cd75a8d49433a2a5c3612f418b-5957x3493-1_281980dcd2\/cb1ca04df335d4cd75a8d49433a2a5c3612f418b-5957x3493-1_281980dcd2.jpg?_i=AA","contentUrl":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1681925138\/Web_Assets\/blog\/cb1ca04df335d4cd75a8d49433a2a5c3612f418b-5957x3493-1_281980dcd2\/cb1ca04df335d4cd75a8d49433a2a5c3612f418b-5957x3493-1_281980dcd2.jpg?_i=AA","width":5957,"height":3493},{"@type":"BreadcrumbList","@id":"https:\/\/cloudinary.com\/blog\/guest_post\/making-a-media-library-with-redwood\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/cloudinary.com\/blog\/"},{"@type":"ListItem","position":2,"name":"Making a Media Library"}]},{"@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\/v1681925138\/Web_Assets\/blog\/cb1ca04df335d4cd75a8d49433a2a5c3612f418b-5957x3493-1_281980dcd2\/cb1ca04df335d4cd75a8d49433a2a5c3612f418b-5957x3493-1_281980dcd2.jpg?_i=AA","_links":{"self":[{"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/posts\/28197","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=28197"}],"version-history":[{"count":0,"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/posts\/28197\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/media\/28198"}],"wp:attachment":[{"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/media?parent=28197"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/categories?post=28197"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/tags?post=28197"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}