{"id":21610,"date":"2017-11-13T19:26:30","date_gmt":"2017-11-13T19:26:30","guid":{"rendered":"http:\/\/offline_first_masonry_grid_showcase_with_vue"},"modified":"2017-11-13T19:26:30","modified_gmt":"2017-11-13T19:26:30","slug":"offline_first_masonry_grid_showcase_with_vue","status":"publish","type":"post","link":"https:\/\/cloudinary.com\/blog\/offline_first_masonry_grid_showcase_with_vue","title":{"rendered":"Offline First Masonry Grid Showcase with Vue"},"content":{"rendered":"<div class=\"wp-block-cloudinary-markdown \"><p>To keep your product relevant in the market, you should be building <a href=\"https:\/\/developers.google.com\/web\/fundamentals\/getting-started\/codelabs\/your-first-pwapp\/\">Progressive Web Apps (PWA)<\/a>. Consider these <a href=\"https:\/\/developers.google.com\/web\/showcase\/\">testimonies<\/a> on conversion rates, provided by leading companies, such as Twitter, Forbes, AliExpress, Booking.com and others. This article doesn\u2019t go into background, history or principles surrounding PWA. Instead we want to show a practical approach to building a progressive web app using the Vue.js library.<\/p>\n<p><strong>Here is a breakdown of the project we will be tackling<\/strong>:<\/p>\n<ul>\n<li>A masonry grid of images, shown as collections. The collector, and a description, is attributed to each image. This is what a masonry grid looks like:\n<img decoding=\"async\" src=\"https:\/\/cloudinary-res.cloudinary.com\/image\/upload\/w_700,c_fill\/dpr_auto\/Screen_Shot_2017-09-14_at_4.25.11_PM_gzl07h.png\" alt=\"Masonry Grid\" loading=\"lazy\" class=\"c-transformed-asset\"  width=\"1400\" height=\"1477\"\/>\n<\/li>\n<li>An offline app showing the grid of images. The app will be built with Vue, a fast JavaScript framework for small- and large-scale apps.<\/li>\n<li>Because PWA images need to be effectively optimized to enhance smooth user experience, we will  store and deliver them via <a href=\"https:\/\/cloudinary.com\">Cloudinary<\/a>, an end-to-end media management service.<\/li>\n<li>Native app-like behavior when launched on supported mobile browsers.<\/li>\n<\/ul>\n<p>Let\u2019s get right to it!<\/p>\n<h2>Setting up Vue with PWA Features<\/h2>\n<p>A service worker is a background worker that runs independently in the browser. It doesn\u2019t make use of the main thread during execution. In fact, it\u2019s unaware of the DOM. Just JavaScript.<\/p>\n<p>Utilizing the service worker simplifies the process of making an app run offline. Even though setting it up is simple, things can go really bad when it\u2019s not done right. For this reason, a lot of community-driven utility tools exist to help scaffold a service worker with all the recommended configurations. Vue is not an exception.<\/p>\n<p>Vue CLI has a community template that comes configured with a service worker. To create a new Vue app with this template, make sure you have the Vue CLI installed:<\/p>\n<pre class=\"js-syntax-highlighted\"><span><code class=\"hljs shcb-wrap-lines\">npm install -g vue-cli\n<\/code><\/span><\/pre>\n<p>Then run the following to initialize an app:<\/p>\n<pre class=\"js-syntax-highlighted\"><span><code class=\"hljs shcb-wrap-lines\">vue init pwa offline-gallery\n<\/code><\/span><\/pre>\n<p>The major difference is in the <code>build\/webpack.prod.conf.js<\/code> file. Here is what one of the plugins configuration looks like:<\/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\"><span class=\"hljs-comment\">\/\/ service worker caching<\/span>\n<span class=\"hljs-keyword\">new<\/span> SWPrecacheWebpackPlugin({\n  <span class=\"hljs-attr\">cacheId<\/span>: <span class=\"hljs-string\">'my-vue-app'<\/span>,\n  <span class=\"hljs-attr\">filename<\/span>: <span class=\"hljs-string\">'service-worker.js'<\/span>,\n  <span class=\"hljs-attr\">staticFileGlobs<\/span>: &#91;<span class=\"hljs-string\">'dist\/**\/*.{js,html,css}'<\/span>],\n  <span class=\"hljs-attr\">minify<\/span>: <span class=\"hljs-literal\">true<\/span>,\n  <span class=\"hljs-attr\">stripPrefix<\/span>: <span class=\"hljs-string\">'dist\/'<\/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>The plugin generates a service worker file when we run the build command. The generated service worker caches all the files that match the glob expression in <code>staticFileGlobs<\/code>.<\/p>\n<p>As you can see, it is matching all the files in the <code>dist<\/code> folder. This folder is also generated after running the build command. We will see it in action after building the example app.<\/p>\n<h2>Masonry Card Component<\/h2>\n<p>Each of the cards will have an image, the image collector and the image description. Create a <code>src\/components\/Card.vue<\/code> file with the following template:<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-2\" 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\">template<\/span>&gt;<\/span>\n <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">div<\/span> <span class=\"hljs-attr\">class<\/span>=<span class=\"hljs-string\">\"card\"<\/span>&gt;<\/span>\n   <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">div<\/span> <span class=\"hljs-attr\">class<\/span>=<span class=\"hljs-string\">\"card-content\"<\/span>&gt;<\/span>\n     <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">img<\/span> <span class=\"hljs-attr\">:src<\/span>=<span class=\"hljs-string\">\"collection.imageUrl\"<\/span> <span class=\"hljs-attr\">:alt<\/span>=<span class=\"hljs-string\">\"collection.collector\"<\/span>&gt;<\/span>\n     <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">h4<\/span>&gt;<\/span>{{collection.collector}}<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">h4<\/span>&gt;<\/span>\n     <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">p<\/span>&gt;<\/span>{{collection.description}}<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">p<\/span>&gt;<\/span>\n   <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">div<\/span>&gt;<\/span>\n <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">div<\/span>&gt;<\/span>\n<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">template<\/span>&gt;<\/span>\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-2\"><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>The card expects a <code>collection<\/code> property from whatever parent it will have in the near future. To indicate that, add a Vue object with the <code>props<\/code> property:<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-3\" 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\">template<\/span>&gt;<\/span>\n...\n<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">template<\/span>&gt;<\/span>\n<span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">script<\/span>&gt;<\/span><span class=\"javascript\">\n<span class=\"hljs-keyword\">export<\/span> <span class=\"hljs-keyword\">default<\/span> {\n  <span class=\"hljs-attr\">props<\/span>: &#91;<span class=\"hljs-string\">'collection'<\/span>],\n  <span class=\"hljs-attr\">name<\/span>: <span class=\"hljs-string\">'card'<\/span>\n}\n<\/span><span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">script<\/span>&gt;<\/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\">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>Then add a basic style to make the card pretty, with some hover animations:<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-4\" 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\">template<\/span>&gt;<\/span>\n ...\n<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">template<\/span>&gt;<\/span>\n\n<span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">script<\/span>&gt;<\/span>\n...\n<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">script<\/span>&gt;<\/span>\n\n<span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">style<\/span>&gt;<\/span><span class=\"css\">\n  <span class=\"hljs-selector-class\">.card<\/span> {\n    <span class=\"hljs-attribute\">background<\/span>: <span class=\"hljs-number\">#F5F5F5<\/span>;\n    <span class=\"hljs-attribute\">padding<\/span>: <span class=\"hljs-number\">10px<\/span>;\n    <span class=\"hljs-attribute\">margin<\/span>: <span class=\"hljs-number\">0<\/span> <span class=\"hljs-number\">0<\/span> <span class=\"hljs-number\">1em<\/span>;\n    <span class=\"hljs-attribute\">width<\/span>: <span class=\"hljs-number\">100%<\/span>;\n    <span class=\"hljs-attribute\">cursor<\/span>: pointer;\n    <span class=\"hljs-attribute\">transition<\/span>: all <span class=\"hljs-number\">100ms<\/span> ease-in-out;\n  }\n  <span class=\"hljs-selector-class\">.card<\/span><span class=\"hljs-selector-pseudo\">:hover<\/span> {\n    <span class=\"hljs-attribute\">transform<\/span>: <span class=\"hljs-built_in\">translateY<\/span>(-<span class=\"hljs-number\">0.5em<\/span>);\n    <span class=\"hljs-attribute\">background<\/span>: <span class=\"hljs-number\">#EBEBEB<\/span>;\n  }\n  <span class=\"hljs-selector-tag\">img<\/span> {\n    <span class=\"hljs-attribute\">display<\/span>: block;\n    <span class=\"hljs-attribute\">width<\/span>: <span class=\"hljs-number\">100%<\/span>;\n  }\n<\/span><span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">style<\/span>&gt;<\/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\">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<h2>Rendering Cards with Images Stored in Cloudinary<\/h2>\n<p><a href=\"https:\/\/cloudinary.com\">Cloudinary<\/a> is a web service that provides an end-to-end solution for managing media. Storage, delivery, transformation, optimization and more are all provided as one service by Cloudinary.<\/p>\n<p>Cloudinary provides an <a href=\"https:\/\/cloudinary.com\/documentation\/upload_images\">upload API and widget<\/a>. But I already have some cool images stored on my Cloudinary server, so we can focus on delivering, transforming and optimizing them.<\/p>\n<p>Create an array of JSON data in <code>src\/db.json<\/code> with the content found <a href=\"https:\/\/github.com\/christiannwamba\/offline-masonry\/blob\/master\/src\/db.json\">here<\/a>. This is a truncated version of the file:<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-5\" data-shcb-language-name=\"JSON \/ JSON with Comments\" data-shcb-language-slug=\"json\"><span><code class=\"hljs language-json shcb-wrap-lines\">&#91;\n  {\n    <span class=\"hljs-attr\">\"imageId\"<\/span>: <span class=\"hljs-string\">\"jorge-vasconez-364878_me6ao9\"<\/span>,\n    <span class=\"hljs-attr\">\"collector\"<\/span>: <span class=\"hljs-string\">\"John Brian\"<\/span>,\n    <span class=\"hljs-attr\">\"description\"<\/span>: <span class=\"hljs-string\">\"Yikes invaluably thorough hello more some that neglectfully on badger crud inside mallard thus crud wildebeest pending much because therefore hippopotamus disbanded much.\"<\/span>\n  },\n  {\n    <span class=\"hljs-attr\">\"imageId\"<\/span>: <span class=\"hljs-string\">\"wynand-van-poortvliet-364366_gsvyby\"<\/span>,\n    <span class=\"hljs-attr\">\"collector\"<\/span>: <span class=\"hljs-string\">\"Nnaemeka Ogbonnaya\"<\/span>,\n    <span class=\"hljs-attr\">\"description\"<\/span>: <span class=\"hljs-string\">\"Inimically kookaburra furrowed impala jeering porcupine flaunting across following raccoon that woolly less gosh weirdly more fiendishly ahead magnificent calmly manta wow racy brought rabbit otter quiet wretched less brusquely wow inflexible abandoned jeepers.\"<\/span>\n  },\n  {\n    <span class=\"hljs-attr\">\"imageId\"<\/span>: <span class=\"hljs-string\">\"josef-reckziegel-361544_qwxzuw\"<\/span>,\n    <span class=\"hljs-attr\">\"collector\"<\/span>: <span class=\"hljs-string\">\"Ola Oluwa\"<\/span>,\n    <span class=\"hljs-attr\">\"description\"<\/span>: <span class=\"hljs-string\">\"A together cowered the spacious much darn sorely punctiliously hence much less belched goodness however poutingly wow darn fed thought stretched this affectingly more outside waved mad ostrich erect however cuckoo thought.\"<\/span>\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\">JSON \/ JSON with Comments<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">json<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n<p>The <code>imageId<\/code> field is the <code>public_id<\/code> of the image as assigned by the Cloudinary server, while <code>collector<\/code> and <code>description<\/code> are some random name and text respectively.<\/p>\n<p>Next, import this data and consume it in your <code>src\/App.vue<\/code> file:<\/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\">import<\/span> data <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">'.\/db.json'<\/span>;\n\n<span class=\"hljs-keyword\">export<\/span> <span class=\"hljs-keyword\">default<\/span> {\n  <span class=\"hljs-attr\">name<\/span>: <span class=\"hljs-string\">'app'<\/span>,\n  data() {\n    <span class=\"hljs-keyword\">return<\/span> {\n      <span class=\"hljs-attr\">collections<\/span>: &#91;]\n    }\n  },\n  created() {\n    <span class=\"hljs-keyword\">this<\/span>.collections = data.map(<span class=\"hljs-keyword\">this<\/span>.transform);\n  }\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 added a property <code>collections<\/code> and we set it\u2019s value to the JSON data. We are calling a transform method on each of the items in the array using the <code>map<\/code> method.<\/p>\n<h3>Delivering and Transforming with Cloudinary<\/h3>\n<p>You can\u2019t display an image using it\u2019s Cloudinary ID. We need to give Cloudinary the ID so it can generate a valid URL for us. First, install Cloudinary:<\/p>\n<pre class=\"js-syntax-highlighted\"><span><code class=\"hljs shcb-wrap-lines\">npm install --save cloudinary-core\n<\/code><\/span><\/pre>\n<p>Import the SDK and configure it with your cloud name (as seen on Cloudinary dashboard):<\/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> data <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">'.\/db.json'<\/span>;\n\n<span class=\"hljs-keyword\">export<\/span> <span class=\"hljs-keyword\">default<\/span> {\n  <span class=\"hljs-attr\">name<\/span>: <span class=\"hljs-string\">'app'<\/span>,\n  data() {\n    <span class=\"hljs-keyword\">return<\/span> {\n      <span class=\"hljs-attr\">cloudinary<\/span>: <span class=\"hljs-literal\">null<\/span>,\n      <span class=\"hljs-attr\">collections<\/span>: &#91;]\n    }\n  },\n  created() {\n    <span class=\"hljs-keyword\">this<\/span>.cloudinary = cloudinary.Cloudinary.new({\n      <span class=\"hljs-attr\">cloud_name<\/span>: <span class=\"hljs-string\">'christekh'<\/span>\n    })\n    <span class=\"hljs-keyword\">this<\/span>.collections = data.map(<span class=\"hljs-keyword\">this<\/span>.transform);\n  }\n}\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>The <code>new<\/code> method creates a Cloudinary instance that you can use to deliver and transform images. The <code>url<\/code> and <code>image<\/code> method takes the image public ID and returns a URL to the image or the URL in an image tag respectively:<\/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\">import<\/span> cloudinary <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">'cloudinary-core'<\/span>;\n<span class=\"hljs-keyword\">import<\/span> data <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">'.\/db.json'<\/span>;\n\n<span class=\"hljs-keyword\">import<\/span> Card <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">'.\/components\/Card'<\/span>;\n\n<span class=\"hljs-keyword\">export<\/span> <span class=\"hljs-keyword\">default<\/span> {\n  <span class=\"hljs-attr\">name<\/span>: <span class=\"hljs-string\">'app'<\/span>,\n  data() {\n    <span class=\"hljs-keyword\">return<\/span> {\n      <span class=\"hljs-attr\">cloudinary<\/span>: <span class=\"hljs-literal\">null<\/span>,\n      <span class=\"hljs-attr\">collections<\/span>: &#91;]\n    }\n  },\n  created() {\n    <span class=\"hljs-keyword\">this<\/span>.cloudinary = cloudinary.Cloudinary.new({\n      <span class=\"hljs-attr\">cloud_name<\/span>: <span class=\"hljs-string\">'christekh'<\/span>\n    })\n    <span class=\"hljs-keyword\">this<\/span>.collections = data.map(<span class=\"hljs-keyword\">this<\/span>.transform);\n  },\n  <span class=\"hljs-attr\">methods<\/span>: {\n    transform(collection) {\n      <span class=\"hljs-keyword\">const<\/span> imageUrl =\n        <span class=\"hljs-keyword\">this<\/span>.cloudinary.url(collection.imageId});\n      <span class=\"hljs-keyword\">return<\/span> <span class=\"hljs-built_in\">Object<\/span>.assign(collection, { imageUrl });\n    }\n  }\n}\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>The transform method adds an imageUrl property to each of the image collections. The property is set to the URL received from the <code>url<\/code> method.<\/p>\n<p>The images will be returned as is. No reduction in dimension or size. We need to use the Cloudinary transformation feature to customize the image:<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-9\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript shcb-wrap-lines\">methods: {\n  transform(collection) {\n    <span class=\"hljs-keyword\">const<\/span> imageUrl =\n      <span class=\"hljs-keyword\">this<\/span>.cloudinary.url(collection.imageId, { <span class=\"hljs-attr\">width<\/span>: <span class=\"hljs-number\">300<\/span>, <span class=\"hljs-attr\">crop<\/span>: <span class=\"hljs-string\">\"fit\"<\/span> });\n    <span class=\"hljs-keyword\">return<\/span> <span class=\"hljs-built_in\">Object<\/span>.assign(collection, { imageUrl });\n  }\n},\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-9\"><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>The <code>url<\/code> and <code>image<\/code> method takes a second argument, as seen above. This argument is an object and it is where you can customize your image properties and looks.<\/p>\n<p>To display the cards in the browser, import the card component, declare it as a component in the Vue object, then add it to the template:<\/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\">template<\/span>&gt;<\/span>\n  <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">div<\/span> <span class=\"hljs-attr\">id<\/span>=<span class=\"hljs-string\">\"app\"<\/span>&gt;<\/span>\n    <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">header<\/span>&gt;<\/span>\n      <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">span<\/span>&gt;<\/span>Offline Masonary Gallery<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">span<\/span>&gt;<\/span>\n    <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">header<\/span>&gt;<\/span>\n    <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">main<\/span>&gt;<\/span>\n      <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">div<\/span> <span class=\"hljs-attr\">class<\/span>=<span class=\"hljs-string\">\"wrapper\"<\/span>&gt;<\/span>\n        <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">div<\/span> <span class=\"hljs-attr\">class<\/span>=<span class=\"hljs-string\">\"cards\"<\/span>&gt;<\/span>\n          <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">card<\/span> <span class=\"hljs-attr\">v-for<\/span>=<span class=\"hljs-string\">\"collection in collections\"<\/span> <span class=\"hljs-attr\">:key<\/span>=<span class=\"hljs-string\">\"collection.imageId\"<\/span> <span class=\"hljs-attr\">:collection<\/span>=<span class=\"hljs-string\">\"collection\"<\/span>&gt;<\/span><span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">card<\/span>&gt;<\/span>\n        <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">div<\/span>&gt;<\/span>\n      <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">div<\/span>&gt;<\/span>\n    <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">main<\/span>&gt;<\/span>\n  <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">div<\/span>&gt;<\/span>\n<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">template<\/span>&gt;<\/span>\n\n<span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">script<\/span>&gt;<\/span><span class=\"javascript\">\n...\nimport Card <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">'.\/components\/Card'<\/span>;\n\n<span class=\"hljs-keyword\">export<\/span> <span class=\"hljs-keyword\">default<\/span> {\n  <span class=\"hljs-attr\">name<\/span>: <span class=\"hljs-string\">'app'<\/span>,\n  data() {\n    ...\n  },\n  created() {\n    ...\n  },\n  <span class=\"hljs-attr\">methods<\/span>: {\n   ...\n  },\n  <span class=\"hljs-attr\">components<\/span>: {\n    Card\n  }\n}\n<\/span><span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">script<\/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>We iterate over each card and list all the cards in the <code>.cards<\/code> element.<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/cloudinary-res.cloudinary.com\/image\/upload\/w_700,c_fill\/dpr_auto\/Screen_Shot_2017-09-14_at_9.50.45_PM_gm9er5.png\" alt=\"Masonry Grid\" loading=\"lazy\" class=\"c-transformed-asset\"  width=\"1400\" height=\"1473\"\/><\/p>\n<p>Right now we just have a boring single column grid. Let\u2019s write some simple masonry styles.<\/p>\n<h3>Masonry Grid<\/h3>\n<p>To achieve the masonry grid, you need to add styles to both cards (parent) and card (child).<\/p>\n<p>Adding column-count and column-gap properties to the parent kicks things up:<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-11\" data-shcb-language-name=\"CSS\" data-shcb-language-slug=\"css\"><span><code class=\"hljs language-css shcb-wrap-lines\"><span class=\"hljs-selector-class\">.cards<\/span> {\n  <span class=\"hljs-attribute\">column-count<\/span>: <span class=\"hljs-number\">1<\/span>;\n  <span class=\"hljs-attribute\">column-gap<\/span>: <span class=\"hljs-number\">1em<\/span>; \n}\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-11\"><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><img decoding=\"async\" src=\"https:\/\/cloudinary-res.cloudinary.com\/image\/upload\/w_700,c_fill\/dpr_auto\/Screen_Shot_2017-09-14_at_9.58.49_PM_ofu4xq.png\" alt=\"Masonry Grid\" loading=\"lazy\" class=\"c-transformed-asset\"  width=\"1400\" height=\"1473\"\/><\/p>\n<p>We are close. Notice how the top cards seem cut off. Just adding <code>inline-block<\/code> to the <code>display<\/code> property of the child element fixes this:<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-12\" data-shcb-language-name=\"CSS\" data-shcb-language-slug=\"css\"><span><code class=\"hljs language-css shcb-wrap-lines\"><span class=\"hljs-selector-tag\">card<\/span> {\n  <span class=\"hljs-attribute\">display<\/span>: inline-block\n}\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-12\"><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><img decoding=\"async\" src=\"https:\/\/cloudinary-res.cloudinary.com\/image\/upload\/w_700,c_fill\/dpr_auto\/Screen_Shot_2017-09-14_at_4.25.11_PM_gzl07h.png\" alt=\"Masonry Grid\" loading=\"lazy\" class=\"c-transformed-asset\"  width=\"1400\" height=\"1477\"\/><\/p>\n<p>If you consider adding animations to the cards, be careful as you will experience flickers while using the <code>transform<\/code> property. Assuming you have this simple transition on <code>.cards<\/code>:<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-13\" data-shcb-language-name=\"CSS\" data-shcb-language-slug=\"css\"><span><code class=\"hljs language-css shcb-wrap-lines\"><span class=\"hljs-selector-class\">.card<\/span> {\n    <span class=\"hljs-attribute\">transition<\/span>: all <span class=\"hljs-number\">100ms<\/span> ease-in-out;\n  }\n  <span class=\"hljs-selector-class\">.card<\/span><span class=\"hljs-selector-pseudo\">:hover<\/span> {\n    <span class=\"hljs-attribute\">transform<\/span>: <span class=\"hljs-built_in\">translateY<\/span>(-<span class=\"hljs-number\">0.5em<\/span>);\n    <span class=\"hljs-attribute\">background<\/span>: <span class=\"hljs-number\">#EBEBEB<\/span>;\n  }\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-13\"><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><img decoding=\"async\" src=\"https:\/\/cloudinary-res.cloudinary.com\/image\/upload\/w_700,c_fill\/dpr_auto\/masonry-flicker_m3zyjv.gif\" alt=\"Masonry Grid\" loading=\"lazy\" class=\"c-transformed-asset\"  width=\"1400\" height=\"1460\"\/><\/p>\n<p>Setting perspective and backface-visibilty to the element fixes that:<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-14\" data-shcb-language-name=\"CSS\" data-shcb-language-slug=\"css\"><span><code class=\"hljs language-css shcb-wrap-lines\"><span class=\"hljs-selector-class\">.card<\/span> {\n    <span class=\"hljs-attribute\">-webkit-perspective<\/span>: <span class=\"hljs-number\">1000<\/span>;\n    <span class=\"hljs-attribute\">-webkit-backface-visibility<\/span>: hidden; \n    <span class=\"hljs-attribute\">transition<\/span>: all <span class=\"hljs-number\">100ms<\/span> ease-in-out;\n  }\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-14\"><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>You also can account for screen sizes and make the grids responsive:<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-15\" data-shcb-language-name=\"CSS\" data-shcb-language-slug=\"css\"><span><code class=\"hljs language-css shcb-wrap-lines\"><span class=\"hljs-keyword\">@media<\/span> <span class=\"hljs-keyword\">only<\/span> screen <span class=\"hljs-keyword\">and<\/span> (<span class=\"hljs-attribute\">min-width:<\/span> <span class=\"hljs-number\">500px<\/span>) {\n  <span class=\"hljs-selector-class\">.cards<\/span> {\n    <span class=\"hljs-attribute\">column-count<\/span>: <span class=\"hljs-number\">2<\/span>;\n  }\n}\n\n<span class=\"hljs-keyword\">@media<\/span> <span class=\"hljs-keyword\">only<\/span> screen <span class=\"hljs-keyword\">and<\/span> (<span class=\"hljs-attribute\">min-width:<\/span> <span class=\"hljs-number\">700px<\/span>) {\n  <span class=\"hljs-selector-class\">.cards<\/span> {\n    <span class=\"hljs-attribute\">column-count<\/span>: <span class=\"hljs-number\">3<\/span>;\n  }\n}\n\n<span class=\"hljs-keyword\">@media<\/span> <span class=\"hljs-keyword\">only<\/span> screen <span class=\"hljs-keyword\">and<\/span> (<span class=\"hljs-attribute\">min-width:<\/span> <span class=\"hljs-number\">900px<\/span>) {\n  <span class=\"hljs-selector-class\">.cards<\/span> {\n    <span class=\"hljs-attribute\">column-count<\/span>: <span class=\"hljs-number\">4<\/span>;\n  }\n}\n\n<span class=\"hljs-keyword\">@media<\/span> <span class=\"hljs-keyword\">only<\/span> screen <span class=\"hljs-keyword\">and<\/span> (<span class=\"hljs-attribute\">min-width:<\/span> <span class=\"hljs-number\">1100px<\/span>) {\n  <span class=\"hljs-selector-class\">.cards<\/span> {\n    <span class=\"hljs-attribute\">column-count<\/span>: <span class=\"hljs-number\">5<\/span>;\n  }\n}\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-15\"><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><img decoding=\"async\" src=\"https:\/\/cloudinary-res.cloudinary.com\/image\/upload\/w_700,c_fill\/masonry2_h7xlb2.gif\" alt=\"Masonry Grid\" loading=\"lazy\" class=\"c-transformed-asset\"  width=\"700\" height=\"728\"\/><\/p>\n<h3>Optimizing Images<\/h3>\n<p>Cloudinary is already doing a great job by optimizing the size of the images after scaling them. You can optimize these images further, without losing quality while making your app much faster.<\/p>\n<p>Set the <code>quality<\/code> property to <code>auto<\/code> while transforming the images. Cloudinary will find a perfect balance of size and quality for your app:<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-16\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript shcb-wrap-lines\">transform(collection) {\n<span class=\"hljs-keyword\">const<\/span> imageUrl =\n   <span class=\"hljs-comment\">\/\/ Optimize<\/span>\n   <span class=\"hljs-keyword\">this<\/span>.cloudinary.url(collection.imageId, { <span class=\"hljs-attr\">width<\/span>: <span class=\"hljs-number\">300<\/span>, <span class=\"hljs-attr\">crop<\/span>: <span class=\"hljs-string\">\"fit\"<\/span>, <span class=\"hljs-attr\">quality<\/span>: <span class=\"hljs-string\">'auto'<\/span> });\n <span class=\"hljs-keyword\">return<\/span> <span class=\"hljs-built_in\">Object<\/span>.assign(collection, { imageUrl });\n}\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-16\"><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>This is a picture showing the impact:<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/cloudinary-res.cloudinary.com\/image\/upload\/w_700,c_fill\/dpr_auto\/cl-masonry-optimation_xdjjs1.png\" alt=\"Masonry Grid\" loading=\"lazy\" class=\"c-transformed-asset\"  width=\"1400\" height=\"748\"\/><\/p>\n<p>The first image was optimized from 31kb to 8kb, the second from 16kb to 6kb, and so on. Almost 1\/4 of the initial size; about 75 percent. That\u2019s a huge gain.<\/p>\n<p>Another screenshot of the app shows no loss in the quality of the images:<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/cloudinary-res.cloudinary.com\/image\/upload\/w_700,c_fill\/dpr_auto\/Screen_Shot_2017-09-14_at_10.45.14_PM_zo4dvc.png\" alt=\"Masonry Grid\" loading=\"lazy\" class=\"c-transformed-asset\"  width=\"1400\" height=\"908\"\/><\/p>\n<h2>Making the App Work Offline<\/h2>\n<p>This is the most interesting aspect of this tutorial. Right now if we were to deploy, then go offline, we would get an error message. If you\u2019re using Chrome, you will see the popular dinosaur game.<\/p>\n<p>Remember we already have service worker configured. Now all we need to do is to generate the service worker file when we run the build command. To do so, run the following in your terminal:<\/p>\n<pre class=\"js-syntax-highlighted\"><span><code class=\"hljs shcb-wrap-lines\">npm run build\n<\/code><\/span><\/pre>\n<p>Next, serve the generated build file (found in the the <code>dist<\/code> folder). There are lots of options for serving files on localhost, but my favorite still remains <code>serve<\/code>:<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-17\" data-shcb-language-name=\"PHP\" data-shcb-language-slug=\"php\"><span><code class=\"hljs language-php shcb-wrap-lines\"><span class=\"hljs-comment\"># install serve<\/span>\nnpm install -g serve\n\n<span class=\"hljs-comment\"># serve<\/span>\nserve dist\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-17\"><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 launch the app on localhost at port 5000. You would still see the page running as before. Open the developer tool, click the Application tab and select Service Workers. You should see a registered service worker:<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/cloudinary-res.cloudinary.com\/image\/upload\/w_700,c_fill\/dpr_auto\/Screen_Shot_2017-09-15_at_8.50.08_AM_djrchd.png\" alt=\"Masonry Grid\" loading=\"lazy\" class=\"c-transformed-asset\"  width=\"1400\" height=\"908\"\/><\/p>\n<p>The huge red box highlights the status of the registered service worker. As you can see, the status shows it\u2019s active. Now let\u2019s attempt going offline by clicking the check box in small red box. Reload the page and you should see our app runs offline:<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/cloudinary-res.cloudinary.com\/image\/upload\/w_700,c_fill\/dpr_auto\/Screen_Shot_2017-09-15_at_9.08.00_AM_d5gjoq.png\" alt=\"Masonry Grid\" loading=\"lazy\" class=\"c-transformed-asset\"  width=\"1400\" height=\"1079\"\/><\/p>\n<p>The app runs, but the images are gone. Don\u2019t panic, there is a reasonable explanation for that. Take another look at the service worker config:<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-18\" data-shcb-language-name=\"CSS\" data-shcb-language-slug=\"css\"><span><code class=\"hljs language-css shcb-wrap-lines\"><span class=\"hljs-selector-tag\">new<\/span> <span class=\"hljs-selector-tag\">SWPrecacheWebpackPlugin<\/span>({\n   <span class=\"hljs-attribute\">cacheId<\/span>: <span class=\"hljs-string\">'my-vue-app'<\/span>,\n   filename: <span class=\"hljs-string\">'service-worker.js'<\/span>,\n   staticFileGlobs: &#91;<span class=\"hljs-string\">'dist\/**\/*.{js,html,css}'<\/span>],\n   minify: true,\n   stripPrefix: <span class=\"hljs-string\">'dist\/'<\/span>\n })\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-18\"><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><code>staticFileGlobs<\/code> property is an array of local files we need to cache and we didn\u2019t tell the service worker to cache remote images from Cloudinary.<\/p>\n<p>To cache remotely stored assets and resources, you need to make use of a different property called <code>runtimeCaching<\/code>. It\u2019s an array and takes an object that contains the URL pattern to be cached, as well as the caching strategy:<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-19\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript shcb-wrap-lines\"><span class=\"hljs-keyword\">new<\/span> SWPrecacheWebpackPlugin({\n  <span class=\"hljs-attr\">cacheId<\/span>: <span class=\"hljs-string\">'my-vue-app'<\/span>,\n  <span class=\"hljs-attr\">filename<\/span>: <span class=\"hljs-string\">'service-worker.js'<\/span>,\n  <span class=\"hljs-attr\">staticFileGlobs<\/span>: &#91;<span class=\"hljs-string\">'dist\/**\/*.{js,html,css}'<\/span>],\n  <span class=\"hljs-attr\">runtimeCaching<\/span>: &#91;\n    {\n      <span class=\"hljs-attr\">urlPattern<\/span>: <span class=\"hljs-regexp\">\/^https:\\\/\\\/res\\.cloudinary\\.com\\\/\/<\/span>,\n      <span class=\"hljs-attr\">handler<\/span>: <span class=\"hljs-string\">'cacheFirst'<\/span>\n    }\n  ],\n  <span class=\"hljs-attr\">minify<\/span>: <span class=\"hljs-literal\">true<\/span>,\n  <span class=\"hljs-attr\">stripPrefix<\/span>: <span class=\"hljs-string\">'dist\/'<\/span>\n})\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-19\"><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>Notice the URL pattern, we are using https rather than http. Service workers, for security reasons, only work with HTTPS, with localhost as exception. Therefore, make sure all your assets and resources are served over HTTPS. Cloudinary by default serves images over HTTP, so we need to update our transformation so it serves over HTTPS:<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-20\" 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> imageUrl =\n        <span class=\"hljs-keyword\">this<\/span>.cloudinary.url(collection.imageId, { <span class=\"hljs-attr\">width<\/span>: <span class=\"hljs-number\">300<\/span>, <span class=\"hljs-attr\">crop<\/span>: <span class=\"hljs-string\">\"fit\"<\/span>, <span class=\"hljs-attr\">quality<\/span>: <span class=\"hljs-string\">'auto'<\/span>, <span class=\"hljs-attr\">secure<\/span>: <span class=\"hljs-literal\">true<\/span> });\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-20\"><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>Setting the <code>secure<\/code> property to <code>true<\/code> does the trick. Now we can rebuild the app again, then try serving offline:<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-21\" data-shcb-language-name=\"PHP\" data-shcb-language-slug=\"php\"><span><code class=\"hljs language-php shcb-wrap-lines\"><span class=\"hljs-comment\"># Build<\/span>\nnpm run build\n\n<span class=\"hljs-comment\"># Serve<\/span>\nserve dist\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-21\"><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>Unregister the service worker from the developer tool, go offline, the reload. Now you have an offline app:<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/cloudinary-res.cloudinary.com\/image\/upload\/w_700,c_fill\/dpr_auto\/Screen_Shot_2017-09-15_at_9.27.47_AM_slgxen.png\" alt=\"Masonry Grid\" loading=\"lazy\" class=\"c-transformed-asset\"  width=\"1400\" height=\"1116\"\/><\/p>\n<p>You can <a href=\"https:\/\/christiannwamba.github.io\/offline-masonry\/\">launch the app<\/a> on your phone, activate airplane mode, reload the page and see the app running offline.<\/p>\n<h2>Conclusion<\/h2>\n<p>When your app is optimized and caters for users experiencing poor connectivity or no internet access, there is a high tendency of retaining users because you\u2019re keeping them engaged at all times. This is what PWA does for you. Keep in mind that a PWS must be characterized with optimized contents. Cloudinary takes care of that for you, as we saw in the article. You can <a href=\"https:\/\/cloudinary.com\/users\/register\/free\">create a free account<\/a> to get started.<\/p>\n<p><em>This post originally appeared on <a href=\"https:\/\/vuejsdevelopers.com\/2017\/10\/09\/vue-js-masonary-grid\/\">VueJS Developers<\/a><\/em><\/p>\n<table>\n<tr>\n<td style = \"padding: 5px;\">\n<img decoding=\"async\" src=\"https:\/\/res.cloudinary.com\/cloudinary\/image\/upload\/c_thumb,w_100\/christian_nwamba.jpg\" alt=\"Christian Nwamba\" title=\"Christian Nwamba\"><\/img><\/td>\n<td style = \"padding: 10px;\"><i><a href=\"https:\/\/twitter.com\/codebeast\" target=\"_new\">Christian Nwamba<\/a>  (CodeBeast), is a JavaScript Preacher, Community Builder and Developer Evangelist.  In his next life, Chris hopes to remain a computer programmer.<\/i><\/td>\n<\/tr>\n<\/table>\n<\/div>","protected":false},"excerpt":{"rendered":"","protected":false},"author":41,"featured_media":21611,"comment_status":"","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"_cloudinary_featured_overwrite":false,"footnotes":""},"categories":[1],"tags":[134,315],"class_list":["post-21610","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-uncategorized","tag-guest-post","tag-vue"],"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>Offline First Masonry Grid Showcase with Vue<\/title>\n<meta name=\"description\" content=\"Learn how to build an offline Masonry Grid with Vue\" \/>\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\/offline_first_masonry_grid_showcase_with_vue\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"Offline First Masonry Grid Showcase with Vue\" \/>\n<meta property=\"og:description\" content=\"Learn how to build an offline Masonry Grid with Vue\" \/>\n<meta property=\"og:url\" content=\"https:\/\/cloudinary.com\/blog\/offline_first_masonry_grid_showcase_with_vue\" \/>\n<meta property=\"og:site_name\" content=\"Cloudinary Blog\" \/>\n<meta property=\"article:published_time\" content=\"2017-11-13T19:26:30+00:00\" \/>\n<meta property=\"og:image\" content=\"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1649723724\/Web_Assets\/blog\/VUE_grid\/VUE_grid.png?_i=AA\" \/>\n\t<meta property=\"og:image:width\" content=\"1540\" \/>\n\t<meta property=\"og:image:height\" content=\"847\" \/>\n\t<meta property=\"og:image:type\" content=\"image\/png\" \/>\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\/offline_first_masonry_grid_showcase_with_vue#article\",\"isPartOf\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/offline_first_masonry_grid_showcase_with_vue\"},\"author\":{\"name\":\"\",\"@id\":\"\"},\"headline\":\"Offline First Masonry Grid Showcase with Vue\",\"datePublished\":\"2017-11-13T19:26:30+00:00\",\"mainEntityOfPage\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/offline_first_masonry_grid_showcase_with_vue\"},\"wordCount\":7,\"publisher\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/#organization\"},\"image\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/offline_first_masonry_grid_showcase_with_vue#primaryimage\"},\"thumbnailUrl\":\"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1649723724\/Web_Assets\/blog\/VUE_grid\/VUE_grid.png?_i=AA\",\"keywords\":[\"Guest Post\",\"Vue\"],\"inLanguage\":\"en-US\",\"copyrightYear\":\"2017\",\"copyrightHolder\":{\"@id\":\"https:\/\/cloudinary.com\/#organization\"}},{\"@type\":\"WebPage\",\"@id\":\"https:\/\/cloudinary.com\/blog\/offline_first_masonry_grid_showcase_with_vue\",\"url\":\"https:\/\/cloudinary.com\/blog\/offline_first_masonry_grid_showcase_with_vue\",\"name\":\"Offline First Masonry Grid Showcase with Vue\",\"isPartOf\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/#website\"},\"primaryImageOfPage\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/offline_first_masonry_grid_showcase_with_vue#primaryimage\"},\"image\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/offline_first_masonry_grid_showcase_with_vue#primaryimage\"},\"thumbnailUrl\":\"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1649723724\/Web_Assets\/blog\/VUE_grid\/VUE_grid.png?_i=AA\",\"datePublished\":\"2017-11-13T19:26:30+00:00\",\"description\":\"Learn how to build an offline Masonry Grid with Vue\",\"breadcrumb\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/offline_first_masonry_grid_showcase_with_vue#breadcrumb\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\/\/cloudinary.com\/blog\/offline_first_masonry_grid_showcase_with_vue\"]}]},{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\/\/cloudinary.com\/blog\/offline_first_masonry_grid_showcase_with_vue#primaryimage\",\"url\":\"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1649723724\/Web_Assets\/blog\/VUE_grid\/VUE_grid.png?_i=AA\",\"contentUrl\":\"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1649723724\/Web_Assets\/blog\/VUE_grid\/VUE_grid.png?_i=AA\",\"width\":1540,\"height\":847},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\/\/cloudinary.com\/blog\/offline_first_masonry_grid_showcase_with_vue#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\/\/cloudinary.com\/blog\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"Offline First Masonry Grid Showcase with Vue\"}]},{\"@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":"Offline First Masonry Grid Showcase with Vue","description":"Learn how to build an offline Masonry Grid with Vue","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\/offline_first_masonry_grid_showcase_with_vue","og_locale":"en_US","og_type":"article","og_title":"Offline First Masonry Grid Showcase with Vue","og_description":"Learn how to build an offline Masonry Grid with Vue","og_url":"https:\/\/cloudinary.com\/blog\/offline_first_masonry_grid_showcase_with_vue","og_site_name":"Cloudinary Blog","article_published_time":"2017-11-13T19:26:30+00:00","og_image":[{"width":1540,"height":847,"url":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1649723724\/Web_Assets\/blog\/VUE_grid\/VUE_grid.png?_i=AA","type":"image\/png"}],"twitter_card":"summary_large_image","schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"NewsArticle","@id":"https:\/\/cloudinary.com\/blog\/offline_first_masonry_grid_showcase_with_vue#article","isPartOf":{"@id":"https:\/\/cloudinary.com\/blog\/offline_first_masonry_grid_showcase_with_vue"},"author":{"name":"","@id":""},"headline":"Offline First Masonry Grid Showcase with Vue","datePublished":"2017-11-13T19:26:30+00:00","mainEntityOfPage":{"@id":"https:\/\/cloudinary.com\/blog\/offline_first_masonry_grid_showcase_with_vue"},"wordCount":7,"publisher":{"@id":"https:\/\/cloudinary.com\/blog\/#organization"},"image":{"@id":"https:\/\/cloudinary.com\/blog\/offline_first_masonry_grid_showcase_with_vue#primaryimage"},"thumbnailUrl":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1649723724\/Web_Assets\/blog\/VUE_grid\/VUE_grid.png?_i=AA","keywords":["Guest Post","Vue"],"inLanguage":"en-US","copyrightYear":"2017","copyrightHolder":{"@id":"https:\/\/cloudinary.com\/#organization"}},{"@type":"WebPage","@id":"https:\/\/cloudinary.com\/blog\/offline_first_masonry_grid_showcase_with_vue","url":"https:\/\/cloudinary.com\/blog\/offline_first_masonry_grid_showcase_with_vue","name":"Offline First Masonry Grid Showcase with Vue","isPartOf":{"@id":"https:\/\/cloudinary.com\/blog\/#website"},"primaryImageOfPage":{"@id":"https:\/\/cloudinary.com\/blog\/offline_first_masonry_grid_showcase_with_vue#primaryimage"},"image":{"@id":"https:\/\/cloudinary.com\/blog\/offline_first_masonry_grid_showcase_with_vue#primaryimage"},"thumbnailUrl":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1649723724\/Web_Assets\/blog\/VUE_grid\/VUE_grid.png?_i=AA","datePublished":"2017-11-13T19:26:30+00:00","description":"Learn how to build an offline Masonry Grid with Vue","breadcrumb":{"@id":"https:\/\/cloudinary.com\/blog\/offline_first_masonry_grid_showcase_with_vue#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/cloudinary.com\/blog\/offline_first_masonry_grid_showcase_with_vue"]}]},{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/cloudinary.com\/blog\/offline_first_masonry_grid_showcase_with_vue#primaryimage","url":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1649723724\/Web_Assets\/blog\/VUE_grid\/VUE_grid.png?_i=AA","contentUrl":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1649723724\/Web_Assets\/blog\/VUE_grid\/VUE_grid.png?_i=AA","width":1540,"height":847},{"@type":"BreadcrumbList","@id":"https:\/\/cloudinary.com\/blog\/offline_first_masonry_grid_showcase_with_vue#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/cloudinary.com\/blog\/"},{"@type":"ListItem","position":2,"name":"Offline First Masonry Grid Showcase with Vue"}]},{"@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\/v1649723724\/Web_Assets\/blog\/VUE_grid\/VUE_grid.png?_i=AA","_links":{"self":[{"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/posts\/21610","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=21610"}],"version-history":[{"count":0,"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/posts\/21610\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/media\/21611"}],"wp:attachment":[{"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/media?parent=21610"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/categories?post=21610"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/tags?post=21610"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}