{"componentChunkName":"component---src-templates-author-jsx","path":"/blog/author/kamal-nasser/","result":{"data":{"prismic":{"allFeaturedblogs":{"edges":[{"node":{"featured_blogs_enabled":true,"heading":[{"type":"paragraph","text":"Featured posts","spans":[]}],"featured_blog_1":{"__typename":"PRISMIC_Blog","_linkType":"Link.document","blog_header_image":{"dimensions":{"width":790,"height":395},"alt":null,"copyright":null,"url":"https://images.prismic.io/www-static/6d8d81b1-971a-4313-b033-b4e125cb14a0_MondoDB-blog-header-790x395.PNG?auto=compress,format"},"blog_headline":[{"type":"heading1","text":"Introducing DigitalOcean Managed MongoDB – a fully managed, database as a service for modern apps","spans":[]}],"blog_post_date":"2021-06-29","blog_post_content":[{"type":"paragraph","text":"MongoDB is one of the most popular databases, and it’s ideal for apps that evolve rapidly and need to handle huge volumes of data and traffic. It offers advantages like flexible document schemas, code-native data access, change-friendly design, and easy horizontal scale-out.","spans":[{"start":22,"end":44,"type":"hyperlink","data":{"link_type":"Web","url":"https://db-engines.com/en/ranking","target":"_blank"}}]},{"type":"paragraph","text":"However, building and maintaining MongoDB clusters from the ground up can be a huge undertaking. Developers often complain that they have to spend their valuable time and resources on database management. Well, we’ve been listening and have some great news: accessing and managing MongoDB on DigitalOcean just got a lot simpler!","spans":[]},{"type":"paragraph","text":"We are excited to announce that DigitalOcean Managed MongoDB is now in General Availability. Managed MongoDB is a fully managed, database as a service (DBaaS) offering from DigitalOcean, built in partnership with and certified by MongoDB Inc. It provides you all the technical capabilities that make MongoDB so beloved in the developer community. Together we have ensured that you will get access to all the latest releases of the MongoDB document database as they become available.","spans":[{"start":32,"end":91,"type":"hyperlink","data":{"link_type":"Web","url":"https://www.digitalocean.com/products/managed-databases-mongodb/"}},{"start":230,"end":241,"type":"hyperlink","data":{"link_type":"Web","url":"https://www.mongodb.com/","target":"_blank"}}]},{"type":"paragraph","text":"Managed MongoDB simplifies the MongoDB administration. Developers of all skill levels, even those who do not have prior experience in databases, can spin up MongoDB clusters in just a few minutes. We handle the provisioning, managing, scaling, updates, backups, and security of your MongoDB clusters, allowing you to offload the complex, time consuming –yet critical – database administration tasks to us. This empowers you to focus on what really matters: building awesome apps.","spans":[]},{"type":"embed","oembed":{"height":113,"width":200,"embed_url":"https://www.youtube.com/watch?v=NvHQSV7jnKA","type":"video","version":"1.0","title":"Create a MongoDB Database on DigitalOcean","author_name":"DigitalOcean","author_url":"https://www.youtube.com/c/Digitalocean","provider_name":"YouTube","provider_url":"https://www.youtube.com/","cache_age":null,"thumbnail_url":"https://i.ytimg.com/vi/NvHQSV7jnKA/hqdefault.jpg","thumbnail_width":480,"thumbnail_height":360,"html":"<iframe width=\"200\" height=\"113\" src=\"https://www.youtube.com/embed/NvHQSV7jnKA?feature=oembed\" frameborder=\"0\" allow=\"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture\" allowfullscreen></iframe>"}},{"type":"heading2","text":"Benefits of Managed MongoDB","spans":[]},{"type":"paragraph","text":"","spans":[]},{"type":"list-item","text":"Easy set up and maintenance: We create the database clusters for you. Simply choose the cluster configuration (e.g., memory, disk size, number of nodes, etc.), and the data center in which you want to host the database. Follow a few simple steps and your database cluster will be up and running in a matter of minutes. You can spin up clusters using the cloud control panel, CLI, or API.\n\n","spans":[{"start":0,"end":28,"type":"strong"}]},{"type":"list-item","text":"Automatic daily backups with point in time recovery: Data is one of the most important assets of an app, so it’s critical to backup your database. We take backups of your entire clusters automatically on a daily basis, for free. We also provide a point in time recovery for 7 days, that way if things go wrong due to human error, machine error, or some combination of both, you can easily restore the database as it was at any point in the previous 7 days. \n\n","spans":[{"start":0,"end":52,"type":"strong"}]},{"type":"list-item","text":"Automatic updates and access to latest MongoDB releases: You get access to MongoDB 4.4. This is the latest release of MongoDB and comes packed with numerous enhancements like hedged reads, rust, and swift drivers. Since we have developed Managed MongoDB in partnership with MongoDB Inc, you will always get access to new releases as they become available. With Managed MongoDB, the updates happen automatically. Just select a date and time for the updates and we take care of the rest. This makes it easy to stay up to date with MongoDB releases without disrupting your business.\n\n","spans":[{"start":0,"end":56,"type":"strong"},{"start":148,"end":169,"type":"hyperlink","data":{"link_type":"Web","url":"https://www.mongodb.com/new","target":"_blank"}}]},{"type":"list-item","text":"High availability with automated failover: If your database goes down, it can take down the entire app, leading to bad customer experiences. With Managed MongoDB, you can easily minimize the downtime for your database and make it highly available with standby nodes. Standby nodes add redundancy, so if for example the primary node fails, the standby node is immediately promoted to primary and begins serving requests while we provision a replacement standby node in the background.\n\n","spans":[{"start":0,"end":42,"type":"strong"}]},{"type":"list-item","text":"Scale up easily to handle traffic spikes: As your app gains traction and the usage grows, it’s important to have a database that can keep up with the increased demand. With Managed MongoDB, you can easily scale up the size of database nodes when needed.\n\n","spans":[{"start":0,"end":41,"type":"strong"}]},{"type":"list-item","text":"Secure by default: Since data is critical, it also needs to be secure. We encrypt data at rest with LUKS and in transit with SSL. When you create a new cluster, it’s placed in a VPC network by default that provides a more secure connection between resources. You can also restrict access to your nodes to prevent brute-force password and denial-of-service attacks.","spans":[{"start":0,"end":18,"type":"strong"},{"start":178,"end":189,"type":"hyperlink","data":{"link_type":"Web","url":"https://www.digitalocean.com/docs/networking/vpc/"}}]},{"type":"heading2","text":"The need for Managed Databases","spans":[]},{"type":"paragraph","text":"DigitalOcean’s mission is to simplify cloud computing so developers, startups, and SMBs can spend more time building software that changes the world. While databases are a critical component to any application, building, maintaining, and scaling them can be complex and time consuming. For developers that are building apps for their business, database administration is often not a core focus area. But it’s quite common to find developers that write the code and then also roll up their sleeves to maintain databases. Such users would rather offload the tedious database administration and focus their limited time and energy on building and enhancing their apps. ","spans":[]},{"type":"paragraph","text":"With this in mind, we introduced Managed Databases a couple of years ago and are excited to add Managed MongoDB to our portfolio. With this release, DigitalOcean Managed Databases now supports the following engines:","spans":[{"start":33,"end":50,"type":"hyperlink","data":{"link_type":"Web","url":"https://www.digitalocean.com/products/managed-databases/"}}]},{"type":"image","url":"https://images.prismic.io/www-static/87745cc1-1c5f-4463-b104-104b7fc30dc7_managed-databases-logos.png?auto=compress,format","alt":null,"copyright":null,"dimensions":{"width":849,"height":104}},{"type":"paragraph","text":"Managed MongoDB launch comes on the heels of DigitalOcean App Platform, a modern, reimagined PaaS (Platform as a Service) that we released a few months ago. App Platform makes it very easy to build, deploy, and scale apps and static sites. You can deploy code by simply pointing to your GitHub and GitLab repos, and App Platform will do all the heavy lifting of managing infrastructure, app runtimes, and dependencies. App Platform, along with Managed Databases, helps fulfill DigitalOcean’s mission by empowering developers, startups, and SMBs to focus more on their apps, and less on the underlying infrastructure and databases.","spans":[{"start":45,"end":70,"type":"hyperlink","data":{"link_type":"Web","url":"https://www.digitalocean.com/products/app-platform/"}}]},{"type":"heading2","text":"How Managed MongoDB works","spans":[]},{"type":"paragraph","text":"DigitalOcean provides you with various compute options to build your apps like:","spans":[]},{"type":"list-item","text":"Droplets: On-demand, Linux virtual machines suitable for production business applications and personal passion projects.","spans":[{"start":0,"end":8,"type":"hyperlink","data":{"link_type":"Web","url":"https://www.digitalocean.com/products/droplets/"}}]},{"type":"list-item","text":"DigitalOcean Kubernetes: Managed Kubernetes with automatic scaling, upgrades, and a free control plane.","spans":[{"start":0,"end":23,"type":"hyperlink","data":{"link_type":"Web","url":"https://www.digitalocean.com/products/kubernetes/"}}]},{"type":"list-item","text":"DigitalOcean App Platform: A fully managed Platform as a Service.","spans":[{"start":0,"end":25,"type":"hyperlink","data":{"link_type":"Web","url":"https://www.digitalocean.com/products/app-platform/"}}]},{"type":"paragraph","text":"No matter which compute option you choose to build your apps, you can easily add Managed MongoDB to it. In addition to this, Managed MongoDB also integrates with the Node.js 1-Click App from DigitalOcean Marketplace making it a lot easier to build Node.js apps.","spans":[{"start":166,"end":215,"type":"hyperlink","data":{"link_type":"Web","url":"https://marketplace.digitalocean.com/apps/nodejs"}}]},{"type":"heading2","text":"Simple, predictable pricing","spans":[]},{"type":"paragraph","text":"Just like all DigitalOcean products, Managed MongoDB provides simple, predictable pricing that allows you to control costs and prevent any surprise bills. You can spin up a database cluster for just $15/month, or a highly available three-node replica set for $45/month. Click here for more information.","spans":[{"start":270,"end":301,"type":"hyperlink","data":{"link_type":"Web","url":"https://www.digitalocean.com/pricing/#managed-databases"}}]},{"type":"heading2","text":"Regional availability","spans":[]},{"type":"paragraph","text":"Managed MongoDB is currently available in the following regions:","spans":[]},{"type":"list-item","text":"NYC3 (New York, USA)","spans":[]},{"type":"list-item","text":"FRA1 (Frankfurt, Germany)","spans":[]},{"type":"list-item","text":"AMS3 (Amsterdam, Netherlands)","spans":[]},{"type":"paragraph","text":"We will be making Managed Mongo available in other regions soon. Please check out the release notes for most up to date information on regional availability.","spans":[{"start":86,"end":99,"type":"hyperlink","data":{"link_type":"Web","url":"https://www.digitalocean.com/docs/release-notes/"}}]},{"type":"heading2","text":"Join us at deploy, DigitalOcean’s virtual user conference","spans":[]},{"type":"paragraph","text":"Today we have deploy, DigitalOcean’s signature user conference, which focuses on celebrating, educating, and connecting awesome builders from all over the world.","spans":[{"start":14,"end":20,"type":"hyperlink","data":{"link_type":"Web","url":"https://deploy.digitalocean.com/home"}}]},{"type":"paragraph","text":"Check out the keynote session from DigitalOcean's CEO, Yancey Spruill, in which he talks about where we're headed as a company and shares some exciting product updates. His keynote will be followed by sessions from community members, engineers, customers, and other experts that are building technologies and businesses powered by the cloud. With live Q&A and an active Discord server, there’s ample opportunity to engage and learn something new. Click here to attend the deploy conference.","spans":[{"start":14,"end":69,"type":"hyperlink","data":{"link_type":"Web","url":"https://deploy.digitalocean.com/agenda/session/552806"}},{"start":347,"end":384,"type":"hyperlink","data":{"link_type":"Web","url":"http://do.co/deploy-discord"}},{"start":461,"end":489,"type":"hyperlink","data":{"link_type":"Web","url":"http://do.co/deploy"}}]},{"type":"paragraph","text":"We are also launching a hackathon for DigitalOcean Managed MongoDB. Learn how you can participate, submit an app and get a t-shirt.","spans":[{"start":24,"end":66,"type":"hyperlink","data":{"link_type":"Web","url":"https://www.digitalocean.com/mongodb-hackathon"}}]},{"type":"paragraph","text":"We hope you will give Managed MongoDB a try. Here are some sample datasets and sample apps that you can use to kick the tires. Check out the docs and let us know what you think!","spans":[{"start":22,"end":43,"type":"hyperlink","data":{"link_type":"Web","url":"https://cloud.digitalocean.com/databases/new?engine=mongodb"}},{"start":59,"end":90,"type":"hyperlink","data":{"link_type":"Web","url":"https://github.com/do-community/mongodb-resources","target":"_blank"}},{"start":141,"end":145,"type":"hyperlink","data":{"link_type":"Web","url":"https://docs.digitalocean.com/products/databases/mongodb/"}}]},{"type":"paragraph","text":"If you’d like to have a conversation about using DigitalOcean and Managed MongoDB in your business, please feel free to contact our sales team.","spans":[{"start":120,"end":142,"type":"hyperlink","data":{"link_type":"Web","url":"https://www.digitalocean.com/company/contact/sales/"}}]},{"type":"paragraph","text":"Happy coding!","spans":[]},{"type":"paragraph","text":"André Bearfield","spans":[]},{"type":"paragraph","text":"Director of Product Management","spans":[]}],"tags":[{"tag1":{"__typename":"PRISMIC_Tag","tag":"Product Updates","_linkType":"Link.document","_meta":{"uid":"product-updates"}}}],"author":{"__typename":"PRISMIC_Author","author_name":"André Bearfield","author_image":{"dimensions":{"width":553,"height":547},"alt":"André Bearfield","copyright":null,"url":"https://images.prismic.io/www-static/fdc7c85186f0a850b04083e1d4306bd1c19772e8_andre-bearfield.png?auto=compress,format"},"_meta":{"uid":"andre-bearfield"}},"_meta":{"uid":"introducing-digitalocean-managed-mongodb"}},"featured_blog_2":{"__typename":"PRISMIC_Blog","_linkType":"Link.document","blog_header_image":{"dimensions":{"width":790,"height":400},"alt":"Droplet Console","copyright":null,"url":"https://images.prismic.io/www-static/710499ae-78cc-4179-afc1-15793637b200_DODX3727-790x400-logo-2.jpg?auto=compress,format"},"blog_headline":[{"type":"heading1","text":"Securely connect to Droplets with SSH key pairs using a new Droplet Console","spans":[]}],"blog_post_date":"2021-08-10","blog_post_content":[{"type":"paragraph","text":"The famous author Ken Blanchard once said, “Feedback is the breakfast of champions.\" This is something we truly believe at DigitalOcean, and we always strive to enhance our products based on customer feedback.","spans":[]},{"type":"paragraph","text":"With this goal in mind, we are excited to introduce a new Droplet Console that will make it much easier to connect to your Droplets securely. The new Droplet Console provides one-click SSH access to your Droplets through a native-like SSH/Terminal experience. It also eliminates the need for a password or manual configuration of SSH keys. Starting today, we’re pleased to announce that the new Droplet Console is now available to all Droplet users.","spans":[]},{"type":"heading2","text":"Why you should be using Secure Shell (SSH) ","spans":[]},{"type":"paragraph","text":"Password-based security is notoriously insecure due to password fatigue and the overuse of passwords such as ‘123456’. Secure Shell or SSH is a network communication protocol that solves this by using passwordless solutions for encryption, enabling two computers to communicate and securely share data. At a high level, SSH works by creating cryptographic key pairs consisting of a public and private key, which are computer generated and stored separately to ensure their security. ","spans":[{"start":80,"end":117,"type":"hyperlink","data":{"link_type":"Web","url":"https://cybernews.com/best-password-managers/most-common-passwords/"}}]},{"type":"paragraph","text":"SSH has become the default encryption protocol for many industries, but it was difficult to use SSH keys with DigitalOcean’s current Recovery (VNC) console, which is why we developed our new Droplet Console. The new Droplet Console is backed by an agent that security supervises the key pair, while also providing one-click SSH access to our users. You can see the full list of features below.","spans":[]},{"type":"heading2","text":"The new Droplet Console: More time saving, less time wasting ","spans":[]},{"type":"paragraph","text":"The new Droplet Console is for everyone who is looking to build fast, secure apps and avoid hassles with SSH access & usability issues.","spans":[]},{"type":"paragraph","text":"In addition to easier SSH access, the new Droplet Console comes with:","spans":[]},{"type":"list-item","text":"Copy/paste text: Instead of typing lengthy key pairs and text manually, you can use copy/paste to save time. ","spans":[{"start":0,"end":17,"type":"strong"}]},{"type":"list-item","text":"Multi-color support: Multi-color support makes the console more useful and intuitive, and breaks the conventional standard appearance which is black text on a white background. ","spans":[{"start":0,"end":41,"type":"strong"}]},{"type":"list-item","text":"Multi-language support: DigitalOcean’s new Droplet Console supports multiple languages, meaning you can now type and view any content in any language that is supported by UTF-8","spans":[{"start":0,"end":24,"type":"strong"}]},{"type":"list-item","text":"OS/images supported: Linux distributions (Ubuntu(16.04 - 20.04), Fedora (32 & 33), Debian (9), CentOS (7.6 & 8.3), CentOS 8 Stream, Rocky Linux and Marketplace images.","spans":[{"start":0,"end":20,"type":"strong"},{"start":148,"end":159,"type":"hyperlink","data":{"link_type":"Web","url":"https://marketplace.digitalocean.com/"}}]},{"type":"paragraph","text":"The new Droplet Console is available by default on any new Droplets you spin up. You can also enable it manually on older Droplets. Click here to learn more!","spans":[{"start":132,"end":157,"type":"hyperlink","data":{"link_type":"Web","url":"https://docs.digitalocean.com/products/droplets/how-to/connect-with-console/"}}]},{"type":"paragraph","text":"Check out this short walkthrough video that shows the new Droplet Console in action: ","spans":[]},{"type":"embed","oembed":{"type":"video","embed_url":"https://www.youtube.com/watch?v=Qt7QihVuxiE","title":"Access Your Droplet Terminal Through the Web Console","provider_name":"YouTube","thumbnail_url":"https://i.ytimg.com/vi/Qt7QihVuxiE/hqdefault.jpg","provider_url":"https://www.youtube.com/","author_name":"DigitalOcean","author_url":"https://www.youtube.com/c/Digitalocean","height":113,"width":200,"version":"1.0","thumbnail_height":360,"thumbnail_width":480,"html":"<iframe width=\"200\" height=\"113\" src=\"https://www.youtube.com/embed/Qt7QihVuxiE?feature=oembed\" frameborder=\"0\" allow=\"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture\" allowfullscreen></iframe>"}},{"type":"paragraph","text":"We hope you’re excited about the new Droplet Console. You’re welcome to spin some Droplets up right now, and try out the new Droplet Console – why wait?","spans":[{"start":72,"end":103,"type":"hyperlink","data":{"link_type":"Web","url":"https://cloud.digitalocean.com/droplets/new"}}]},{"type":"paragraph","text":"Happy coding!","spans":[]},{"type":"paragraph","text":"Harsh Banwait, Senior Product Manager","spans":[]}],"tags":[{"tag1":{"__typename":"PRISMIC_Tag","tag":"Product Updates","_linkType":"Link.document","_meta":{"uid":"product-updates"}}}],"author":{"__typename":"PRISMIC_Author","author_name":"Harsh Banwait","author_image":{"dimensions":{"width":600,"height":399},"alt":null,"copyright":null,"url":"https://images.prismic.io/www-static/e83ff690-b20c-4d88-a2b6-57e562558cd6_download.png?auto=compress,format"},"_meta":{"uid":"harsh-banwait"}},"_meta":{"uid":"new-droplet-console-ssh-support"}},"featured_blog_3":{"__typename":"PRISMIC_Blog","_linkType":"Link.document","blog_header_image":{"dimensions":{"width":790,"height":400},"alt":null,"copyright":null,"url":"https://images.prismic.io/www-static/588e28d3-d41e-480b-937b-8c3b19201f6e_DODX3568-790x400-Blog.jpg?auto=compress,format"},"blog_headline":[{"type":"heading1","text":"How to scale your SaaS product without breaking the bank","spans":[]}],"blog_post_date":"2021-06-22","blog_post_content":[{"type":"paragraph","text":"These days, if you are in the business of software, chances are you are delivering or plan to deliver your services using a Software-as-a-Service (SaaS) model. A combination of internet-based delivery, subscription-based pricing, and low-friction product experiences have made SaaS solutions valuable tools for their users, and an excellent vehicle for software builders looking to distribute their products.","spans":[]},{"type":"paragraph","text":"These factors have made SaaS solutions ubiquitous; SaaS is the largest segment in the public cloud market, and is used to provide functionality ranging from personal finance apps for consumers, to productivity software for businesses, and even tools and services for software developers themselves to compose their applications and simplify their workflows. It is also not uncommon to find micro-SaaS applications being built for specific industries such as retail, job functions such as accounting or marketing, or tasks such as event management. ","spans":[]},{"type":"paragraph","text":"The best thing about this SaaS wave has been that it has allowed a new generation of software builders to build and monetize applications and participate in the digital economy. Previously, you had to be a big company with lots of resources, name recognition and distribution networks to successfully sell software products. Now, irrespective of whether you are a single person working on a passion project, a small team of developers in a startup, or a small and medium-sized business (SMB), the SaaS model enables you to express your ideas in the form of software and deliver them to customers anywhere in the world.","spans":[]},{"type":"heading2","text":"The unique challenges of building SaaS solutions","spans":[]},{"type":"paragraph","text":"","spans":[]},{"type":"paragraph","text":"Despite the opportunities that come with the widespread adoption of SaaS products, software builders still have to answer key questions in their journey to building successful SaaS products. Understanding what customers to target, features to prioritize, how to price your product, and how to acquire customers are all critical questions to figure out while you are also doing the important job of actually building and operating the product. ","spans":[]},{"type":"paragraph","text":"Writing the code, testing, deployment, monitoring the usage in production, and ensuring that your apps are able to handle the additional demand when customer base and usage grows are all essential and time-consuming tasks.","spans":[]},{"type":"paragraph","text":"Additionally, being able to test multiple ideas, pivot, and double down on the ideas that actually work is critical in early stages of SaaS development. Once growth comes, it is equally important to scale up without compromising on performance or reliability. Needless to say, all of this needs to be economically viable as well, since not everyone has the resources of large SaaS providers like Salesforce or Adobe.","spans":[]},{"type":"heading2","text":"Cloud Computing enables builders but also poses challenges","spans":[]},{"type":"paragraph","text":"","spans":[]},{"type":"paragraph","text":"Fortunately, for the act of building and operating your apps, cloud computing can help take some load off your shoulders. Unless you have the scale and resources of Facebook, chances are you are not going to set up your own data centers to host the computing infrastructure that powers your SaaS company. Public cloud infrastructure providers can bring great value to SaaS builders by providing on-demand computing services with usage-based pricing. However, just like how the legacy software companies weren't built for the SaaS model, the early (and big) cloud computing services were not optimized for the unique needs of small SaaS building teams. ","spans":[]},{"type":"paragraph","text":"Smaller SaaS teams face challenges with large cloud computing providers, including:","spans":[]},{"type":"heading4","text":"Too many technology options","spans":[]},{"type":"paragraph","text":"There are just too many options for tech stacks on which to build your SaaS - programming languages, application development frameworks, libraries, runtime environments, architectural patterns, and deployment models - and the list is growing by the day.","spans":[]},{"type":"heading4","text":"Complexity of cloud computing services","spans":[]},{"type":"paragraph","text":"Even when you have decided on a technology stack, there is a lot of cloud vendor-specific terminology you need to learn and heavy lifting you need to do to build on the cloud, not all of which contributes to making your SaaS applications successful.","spans":[]},{"type":"heading4","text":"Unpredictable costs","spans":[]},{"type":"paragraph","text":"The experimentation necessary in early stages of SaaS development, as well as the scaling of applications required during the growth phase, call for affordable and predictable pricing from your cloud provider. The last thing SaaS teams want is surprising and indecipherable bills from your cloud provider. Unfortunately, smaller businesses often experience unpredictable costs with cloud providers who are busy serving only the large enterprises.","spans":[]},{"type":"heading2","text":"DigitalOcean provides a simple, cost effective solution for SaaS builders","spans":[]},{"type":"paragraph","text":"Fortunately, at DigitalOcean we have a laser focus on small software development teams, who are trying to build the next generation of applications. Today, DigitalOcean customers are already building SaaS applications which serve all kinds of customers.","spans":[{"start":191,"end":217,"type":"hyperlink","data":{"link_type":"Web","url":"https://www.digitalocean.com/solutions/saas/"}}]},{"type":"paragraph","text":"We believe SaaS builders should focus on building apps that power their business, and not spend their valuable time on managing infrastructure. That is exactly what we have been able to enable through our intuitive products that are built for scale and reliability.","spans":[{"start":205,"end":223,"type":"hyperlink","data":{"link_type":"Web","url":"https://www.digitalocean.com/products/"}}]},{"type":"list-item","text":"Vidazoo is an advertising technology company specializing in video streaming and serving. It serves video ads to thousands of websites and handles close to 10 billion requests per day. \n\n“We are as much a data company as an adtech company. Our business relies on speedy and accurate data processing at massive scale. DigitalOcean provides us the perfect set of tools to operate our SaaS business profitably, while not making us feel the need to become full time system administrators. We plan to move a lot of our apps to DigitalOcean App Platform and other fully managed products.” - Roman Svichar, CTO of Vidazoo","spans":[{"start":0,"end":7,"type":"hyperlink","data":{"link_type":"Web","url":"https://vidazoo.com/"}},{"start":187,"end":583,"type":"em"}]},{"type":"paragraph","text":"We believe in meeting customers where they are. If they already have an understanding of cloud infrastructure technologies, they should be able to leverage that knowledge and get started with our products without any further ramp up.","spans":[]},{"type":"list-item","text":"Whatfix is an enterprise SaaS provider that offers a digital adoption platform to businesses. The company helps enterprises gain the full value of their investments in enterprise applications by providing real-time, interactive, and contextual guidance to users of those applications. \n\n“What we really love about the DigitalOcean platform is the ease of use. We feel like we know infrastructure and can handle most of the configuration and management. What we needed from a cloud was not bells and whistles but efficiency and reliability. DigitalOcean provides us a platform to build our apps and then gets out of the way. Just how we like it.” - Achyuth Krishna, Director of Engineering of Whatfix","spans":[{"start":0,"end":7,"type":"hyperlink","data":{"link_type":"Web","url":"https://whatfix.com/blog/driving-the-future-now-were-excited-to-announce-our-90-million-series-d-funding/"}},{"start":287,"end":648,"type":"em"}]},{"type":"paragraph","text":"We understand that scaling while maintaining reliability of applications and profitability of business is important, so we provide robust solutions which minimize downtime.","spans":[]},{"type":"list-item","text":"Centra is a SaaS-based e-commerce platform for global direct-to-consumer and wholesale e-commerce brands. Centra provides a powerful e-commerce backend that lets brands build pixel-perfect, custom designed, online flagship stores. \n\n“How do we enable our customers to create differentiated online experiences? How do we ensure their e-commerce apps stay up and running at all times? How do we scale on-demand when traffic grows or new customers come in? These are the questions that we ask ourselves every day. Thankfully, we have a partner in DigitalOcean that provides just the platform to answer those questions enabling us to guarantee 99.9% uptime for our clients.” - Martin Jensen, CEO of Centra","spans":[{"start":0,"end":6,"type":"hyperlink","data":{"link_type":"Web","url":"https://centra.com/"}},{"start":233,"end":673,"type":"em"}]},{"type":"paragraph","text":"These are just a few examples of SaaS businesses finding success on DigitalOcean. We are constantly amazed by the creativity and innovation that software builders are utilizing our platform for. If you are interested in learning more about product updates, technical deep-dives and best practices for building SaaS products and businesses, please contact us to learn how we can help you get started. ","spans":[{"start":340,"end":357,"type":"hyperlink","data":{"link_type":"Web","url":"https://www.digitalocean.com/migrate/?utmmedium=blog","target":"_blank"}}]},{"type":"paragraph","text":"Come build with DigitalOcean!","spans":[]},{"type":"paragraph","text":"Looking to migrate your SaaS to DigitalOcean? Leverage free infrastructure credits, robust training, and technical support to ensure a worry-free migration.","spans":[{"start":0,"end":156,"type":"strong"},{"start":0,"end":156,"type":"hyperlink","data":{"link_type":"Web","url":"https://www.digitalocean.com/migrate/?utmmedium=blog","target":"_blank"}}]},{"type":"paragraph","text":"","spans":[]},{"type":"paragraph","text":"Raman Sharma","spans":[]},{"type":"paragraph","text":"Vice President, Product & Programs Marketing","spans":[]}],"tags":[{"tag1":{"__typename":"PRISMIC_Tag","tag":"Developer Relations","_linkType":"Link.document","_meta":{"uid":"developer-relations"}}}],"author":{"__typename":"PRISMIC_Author","author_name":"Raman Sharma","author_image":{"dimensions":{"width":512,"height":512},"alt":null,"copyright":null,"url":"https://images.prismic.io/www-static/497b4b14-d192-493a-8b66-7ae176ba99f3_raman.png?auto=compress,format"},"_meta":{"uid":"raman-sharma"}},"_meta":{"uid":"how-to-scale-your-saas-product-without-breaking-the-bank"}}}}]}}},"pageContext":{"limit":12,"skip":0,"numAuthorPages":1,"currentPage":1,"uid":"kamal-nasser","data":[{"node":{"author":{"_linkType":"Link.document","author_name":"Kamal Nasser","author_image":{"dimensions":{"width":1008,"height":1008},"alt":null,"copyright":null,"url":"https://images.prismic.io/www-static/e2285fcfaf32ce7ec26329fe7e416ae896fbf991_portrait_2k18_bw_smallres.jpg?auto=compress,format"},"_meta":{"uid":"kamal-nasser"}},"blog_header_image":{"dimensions":{"width":646,"height":220},"alt":"ssh img 1","copyright":null,"url":"https://images.prismic.io/www-static/aad6a8cd-1c72-4d04-a16b-e58af7a94fec_ssh-alias-1.png?auto=compress,format"},"blog_headline":[{"type":"heading1","text":"Have a lot of Droplets? Use do-ssh-alias for easier SSH access","spans":[]}],"blog_post_content":[{"type":"paragraph","text":"If you have a lot of Droplets on your account, you probably agree that it's hard to keep track of all of them—especially if you create new ones and destroy them frequently for one-off workloads. A common pain point is having to look up a Droplet's IP address when needing to SSH into it.","spans":[]},{"type":"paragraph","text":"I created do-ssh-alias to help address that. Let's look at how it can help.","spans":[{"start":10,"end":22,"type":"hyperlink","data":{"link_type":"Web","url":"https://github.com/kamaln7/do-ssh-alias"}}]},{"type":"heading2","text":"What is do-ssh-alias?","spans":[]},{"type":"paragraph","text":"Let's assume you have a Droplet named shiny-blog. Usually you would look up its IP address and then SSH into it like so:","spans":[]},{"type":"preformatted","text":"ssh username@1.2.3.4  \n","spans":[]},{"type":"paragraph","text":"What if, instead, you could simply run the following command?","spans":[]},{"type":"preformatted","text":"ssh shiny-blog  \n","spans":[]},{"type":"paragraph","text":"This is where do-ssh-alias comes in. It creates SSH aliases for all your Droplets at once so you can easily SSH in, without having to worry about what user or hostname to use.","spans":[]},{"type":"paragraph","text":"It is especially useful if your Droplets' hostnames are FQDNs (e.g. shiny.example.com) that don't point directly to the Droplets' IP addresses. One example is using Cloudflare in front of your website, so your domain name resolves to a Cloudflare server instead of your Droplet.","spans":[]},{"type":"heading2","text":"How do I use it?","spans":[]},{"type":"paragraph","text":"do-ssh-alias depends on the programs jq and doctl. The first step is installing jq and installing doctl. Linked are the installation instructions for each program. Once you install doctl, log it in to your DigitalOcean account.","spans":[{"start":37,"end":39,"type":"em"},{"start":44,"end":49,"type":"em"},{"start":69,"end":82,"type":"hyperlink","data":{"link_type":"Web","url":"https://stedolan.github.io/jq/download/"}},{"start":87,"end":103,"type":"hyperlink","data":{"link_type":"Web","url":"https://github.com/digitalocean/doctl#installing-doctl"}},{"start":188,"end":226,"type":"hyperlink","data":{"link_type":"Web","url":"https://github.com/digitalocean/doctl#authenticating-with-digitalocean"}}]},{"type":"paragraph","text":"With the dependencies taken care of, let's now install do-ssh-alias. You can either download the script from GitHub or use the command line:","spans":[{"start":93,"end":103,"type":"hyperlink","data":{"link_type":"Web","url":"https://github.com/kamaln7/do-ssh-alias/blob/master/do-ssh-alias.sh"}}]},{"type":"preformatted","text":"wget https://do.co/do-ssh-alias  \n","spans":[]},{"type":"paragraph","text":"It's always a good idea to review any scripts you download from the internet before executing them.","spans":[]},{"type":"paragraph","text":"Once you have the file on your computer, update its permissions to allow it to be executed:","spans":[]},{"type":"preformatted","text":"chmod +x do-ssh-alias  \n","spans":[]},{"type":"paragraph","text":"It's now ready to use. To generate aliases for your Droplets, run:","spans":[]},{"type":"preformatted","text":"./do-ssh-alias > ~/.ssh/do_aliases\n","spans":[]},{"type":"paragraph","text":"This will run do-ssh-alias and save the results in the file ~/.ssh/do_aliases.","spans":[]},{"type":"paragraph","text":"Finally, update your ssh config to actually use the file with the aliases. Open ~/.ssh/config in a text editor and add the following line at the top:","spans":[]},{"type":"preformatted","text":"Include do_aliases  \n","spans":[]},{"type":"paragraph","text":"That's it. You can now SSH to your Droplets using their hostnames! Any time you create or remove Droplets, simply run it again to update the configuration.","spans":[]},{"type":"heading2","text":"What else can it do?","spans":[]},{"type":"paragraph","text":"do-ssh-alias only generates SSH aliases, but it accepts a few options for some flexibility:","spans":[]},{"type":"list-item","text":"-u: Pass your SSH username like -u sammy to automatically use it for all hosts.","spans":[]},{"type":"list-item","text":"-i: To ignore certain Droplets and not create aliases for them, pass their hostnames like -i ignored-hostname-1 -i ignored-hostname-2.","spans":[]},{"type":"list-item","text":"-s: Pass a suffix with the -s option to generate additional aliases with that suffix stripped. For example, if your Droplet's hostname is shiny.example.com passing -s .example.com will generate an alias for ssh shiny in addition to ssh shiny.example.com.","spans":[]},{"type":"paragraph","text":"Below is an example of using all three options.","spans":[]},{"type":"heading2","text":"Show me an example, please!","spans":[]},{"type":"paragraph","text":"Let's assume you have the following Droplets on your account:","spans":[]},{"type":"list-item","text":"droplet1","spans":[]},{"type":"list-item","text":"droplet2.domain.com","spans":[]},{"type":"list-item","text":"droplet3.domain.com","spans":[]},{"type":"paragraph","text":"Running:","spans":[]},{"type":"preformatted","text":"do-ssh-alias -u sammy -i droplet1 -s .domain.com  \n","spans":[]},{"type":"paragraph","text":"will generate aliases for:","spans":[]},{"type":"list-item","text":"ssh droplet2.domain.com","spans":[]},{"type":"list-item","text":"ssh droplet2","spans":[]},{"type":"list-item","text":"ssh droplet3.domain.com","spans":[]},{"type":"list-item","text":"ssh droplet3","spans":[]},{"type":"paragraph","text":"all using the username sammy to log in. The SSH config will look like so:","spans":[]},{"type":"preformatted","text":"Host droplet2.domain.com droplet2  \n    Hostname Droplet2-IP\n    User sammy\n\nHost droplet3.domain.com droplet3  \n    Hostname Droplet3-IP\n    User sammy\n","spans":[]},{"type":"heading2","text":"Resources","spans":[]},{"type":"paragraph","text":"Here are some resources you may find useful:","spans":[]},{"type":"list-item","text":"do-ssh-alias on GitHub","spans":[{"start":0,"end":22,"type":"hyperlink","data":{"link_type":"Web","url":"https://github.com/kamaln7/do-ssh-alias"}}]},{"type":"list-item","text":"How To Configure Custom Connection Options for your SSH Client","spans":[{"start":0,"end":62,"type":"hyperlink","data":{"link_type":"Web","url":"https://www.digitalocean.com/community/tutorials/how-to-configure-custom-connection-options-for-your-ssh-client"}}]},{"type":"paragraph","text":"Note: doctl itself provides similar functionality through the doctl compute ssh command which allows you to SSH into a Droplet using its ID or name. The main difference is that doctl looks up the Droplet's IP address using the DigitalOcean API every time you run it, while do-ssh-alias generates a static config file that ssh reads. You might prefer do-ssh-alias if:","spans":[{"start":0,"end":5,"type":"strong"}]},{"type":"list-item","text":"you want an SSH config that can be copied to other computers without having to install doctl or store your API token on them; or","spans":[]},{"type":"list-item","text":"want to avoid the added latency of doctl's API request.","spans":[]}],"blog_post_date":"2020-05-07","tags":[{"tag1":{"tag":"Developer Relations","_linkType":"Link.document","_meta":{"uid":"developer-relations"}}}],"_meta":{"uid":"use-do-ssh-alias-for-easier-droplet-ssh-access"}}},{"node":{"author":{"_linkType":"Link.document","author_name":"Kamal Nasser","author_image":{"dimensions":{"width":1008,"height":1008},"alt":null,"copyright":null,"url":"https://images.prismic.io/www-static/e2285fcfaf32ce7ec26329fe7e416ae896fbf991_portrait_2k18_bw_smallres.jpg?auto=compress,format"},"_meta":{"uid":"kamal-nasser"}},"blog_header_image":{"dimensions":{"width":1200,"height":900},"alt":null,"copyright":null,"url":"https://images.prismic.io/www-static/c4446d83-1e27-4c4e-822c-5ca308967e52_database-mostov_dribbble.png?auto=compress,format"},"blog_headline":[{"type":"heading1","text":"Creating a Simple Contacts List with Go and PostgreSQL","spans":[]}],"blog_post_content":[{"type":"paragraph","text":"In this post, we will build a simple web page containing a contacts list, with the contacts fetched from a PostgreSQL database. We will connect to the database in Go and use PostgreSQL's support for JSON columns. This is what the result will look like:","spans":[]},{"type":"image","url":"https://images.prismic.io/www-static/76f9193e-d599-4c79-b1d0-015d71bce6fb_Kamal-1.png?auto=compress,format","alt":null,"copyright":null,"dimensions":{"width":1600,"height":1396}},{"type":"paragraph","text":"By following this post, you will learn how to connect to a PostgreSQL database in Go using the sqlx and pgx packages, render data dynamically using a template, and serve the resulting page on an HTTP server.","spans":[]},{"type":"heading1","text":"Requirements","spans":[]},{"type":"paragraph","text":"Before we get started:","spans":[]},{"type":"o-list-item","text":"Make sure you have Go installed. See this post for instructions.  ","spans":[{"start":37,"end":46,"type":"hyperlink","data":{"link_type":"Web","url":"https://www.callicoder.com/golang-installation-setup-gopath-workspace/"}}]},{"type":"o-list-item","text":"Make sure you know where your GOPATH is. It's usually ~/go unless set differently.","spans":[]},{"type":"heading1","text":"Getting an HTTP Server Up","spans":[]},{"type":"paragraph","text":"In a new empty directory inside your $GOPATH, create a file named main.go. You can name the directory anything you like: I went with go-contacts. We'll start with setting up the HTTP server using Go's built-in net/http package.","spans":[]},{"type":"preformatted","text":"    package main\n    import (\n        \"flag\"\n        \"log\"\n        \"net/http\"\n        \"os\"\n    )\n    var (\n        listenAddr = flag.String(\"addr\", getenvWithDefault(\"LISTENADDR\", \":8080\"), \"HTTP address to listen on\")\n    )\n    func getenvWithDefault(name, defaultValue string) string {\n            val := os.Getenv(name)\n            if val == \"\" {\n                    val = defaultValue\n            }\n            return val\n    }\n    func main() {\n        flag.Parse()\n        log.Printf(\"listening on %s\\n\", *listenAddr)\n        http.ListenAndServe(*listenAddr, nil)\n    }","spans":[]},{"type":"paragraph","text":"The server will want a host and a port to listen on, so we ask for that in a CLI flag named addr. We also want to offer the option to pass in the setting in an environment variable, so the default value for the flag will be taken from the LISTENADDR environment variable. This means that if the CLI flag is passed, the value of the environment variable will be used. If neither are set, we'll fall back to port 8080.","spans":[]},{"type":"paragraph","text":"If you save the file and run it now, you should be able to browse to http://localhost:8080.","spans":[{"start":69,"end":90,"type":"hyperlink","data":{"link_type":"Web","url":"http://localhost:8080"}}]},{"type":"preformatted","text":"go run main.go","spans":[]},{"type":"paragraph","text":"and see—hold on, is that a \"404 page not found\" error?!","spans":[]},{"type":"paragraph","text":"That's fine! It's because we haven't configured any routes or pages yet, so the server doesn't know how to respond to the request. Why don't we go ahead and do that now.","spans":[]},{"type":"heading2","text":"Contacts List Page","spans":[]},{"type":"paragraph","text":"Let's create the contacts list page and serve it on the root path, /. We'll use the template/html package so that we can easily pass in dynamic data (the contacts) to be rendered in the page later.","spans":[]},{"type":"paragraph","text":"Create a directory named templates alongside main.go and within it a file named index.html with the following content:","spans":[]},{"type":"preformatted","text":"    <!doctype html>\n    <html>\n        <head>\n            <meta charset=\"utf-8\">\n            <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n            <title>Contacts</title>\n            <link rel=\"stylesheet\" href=\"https://unpkg.com/tachyons@4.10.0/css/tachyons.min.css\"/>\n        </head>\n        <body>\n            <div class=\"mw6 center pa3 sans-serif\">\n                <h1 class=\"mb4\">Contacts</h1>\n            </div>\n        </body>\n    </html>","spans":[]},{"type":"paragraph","text":"This is a page with basic styling that will serve as the base for our contacts list.","spans":[]},{"type":"paragraph","text":"Now we need to read the index.html template in our program. Import html/template and add a global variable to hold the templates right after listenAddr at the top:","spans":[]},{"type":"preformatted","text":"    import (\n        \"flag\"\n        \"log\"\n        \"html/template\"\n        \"net/http\"\n    )\n    var (\n            listenAddr       = flag.String(\"addr\", getenvWithDefault(\"LISTENADDR\", \":8080\"), \"HTTP address to listen on\")\n            tmpl             = template.New(\"\")\n    )","spans":[]},{"type":"paragraph","text":"Inside main(), after the flag.Parse() line, add the following. For compatibility with all operating systems, import the path/filepath package as we will use to construct the path to the template files.","spans":[]},{"type":"preformatted","text":"    var err error\n    _, err = tmpl.ParseGlob(filepath.Join(\".\", \"templates\", \"*.html\"))\n    if err != nil {\n        log.Fatalf(\"Unable to parse templates: %v\\n\", err)\n    }","spans":[]},{"type":"paragraph","text":"This will read every HTML file in the templates directory and prepare it for rendering. Now that we've done that, we want to configure the template to be rendered on /. Add a new function at the very bottom of the file to serve the page:","spans":[]},{"type":"preformatted","text":"    func handler(w http.ResponseWriter, r *http.Request) {\n        tmpl.ExecuteTemplate(w, \"index.html\", nil)\n    }","spans":[]},{"type":"paragraph","text":"Finally, configure the server to use this handler function. Above the log.Printf() line in main(), add:","spans":[]},{"type":"preformatted","text":"http.HandleFunc(\"/\", handler)","spans":[]},{"type":"paragraph","text":"Now we're ready! The whole file should look like this:","spans":[]},{"type":"preformatted","text":"    package main\n\n    import (\n        \"flag\"\n        \"log\"\n        \"html/template\"\n        \"net/http\"\n    )\n    var (\n        listenAddr = flag.String(\"addr\", getenvWithDefault(\"LISTENADDR\", \":8080\"), \"HTTP address to listen on\")\n        tmpl       = template.New(\"\")\n    )\n    \n    func getenvWithDefault(name, defaultValue string) string {\n            val := os.Getenv(name)\n            if val == \"\" {\n                    val = defaultValue\n            }\n            return val\n    }\n\n    func main() {\n        flag.Parse()\n        var err error\n        _, err = tmpl.ParseGlob(filepath.Join(\".\", \"templates\", \"*.html\"))\n        if err != nil {\n            log.Fatalf(\"Unable to parse templates: %v\\n\", err)\n        }\n        http.HandleFunc(\"/\", handler)\n        log.Printf(\"listening on %s\\n\", *listenAddr)\n        http.ListenAndServe(*listenAddr, nil)\n    }\n    func handler(w http.ResponseWriter, r *http.Request) {\n        tmpl.ExecuteTemplate(w, \"index.html\", nil)\n    }","spans":[]},{"type":"paragraph","text":"Run go run main.go again and you should see the template we've configured.","spans":[]},{"type":"image","url":"https://images.prismic.io/www-static/4d14d033-b87d-4e16-ad5c-84cad8b14f7a_Kamal-2.png?auto=compress,format","alt":null,"copyright":null,"dimensions":{"width":1600,"height":994}},{"type":"heading1","text":"Contacts in a Database","spans":[]},{"type":"paragraph","text":"Something is missing in the page—the actual contacts! Let's add them in.","spans":[]},{"type":"paragraph","text":"We will use DigitalOcean Databases to quickly get a PostgreSQL cluster up. If you haven’t yet, create a new one—it only takes a few minutes: if you prefer a text post, see the product documentation for Databases. If you prefer a video, click here.","spans":[{"start":172,"end":211,"type":"hyperlink","data":{"link_type":"Web","url":"https://www.digitalocean.com/docs/databases/how-to/clusters/create/"}},{"start":236,"end":246,"type":"hyperlink","data":{"link_type":"Web","url":"https://www.youtube.com/watch?v=jY5FhyiEdig"}}]},{"type":"image","url":"https://images.prismic.io/www-static/a010b376-74b2-4837-892f-db1d7d8da99e_Kamal-3.png?auto=compress,format","alt":null,"copyright":null,"dimensions":{"width":1600,"height":427}},{"type":"paragraph","text":"Once you've created the cluster, copy its Connection String from the control panel. In the Connection Details section in the Overview page, choose \"Connection string\" from the list and copy it:","spans":[]},{"type":"image","url":"https://images.prismic.io/www-static/2c01727f-4ff8-4f5c-ada2-80ff7f171168_Kamal-4.png?auto=compress,format","alt":null,"copyright":null,"dimensions":{"width":990,"height":540}},{"type":"paragraph","text":"The connection string contains all the details necessary to connect to your database (including your password) so be sure to keep it safe.","spans":[]},{"type":"heading2","text":"Initializing the Database","spans":[]},{"type":"paragraph","text":"Our Go app will only handle displaying the contacts, so I have prepared an SQL export containing 10 randomly generated contacts that you can import into your database. You can find it here.","spans":[{"start":184,"end":188,"type":"hyperlink","data":{"link_type":"Web","url":"https://raw.githubusercontent.com/digitalocean/databases/master/examples/contacts.sql"}}]},{"type":"paragraph","text":"On macOS, I like to use TablePlus to work with my databases, but you can use any client you prefer or import it using the ```[php]{`psql`}``` CLI command like so:","spans":[]},{"type":"preformatted","text":"psql 'your connection string here' < contacts.sql","spans":[]},{"type":"heading2","text":"Fetching the Contacts","spans":[]},{"type":"paragraph","text":"Ok, so now we have a database with some contacts in it 🎉 Let's have our program connect to it and fetch the contacts. We'll build this functionality step by step.","spans":[]},{"type":"paragraph","text":"There are many ways to connect to a PostgreSQL database in Go. In this case, we also need a convenient way to access JSONB fields since our contacts database uses them. I personally found the combination of `github.com/jmoiron/sqlx` and `github.com/jackc/pgx` to work best.","spans":[{"start":207,"end":232,"type":"hyperlink","data":{"link_type":"Web","url":"http://github.com/jmoiron/sqlx"}},{"start":237,"end":259,"type":"hyperlink","data":{"link_type":"Web","url":"http://github.com/jackc/pgx"}}]},{"type":"paragraph","text":"Start by importing the packages:","spans":[]},{"type":"preformatted","text":"go get -u -v github.com/jackc/pgx github.com/jmoiron/sqlx","spans":[]},{"type":"paragraph","text":"And adding them at the top of main.go:","spans":[]},{"type":"preformatted","text":"    import (\n        ...\n\n        _ \"github.com/jackc/pgx/stdlib\"\n        \"github.com/jmoiron/sqlx\"\n        \"github.com/jmoiron/sqlx/types\"\n    )","spans":[]},{"type":"paragraph","text":"Now, there are a few things that we need to do. We need to define the Contact type based on the database's table structure and connect to our PostgreSQL database. When serving the contacts page, we will query the database for the contacts and pass them to the template for rendering.","spans":[]},{"type":"heading3","text":"Contact Type","spans":[]},{"type":"paragraph","text":"Add these types to main.go. They match the structure of the contacts database export and prepare support for the JSONB field favorites:  ","spans":[{"start":56,"end":84,"type":"hyperlink","data":{"link_type":"Web","url":"https://raw.githubusercontent.com/digitalocean/databases/master/examples/contacts.sql"}}]},{"type":"preformatted","text":"    // ContactFavorites is a field that contains a contact's favorites\n    type ContactFavorites struct {  \n        Colors []string `json:\"colors\"`\n    }\n    // Contact represents a Contact model in the database \n    type Contact struct {  \n        ID                   int\n        Name, Address, Phone string\n        FavoritesJSON types.JSONText    `db:\"favorites\"`\n        Favorites     *ContactFavorites `db:\"-\"`\n        CreatedAt string `db:\"created_at\"`\n        UpdatedAt string `db:\"updated_at\"`\n    }","spans":[]},{"type":"heading3","text":"Database Connection","spans":[]},{"type":"paragraph","text":"Note that we haven't connected to the database yet 👀 Let's do that now. We'll pass in the PostgreSQL connection string as a CLI flag and add a global database variable. So again at the top of main.go:","spans":[]},{"type":"preformatted","text":"    var (\n        connectionString = flag.String(\"conn\", getenvWithDefault(\"DATABASE_URL\", \"\"), \"PostgreSQL connection string\")\n        listenAddr       = flag.String(\"addr\", \":8080\", \"HTTP address to listen on\")\n        db               *sqlx.DB\n        tmpl             = template.New(\"\")\n    )","spans":[]},{"type":"paragraph","text":"Note that we use the function getenvWithDefault like with the listen address to allow the connection string to be passed using an environment variable (DATABASE_URL) in addition to the CLI flag (-conn).","spans":[]},{"type":"paragraph","text":"After the templating logic in main()(right above http.HandleFunc()), add the following:","spans":[]},{"type":"preformatted","text":"    if *connectionString == \"\" {\n        log.Fatalln(\"Please pass the connection string using the -conn option\")\n    }\n    \n    db, err = sqlx.Connect(\"pgx\", *connectionString)\n    if err != nil {\n        log.Fatalf(\"Unable to establish connection: %v\\n\", err)\n    }","spans":[]},{"type":"paragraph","text":"We're now connected to our PostgreSQL database!","spans":[]},{"type":"heading3","text":"Querying the Database for Contacts","spans":[]},{"type":"paragraph","text":"Add a new function to the bottom of the file to fetch all contacts from the database. For clearer errors, we'll make use of another package: github.com/pkg/errors. Download it and import it at the top of main.go as usual.","spans":[]},{"type":"preformatted","text":"    go get -u -v github.com/pkg/errors\n    \n    import (\n        ...\n        \"github.com/pkg/errors\"\n        ...\n    )\n    \n    …\n    \n    func fetchContacts() ([]*Contact, error) {\n        contacts := []*Contact{}\n        err := db.Select(&contacts, \"select * from contacts\")\n        if err != nil {\n            return nil, errors.Wrap(err, \"Unable to fetch contacts\")\n        }\n    \n        return contacts, nil\n    }","spans":[]},{"type":"paragraph","text":"One thing that's missing right now is the favorites column. If you look at the Contact type, we've defined this field: FavoritesJSON types.JSONText db:\"favorites\". This maps the favorites column in the database to the FavoritesJSON field in the Contact struct, making it available as a JSON object serialized as text.","spans":[]},{"type":"paragraph","text":"This means that we need to manually parse and un-marshal the JSON objects into actual Go structs. We will use Go’s encoding/json package so make sure to import it at the top of main.go. Adding onto fetchContacts():","spans":[]},{"type":"preformatted","text":"    import (\n        ...\n        \"encoding/json\"\n        ...\n    )\n    ...\n    func fetchContacts() ([]*Contact, error) {\n        ...\n    \n        for _, contact := range contacts {\n            err := json.Unmarshal(contact.FavoritesJSON, &contact.Favorites)\n    \n            if err != nil {\n                return nil, errors.Wrap(err, \"Unable to parse JSON favorites\")\n            }\n        }\n    \n        return contacts, nil\n    }","spans":[]},{"type":"paragraph","text":"The resulting structs will be stored in the Favorites field in the Contact struct.","spans":[]},{"type":"heading2","text":"Rendering the Contacts","spans":[]},{"type":"paragraph","text":"Cool, we have data. Let's use it! Inside the handler() function, we'll use fetchContacts() to get the contacts and then pass them to the template:","spans":[]},{"type":"preformatted","text":"    func handler(w http.ResponseWriter, r *http.Request) {\n        contacts, err := fetchContacts()\n        if err != nil {\n            w.WriteHeader(http.StatusInternalServerError)\n            w.Write([]byte(err.Error()))\n            return\n        }\n    \n        tmpl.ExecuteTemplate(w, \"index.html\", struct{ Contacts []*Contact }{contacts})\n    }","spans":[]},{"type":"paragraph","text":"This will attempt to fetch the contacts, display an error on failure, and pass them to the template. Note that if an error occurs, the full error will be sent as the response. In a production environment you will want to log the error and send a generic error message instead.","spans":[]},{"type":"paragraph","text":"Now we need to modify the template to do something with the contacts we are passing to it. To display favorite colors as a comma-separated list, we'll use the strings.Join function. Before we are able to use it inside the template, we need to define it as a template function, inside main() above the tmpl.ParseGlob line. Don’t forget to import the strings package at the top:","spans":[]},{"type":"preformatted","text":"    import (\n        ...\n        \"strings\"\n        ...\n    )\n    \n    …\n    \n    tmpl.Funcs(template.FuncMap{\"StringsJoin\": strings.Join})\n    _, err = tmpl.ParseGlob(filepath.Join(\".\", \"templates\", \"*.html\"))\n    \n    ...","spans":[]},{"type":"paragraph","text":"Then, under the <h1> line in the HTML template, add the following:","spans":[]},{"type":"preformatted","text":"    {{range .Contacts}}\n    <div class=\"pa2 mb3 striped--near-white\">\n        <header class=\"b mb2\">{{.Name}}</header>\n        <div class=\"pl2\">\n            <p class=\"mb2\">{{.Phone }}</p>\n            <p class=\"pre mb3\">{{.Address}}</p>\n            <p class=\"mb2\"><span class=\"fw5\">Favorite colors:</span> {{StringsJoin .Favorites.Colors \", \"}}</p>\n        </div>\n    </div>\n    {{end}}","spans":[]},{"type":"paragraph","text":"That's all! The final main.go file should look like so:","spans":[]},{"type":"preformatted","text":"    package main\n    \n    import (\n        \"encoding/json\"\n        \"flag\"\n        \"log\"\n        \"html/template\"\n        \"net/http\"\n        \"path/filepath\"\n        \"strings\"\n    \n        _ \"github.com/jackc/pgx/stdlib\"\n        \"github.com/jmoiron/sqlx\"\n        \"github.com/jmoiron/sqlx/types\"\n        \"github.com/pkg/errors\"\n    )\n    \n    // ContactFavorites is a field that contains a contact's favorites\n    type ContactFavorites struct {\n        Colors []string `json:\"colors\"`\n    }\n    \n    // Contact represents a Contact model in the database    \n    type Contact struct {\n        ID                   int\n        Name, Address, Phone string\n    \n        FavoritesJSON types.JSONText    `db:\"favorites\"`\n        Favorites     *ContactFavorites `db:\"-\"`\n    \n        CreatedAt string `db:\"created_at\"`\n        UpdatedAt string `db:\"updated_at\"`\n    }\n    \n    var (\n        connectionString = flag.String(\"conn\", getenvWithDefault(\"DATABASE_URL\", \"\"), \"PostgreSQL connection string\")\n        listenAddr       = flag.String(\"addr\", getenvWithDefault(\"LISTENADDR\", \":8080\"), \"HTTP address to listen on\")\n        db               *sqlx.DB\n        tmpl             = template.New(\"\")\n    )\n    \n    func getenvWithDefault(name, defaultValue string) string {\n            val := os.Getenv(name)\n            if val == \"\" {\n                    val = defaultValue\n            }\n    \n            return val\n    }\n    \n    func main() {\n        flag.Parse()\n        var err error\n    \n        // templating\n    \n        tmpl.Funcs(template.FuncMap{\"StringsJoin\": strings.Join})\n        _, err = tmpl.ParseGlob(filepath.Join(\".\", \"templates\", \"*.html\"))\n        if err != nil {\n            log.Fatalf(\"Unable to parse templates: %v\\n\", err)\n        }\n    \n        // postgres connection\n    \n        if *connectionString == \"\" {\n            log.Fatalln(\"Please pass the connection string using the -conn option\")\n        }\n    \n        db, err = sqlx.Connect(\"pgx\", *connectionString)\n        if err != nil {\n            log.Fatalf(\"Unable to establish connection: %v\\n\", err)\n        }\n    \n        // http server\n    \n        http.HandleFunc(\"/\", handler)\n    \n        log.Printf(\"listening on %s\\n\", *listenAddr)\n        http.ListenAndServe(*listenAddr, nil)\n    }\n    \n    func fetchContacts() ([]*Contact, error) {\n        contacts := []*Contact{}\n        err := db.Select(&contacts, \"select * from contacts\")\n        if err != nil {\n            return nil, errors.Wrap(err, \"Unable to fetch contacts\")\n        }\n    \n        for _, contact := range contacts {\n            err := json.Unmarshal(contact.FavoritesJSON, &contact.Favorites)\n    \n            if err != nil {\n                return nil, errors.Wrap(err, \"Unable to parse JSON favorites\")\n            }\n        }\n    \n        return contacts, nil\n    }\n    \n    func handler(w http.ResponseWriter, r *http.Request) {\n        contacts, err := fetchContacts()\n        if err != nil {\n            w.WriteHeader(http.StatusInternalServerError)\n            w.Write([]byte(err.Error()))\n            return\n        }\n    \n        tmpl.ExecuteTemplate(w, \"index.html\", struct{ Contacts []*Contact }{contacts})\n    }","spans":[]},{"type":"paragraph","text":"Run the program again, passing in your database's connection string like so and you should see the contacts list:","spans":[]},{"type":"preformatted","text":"    go run main.go -conn \"connection string here\"\n    # alternatively:\n    DATABASE_URL=\"connection string here\" go run main.go","spans":[]},{"type":"image","url":"https://images.prismic.io/www-static/3c4e728b-5295-49b9-af19-4ea7e7b4a730_Kamal-5.png?auto=compress,format","alt":null,"copyright":null,"dimensions":{"width":1600,"height":1396}},{"type":"heading1","text":"Conclusion","spans":[]},{"type":"paragraph","text":"After following this post, you will have learned how to build a simple contacts list step-by-step, starting with an empty page served by an HTTP web-server and ending with one that renders a list of contacts fetched from a PostgreSQL database. Along the way, you will have become familiar with using html/template to render a web page with dynamic data, connecting to a PostgreSQL database, and interacting with JSONB objects stored in the database.","spans":[]},{"type":"heading2","text":"Next Steps","spans":[]},{"type":"paragraph","text":"Here are some things you can do after following this post for further practice:","spans":[]},{"type":"list-item","text":"Print favorite colors as a bullet point list with each color being a separate item. Use html/template's built-in range function to loop over the favorite colors slice.","spans":[]},{"type":"list-item","text":"Add a favorite shape (square, circle, etc.) to one or more contacts and edit the template to display it. The Contact struct should stay unmodified.","spans":[]},{"type":"list-item","text":"List the contacts in the order that they were last updated, most recent first.","spans":[]},{"type":"paragraph","text":"[Hungry for more tutorials? Try Kamal's guides to \"Creating a Simple Contacts List with Laravel and PostgreSQL\" and \"Deploying a Fully-automated Git-based Static Website in Under 5 Minutes\"]","spans":[{"start":50,"end":111,"type":"hyperlink","data":{"link_type":"Web","url":"https://blog.digitalocean.com/create-simple-contacts-laravel-postgresql/"}},{"start":116,"end":189,"type":"hyperlink","data":{"link_type":"Web","url":"https://blog.digitalocean.com/deploying-a-fully-automated-git-based-static-website-in-under-5-minutes/"}}]},{"type":"paragraph","text":"Kamal Nasser is a Developer Advocate at DigitalOcean. He is also a Computer Science student with a passion for software engineering and avocados. You can find him on Twitter @kamaln7.","spans":[{"start":0,"end":183,"type":"em"},{"start":174,"end":182,"type":"hyperlink","data":{"link_type":"Web","url":"https://twitter.com/kamaln7"}}]}],"blog_post_date":"2019-03-18","tags":[{"tag1":{"tag":"Developer Relations","_linkType":"Link.document","_meta":{"uid":"developer-relations"}}},{"tag1":{"tag":"Community","_linkType":"Link.document","_meta":{"uid":"community"}}}],"_meta":{"uid":"create-a-simple-contacts-list-with-go"}}},{"node":{"author":{"_linkType":"Link.document","author_name":"Kamal Nasser","author_image":{"dimensions":{"width":1008,"height":1008},"alt":null,"copyright":null,"url":"https://images.prismic.io/www-static/e2285fcfaf32ce7ec26329fe7e416ae896fbf991_portrait_2k18_bw_smallres.jpg?auto=compress,format"},"_meta":{"uid":"kamal-nasser"}},"blog_header_image":{"dimensions":{"width":1200,"height":900},"alt":null,"copyright":null,"url":"https://images.prismic.io/www-static/cfc9e5e6-a7f9-4565-8023-a8a94fd572fb_database-mostov_dribbble.png?auto=compress,format"},"blog_headline":[{"type":"heading1","text":"Creating a Simple Contacts List with Laravel and PostgreSQL","spans":[]}],"blog_post_content":[{"type":"paragraph","text":"In this post, we will build a simple Laravel app that displays a contacts list on a page. Using Eloquent and PostgreSQL's JSON object support, the app will query the database for the contacts and their details. This is what the result will look like:","spans":[]},{"type":"image","url":"https://images.prismic.io/www-static/03a96195-00d0-4aa8-8c94-d05375d015f6_Kamal-1.png?auto=compress,format","alt":null,"copyright":null,"dimensions":{"width":1600,"height":1396}},{"type":"paragraph","text":"By doing this, you will learn how to connect Laravel to a DigitalOcean Managed PostgreSQL database cluster, initialize the database with random data using Laravel factories and seeders, and store and read JSON documents in PostgreSQL using Laravel Eloquent.","spans":[{"start":58,"end":106,"type":"hyperlink","data":{"link_type":"Web","url":"https://blog.digitalocean.com/announcing-managed-databases-for-postgresql/"}}]},{"type":"heading2","text":"Requirements","spans":[]},{"type":"paragraph","text":"This post assumes that you have a working PHP development environment. You will need:","spans":[]},{"type":"list-item","text":"PHP 7.1+","spans":[]},{"type":"list-item","text":"Composer","spans":[{"start":0,"end":8,"type":"hyperlink","data":{"link_type":"Web","url":"https://getcomposer.org/doc/00-intro.md#installation-linux-unix-macos"}}]},{"type":"list-item","text":"Laravel’s required PHP extensions","spans":[{"start":10,"end":33,"type":"hyperlink","data":{"link_type":"Web","url":"https://laravel.com/docs/5.7/installation#server-requirements"}}]},{"type":"heading2","text":"Step 1: Create a Base Laravel App","spans":[]},{"type":"paragraph","text":"Let's start by creating a new blank Laravel app that will serve as a base for our web app. In a directory of your liking, generate a new project using Composer:","spans":[]},{"type":"preformatted","text":"```[php]{`","spans":[]},{"type":"paragraph","text":"    composer create-project --prefer-dist laravel/laravel laravel-contacts","spans":[]},{"type":"preformatted","text":"`}```","spans":[]},{"type":"paragraph","text":"If you browse into the newly-created `laravel-contacts` directory and run Laravel's built-in web server, you will see the default Laravel welcome page:","spans":[]},{"type":"preformatted","text":"```[php]{`","spans":[]},{"type":"paragraph","text":"    cd laravel-contacts","spans":[]},{"type":"paragraph","text":"    php artisan serve","spans":[]},{"type":"preformatted","text":"`}```","spans":[]},{"type":"image","url":"https://images.prismic.io/www-static/67fc2310-14e8-420c-b837-1c583b84708b_Kamal-2.png?auto=compress,format","alt":null,"copyright":null,"dimensions":{"width":1600,"height":869}},{"type":"paragraph","text":"Let's think about what our app will need. We want to have a list of contacts, so the database will store them. This means that we will need a ```[php]{`Contact`}``` model and a migration for the ```[php]{`contacts`}``` table. We'll also want to initialize the database with some random contacts so we have something to see, which will require a ```[php]{`Contact`}``` Factory and Seeder. ","spans":[]},{"type":"paragraph","text":"Let's build them out step by step:","spans":[]},{"type":"heading2","text":"Step 2: Create the Contact Model","spans":[]},{"type":"paragraph","text":"As shown in the screenshot above, a contact will have the following properties:","spans":[]},{"type":"list-item","text":"Name","spans":[]},{"type":"list-item","text":"Phone Number","spans":[]},{"type":"list-item","text":"Address","spans":[]},{"type":"list-item","text":"Favorite Colors","spans":[]},{"type":"paragraph","text":"For the favorite colors property, we will make use of Postgres's JSON data type. We will create a generic \"favorites\" column that will contain a JSON object with a list of favorite things. In this post we will store favorite colors only, but using a generic “favorites” object allows us to add different types in the future.","spans":[]},{"type":"paragraph","text":"Generate a model, factory, and migration using ```[php]{`artisan`}```:","spans":[]},{"type":"preformatted","text":"```[php]{`","spans":[]},{"type":"paragraph","text":"    php artisan make:model -f -m Contact","spans":[]},{"type":"preformatted","text":"`}```","spans":[]},{"type":"paragraph","text":"Now we'll configure the different parts of the model.","spans":[]},{"type":"heading3","text":"Migration","spans":[]},{"type":"paragraph","text":"Edit the generated migration file stored in ```[php]{`database/migrations/*_create_contacts_table.php`}```. Inside the ```[php]{`Schema::create()`}``` block, we will define the structure of the table:","spans":[]},{"type":"preformatted","text":"```[php]{`","spans":[]},{"type":"paragraph","text":"    <?php  ","spans":[]},{"type":"paragraph","text":"    use Illuminate\\Support\\Facades\\Schema;  ","spans":[]},{"type":"paragraph","text":"    use Illuminate\\Database\\Schema\\Blueprint;  ","spans":[]},{"type":"paragraph","text":"    use Illuminate\\Database\\Migrations\\Migration;  ","spans":[]},{"type":"paragraph","text":"    class CreateContactsTable extends Migration  ","spans":[]},{"type":"paragraph","text":"    {","spans":[]},{"type":"paragraph","text":"        /**","spans":[]},{"type":"paragraph","text":"          * Run the migrations.","spans":[]},{"type":"paragraph","text":"          *","spans":[]},{"type":"paragraph","text":"          * @return void","spans":[]},{"type":"paragraph","text":"          */","spans":[]},{"type":"paragraph","text":"        public function up()","spans":[]},{"type":"paragraph","text":"        {","spans":[]},{"type":"paragraph","text":"            Schema::create('contacts', function (Blueprint $table) {","spans":[]},{"type":"paragraph","text":"                $table->increments('id');","spans":[]},{"type":"paragraph","text":"                $table->string('name');","spans":[]},{"type":"paragraph","text":"                $table->string('phone');","spans":[]},{"type":"paragraph","text":"                $table->string('address');","spans":[]},{"type":"paragraph","text":"                $table->jsonb('favorites')->default('{}');","spans":[]},{"type":"paragraph","text":"                $table->timestamps();","spans":[]},{"type":"paragraph","text":"            });","spans":[]},{"type":"paragraph","text":"        }","spans":[]},{"type":"paragraph","text":"        /**","spans":[]},{"type":"paragraph","text":"            * Reverse the migrations.","spans":[]},{"type":"paragraph","text":"            *","spans":[]},{"type":"paragraph","text":"            * @return void","spans":[]},{"type":"paragraph","text":"            */","spans":[]},{"type":"paragraph","text":"        public function down()","spans":[]},{"type":"paragraph","text":"        {","spans":[]},{"type":"paragraph","text":"            Schema::dropIfExists('contacts');","spans":[]},{"type":"paragraph","text":"        }","spans":[]},{"type":"paragraph","text":"    }","spans":[]},{"type":"preformatted","text":"`}```","spans":[]},{"type":"paragraph","text":"This will configure the database column as described above with the `favorites` column set to a JSON object. The default value is an empty object.","spans":[]},{"type":"heading3","text":"Random Contact Generation","spans":[]},{"type":"paragraph","text":"The contact factory generates random values for a contact. We will configure a seeder that will use the factory to insert 10 random contacts to the database. This will provide us with an option to seed the database with random contacts when running the migration. Let's start with the factory.","spans":[]},{"type":"heading4","text":"Factory","spans":[]},{"type":"paragraph","text":"Edit the generated factory file stored in ```[php]{`database/factories/ContactFactory.php`}```. First, we will generate three random colors to use as favorites:","spans":[]},{"type":"preformatted","text":"```[php]{`","spans":[]},{"type":"paragraph","text":"    $colors = collect(range(1, 3))->map(function() use ($faker) {","spans":[]},{"type":"paragraph","text":"        return $faker->colorName;","spans":[]},{"type":"paragraph","text":"    })->toArray();","spans":[]},{"type":"preformatted","text":"`}```","spans":[]},{"type":"paragraph","text":"We will use these colors along with other data generated using the Faker library to return the contact's properties:","spans":[]},{"type":"preformatted","text":"```[php]{`","spans":[]},{"type":"paragraph","text":"    return [","spans":[]},{"type":"paragraph","text":"        'name' => $faker->name,","spans":[]},{"type":"paragraph","text":"        'phone' => $faker->e164PhoneNumber,","spans":[]},{"type":"paragraph","text":"        'address' => $faker->address,","spans":[]},{"type":"paragraph","text":"        'favorites' => ['colors' => $colors],","spans":[]},{"type":"paragraph","text":"    ];","spans":[]},{"type":"preformatted","text":"`}```","spans":[]},{"type":"paragraph","text":"The resulting factory should look like this:","spans":[]},{"type":"preformatted","text":"```[php]{`","spans":[]},{"type":"paragraph","text":"    <?php","spans":[]},{"type":"paragraph","text":"    ","spans":[]},{"type":"paragraph","text":"    use Faker\\Generator as Faker;","spans":[]},{"type":"paragraph","text":"    ","spans":[]},{"type":"paragraph","text":"    $factory->define(App\\Contact::class, function (Faker $faker) {","spans":[]},{"type":"paragraph","text":"        // generate 3 random colors","spans":[]},{"type":"paragraph","text":"        $colors = collect(range(1, 3))->map(function() use ($faker) {","spans":[]},{"type":"paragraph","text":"            return $faker->colorName;","spans":[]},{"type":"paragraph","text":"        })->toArray();","spans":[]},{"type":"paragraph","text":"    ","spans":[]},{"type":"paragraph","text":"        return [","spans":[]},{"type":"paragraph","text":"            'name' => $faker->name,","spans":[]},{"type":"paragraph","text":"            'phone' => $faker->e164PhoneNumber,","spans":[]},{"type":"paragraph","text":"            'address' => $faker->address,","spans":[]},{"type":"paragraph","text":"            'favorites' => ['colors' => $colors],","spans":[]},{"type":"paragraph","text":"        ];","spans":[]},{"type":"paragraph","text":"    });","spans":[]},{"type":"preformatted","text":"`}```","spans":[]},{"type":"heading4","text":"Seeder","spans":[]},{"type":"paragraph","text":"Now we need to define a seeder that will be run upon migrating the database. Start by generating the file:","spans":[]},{"type":"paragraph","text":"    ```[php]{`php artisan make:seeder ContactsTableSeeder`}```","spans":[]},{"type":"paragraph","text":"Open the generated file ```[php]{`database/seeds/ContactsTableSeeder.php`}``` in an editor and call the factory inside the ```[php]{`run()`}``` function like so:","spans":[]},{"type":"preformatted","text":"```[php]{`","spans":[]},{"type":"paragraph","text":"    <?php","spans":[]},{"type":"paragraph","text":"    use Illuminate\\Database\\Seeder;","spans":[]},{"type":"paragraph","text":"    class ContactsTableSeeder extends Seeder","spans":[]},{"type":"paragraph","text":"    {","spans":[]},{"type":"paragraph","text":"        /**","spans":[]},{"type":"paragraph","text":"         * Run the database seeds.","spans":[]},{"type":"paragraph","text":"         *","spans":[]},{"type":"paragraph","text":"         * @return void","spans":[]},{"type":"paragraph","text":"         */","spans":[]},{"type":"paragraph","text":"        public function run()","spans":[]},{"type":"paragraph","text":"        {","spans":[]},{"type":"paragraph","text":"            factory(App\\Contact::class, 10)->create();","spans":[]},{"type":"paragraph","text":"        }","spans":[]},{"type":"paragraph","text":"    }","spans":[]},{"type":"preformatted","text":"`}```","spans":[]},{"type":"paragraph","text":"To enable the seeder, edit `database/seeds/DatabaseSeeder.php` and call it inside the `run()` function:","spans":[]},{"type":"preformatted","text":"```[php]{`","spans":[]},{"type":"paragraph","text":"    <?php","spans":[]},{"type":"paragraph","text":"    use Illuminate\\Database\\Seeder;","spans":[]},{"type":"paragraph","text":"    class DatabaseSeeder extends Seeder","spans":[]},{"type":"paragraph","text":"    {","spans":[]},{"type":"paragraph","text":"        /**","spans":[]},{"type":"paragraph","text":"         * Seed the application's database.","spans":[]},{"type":"paragraph","text":"         *","spans":[]},{"type":"paragraph","text":"         * @return void","spans":[]},{"type":"paragraph","text":"         */","spans":[]},{"type":"paragraph","text":"        public function run()","spans":[]},{"type":"paragraph","text":"        {","spans":[]},{"type":"paragraph","text":"            $this->call(ContactsTableSeeder::class);","spans":[]},{"type":"paragraph","text":"        }","spans":[]},{"type":"paragraph","text":"    }","spans":[]},{"type":"preformatted","text":"`}```","spans":[]},{"type":"heading3","text":"Model","spans":[]},{"type":"paragraph","text":"There is one final adjustment that we need to make to the model. The ```[php]{`artisan make:model`}``` command generated a model file for us, located in ```[php]{`app/Contact.php`}```. The `favorites` column represents a JSON object, so we need to cast it to a PHP array before using it. Eloquent makes this very easy by automatically casting back and forth between the correct formats. Inside the ```[php]{`Contact`}``` class in ```[php]{`app/Contact.php`}```, we can add the cast:","spans":[]},{"type":"preformatted","text":"```[php]{`","spans":[]},{"type":"paragraph","text":"    <?php","spans":[]},{"type":"paragraph","text":"    namespace App;","spans":[]},{"type":"paragraph","text":"    use Illuminate\\Database\\Eloquent\\Model;","spans":[]},{"type":"paragraph","text":"    class Contact extends Model","spans":[]},{"type":"paragraph","text":"    {","spans":[]},{"type":"paragraph","text":"        protected $casts = [","spans":[]},{"type":"paragraph","text":"            'favorites' => 'array',","spans":[]},{"type":"paragraph","text":"        ];","spans":[]},{"type":"paragraph","text":"    }","spans":[]},{"type":"preformatted","text":"`}```","spans":[]},{"type":"heading2","text":"Step 3: Configure the Database","spans":[]},{"type":"paragraph","text":"We will use DigitalOcean Databases for our PostgreSQL cluster. If you haven’t yet, create a new one—it only takes a few minutes. If you prefer a text post, see the product documentation for Databases. If you prefer a video, click here.","spans":[{"start":160,"end":199,"type":"hyperlink","data":{"link_type":"Web","url":"https://www.digitalocean.com/docs/databases/how-to/clusters/create/"}},{"start":224,"end":234,"type":"hyperlink","data":{"link_type":"Web","url":"https://www.youtube.com/watch?v=jY5FhyiEdig"}}]},{"type":"image","url":"https://images.prismic.io/www-static/e80ddc5c-b52d-42a0-a6b9-faf3e0a10a51_Kamal-3.png?auto=compress,format","alt":null,"copyright":null,"dimensions":{"width":1600,"height":427}},{"type":"paragraph","text":"In the cluster’s Overview page in the control panel, get its connection details as “Connection parameters”. Open Laravel's ```[php]{`.env`}``` file and set ```[php]{`DB_CONNECTION=pgsql`}```. Below it, set all the other variables according to your connection credentials.","spans":[]},{"type":"image","url":"https://images.prismic.io/www-static/3f4ec974-28d2-46ca-80b3-18a8461abca2_Kamal-4.png?auto=compress,format","alt":null,"copyright":null,"dimensions":{"width":990,"height":540}},{"type":"paragraph","text":"There is one setting that isn’t available as an environment variable: ```[php]{`sslmode`}```. DigitalOcean Databases do not support non-TLS connections so we need to set ```[php]{`sslmode`}``` to ```[php]{`require`}```. Open ```[php]{`config/database.php`}``` in an editor, scroll down to the ```[php]{`pgsql`}``` definition, and update the setting like so:","spans":[]},{"type":"preformatted","text":"```[php]{`","spans":[]},{"type":"paragraph","text":"    'pgsql' => [","spans":[]},{"type":"paragraph","text":"        ...","spans":[]},{"type":"paragraph","text":"        'sslmode' => 'require',","spans":[]},{"type":"paragraph","text":"    ],","spans":[]},{"type":"preformatted","text":"`}```","spans":[]},{"type":"paragraph","text":"[Related: Check out our Resource Center for resources and guides on Managed Databases]","spans":[{"start":0,"end":86,"type":"hyperlink","data":{"link_type":"Web","url":"https://www.digitalocean.com/resources/managed-databases/"}}]},{"type":"heading2","text":"Step 4: Migrate and seed","spans":[]},{"type":"paragraph","text":"Now that we have completed defining everything database-related in our app, from the Contact model to the database connection info, we can execute the migration and seed the database:","spans":[]},{"type":"preformatted","text":"```[php]{`","spans":[]},{"type":"paragraph","text":"    php artisan migrate --seed","spans":[]},{"type":"preformatted","text":"`}```","spans":[]},{"type":"paragraph","text":"We have a database filled with contacts now—nice!  ","spans":[]},{"type":"heading2","text":"Step 4: Build the Contact List Page","spans":[]},{"type":"paragraph","text":"Ok, let's finish up by creating a page to show our contacts.","spans":[]},{"type":"paragraph","text":"To keep things simple we will replace Laravel's default home page. Edit `routes/web.php` and set the `/` route to the following:","spans":[]},{"type":"preformatted","text":"```[php]{`","spans":[]},{"type":"paragraph","text":"    Route::get('/', function () {","spans":[]},{"type":"paragraph","text":"        $contacts = App\\Contact::all();","spans":[]},{"type":"paragraph","text":"        return view('welcome', ['contacts' => $contacts]);","spans":[]},{"type":"paragraph","text":"    });","spans":[]},{"type":"preformatted","text":"`}```","spans":[]},{"type":"paragraph","text":"This will fetch all the contacts from the database and pass them to the ```[php]{`welcome`}``` view.","spans":[]},{"type":"paragraph","text":"Then, edit the view located in `resources/views/welcome.blade.php` and replace its contents with the following:","spans":[]},{"type":"preformatted","text":"```[php]{`","spans":[]},{"type":"paragraph","text":"    <!doctype html>","spans":[]},{"type":"paragraph","text":"    <html>","spans":[]},{"type":"paragraph","text":"        <head>","spans":[]},{"type":"paragraph","text":"            <meta charset=\"utf-8\">","spans":[]},{"type":"paragraph","text":"            <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">","spans":[]},{"type":"paragraph","text":"    ","spans":[]},{"type":"paragraph","text":"            <title>Contacts</title>","spans":[]},{"type":"paragraph","text":"            <link rel=\"stylesheet\" href=\"https://unpkg.com/tachyons@4.10.0/css/tachyons.min.css\"/>","spans":[]},{"type":"paragraph","text":"        </head>","spans":[]},{"type":"paragraph","text":"        <body>","spans":[]},{"type":"paragraph","text":"            <div class=\"mw6 center pa3 sans-serif\">","spans":[]},{"type":"paragraph","text":"                <h1 class=\"mb4\">Contacts</h1>","spans":[]},{"type":"paragraph","text":"    ","spans":[]},{"type":"paragraph","text":"                @foreach($contacts as $contact)","spans":[]},{"type":"paragraph","text":"                <div class=\"pa2 mb3 striped--near-white\">","spans":[]},{"type":"paragraph","text":"                    <header class=\"b mb2\">{{ $contact->name }}</header>","spans":[]},{"type":"paragraph","text":"                    <div class=\"pl2\">","spans":[]},{"type":"paragraph","text":"                        <p class=\"mb2\">{{ $contact->phone }}</p>","spans":[]},{"type":"paragraph","text":"                        <p class=\"pre mb3\">{{ $contact->address }}</p>","spans":[]},{"type":"paragraph","text":"                        <p class=\"mb2\"><span class=\"fw5\">Favorite colors:</span> {{ implode(', ', $contact->favorites['colors']) }}</p>","spans":[]},{"type":"paragraph","text":"                    </div>","spans":[]},{"type":"paragraph","text":"                </div>","spans":[]},{"type":"paragraph","text":"                @endforeach","spans":[]},{"type":"paragraph","text":"            </div>","spans":[]},{"type":"paragraph","text":"        </body>","spans":[]},{"type":"paragraph","text":"    </html>","spans":[]},{"type":"preformatted","text":"`}```","spans":[]},{"type":"heading2","text":"Step 5: Deploy","spans":[]},{"type":"paragraph","text":"We're all done! Let's go ahead and try out our app.","spans":[]},{"type":"paragraph","text":"Like before, we can use Laravel's built-in web server. Simply run ```[php]{`php artisan serve`}``` and browse to http://127.0.0.1:8000.","spans":[{"start":113,"end":134,"type":"hyperlink","data":{"link_type":"Web","url":"http://127.0.0.1:8000/"}}]},{"type":"heading3","text":"Deploy on a Droplet","spans":[]},{"type":"paragraph","text":"To deploy the app on a Droplet, follow the How To Deploy a Laravel Application with Nginx on Ubuntu 16.04 guide on the DigitalOcean community.","spans":[{"start":43,"end":105,"type":"hyperlink","data":{"link_type":"Web","url":"https://www.digitalocean.com/community/tutorials/how-to-deploy-a-laravel-application-with-nginx-on-ubuntu-16-04"}}]},{"type":"heading2","text":"Conclusion","spans":[]},{"type":"paragraph","text":"In this post, we went through building a simple contacts list step by step and explored a few concepts such as seeding the database with sample random data, using JSON-type database fields, and connecting to a managed PostgreSQL Database.","spans":[]},{"type":"paragraph","text":"Here are a few things you can do following this post, building on the app we created:","spans":[]},{"type":"list-item","text":"Add a favorite shape (square, circle, etc.) to one or more contacts and edit the view template to display it. Update the seeder to include a random shape at random, or for every contact.","spans":[]},{"type":"list-item","text":"Extract the route handler (in ```[php]{`web.php`}```) into its own controller. Use ```[php]{`artisan make:controller`}``` to create the controller file. Consult the Laravel documentation on controllers for more details.","spans":[]},{"type":"list-item","text":"List the contacts in the order that they were last updated, most recent first.","spans":[]},{"type":"paragraph","text":"[Hungry for another tutorial? Try Kamal's guide to \"Deploying a Fully-automated Git-based Static Website in Under 5 Minutes\"]","spans":[{"start":0,"end":125,"type":"hyperlink","data":{"link_type":"Web","url":"https://blog.digitalocean.com/deploying-a-fully-automated-git-based-static-website-in-under-5-minutes/"}}]},{"type":"paragraph","text":"Kamal Nasser is a Developer Advocate at DigitalOcean. He is also a Computer Science student with a passion for software engineering and avocados. You can find him on Twitter @kamaln7.","spans":[{"start":0,"end":183,"type":"em"},{"start":174,"end":182,"type":"hyperlink","data":{"link_type":"Web","url":"https://twitter.com/kamaln7"}}]}],"blog_post_date":"2019-02-21","tags":[{"tag1":{"tag":"Developer Relations","_linkType":"Link.document","_meta":{"uid":"developer-relations"}}},{"tag1":{"tag":"Community","_linkType":"Link.document","_meta":{"uid":"community"}}}],"_meta":{"uid":"create-simple-contacts-laravel-postgresql"}}},{"node":{"author":{"_linkType":"Link.document","author_name":"Kamal Nasser","author_image":{"dimensions":{"width":1008,"height":1008},"alt":null,"copyright":null,"url":"https://images.prismic.io/www-static/e2285fcfaf32ce7ec26329fe7e416ae896fbf991_portrait_2k18_bw_smallres.jpg?auto=compress,format"},"_meta":{"uid":"kamal-nasser"}},"blog_header_image":{"dimensions":{"width":784,"height":418},"alt":"Fully-automated Git-based Static Website","copyright":null,"url":"https://images.prismic.io/www-static/0aaec289-b9b2-4d3f-8dfc-0d1d2bdb349a_www_under_5_blog.png?auto=compress,format"},"blog_headline":[{"type":"heading1","text":"Deploying a Fully-automated Git-based Static Website in Under 5 Minutes","spans":[]}],"blog_post_content":[{"type":"paragraph","text":"Sometimes you simply want to get a static website up and running as quickly as possible, whether it be the actual website, a placeholder, or a basic landing page. Recently I started using Caddy, a modern web-server focused on simplicity and security. It includes native support for Git and Let's Encrypt thanks to its plugin-based architecture. ","spans":[]},{"type":"paragraph","text":"RELATED: Implementing HTTPS for Chrome Users","spans":[{"start":0,"end":44,"type":"strong"},{"start":9,"end":44,"type":"hyperlink","data":{"link_type":"Web","url":"https://blog.digitalocean.com/implementing-https-for-chrome-users/"}}]},{"type":"paragraph","text":"I love how easy-to-use Caddy is, and I wanted to share a tutorial about how we can deploy a static website synced with a Git repository in under 5 minutes—all on a Droplet that spins up in 55 seconds.","spans":[]},{"type":"heading4","text":"Prerequisites","spans":[]},{"type":"o-list-item","text":"An Ubuntu 16.04 server configured according to our Initial Server Setup guide. While the goal is to get a website up quickly, we don't want to skimp on security. The guide will set up a secure environment for Caddy. Skip step 7 of the guide as we will be using a Cloud Firewall instead of a software firewall installed on the Droplet.","spans":[{"start":51,"end":77,"type":"hyperlink","data":{"link_type":"Web","url":"https://www.digitalocean.com/community/tutorials/initial-server-setup-with-ubuntu-16-04"}},{"start":263,"end":277,"type":"hyperlink","data":{"link_type":"Web","url":"https://www.digitalocean.com/products/cloud-firewalls/"}}]},{"type":"o-list-item","text":"A domain name you own, to be used for the website.","spans":[]},{"type":"heading4","text":"Step 1 — Install Caddy","spans":[]},{"type":"paragraph","text":"Caddy provides pre-built binaries on its website. Download Caddy on your Droplet:","spans":[{"start":37,"end":48,"type":"hyperlink","data":{"link_type":"Web","url":"https://caddyserver.com/download"}}]},{"type":"preformatted","text":"```[php]{`","spans":[]},{"type":"paragraph","text":"    wget -O caddy.tar.gz \"https://caddyserver.com/download/linux/amd64?plugins=http.git&license=personal\"  ","spans":[]},{"type":"preformatted","text":"`}```","spans":[]},{"type":"paragraph","text":"This command will download a Caddy binary with the following settings:","spans":[]},{"type":"paragraph","text":"Platform: Linux 64-bit ","spans":[]},{"type":"paragraph","text":"Plugins: http.git ","spans":[]},{"type":"paragraph","text":"License: Personal","spans":[]},{"type":"paragraph","text":"Keep in mind that the personal license is available for non-commercial use only.","spans":[]},{"type":"paragraph","text":"Extract the downloaded archive into a new directory and `cd` into it:","spans":[]},{"type":"preformatted","text":"```[php]{`","spans":[]},{"type":"paragraph","text":"    mkdir caddy  ","spans":[]},{"type":"paragraph","text":"    tar vxf caddy.tar.gz -C caddy  ","spans":[]},{"type":"paragraph","text":"    cd caddy  ","spans":[]},{"type":"preformatted","text":"`}```","spans":[]},{"type":"paragraph","text":"The archive contains the Caddy binary and a Systemd service file. We will use both in this guide. First, install the binary:","spans":[]},{"type":"preformatted","text":"```[php]{`","spans":[]},{"type":"paragraph","text":"    sudo cp caddy /usr/local/bin  ","spans":[]},{"type":"paragraph","text":"    sudo chown root:root /usr/local/bin/caddy  ","spans":[]},{"type":"paragraph","text":"    sudo chmod 755 /usr/local/bin/caddy  ","spans":[]},{"type":"preformatted","text":"`}```","spans":[]},{"type":"paragraph","text":"Because Caddy will serve as our front-facing web server, it will need to be able to listen on ports 80 and 443. Linux requires binaries to be run as root in order to listen on any port under 1024. It is however possible to allow specific binaries to do so without full root privileges:","spans":[]},{"type":"preformatted","text":"```[php]{`","spans":[]},{"type":"paragraph","text":"    sudo setcap 'cap_net_bind_service=+ep' /usr/local/bin/caddy  ","spans":[]},{"type":"preformatted","text":"`}```","spans":[]},{"type":"paragraph","text":"Then, create Caddy's configuration directories and set proper permissions:","spans":[]},{"type":"preformatted","text":"```[php]{`","spans":[]},{"type":"paragraph","text":"    sudo mkdir /etc/caddy  ","spans":[]},{"type":"paragraph","text":"    sudo chown -R root:www-data /etc/caddy  ","spans":[]},{"type":"paragraph","text":"    sudo mkdir /etc/ssl/caddy  ","spans":[]},{"type":"paragraph","text":"    sudo chown -R www-data:root /etc/ssl/caddy  ","spans":[]},{"type":"paragraph","text":"    sudo chmod 0770 /etc/ssl/caddy  ","spans":[]},{"type":"preformatted","text":"`}```","spans":[]},{"type":"paragraph","text":"Finally, install the Systemd service file:","spans":[]},{"type":"preformatted","text":"```[php]{`","spans":[]},{"type":"paragraph","text":"    sudo cp init/linux-systemd/caddy.service /etc/systemd/system/  ","spans":[]},{"type":"paragraph","text":"    sudo chown root:root /etc/systemd/system/caddy.service  ","spans":[]},{"type":"paragraph","text":"    sudo chmod 644 /etc/systemd/system/caddy.service  ","spans":[]},{"type":"paragraph","text":"    sudo systemctl daemon-reload  ","spans":[]},{"type":"preformatted","text":"`}```","spans":[]},{"type":"heading4","text":"Step 2 — Configure DNS","spans":[]},{"type":"paragraph","text":"Before configuring and starting Caddy we want to set up DNS so that Caddy is able to issue an SSL certificate via Let's Encrypt.","spans":[]},{"type":"paragraph","text":"Add your domain name in the Domains page. We will create two DNS records pointing to the Droplet: one for IPv4 and one for IPv6.","spans":[{"start":28,"end":35,"type":"hyperlink","data":{"link_type":"Web","url":"https://cloud.digitalocean.com/networking/domains"}}]},{"type":"paragraph","text":"The first will be of type A. In the hostname field, enter `@`. Select your Droplet in the Will Direct To field and add the record. The second record will have the same settings but with type AAAA.","spans":[]},{"type":"heading4","text":"Step 3 — Configure Caddy","spans":[]},{"type":"paragraph","text":"For the purposes of this guide, we will use the following example website: https://github.com/kamaln7/basic-static-website","spans":[{"start":75,"end":122,"type":"hyperlink","data":{"link_type":"Web","url":"https://github.com/kamaln7/basic-static-website"}}]},{"type":"paragraph","text":"Caddy's configuration file will be located in `/etc/caddy/Caddyfile`. Open the file in a text editor (nano, vim, etc.) and enter the following:  ","spans":[]},{"type":"preformatted","text":"```[php]{`","spans":[]},{"type":"paragraph","text":"    example.com {  ","spans":[]},{"type":"paragraph","text":"        tls you@example.com","spans":[]},{"type":"paragraph","text":"        internal /.git","spans":[]},{"type":"paragraph","text":"        git https://github.com/kamaln7/basic-static-website.git {","spans":[]},{"type":"paragraph","text":"            interval 300","spans":[]},{"type":"paragraph","text":"        }","spans":[]},{"type":"paragraph","text":"        gzip","spans":[]},{"type":"paragraph","text":"        redir 301 {","spans":[]},{"type":"paragraph","text":"            if {scheme} is http","spans":[]},{"type":"paragraph","text":"            /  https://{host}{uri}","spans":[]},{"type":"paragraph","text":"        }","spans":[]},{"type":"paragraph","text":"    }","spans":[]},{"type":"preformatted","text":"`}```","spans":[]},{"type":"paragraph","text":"Replace `example.com` with your domain name and `you@example.com` with your email address. This email address will be used to issue a Let's Encrypt certificate for your domain so make sure to enter a valid one that you have access to.","spans":[]},{"type":"paragraph","text":"This configures basic sane defaults: gzip compression will be used when suitable and all HTTP traffic will be redirected to HTTPS.","spans":[]},{"type":"paragraph","text":"The main piece of this configuration is the `git` block. This will configure Caddy to use the Git repository’s contents as the website's files, checking for updates every 5 minutes.","spans":[]},{"type":"heading4","text":"Step 4 — Start Caddy","spans":[]},{"type":"paragraph","text":"Start Caddy, and enable it to start on boot:","spans":[]},{"type":"preformatted","text":"```[php]{`","spans":[]},{"type":"paragraph","text":"    sudo systemctl start caddy  ","spans":[]},{"type":"paragraph","text":"    sudo systemctl enable caddy","spans":[]},{"type":"preformatted","text":"`}```","spans":[]},{"type":"paragraph","text":"It might take a few seconds for Caddy to receive the certificate from Let's Encrypt and clone your repository, but you should now be able to browse to your domain name and see your website.","spans":[]},{"type":"paragraph","text":"Now, any changes you make in your Git repository will be automatically applied.","spans":[]},{"type":"heading4","text":"Step 5 — Configure a Firewall","spans":[]},{"type":"paragraph","text":"DigitalOcean Cloud Firewalls make it very easy to configure a secure firewall. Browse to the Firewalls page and click on the Create Firewall button. If you already have one or more Firewalls on your account, you can access the Create Firewall page through the Create menu at the top of the page.","spans":[{"start":93,"end":107,"type":"hyperlink","data":{"link_type":"Web","url":"https://cloud.digitalocean.com/networking/firewalls"}}]},{"type":"paragraph","text":"For the inbound rules, we will allow SSH, HTTP, and HTTPS traffic. Keep the outbound rules as is. Select your Droplet and create the firewall. ","spans":[]},{"type":"paragraph","text":"For further instructions on Cloud Firewalls, see our tutorial How To Create Your First DigitalOcean Cloud Firewall.","spans":[{"start":62,"end":114,"type":"hyperlink","data":{"link_type":"Web","url":"https://www.digitalocean.com/community/tutorials/how-to-create-your-first-digitalocean-cloud-firewall"}}]},{"type":"heading4","text":"Optional: Step 6 — Enable Instant Deployments using Webhooks","spans":[]},{"type":"paragraph","text":"Caddy will check the Git repository for changes every five minutes by default. While you can set the interval to a lower value, a better solution would be to configure GitHub to push any changes to Caddy instead. This will allow for near-instant updates.","spans":[]},{"type":"paragraph","text":"Caddy makes this process very easy as well. The webhook will require a secret—you can use anything you want. The `uuidgen` program is a convenient tool that allows you to easily generate a random secure string. Simply run `uuidgen` and copy its output.","spans":[]},{"type":"paragraph","text":"Edit `Caddyfile` and add the following line inside the Git block, replacing secret with your secret:","spans":[]},{"type":"preformatted","text":"```[php]{`","spans":[]},{"type":"paragraph","text":"    hook /github_hook secret  ","spans":[]},{"type":"preformatted","text":"`}```","spans":[]},{"type":"paragraph","text":"Restart Caddy to apply the changes:","spans":[]},{"type":"preformatted","text":"```[php]{`","spans":[]},{"type":"paragraph","text":"    sudo systemctl restart caddy  ","spans":[]},{"type":"preformatted","text":"`}```","spans":[]},{"type":"paragraph","text":"Then, configure Github to use the new webhook endpoint: browse to your repository's settings page and click on Webhooks. Add a new webhook and set the Payload URL to `https://domain.com/github_hook`. Set the the Content type to `application/json` and enter your secret and click on Add Webhook.","spans":[]},{"type":"paragraph","text":"Now, whenever you push a change to your Git repository, it will be reflected on your website in seconds. For instance, if you are using the example website mentioned above, go ahead and change the highlight color to blue by replacing `b--gold` with `b--blue`. Commit the updated file and reload the page!","spans":[{"start":140,"end":155,"type":"hyperlink","data":{"link_type":"Web","url":"https://github.com/kamaln7/basic-static-website"}}]},{"type":"heading4","text":"Conclusion","spans":[]},{"type":"paragraph","text":"By following this guide, you will have deployed a fully-automated low-maintenance website using modern technologies such as HTTP/2 and Let's Encrypt. Caddy is a versatile web server and supports many great features such as on-the-fly Markdown rendering. You can find a list of plugins and features on the Features page on its website. Browse through the documentation to see what features you might want to enable and how to do so.","spans":[{"start":305,"end":318,"type":"hyperlink","data":{"link_type":"Web","url":"https://caddyserver.com/features"}},{"start":350,"end":367,"type":"hyperlink","data":{"link_type":"Web","url":"https://caddyserver.com/docs"}}]},{"type":"paragraph","text":"Kamal Nasser is a Developer Advocate at DigitalOcean. He is also a Computer Science student with a passion for software engineering and avocados. You can find him on Twitter @kamaln7.","spans":[{"start":0,"end":183,"type":"em"},{"start":174,"end":182,"type":"hyperlink","data":{"link_type":"Web","url":"https://twitter.com/kamaln7"}}]}],"blog_post_date":"2018-08-08","tags":[{"tag1":{"tag":"Community","_linkType":"Link.document","_meta":{"uid":"community"}}}],"_meta":{"uid":"deploying-a-fully-automated-git-based-static-website-in-under-5-minutes"}}}]}}}