\n{/if}\n",[171,4029,4030,4054,4111,4166],{"__ignoreMap":240},[321,4031,4032,4034,4037,4040,4043,4045,4047,4050,4052],{"class":323,"line":324},[321,4033,2477],{"class":355},[321,4035,4036],{"class":348},"#",[321,4038,4039],{"class":344},"if",[321,4041,4042],{"class":348}," setting",[321,4044,356],{"class":384},[321,4046,1841],{"class":348},[321,4048,4049],{"class":384},"===",[321,4051,3940],{"class":431},[321,4053,555],{"class":355},[321,4055,4056,4059,4063,4067,4069,4071,4074,4076,4079,4081,4084,4086,4088,4090,4092,4094,4096,4098,4101,4103,4106,4108],{"class":323,"line":241},[321,4057,4058],{"class":384}," \u003C",[321,4060,4062],{"class":4061},"s_ZFR","Label",[321,4064,4066],{"class":4065},"swja1"," for",[321,4068,818],{"class":384},[321,4070,2477],{"class":355},[321,4072,4073],{"class":348},"setting",[321,4075,356],{"class":384},[321,4077,4078],{"class":348},"label",[321,4080,356],{"class":384},[321,4082,4083],{"class":348},"htmlFor",[321,4085,1584],{"class":355},[321,4087,3896],{"class":384},[321,4089,2477],{"class":355},[321,4091,4073],{"class":348},[321,4093,356],{"class":384},[321,4095,4078],{"class":348},[321,4097,356],{"class":384},[321,4099,4100],{"class":348},"value",[321,4102,1584],{"class":355},[321,4104,4105],{"class":384},"\u003C/",[321,4107,4062],{"class":4061},[321,4109,4110],{"class":384},">\n",[321,4112,4113,4115,4118,4121,4123,4125,4127,4129,4132,4134,4137,4139,4141,4143,4145,4147,4149,4151,4154,4156,4158,4161,4163],{"class":323,"line":248},[321,4114,4058],{"class":384},[321,4116,4117],{"class":4061},"Input",[321,4119,4120],{"class":4065}," id",[321,4122,818],{"class":384},[321,4124,2477],{"class":355},[321,4126,4073],{"class":348},[321,4128,356],{"class":384},[321,4130,4131],{"class":348},"name",[321,4133,1584],{"class":355},[321,4135,4136],{"class":4065}," name",[321,4138,818],{"class":384},[321,4140,2477],{"class":355},[321,4142,4073],{"class":348},[321,4144,356],{"class":384},[321,4146,4131],{"class":348},[321,4148,1584],{"class":355},[321,4150,3851],{"class":355},[321,4152,4153],{"class":384},"...",[321,4155,4073],{"class":348},[321,4157,356],{"class":384},[321,4159,4160],{"class":348},"settings",[321,4162,1584],{"class":355},[321,4164,4165],{"class":384}," />\n",[321,4167,4168,4170,4173,4175],{"class":323,"line":341},[321,4169,2477],{"class":355},[321,4171,4172],{"class":384},"/",[321,4174,4039],{"class":344},[321,4176,555],{"class":355},[17,4178,4179],{},"If you ever need to do more field types, just add them as available types for each setting element and modify the if condition (hoping someday to have a switch statement in svelte - or pattern matching).",[105,4181,4183],{"id":4182},"handling-submission","Handling submission",[17,4185,4186,4187,4190,4191,4196],{},"This next step is probably the easiest. Since you're also sending the select platform ",[230,4188,4189],{},"template",", you can reference that to determine if the data is valid (why not try ",[34,4192,4195],{"href":4193,"rel":4194},"https://superforms.rocks",[38],"superforms","? I made an adapter for it).",[17,4198,4199],{},"With no validation whatsoever, it could look something like this:",[313,4201,4203],{"className":1387,"code":4202,"language":1389,"meta":240,"style":240},"const formData = await request.formData();\nconst { publisher_id, publisher_name, ...args } = Object.fromEntries(formData);\nawait db.insert(userPublications).values({\n publisherName: publisher_id,\n publisherData: args,\n name: publisher_name,\n userId: user.id\n});\nreturn { message: 'ok' };\n",[171,4204,4205,4230,4268,4293,4304,4316,4326,4340,4348],{"__ignoreMap":240},[321,4206,4207,4210,4213,4215,4218,4221,4223,4226,4228],{"class":323,"line":324},[321,4208,4209],{"class":344},"const",[321,4211,4212],{"class":348}," formData ",[321,4214,818],{"class":384},[321,4216,4217],{"class":344}," await",[321,4219,4220],{"class":348}," request",[321,4222,356],{"class":384},[321,4224,4225],{"class":373},"formData",[321,4227,1455],{"class":348},[321,4229,1404],{"class":355},[321,4231,4232,4234,4236,4239,4241,4244,4246,4248,4251,4253,4255,4258,4260,4263,4266],{"class":323,"line":241},[321,4233,4209],{"class":344},[321,4235,3851],{"class":355},[321,4237,4238],{"class":348}," publisher_id",[321,4240,407],{"class":355},[321,4242,4243],{"class":348}," publisher_name",[321,4245,407],{"class":355},[321,4247,2490],{"class":384},[321,4249,4250],{"class":348},"args ",[321,4252,1584],{"class":355},[321,4254,2573],{"class":384},[321,4256,4257],{"class":348}," Object",[321,4259,356],{"class":384},[321,4261,4262],{"class":373},"fromEntries",[321,4264,4265],{"class":348},"(formData)",[321,4267,1404],{"class":355},[321,4269,4270,4273,4276,4278,4281,4284,4286,4289,4291],{"class":323,"line":248},[321,4271,4272],{"class":344},"await",[321,4274,4275],{"class":348}," db",[321,4277,356],{"class":384},[321,4279,4280],{"class":373},"insert",[321,4282,4283],{"class":348},"(userPublications)",[321,4285,356],{"class":384},[321,4287,4288],{"class":373},"values",[321,4290,377],{"class":348},[321,4292,1732],{"class":355},[321,4294,4295,4298,4300,4302],{"class":323,"line":341},[321,4296,4297],{"class":348}," publisherName",[321,4299,1350],{"class":384},[321,4301,4238],{"class":348},[321,4303,3943],{"class":355},[321,4305,4306,4309,4311,4314],{"class":323,"line":362},[321,4307,4308],{"class":348}," publisherData",[321,4310,1350],{"class":384},[321,4312,4313],{"class":348}," args",[321,4315,3943],{"class":355},[321,4317,4318,4320,4322,4324],{"class":323,"line":367},[321,4319,2667],{"class":348},[321,4321,1350],{"class":384},[321,4323,4243],{"class":348},[321,4325,3943],{"class":355},[321,4327,4328,4331,4333,4336,4338],{"class":323,"line":401},[321,4329,4330],{"class":348}," userId",[321,4332,1350],{"class":384},[321,4334,4335],{"class":348}," user",[321,4337,356],{"class":384},[321,4339,1943],{"class":348},[321,4341,4342,4344,4346],{"class":323,"line":438},[321,4343,1584],{"class":355},[321,4345,238],{"class":348},[321,4347,1404],{"class":355},[321,4349,4350,4353,4355,4358,4360,4363],{"class":323,"line":455},[321,4351,4352],{"class":344},"return",[321,4354,3851],{"class":355},[321,4356,4357],{"class":348}," message",[321,4359,1350],{"class":384},[321,4361,4362],{"class":431}," 'ok'",[321,4364,3871],{"class":355},[1167,4366,4367],{},"html pre.shiki code .sIF4r, html code.shiki .sIF4r{--shiki-default:#C6A0F6}html pre.shiki code .s80kZ, html code.shiki .s80kZ{--shiki-default:#EED49F;--shiki-default-font-style:italic}html pre.shiki code .sXptk, html code.shiki .sXptk{--shiki-default:#8BD5CA}html pre.shiki code .slVFb, html code.shiki .slVFb{--shiki-default:#939AB7}html pre.shiki code .siJkK, html code.shiki .siJkK{--shiki-default:#CAD3F5;--shiki-default-font-style:italic}html pre.shiki code .sjARh, html code.shiki .sjARh{--shiki-default:#91D7E3}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sFaBz, html code.shiki .sFaBz{--shiki-default:#CAD3F5}html pre.shiki code .sSZ1V, html code.shiki .sSZ1V{--shiki-default:#A6DA95}html pre.shiki code .s7Qn8, html code.shiki .s7Qn8{--shiki-default:#F5A97F}html pre.shiki code .s_ZFR, html code.shiki .s_ZFR{--shiki-default:#F5BDE6}html pre.shiki code .swja1, html code.shiki .swja1{--shiki-default:#EED49F}html pre.shiki code .siK8c, html code.shiki .siK8c{--shiki-default:#8AADF4;--shiki-default-font-style:italic}",{"title":240,"searchDepth":241,"depth":241,"links":4369},[4370],{"id":3784,"depth":241,"text":3785,"children":4371},[4372,4373,4374],{"id":3795,"depth":248,"text":3796},{"id":4020,"depth":248,"text":4021},{"id":4182,"depth":248,"text":4183},"In the quest to make magiedit always better, I figured out that people may want to publish to multiple \"accounts\" on the same platform. So, the goal for the next feature was to allow just that. A user should be able to select the platform they want to create a Publisher for, fill in the required data, and be able to publish to it.\nThe problem was in the second to last step; the form. How do you handle the fact that different platforms require different authenticating elements to send requests?",{"cover_image":258},"/articles/building-a-dynamic-form-with-svelte-and-typescript","2024-02-23T00:00:00.000Z","---\ntitle: \"Building a dynamic form with Svelte and Typescript\"\nseries: \"magiedit\"\ncover_image: ./images/og.png\nsummary: \"generate forms based on user input with sveltekit and some typescript magic\"\npublishDate: 2024-02-23\nslug: building-a-dynamic-form-with-svelte-and-typescript\ntags:\n- typescript\n- svelte\n- javascript\n---\n\nIn the quest to make [magiedit](https://magiedit.magitools.app) always better, I figured out that people may want to publish to multiple \"accounts\" on the same platform. So, the goal for the next feature was to allow just that. A user should be able to select the platform they want to create a **Publisher** for, fill in the required data, and be able to publish to it.\nThe problem was in the second to last step; the form. How do you handle the fact that different platforms require different authenticating elements to send requests?\n\n## Generating a form from a class\n\nAs mentioned in a previous article, all the platforms have a decorator that adds them to a list that can be imported in other files, meaning I could get a list of supported platforms and generate a `select`, allowing the user to select the platform they wanted to configure. The next step consisted of having a way for the class to describe the input it needs to handle publishing an article.\n\n### Describing form elements\n\nAfter some deliberation, I settled on the following structure:\n\n```typescript\ntype IPlatformSetting = {\n type: string;\n name: string;\n label: { htmlFor: string; value: string };\n settings: Record\u003Cstring, unknown>;\n};\n```\n\n- type defines what form element will be rendered (i.e. input)\n- name: the name of the element (pretty self explanatory, I know)\n- label: the settings for the label (both the `for` attribute and the actual text inside it)\n- settings: an object of settings for the html element; as an example, here's what the Dev.to platform settings look like:\n\n```typescript\n{\n\ttype: 'input',\n\tname: 'api_token',\n\tlabel: { htmlFor: 'api_key', value: 'API Key' },\n\tsettings: { type: 'text', required: true }\n}\n```\n\nThere still is a bit of information duplication, but for now it works pretty well.\n\n### Displaying the form elements\n\nOnce the user has selected the platforms they want to publish on, the next step is to display all the fields necessary for that specific platform. Remember how we had a method that described precisely this? This is where we get to use it.\nWith Sveltekit, this could look somthing like this:\n\n```js\n{#if setting.type === 'input'}\n \u003CLabel for={setting.label.htmlFor}>{setting.label.value}\u003C/Label>\n \u003CInput id={setting.name} name={setting.name} {...setting.settings} />\n{/if}\n```\n\nIf you ever need to do more field types, just add them as available types for each setting element and modify the if condition (hoping someday to have a switch statement in svelte - or pattern matching).\n\n### Handling submission\n\nThis next step is probably the easiest. Since you're also sending the select platform **template**, you can reference that to determine if the data is valid (why not try [superforms](https://superforms.rocks)? I made an adapter for it).\n\nWith no validation whatsoever, it could look something like this:\n\n```js\nconst formData = await request.formData();\nconst { publisher_id, publisher_name, ...args } = Object.fromEntries(formData);\nawait db.insert(userPublications).values({\n\tpublisherName: publisher_id,\n\tpublisherData: args,\n\tname: publisher_name,\n\tuserId: user.id\n});\nreturn { message: 'ok' };\n```\n",{"title":3766,"description":4375},"building-a-dynamic-form-with-svelte-and-typescript","articles/building-a-dynamic-form-with-svelte-and-typescript","generate forms based on user input with sveltekit and some typescript magic",[3804,4385,269],"svelte","Rs_jf9chL5pZQem4Is5hSFBQXyn70dfW8DgWVVPpRLs",{"id":4388,"title":4389,"body":4390,"description":5349,"extension":256,"meta":5350,"navigation":259,"path":5351,"publishDate":5352,"rawbody":5353,"seo":5354,"series":264,"slug":5355,"stem":5356,"summary":5357,"tags":5358,"__hash__":5359},"articles/articles/file-based-router.md","How filesystem-based routers work: building one for express",{"type":9,"value":4391,"toc":5341},[4392,4409,4415,4419,4436,4447,4451,4455,4468,4479,4484,4586,4596,4602,4606,4613,4619,4811,4814,4821,4825,4828,4834,4837,4851,4858,4927,4932,4992,4995,5070,5073,5171,5174,5314,5319,5333,5336,5338],[17,4393,4394,4395,4398,4399,4402,4403,4408],{},"Have you ever wondered how Next, Sveltekit, Nuxt (even Expo now) do their routing? What's the magic that makes it so when you create a file called ",[171,4396,4397],{},"+page.svelte"," in a directory called ",[171,4400,4401],{},"routes",", just for it to magically works? Well, wonder no more! I'll show you how to build your own file-based router for ",[34,4404,4407],{"href":4405,"rel":4406},"https://expressjs.com/",[38],"express"," in Javascript, though the concepts we'll see are usable in other frameworks and other languages. This article will focus on parsing files, ignoring folders (because that is a whole of grenades I'm not yet ready to step - and write - on). Let's get to it!",[17,4410,4411],{},[130,4412],{"alt":4413,"src":4414},"GIF by MIRAMAX","https://media0.giphy.com/media/W5WwFpEtd5Tvq/giphy.gif?cid=bcfb6944t52lnri18evg0ro1r8f5vl3fjaxm54dhbeqpceom&ep=v1_gifs_search&rid=giphy.gif&ct=g",[12,4416,4418],{"id":4417},"how-file-routing-works","How file routing works",[17,4420,4421,4422,4424,4425,4428,4429,4431,4432],{},"First, let's start by saying what file-based routing is and how it works.\nUsually, a function will loop through the ",[171,4423,4401],{}," folder (ever noticed how you always have a single place where you place your ",[171,4426,4427],{},"pages","? this avoid crawling the whole project directory every time your app starts), getting all the files matching a certain pattern (for svelte, that will be ",[171,4430,4397],{},") and, based on their location in the folder, determines what requests should be sent to it. The first part of this series, as specified above, will not be handling folders; instead, we will just do a basic file router; here's how it'll work:\n",[130,4433],{"alt":4434,"src":4435},"file based structure explainer","https://cdn.blog.matteogassend.com/file-based-router-structure.png",[17,4437,4438,4439,4442,4443,4446],{},"This means that when we'll call our function, it will step through our route directory and generate a separate route for each filename: ",[171,4440,4441],{},"user.js"," will create ",[171,4444,4445],{},"/user"," for example.",[12,4448,4450],{"id":4449},"building-a-file-based-router","Building a file-based router",[105,4452,4454],{"id":4453},"initial-setup","Initial Setup",[17,4456,4457,4458,4461,4462,4464,4465,4467],{},"First of all, we should setup our base structure. For this example, I'll have an ",[171,4459,4460],{},"index.js"," file at the root of my project that will instanciate the express application and call the function to generate the routing. Then I'll have a ",[171,4463,4401],{}," folder that will contain - you guessed it - our routing files. The only dependency you should need is the ",[171,4466,4407],{}," package; if you haven't already done so, you should install it in a new project using:",[313,4469,4473],{"className":4470,"code":4471,"language":4472,"meta":240,"style":240},"language-sh shiki shiki-themes catppuccin-macchiato","npm install --save express\n","sh",[171,4474,4475],{"__ignoreMap":240},[321,4476,4477],{"class":323,"line":324},[321,4478,4471],{},[17,4480,4481,4482],{},"and let's initialize the express application in ",[171,4483,4460],{},[313,4485,4487],{"className":1387,"code":4486,"language":1389,"meta":240,"style":240},"const express = require(\"express\");\n\nconst app = express();\n\napp.listen(4000, () => {\n console.log(\"listening\");\n});\n",[171,4488,4489,4510,4514,4530,4534,4559,4578],{"__ignoreMap":240},[321,4490,4491,4493,4496,4498,4501,4503,4506,4508],{"class":323,"line":324},[321,4492,4209],{"class":344},[321,4494,4495],{"class":348}," express ",[321,4497,818],{"class":384},[321,4499,4500],{"class":373}," require",[321,4502,377],{"class":348},[321,4504,4505],{"class":431},"\"express\"",[321,4507,238],{"class":348},[321,4509,1404],{"class":355},[321,4511,4512],{"class":323,"line":241},[321,4513,333],{"emptyLinePlaceholder":259},[321,4515,4516,4518,4521,4523,4526,4528],{"class":323,"line":248},[321,4517,4209],{"class":344},[321,4519,4520],{"class":348}," app ",[321,4522,818],{"class":384},[321,4524,4525],{"class":373}," express",[321,4527,1455],{"class":348},[321,4529,1404],{"class":355},[321,4531,4532],{"class":323,"line":341},[321,4533,333],{"emptyLinePlaceholder":259},[321,4535,4536,4539,4541,4544,4546,4549,4551,4554,4557],{"class":323,"line":362},[321,4537,4538],{"class":348},"app",[321,4540,356],{"class":384},[321,4542,4543],{"class":373},"listen",[321,4545,377],{"class":348},[321,4547,4548],{"class":2597},"4000",[321,4550,407],{"class":355},[321,4552,4553],{"class":355}," ()",[321,4555,4556],{"class":344}," =>",[321,4558,398],{"class":355},[321,4560,4561,4564,4566,4569,4571,4574,4576],{"class":323,"line":367},[321,4562,4563],{"class":348}," console",[321,4565,356],{"class":384},[321,4567,4568],{"class":373},"log",[321,4570,377],{"class":348},[321,4572,4573],{"class":431},"\"listening\"",[321,4575,238],{"class":348},[321,4577,1404],{"class":355},[321,4579,4580,4582,4584],{"class":323,"line":401},[321,4581,1584],{"class":355},[321,4583,238],{"class":348},[321,4585,1404],{"class":355},[17,4587,4588,4589,4591,4592,4595],{},"This should be enough to have a basic http server listening on port 4000 (btw, you should probably use an environment variable here instead of hardcoding it like this). Next, let's see how we should define our route file; inside our ",[171,4590,4401],{}," folder, let's create a ",[171,4593,4594],{},"user.route.js"," file; this suffix (.route) allows us to filter only the files we are sure belong to our application thanks to the power of regex (oh yes, we'll be writing a regex - just one, I promise)!",[17,4597,4598],{},[130,4599],{"alt":4600,"src":4601},"Hacker Deal With It GIF by Sleeping Giant Media","https://media1.giphy.com/media/mYhd1NHQkHmZLiqN7M/giphy.gif?cid=bcfb6944cxheq2gck8p5iorudqqvis1ad0z2o4aathprma1b&ep=v1_gifs_search&rid=giphy.gif&ct=g",[105,4603,4605],{"id":4604},"making-a-route","Making a route",[17,4607,4608,4609,4612],{},"Let's start by defining a simple rule: ",[230,4610,4611],{},"every route file should have a Router instance as its main export",". This will allow us to do simple programmatic requires to load our route files (we'll look at how to do more specific filtering and imports in a following article, don't worry).",[17,4614,4615,4616,4618],{},"So, let's say we have a ",[171,4617,4594],{}," file containing the following:",[313,4620,4622],{"className":1387,"code":4621,"language":1389,"meta":240,"style":240},"const router = require(\"express\").Router();\n\nrouter.get(\"/\", (req, res) => {\n res.send(\"test\");\n});\n\nrouter.get(\"/:id\", (req, res) => {\n res.send(`test ${req.params.id}`);\n});\n\nmodule.exports = router;\n",[171,4623,4624,4650,4654,4686,4705,4713,4717,4746,4781,4789,4793],{"__ignoreMap":240},[321,4625,4626,4628,4631,4633,4635,4637,4639,4641,4643,4646,4648],{"class":323,"line":324},[321,4627,4209],{"class":344},[321,4629,4630],{"class":348}," router ",[321,4632,818],{"class":384},[321,4634,4500],{"class":373},[321,4636,377],{"class":348},[321,4638,4505],{"class":431},[321,4640,238],{"class":348},[321,4642,356],{"class":384},[321,4644,4645],{"class":373},"Router",[321,4647,1455],{"class":348},[321,4649,1404],{"class":355},[321,4651,4652],{"class":323,"line":241},[321,4653,333],{"emptyLinePlaceholder":259},[321,4655,4656,4659,4661,4664,4666,4668,4670,4672,4675,4677,4680,4682,4684],{"class":323,"line":248},[321,4657,4658],{"class":348},"router",[321,4660,356],{"class":384},[321,4662,4663],{"class":373},"get",[321,4665,377],{"class":348},[321,4667,524],{"class":431},[321,4669,407],{"class":355},[321,4671,1817],{"class":355},[321,4673,4674],{"class":380},"req",[321,4676,407],{"class":355},[321,4678,4679],{"class":380}," res",[321,4681,238],{"class":355},[321,4683,4556],{"class":344},[321,4685,398],{"class":355},[321,4687,4688,4691,4693,4696,4698,4701,4703],{"class":323,"line":341},[321,4689,4690],{"class":348}," res",[321,4692,356],{"class":384},[321,4694,4695],{"class":373},"send",[321,4697,377],{"class":348},[321,4699,4700],{"class":431},"\"test\"",[321,4702,238],{"class":348},[321,4704,1404],{"class":355},[321,4706,4707,4709,4711],{"class":323,"line":362},[321,4708,1584],{"class":355},[321,4710,238],{"class":348},[321,4712,1404],{"class":355},[321,4714,4715],{"class":323,"line":367},[321,4716,333],{"emptyLinePlaceholder":259},[321,4718,4719,4721,4723,4725,4727,4730,4732,4734,4736,4738,4740,4742,4744],{"class":323,"line":401},[321,4720,4658],{"class":348},[321,4722,356],{"class":384},[321,4724,4663],{"class":373},[321,4726,377],{"class":348},[321,4728,4729],{"class":431},"\"/:id\"",[321,4731,407],{"class":355},[321,4733,1817],{"class":355},[321,4735,4674],{"class":380},[321,4737,407],{"class":355},[321,4739,4679],{"class":380},[321,4741,238],{"class":355},[321,4743,4556],{"class":344},[321,4745,398],{"class":355},[321,4747,4748,4750,4752,4754,4756,4759,4761,4763,4765,4768,4770,4773,4775,4777,4779],{"class":323,"line":438},[321,4749,4690],{"class":348},[321,4751,356],{"class":384},[321,4753,4695],{"class":373},[321,4755,377],{"class":348},[321,4757,4758],{"class":431},"`test ",[321,4760,1560],{"class":355},[321,4762,4674],{"class":348},[321,4764,356],{"class":384},[321,4766,4767],{"class":348},"params",[321,4769,356],{"class":384},[321,4771,4772],{"class":348},"id",[321,4774,1584],{"class":355},[321,4776,1587],{"class":431},[321,4778,238],{"class":348},[321,4780,1404],{"class":355},[321,4782,4783,4785,4787],{"class":323,"line":455},[321,4784,1584],{"class":355},[321,4786,238],{"class":348},[321,4788,1404],{"class":355},[321,4790,4791],{"class":323,"line":473},[321,4792,333],{"emptyLinePlaceholder":259},[321,4794,4795,4799,4801,4804,4806,4809],{"class":323,"line":479},[321,4796,4798],{"class":4797},"sxfwU","module",[321,4800,356],{"class":384},[321,4802,4803],{"class":4797},"exports",[321,4805,2573],{"class":384},[321,4807,4808],{"class":348}," router",[321,4810,1404],{"class":355},[17,4812,4813],{},"When our loader will be finished, this should generate two routes:",[21,4815,4816,4818],{},[24,4817,4445],{},[24,4819,4820],{},"/user/:id (this one will match /user/1, /user/2 etc)",[105,4822,4824],{"id":4823},"making-the-loader","Making the loader",[17,4826,4827],{},"Now it's time to make the actual loader:",[17,4829,4830],{},[130,4831],{"alt":4832,"src":4833},"Its Time Vegas GIF by BPONGofficial","https://media2.giphy.com/media/SKcxqI1GiASU783uT2/giphy.gif?cid=bcfb69440p5hlnpm71c54qwekk0jbebhx4qwp6q7v02oa60c&ep=v1_gifs_search&rid=giphy.gif&ct=g",[17,4835,4836],{},"Let's recap what our loader needs to do:",[21,4838,4839,4845],{},[24,4840,4841,4842,4844],{},"walk the ",[171,4843,4401],{}," folder",[24,4846,4847,4848],{},"for each file matching the pattern (routeName).route.js, add a route to our express app that looks like ",[171,4849,4850],{},"/routeName",[17,4852,4853,4854,4857],{},"So, i'll make a new file called ",[171,4855,4856],{},"router.js"," which exports an asynchronous function. This function will take as an argument the express application and define a regex we will use to match our route name and save it as a group.",[313,4859,4861],{"className":1387,"code":4860,"language":1389,"meta":240,"style":240},"module.exports = async (app) => {\n const fileNameRegex = /(.*).routes.js$/;\n};\n",[171,4862,4863,4886,4923],{"__ignoreMap":240},[321,4864,4865,4867,4869,4871,4873,4876,4878,4880,4882,4884],{"class":323,"line":324},[321,4866,4798],{"class":4797},[321,4868,356],{"class":384},[321,4870,4803],{"class":4797},[321,4872,2573],{"class":384},[321,4874,4875],{"class":344}," async",[321,4877,1817],{"class":355},[321,4879,4538],{"class":380},[321,4881,238],{"class":355},[321,4883,4556],{"class":344},[321,4885,398],{"class":355},[321,4887,4888,4891,4894,4896,4899,4901,4903,4906,4908,4910,4912,4914,4916,4919,4921],{"class":323,"line":241},[321,4889,4890],{"class":344}," const",[321,4892,4893],{"class":348}," fileNameRegex ",[321,4895,818],{"class":384},[321,4897,4898],{"class":4061}," /",[321,4900,377],{"class":431},[321,4902,356],{"class":4061},[321,4904,4905],{"class":384},"*",[321,4907,238],{"class":431},[321,4909,356],{"class":4061},[321,4911,4401],{"class":431},[321,4913,356],{"class":4061},[321,4915,1389],{"class":431},[321,4917,4918],{"class":344},"$",[321,4920,4172],{"class":4061},[321,4922,1404],{"class":355},[321,4924,4925],{"class":323,"line":248},[321,4926,3903],{"class":355},[17,4928,4929,4930,4844],{},"And in this function we'll begin by walking the whole ",[171,4931,4401],{},[313,4933,4935],{"className":1387,"code":4934,"language":1389,"meta":240,"style":240},"/* add this at the top of the file */ const fs = require(\"node:fs/promises\");\n\nconst folders = await fs.readdir(\"./routes\");\n",[171,4936,4937,4961,4965],{"__ignoreMap":240},[321,4938,4939,4942,4945,4948,4950,4952,4954,4957,4959],{"class":323,"line":324},[321,4940,4941],{"class":327},"/* add this at the top of the file */",[321,4943,4944],{"class":344}," const",[321,4946,4947],{"class":348}," fs ",[321,4949,818],{"class":384},[321,4951,4500],{"class":373},[321,4953,377],{"class":348},[321,4955,4956],{"class":431},"\"node:fs/promises\"",[321,4958,238],{"class":348},[321,4960,1404],{"class":355},[321,4962,4963],{"class":323,"line":241},[321,4964,333],{"emptyLinePlaceholder":259},[321,4966,4967,4969,4972,4974,4976,4978,4980,4983,4985,4988,4990],{"class":323,"line":248},[321,4968,4209],{"class":344},[321,4970,4971],{"class":348}," folders ",[321,4973,818],{"class":384},[321,4975,4217],{"class":344},[321,4977,416],{"class":348},[321,4979,356],{"class":384},[321,4981,4982],{"class":373},"readdir",[321,4984,377],{"class":348},[321,4986,4987],{"class":431},"\"./routes\"",[321,4989,238],{"class":348},[321,4991,1404],{"class":355},[17,4993,4994],{},"And then check each file to see if it matches the regex we defined earlier:",[313,4996,4998],{"className":1387,"code":4997,"language":1389,"meta":240,"style":240},"folders.forEach((file) => {\n const regexName = fileNameRegex.exec(file);\n if (!regexName)\n return;\n});\n",[171,4999,5000,5022,5044,5056,5062],{"__ignoreMap":240},[321,5001,5002,5005,5007,5010,5012,5014,5016,5018,5020],{"class":323,"line":324},[321,5003,5004],{"class":348},"folders",[321,5006,356],{"class":384},[321,5008,5009],{"class":373},"forEach",[321,5011,377],{"class":348},[321,5013,377],{"class":355},[321,5015,3038],{"class":380},[321,5017,238],{"class":355},[321,5019,4556],{"class":344},[321,5021,398],{"class":355},[321,5023,5024,5026,5029,5031,5034,5036,5039,5042],{"class":323,"line":241},[321,5025,4890],{"class":344},[321,5027,5028],{"class":348}," regexName ",[321,5030,818],{"class":384},[321,5032,5033],{"class":348}," fileNameRegex",[321,5035,356],{"class":384},[321,5037,5038],{"class":373},"exec",[321,5040,5041],{"class":348},"(file)",[321,5043,1404],{"class":355},[321,5045,5046,5049,5051,5053],{"class":323,"line":248},[321,5047,5048],{"class":344}," if",[321,5050,1817],{"class":348},[321,5052,1983],{"class":384},[321,5054,5055],{"class":348},"regexName)\n",[321,5057,5058,5060],{"class":323,"line":341},[321,5059,759],{"class":344},[321,5061,1404],{"class":355},[321,5063,5064,5066,5068],{"class":323,"line":362},[321,5065,1584],{"class":355},[321,5067,238],{"class":348},[321,5069,1404],{"class":355},[17,5071,5072],{},"And finally, we add the route to our application with a little log line:",[313,5074,5076],{"className":1387,"code":5075,"language":1389,"meta":240,"style":240},"console.log(`[+] Router file ${file} loaded under /${regexName[1]}`);\napp.use(`/${regexName[1]}`, require(`./routes/${file}`));\n",[171,5077,5078,5121],{"__ignoreMap":240},[321,5079,5080,5083,5085,5087,5089,5092,5094,5096,5098,5101,5103,5106,5108,5111,5113,5115,5117,5119],{"class":323,"line":324},[321,5081,5082],{"class":348},"console",[321,5084,356],{"class":384},[321,5086,4568],{"class":373},[321,5088,377],{"class":348},[321,5090,5091],{"class":431},"`[+] Router file ",[321,5093,1560],{"class":355},[321,5095,3038],{"class":348},[321,5097,1584],{"class":355},[321,5099,5100],{"class":431}," loaded under /",[321,5102,1560],{"class":355},[321,5104,5105],{"class":348},"regexName",[321,5107,1285],{"class":431},[321,5109,5110],{"class":2597},"1",[321,5112,1290],{"class":431},[321,5114,1584],{"class":355},[321,5116,1587],{"class":431},[321,5118,238],{"class":348},[321,5120,1404],{"class":355},[321,5122,5123,5125,5127,5130,5132,5135,5137,5139,5141,5143,5145,5147,5149,5151,5153,5155,5158,5160,5162,5164,5166,5169],{"class":323,"line":241},[321,5124,4538],{"class":348},[321,5126,356],{"class":384},[321,5128,5129],{"class":373},"use",[321,5131,377],{"class":348},[321,5133,5134],{"class":431},"`/",[321,5136,1560],{"class":355},[321,5138,5105],{"class":348},[321,5140,1285],{"class":431},[321,5142,5110],{"class":2597},[321,5144,1290],{"class":431},[321,5146,1584],{"class":355},[321,5148,1587],{"class":431},[321,5150,407],{"class":355},[321,5152,4500],{"class":373},[321,5154,377],{"class":348},[321,5156,5157],{"class":431},"`./routes/",[321,5159,1560],{"class":355},[321,5161,3038],{"class":348},[321,5163,1584],{"class":355},[321,5165,1587],{"class":431},[321,5167,5168],{"class":348},"))",[321,5170,1404],{"class":355},[17,5172,5173],{},"The last step is to use our loader function: your index.js should look something like this:",[313,5175,5177],{"className":1387,"code":5176,"language":1389,"meta":240,"style":240},"const express = require(\"express\");\n\nconst loadRoutes = require(\"./router\");\n\nconst app = express();\nloadRoutes(app).then(() => {\n app.listen(4000, () => {\n console.log(\"listening\");\n });\n});\n",[171,5178,5179,5197,5201,5221,5225,5239,5260,5281,5298,5306],{"__ignoreMap":240},[321,5180,5181,5183,5185,5187,5189,5191,5193,5195],{"class":323,"line":324},[321,5182,4209],{"class":344},[321,5184,4495],{"class":348},[321,5186,818],{"class":384},[321,5188,4500],{"class":373},[321,5190,377],{"class":348},[321,5192,4505],{"class":431},[321,5194,238],{"class":348},[321,5196,1404],{"class":355},[321,5198,5199],{"class":323,"line":241},[321,5200,333],{"emptyLinePlaceholder":259},[321,5202,5203,5205,5208,5210,5212,5214,5217,5219],{"class":323,"line":248},[321,5204,4209],{"class":344},[321,5206,5207],{"class":348}," loadRoutes ",[321,5209,818],{"class":384},[321,5211,4500],{"class":373},[321,5213,377],{"class":348},[321,5215,5216],{"class":431},"\"./router\"",[321,5218,238],{"class":348},[321,5220,1404],{"class":355},[321,5222,5223],{"class":323,"line":341},[321,5224,333],{"emptyLinePlaceholder":259},[321,5226,5227,5229,5231,5233,5235,5237],{"class":323,"line":362},[321,5228,4209],{"class":344},[321,5230,4520],{"class":348},[321,5232,818],{"class":384},[321,5234,4525],{"class":373},[321,5236,1455],{"class":348},[321,5238,1404],{"class":355},[321,5240,5241,5244,5247,5249,5252,5254,5256,5258],{"class":323,"line":367},[321,5242,5243],{"class":373},"loadRoutes",[321,5245,5246],{"class":348},"(app)",[321,5248,356],{"class":384},[321,5250,5251],{"class":373},"then",[321,5253,377],{"class":348},[321,5255,1455],{"class":355},[321,5257,4556],{"class":344},[321,5259,398],{"class":355},[321,5261,5262,5265,5267,5269,5271,5273,5275,5277,5279],{"class":323,"line":401},[321,5263,5264],{"class":348}," app",[321,5266,356],{"class":384},[321,5268,4543],{"class":373},[321,5270,377],{"class":348},[321,5272,4548],{"class":2597},[321,5274,407],{"class":355},[321,5276,4553],{"class":355},[321,5278,4556],{"class":344},[321,5280,398],{"class":355},[321,5282,5283,5286,5288,5290,5292,5294,5296],{"class":323,"line":438},[321,5284,5285],{"class":348}," console",[321,5287,356],{"class":384},[321,5289,4568],{"class":373},[321,5291,377],{"class":348},[321,5293,4573],{"class":431},[321,5295,238],{"class":348},[321,5297,1404],{"class":355},[321,5299,5300,5302,5304],{"class":323,"line":455},[321,5301,2695],{"class":355},[321,5303,238],{"class":348},[321,5305,1404],{"class":355},[321,5307,5308,5310,5312],{"class":323,"line":473},[321,5309,1584],{"class":355},[321,5311,238],{"class":348},[321,5313,1404],{"class":355},[17,5315,5316],{},[130,5317],{"alt":240,"src":5318},"https://media1.giphy.com/media/vN3fMMSAmVwoo/giphy.gif?cid=bcfb694476ov89l4f91ymnaon8r1wwg13pgrqobo6ijfnke7&ep=v1_gifs_search&rid=giphy.gif&ct=g",[17,5320,5321,5322,5327,5328],{},"This is the simplest way to make a file-based router in express js. This logic can obviously be translated to be used in other framework, like ",[34,5323,5326],{"href":5324,"rel":5325},"https://github.com/GiovanniCardamone/fastify-autoroutes",[38],"this plugin"," for ",[34,5329,5332],{"href":5330,"rel":5331},"https://fastify.dev/",[38],"fastify",[17,5334,5335],{},"You can see a working example below:",[1223,5337],{"id":1373},[1167,5339,5340],{},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sIF4r, html code.shiki .sIF4r{--shiki-default:#C6A0F6}html pre.shiki code .sFaBz, html code.shiki .sFaBz{--shiki-default:#CAD3F5}html pre.shiki code .sXptk, html code.shiki .sXptk{--shiki-default:#8BD5CA}html pre.shiki code .siK8c, html code.shiki .siK8c{--shiki-default:#8AADF4;--shiki-default-font-style:italic}html pre.shiki code .sSZ1V, html code.shiki .sSZ1V{--shiki-default:#A6DA95}html pre.shiki code .slVFb, html code.shiki .slVFb{--shiki-default:#939AB7}html pre.shiki code .s7Qn8, html code.shiki .s7Qn8{--shiki-default:#F5A97F}html pre.shiki code .skVQi, html code.shiki .skVQi{--shiki-default:#EE99A0;--shiki-default-font-style:italic}html pre.shiki code .sxfwU, html code.shiki .sxfwU{--shiki-default:#C6A0F6;--shiki-default-font-style:italic}html pre.shiki code .s_ZFR, html code.shiki .s_ZFR{--shiki-default:#F5BDE6}html pre.shiki code .sfEIy, html code.shiki .sfEIy{--shiki-default:#939AB7;--shiki-default-font-style:italic}",{"title":240,"searchDepth":241,"depth":241,"links":5342},[5343,5344],{"id":4417,"depth":241,"text":4418},{"id":4449,"depth":241,"text":4450,"children":5345},[5346,5347,5348],{"id":4453,"depth":248,"text":4454},{"id":4604,"depth":248,"text":4605},{"id":4823,"depth":248,"text":4824},"Have you ever wondered how Next, Sveltekit, Nuxt (even Expo now) do their routing? What's the magic that makes it so when you create a file called +page.svelte in a directory called routes, just for it to magically works? Well, wonder no more! I'll show you how to build your own file-based router for express in Javascript, though the concepts we'll see are usable in other frameworks and other languages. This article will focus on parsing files, ignoring folders (because that is a whole of grenades I'm not yet ready to step - and write - on). Let's get to it!",{"cover_image":258},"/articles/file-based-router","2023-10-22T00:00:00.000Z","---\ntitle: \"How filesystem-based routers work: building one for express\"\ncover_image: ./images/og.png\npublishDate: 2023-10-22\nsummary: \"building a file-based router is easier than it seems\"\nslug: file-based-router\ntags:\n- javascript\n- node\n---\n\nHave you ever wondered how Next, Sveltekit, Nuxt (even Expo now) do their routing? What's the magic that makes it so when you create a file called `+page.svelte` in a directory called `routes`, just for it to magically works? Well, wonder no more! I'll show you how to build your own file-based router for [express](https://expressjs.com/) in Javascript, though the concepts we'll see are usable in other frameworks and other languages. This article will focus on parsing files, ignoring folders (because that is a whole of grenades I'm not yet ready to step - and write - on). Let's get to it!\n\n\n\n## How file routing works\n\nFirst, let's start by saying what file-based routing is and how it works.\nUsually, a function will loop through the `routes` folder (ever noticed how you always have a single place where you place your `pages`? this avoid crawling the whole project directory every time your app starts), getting all the files matching a certain pattern (for svelte, that will be `+page.svelte`) and, based on their location in the folder, determines what requests should be sent to it. The first part of this series, as specified above, will not be handling folders; instead, we will just do a basic file router; here's how it'll work:\n\n\nThis means that when we'll call our function, it will step through our route directory and generate a separate route for each filename: `user.js` will create `/user` for example.\n\n## Building a file-based router\n\n### Initial Setup\n\nFirst of all, we should setup our base structure. For this example, I'll have an `index.js` file at the root of my project that will instanciate the express application and call the function to generate the routing. Then I'll have a `routes` folder that will contain - you guessed it - our routing files. The only dependency you should need is the `express` package; if you haven't already done so, you should install it in a new project using:\n\n```sh\nnpm install --save express\n```\n\nand let's initialize the express application in `index.js`\n\n```js\nconst express = require(\"express\");\n\nconst app = express();\n\napp.listen(4000, () => {\n console.log(\"listening\");\n});\n```\n\nThis should be enough to have a basic http server listening on port 4000 (btw, you should probably use an environment variable here instead of hardcoding it like this). Next, let's see how we should define our route file; inside our `routes` folder, let's create a `user.route.js` file; this suffix (.route) allows us to filter only the files we are sure belong to our application thanks to the power of regex (oh yes, we'll be writing a regex - just one, I promise)!\n\n\n\n### Making a route\n\nLet's start by defining a simple rule: **every route file should have a Router instance as its main export**. This will allow us to do simple programmatic requires to load our route files (we'll look at how to do more specific filtering and imports in a following article, don't worry).\n\nSo, let's say we have a `user.route.js` file containing the following:\n\n```js\nconst router = require(\"express\").Router();\n\nrouter.get(\"/\", (req, res) => {\n res.send(\"test\");\n});\n\nrouter.get(\"/:id\", (req, res) => {\n res.send(`test ${req.params.id}`);\n});\n\nmodule.exports = router;\n```\n\nWhen our loader will be finished, this should generate two routes:\n\n- /user\n- /user/:id (this one will match /user/1, /user/2 etc)\n\n### Making the loader\n\nNow it's time to make the actual loader:\n\n\n\nLet's recap what our loader needs to do:\n\n- walk the `routes` folder\n- for each file matching the pattern (routeName).route.js, add a route to our express app that looks like `/routeName`\n\nSo, i'll make a new file called `router.js` which exports an asynchronous function. This function will take as an argument the express application and define a regex we will use to match our route name and save it as a group.\n\n```js\nmodule.exports = async (app) => {\n const fileNameRegex = /(.*).routes.js$/;\n};\n```\n\nAnd in this function we'll begin by walking the whole `routes` folder\n\n```js\n/* add this at the top of the file */ const fs = require(\"node:fs/promises\");\n\nconst folders = await fs.readdir(\"./routes\");\n```\n\nAnd then check each file to see if it matches the regex we defined earlier:\n\n```js\nfolders.forEach((file) => {\n const regexName = fileNameRegex.exec(file);\n if (!regexName)\n return;\n});\n```\n\nAnd finally, we add the route to our application with a little log line:\n\n```js\nconsole.log(`[+] Router file ${file} loaded under /${regexName[1]}`);\napp.use(`/${regexName[1]}`, require(`./routes/${file}`));\n```\n\nThe last step is to use our loader function: your index.js should look something like this:\n\n```js\nconst express = require(\"express\");\n\nconst loadRoutes = require(\"./router\");\n\nconst app = express();\nloadRoutes(app).then(() => {\n app.listen(4000, () => {\n console.log(\"listening\");\n });\n});\n```\n\n\n\nThis is the simplest way to make a file-based router in express js. This logic can obviously be translated to be used in other framework, like [this plugin](https://github.com/GiovanniCardamone/fastify-autoroutes) for [fastify](https://fastify.dev/)\n\nYou can see a working example below:\n\n::stackblitz{#mg-fbr-express}\n::",{"title":4389,"description":5349},"file-based-router","articles/file-based-router","building a file-based router is easier than it seems",[269,1396],"p1vMzrLbN_7gxAI9dPkBdERVNbDaqbiNtzQi8ktfnKw",{"id":5361,"title":5362,"body":5363,"description":5367,"extension":256,"meta":5859,"navigation":259,"path":5860,"publishDate":5861,"rawbody":5862,"seo":5863,"series":264,"slug":5864,"stem":5865,"summary":5866,"tags":5867,"__hash__":5871},"articles/articles/test-with-dagger.md","test everywhere with dagger.io",{"type":9,"value":5364,"toc":5848},[5365,5368,5372,5375,5380,5387,5391,5394,5398,5401,5405,5410,5423,5427,5435,5450,5454,5457,5466,5470,5477,5487,5490,5520,5523,5526,5566,5569,5575,5578,5598,5602,5613,5622,5628,5631,5635,5638,5833,5836,5845],[17,5366,5367],{},"Have you ever had to implement a CI/CD pipeline and then proceed to push to a repository 10/20 times just to see if the pipeline works correctly? Well, this ends today!",[12,5369,5371],{"id":5370},"dagger-not-the-stabby-kind","Dagger (not the stabby kind)",[17,5373,5374],{},"Dagger's tagline is",[162,5376,5377],{},[17,5378,5379],{},"CI/CD as Code that Runs Anywhere",[17,5381,5382,5383,5386],{},"It is a way for developers to create pipelines that run everywhere, be it locally, as Github Actions or anywhere where you can spawn containers; it is what I use to do a test build on ",[34,5384,3776],{"href":3774,"rel":5385},[38]," and what I'll use when I eventually write unit tests (it'll come one day, I promise.)",[105,5388,5390],{"id":5389},"how-do-you-use-it","How do you use it?",[17,5392,5393],{},"Dagger provides different sdks (for Javascript, Python, Go etc) and they also have an HTTP and GraphQL api, so you can use with whatever coding language you prefer. Then you just run it wherever your pipelines run.",[12,5395,5397],{"id":5396},"creating-a-pipeline","Creating a Pipeline",[17,5399,5400],{},"In this article I'll be using the Javascript sdk, because that is the one I know better (and because it is my article, after all). We'll be looking at how to create a simple pipeline script with some extra configuration like setting some env variables.",[105,5402,5404],{"id":5403},"prerequisites","Prerequisites",[5406,5407,5409],"h4",{"id":5408},"container-engine","Container engine",[17,5411,5412,5413,5416,5417,5422],{},"Dagger requires a container engine for it run wherever it needs to run (at this stage, at least, locally); the recommended one is Docker (I have an article on the basics of Docker ",[34,5414,39],{"href":5415},"/articles/taming-the-whale","), but other runtimes are compatible (podman, containerd etc): here's ",[34,5418,5421],{"href":5419,"rel":5420},"https://docs.dagger.io/541047/alternative-runtimes",[38],"an article"," detailing how to use them, though I would still recommend Docker.",[5406,5424,5426],{"id":5425},"install-dagger-cli","Install Dagger CLI",[17,5428,5429,5430,5434],{},"You can find the installation instructions ",[34,5431,39],{"href":5432,"rel":5433},"https://docs.dagger.io/quickstart/729236/cli",[38],", but the gist is (for Linux / WSL) to execute this line:",[313,5436,5438],{"className":4470,"code":5437,"language":4472,"meta":240,"style":240},"cd /usr/local\ncurl -L https://dl.dagger.io/dagger/install.sh | sh\n",[171,5439,5440,5445],{"__ignoreMap":240},[321,5441,5442],{"class":323,"line":324},[321,5443,5444],{},"cd /usr/local\n",[321,5446,5447],{"class":323,"line":241},[321,5448,5449],{},"curl -L https://dl.dagger.io/dagger/install.sh | sh\n",[5406,5451,5453],{"id":5452},"install-dagger-sdk","Install Dagger SDK",[17,5455,5456],{},"For this example I'll be using the NodeJS sdk which you can install like this:",[313,5458,5460],{"className":4470,"code":5459,"language":4472,"meta":240,"style":240},"npm install @dagger.io/dagger --save-dev\n",[171,5461,5462],{"__ignoreMap":240},[321,5463,5464],{"class":323,"line":324},[321,5465,5459],{},[105,5467,5469],{"id":5468},"writing-the-actual-pipeline","Writing the actual pipeline",[17,5471,5472,5473,5476],{},"The main thing to do is import the ",[171,5474,5475],{},"connect"," function from the Dagger sdk, because everything will happen in there:",[313,5478,5481],{"className":5479,"code":5480,"language":269,"meta":240,"style":240},"language-javascript shiki shiki-themes catppuccin-macchiato","import { connect } from \"@dagger.io/dagger\";\n",[171,5482,5483],{"__ignoreMap":240},[321,5484,5485],{"class":323,"line":324},[321,5486,5480],{},[17,5488,5489],{},"Once we call this function, we can pass it a callback that gets as its argument the client instance; this is what we will use to define our pipeline:",[313,5491,5493],{"className":5479,"code":5492,"language":269,"meta":240,"style":240},"connect(\n async (client) => {\n },\n { LogOutput: process.stdout }\n);\n",[171,5494,5495,5500,5505,5510,5515],{"__ignoreMap":240},[321,5496,5497],{"class":323,"line":324},[321,5498,5499],{},"connect(\n",[321,5501,5502],{"class":323,"line":241},[321,5503,5504],{}," async (client) => {\n",[321,5506,5507],{"class":323,"line":248},[321,5508,5509],{}," },\n",[321,5511,5512],{"class":323,"line":341},[321,5513,5514],{}," { LogOutput: process.stdout }\n",[321,5516,5517],{"class":323,"line":362},[321,5518,5519],{},");\n",[17,5521,5522],{},"As you can see from the snippet above, you can also specify where the output logs will be sent; in this case, the standard output is good enough, but you could also sent them to a file that could, for example, be uploaded as an artifact for the pipeline run.",[17,5524,5525],{},"Then you can declare a runner with a base image and run all the commands you need with it;",[313,5527,5529],{"className":5479,"code":5528,"language":269,"meta":240,"style":240},"const node = await client\n .container()\n .from(\"node:18\")\n .withDirectory(\"/app\", client.host().directory(\".\"), {\n exclude: [\"node_modules\", \"ci\"]\n });\nconst runner = node.withWorkdir(\"/app\").withExec([\"npm\", \"install\"]);\n",[171,5530,5531,5536,5541,5546,5551,5556,5561],{"__ignoreMap":240},[321,5532,5533],{"class":323,"line":324},[321,5534,5535],{},"const node = await client\n",[321,5537,5538],{"class":323,"line":241},[321,5539,5540],{}," .container()\n",[321,5542,5543],{"class":323,"line":248},[321,5544,5545],{}," .from(\"node:18\")\n",[321,5547,5548],{"class":323,"line":341},[321,5549,5550],{}," .withDirectory(\"/app\", client.host().directory(\".\"), {\n",[321,5552,5553],{"class":323,"line":362},[321,5554,5555],{}," exclude: [\"node_modules\", \"ci\"]\n",[321,5557,5558],{"class":323,"line":367},[321,5559,5560],{}," });\n",[321,5562,5563],{"class":323,"line":401},[321,5564,5565],{},"const runner = node.withWorkdir(\"/app\").withExec([\"npm\", \"install\"]);\n",[17,5567,5568],{},"In the above snippet, we declare a container based on node 18 and we copy the contents of the current directory (excluding the folders called node_modules and ci), then set the working directory to /app (the directory we copied our project to) and execute npm install inside it.",[17,5570,5571],{},[130,5572],{"alt":5573,"src":5574},"The Walking Dead Easy Peasy GIF","https://media0.giphy.com/media/NaboQwhxK3gMU/giphy.gif?cid=bcfb6944db5rkmw2adjnr7wtx95a7veo71t5t5zl7aj2gs9h&ep=v1_gifs_search&rid=giphy.gif&ct=g",[17,5576,5577],{},"Now, how about running a build script and getting the error output (if any)? Simple, we just continue adding calls to our connect callback:",[313,5579,5581],{"className":5479,"code":5580,"language":269,"meta":240,"style":240},"await runner\n .withExec([\"npm\", \"run\", \"build\"])\n .stderr();\n",[171,5582,5583,5588,5593],{"__ignoreMap":240},[321,5584,5585],{"class":323,"line":324},[321,5586,5587],{},"await runner\n",[321,5589,5590],{"class":323,"line":241},[321,5591,5592],{}," .withExec([\"npm\", \"run\", \"build\"])\n",[321,5594,5595],{"class":323,"line":248},[321,5596,5597],{}," .stderr();\n",[105,5599,5601],{"id":5600},"running-the-pipeline","Running the pipeline",[17,5603,5604,5605,5608,5609,5612],{},"To run pipeline, you simply execute it like any other NodeJS script; assuming you have saved your script as ",[171,5606,5607],{},"build.mjs"," in a ",[171,5610,5611],{},"ci"," folder, you can just run:",[313,5614,5616],{"className":4470,"code":5615,"language":4472,"meta":240,"style":240},"node ci/build.mjs\n",[171,5617,5618],{"__ignoreMap":240},[321,5619,5620],{"class":323,"line":324},[321,5621,5615],{},[17,5623,5624],{},[130,5625],{"alt":5626,"src":5627},"Weird Al GIF by The Roku Channel","https://media1.giphy.com/media/iFCmbYrTnj96luPXhE/giphy.gif?cid=bcfb69441tcje7xshjfsao2o95j0e18ft09mbqt9mfjs0g6f&ep=v1_gifs_search&rid=giphy.gif&ct=g",[17,5629,5630],{},"And that's how you can create and run a basic pipeline; note that you can create multi-stage builds, publish images, using existing Dockerfiles and much more!",[105,5632,5634],{"id":5633},"bonus-round-github-actions","Bonus Round: Github Actions",[17,5636,5637],{},"Running a Dagger pipeline in Github Actions is pretty straightforward; you need to setup your environment to be able to run Dagger, which means basically installing nodejs js and installing dependencies:",[313,5639,5643],{"className":5640,"code":5641,"language":5642,"meta":240,"style":240},"language-yaml shiki shiki-themes catppuccin-macchiato","name: build\non:\n push:\n branches:\n - master\n - feature/*\n\njobs:\n build:\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@v2\n - uses: pnpm/action-setup@v2\n with:\n version: 8\n - uses: actions/setup-node@v3\n with:\n node-version: 18\n - name: Install dependencies\n run: pnpm install\n - name: Build\n run: node ci/build.mjs\n","yaml",[171,5644,5645,5654,5661,5668,5675,5683,5690,5694,5701,5708,5718,5725,5737,5748,5755,5765,5776,5782,5792,5803,5813,5824],{"__ignoreMap":240},[321,5646,5647,5649,5651],{"class":323,"line":324},[321,5648,4131],{"class":1295},[321,5650,1350],{"class":384},[321,5652,5653],{"class":431}," build\n",[321,5655,5656,5659],{"class":323,"line":241},[321,5657,5658],{"class":2597},"on",[321,5660,1973],{"class":384},[321,5662,5663,5666],{"class":323,"line":248},[321,5664,5665],{"class":1295}," push",[321,5667,1973],{"class":384},[321,5669,5670,5673],{"class":323,"line":341},[321,5671,5672],{"class":1295}," branches",[321,5674,1973],{"class":384},[321,5676,5677,5680],{"class":323,"line":362},[321,5678,5679],{"class":355}," -",[321,5681,5682],{"class":431}," master\n",[321,5684,5685,5687],{"class":323,"line":367},[321,5686,5679],{"class":355},[321,5688,5689],{"class":431}," feature/*\n",[321,5691,5692],{"class":323,"line":401},[321,5693,333],{"emptyLinePlaceholder":259},[321,5695,5696,5699],{"class":323,"line":438},[321,5697,5698],{"class":1295},"jobs",[321,5700,1973],{"class":384},[321,5702,5703,5706],{"class":323,"line":455},[321,5704,5705],{"class":1295}," build",[321,5707,1973],{"class":384},[321,5709,5710,5713,5715],{"class":323,"line":473},[321,5711,5712],{"class":1295}," runs-on",[321,5714,1350],{"class":384},[321,5716,5717],{"class":431}," ubuntu-latest\n",[321,5719,5720,5723],{"class":323,"line":479},[321,5721,5722],{"class":1295}," steps",[321,5724,1973],{"class":384},[321,5726,5727,5729,5732,5734],{"class":323,"line":512},[321,5728,5679],{"class":355},[321,5730,5731],{"class":1295}," uses",[321,5733,1350],{"class":384},[321,5735,5736],{"class":431}," actions/checkout@v2\n",[321,5738,5739,5741,5743,5745],{"class":323,"line":534},[321,5740,5679],{"class":355},[321,5742,5731],{"class":1295},[321,5744,1350],{"class":384},[321,5746,5747],{"class":431}," pnpm/action-setup@v2\n",[321,5749,5750,5753],{"class":323,"line":552},[321,5751,5752],{"class":1295}," with",[321,5754,1973],{"class":384},[321,5756,5757,5760,5762],{"class":323,"line":891},[321,5758,5759],{"class":1295}," version",[321,5761,1350],{"class":384},[321,5763,5764],{"class":2597}," 8\n",[321,5766,5767,5769,5771,5773],{"class":323,"line":897},[321,5768,5679],{"class":355},[321,5770,5731],{"class":1295},[321,5772,1350],{"class":384},[321,5774,5775],{"class":431}," actions/setup-node@v3\n",[321,5777,5778,5780],{"class":323,"line":902},[321,5779,5752],{"class":1295},[321,5781,1973],{"class":384},[321,5783,5784,5787,5789],{"class":323,"line":907},[321,5785,5786],{"class":1295}," node-version",[321,5788,1350],{"class":384},[321,5790,5791],{"class":2597}," 18\n",[321,5793,5794,5796,5798,5800],{"class":323,"line":930},[321,5795,5679],{"class":355},[321,5797,4136],{"class":1295},[321,5799,1350],{"class":384},[321,5801,5802],{"class":431}," Install dependencies\n",[321,5804,5805,5808,5810],{"class":323,"line":947},[321,5806,5807],{"class":1295}," run",[321,5809,1350],{"class":384},[321,5811,5812],{"class":431}," pnpm install\n",[321,5814,5815,5817,5819,5821],{"class":323,"line":967},[321,5816,5679],{"class":355},[321,5818,4136],{"class":1295},[321,5820,1350],{"class":384},[321,5822,5823],{"class":431}," Build\n",[321,5825,5826,5828,5830],{"class":323,"line":984},[321,5827,5807],{"class":1295},[321,5829,1350],{"class":384},[321,5831,5832],{"class":431}," node ci/build.mjs\n",[17,5834,5835],{},"In this example, I'm using pnpm to install my dependencies, and then simply execute the pipeline.",[17,5837,5838,5839,5844],{},"If you want to learn more about Dagger, check out ",[34,5840,5843],{"href":5841,"rel":5842},"https://dagger.io/",[38],"their website","; they have some very nice real case articles on how to use Dagger to build more complex pipelines.",[1167,5846,5847],{},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .s57MT, html code.shiki .s57MT{--shiki-default:#8AADF4}html pre.shiki code .sXptk, html code.shiki .sXptk{--shiki-default:#8BD5CA}html pre.shiki code .sSZ1V, html code.shiki .sSZ1V{--shiki-default:#A6DA95}html pre.shiki code .s7Qn8, html code.shiki .s7Qn8{--shiki-default:#F5A97F}html pre.shiki code .slVFb, html code.shiki .slVFb{--shiki-default:#939AB7}",{"title":240,"searchDepth":241,"depth":241,"links":5849},[5850,5853],{"id":5370,"depth":241,"text":5371,"children":5851},[5852],{"id":5389,"depth":248,"text":5390},{"id":5396,"depth":241,"text":5397,"children":5854},[5855,5856,5857,5858],{"id":5403,"depth":248,"text":5404},{"id":5468,"depth":248,"text":5469},{"id":5600,"depth":248,"text":5601},{"id":5633,"depth":248,"text":5634},{"cover_image":258},"/articles/test-with-dagger","2023-10-15T00:00:00.000Z","---\ntitle: \"test everywhere with dagger.io\"\ncover_image: ./images/og.png\npublishDate: 2023-10-15\nsummary: \"How to use Dagger to create CI/CD pipelines that run everywhere\"\nslug: test-with-dagger\ntags:\n- docker\n- devops\n- dagger.io\n- javascript\n---\n\nHave you ever had to implement a CI/CD pipeline and then proceed to push to a repository 10/20 times just to see if the pipeline works correctly? Well, this ends today!\n\n## Dagger (not the stabby kind)\n\nDagger's tagline is\n\n> CI/CD as Code that Runs Anywhere\n\nIt is a way for developers to create pipelines that run everywhere, be it locally, as Github Actions or anywhere where you can spawn containers; it is what I use to do a test build on [magiedit](https://magiedit.magitools.app) and what I'll use when I eventually write unit tests (it'll come one day, I promise.)\n\n### How do you use it?\n\nDagger provides different sdks (for Javascript, Python, Go etc) and they also have an HTTP and GraphQL api, so you can use with whatever coding language you prefer. Then you just run it wherever your pipelines run.\n\n## Creating a Pipeline\n\nIn this article I'll be using the Javascript sdk, because that is the one I know better (and because it is my article, after all). We'll be looking at how to create a simple pipeline script with some extra configuration like setting some env variables.\n\n### Prerequisites\n\n#### Container engine\n\nDagger requires a container engine for it run wherever it needs to run (at this stage, at least, locally); the recommended one is Docker (I have an article on the basics of Docker [here](/articles/taming-the-whale)), but other runtimes are compatible (podman, containerd etc): here's [an article](https://docs.dagger.io/541047/alternative-runtimes) detailing how to use them, though I would still recommend Docker.\n\n#### Install Dagger CLI\n\nYou can find the installation instructions [here](https://docs.dagger.io/quickstart/729236/cli), but the gist is (for Linux / WSL) to execute this line:\n\n```sh\ncd /usr/local\ncurl -L https://dl.dagger.io/dagger/install.sh | sh\n```\n\n#### Install Dagger SDK\n\nFor this example I'll be using the NodeJS sdk which you can install like this:\n\n```sh\nnpm install @dagger.io/dagger --save-dev\n```\n\n### Writing the actual pipeline\n\nThe main thing to do is import the `connect` function from the Dagger sdk, because everything will happen in there:\n\n```javascript\nimport { connect } from \"@dagger.io/dagger\";\n```\n\nOnce we call this function, we can pass it a callback that gets as its argument the client instance; this is what we will use to define our pipeline:\n\n```javascript\nconnect(\n async (client) => {\n },\n { LogOutput: process.stdout }\n);\n```\n\nAs you can see from the snippet above, you can also specify where the output logs will be sent; in this case, the standard output is good enough, but you could also sent them to a file that could, for example, be uploaded as an artifact for the pipeline run.\n\nThen you can declare a runner with a base image and run all the commands you need with it;\n\n```javascript\nconst node = await client\n .container()\n .from(\"node:18\")\n .withDirectory(\"/app\", client.host().directory(\".\"), {\n exclude: [\"node_modules\", \"ci\"]\n });\nconst runner = node.withWorkdir(\"/app\").withExec([\"npm\", \"install\"]);\n```\n\nIn the above snippet, we declare a container based on node 18 and we copy the contents of the current directory (excluding the folders called node_modules and ci), then set the working directory to /app (the directory we copied our project to) and execute npm install inside it.\n\n\n\nNow, how about running a build script and getting the error output (if any)? Simple, we just continue adding calls to our connect callback:\n\n```javascript\nawait runner\n .withExec([\"npm\", \"run\", \"build\"])\n .stderr();\n```\n\n### Running the pipeline\n\nTo run pipeline, you simply execute it like any other NodeJS script; assuming you have saved your script as `build.mjs` in a `ci` folder, you can just run:\n\n```sh\nnode ci/build.mjs\n```\n\n\n\nAnd that's how you can create and run a basic pipeline; note that you can create multi-stage builds, publish images, using existing Dockerfiles and much more!\n\n### Bonus Round: Github Actions\n\nRunning a Dagger pipeline in Github Actions is pretty straightforward; you need to setup your environment to be able to run Dagger, which means basically installing nodejs js and installing dependencies:\n\n```yaml\nname: build\non:\n push:\n branches:\n - master\n - feature/*\n\njobs:\n build:\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@v2\n - uses: pnpm/action-setup@v2\n with:\n version: 8\n - uses: actions/setup-node@v3\n with:\n node-version: 18\n - name: Install dependencies\n run: pnpm install\n - name: Build\n run: node ci/build.mjs\n```\n\nIn this example, I'm using pnpm to install my dependencies, and then simply execute the pipeline.\n\nIf you want to learn more about Dagger, check out [their website](https://dagger.io/); they have some very nice real case articles on how to use Dagger to build more complex pipelines.\n",{"title":5362,"description":5367},"test-with-dagger","articles/test-with-dagger","How to use Dagger to create CI/CD pipelines that run everywhere",[5868,5869,5870,269],"docker","devops","dagger.io","9x3QWAPYG8mAFAtjHvWK1ySG74nhaxBzEwhTtwydLRg",{"id":5873,"title":5874,"body":5875,"description":5879,"extension":256,"meta":6011,"navigation":259,"path":1248,"publishDate":6012,"rawbody":6013,"seo":6014,"series":3776,"slug":6015,"stem":6016,"summary":6017,"tags":6018,"__hash__":6020},"articles/articles/markdown-editor.md","building a basic markdown editor: unified, trees and data",{"type":9,"value":5876,"toc":6004},[5877,5880,5886,5889,5892,5907,5911,5914,5920,5923,5929,5932,5945,5969,5975,5984,5994,5998,6001],[17,5878,5879],{},"As you have probably guessed from the previous articles in this series (go read them if you haven't, btw), the main thing when building a markdown publishing webapp, is the markdown editor itself.",[17,5881,5882],{},[130,5883],{"alt":5884,"src":5885},"Season 3 Walk GIF by The Simpsons","https://media1.giphy.com/media/3orif1pbMEL5VJnmwM/giphy.gif?cid=bcfb6944ona09wkpkujnf1j7yvcqraf3kve1b8t4wnih6ick&ep=v1_gifs_search&rid=giphy.gif&ct=g",[17,5887,5888],{},"But not all editors are born equal; there are many ways of building one, with benefits and tradeoffs. In this article I'll try to explain what led me to my current solution (not yet completed, but still I'd say it's about 80% done).",[12,5890,5891],{"id":270},"Markdown",[17,5893,5894,5895,5898,5899,5902,5903],{},"if you don't know what markdown is, think of it as kind of like html; it is a type of text with special characters or combination of characters that get interpreted and displayed according to a determined specification; for instance, an html ",[171,5896,5897],{},"\u003Ch2>"," tag would be translated in markdown with ",[171,5900,5901],{},"##"," and vice versa. It is especially useful if you want to quickly write a README, some documentation or, turns out, articles for certain platforms. You can find a more complete introduction that I can provide ",[34,5904,39],{"href":5905,"rel":5906},"https://www.markdownguide.org/",[38],[12,5908,5910],{"id":5909},"what-i-needed-to-build","What I needed to build",[17,5912,5913],{},"I needed to build a markdown editor with a live preview. The first part is not that difficult; put down a textarea on a page and you have your editor! The problem was with the live preview; turns out Markdown is not natively supported by browsers...",[17,5915,5916],{},[130,5917],{"alt":5918,"src":5919},"Excuse Me Wow GIF by Mashable","https://media0.giphy.com/media/l3q2K5jinAlChoCLS/giphy.gif?cid=bcfb6944zbg8lkbkejj0satp1tv1mrykzw8vogwf2sjr4vz7&ep=v1_gifs_search&rid=giphy.gif&ct=g",[17,5921,5922],{},"But you know what is? HTML! So I just needed to build (or find, in this case) a tool that allowed me to convert Markdown to HTML!",[17,5924,5925],{},[130,5926],{"alt":5927,"src":5928},"Tyler Perry Problem Solved GIF by Nickelodeon","https://media2.giphy.com/media/LJbberzGLVChcsdNOv/giphy.gif?cid=bcfb6944h5e0uf8r7n2yyktj224ougmnxeycs0uu8pbdp5qb&ep=v1_gifs_search&rid=giphy.gif&ct=g",[105,5930,5931],{"id":186},"Unified",[17,5933,5934,5935,5938,5939,5944],{},"To build the Markdown editor (and the preview, mostly), I decided to use ",[34,5936,186],{"href":184,"rel":5937},[38],", an ecosystem of tools allowing the developer to parse a format into an abstract tree and back into another format (for example, markdown to html) and modify said tree (for example, to add specific classes to certain html elements before they are converted to an actual html string. The basics of how to do so can be found in ",[34,5940,5943],{"href":5941,"rel":5942},"https://unifiedjs.com/learn/guide/using-unified/",[38],"this article",", but they mostly consist of:",[21,5946,5947,5950,5957,5963],{},[24,5948,5949],{},"reading the markdown content (from the textarea in this case)",[24,5951,5952,5953,5956],{},"using ",[171,5954,5955],{},"remarkParse"," to convert the markdown into a syntax tree",[24,5958,5952,5959,5962],{},[171,5960,5961],{},"remarkRehype"," to convert the markdown syntax tree to an html syntax tree",[24,5964,5952,5965,5968],{},[171,5966,5967],{},"rehypeStringify"," to convert the html syntax tree to an html string",[17,5970,5971],{},[130,5972],{"alt":5973,"src":5974},"GIF by South Park ","https://media4.giphy.com/media/3o7ypD3Ho7jMArHDxe/giphy.gif?cid=bcfb6944tegi9dndgxq1mo0tz531rpudh84d657kl2llua1n&ep=v1_gifs_search&rid=giphy.gif&ct=g",[17,5976,5977,5978,5983],{},"The real magic is what happens once you generate the syntax trees; at that point, you can modify them with the existing plugins (or make you own, if you really want to). For instance, I use a plugin to add specific css classes to certain elements so they integrate better with the visual design of the website another to add code highlighting with ",[34,5979,5982],{"href":5980,"rel":5981},"https://highlightjs.org/",[38],"highlight.js"," and some others for generating a js object from the frontmatter of a Markdown file and to add support for Github flavored Markdown. I could do a lot more with these, like add support for videos, embeds and more, but for now this is enough for a simple preview.",[17,5985,5986],{},[230,5987,5988,5989,5993],{},"NB: remark and rehype are also used by ",[34,5990,2179],{"href":5991,"rel":5992},"https://astro.build",[38]," to render the collection's markdown content, so any plugins you use there can also be used here",[12,5995,5997],{"id":5996},"putting-it-all-together","Putting it all together",[17,5999,6000],{},"Now that we have a basic understanding of how unified works, let's practice! I have already created a stackblitz example you can see just below this paragraph: try writing something in the textarea and it should be parsed and displayed in the preview next to it!",[1223,6002],{"id":6003},"mg-markdown-parser",{"title":240,"searchDepth":241,"depth":241,"links":6005},[6006,6007,6010],{"id":270,"depth":241,"text":5891},{"id":5909,"depth":241,"text":5910,"children":6008},[6009],{"id":186,"depth":248,"text":5931},{"id":5996,"depth":241,"text":5997},{"cover_image":258},"2023-10-02T00:00:00.000Z","---\ntitle: \"building a basic markdown editor: unified, trees and data\"\nseries: \"magiedit\"\ncover_image: ./images/og.png\npublishDate: 2023-10-02\nsummary: \"how to build a simple yet extensible markdown editor\"\nslug: markdown-editor\ntags:\n- svelte\n- sveltekit\n- markdown\n- unified\n---\n\nAs you have probably guessed from the previous articles in this series (go read them if you haven't, btw), the main thing when building a markdown publishing webapp, is the markdown editor itself.\n\n\n\nBut not all editors are born equal; there are many ways of building one, with benefits and tradeoffs. In this article I'll try to explain what led me to my current solution (not yet completed, but still I'd say it's about 80% done).\n\n## Markdown\n\nif you don't know what markdown is, think of it as kind of like html; it is a type of text with special characters or combination of characters that get interpreted and displayed according to a determined specification; for instance, an html `\u003Ch2>` tag would be translated in markdown with `##` and vice versa. It is especially useful if you want to quickly write a README, some documentation or, turns out, articles for certain platforms. You can find a more complete introduction that I can provide [here](https://www.markdownguide.org/)\n\n## What I needed to build\n\nI needed to build a markdown editor with a live preview. The first part is not that difficult; put down a textarea on a page and you have your editor! The problem was with the live preview; turns out Markdown is not natively supported by browsers...\n\n\n\nBut you know what is? HTML! So I just needed to build (or find, in this case) a tool that allowed me to convert Markdown to HTML!\n\n\n\n### Unified\n\nTo build the Markdown editor (and the preview, mostly), I decided to use [unified](https://unifiedjs.com/), an ecosystem of tools allowing the developer to parse a format into an abstract tree and back into another format (for example, markdown to html) and modify said tree (for example, to add specific classes to certain html elements before they are converted to an actual html string. The basics of how to do so can be found in [this article](https://unifiedjs.com/learn/guide/using-unified/), but they mostly consist of:\n\n- reading the markdown content (from the textarea in this case)\n- using `remarkParse` to convert the markdown into a syntax tree\n- using `remarkRehype` to convert the markdown syntax tree to an html syntax tree\n- using `rehypeStringify` to convert the html syntax tree to an html string\n\n\n\nThe real magic is what happens once you generate the syntax trees; at that point, you can modify them with the existing plugins (or make you own, if you really want to). For instance, I use a plugin to add specific css classes to certain elements so they integrate better with the visual design of the website another to add code highlighting with [highlight.js](https://highlightjs.org/) and some others for generating a js object from the frontmatter of a Markdown file and to add support for Github flavored Markdown. I could do a lot more with these, like add support for videos, embeds and more, but for now this is enough for a simple preview.\n\n**NB: remark and rehype are also used by [astro](https://astro.build) to render the collection's markdown content, so any plugins you use there can also be used here**\n\n## Putting it all together\n\nNow that we have a basic understanding of how unified works, let's practice! I have already created a stackblitz example you can see just below this paragraph: try writing something in the textarea and it should be parsed and displayed in the preview next to it!\n\n::stackblitz{#mg-markdown-parser}\n::",{"title":5874,"description":5879},"markdown-editor","articles/markdown-editor","how to build a simple yet extensible markdown editor",[4385,6019,270,186],"sveltekit","W-6zsc7Chkkdkdq85YgnKhi_rW34avAykc3f3CY3sCQ",{"id":6022,"title":6023,"body":6024,"description":240,"extension":256,"meta":6445,"navigation":259,"path":6446,"publishDate":6447,"rawbody":6448,"seo":6449,"series":3776,"slug":6450,"stem":6451,"summary":6452,"tags":6453,"__hash__":6455},"articles/articles/end-to-end-sveltekit.md","end-to-end encryption with sveltekit",{"type":9,"value":6025,"toc":6437},[6026,6030,6039,6043,6047,6050,6055,6059,6063,6068,6076,6080,6087,6090,6095,6205,6210,6277,6282,6351,6356,6395,6400,6403,6420,6427,6431,6434],[12,6027,6029],{"id":6028},"introduction","Introduction",[17,6031,6032,6033,6038],{},"Now that Magiedit is kind of done (except for user experience, I guess), I figured I had to tackle the security / sync of notes; at the beginning of this project, I thought I could store everything locally (first versions did not even have authentication), and later on implement a solution to sync back and forth with a remote server using something like couchdb and pouchdb. When I tried implementing things like those, however, I ran into all kinds of problems, namely making it play nice with sveltekit (and vite) and syncing different models (articles and settings for each user). And so I decided, at least for now, to ditch storing articles locally, and simply store them on a remote db (right now ",[34,6034,6037],{"href":6035,"rel":6036},"https://turso.tech",[38],"turso",", btw). The thing is, I didn't want to store raw article data for reasons like data privacy and privacy in general (I don't want to know what you write using Magiedit). To solve this problem, I started looking into cryptography, specifically the web-cryptography api.",[12,6040,6042],{"id":6041},"how-this-all-works","How this all works",[105,6044,6046],{"id":6045},"genering-a-master-password","Genering a master password",[17,6048,6049],{},"When a user creates an account, it is now required to create a master password; this will then be stored (hashed, obviously) on the server and at each new session they will need to enter it again. This master password is the origin for every cryptographic key used to encrypt and decrypt the articles.",[17,6051,6052],{},[230,6053,6054],{},"NB: except for the master password creation and unlocking, the clear password is never sent to the server and is instead stored in the browser's session storage",[105,6056,6058],{"id":6057},"how-and-where-things-happen","How and where things happen",[5406,6060,6062],{"id":6061},"how-it-works-algorithms-and-stuff","How it works (algorithms and stuff)",[17,6064,6065],{},[230,6066,6067],{},"DISCLAIMER: I'm not an expert in cryptography, so take everything I say with a grain of salt",[21,6069,6070,6073],{},[24,6071,6072],{},"The key is based on a hash of the master password using SHA-256",[24,6074,6075],{},"The algorithm used for encryption is AES-CBC, since it is symmetric and does not make me manage private and public keys (they would work better, but let's be honest; they are just articles, after all)",[5406,6077,6079],{"id":6078},"where-it-works","Where it works",[17,6081,6082,6083],{},"Let's first take a look at this schema:\n",[130,6084],{"alt":6085,"src":6086},"magiedit's data flow diagram","https://cdn.blog.matteogassend.com/magiedit-encryption-flow.png",[17,6088,6089],{},"When a user creates, loads (from a file) or saves an article, the same steps happen:",[21,6091,6092],{},[24,6093,6094],{},"A cryptographic key is generated using the master password hashed using SHA-256",[313,6096,6098],{"className":1387,"code":6097,"language":1389,"meta":240,"style":240},"const keyBytes = await crypto.subtle.digest(\"SHA-256\", new TextEncoder().encode(keyData));\nconst key = await crypto.subtle.importKey(\"raw\", keyBytes, \"AES-CBC\", false, [\"encrypt\"]);\n",[171,6099,6100,6150],{"__ignoreMap":240},[321,6101,6102,6104,6107,6109,6111,6114,6116,6119,6121,6124,6126,6129,6131,6135,6138,6140,6142,6145,6148],{"class":323,"line":324},[321,6103,4209],{"class":344},[321,6105,6106],{"class":348}," keyBytes ",[321,6108,818],{"class":384},[321,6110,4217],{"class":344},[321,6112,6113],{"class":348}," crypto",[321,6115,356],{"class":384},[321,6117,6118],{"class":348},"subtle",[321,6120,356],{"class":384},[321,6122,6123],{"class":373},"digest",[321,6125,377],{"class":348},[321,6127,6128],{"class":431},"\"SHA-256\"",[321,6130,407],{"class":355},[321,6132,6134],{"class":6133},"sPUSY"," new",[321,6136,6137],{"class":373}," TextEncoder",[321,6139,1455],{"class":348},[321,6141,356],{"class":384},[321,6143,6144],{"class":373},"encode",[321,6146,6147],{"class":348},"(keyData))",[321,6149,1404],{"class":355},[321,6151,6152,6154,6157,6159,6161,6163,6165,6167,6169,6172,6174,6177,6179,6182,6184,6187,6189,6192,6194,6197,6200,6203],{"class":323,"line":241},[321,6153,4209],{"class":344},[321,6155,6156],{"class":348}," key ",[321,6158,818],{"class":384},[321,6160,4217],{"class":344},[321,6162,6113],{"class":348},[321,6164,356],{"class":384},[321,6166,6118],{"class":348},[321,6168,356],{"class":384},[321,6170,6171],{"class":373},"importKey",[321,6173,377],{"class":348},[321,6175,6176],{"class":431},"\"raw\"",[321,6178,407],{"class":355},[321,6180,6181],{"class":348}," keyBytes",[321,6183,407],{"class":355},[321,6185,6186],{"class":431}," \"AES-CBC\"",[321,6188,407],{"class":355},[321,6190,6191],{"class":2597}," false",[321,6193,407],{"class":355},[321,6195,6196],{"class":348}," [",[321,6198,6199],{"class":431},"\"encrypt\"",[321,6201,6202],{"class":348},"])",[321,6204,1404],{"class":355},[21,6206,6207],{},[24,6208,6209],{},"We generate an iv (initialization vector, kind of like a seed)",[313,6211,6213],{"className":1387,"code":6212,"language":1389,"meta":240,"style":240},"function generateIv() {\n const data = new Uint8Array(16);\n crypto.getRandomValues(data);\n return data;\n}\n",[171,6214,6215,6226,6248,6263,6273],{"__ignoreMap":240},[321,6216,6217,6219,6222,6224],{"class":323,"line":324},[321,6218,1778],{"class":344},[321,6220,6221],{"class":373}," generateIv",[321,6223,1455],{"class":355},[321,6225,398],{"class":355},[321,6227,6228,6230,6232,6234,6236,6239,6241,6244,6246],{"class":323,"line":241},[321,6229,4890],{"class":344},[321,6231,1878],{"class":348},[321,6233,818],{"class":384},[321,6235,6134],{"class":6133},[321,6237,6238],{"class":373}," Uint8Array",[321,6240,377],{"class":348},[321,6242,6243],{"class":2597},"16",[321,6245,238],{"class":348},[321,6247,1404],{"class":355},[321,6249,6250,6253,6255,6258,6261],{"class":323,"line":248},[321,6251,6252],{"class":348}," crypto",[321,6254,356],{"class":384},[321,6256,6257],{"class":373},"getRandomValues",[321,6259,6260],{"class":348},"(data)",[321,6262,1404],{"class":355},[321,6264,6265,6268,6271],{"class":323,"line":341},[321,6266,6267],{"class":344}," return",[321,6269,6270],{"class":348}," data",[321,6272,1404],{"class":355},[321,6274,6275],{"class":323,"line":362},[321,6276,555],{"class":355},[21,6278,6279],{},[24,6280,6281],{},"The text is then encrypted using the key and the iv",[313,6283,6285],{"className":1387,"code":6284,"language":1389,"meta":240,"style":240},"const encodedContent = await crypto.subtle.encrypt({ name: \"AES-CBC\", iv }, key, new TextEncoder().encode(\"write here\"));\n",[171,6286,6287],{"__ignoreMap":240},[321,6288,6289,6291,6294,6296,6298,6300,6302,6304,6306,6309,6311,6313,6315,6317,6319,6321,6324,6327,6330,6332,6334,6336,6338,6340,6342,6344,6347,6349],{"class":323,"line":324},[321,6290,4209],{"class":344},[321,6292,6293],{"class":348}," encodedContent ",[321,6295,818],{"class":384},[321,6297,4217],{"class":344},[321,6299,6113],{"class":348},[321,6301,356],{"class":384},[321,6303,6118],{"class":348},[321,6305,356],{"class":384},[321,6307,6308],{"class":373},"encrypt",[321,6310,377],{"class":348},[321,6312,2477],{"class":355},[321,6314,4136],{"class":348},[321,6316,1350],{"class":384},[321,6318,6186],{"class":431},[321,6320,407],{"class":355},[321,6322,6323],{"class":348}," iv ",[321,6325,6326],{"class":355},"},",[321,6328,6329],{"class":348}," key",[321,6331,407],{"class":355},[321,6333,6134],{"class":6133},[321,6335,6137],{"class":373},[321,6337,1455],{"class":348},[321,6339,356],{"class":384},[321,6341,6144],{"class":373},[321,6343,377],{"class":348},[321,6345,6346],{"class":431},"\"write here\"",[321,6348,5168],{"class":348},[321,6350,1404],{"class":355},[21,6352,6353],{},[24,6354,6355],{},"It is then converted to base64",[313,6357,6359],{"className":1387,"code":6358,"language":1389,"meta":240,"style":240},"const base64 = btoa(String.fromCharCode(...new Uint8Array(encodedContent)));\n",[171,6360,6361],{"__ignoreMap":240},[321,6362,6363,6365,6368,6370,6373,6376,6378,6381,6383,6385,6388,6390,6393],{"class":323,"line":324},[321,6364,4209],{"class":344},[321,6366,6367],{"class":348}," base64 ",[321,6369,818],{"class":384},[321,6371,6372],{"class":373}," btoa",[321,6374,6375],{"class":348},"(String",[321,6377,356],{"class":384},[321,6379,6380],{"class":373},"fromCharCode",[321,6382,377],{"class":348},[321,6384,4153],{"class":384},[321,6386,6387],{"class":6133},"new",[321,6389,6238],{"class":373},[321,6391,6392],{"class":348},"(encodedContent)))",[321,6394,1404],{"class":355},[21,6396,6397],{},[24,6398,6399],{},"and finally sent to the server (along with its iv) to get it stored securely",[17,6401,6402],{},"As for decrypting, it is the same process, but in reverse:",[21,6404,6405,6408,6411,6414,6417],{},[24,6406,6407],{},"get the article from the db",[24,6409,6410],{},"convert the base64 string to a buffer",[24,6412,6413],{},"generate a key from the master password",[24,6415,6416],{},"decrypt the buffer",[24,6418,6419],{},"convert the buffer back to a string",[17,6421,6422,6426],{},[130,6423],{"alt":6424,"src":6425},"Art Drawing GIF by GEICO","https://media4.giphy.com/media/MXM5QQ3jY7WmcmPwTI/giphy.gif?cid=bcfb6944sukc5vegbsl0xkey1dmb6kxar5fen0smog3hhr32&ep=v1_gifs_search&rid=giphy.gif&ct=g","\nI really like this gif, you know?",[12,6428,6430],{"id":6429},"why-end-to-end-encryption","Why end to end encryption ?",[17,6432,6433],{},"Because I wanted users to know that even if I wanted to (and I don't) read what they wrote before publishing, I couldn't, because I wanted to be in line with current regulations about data privacy and, most of all, because it looked like it could be fun!",[1167,6435,6436],{},"html pre.shiki code .sIF4r, html code.shiki .sIF4r{--shiki-default:#C6A0F6}html pre.shiki code .sFaBz, html code.shiki .sFaBz{--shiki-default:#CAD3F5}html pre.shiki code .sXptk, html code.shiki .sXptk{--shiki-default:#8BD5CA}html pre.shiki code .siK8c, html code.shiki .siK8c{--shiki-default:#8AADF4;--shiki-default-font-style:italic}html pre.shiki code .sSZ1V, html code.shiki .sSZ1V{--shiki-default:#A6DA95}html pre.shiki code .slVFb, html code.shiki .slVFb{--shiki-default:#939AB7}html pre.shiki code .sPUSY, html code.shiki .sPUSY{--shiki-default:#C6A0F6;--shiki-default-font-weight:bold}html pre.shiki code .s7Qn8, html code.shiki .s7Qn8{--shiki-default:#F5A97F}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}",{"title":240,"searchDepth":241,"depth":241,"links":6438},[6439,6440,6444],{"id":6028,"depth":241,"text":6029},{"id":6041,"depth":241,"text":6042,"children":6441},[6442,6443],{"id":6045,"depth":248,"text":6046},{"id":6057,"depth":248,"text":6058},{"id":6429,"depth":241,"text":6430},{"cover_image":258},"/articles/end-to-end-sveltekit","2023-09-25T00:00:00.000Z","---\ntitle: \"end-to-end encryption with sveltekit\"\nseries: \"magiedit\"\ncover_image: ./images/og.png\npublishDate: 2023-09-25\nsummary: \"what is end-to-end encryption and how to implement it with sveltekit\"\nslug: end-to-end-sveltekit\ntags:\n- sveltekit\n- typescript\n- cryptography\n---\n\n## Introduction\n\nNow that Magiedit is kind of done (except for user experience, I guess), I figured I had to tackle the security / sync of notes; at the beginning of this project, I thought I could store everything locally (first versions did not even have authentication), and later on implement a solution to sync back and forth with a remote server using something like couchdb and pouchdb. When I tried implementing things like those, however, I ran into all kinds of problems, namely making it play nice with sveltekit (and vite) and syncing different models (articles and settings for each user). And so I decided, at least for now, to ditch storing articles locally, and simply store them on a remote db (right now [turso](https://turso.tech), btw). The thing is, I didn't want to store raw article data for reasons like data privacy and privacy in general (I don't want to know what you write using Magiedit). To solve this problem, I started looking into cryptography, specifically the web-cryptography api.\n\n## How this all works\n\n### Genering a master password\n\nWhen a user creates an account, it is now required to create a master password; this will then be stored (hashed, obviously) on the server and at each new session they will need to enter it again. This master password is the origin for every cryptographic key used to encrypt and decrypt the articles.\n\n**NB: except for the master password creation and unlocking, the clear password is never sent to the server and is instead stored in the browser's session storage**\n\n### How and where things happen\n\n#### How it works (algorithms and stuff)\n\n**DISCLAIMER: I'm not an expert in cryptography, so take everything I say with a grain of salt**\n\n- The key is based on a hash of the master password using SHA-256\n- The algorithm used for encryption is AES-CBC, since it is symmetric and does not make me manage private and public keys (they would work better, but let's be honest; they are just articles, after all)\n\n#### Where it works\n\nLet's first take a look at this schema:\n\n\nWhen a user creates, loads (from a file) or saves an article, the same steps happen:\n\n- A cryptographic key is generated using the master password hashed using SHA-256\n\n```js\nconst keyBytes = await crypto.subtle.digest(\"SHA-256\", new TextEncoder().encode(keyData));\nconst key = await crypto.subtle.importKey(\"raw\", keyBytes, \"AES-CBC\", false, [\"encrypt\"]);\n```\n\n- We generate an iv (initialization vector, kind of like a seed)\n\n```js\nfunction generateIv() {\n const data = new Uint8Array(16);\n crypto.getRandomValues(data);\n return data;\n}\n```\n\n- The text is then encrypted using the key and the iv\n\n```js\nconst encodedContent = await crypto.subtle.encrypt({ name: \"AES-CBC\", iv }, key, new TextEncoder().encode(\"write here\"));\n```\n\n- It is then converted to base64\n\n```js\nconst base64 = btoa(String.fromCharCode(...new Uint8Array(encodedContent)));\n```\n\n- and finally sent to the server (along with its iv) to get it stored securely\n\nAs for decrypting, it is the same process, but in reverse:\n\n- get the article from the db\n- convert the base64 string to a buffer\n- generate a key from the master password\n- decrypt the buffer\n- convert the buffer back to a string\n\n\nI really like this gif, you know?\n\n## Why end to end encryption ?\n\nBecause I wanted users to know that even if I wanted to (and I don't) read what they wrote before publishing, I couldn't, because I wanted to be in line with current regulations about data privacy and, most of all, because it looked like it could be fun!\n",{"title":6023,"description":240},"end-to-end-sveltekit","articles/end-to-end-sveltekit","what is end-to-end encryption and how to implement it with sveltekit",[6019,3804,6454],"cryptography","uL_KNUrvZD_7SjMuv4YBNTiM8gLB5BN1oVLs2pJ-5ls",{"id":6457,"title":6458,"body":6459,"description":7443,"extension":256,"meta":7444,"navigation":259,"path":7445,"publishDate":7446,"rawbody":7447,"seo":7448,"series":3776,"slug":7449,"stem":7450,"summary":7451,"tags":7452,"__hash__":7453},"articles/articles/publishing-articles-to-multiple-platforms-with-decorators-and-interfaces.md","Publishing articles to multiple platforms with decorators and interfaces",{"type":9,"value":6460,"toc":7436},[6461,6479,6483,6486,6608,6611,6628,6631,7208,7213,7217,7221,7224,7230,7238,7330,7333,7344,7373,7376,7382,7384,7391,7424,7427,7433],[17,6462,6463,6464,6467,6468,1233,6473,6478],{},"I have been working on ",[34,6465,3776],{"href":3774,"rel":6466},[38]," for a while now, and a few days ago I finally merged the pull request implementing one of the main ideas I had when I started building this tool; publishing an article to different platforms at once. For now, the only supported platforms are ",[34,6469,6472],{"href":6470,"rel":6471},"https://dev.to",[38],"dev.to",[34,6474,6477],{"href":6475,"rel":6476},"https://hashnode.com",[38],"hashnode",", but the api allows for easily adding more providers if needed (medium, the fediverse, etc); that api is what I want to talk about, because it is one of the things that took me the longer to work out (and it still isn't exactly done).",[12,6480,6482],{"id":6481},"api-design","API Design",[17,6484,6485],{},"This is the code for the basic api for the platform interface:",[313,6487,6489],{"className":1387,"code":6488,"language":1389,"meta":240,"style":240},"interface IBasePlatform\u003CT> {\n settings: Record\u003Cstring, string>;\n publish(article: Article): void;\n setSettings(settings: UserPreferences[]): T;\n getRequiredSettings(): string[];\n getPlatformName(): string;\n}\n",[171,6490,6491,6508,6528,6552,6575,6591,6604],{"__ignoreMap":240},[321,6492,6493,6496,6499,6501,6504,6506],{"class":323,"line":324},[321,6494,6495],{"class":344},"interface",[321,6497,6498],{"class":352}," IBasePlatform",[321,6500,3885],{"class":3884},[321,6502,6503],{"class":352},"T",[321,6505,3896],{"class":3884},[321,6507,398],{"class":355},[321,6509,6510,6512,6514,6516,6518,6520,6522,6524,6526],{"class":323,"line":241},[321,6511,2583],{"class":348},[321,6513,1350],{"class":384},[321,6515,3881],{"class":352},[321,6517,3885],{"class":3884},[321,6519,3888],{"class":344},[321,6521,407],{"class":355},[321,6523,666],{"class":344},[321,6525,3896],{"class":3884},[321,6527,1404],{"class":355},[321,6529,6530,6533,6535,6538,6540,6543,6545,6547,6550],{"class":323,"line":248},[321,6531,6532],{"class":373}," publish",[321,6534,377],{"class":355},[321,6536,6537],{"class":380},"article",[321,6539,1350],{"class":384},[321,6541,6542],{"class":352}," Article",[321,6544,238],{"class":355},[321,6546,1350],{"class":384},[321,6548,6549],{"class":344}," void",[321,6551,1404],{"class":355},[321,6553,6554,6557,6559,6561,6563,6566,6568,6570,6573],{"class":323,"line":341},[321,6555,6556],{"class":373}," setSettings",[321,6558,377],{"class":355},[321,6560,4160],{"class":380},[321,6562,1350],{"class":384},[321,6564,6565],{"class":352}," UserPreferences[]",[321,6567,238],{"class":355},[321,6569,1350],{"class":384},[321,6571,6572],{"class":352}," T",[321,6574,1404],{"class":355},[321,6576,6577,6580,6582,6584,6586,6589],{"class":323,"line":362},[321,6578,6579],{"class":373}," getRequiredSettings",[321,6581,1455],{"class":355},[321,6583,1350],{"class":384},[321,6585,666],{"class":344},[321,6587,6588],{"class":352},"[]",[321,6590,1404],{"class":355},[321,6592,6593,6596,6598,6600,6602],{"class":323,"line":367},[321,6594,6595],{"class":373}," getPlatformName",[321,6597,1455],{"class":355},[321,6599,1350],{"class":384},[321,6601,666],{"class":344},[321,6603,1404],{"class":355},[321,6605,6606],{"class":323,"line":401},[321,6607,555],{"class":355},[17,6609,6610],{},"It is kind of built on the builder pattern (I'll eventually write an article on that), and allows to generalize the declaration and use of any implementation of this class because each of these has the same exact invocation method (the basics of an interface, I know, but sometimes it's necessary to restate the basics).",[21,6612,6613,6616,6619,6622,6625],{},[24,6614,6615],{},"settings: holds all the specific settings required to use this implementation (api keys, tokens and whatnot)",[24,6617,6618],{},"setSettings: loops over the settings and gets those required by the implementation (not very safe, but right now it what I found)",[24,6620,6621],{},"getRequiredSettings: returns a list of keys required for the integration to work (this usually works within setSettings)",[24,6623,6624],{},"getPlatformName: pretty self explanatory, usually used for logging of successful or unsuccesful publishing",[24,6626,6627],{},"publish: actually handles all the publishing step (mostly handling the api call to the platform with required formatting, headers and body)",[17,6629,6630],{},"Let's take a look at a concrete example; the dev.to implementation:",[313,6632,6634],{"className":1387,"code":6633,"language":1389,"meta":240,"style":240},"class DevPlatform implements IBasePlatform\u003CDevPlatform> {\n settings: Record\u003Cstring, string> = {};\n public getRequiredSettings(): string[] {\n return ['dev'];\n }\n\n getPlatformName(): string {\n return 'dev.to';\n }\n\n setSettings(settings: UserPreferences[]) {\n settings.forEach((e) => {\n if (this.getRequiredSettings().includes(e.key.split(':')[1])) {\n this.settings[e.key.split(':')[1]] = e.value;\n }\n });\n return this;\n }\n\n public async publish(article: Article) {\n const setting = this.settings['dev'];\n if (!setting) throw new Error('could not find required settings');\n const res = await fetch('https://dev.to/api/articles', {\n method: 'post',\n body: JSON.stringify({\n article: {\n title: article.title,\n body_markdown: article.content,\n published: article.published\n }\n }),\n headers: {\n accept: 'application/vnd.forem.api-v1+json',\n 'content-type': 'application/json',\n 'api-key': setting\n }\n });\n if (!res.ok) {\n throw new Error('something went wrong');\n }\n }\n}\n",[171,6635,6636,6657,6680,6698,6712,6716,6720,6732,6741,6745,6749,6765,6787,6835,6875,6879,6887,6896,6900,6904,6925,6948,6977,7000,7012,7031,7040,7057,7072,7086,7090,7099,7108,7120,7132,7142,7146,7155,7174,7193,7198,7203],{"__ignoreMap":240},[321,6637,6638,6640,6643,6646,6648,6650,6653,6655],{"class":323,"line":324},[321,6639,1436],{"class":344},[321,6641,6642],{"class":352}," DevPlatform",[321,6644,6645],{"class":344}," implements",[321,6647,6498],{"class":352},[321,6649,3885],{"class":3884},[321,6651,6652],{"class":352},"DevPlatform",[321,6654,3896],{"class":3884},[321,6656,398],{"class":355},[321,6658,6659,6661,6663,6665,6667,6669,6671,6673,6675,6677],{"class":323,"line":241},[321,6660,2583],{"class":348},[321,6662,1350],{"class":384},[321,6664,3881],{"class":352},[321,6666,3885],{"class":3884},[321,6668,3888],{"class":344},[321,6670,407],{"class":355},[321,6672,666],{"class":344},[321,6674,3896],{"class":3884},[321,6676,2573],{"class":384},[321,6678,6679],{"class":355}," {};\n",[321,6681,6682,6685,6688,6690,6692,6694,6696],{"class":323,"line":248},[321,6683,6684],{"class":344}," public",[321,6686,6687],{"class":373}," getRequiredSettings",[321,6689,1455],{"class":355},[321,6691,1350],{"class":384},[321,6693,666],{"class":344},[321,6695,6588],{"class":352},[321,6697,398],{"class":355},[321,6699,6700,6703,6705,6708,6710],{"class":323,"line":341},[321,6701,6702],{"class":344}," return",[321,6704,6196],{"class":348},[321,6706,6707],{"class":431},"'dev'",[321,6709,1290],{"class":348},[321,6711,1404],{"class":355},[321,6713,6714],{"class":323,"line":362},[321,6715,476],{"class":355},[321,6717,6718],{"class":323,"line":367},[321,6719,333],{"emptyLinePlaceholder":259},[321,6721,6722,6724,6726,6728,6730],{"class":323,"line":401},[321,6723,6595],{"class":373},[321,6725,1455],{"class":355},[321,6727,1350],{"class":384},[321,6729,666],{"class":344},[321,6731,398],{"class":355},[321,6733,6734,6736,6739],{"class":323,"line":438},[321,6735,6702],{"class":344},[321,6737,6738],{"class":431}," 'dev.to'",[321,6740,1404],{"class":355},[321,6742,6743],{"class":323,"line":455},[321,6744,476],{"class":355},[321,6746,6747],{"class":323,"line":473},[321,6748,333],{"emptyLinePlaceholder":259},[321,6750,6751,6753,6755,6757,6759,6761,6763],{"class":323,"line":479},[321,6752,6556],{"class":373},[321,6754,377],{"class":355},[321,6756,4160],{"class":380},[321,6758,1350],{"class":384},[321,6760,6565],{"class":352},[321,6762,238],{"class":355},[321,6764,398],{"class":355},[321,6766,6767,6770,6772,6774,6776,6778,6781,6783,6785],{"class":323,"line":512},[321,6768,6769],{"class":348}," settings",[321,6771,356],{"class":384},[321,6773,5009],{"class":373},[321,6775,377],{"class":348},[321,6777,377],{"class":355},[321,6779,6780],{"class":380},"e",[321,6782,238],{"class":355},[321,6784,4556],{"class":344},[321,6786,398],{"class":355},[321,6788,6789,6791,6793,6795,6797,6800,6802,6804,6807,6810,6812,6815,6817,6820,6822,6825,6828,6830,6833],{"class":323,"line":534},[321,6790,1828],{"class":344},[321,6792,1817],{"class":348},[321,6794,1563],{"class":449},[321,6796,356],{"class":384},[321,6798,6799],{"class":373},"getRequiredSettings",[321,6801,1455],{"class":348},[321,6803,356],{"class":384},[321,6805,6806],{"class":373},"includes",[321,6808,6809],{"class":348},"(e",[321,6811,356],{"class":384},[321,6813,6814],{"class":348},"key",[321,6816,356],{"class":384},[321,6818,6819],{"class":373},"split",[321,6821,377],{"class":348},[321,6823,6824],{"class":431},"':'",[321,6826,6827],{"class":348},")[",[321,6829,5110],{"class":2597},[321,6831,6832],{"class":348},"])) ",[321,6834,1732],{"class":355},[321,6836,6837,6840,6842,6845,6847,6849,6851,6853,6855,6857,6859,6861,6864,6866,6869,6871,6873],{"class":323,"line":552},[321,6838,6839],{"class":449}," this",[321,6841,356],{"class":384},[321,6843,6844],{"class":348},"settings[e",[321,6846,356],{"class":384},[321,6848,6814],{"class":348},[321,6850,356],{"class":384},[321,6852,6819],{"class":373},[321,6854,377],{"class":348},[321,6856,6824],{"class":431},[321,6858,6827],{"class":348},[321,6860,5110],{"class":2597},[321,6862,6863],{"class":348},"]] ",[321,6865,818],{"class":384},[321,6867,6868],{"class":348}," e",[321,6870,356],{"class":384},[321,6872,4100],{"class":348},[321,6874,1404],{"class":355},[321,6876,6877],{"class":323,"line":891},[321,6878,2065],{"class":355},[321,6880,6881,6883,6885],{"class":323,"line":897},[321,6882,2077],{"class":355},[321,6884,238],{"class":348},[321,6886,1404],{"class":355},[321,6888,6889,6891,6894],{"class":323,"line":902},[321,6890,6702],{"class":344},[321,6892,6893],{"class":449}," this",[321,6895,1404],{"class":355},[321,6897,6898],{"class":323,"line":907},[321,6899,476],{"class":355},[321,6901,6902],{"class":323,"line":930},[321,6903,333],{"emptyLinePlaceholder":259},[321,6905,6906,6908,6910,6913,6915,6917,6919,6921,6923],{"class":323,"line":947},[321,6907,6684],{"class":344},[321,6909,4875],{"class":344},[321,6911,6912],{"class":373}," publish",[321,6914,377],{"class":355},[321,6916,6537],{"class":380},[321,6918,1350],{"class":384},[321,6920,6542],{"class":352},[321,6922,238],{"class":355},[321,6924,398],{"class":355},[321,6926,6927,6930,6933,6935,6937,6939,6942,6944,6946],{"class":323,"line":967},[321,6928,6929],{"class":344}," const",[321,6931,6932],{"class":348}," setting ",[321,6934,818],{"class":384},[321,6936,6893],{"class":449},[321,6938,356],{"class":384},[321,6940,6941],{"class":348},"settings[",[321,6943,6707],{"class":431},[321,6945,1290],{"class":348},[321,6947,1404],{"class":355},[321,6949,6950,6953,6955,6957,6960,6963,6965,6968,6970,6973,6975],{"class":323,"line":984},[321,6951,6952],{"class":344}," if",[321,6954,1817],{"class":348},[321,6956,1983],{"class":384},[321,6958,6959],{"class":348},"setting) ",[321,6961,6962],{"class":344},"throw",[321,6964,6134],{"class":6133},[321,6966,6967],{"class":373}," Error",[321,6969,377],{"class":348},[321,6971,6972],{"class":431},"'could not find required settings'",[321,6974,238],{"class":348},[321,6976,1404],{"class":355},[321,6978,6979,6981,6984,6986,6988,6991,6993,6996,6998],{"class":323,"line":2068},[321,6980,6929],{"class":344},[321,6982,6983],{"class":348}," res ",[321,6985,818],{"class":384},[321,6987,4217],{"class":344},[321,6989,6990],{"class":373}," fetch",[321,6992,377],{"class":348},[321,6994,6995],{"class":431},"'https://dev.to/api/articles'",[321,6997,407],{"class":355},[321,6999,398],{"class":355},[321,7001,7002,7005,7007,7010],{"class":323,"line":2074},[321,7003,7004],{"class":348}," method",[321,7006,1350],{"class":384},[321,7008,7009],{"class":431}," 'post'",[321,7011,3943],{"class":355},[321,7013,7014,7017,7019,7022,7024,7027,7029],{"class":323,"line":2082},[321,7015,7016],{"class":348}," body",[321,7018,1350],{"class":384},[321,7020,7021],{"class":2597}," JSON",[321,7023,356],{"class":384},[321,7025,7026],{"class":373},"stringify",[321,7028,377],{"class":348},[321,7030,1732],{"class":355},[321,7032,7033,7036,7038],{"class":323,"line":2087},[321,7034,7035],{"class":348}," article",[321,7037,1350],{"class":384},[321,7039,398],{"class":355},[321,7041,7042,7045,7047,7050,7052,7055],{"class":323,"line":2868},[321,7043,7044],{"class":348}," title",[321,7046,1350],{"class":384},[321,7048,7049],{"class":348}," article",[321,7051,356],{"class":384},[321,7053,7054],{"class":348},"title",[321,7056,3943],{"class":355},[321,7058,7059,7062,7064,7066,7068,7070],{"class":323,"line":2874},[321,7060,7061],{"class":348}," body_markdown",[321,7063,1350],{"class":384},[321,7065,7049],{"class":348},[321,7067,356],{"class":384},[321,7069,3689],{"class":348},[321,7071,3943],{"class":355},[321,7073,7074,7077,7079,7081,7083],{"class":323,"line":2880},[321,7075,7076],{"class":348}," published",[321,7078,1350],{"class":384},[321,7080,7049],{"class":348},[321,7082,356],{"class":384},[321,7084,7085],{"class":348},"published\n",[321,7087,7088],{"class":323,"line":2886},[321,7089,2013],{"class":355},[321,7091,7092,7095,7097],{"class":323,"line":2892},[321,7093,7094],{"class":355}," }",[321,7096,238],{"class":348},[321,7098,3943],{"class":355},[321,7100,7101,7104,7106],{"class":323,"line":2898},[321,7102,7103],{"class":348}," headers",[321,7105,1350],{"class":384},[321,7107,398],{"class":355},[321,7109,7110,7113,7115,7118],{"class":323,"line":2903},[321,7111,7112],{"class":348}," accept",[321,7114,1350],{"class":384},[321,7116,7117],{"class":431}," 'application/vnd.forem.api-v1+json'",[321,7119,3943],{"class":355},[321,7121,7122,7125,7127,7130],{"class":323,"line":2909},[321,7123,7124],{"class":431}," 'content-type'",[321,7126,1350],{"class":384},[321,7128,7129],{"class":431}," 'application/json'",[321,7131,3943],{"class":355},[321,7133,7134,7137,7139],{"class":323,"line":2915},[321,7135,7136],{"class":431}," 'api-key'",[321,7138,1350],{"class":384},[321,7140,7141],{"class":348}," setting\n",[321,7143,7144],{"class":323,"line":2923},[321,7145,2065],{"class":355},[321,7147,7149,7151,7153],{"class":323,"line":7148},37,[321,7150,2077],{"class":355},[321,7152,238],{"class":348},[321,7154,1404],{"class":355},[321,7156,7158,7160,7162,7164,7167,7169,7172],{"class":323,"line":7157},38,[321,7159,6952],{"class":344},[321,7161,1817],{"class":348},[321,7163,1983],{"class":384},[321,7165,7166],{"class":348},"res",[321,7168,356],{"class":384},[321,7170,7171],{"class":348},"ok) ",[321,7173,1732],{"class":355},[321,7175,7177,7180,7182,7184,7186,7189,7191],{"class":323,"line":7176},39,[321,7178,7179],{"class":344}," throw",[321,7181,6134],{"class":6133},[321,7183,6967],{"class":373},[321,7185,377],{"class":348},[321,7187,7188],{"class":431},"'something went wrong'",[321,7190,238],{"class":348},[321,7192,1404],{"class":355},[321,7194,7196],{"class":323,"line":7195},40,[321,7197,2071],{"class":355},[321,7199,7201],{"class":323,"line":7200},41,[321,7202,476],{"class":355},[321,7204,7206],{"class":323,"line":7205},42,[321,7207,555],{"class":355},[17,7209,7210],{},[230,7211,7212],{},"NB: this does not yet support tags matching for the different platforms: that will come in a future release",[12,7214,7216],{"id":7215},"registering-a-new-platform-provider","Registering a new platform provider",[105,7218,7220],{"id":7219},"typescript-decorators","Typescript Decorators",[17,7222,7223],{},"Now that we have a (mostly) working platform implementation, how do let the program know? By using decorators!",[17,7225,7226],{},[130,7227],{"alt":7228,"src":7229},"Season 6 What GIF by The Office","https://media3.giphy.com/media/ghuvaCOI6GOoTX0RmH/giphy.gif?cid=bcfb69442kgj0xwn63swq7t1t97b3irfiuh163t7c92wwgcn&ep=v1_gifs_search&rid=giphy.gif&ct=g",[17,7231,7232,7233,7237],{},"Decorators are a way to \"decorate\" methods and classes; what this means is it can execute functions on classes and methods (more info ",[34,7234,39],{"href":7235,"rel":7236},"https://www.typescriptlang.org/docs/handbook/decorators.html",[38],"). The way I decided to use it is by creating a list of registered implementations that gets an element pushed each time a class gets \"decorated\"; let's take a look.",[313,7239,7241],{"className":1387,"code":7240,"language":1389,"meta":240,"style":240},"const supportedPlatforms: (new () => IBasePlatform\u003Cany>)[] = [];\nfunction RegisterPlatform(constructor: new () => IBasePlatform\u003Cany>) {\n supportedPlatforms.push(constructor);\n}\n",[171,7242,7243,7279,7311,7326],{"__ignoreMap":240},[321,7244,7245,7247,7250,7252,7254,7256,7258,7260,7262,7264,7267,7269,7272,7274,7277],{"class":323,"line":324},[321,7246,4209],{"class":344},[321,7248,7249],{"class":348}," supportedPlatforms",[321,7251,1350],{"class":384},[321,7253,1817],{"class":352},[321,7255,6387],{"class":344},[321,7257,4553],{"class":355},[321,7259,4556],{"class":344},[321,7261,6498],{"class":352},[321,7263,3885],{"class":3884},[321,7265,7266],{"class":344},"any",[321,7268,3896],{"class":3884},[321,7270,7271],{"class":352},")[] ",[321,7273,818],{"class":384},[321,7275,7276],{"class":348}," []",[321,7278,1404],{"class":355},[321,7280,7281,7283,7286,7288,7291,7293,7295,7297,7299,7301,7303,7305,7307,7309],{"class":323,"line":241},[321,7282,1778],{"class":344},[321,7284,7285],{"class":373}," RegisterPlatform",[321,7287,377],{"class":355},[321,7289,7290],{"class":380},"constructor",[321,7292,1350],{"class":384},[321,7294,6134],{"class":344},[321,7296,4553],{"class":355},[321,7298,4556],{"class":344},[321,7300,6498],{"class":352},[321,7302,3885],{"class":3884},[321,7304,7266],{"class":344},[321,7306,3896],{"class":3884},[321,7308,238],{"class":355},[321,7310,398],{"class":355},[321,7312,7313,7316,7318,7321,7324],{"class":323,"line":248},[321,7314,7315],{"class":348}," supportedPlatforms",[321,7317,356],{"class":384},[321,7319,7320],{"class":373},"push",[321,7322,7323],{"class":348},"(constructor)",[321,7325,1404],{"class":355},[321,7327,7328],{"class":323,"line":341},[321,7329,555],{"class":355},[17,7331,7332],{},"These are the two sections used (NB: this is still not final, but for now it works enough)",[21,7334,7335,7341],{},[24,7336,7337,7338,7340],{},"supportedPlatforms: the list of platforms with said decorator; the typing of the variables specifies that they must be something having a ",[171,7339,6387],{}," method that returns an implementation of the interface we saw above.",[24,7342,7343],{},"RegisterPlatform: the actual decorator function that can be used like this:",[313,7345,7347],{"className":1387,"code":7346,"language":1389,"meta":240,"style":240},"@RegisterPlatform\nclass DevPlatform implements IBasePlatform\u003CDevPlatform>\n",[171,7348,7349,7357],{"__ignoreMap":240},[321,7350,7351,7354],{"class":323,"line":324},[321,7352,7353],{"class":2597},"@",[321,7355,7356],{"class":348},"RegisterPlatform\n",[321,7358,7359,7361,7363,7365,7367,7369,7371],{"class":323,"line":241},[321,7360,1436],{"class":344},[321,7362,6642],{"class":352},[321,7364,6645],{"class":344},[321,7366,6498],{"class":352},[321,7368,3885],{"class":3884},[321,7370,6652],{"class":352},[321,7372,4110],{"class":3884},[17,7374,7375],{},"Having defined these methods, if we decorate the dev.to implementation we saw above, it will get added to the list.",[17,7377,7378,7381],{},[230,7379,7380],{},"ATTENTION",": there is a caveat here, because you need to import the file containing the implementation somewhere, otherwise it will just get skipped. In magiedit, this was solved by having an index file importing all the needed implementations.",[12,7383,5997],{"id":5996},[17,7385,7386,7387,7390],{},"Now we have a list of all the possible implementations in a single variable and all with the same methods, but how do we use them?\nFor each element of the list (here ",[171,7388,7389],{},"platform","), we instantiate it, call getSettings() and run publish:",[313,7392,7394],{"className":1387,"code":7393,"language":1389,"meta":240,"style":240},"await new platform().setSettings(tokens).publish();\n",[171,7395,7396],{"__ignoreMap":240},[321,7397,7398,7400,7402,7405,7407,7409,7412,7415,7417,7420,7422],{"class":323,"line":324},[321,7399,4272],{"class":344},[321,7401,6134],{"class":6133},[321,7403,7404],{"class":373}," platform",[321,7406,1455],{"class":348},[321,7408,356],{"class":384},[321,7410,7411],{"class":373},"setSettings",[321,7413,7414],{"class":348},"(tokens)",[321,7416,356],{"class":384},[321,7418,7419],{"class":373},"publish",[321,7421,1455],{"class":348},[321,7423,1404],{"class":355},[17,7425,7426],{},"And voilà!",[17,7428,7429],{},[130,7430],{"alt":7431,"src":7432},"Season 9 Thank You GIF by The Office","https://media3.giphy.com/media/1BFEEIo4h1BuTH8eqP/giphy.gif?cid=bcfb69442ubf7xftc9bpq6n6h9a2q3d5gi0q7ch5tr0w2sn3&ep=v1_gifs_search&rid=giphy.gif&ct=g",[1167,7434,7435],{},"html pre.shiki code .sIF4r, html code.shiki .sIF4r{--shiki-default:#C6A0F6}html pre.shiki code .s80kZ, html code.shiki .s80kZ{--shiki-default:#EED49F;--shiki-default-font-style:italic}html pre.shiki code .sjARh, html code.shiki .sjARh{--shiki-default:#91D7E3}html pre.shiki code .slVFb, html code.shiki .slVFb{--shiki-default:#939AB7}html pre.shiki code .sFaBz, html code.shiki .sFaBz{--shiki-default:#CAD3F5}html pre.shiki code .sXptk, html code.shiki .sXptk{--shiki-default:#8BD5CA}html pre.shiki code .siK8c, html code.shiki .siK8c{--shiki-default:#8AADF4;--shiki-default-font-style:italic}html pre.shiki code .skVQi, html code.shiki .skVQi{--shiki-default:#EE99A0;--shiki-default-font-style:italic}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sSZ1V, html code.shiki .sSZ1V{--shiki-default:#A6DA95}html pre.shiki code .s4s3B, html code.shiki .s4s3B{--shiki-default:#ED8796}html pre.shiki code .s7Qn8, html code.shiki .s7Qn8{--shiki-default:#F5A97F}html pre.shiki code .sPUSY, html code.shiki .sPUSY{--shiki-default:#C6A0F6;--shiki-default-font-weight:bold}",{"title":240,"searchDepth":241,"depth":241,"links":7437},[7438,7439,7442],{"id":6481,"depth":241,"text":6482},{"id":7215,"depth":241,"text":7216,"children":7440},[7441],{"id":7219,"depth":248,"text":7220},{"id":5996,"depth":241,"text":5997},"I have been working on magiedit for a while now, and a few days ago I finally merged the pull request implementing one of the main ideas I had when I started building this tool; publishing an article to different platforms at once. For now, the only supported platforms are dev.to and hashnode, but the api allows for easily adding more providers if needed (medium, the fediverse, etc); that api is what I want to talk about, because it is one of the things that took me the longer to work out (and it still isn't exactly done).",{"cover_image":258},"/articles/publishing-articles-to-multiple-platforms-with-decorators-and-interfaces","2023-09-18T00:00:00.000Z","---\ntitle: \"Publishing articles to multiple platforms with decorators and interfaces\"\ncover_image: ./images/og.png\npublishDate: 2023-09-18\nseries: \"magiedit\"\nsummary: \"typescript decorators, interfaces and classes to create publishers for multiple platforms\"\nslug: publishing-articles-to-multiple-platforms-with-decorators-and-interfaces\ntags:\n- sveltekit\n- typescript\n---\n\nI have been working on [magiedit](https://magiedit.magitools.app) for a while now, and a few days ago I finally merged the pull request implementing one of the main ideas I had when I started building this tool; publishing an article to different platforms at once. For now, the only supported platforms are [dev.to](https://dev.to) and [hashnode](https://hashnode.com), but the api allows for easily adding more providers if needed (medium, the fediverse, etc); that api is what I want to talk about, because it is one of the things that took me the longer to work out (and it still isn't exactly done).\n\n## API Design\n\nThis is the code for the basic api for the platform interface:\n\n```js\ninterface IBasePlatform\u003CT> {\n\tsettings: Record\u003Cstring, string>;\n\tpublish(article: Article): void;\n\tsetSettings(settings: UserPreferences[]): T;\n\tgetRequiredSettings(): string[];\n\tgetPlatformName(): string;\n}\n```\n\nIt is kind of built on the builder pattern (I'll eventually write an article on that), and allows to generalize the declaration and use of any implementation of this class because each of these has the same exact invocation method (the basics of an interface, I know, but sometimes it's necessary to restate the basics).\n\n- settings: holds all the specific settings required to use this implementation (api keys, tokens and whatnot)\n- setSettings: loops over the settings and gets those required by the implementation (not very safe, but right now it what I found)\n- getRequiredSettings: returns a list of keys required for the integration to work (this usually works within setSettings)\n- getPlatformName: pretty self explanatory, usually used for logging of successful or unsuccesful publishing\n- publish: actually handles all the publishing step (mostly handling the api call to the platform with required formatting, headers and body)\n\nLet's take a look at a concrete example; the dev.to implementation:\n\n```js\nclass DevPlatform implements IBasePlatform\u003CDevPlatform> {\n\tsettings: Record\u003Cstring, string> = {};\n\tpublic getRequiredSettings(): string[] {\n\t\treturn ['dev'];\n\t}\n\n\tgetPlatformName(): string {\n\t\treturn 'dev.to';\n\t}\n\n\tsetSettings(settings: UserPreferences[]) {\n\t\tsettings.forEach((e) => {\n\t\t\tif (this.getRequiredSettings().includes(e.key.split(':')[1])) {\n\t\t\t\tthis.settings[e.key.split(':')[1]] = e.value;\n\t\t\t}\n\t\t});\n\t\treturn this;\n\t}\n\n\tpublic async publish(article: Article) {\n\t\tconst setting = this.settings['dev'];\n\t\tif (!setting) throw new Error('could not find required settings');\n\t\tconst res = await fetch('https://dev.to/api/articles', {\n\t\t\tmethod: 'post',\n\t\t\tbody: JSON.stringify({\n\t\t\t\tarticle: {\n\t\t\t\t\ttitle: article.title,\n\t\t\t\t\tbody_markdown: article.content,\n\t\t\t\t\tpublished: article.published\n\t\t\t\t}\n\t\t\t}),\n\t\t\theaders: {\n\t\t\t\taccept: 'application/vnd.forem.api-v1+json',\n\t\t\t\t'content-type': 'application/json',\n\t\t\t\t'api-key': setting\n\t\t\t}\n\t\t});\n\t\tif (!res.ok) {\n\t\t\tthrow new Error('something went wrong');\n\t\t}\n\t}\n}\n```\n\n**NB: this does not yet support tags matching for the different platforms: that will come in a future release**\n\n## Registering a new platform provider\n\n### Typescript Decorators\n\nNow that we have a (mostly) working platform implementation, how do let the program know? By using decorators!\n\n\n\nDecorators are a way to \"decorate\" methods and classes; what this means is it can execute functions on classes and methods (more info [here](https://www.typescriptlang.org/docs/handbook/decorators.html)). The way I decided to use it is by creating a list of registered implementations that gets an element pushed each time a class gets \"decorated\"; let's take a look.\n\n```js\nconst supportedPlatforms: (new () => IBasePlatform\u003Cany>)[] = [];\nfunction RegisterPlatform(constructor: new () => IBasePlatform\u003Cany>) {\n\tsupportedPlatforms.push(constructor);\n}\n```\n\nThese are the two sections used (NB: this is still not final, but for now it works enough)\n\n- supportedPlatforms: the list of platforms with said decorator; the typing of the variables specifies that they must be something having a `new` method that returns an implementation of the interface we saw above.\n- RegisterPlatform: the actual decorator function that can be used like this:\n\n```js\n@RegisterPlatform\nclass DevPlatform implements IBasePlatform\u003CDevPlatform>\n```\n\nHaving defined these methods, if we decorate the dev.to implementation we saw above, it will get added to the list.\n\n**ATTENTION**: there is a caveat here, because you need to import the file containing the implementation somewhere, otherwise it will just get skipped. In magiedit, this was solved by having an index file importing all the needed implementations.\n\n## Putting it all together\n\nNow we have a list of all the possible implementations in a single variable and all with the same methods, but how do we use them?\nFor each element of the list (here `platform`), we instantiate it, call getSettings() and run publish:\n\n```js\nawait new platform().setSettings(tokens).publish();\n```\n\nAnd voilà!\n\n\n",{"title":6458,"description":7443},"publishing-articles-to-multiple-platforms-with-decorators-and-interfaces","articles/publishing-articles-to-multiple-platforms-with-decorators-and-interfaces","typescript decorators, interfaces and classes to create publishers for multiple platforms",[6019,3804],"gpuhc18Soxn3YPW6s2Hbsy16onek84ohKVsI_w-CJ_E",{"id":7455,"title":7456,"body":7457,"description":8333,"extension":256,"meta":8334,"navigation":259,"path":8335,"publishDate":8336,"rawbody":8337,"seo":8338,"series":3776,"slug":8339,"stem":8340,"summary":8341,"tags":8342,"__hash__":8345},"articles/articles/file-uploads-with-sveltekit-and-cloudflare-cover.md","File Uploads with Sveltekit and Cloudflare",{"type":9,"value":7458,"toc":8325},[7459,7480,7489,7495,7499,7506,7509,7513,7516,7522,7526,7529,7532,7985,7988,8049,8056,8060,8066,8307,8310,8315,8319,8322],[17,7460,7461,7462,7466,7467,1233,7471,7475,7476],{},"I am currently building ",[34,7463,3776],{"href":7464,"rel":7465},"https://github.com/magitools/magiedit",[38],", a markdown editor that also allows publishing to different platforms (like ",[34,7468,7470],{"href":6475,"rel":7469},[38],"Hashnode",[34,7472,7474],{"href":6470,"rel":7473},[38],"Dev.to",") and decided to add some shortcuts/commands to make my life simpler (since I am building this tool primarily for myself xD), including adding images from unsplash and gifs and giphy because\n",[130,7477],{"alt":7478,"src":7479},"Old Man What GIF by Amazon Prime Video","https://media0.giphy.com/media/FEBDBbLFT9px3da0vT/giphy.gif?cid=bcfb6944bd46uy7laheqck7cr13jjhkkjlsv9lkz8j64mnz6&ep=v1_gifs_search&rid=giphy.gif&ct=g",[17,7481,7482,7483,7488],{},"For one of the features I'm working on that will allow users to use dall-e to generate images (and cover images based on their article's content as soon I can get that to working) I needed to store the selected images (since openais urls only last for 1 hour and storing 4mb of base64 data in an article makes editing it awful. For this, I turned to ",[34,7484,7487],{"href":7485,"rel":7486},"https://www.cloudflare.com/fr-fr/developer-platform/r2/",[38],"cloudflare's r2 storage",", because I already use their solution for my blog's images and my dns management (plus, their free tier is pretty generous).",[17,7490,7491],{},[130,7492],{"alt":7493,"src":7494},"Jimmy Fallon Free Stuff GIF by The Tonight Show Starring Jimmy Fallon","https://media3.giphy.com/media/3tpzkqpbVdshXX1By7/giphy.gif?cid=bcfb69442ar464da1u8i5rc10qqbj90qmbbtmr2g269feyn6&ep=v1_gifs_search&rid=giphy.gif&ct=g",[12,7496,7498],{"id":7497},"using-cloudflare-r2-storage","Using Cloudflare R2 Storage",[17,7500,7501,7502,356],{},"Cloudflare's R2 Storage is an S3 (the one from Amazon, exactly) compatible storage solution, meaning you can interact with it in \"almost\" the say way you would with an S3 bucket (there are some methods that are not supported and that you can find ",[34,7503,39],{"href":7504,"rel":7505},"https://developers.cloudflare.com/r2/",[38],[17,7507,7508],{},"So, for my use case, I created a bucket and paired it with a domain name and then generated an access key to that specific bucket (so that I could upload things from my sveltekit api function). Again, the link above should provide all instructions on how to get these informations and what they all mean. Now that I had a bucket, let's get to the heart of this article, which is how to interact with a bucket using sveltekit (try saying that sentence out of context).",[12,7510,7512],{"id":7511},"sveltekit-integration","Sveltekit integration",[17,7514,7515],{},"Now for the fun part; using all this in sveltekit. Let me preface this section by saying that I had specific needs in this application that required something of an unorthodox architecture. Since the application is storing user articles, I wanted those to be offline first, as well as other potential informations required for using the application (like tokens for publishing and stuff); this means that I had to create api routes instead of handling all of this in form actions. Having said all this, let's see how it all works.",[17,7517,7518],{},[130,7519],{"alt":7520,"src":7521},"Lets Go Start GIF","https://media3.giphy.com/media/3aGZA6WLI9Jde/giphy.gif?cid=bcfb69440en0a56he59l99bvpxc33352js92uu1b4j031q3u&ep=v1_gifs_search&rid=giphy.gif&ct=g",[105,7523,7525],{"id":7524},"getting-the-images","Getting the images",[17,7527,7528],{},"For my specific use case, I needed to store images generated by dall-e (openai), so that meant calling that service; said service returns either a base64 string containing the image or an url (that expires after 1 hour). Passing around a 4mb string didn't sound like a good idea (trust me, I tried), so I was left with the url.",[17,7530,7531],{},"This is the (hopefully) final code I came up with:",[313,7533,7535],{"className":1387,"code":7534,"language":1389,"meta":240,"style":240},"export const POST: RequestHandler = async ({ locals, request }) => {\n const session = await locals.auth.validate();\n if (!session) {\n throw error(401, { message: 'not authorized' });\n }\n const formData = await request.formData();\n const { content, description } = Object.fromEntries(formData);\n if (!content) {\n throw error(500, { message: 'invalid input' });\n }\n const arrayBuffer = await (await fetch(content.toString())).arrayBuffer();\n const data = Buffer.from(new Uint8Array(arrayBuffer));\n const key = `${session?.user?.userId}_${uuidv4()}`;\n const url = await saveToBucket(data, key);\n // get data from function\n await db.insert(userImages).values({\n url,\n userId: session?.user.userId,\n description: description?.toString()\n });\n return json({ message: 'ok', url });\n};\n",[171,7536,7537,7573,7600,7613,7644,7648,7668,7696,7709,7737,7741,7778,7805,7850,7874,7879,7901,7908,7928,7945,7954,7981],{"__ignoreMap":240},[321,7538,7539,7542,7544,7547,7549,7552,7554,7556,7559,7562,7564,7566,7569,7571],{"class":323,"line":324},[321,7540,7541],{"class":344},"export",[321,7543,4944],{"class":344},[321,7545,7546],{"class":373}," POST",[321,7548,1350],{"class":384},[321,7550,7551],{"class":352}," RequestHandler ",[321,7553,818],{"class":384},[321,7555,4875],{"class":344},[321,7557,7558],{"class":355}," ({",[321,7560,7561],{"class":380}," locals",[321,7563,407],{"class":355},[321,7565,4220],{"class":380},[321,7567,7568],{"class":355}," })",[321,7570,4556],{"class":344},[321,7572,398],{"class":355},[321,7574,7575,7577,7580,7582,7584,7586,7588,7591,7593,7596,7598],{"class":323,"line":241},[321,7576,1520],{"class":344},[321,7578,7579],{"class":348}," session ",[321,7581,818],{"class":384},[321,7583,4217],{"class":344},[321,7585,7561],{"class":348},[321,7587,356],{"class":384},[321,7589,7590],{"class":348},"auth",[321,7592,356],{"class":384},[321,7594,7595],{"class":373},"validate",[321,7597,1455],{"class":348},[321,7599,1404],{"class":355},[321,7601,7602,7604,7606,7608,7611],{"class":323,"line":248},[321,7603,441],{"class":344},[321,7605,1817],{"class":348},[321,7607,1983],{"class":384},[321,7609,7610],{"class":348},"session) ",[321,7612,1732],{"class":355},[321,7614,7615,7618,7621,7623,7626,7628,7630,7632,7634,7637,7640,7642],{"class":323,"line":341},[321,7616,7617],{"class":344}," throw",[321,7619,7620],{"class":373}," error",[321,7622,377],{"class":348},[321,7624,7625],{"class":2597},"401",[321,7627,407],{"class":355},[321,7629,3851],{"class":355},[321,7631,4357],{"class":348},[321,7633,1350],{"class":384},[321,7635,7636],{"class":431}," 'not authorized'",[321,7638,7639],{"class":355}," }",[321,7641,238],{"class":348},[321,7643,1404],{"class":355},[321,7645,7646],{"class":323,"line":362},[321,7647,476],{"class":355},[321,7649,7650,7652,7654,7656,7658,7660,7662,7664,7666],{"class":323,"line":367},[321,7651,1520],{"class":344},[321,7653,4212],{"class":348},[321,7655,818],{"class":384},[321,7657,4217],{"class":344},[321,7659,4220],{"class":348},[321,7661,356],{"class":384},[321,7663,4225],{"class":373},[321,7665,1455],{"class":348},[321,7667,1404],{"class":355},[321,7669,7670,7672,7674,7677,7679,7682,7684,7686,7688,7690,7692,7694],{"class":323,"line":401},[321,7671,1520],{"class":344},[321,7673,3851],{"class":355},[321,7675,7676],{"class":348}," content",[321,7678,407],{"class":355},[321,7680,7681],{"class":348}," description ",[321,7683,1584],{"class":355},[321,7685,2573],{"class":384},[321,7687,4257],{"class":348},[321,7689,356],{"class":384},[321,7691,4262],{"class":373},[321,7693,4265],{"class":348},[321,7695,1404],{"class":355},[321,7697,7698,7700,7702,7704,7707],{"class":323,"line":438},[321,7699,441],{"class":344},[321,7701,1817],{"class":348},[321,7703,1983],{"class":384},[321,7705,7706],{"class":348},"content) ",[321,7708,1732],{"class":355},[321,7710,7711,7713,7715,7717,7720,7722,7724,7726,7728,7731,7733,7735],{"class":323,"line":455},[321,7712,7617],{"class":344},[321,7714,7620],{"class":373},[321,7716,377],{"class":348},[321,7718,7719],{"class":2597},"500",[321,7721,407],{"class":355},[321,7723,3851],{"class":355},[321,7725,4357],{"class":348},[321,7727,1350],{"class":384},[321,7729,7730],{"class":431}," 'invalid input'",[321,7732,7639],{"class":355},[321,7734,238],{"class":348},[321,7736,1404],{"class":355},[321,7738,7739],{"class":323,"line":473},[321,7740,476],{"class":355},[321,7742,7743,7745,7748,7750,7752,7754,7756,7758,7761,7763,7766,7769,7771,7774,7776],{"class":323,"line":479},[321,7744,1520],{"class":344},[321,7746,7747],{"class":348}," arrayBuffer ",[321,7749,818],{"class":384},[321,7751,4217],{"class":344},[321,7753,1817],{"class":348},[321,7755,4272],{"class":344},[321,7757,6990],{"class":373},[321,7759,7760],{"class":348},"(content",[321,7762,356],{"class":384},[321,7764,7765],{"class":373},"toString",[321,7767,7768],{"class":348},"()))",[321,7770,356],{"class":384},[321,7772,7773],{"class":373},"arrayBuffer",[321,7775,1455],{"class":348},[321,7777,1404],{"class":355},[321,7779,7780,7782,7784,7786,7789,7791,7794,7796,7798,7800,7803],{"class":323,"line":512},[321,7781,1520],{"class":344},[321,7783,1878],{"class":348},[321,7785,818],{"class":384},[321,7787,7788],{"class":348}," Buffer",[321,7790,356],{"class":384},[321,7792,7793],{"class":373},"from",[321,7795,377],{"class":348},[321,7797,6387],{"class":6133},[321,7799,6238],{"class":373},[321,7801,7802],{"class":348},"(arrayBuffer))",[321,7804,1404],{"class":355},[321,7806,7807,7809,7811,7813,7816,7818,7821,7824,7827,7829,7832,7834,7837,7839,7842,7844,7846,7848],{"class":323,"line":534},[321,7808,1520],{"class":344},[321,7810,6156],{"class":348},[321,7812,818],{"class":384},[321,7814,7815],{"class":431}," `",[321,7817,1560],{"class":355},[321,7819,7820],{"class":348},"session",[321,7822,7823],{"class":384},"?.",[321,7825,7826],{"class":348},"user",[321,7828,7823],{"class":384},[321,7830,7831],{"class":348},"userId",[321,7833,1584],{"class":355},[321,7835,7836],{"class":431},"_",[321,7838,1560],{"class":355},[321,7840,7841],{"class":373},"uuidv4",[321,7843,1455],{"class":431},[321,7845,1584],{"class":355},[321,7847,1587],{"class":431},[321,7849,1404],{"class":355},[321,7851,7852,7854,7857,7859,7861,7864,7867,7869,7872],{"class":323,"line":552},[321,7853,1520],{"class":344},[321,7855,7856],{"class":348}," url ",[321,7858,818],{"class":384},[321,7860,4217],{"class":344},[321,7862,7863],{"class":373}," saveToBucket",[321,7865,7866],{"class":348},"(data",[321,7868,407],{"class":355},[321,7870,7871],{"class":348}," key)",[321,7873,1404],{"class":355},[321,7875,7876],{"class":323,"line":891},[321,7877,7878],{"class":327}," // get data from function\n",[321,7880,7881,7884,7886,7888,7890,7893,7895,7897,7899],{"class":323,"line":897},[321,7882,7883],{"class":344}," await",[321,7885,4275],{"class":348},[321,7887,356],{"class":384},[321,7889,4280],{"class":373},[321,7891,7892],{"class":348},"(userImages)",[321,7894,356],{"class":384},[321,7896,4288],{"class":373},[321,7898,377],{"class":348},[321,7900,1732],{"class":355},[321,7902,7903,7906],{"class":323,"line":902},[321,7904,7905],{"class":348}," url",[321,7907,3943],{"class":355},[321,7909,7910,7913,7915,7918,7920,7922,7924,7926],{"class":323,"line":907},[321,7911,7912],{"class":348}," userId",[321,7914,1350],{"class":384},[321,7916,7917],{"class":348}," session",[321,7919,7823],{"class":384},[321,7921,7826],{"class":348},[321,7923,356],{"class":384},[321,7925,7831],{"class":348},[321,7927,3943],{"class":355},[321,7929,7930,7933,7935,7938,7940,7942],{"class":323,"line":930},[321,7931,7932],{"class":348}," description",[321,7934,1350],{"class":384},[321,7936,7937],{"class":348}," description",[321,7939,7823],{"class":384},[321,7941,7765],{"class":373},[321,7943,7944],{"class":348},"()\n",[321,7946,7947,7950,7952],{"class":323,"line":947},[321,7948,7949],{"class":355}," }",[321,7951,238],{"class":348},[321,7953,1404],{"class":355},[321,7955,7956,7958,7961,7963,7965,7967,7969,7971,7973,7975,7977,7979],{"class":323,"line":967},[321,7957,759],{"class":344},[321,7959,7960],{"class":373}," json",[321,7962,377],{"class":348},[321,7964,2477],{"class":355},[321,7966,4357],{"class":348},[321,7968,1350],{"class":384},[321,7970,4362],{"class":431},[321,7972,407],{"class":355},[321,7974,7856],{"class":348},[321,7976,1584],{"class":355},[321,7978,238],{"class":348},[321,7980,1404],{"class":355},[321,7982,7983],{"class":323,"line":984},[321,7984,3903],{"class":355},[17,7986,7987],{},"These two lines are what took me a long time to figure out because I kept running into heap problems (my program was using too much memory on this single call, which is not normal)",[313,7989,7991],{"className":1387,"code":7990,"language":1389,"meta":240,"style":240},"const arrayBuffer = await (await fetch(content.toString())).arrayBuffer();\nconst data = Buffer.from(new Uint8Array(arrayBuffer));\n",[171,7992,7993,8025],{"__ignoreMap":240},[321,7994,7995,7997,7999,8001,8003,8005,8007,8009,8011,8013,8015,8017,8019,8021,8023],{"class":323,"line":324},[321,7996,4209],{"class":344},[321,7998,7747],{"class":348},[321,8000,818],{"class":384},[321,8002,4217],{"class":344},[321,8004,1817],{"class":348},[321,8006,4272],{"class":344},[321,8008,6990],{"class":373},[321,8010,7760],{"class":348},[321,8012,356],{"class":384},[321,8014,7765],{"class":373},[321,8016,7768],{"class":348},[321,8018,356],{"class":384},[321,8020,7773],{"class":373},[321,8022,1455],{"class":348},[321,8024,1404],{"class":355},[321,8026,8027,8029,8031,8033,8035,8037,8039,8041,8043,8045,8047],{"class":323,"line":241},[321,8028,4209],{"class":344},[321,8030,1878],{"class":348},[321,8032,818],{"class":384},[321,8034,7788],{"class":348},[321,8036,356],{"class":384},[321,8038,7793],{"class":373},[321,8040,377],{"class":348},[321,8042,6387],{"class":6133},[321,8044,6238],{"class":373},[321,8046,7802],{"class":348},[321,8048,1404],{"class":355},[17,8050,8051,8052,8055],{},"This allows me to download the image from the url openai provides, store it as a buffer and then do whatever I want with it (in this case, store it in a bucket). Then I generate a key using the user's id a random uuid (I will always have access to the user, because this is one of the few commands requiring authentication) which will become the file name, and then call the ",[171,8053,8054],{},"saveToBucket"," function.",[105,8057,8059],{"id":8058},"saving-the-images-or-any-other-file-really","Saving the images (or any other file, really)",[17,8061,8062,8063,8065],{},"You may have noticed I had a call to a functionn ",[171,8064,8054],{}," in the previous code sample; well, as the name suggests, this is where the buffer we got earlier gets saved into the bucket and we get a url back:",[313,8067,8069],{"className":1387,"code":8068,"language":1389,"meta":240,"style":240},"export async function saveToBucket(data: Buffer, key: string) {\n const S3 = new S3Client({\n region: 'auto',\n endpoint: `https://${CLOUDFLARE_ACCOUNT_ID}.r2.cloudflarestorage.com`,\n credentials: {\n accessKeyId: CLOUDFLARE_ACCESS_KEY_ID,\n secretAccessKey: CLOUDFLARE_SECRET_ACCESS_KEY\n }\n });\n await S3.send(\n new PutObjectCommand({\n ACL: 'public-read',\n Key: key,\n Body: data,\n Bucket: CLOUDFLARE_BUCKET_NAME\n })\n );\n return `${CLOUDFLARE_BUCKET_URL}/${key}`;\n}\n",[171,8070,8071,8102,8120,8132,8154,8163,8175,8185,8189,8197,8210,8222,8234,8245,8256,8266,8272,8278,8303],{"__ignoreMap":240},[321,8072,8073,8075,8077,8079,8081,8083,8086,8088,8090,8092,8094,8096,8098,8100],{"class":323,"line":324},[321,8074,7541],{"class":344},[321,8076,4875],{"class":344},[321,8078,995],{"class":344},[321,8080,7863],{"class":373},[321,8082,377],{"class":355},[321,8084,8085],{"class":380},"data",[321,8087,1350],{"class":384},[321,8089,7788],{"class":352},[321,8091,407],{"class":355},[321,8093,6329],{"class":380},[321,8095,1350],{"class":384},[321,8097,666],{"class":344},[321,8099,238],{"class":355},[321,8101,398],{"class":355},[321,8103,8104,8106,8109,8111,8113,8116,8118],{"class":323,"line":241},[321,8105,1520],{"class":344},[321,8107,8108],{"class":348}," S3 ",[321,8110,818],{"class":384},[321,8112,6134],{"class":6133},[321,8114,8115],{"class":373}," S3Client",[321,8117,377],{"class":348},[321,8119,1732],{"class":355},[321,8121,8122,8125,8127,8130],{"class":323,"line":248},[321,8123,8124],{"class":348}," region",[321,8126,1350],{"class":384},[321,8128,8129],{"class":431}," 'auto'",[321,8131,3943],{"class":355},[321,8133,8134,8137,8139,8142,8144,8147,8149,8152],{"class":323,"line":341},[321,8135,8136],{"class":348}," endpoint",[321,8138,1350],{"class":384},[321,8140,8141],{"class":431}," `https://",[321,8143,1560],{"class":355},[321,8145,8146],{"class":348},"CLOUDFLARE_ACCOUNT_ID",[321,8148,1584],{"class":355},[321,8150,8151],{"class":431},".r2.cloudflarestorage.com`",[321,8153,3943],{"class":355},[321,8155,8156,8159,8161],{"class":323,"line":362},[321,8157,8158],{"class":348}," credentials",[321,8160,1350],{"class":384},[321,8162,398],{"class":355},[321,8164,8165,8168,8170,8173],{"class":323,"line":367},[321,8166,8167],{"class":348}," accessKeyId",[321,8169,1350],{"class":384},[321,8171,8172],{"class":348}," CLOUDFLARE_ACCESS_KEY_ID",[321,8174,3943],{"class":355},[321,8176,8177,8180,8182],{"class":323,"line":401},[321,8178,8179],{"class":348}," secretAccessKey",[321,8181,1350],{"class":384},[321,8183,8184],{"class":348}," CLOUDFLARE_SECRET_ACCESS_KEY\n",[321,8186,8187],{"class":323,"line":438},[321,8188,2071],{"class":355},[321,8190,8191,8193,8195],{"class":323,"line":455},[321,8192,7949],{"class":355},[321,8194,238],{"class":348},[321,8196,1404],{"class":355},[321,8198,8199,8201,8204,8206,8208],{"class":323,"line":473},[321,8200,7883],{"class":344},[321,8202,8203],{"class":2597}," S3",[321,8205,356],{"class":384},[321,8207,4695],{"class":373},[321,8209,1571],{"class":348},[321,8211,8212,8215,8218,8220],{"class":323,"line":479},[321,8213,8214],{"class":6133}," new",[321,8216,8217],{"class":373}," PutObjectCommand",[321,8219,377],{"class":348},[321,8221,1732],{"class":355},[321,8223,8224,8227,8229,8232],{"class":323,"line":512},[321,8225,8226],{"class":348}," ACL",[321,8228,1350],{"class":384},[321,8230,8231],{"class":431}," 'public-read'",[321,8233,3943],{"class":355},[321,8235,8236,8239,8241,8243],{"class":323,"line":534},[321,8237,8238],{"class":348}," Key",[321,8240,1350],{"class":384},[321,8242,6329],{"class":348},[321,8244,3943],{"class":355},[321,8246,8247,8250,8252,8254],{"class":323,"line":552},[321,8248,8249],{"class":348}," Body",[321,8251,1350],{"class":384},[321,8253,6270],{"class":348},[321,8255,3943],{"class":355},[321,8257,8258,8261,8263],{"class":323,"line":891},[321,8259,8260],{"class":348}," Bucket",[321,8262,1350],{"class":384},[321,8264,8265],{"class":348}," CLOUDFLARE_BUCKET_NAME\n",[321,8267,8268,8270],{"class":323,"line":897},[321,8269,2077],{"class":355},[321,8271,435],{"class":348},[321,8273,8274,8276],{"class":323,"line":902},[321,8275,1581],{"class":348},[321,8277,1404],{"class":355},[321,8279,8280,8282,8284,8286,8289,8291,8293,8295,8297,8299,8301],{"class":323,"line":907},[321,8281,759],{"class":344},[321,8283,7815],{"class":431},[321,8285,1560],{"class":355},[321,8287,8288],{"class":348},"CLOUDFLARE_BUCKET_URL",[321,8290,1584],{"class":355},[321,8292,4172],{"class":431},[321,8294,1560],{"class":355},[321,8296,6814],{"class":348},[321,8298,1584],{"class":355},[321,8300,1587],{"class":431},[321,8302,1404],{"class":355},[321,8304,8305],{"class":323,"line":930},[321,8306,555],{"class":355},[17,8308,8309],{},"Again, most of this stuff comes from the cloudflare documentation I linked above, like how to setup the S3Client. This function basically puts together a bunch of environment variables and call a single function with the command needed to upload a file to a specific bucket. And just like that, we have image upload !",[17,8311,8312],{},[130,8313],{"alt":6424,"src":8314},"https://media3.giphy.com/media/MXM5QQ3jY7WmcmPwTI/giphy.gif?cid=bcfb6944pnewgj5qvmk7rqd9rizxnherrb1klviruuvl5jfc&ep=v1_gifs_search&rid=giphy.gif&ct=g",[12,8316,8318],{"id":8317},"whats-left-after-uploading","What's left after uploading",[17,8320,8321],{},"The one thing I wanted was for users to be able to retrieve all the images they had generated (and be able to download them at some point), so I simply stored the url for the new image with the user's in my database, and retrieve all those belonging to a specific user id when necessary.",[1167,8323,8324],{},"html pre.shiki code .sIF4r, html code.shiki .sIF4r{--shiki-default:#C6A0F6}html pre.shiki code .siK8c, html code.shiki .siK8c{--shiki-default:#8AADF4;--shiki-default-font-style:italic}html pre.shiki code .sXptk, html code.shiki .sXptk{--shiki-default:#8BD5CA}html pre.shiki code .s80kZ, html code.shiki .s80kZ{--shiki-default:#EED49F;--shiki-default-font-style:italic}html pre.shiki code .slVFb, html code.shiki .slVFb{--shiki-default:#939AB7}html pre.shiki code .skVQi, html code.shiki .skVQi{--shiki-default:#EE99A0;--shiki-default-font-style:italic}html pre.shiki code .sFaBz, html code.shiki .sFaBz{--shiki-default:#CAD3F5}html pre.shiki code .s7Qn8, html code.shiki .s7Qn8{--shiki-default:#F5A97F}html pre.shiki code .sSZ1V, html code.shiki .sSZ1V{--shiki-default:#A6DA95}html pre.shiki code .sPUSY, html code.shiki .sPUSY{--shiki-default:#C6A0F6;--shiki-default-font-weight:bold}html pre.shiki code .sfEIy, html code.shiki .sfEIy{--shiki-default:#939AB7;--shiki-default-font-style:italic}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}",{"title":240,"searchDepth":241,"depth":241,"links":8326},[8327,8328,8332],{"id":7497,"depth":241,"text":7498},{"id":7511,"depth":241,"text":7512,"children":8329},[8330,8331],{"id":7524,"depth":248,"text":7525},{"id":8058,"depth":248,"text":8059},{"id":8317,"depth":241,"text":8318},"I am currently building magiedit, a markdown editor that also allows publishing to different platforms (like Hashnode and Dev.to) and decided to add some shortcuts/commands to make my life simpler (since I am building this tool primarily for myself xD), including adding images from unsplash and gifs and giphy because\n",{"cover_image":258},"/articles/file-uploads-with-sveltekit-and-cloudflare-cover","2023-09-12T00:00:00.000Z","---\ntitle: \"File Uploads with Sveltekit and Cloudflare\"\ncover_image: ./images/og.png\npublishDate: 2023-09-12\nsummary: \"how to upload to cloudflare (or S3 in general) using sveltekit\"\nslug: file-uploads-with-sveltekit-and-cloudflare-cover\ntags:\n- s3\n- cloudflare\n- sveltekit\nseries: \"magiedit\"\n---\n\nI am currently building [magiedit](https://github.com/magitools/magiedit), a markdown editor that also allows publishing to different platforms (like [Hashnode](https://hashnode.com) and [Dev.to](https://dev.to)) and decided to add some shortcuts/commands to make my life simpler (since I am building this tool primarily for myself xD), including adding images from unsplash and gifs and giphy because\n\n\nFor one of the features I'm working on that will allow users to use dall-e to generate images (and cover images based on their article's content as soon I can get that to working) I needed to store the selected images (since openais urls only last for 1 hour and storing 4mb of base64 data in an article makes editing it awful. For this, I turned to [cloudflare's r2 storage](https://www.cloudflare.com/fr-fr/developer-platform/r2/), because I already use their solution for my blog's images and my dns management (plus, their free tier is pretty generous).\n\n\n\n## Using Cloudflare R2 Storage\n\nCloudflare's R2 Storage is an S3 (the one from Amazon, exactly) compatible storage solution, meaning you can interact with it in \"almost\" the say way you would with an S3 bucket (there are some methods that are not supported and that you can find [here](https://developers.cloudflare.com/r2/).\n\nSo, for my use case, I created a bucket and paired it with a domain name and then generated an access key to that specific bucket (so that I could upload things from my sveltekit api function). Again, the link above should provide all instructions on how to get these informations and what they all mean. Now that I had a bucket, let's get to the heart of this article, which is how to interact with a bucket using sveltekit (try saying that sentence out of context).\n\n## Sveltekit integration\n\nNow for the fun part; using all this in sveltekit. Let me preface this section by saying that I had specific needs in this application that required something of an unorthodox architecture. Since the application is storing user articles, I wanted those to be offline first, as well as other potential informations required for using the application (like tokens for publishing and stuff); this means that I had to create api routes instead of handling all of this in form actions. Having said all this, let's see how it all works.\n\n\n\n### Getting the images\n\nFor my specific use case, I needed to store images generated by dall-e (openai), so that meant calling that service; said service returns either a base64 string containing the image or an url (that expires after 1 hour). Passing around a 4mb string didn't sound like a good idea (trust me, I tried), so I was left with the url.\n\nThis is the (hopefully) final code I came up with:\n\n```js\nexport const POST: RequestHandler = async ({ locals, request }) => {\n\tconst session = await locals.auth.validate();\n\tif (!session) {\n\t\tthrow error(401, { message: 'not authorized' });\n\t}\n\tconst formData = await request.formData();\n\tconst { content, description } = Object.fromEntries(formData);\n\tif (!content) {\n\t\tthrow error(500, { message: 'invalid input' });\n\t}\n\tconst arrayBuffer = await (await fetch(content.toString())).arrayBuffer();\n\tconst data = Buffer.from(new Uint8Array(arrayBuffer));\n\tconst key = `${session?.user?.userId}_${uuidv4()}`;\n\tconst url = await saveToBucket(data, key);\n\t// get data from function\n\tawait db.insert(userImages).values({\n\t\turl,\n\t\tuserId: session?.user.userId,\n\t\tdescription: description?.toString()\n\t});\n\treturn json({ message: 'ok', url });\n};\n```\n\nThese two lines are what took me a long time to figure out because I kept running into heap problems (my program was using too much memory on this single call, which is not normal)\n\n```js\nconst arrayBuffer = await (await fetch(content.toString())).arrayBuffer();\nconst data = Buffer.from(new Uint8Array(arrayBuffer));\n```\n\nThis allows me to download the image from the url openai provides, store it as a buffer and then do whatever I want with it (in this case, store it in a bucket). Then I generate a key using the user's id a random uuid (I will always have access to the user, because this is one of the few commands requiring authentication) which will become the file name, and then call the `saveToBucket` function.\n\n### Saving the images (or any other file, really)\n\nYou may have noticed I had a call to a functionn `saveToBucket` in the previous code sample; well, as the name suggests, this is where the buffer we got earlier gets saved into the bucket and we get a url back:\n\n```js\nexport async function saveToBucket(data: Buffer, key: string) {\n\tconst S3 = new S3Client({\n\t\tregion: 'auto',\n\t\tendpoint: `https://${CLOUDFLARE_ACCOUNT_ID}.r2.cloudflarestorage.com`,\n\t\tcredentials: {\n\t\t\taccessKeyId: CLOUDFLARE_ACCESS_KEY_ID,\n\t\t\tsecretAccessKey: CLOUDFLARE_SECRET_ACCESS_KEY\n\t\t}\n\t});\n\tawait S3.send(\n\t\tnew PutObjectCommand({\n\t\t\tACL: 'public-read',\n\t\t\tKey: key,\n\t\t\tBody: data,\n\t\t\tBucket: CLOUDFLARE_BUCKET_NAME\n\t\t})\n\t);\n\treturn `${CLOUDFLARE_BUCKET_URL}/${key}`;\n}\n```\n\nAgain, most of this stuff comes from the cloudflare documentation I linked above, like how to setup the S3Client. This function basically puts together a bunch of environment variables and call a single function with the command needed to upload a file to a specific bucket. And just like that, we have image upload !\n\n\n\n## What's left after uploading\n\nThe one thing I wanted was for users to be able to retrieve all the images they had generated (and be able to download them at some point), so I simply stored the url for the new image with the user's in my database, and retrieve all those belonging to a specific user id when necessary.\n",{"title":7456,"description":8333},"file-uploads-with-sveltekit-and-cloudflare-cover","articles/file-uploads-with-sveltekit-and-cloudflare-cover","how to upload to cloudflare (or S3 in general) using sveltekit",[8343,8344,6019],"s3","cloudflare","ECH9UJX4tZ5yCCkUDBdnsSqtWj_PC5UPnn9d4VQSLhI",{"id":8347,"title":8348,"body":8349,"description":8758,"extension":256,"meta":8759,"navigation":259,"path":8760,"publishDate":8761,"rawbody":8762,"seo":8763,"series":264,"slug":8764,"stem":8765,"summary":8766,"tags":8767,"__hash__":8768},"articles/articles/astro-blog-md.md","Building an Astro Blog with View Transitions",{"type":9,"value":8350,"toc":8748},[8351,8372,8376,8378,8394,8401,8415,8418,8482,8485,8489,8492,8495,8637,8643,8646,8673,8680,8684,8688,8691,8694,8716,8720,8742,8745],[17,8352,8353,8354,8357,8358,8363,8364,8367,8368,8371],{},"I've recently decided to recentralize all my articles on my own blog instead of publishing them to ",[34,8355,7470],{"href":6475,"rel":8356},[38]," (nothing wrong with Hashnode, just something that I had wanted to do for a while) and since I have a wondeful brand new ",[34,8359,8362],{"href":8360,"rel":8361},"https://matteogassend.com",[38],"personal site"," built with ",[34,8365,2179],{"href":5991,"rel":8366},[38]," I decided to take advantage of their collection system and the new ",[230,8369,8370],{},"experimental"," (at least at the time of writing this article) view transitions api to build something a bit more tailored to what I like. Here's how it went.",[12,8373,8375],{"id":8374},"astro-collections","Astro Collections",[105,8377,6029],{"id":6028},[17,8379,8380,8381,8386,8387,1233,8390,8393],{},"Astro content collection are as simple as a folder containing a bunch of Markdown (or Markdoc or MDX) files if that's the only thing you need, but they can also do relationship matching between different collections, frontmatter validation using ",[34,8382,8385],{"href":8383,"rel":8384},"https://zod.dev",[38],"zod"," and you can also customize how the markdown is parsed and translated to html using ",[34,8388,1238],{"href":1236,"rel":8389},[38],[34,8391,1232],{"href":1230,"rel":8392},[38]," and their plugin ecosystem.",[17,8395,8396,8397,238],{},"Let's look at an example, shall we? (btw, documentation for what I'm about to talk about is ",[34,8398,39],{"href":8399,"rel":8400},"https://docs.astro.build/en/guides/content-collections/",[38],[17,8402,8403,8404,8406,8407,8410,8411,8414],{},"Taking my blog as an example, we can see that (for now) the \"articles\" content collection has a bit of frontmatter validation; I am requiring that each article have a ",[230,8405,7054],{},", a ",[230,8408,8409],{},"cover"," image, a ",[230,8412,8413],{},"publish date"," and a list of tags. (Maybe I'll add something like article series in a future update? who knows?)",[17,8416,8417],{},"With this \"schema\" defined then, all my articles will need to have a frontmatter section looking kind of like this:",[313,8419,8421],{"className":5640,"code":8420,"language":5642,"meta":240,"style":240},"---\ntitle: Some title here\npublishDate: 2023-06-10\ncover: https://example.com/image.webp\ntags:\n - tag1\n - tag2\n---\n",[171,8422,8423,8428,8437,8447,8456,8463,8471,8478],{"__ignoreMap":240},[321,8424,8425],{"class":323,"line":324},[321,8426,8427],{"class":4061},"---\n",[321,8429,8430,8432,8434],{"class":323,"line":241},[321,8431,7054],{"class":1295},[321,8433,1350],{"class":384},[321,8435,8436],{"class":431}," Some title here\n",[321,8438,8439,8442,8444],{"class":323,"line":248},[321,8440,8441],{"class":1295},"publishDate",[321,8443,1350],{"class":384},[321,8445,8446],{"class":348}," 2023-06-10\n",[321,8448,8449,8451,8453],{"class":323,"line":341},[321,8450,8409],{"class":1295},[321,8452,1350],{"class":384},[321,8454,8455],{"class":431}," https://example.com/image.webp\n",[321,8457,8458,8461],{"class":323,"line":362},[321,8459,8460],{"class":1295},"tags",[321,8462,1973],{"class":384},[321,8464,8465,8468],{"class":323,"line":367},[321,8466,8467],{"class":355}," -",[321,8469,8470],{"class":431}," tag1\n",[321,8472,8473,8475],{"class":323,"line":401},[321,8474,8467],{"class":355},[321,8476,8477],{"class":431}," tag2\n",[321,8479,8480],{"class":323,"line":438},[321,8481,8427],{"class":4061},[17,8483,8484],{},"Then you can do a bunch of operations, like retrieving all the collection's \"entries\" (in this case, each article) and they can be handled like any other array in javascript or typescript (map over them, sort them by publication date etc).",[105,8486,8488],{"id":8487},"displaying-articles-and-more","Displaying articles and more",[17,8490,8491],{},"When you nativate to a blog post on my website, I have a route that catches the article's slug (a human readable name that can be used in a url, basically), fetches the corresponding article and displays it along with its frontmatter data.",[17,8493,8494],{},"The code for it would look a bit like this:",[313,8496,8498],{"className":1387,"code":8497,"language":1389,"meta":240,"style":240},"const { slug } = Astro.params;\nif (slug === undefined) {\n throw new Error(\"Slug is required\");\n}\n// 2. Query for the entry directly using the request slug\nconst entry = await getEntry(\"articles\", slug);\n// 3. Redirect if the entry does not exist\nif (entry === undefined) {\n return Astro.redirect(\"/404\");\n}\n",[171,8499,8500,8522,8539,8557,8561,8566,8592,8597,8612,8633],{"__ignoreMap":240},[321,8501,8502,8504,8506,8509,8511,8513,8516,8518,8520],{"class":323,"line":324},[321,8503,4209],{"class":344},[321,8505,3851],{"class":355},[321,8507,8508],{"class":348}," slug ",[321,8510,1584],{"class":355},[321,8512,2573],{"class":384},[321,8514,8515],{"class":348}," Astro",[321,8517,356],{"class":384},[321,8519,4767],{"class":348},[321,8521,1404],{"class":355},[321,8523,8524,8526,8529,8531,8534,8537],{"class":323,"line":241},[321,8525,4039],{"class":344},[321,8527,8528],{"class":348}," (slug ",[321,8530,4049],{"class":384},[321,8532,8533],{"class":344}," undefined",[321,8535,8536],{"class":348},") ",[321,8538,1732],{"class":355},[321,8540,8541,8544,8546,8548,8550,8553,8555],{"class":323,"line":248},[321,8542,8543],{"class":344}," throw",[321,8545,6134],{"class":6133},[321,8547,6967],{"class":373},[321,8549,377],{"class":348},[321,8551,8552],{"class":431},"\"Slug is required\"",[321,8554,238],{"class":348},[321,8556,1404],{"class":355},[321,8558,8559],{"class":323,"line":341},[321,8560,555],{"class":355},[321,8562,8563],{"class":323,"line":362},[321,8564,8565],{"class":327},"// 2. Query for the entry directly using the request slug\n",[321,8567,8568,8570,8573,8575,8577,8580,8582,8585,8587,8590],{"class":323,"line":367},[321,8569,4209],{"class":344},[321,8571,8572],{"class":348}," entry ",[321,8574,818],{"class":384},[321,8576,4217],{"class":344},[321,8578,8579],{"class":373}," getEntry",[321,8581,377],{"class":348},[321,8583,8584],{"class":431},"\"articles\"",[321,8586,407],{"class":355},[321,8588,8589],{"class":348}," slug)",[321,8591,1404],{"class":355},[321,8593,8594],{"class":323,"line":401},[321,8595,8596],{"class":327},"// 3. Redirect if the entry does not exist\n",[321,8598,8599,8601,8604,8606,8608,8610],{"class":323,"line":438},[321,8600,4039],{"class":344},[321,8602,8603],{"class":348}," (entry ",[321,8605,4049],{"class":384},[321,8607,8533],{"class":344},[321,8609,8536],{"class":348},[321,8611,1732],{"class":355},[321,8613,8614,8617,8619,8621,8624,8626,8629,8631],{"class":323,"line":455},[321,8615,8616],{"class":344}," return",[321,8618,8515],{"class":348},[321,8620,356],{"class":384},[321,8622,8623],{"class":373},"redirect",[321,8625,377],{"class":348},[321,8627,8628],{"class":431},"\"/404\"",[321,8630,238],{"class":348},[321,8632,1404],{"class":355},[321,8634,8635],{"class":323,"line":473},[321,8636,555],{"class":355},[17,8638,8639,8640,356],{},"and you would store in a file called ",[171,8641,8642],{},"[...slug].astro",[17,8644,8645],{},"Then to display the markdown content, you can call the render method and then display the content on the page:",[313,8647,8649],{"className":5479,"code":8648,"language":269,"meta":240,"style":240},"---\nconst { Content, headings } = await entry.render();\n---\n\n\u003CContent />\n",[171,8650,8651,8655,8660,8664,8668],{"__ignoreMap":240},[321,8652,8653],{"class":323,"line":324},[321,8654,8427],{},[321,8656,8657],{"class":323,"line":241},[321,8658,8659],{},"const { Content, headings } = await entry.render();\n",[321,8661,8662],{"class":323,"line":248},[321,8663,8427],{},[321,8665,8666],{"class":323,"line":341},[321,8667,333],{"emptyLinePlaceholder":259},[321,8669,8670],{"class":323,"line":362},[321,8671,8672],{},"\u003CContent />\n",[17,8674,8675,8676,8679],{},"That should take care of displaying the content you wrote in your markdown file; you want to get the frontmatter data (like a title, cover image and such), you can do so using ",[171,8677,8678],{},"entry.data.title"," and so on.",[12,8681,8683],{"id":8682},"adding-functionalities","Adding Functionalities",[105,8685,8687],{"id":8686},"table-of-contents","Table of Contents",[17,8689,8690],{},"But what if I wanted to add a summary ?",[17,8692,8693],{},"You could obviously write by hand each time, but you could also leverage the data Astro gives us; you may have noticed that I destructured 2 properties from the renderer entry: we have already used content, so let's look at the headings.",[17,8695,8696,8697,8699,8700,8702,8703,8705,8706,8709,8710,8715],{},"The headings variable is an array of all the headings in a file (think ",[171,8698,4036],{},", ",[171,8701,5901],{}," etc) as well as their level (",[171,8704,5901],{}," is 2, ",[171,8707,8708],{},"###"," is 3 etc). With these informations, we can build a structure displaying each section and subsection and display it accordingly (more info on this article from ",[34,8711,8714],{"href":8712,"rel":8713},"https://kld.dev/building-table-of-contents/",[38],"Kevin Drum"," and add it our page.",[105,8717,8719],{"id":8718},"embeds-and-markdown-customization","Embeds and markdown customization",[17,8721,8722,8723,8728,8729,8732,8733,8736,8737,8741],{},"You may also notice I have some embeds on my articles even though Markdown natively does not support embedding content. This is done by using a Remark plugin. Remark is a tool that can be used to parse and transform Markdown. In this case, I used a plugin called ",[34,8724,8727],{"href":8725,"rel":8726},"https://github.com/remark-embedder/core",[38],"remark-embedder"," to add custom logic to replace links from specific websites (in this case, Youtube and CodeSandbox) with ",[171,8730,8731],{},"\u003Ciframe>","s containing the actual page; without the plugin, those would simply be text links and would make for a much less pleasing lecture, wouldn't you agree ?\nYou can obviously do more with remark than just that, so take a look at the plugins they offer. But how do you use with Astro? You simply add the plugins in your ",[171,8734,8735],{},"astro.config.mjs"," file (documentation on how to do that is ",[34,8738,39],{"href":8739,"rel":8740},"https://docs.astro.build/en/reference/configuration-reference/#markdownremarkplugins",[38],").",[17,8743,8744],{},"This post was a bit chaotic, but I hope I was able to share a bit of what I did on my blog section (where you are hopefully reading this right now).",[1167,8746,8747],{},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .s_ZFR, html code.shiki .s_ZFR{--shiki-default:#F5BDE6}html pre.shiki code .s57MT, html code.shiki .s57MT{--shiki-default:#8AADF4}html pre.shiki code .sXptk, html code.shiki .sXptk{--shiki-default:#8BD5CA}html pre.shiki code .sSZ1V, html code.shiki .sSZ1V{--shiki-default:#A6DA95}html pre.shiki code .sFaBz, html code.shiki .sFaBz{--shiki-default:#CAD3F5}html pre.shiki code .slVFb, html code.shiki .slVFb{--shiki-default:#939AB7}html pre.shiki code .sIF4r, html code.shiki .sIF4r{--shiki-default:#C6A0F6}html pre.shiki code .sPUSY, html code.shiki .sPUSY{--shiki-default:#C6A0F6;--shiki-default-font-weight:bold}html pre.shiki code .siK8c, html code.shiki .siK8c{--shiki-default:#8AADF4;--shiki-default-font-style:italic}html pre.shiki code .sfEIy, html code.shiki .sfEIy{--shiki-default:#939AB7;--shiki-default-font-style:italic}",{"title":240,"searchDepth":241,"depth":241,"links":8749},[8750,8754],{"id":8374,"depth":241,"text":8375,"children":8751},[8752,8753],{"id":6028,"depth":248,"text":6029},{"id":8487,"depth":248,"text":8488},{"id":8682,"depth":241,"text":8683,"children":8755},[8756,8757],{"id":8686,"depth":248,"text":8687},{"id":8718,"depth":248,"text":8719},"I've recently decided to recentralize all my articles on my own blog instead of publishing them to Hashnode (nothing wrong with Hashnode, just something that I had wanted to do for a while) and since I have a wondeful brand new personal site built with astro I decided to take advantage of their collection system and the new experimental (at least at the time of writing this article) view transitions api to build something a bit more tailored to what I like. Here's how it went.",{"cover_image":258},"/articles/astro-blog-md","2023-08-24T00:00:00.000Z","---\ntitle: \"Building an Astro Blog with View Transitions\"\nsummary: \"a tale of sweat, content collections, pages and storage\"\npublishDate: 2023-08-24\nslug: astro-blog-md\ntags:\n- astro\n- markdown\ncover_image: ./images/og.png\n---\n\nI've recently decided to recentralize all my articles on my own blog instead of publishing them to [Hashnode](https://hashnode.com) (nothing wrong with Hashnode, just something that I had wanted to do for a while) and since I have a wondeful brand new [personal site](https://matteogassend.com) built with [astro](https://astro.build) I decided to take advantage of their collection system and the new **experimental** (at least at the time of writing this article) view transitions api to build something a bit more tailored to what I like. Here's how it went.\n\n## Astro Collections\n\n### Introduction\n\nAstro content collection are as simple as a folder containing a bunch of Markdown (or Markdoc or MDX) files if that's the only thing you need, but they can also do relationship matching between different collections, frontmatter validation using [zod](https://zod.dev) and you can also customize how the markdown is parsed and translated to html using [rehype](https://github.com/rehypejs/rehype) and [remark](https://github.com/remarkjs/remark) and their plugin ecosystem.\n\nLet's look at an example, shall we? (btw, documentation for what I'm about to talk about is [here](https://docs.astro.build/en/guides/content-collections/))\n\nTaking my blog as an example, we can see that (for now) the \"articles\" content collection has a bit of frontmatter validation; I am requiring that each article have a **title**, a **cover** image, a **publish date** and a list of tags. (Maybe I'll add something like article series in a future update? who knows?)\n\nWith this \"schema\" defined then, all my articles will need to have a frontmatter section looking kind of like this:\n\n```yaml\n---\ntitle: Some title here\npublishDate: 2023-06-10\ncover: https://example.com/image.webp\ntags:\n - tag1\n - tag2\n---\n```\n\nThen you can do a bunch of operations, like retrieving all the collection's \"entries\" (in this case, each article) and they can be handled like any other array in javascript or typescript (map over them, sort them by publication date etc).\n\n### Displaying articles and more\n\nWhen you nativate to a blog post on my website, I have a route that catches the article's slug (a human readable name that can be used in a url, basically), fetches the corresponding article and displays it along with its frontmatter data.\n\nThe code for it would look a bit like this:\n\n```js\nconst { slug } = Astro.params;\nif (slug === undefined) {\n throw new Error(\"Slug is required\");\n}\n// 2. Query for the entry directly using the request slug\nconst entry = await getEntry(\"articles\", slug);\n// 3. Redirect if the entry does not exist\nif (entry === undefined) {\n return Astro.redirect(\"/404\");\n}\n```\n\nand you would store in a file called `[...slug].astro`.\n\nThen to display the markdown content, you can call the render method and then display the content on the page:\n\n```javascript\n---\nconst { Content, headings } = await entry.render();\n---\n\n\u003CContent />\n```\n\nThat should take care of displaying the content you wrote in your markdown file; you want to get the frontmatter data (like a title, cover image and such), you can do so using `entry.data.title` and so on.\n\n## Adding Functionalities\n\n### Table of Contents\n\nBut what if I wanted to add a summary ?\n\nYou could obviously write by hand each time, but you could also leverage the data Astro gives us; you may have noticed that I destructured 2 properties from the renderer entry: we have already used content, so let's look at the headings.\n\nThe headings variable is an array of all the headings in a file (think `#`, `##` etc) as well as their level (`##` is 2, `###` is 3 etc). With these informations, we can build a structure displaying each section and subsection and display it accordingly (more info on this article from [Kevin Drum](https://kld.dev/building-table-of-contents/) and add it our page.\n\n### Embeds and markdown customization\n\nYou may also notice I have some embeds on my articles even though Markdown natively does not support embedding content. This is done by using a Remark plugin. Remark is a tool that can be used to parse and transform Markdown. In this case, I used a plugin called [remark-embedder](https://github.com/remark-embedder/core) to add custom logic to replace links from specific websites (in this case, Youtube and CodeSandbox) with `\u003Ciframe>`s containing the actual page; without the plugin, those would simply be text links and would make for a much less pleasing lecture, wouldn't you agree ?\nYou can obviously do more with remark than just that, so take a look at the plugins they offer. But how do you use with Astro? You simply add the plugins in your `astro.config.mjs` file (documentation on how to do that is [here](https://docs.astro.build/en/reference/configuration-reference/#markdownremarkplugins)).\n\nThis post was a bit chaotic, but I hope I was able to share a bit of what I did on my blog section (where you are hopefully reading this right now).\n",{"title":8348,"description":8758},"astro-blog-md","articles/astro-blog-md","a tale of sweat, content collections, pages and storage",[2179,270],"0eup2bIlhmUpzAGnqBpsBKmofsmMu4ykCwV1JmbRRuc",{"id":8770,"title":8771,"body":8772,"description":8925,"extension":256,"meta":8926,"navigation":259,"path":8927,"publishDate":8928,"rawbody":8929,"seo":8930,"series":264,"slug":8931,"stem":8932,"summary":8933,"tags":8934,"__hash__":8937},"articles/articles/appwrite-hackaton-movieplay.md","Appwrite Hackaton: MoviePlay",{"type":9,"value":8773,"toc":8913},[8774,8791,8795,8798,8804,8808,8813,8817,8822,8861,8865,8877,8881,8884,8888,8891,8894,8897,8900,8903,8906],[17,8775,8776,8777,1233,8780,8785,8786,8790],{},"Well, I recently quit my job so I got free time and this hackathon between ",[34,8778,7470],{"href":6475,"rel":8779},[38],[34,8781,8784],{"href":8782,"rel":8783},"https://appwrite.io",[38],"Appwrite"," is announced. This is clearly a sign. So I decided to build ",[34,8787,8789],{"href":8782,"rel":8788},[38],"MoviePlay",". Before we get into the trials and tribulations of this project, let's get all the technical stuff out of the way.",[12,8792,8794],{"id":8793},"the-idea","The Idea",[17,8796,8797],{},"Have you ever had a debate with someone as to what is the correct order to watch Star Wars? There's the chronological order, the release order, the machete order",[17,8799,8800],{},[130,8801],{"alt":8802,"src":8803},"enough already","https://media.giphy.com/media/SRka2MLKzpzE6K24al/giphy.gif",[12,8805,8807],{"id":8806},"team","Team",[21,8809,8810],{},[24,8811,8812],{},"Me",[12,8814,8816],{"id":8815},"tech-stack","Tech Stack",[21,8818,8819],{},[24,8820,8821],{},"ReactJS & Typescript (ViteJS)",[21,8823,8824,8827,8850,8853],{},[24,8825,8826],{},"TailwindCSS & DaisyUI",[24,8828,8829,8834],{},[34,8830,8833],{"href":8831,"rel":8832},"https://cloud.appwrite.io",[38],"Appwrite Cloud",[21,8835,8836,8839,8842],{},[24,8837,8838],{},"Database",[24,8840,8841],{},"Account",[24,8843,8844,8845],{},"Functions",[21,8846,8847],{},[24,8848,8849],{},"NodeJS",[24,8851,8852],{},"Vercel",[24,8854,8855,8860],{},[34,8856,8859],{"href":8857,"rel":8858},"https://www.npmjs.com/package/@matfire/the_movie_wrapper",[38],"The-Movie-Wrapper"," (I made this one, but still...)",[12,8862,8864],{"id":8863},"look-at-the-code","Look at the code",[17,8866,8867,8868,8872,8873],{},"You can see the code ",[34,8869,39],{"href":8870,"rel":8871},"https://github.com/matfire/movieplay",[38]," and see it live ",[34,8874,39],{"href":8875,"rel":8876},"https://movieplay.nirah.tech",[38],[12,8878,8880],{"id":8879},"demo","Demo",[3176,8882],{"id":8883},"8GJyqRNkZrA",[12,8885,8887],{"id":8886},"on-the-using-of-appwrite","On the using of Appwrite",[17,8889,8890],{},"I had already used a self-hosted version of Appwrite and have been tinkering with the closed cloud beta for a bit, so I was already familiar with the tool and will not really talk about onboarding.",[105,8892,8838],{"id":8893},"database",[17,8895,8896],{},"I was a bit disappointed relationships (as of 06/04/2023) are not yet supported on Cloud, so you still need to do all the foreign key constraints by hand, which makes fetching client-side a bit of a hassle.",[105,8898,8841],{"id":8899},"account",[17,8901,8902],{},"I really like how accounts are handled, especially oauth2 stuff; the fact that you can connect multiple providers for the same account seamlessly is pretty cool",[105,8904,8844],{"id":8905},"functions",[17,8907,8908,8909,8912],{},"I needed to use functions to store the number of views a playlist could get. I would have loved a trigger on ",[230,8910,8911],{},"database read operations",", but I can see it would probably be too much performance overhead; the way I solved this is when navigating to a page, the page loader function triggers the function to increment the views number.",{"title":240,"searchDepth":241,"depth":241,"links":8914},[8915,8916,8917,8918,8919,8920],{"id":8793,"depth":241,"text":8794},{"id":8806,"depth":241,"text":8807},{"id":8815,"depth":241,"text":8816},{"id":8863,"depth":241,"text":8864},{"id":8879,"depth":241,"text":8880},{"id":8886,"depth":241,"text":8887,"children":8921},[8922,8923,8924],{"id":8893,"depth":248,"text":8838},{"id":8899,"depth":248,"text":8841},{"id":8905,"depth":248,"text":8844},"Well, I recently quit my job so I got free time and this hackathon between Hashnode and Appwrite is announced. This is clearly a sign. So I decided to build MoviePlay. Before we get into the trials and tribulations of this project, let's get all the technical stuff out of the way.",{"cover_image":258},"/articles/appwrite-hackaton-movieplay","2023-06-10T00:00:00.000Z","---\ntitle: \"Appwrite Hackaton: MoviePlay\"\npublishDate: 2023-06-10\nsummary: \"A postmortem of an Appwrite Hackaton to celebrate the launch of their cloud beta\"\nslug: appwrite-hackaton-movieplay\ntags:\n- reactjs\n- typescript\n- tailwind-css\n- appwrite\ncover_image: ./images/og.png\n---\n\nWell, I recently quit my job so I got free time and this hackathon between [Hashnode](https://hashnode.com) and [Appwrite](https://appwrite.io) is announced. This is clearly a sign. So I decided to build [MoviePlay](https://appwrite.io). Before we get into the trials and tribulations of this project, let's get all the technical stuff out of the way.\n\n## The Idea\n\nHave you ever had a debate with someone as to what is the correct order to watch Star Wars? There's the chronological order, the release order, the machete order\n\n\n\n## Team\n\n- Me\n\n## Tech Stack\n\n- ReactJS & Typescript (ViteJS)\n\n* TailwindCSS & DaisyUI\n\n* [Appwrite Cloud](https://cloud.appwrite.io)\n - Database\n\n - Account\n\n - Functions\n - NodeJS\n\n* Vercel\n\n* [The-Movie-Wrapper](https://www.npmjs.com/package/@matfire/the_movie_wrapper) (I made this one, but still...)\n\n## Look at the code\n\nYou can see the code [here](https://github.com/matfire/movieplay) and see it live [here](https://movieplay.nirah.tech)\n\n## Demo\n\n::youtube{#8GJyqRNkZrA}\n::\n\n## On the using of Appwrite\n\nI had already used a self-hosted version of Appwrite and have been tinkering with the closed cloud beta for a bit, so I was already familiar with the tool and will not really talk about onboarding.\n\n### Database\n\nI was a bit disappointed relationships (as of 06/04/2023) are not yet supported on Cloud, so you still need to do all the foreign key constraints by hand, which makes fetching client-side a bit of a hassle.\n\n### Account\n\nI really like how accounts are handled, especially oauth2 stuff; the fact that you can connect multiple providers for the same account seamlessly is pretty cool\n\n### Functions\n\nI needed to use functions to store the number of views a playlist could get. I would have loved a trigger on **database read operations**, but I can see it would probably be too much performance overhead; the way I solved this is when navigating to a page, the page loader function triggers the function to increment the views number.\n",{"title":8771,"description":8925},"appwrite-hackaton-movieplay","articles/appwrite-hackaton-movieplay","A postmortem of an Appwrite Hackaton to celebrate the launch of their cloud beta",[3760,3804,8935,8936],"tailwind-css","appwrite","EpBeP4Pmmr4anj0uu7vqC7C9JGBmfI2bUYFlxClSq4A",{"id":8939,"title":8940,"body":8941,"description":8945,"extension":256,"meta":9493,"navigation":259,"path":5415,"publishDate":9494,"rawbody":9495,"seo":9496,"series":264,"slug":9497,"stem":9498,"summary":9499,"tags":9500,"__hash__":9503},"articles/articles/taming-the-whale.md","Taming the whale: introduction to Docker",{"type":9,"value":8942,"toc":9476},[8943,8946,8950,8953,8957,8970,8974,8977,8981,8984,8989,8994,8998,9001,9005,9013,9035,9044,9048,9051,9054,9060,9063,9122,9125,9139,9150,9153,9172,9175,9179,9182,9197,9201,9207,9223,9226,9230,9233,9245,9248,9271,9274,9278,9281,9300,9309,9313,9320,9333,9388,9403,9426,9441,9444,9448,9457,9462,9465,9470,9473],[17,8944,8945],{},"Have you ever had to work on a project that requires lots of parts that need to be installed separately? And one of those parts refuses to work because maybe the other developer worked on Windows and you are on Linux? Well, what I told you that those problems can be (relatively) easily solved? Let's take a look at what Docker is and how we can use it.",[12,8947,8949],{"id":8948},"what-is-docker","What is Docker?",[17,8951,8952],{},"Docker is a suite of tools allowing you to run containers on your system.",[105,8954,8956],{"id":8955},"but-what-is-a-container","But what is a container?",[17,8958,8959,8960,8965,8966,8969],{},"according to ",[34,8961,8964],{"href":8962,"rel":8963},"https://docker.com",[38],"docker.com",", a container is ",[189,8967,8968],{},"a sandboxed process on your machine that is isolated from all other processes on the host machine","; this feature has been available on Linux for some time, but Docker managed to standardize and make it available on other operating systems.",[12,8971,8973],{"id":8972},"why-would-you-use-a-container","Why would you use a container?",[17,8975,8976],{},"One of the reasons Docker is so popular is that it allows people to get started and run projects without necessarily needing to install a whole environment. This usually allows for faster onboarding and testing while also simplifying the deployment of services; by using a container you needn't worry about the exact system-specific settings you might need to handle for (most) of the production applications (a single container can be run on (basically) any operating system).",[12,8978,8980],{"id":8979},"images-containers","Images? Containers?",[17,8982,8983],{},"I have mentioned both containers and images so far, but what's the difference?",[21,8985,8986],{},[24,8987,8988],{},"An image is the piece of software that contains all the instructions to run your program; the installed programs, the start command etc...",[21,8990,8991],{},[24,8992,8993],{},"A container is what runs an Image; it also handles transmitting environment variables, port forwarding with the host, volumes etc... (more details on those later)",[12,8995,8997],{"id":8996},"docker-cheatsheet","Docker Cheatsheet",[17,8999,9000],{},"Now that we know (kind of) how Docker works, let's create our first image.",[105,9002,9004],{"id":9003},"installing-docker","Installing Docker",[17,9006,9007,9008,9012],{},"on most systems, you can install ",[34,9009,9011],{"href":8962,"rel":9010},[38],"Docker Desktop"," to have a nice GUI to help you. If you can't or don't want to use that, you can also only install the command line tool by running",[313,9014,9016],{"className":1077,"code":9015,"language":1079,"meta":240,"style":240},"curl https://get.docker.com | bash -E\n",[171,9017,9018],{"__ignoreMap":240},[321,9019,9020,9023,9026,9029,9032],{"class":323,"line":324},[321,9021,9022],{"class":373},"curl",[321,9024,9025],{"class":431}," https://get.docker.com",[321,9027,9028],{"class":384}," |",[321,9030,9031],{"class":373}," bash",[321,9033,9034],{"class":431}," -E\n",[17,9036,9037,9038,9043],{},"this should download and install docker on your machine (if this does not work, head on over to the ",[34,9039,9042],{"href":9040,"rel":9041},"https://docs.docker.com/get-started/",[38],"documentation"," for system-specific information).",[105,9045,9047],{"id":9046},"creating-an-image","Creating an Image",[17,9049,9050],{},"For the sake of an example, we will be using this example",[3733,9052],{"project-type":3735,"projectid":9053},"cwmtww",[17,9055,9056,9057],{},"It's a simple application running an HTTP server. ",[230,9058,9059],{},"Let's dockerize it!",[17,9061,9062],{},"To do so, we need to first create a Dockefile; this is a specific file format that enables us to describe how an Image should be created.\nThe simplest Dockerfile for this example project would be:",[313,9064,9066],{"className":1077,"code":9065,"language":1079,"meta":240,"style":240},"FROM node:lts\nCOPY package.json .\nCOPY index.js .\nRUN npm install\nCMD [\"node\", \"index.js\"]\n",[171,9067,9068,9076,9087,9096,9107],{"__ignoreMap":240},[321,9069,9070,9073],{"class":323,"line":324},[321,9071,9072],{"class":373},"FROM",[321,9074,9075],{"class":431}," node:lts\n",[321,9077,9078,9081,9084],{"class":323,"line":241},[321,9079,9080],{"class":373},"COPY",[321,9082,9083],{"class":431}," package.json",[321,9085,9086],{"class":431}," .\n",[321,9088,9089,9091,9094],{"class":323,"line":248},[321,9090,9080],{"class":373},[321,9092,9093],{"class":431}," index.js",[321,9095,9086],{"class":431},[321,9097,9098,9101,9104],{"class":323,"line":341},[321,9099,9100],{"class":373},"RUN",[321,9102,9103],{"class":431}," npm",[321,9105,9106],{"class":431}," install\n",[321,9108,9109,9112,9114,9117,9119],{"class":323,"line":362},[321,9110,9111],{"class":373},"CMD",[321,9113,6196],{"class":348},[321,9115,9116],{"class":431},"\"node\"",[321,9118,8699],{"class":348},[321,9120,9121],{"class":431},"\"index.js\"]\n",[17,9123,9124],{},"let's look a bit more into this file:",[21,9126,9127],{},[24,9128,9129,9130,9132,9133,9136,9137],{},"FROM: this line describes the base of our image. We need to tell Docker what to base our image on. To do so, you can specify the image ",[230,9131,4131],{}," and a ",[230,9134,9135],{},"tag"," separated by ",[230,9138,1350],{},[21,9140,9141,9144,9147],{},[24,9142,9143],{},"COPY: copies a file or folder from our computer's filesystem to the image's.",[24,9145,9146],{},"RUN: executes a command when building the image",[24,9148,9149],{},"CMD: this is the command that gets executed when the image runs.",[17,9151,9152],{},"to build this image we can run:",[313,9154,9156],{"className":1077,"code":9155,"language":1079,"meta":240,"style":240},"docker build -t learn-docker .\n",[171,9157,9158],{"__ignoreMap":240},[321,9159,9160,9162,9164,9167,9170],{"class":323,"line":324},[321,9161,5868],{"class":373},[321,9163,1088],{"class":431},[321,9165,9166],{"class":431}," -t",[321,9168,9169],{"class":431}," learn-docker",[321,9171,9086],{"class":431},[17,9173,9174],{},"the -t option allows us to specify a tag for the image to find it more easily.",[105,9176,9178],{"id":9177},"running-an-image","Running an Image",[17,9180,9181],{},"Once we have an image tagged, we can run it by saying:",[313,9183,9185],{"className":1077,"code":9184,"language":1079,"meta":240,"style":240},"docker run learn-docker\n",[171,9186,9187],{"__ignoreMap":240},[321,9188,9189,9191,9194],{"class":323,"line":324},[321,9190,5868],{"class":373},[321,9192,9193],{"class":431}," run",[321,9195,9196],{"class":431}," learn-docker\n",[105,9198,9200],{"id":9199},"detached-mode","Detached Mode",[17,9202,9203,9204,356],{},"You'll notice that our terminal window is stuck on the output from our terminal; that's all well and good, but it'd be nice if we wouldn't have to open a new terminal for each container we want to run: enter ",[230,9205,9206],{},"detached mode",[313,9208,9210],{"className":1077,"code":9209,"language":1079,"meta":240,"style":240},"docker run -d learn-docker\n",[171,9211,9212],{"__ignoreMap":240},[321,9213,9214,9216,9218,9221],{"class":323,"line":324},[321,9215,5868],{"class":373},[321,9217,9193],{"class":431},[321,9219,9220],{"class":431}," -d",[321,9222,9196],{"class":431},[17,9224,9225],{},"Running a container in detached mode means putting the process in the background which in turn means we get our terminal back.",[105,9227,9229],{"id":9228},"stopping-a-container","Stopping a container",[17,9231,9232],{},"To stop a container, we first need to know its id. To get a list of all your running containers, you can run the command:",[313,9234,9236],{"className":1077,"code":9235,"language":1079,"meta":240,"style":240},"docker ps\n",[171,9237,9238],{"__ignoreMap":240},[321,9239,9240,9242],{"class":323,"line":324},[321,9241,5868],{"class":373},[321,9243,9244],{"class":431}," ps\n",[17,9246,9247],{},"The first element of each line is the container id. Then you can just run:",[313,9249,9251],{"className":1077,"code":9250,"language":1079,"meta":240,"style":240},"docker stop \u003Cthe_id_of_the_container>\n",[171,9252,9253],{"__ignoreMap":240},[321,9254,9255,9257,9260,9263,9266,9269],{"class":323,"line":324},[321,9256,5868],{"class":373},[321,9258,9259],{"class":431}," stop",[321,9261,9262],{"class":384}," \u003C",[321,9264,9265],{"class":431},"the_id_of_the_containe",[321,9267,9268],{"class":348},"r",[321,9270,4110],{"class":384},[17,9272,9273],{},"and it will stop your container.",[105,9275,9277],{"id":9276},"ports","Ports",[17,9279,9280],{},"you may have noticed that, at least for now, we are unable to access our api. This is because, to put it simply, Docker is sandboxed in its own network, so we need to explicitly map the container's ports to those of our system's. Let's take the command we had before and add a little option",[313,9282,9284],{"className":1077,"code":9283,"language":1079,"meta":240,"style":240},"docker run -p 8000:4000 learn-docker\n",[171,9285,9286],{"__ignoreMap":240},[321,9287,9288,9290,9292,9295,9298],{"class":323,"line":324},[321,9289,5868],{"class":373},[321,9291,9193],{"class":431},[321,9293,9294],{"class":431}," -p",[321,9296,9297],{"class":431}," 8000:4000",[321,9299,9196],{"class":431},[17,9301,9302,9303,9308],{},"this should expose port 3000 of our container on port 8000 of our system. If you visit ",[34,9304,9307],{"href":9305,"rel":9306},"http://127.0.0.1:8000",[38],"localhost:8000",", you should see our hello world message.",[105,9310,9312],{"id":9311},"volumes","Volumes",[17,9314,9315,9316,9319],{},"now that we have a basic application running, let's see if we can get a bit more out of it. If you take a look at the code, you'll notice that I'm writing my logs into a file inside a ",[230,9317,9318],{},"logs"," folder; wouldn't it be neat if we could read that data? This is where volumes come in.",[17,9321,9322,9323,9326,9327,8699,9330,9332],{},"But before we get started, let's make our life a bit simpler; in our Dockerfile, let's specify the ",[230,9324,9325],{},"working directory."," This means that when Docker executes the instruction ",[171,9328,9329],{},"COPY index.js .",[230,9331,356],{}," will be replaced by the working directory we specified earlier. Our new Dockerfile should look something like this:",[313,9334,9336],{"className":1077,"code":9335,"language":1079,"meta":240,"style":240},"FROM node:lts\nWORKDIR /app\nCOPY package.json .\nCOPY index.js .\nRUN npm install\nCMD [\"node\", \"index.js\"]\n",[171,9337,9338,9344,9352,9360,9368,9376],{"__ignoreMap":240},[321,9339,9340,9342],{"class":323,"line":324},[321,9341,9072],{"class":373},[321,9343,9075],{"class":431},[321,9345,9346,9349],{"class":323,"line":241},[321,9347,9348],{"class":373},"WORKDIR",[321,9350,9351],{"class":431}," /app\n",[321,9353,9354,9356,9358],{"class":323,"line":248},[321,9355,9080],{"class":373},[321,9357,9083],{"class":431},[321,9359,9086],{"class":431},[321,9361,9362,9364,9366],{"class":323,"line":341},[321,9363,9080],{"class":373},[321,9365,9093],{"class":431},[321,9367,9086],{"class":431},[321,9369,9370,9372,9374],{"class":323,"line":362},[321,9371,9100],{"class":373},[321,9373,9103],{"class":431},[321,9375,9106],{"class":431},[321,9377,9378,9380,9382,9384,9386],{"class":323,"line":367},[321,9379,9111],{"class":373},[321,9381,6196],{"class":348},[321,9383,9116],{"class":431},[321,9385,8699],{"class":348},[321,9387,9121],{"class":431},[17,9389,9390,9391,9394,9395,9398,9399,9402],{},"Our logs should be stored in ",[171,9392,9393],{},"/app/logs",", right? If so, we could define that folder as a ",[230,9396,9397],{},"volume","; a volume is a path that is designated as ",[230,9400,9401],{},"persistent",", meaning if the container stops the data inside the volume will not be lost. As an additional bonus, a volume can also mirror files and folders in the container to the host's disk. You could, let's say setup a development environment with hot reloading with Dockefiles and maybe a Docker-Compose (maybe a topic for another article).\nlet's revise our execution line a bit:",[313,9404,9406],{"className":1077,"code":9405,"language":1079,"meta":240,"style":240},"docker run -p 8000:4000 -v ./logs:/app/logs learn-docker\n",[171,9407,9408],{"__ignoreMap":240},[321,9409,9410,9412,9414,9416,9418,9421,9424],{"class":323,"line":324},[321,9411,5868],{"class":373},[321,9413,9193],{"class":431},[321,9415,9294],{"class":431},[321,9417,9297],{"class":431},[321,9419,9420],{"class":431}," -v",[321,9422,9423],{"class":431}," ./logs:/app/logs",[321,9425,9196],{"class":431},[17,9427,9428,9429,9431,9432,9435,9436,9440],{},"this new addition will tell Docker to use (or create) the folder logs in our terminal's current directory as the volume for the path ",[230,9430,9393],{}," of our container. This means that if we look inside our logs folder, we should see a file called ",[230,9433,9434],{},"log.txt."," If now we send a request to ",[34,9437,9439],{"href":9305,"rel":9438},[38],"http://localhost:8000",", we should a new line get appended to that file.",[17,9442,9443],{},"This is especially useful for databases because you most likely do not want to lose all your customer's data every time you update/stop/restart a container.",[12,9445,9447],{"id":9446},"this-is-the-end","This is the end...",[17,9449,9450,9451,9456],{},"This has hopefully been a useful introduction to Docker. If you want to learn more about this technology, the first step would be to visit ",[34,9452,9455],{"href":9453,"rel":9454},"https://docs.docker.com",[38],"the official documentation","; it is pretty well written.\nOther useful resources may be:",[21,9458,9459],{},[24,9460,9461],{},"Learn Docker in 7 Easy Steps - Full Beginner's Tutorial - Fireship",[3176,9463],{"id":9464},"gAkwW2tuIqE",[21,9466,9467],{},[24,9468,9469],{},"Docker Tutorial for Beginners - Programming with Mosh",[3176,9471],{"id":9472},"pTFZFxd4hOI",[1167,9474,9475],{},"html pre.shiki code .siK8c, html code.shiki .siK8c{--shiki-default:#8AADF4;--shiki-default-font-style:italic}html pre.shiki code .sSZ1V, html code.shiki .sSZ1V{--shiki-default:#A6DA95}html pre.shiki code .sXptk, html code.shiki .sXptk{--shiki-default:#8BD5CA}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sFaBz, html code.shiki .sFaBz{--shiki-default:#CAD3F5}",{"title":240,"searchDepth":241,"depth":241,"links":9477},[9478,9481,9482,9483,9492],{"id":8948,"depth":241,"text":8949,"children":9479},[9480],{"id":8955,"depth":248,"text":8956},{"id":8972,"depth":241,"text":8973},{"id":8979,"depth":241,"text":8980},{"id":8996,"depth":241,"text":8997,"children":9484},[9485,9486,9487,9488,9489,9490,9491],{"id":9003,"depth":248,"text":9004},{"id":9046,"depth":248,"text":9047},{"id":9177,"depth":248,"text":9178},{"id":9199,"depth":248,"text":9200},{"id":9228,"depth":248,"text":9229},{"id":9276,"depth":248,"text":9277},{"id":9311,"depth":248,"text":9312},{"id":9446,"depth":241,"text":9447},{"cover_image":258},"2023-03-09T00:00:00.000Z","---\ntitle: \"Taming the whale: introduction to Docker\"\npublishDate: 2023-03-09\ncover_image: ./images/og.png\nsummary: \"An introduction to Docker, with examples\"\nslug: taming-the-whale\ntags:\n- docker\n- programming\n- devops\n- beginners\n- introduction\n---\n\nHave you ever had to work on a project that requires lots of parts that need to be installed separately? And one of those parts refuses to work because maybe the other developer worked on Windows and you are on Linux? Well, what I told you that those problems can be (relatively) easily solved? Let's take a look at what Docker is and how we can use it.\n\n## What is Docker?\n\nDocker is a suite of tools allowing you to run containers on your system.\n\n### But what is a container?\n\naccording to [docker.com](https://docker.com), a container is _a sandboxed process on your machine that is isolated from all other processes on the host machine_; this feature has been available on Linux for some time, but Docker managed to standardize and make it available on other operating systems.\n\n## Why would you use a container?\n\nOne of the reasons Docker is so popular is that it allows people to get started and run projects without necessarily needing to install a whole environment. This usually allows for faster onboarding and testing while also simplifying the deployment of services; by using a container you needn't worry about the exact system-specific settings you might need to handle for (most) of the production applications (a single container can be run on (basically) any operating system).\n\n## Images? Containers?\n\nI have mentioned both containers and images so far, but what's the difference?\n\n- An image is the piece of software that contains all the instructions to run your program; the installed programs, the start command etc...\n\n* A container is what runs an Image; it also handles transmitting environment variables, port forwarding with the host, volumes etc... (more details on those later)\n\n## Docker Cheatsheet\n\nNow that we know (kind of) how Docker works, let's create our first image.\n\n### Installing Docker\n\non most systems, you can install [Docker Desktop](https://docker.com) to have a nice GUI to help you. If you can't or don't want to use that, you can also only install the command line tool by running\n\n```bash\ncurl https://get.docker.com | bash -E\n```\n\nthis should download and install docker on your machine (if this does not work, head on over to the [documentation](https://docs.docker.com/get-started/) for system-specific information).\n\n### Creating an Image\n\nFor the sake of an example, we will be using this example\n::codesandbox{projectType=\"devbox\" projectid=\"cwmtww\"}\n::\nIt's a simple application running an HTTP server. **Let's dockerize it!**\n\nTo do so, we need to first create a Dockefile; this is a specific file format that enables us to describe how an Image should be created.\nThe simplest Dockerfile for this example project would be:\n\n```bash\nFROM node:lts\nCOPY package.json .\nCOPY index.js .\nRUN npm install\nCMD [\"node\", \"index.js\"]\n```\n\nlet's look a bit more into this file:\n\n- FROM: this line describes the base of our image. We need to tell Docker what to base our image on. To do so, you can specify the image **name** and a **tag** separated by **:**\n\n* COPY: copies a file or folder from our computer's filesystem to the image's.\n\n* RUN: executes a command when building the image\n\n* CMD: this is the command that gets executed when the image runs.\n\nto build this image we can run:\n\n```bash\ndocker build -t learn-docker .\n```\n\nthe -t option allows us to specify a tag for the image to find it more easily.\n\n### Running an Image\n\nOnce we have an image tagged, we can run it by saying:\n\n```bash\ndocker run learn-docker\n```\n\n### Detached Mode\n\nYou'll notice that our terminal window is stuck on the output from our terminal; that's all well and good, but it'd be nice if we wouldn't have to open a new terminal for each container we want to run: enter **detached mode**.\n\n```bash\ndocker run -d learn-docker\n```\n\nRunning a container in detached mode means putting the process in the background which in turn means we get our terminal back.\n\n### Stopping a container\n\nTo stop a container, we first need to know its id. To get a list of all your running containers, you can run the command:\n\n```bash\ndocker ps\n```\n\nThe first element of each line is the container id. Then you can just run:\n\n```bash\ndocker stop \u003Cthe_id_of_the_container>\n```\n\nand it will stop your container.\n\n### Ports\n\nyou may have noticed that, at least for now, we are unable to access our api. This is because, to put it simply, Docker is sandboxed in its own network, so we need to explicitly map the container's ports to those of our system's. Let's take the command we had before and add a little option\n\n```bash\ndocker run -p 8000:4000 learn-docker\n```\n\nthis should expose port 3000 of our container on port 8000 of our system. If you visit [localhost:8000](http://127.0.0.1:8000), you should see our hello world message.\n\n### Volumes\n\nnow that we have a basic application running, let's see if we can get a bit more out of it. If you take a look at the code, you'll notice that I'm writing my logs into a file inside a **logs** folder; wouldn't it be neat if we could read that data? This is where volumes come in.\n\nBut before we get started, let's make our life a bit simpler; in our Dockerfile, let's specify the **working directory.** This means that when Docker executes the instruction `COPY index.js .`, **.** will be replaced by the working directory we specified earlier. Our new Dockerfile should look something like this:\n\n```bash\nFROM node:lts\nWORKDIR /app\nCOPY package.json .\nCOPY index.js .\nRUN npm install\nCMD [\"node\", \"index.js\"]\n```\n\nOur logs should be stored in `/app/logs`, right? If so, we could define that folder as a **volume**; a volume is a path that is designated as **persistent**, meaning if the container stops the data inside the volume will not be lost. As an additional bonus, a volume can also mirror files and folders in the container to the host's disk. You could, let's say setup a development environment with hot reloading with Dockefiles and maybe a Docker-Compose (maybe a topic for another article).\nlet's revise our execution line a bit:\n\n```bash\ndocker run -p 8000:4000 -v ./logs:/app/logs learn-docker\n```\n\nthis new addition will tell Docker to use (or create) the folder logs in our terminal's current directory as the volume for the path **/app/logs** of our container. This means that if we look inside our logs folder, we should see a file called **log.txt.** If now we send a request to [http://localhost:8000](http://127.0.0.1:8000), we should a new line get appended to that file.\n\nThis is especially useful for databases because you most likely do not want to lose all your customer's data every time you update/stop/restart a container.\n\n## This is the end...\n\nThis has hopefully been a useful introduction to Docker. If you want to learn more about this technology, the first step would be to visit [the official documentation](https://docs.docker.com); it is pretty well written.\nOther useful resources may be:\n\n- Learn Docker in 7 Easy Steps - Full Beginner's Tutorial - Fireship\n\n::youtube{#gAkwW2tuIqE}\n::\n\n- Docker Tutorial for Beginners - Programming with Mosh\n\n::youtube{#pTFZFxd4hOI}\n::",{"title":8940,"description":8945},"taming-the-whale","articles/taming-the-whale","An introduction to Docker, with examples",[5868,9501,5869,9502,6028],"programming","beginners","xseRXyFsHoL92XakDtFUBm7f2TL5g1K4ADfIRMM1KOo",{"id":9505,"title":9506,"body":9507,"description":11250,"extension":256,"meta":11251,"navigation":259,"path":11253,"publishDate":11254,"rawbody":11255,"seo":11256,"series":11257,"slug":11258,"stem":11259,"summary":11260,"tags":11261,"__hash__":11262},"articles/articles/hook-line-sinker.md","Hook, line, and sinker",{"type":9,"value":9508,"toc":11244},[9509,9517,9521,9524,9530,9541,9694,9708,9826,9831,9838,9842,9859,9862,9965,9977,9983,10091,10098,10384,10390,10393,10424,10433,10489,10495,10576,10585,10626,10632,10638,10641,10645,10655,10661,10664,10888,10899,11192,11197,11200,11211,11223,11226,11234,11238,11241],[17,9510,9511,9512,9516],{},"Following up on the ",[34,9513,9515],{"href":9514},"/articles/react-for-beginners","Beginner's guide to React",", this article will cover the basic \"hooks\" that React offers.",[12,9518,9520],{"id":9519},"what-are-hooks","What are hooks?",[17,9522,9523],{},"Hooks are a functionality introduced in React 16.8. They allow you to create stateful functional components.",[17,9525,9526],{},[130,9527],{"alt":9528,"src":9529},"the hell?","https://media.giphy.com/media/N25nrRX4rsnkY/giphy.gif",[17,9531,9532,9533,9536,9537,9540],{},"If that last phrase made you have the same reaction as Al here, then this article's for you!\nLet me try to explain it this way. Before, if we wanted to have ",[189,9534,9535],{},"state"," in a component, we'd need to use a ",[171,9538,9539],{},"class component"," that would look something like this:",[313,9542,9544],{"className":1387,"code":9543,"language":1389,"meta":240,"style":240},"export default class TodoList extends React.Component {\n state = {\n todos: []\n };\n\n render() {\n return (\n \u003Cul>\n {this.state.todos.map(e => \u003Cli>{e}\u003C/li>)}\n \u003C/ul>\n );\n }\n}\n",[171,9545,9546,9571,9580,9590,9594,9598,9606,9612,9621,9671,9680,9686,9690],{"__ignoreMap":240},[321,9547,9548,9550,9553,9556,9559,9561,9564,9566,9569],{"class":323,"line":324},[321,9549,7541],{"class":344},[321,9551,9552],{"class":344}," default",[321,9554,9555],{"class":344}," class",[321,9557,9558],{"class":352}," TodoList",[321,9560,1442],{"class":344},[321,9562,9563],{"class":352}," React",[321,9565,356],{"class":384},[321,9567,9568],{"class":352},"Component",[321,9570,398],{"class":355},[321,9572,9573,9576,9578],{"class":323,"line":241},[321,9574,9575],{"class":348}," state ",[321,9577,818],{"class":384},[321,9579,398],{"class":355},[321,9581,9582,9585,9587],{"class":323,"line":248},[321,9583,9584],{"class":348}," todos",[321,9586,1350],{"class":384},[321,9588,9589],{"class":348}," []\n",[321,9591,9592],{"class":323,"line":341},[321,9593,3019],{"class":355},[321,9595,9596],{"class":323,"line":362},[321,9597,333],{"emptyLinePlaceholder":259},[321,9599,9600,9602,9604],{"class":323,"line":367},[321,9601,1511],{"class":373},[321,9603,1455],{"class":355},[321,9605,398],{"class":355},[321,9607,9608,9610],{"class":323,"line":401},[321,9609,759],{"class":344},[321,9611,1831],{"class":348},[321,9613,9614,9617,9619],{"class":323,"line":438},[321,9615,9616],{"class":384}," \u003C",[321,9618,21],{"class":1295},[321,9620,4110],{"class":384},[321,9622,9623,9626,9628,9630,9632,9634,9637,9639,9642,9644,9646,9648,9650,9652,9654,9656,9659,9661,9663,9665,9667,9669],{"class":323,"line":455},[321,9624,9625],{"class":355}," {",[321,9627,1563],{"class":449},[321,9629,356],{"class":384},[321,9631,9535],{"class":348},[321,9633,356],{"class":384},[321,9635,9636],{"class":348},"todos",[321,9638,356],{"class":384},[321,9640,9641],{"class":373},"map",[321,9643,377],{"class":348},[321,9645,6780],{"class":380},[321,9647,4556],{"class":344},[321,9649,9262],{"class":384},[321,9651,24],{"class":1295},[321,9653,3896],{"class":384},[321,9655,2477],{"class":355},[321,9657,6780],{"class":9658},"srmJT",[321,9660,1584],{"class":355},[321,9662,4105],{"class":384},[321,9664,24],{"class":1295},[321,9666,3896],{"class":384},[321,9668,238],{"class":348},[321,9670,555],{"class":355},[321,9672,9673,9676,9678],{"class":323,"line":473},[321,9674,9675],{"class":384}," \u003C/",[321,9677,21],{"class":1295},[321,9679,4110],{"class":384},[321,9681,9682,9684],{"class":323,"line":479},[321,9683,1581],{"class":348},[321,9685,1404],{"class":355},[321,9687,9688],{"class":323,"line":512},[321,9689,1471],{"class":355},[321,9691,9692],{"class":323,"line":534},[321,9693,555],{"class":355},[17,9695,9696,9697,9700,9701,9704,9705,9707],{},"But now there's no need to write something so long! With React hooks functional components can have state! Whereas before you would just use a functional component as a ",[189,9698,9699],{},"stateless"," one (where data comes from ",[189,9702,9703],{},"props",", basically), you can now give ",[189,9706,9535],{}," to those components. Let's take the previous example; using hooks, we can rewrite it like this:",[313,9709,9711],{"className":1387,"code":9710,"language":1389,"meta":240,"style":240},"function TodoList() {\n const [todos] = useState([]);\n return (\n \u003Cul>\n {todos.map(e => \u003Cli>{e}\u003C/li>)}\n \u003C/ul>\n );\n}\n\n// Export the function here\n",[171,9712,9713,9723,9743,9749,9758,9797,9806,9813,9817,9821],{"__ignoreMap":240},[321,9714,9715,9717,9719,9721],{"class":323,"line":324},[321,9716,1778],{"class":344},[321,9718,9558],{"class":373},[321,9720,1455],{"class":355},[321,9722,398],{"class":355},[321,9724,9725,9727,9729,9731,9733,9735,9738,9741],{"class":323,"line":241},[321,9726,4890],{"class":344},[321,9728,6196],{"class":355},[321,9730,9636],{"class":348},[321,9732,1290],{"class":355},[321,9734,2573],{"class":384},[321,9736,9737],{"class":373}," useState",[321,9739,9740],{"class":348},"([])",[321,9742,1404],{"class":355},[321,9744,9745,9747],{"class":323,"line":248},[321,9746,6267],{"class":344},[321,9748,1831],{"class":348},[321,9750,9751,9754,9756],{"class":323,"line":341},[321,9752,9753],{"class":384}," \u003C",[321,9755,21],{"class":1295},[321,9757,4110],{"class":384},[321,9759,9760,9763,9765,9767,9769,9771,9773,9775,9777,9779,9781,9783,9785,9787,9789,9791,9793,9795],{"class":323,"line":362},[321,9761,9762],{"class":355}," {",[321,9764,9636],{"class":348},[321,9766,356],{"class":384},[321,9768,9641],{"class":373},[321,9770,377],{"class":348},[321,9772,6780],{"class":380},[321,9774,4556],{"class":344},[321,9776,9262],{"class":384},[321,9778,24],{"class":1295},[321,9780,3896],{"class":384},[321,9782,2477],{"class":355},[321,9784,6780],{"class":348},[321,9786,1584],{"class":355},[321,9788,4105],{"class":384},[321,9790,24],{"class":1295},[321,9792,3896],{"class":384},[321,9794,238],{"class":348},[321,9796,555],{"class":355},[321,9798,9799,9802,9804],{"class":323,"line":367},[321,9800,9801],{"class":384}," \u003C/",[321,9803,21],{"class":1295},[321,9805,4110],{"class":384},[321,9807,9808,9811],{"class":323,"line":401},[321,9809,9810],{"class":348}," )",[321,9812,1404],{"class":355},[321,9814,9815],{"class":323,"line":438},[321,9816,555],{"class":355},[321,9818,9819],{"class":323,"line":455},[321,9820,333],{"emptyLinePlaceholder":259},[321,9822,9823],{"class":323,"line":473},[321,9824,9825],{"class":327},"// Export the function here\n",[17,9827,9828],{},[189,9829,9830],{},"NB: I prefer to write functional components this way for readability, but you could also export the function directly.",[17,9832,9833,9834,9837],{},"Is it just me or did that code become a bit more readable just now? If you're wondering about the ",[171,9835,9836],{},"[todos]"," part, I'll talk more about it in the next section. Speaking (or writing, such as it is) about that, let's dive into React hooks!",[12,9839,9841],{"id":9840},"usestate","UseState",[17,9843,9844,9845,9848,9849,9851,9852,9855,9856,9858],{},"the ",[171,9846,9847],{},"useState"," hook replaces the ",[171,9850,9535],{}," in a class component. It is used to define part of a component's state. Why do I say part of? Because you can (and ",[189,9853,9854],{},"should",") use ",[171,9857,9847],{}," multiple times in a single component. This allows you to have more control over the state and, when used in conjunction with the next hook (spoiler), will allow you to rerun code or rerender of your app on a specific state change.",[17,9860,9861],{},"Let's take the last bit of code and continue from there:",[313,9863,9865],{"className":1387,"code":9864,"language":1389,"meta":240,"style":240},"function TodoList() {\n const [todos] = useState([]);\n return (\n \u003Cul>\n {todos.map(e => \u003Cli>{e}\u003C/li>)}\n \u003C/ul>\n );\n}\n",[171,9866,9867,9877,9895,9901,9909,9947,9955,9961],{"__ignoreMap":240},[321,9868,9869,9871,9873,9875],{"class":323,"line":324},[321,9870,1778],{"class":344},[321,9872,9558],{"class":373},[321,9874,1455],{"class":355},[321,9876,398],{"class":355},[321,9878,9879,9881,9883,9885,9887,9889,9891,9893],{"class":323,"line":241},[321,9880,4890],{"class":344},[321,9882,6196],{"class":355},[321,9884,9636],{"class":348},[321,9886,1290],{"class":355},[321,9888,2573],{"class":384},[321,9890,9737],{"class":373},[321,9892,9740],{"class":348},[321,9894,1404],{"class":355},[321,9896,9897,9899],{"class":323,"line":248},[321,9898,6267],{"class":344},[321,9900,1831],{"class":348},[321,9902,9903,9905,9907],{"class":323,"line":341},[321,9904,9753],{"class":384},[321,9906,21],{"class":1295},[321,9908,4110],{"class":384},[321,9910,9911,9913,9915,9917,9919,9921,9923,9925,9927,9929,9931,9933,9935,9937,9939,9941,9943,9945],{"class":323,"line":362},[321,9912,9762],{"class":355},[321,9914,9636],{"class":348},[321,9916,356],{"class":384},[321,9918,9641],{"class":373},[321,9920,377],{"class":348},[321,9922,6780],{"class":380},[321,9924,4556],{"class":344},[321,9926,9262],{"class":384},[321,9928,24],{"class":1295},[321,9930,3896],{"class":384},[321,9932,2477],{"class":355},[321,9934,6780],{"class":348},[321,9936,1584],{"class":355},[321,9938,4105],{"class":384},[321,9940,24],{"class":1295},[321,9942,3896],{"class":384},[321,9944,238],{"class":348},[321,9946,555],{"class":355},[321,9948,9949,9951,9953],{"class":323,"line":367},[321,9950,9801],{"class":384},[321,9952,21],{"class":1295},[321,9954,4110],{"class":384},[321,9956,9957,9959],{"class":323,"line":401},[321,9958,9810],{"class":348},[321,9960,1404],{"class":355},[321,9962,9963],{"class":323,"line":438},[321,9964,555],{"class":355},[17,9966,9967,9968,9970,9971,9974,9975,356],{},"At this moment, the component renders a list of ",[189,9969,9636],{},", but you might notice a problem; we have no way to ",[230,9972,9973],{},"set"," those ",[189,9976,9636],{},[17,9978,9979,9980,9982],{},"To do that, we need to understand the return values of ",[171,9981,9847],{},": this function returns an array containing the state's value and a function to update said values. We can call it (and the value, for that matter) whatever we want, but let's stick to the conventions for now.\nLet's update the code we've written before:",[313,9984,9986],{"className":1387,"code":9985,"language":1389,"meta":240,"style":240},"function TodoList() {\n const [todos, setTodos] = useState([]);\n return (\n \u003Cul>\n {todos.map(e => \u003Cli>{e}\u003C/li>)}\n \u003C/ul>\n );\n}\n",[171,9987,9988,9998,10021,10027,10035,10073,10081,10087],{"__ignoreMap":240},[321,9989,9990,9992,9994,9996],{"class":323,"line":324},[321,9991,1778],{"class":344},[321,9993,9558],{"class":373},[321,9995,1455],{"class":355},[321,9997,398],{"class":355},[321,9999,10000,10002,10004,10006,10008,10011,10013,10015,10017,10019],{"class":323,"line":241},[321,10001,4890],{"class":344},[321,10003,6196],{"class":355},[321,10005,9636],{"class":348},[321,10007,407],{"class":355},[321,10009,10010],{"class":348}," setTodos",[321,10012,1290],{"class":355},[321,10014,2573],{"class":384},[321,10016,9737],{"class":373},[321,10018,9740],{"class":348},[321,10020,1404],{"class":355},[321,10022,10023,10025],{"class":323,"line":248},[321,10024,6267],{"class":344},[321,10026,1831],{"class":348},[321,10028,10029,10031,10033],{"class":323,"line":341},[321,10030,9753],{"class":384},[321,10032,21],{"class":1295},[321,10034,4110],{"class":384},[321,10036,10037,10039,10041,10043,10045,10047,10049,10051,10053,10055,10057,10059,10061,10063,10065,10067,10069,10071],{"class":323,"line":362},[321,10038,9762],{"class":355},[321,10040,9636],{"class":348},[321,10042,356],{"class":384},[321,10044,9641],{"class":373},[321,10046,377],{"class":348},[321,10048,6780],{"class":380},[321,10050,4556],{"class":344},[321,10052,9262],{"class":384},[321,10054,24],{"class":1295},[321,10056,3896],{"class":384},[321,10058,2477],{"class":355},[321,10060,6780],{"class":348},[321,10062,1584],{"class":355},[321,10064,4105],{"class":384},[321,10066,24],{"class":1295},[321,10068,3896],{"class":384},[321,10070,238],{"class":348},[321,10072,555],{"class":355},[321,10074,10075,10077,10079],{"class":323,"line":367},[321,10076,9801],{"class":384},[321,10078,21],{"class":1295},[321,10080,4110],{"class":384},[321,10082,10083,10085],{"class":323,"line":401},[321,10084,9810],{"class":348},[321,10086,1404],{"class":355},[321,10088,10089],{"class":323,"line":438},[321,10090,555],{"class":355},[17,10092,10093,10094,10097],{},"Now we have a function to set our todos. Let's add an input and a button to add todos. In the input, we'll listen to the ",[171,10095,10096],{},"onChange"," event to modify some other piece of state.",[313,10099,10101],{"className":1387,"code":10100,"language":1389,"meta":240,"style":240},"function TodoList() {\n const [todos, setTodos] = useState([]);\n const [newTodo, setNewTodo] = useState(\"\");\n return (\n \u003Cdiv>\n \u003Cul>\n {todos.map(e => \u003Cli>{e}\u003C/li>)}\n \u003C/ul>\n \u003Cinput onChange={(e) => { setNewTodo(e.target.value); }} value={newTodo} />\n \u003Cbutton onClick={() => {\n setTodos(oldTodos => [...oldTodos, newTodo]);\n setNewTodo(\"\");\n }}\n >\n Add Todo\n \u003C/button>\n \u003C/div>\n );\n}\n",[171,10102,10103,10113,10135,10164,10170,10179,10187,10225,10233,10286,10305,10330,10343,10348,10353,10358,10366,10374,10380],{"__ignoreMap":240},[321,10104,10105,10107,10109,10111],{"class":323,"line":324},[321,10106,1778],{"class":344},[321,10108,9558],{"class":373},[321,10110,1455],{"class":355},[321,10112,398],{"class":355},[321,10114,10115,10117,10119,10121,10123,10125,10127,10129,10131,10133],{"class":323,"line":241},[321,10116,4890],{"class":344},[321,10118,6196],{"class":355},[321,10120,9636],{"class":348},[321,10122,407],{"class":355},[321,10124,10010],{"class":348},[321,10126,1290],{"class":355},[321,10128,2573],{"class":384},[321,10130,9737],{"class":373},[321,10132,9740],{"class":348},[321,10134,1404],{"class":355},[321,10136,10137,10139,10141,10144,10146,10149,10151,10153,10155,10157,10160,10162],{"class":323,"line":248},[321,10138,4890],{"class":344},[321,10140,6196],{"class":355},[321,10142,10143],{"class":348},"newTodo",[321,10145,407],{"class":355},[321,10147,10148],{"class":348}," setNewTodo",[321,10150,1290],{"class":355},[321,10152,2573],{"class":384},[321,10154,9737],{"class":373},[321,10156,377],{"class":348},[321,10158,10159],{"class":431},"\"\"",[321,10161,238],{"class":348},[321,10163,1404],{"class":355},[321,10165,10166,10168],{"class":323,"line":341},[321,10167,6267],{"class":344},[321,10169,1831],{"class":348},[321,10171,10172,10174,10177],{"class":323,"line":362},[321,10173,9753],{"class":384},[321,10175,10176],{"class":1295},"div",[321,10178,4110],{"class":384},[321,10180,10181,10183,10185],{"class":323,"line":367},[321,10182,9616],{"class":384},[321,10184,21],{"class":1295},[321,10186,4110],{"class":384},[321,10188,10189,10191,10193,10195,10197,10199,10201,10203,10205,10207,10209,10211,10213,10215,10217,10219,10221,10223],{"class":323,"line":401},[321,10190,9625],{"class":355},[321,10192,9636],{"class":348},[321,10194,356],{"class":384},[321,10196,9641],{"class":373},[321,10198,377],{"class":348},[321,10200,6780],{"class":380},[321,10202,4556],{"class":344},[321,10204,9262],{"class":384},[321,10206,24],{"class":1295},[321,10208,3896],{"class":384},[321,10210,2477],{"class":355},[321,10212,6780],{"class":348},[321,10214,1584],{"class":355},[321,10216,4105],{"class":384},[321,10218,24],{"class":1295},[321,10220,3896],{"class":384},[321,10222,238],{"class":348},[321,10224,555],{"class":355},[321,10226,10227,10229,10231],{"class":323,"line":438},[321,10228,9675],{"class":384},[321,10230,21],{"class":1295},[321,10232,4110],{"class":384},[321,10234,10235,10237,10240,10243,10245,10248,10250,10252,10254,10256,10258,10260,10262,10264,10266,10269,10271,10274,10276,10278,10280,10282,10284],{"class":323,"line":455},[321,10236,9616],{"class":384},[321,10238,10239],{"class":1295},"input",[321,10241,10242],{"class":4065}," onChange",[321,10244,818],{"class":384},[321,10246,10247],{"class":355},"{(",[321,10249,6780],{"class":380},[321,10251,238],{"class":355},[321,10253,4556],{"class":344},[321,10255,3851],{"class":355},[321,10257,10148],{"class":373},[321,10259,6809],{"class":348},[321,10261,356],{"class":384},[321,10263,663],{"class":348},[321,10265,356],{"class":384},[321,10267,10268],{"class":348},"value)",[321,10270,3861],{"class":355},[321,10272,10273],{"class":355}," }}",[321,10275,3864],{"class":4065},[321,10277,818],{"class":384},[321,10279,2477],{"class":355},[321,10281,10143],{"class":348},[321,10283,1584],{"class":355},[321,10285,4165],{"class":384},[321,10287,10288,10290,10293,10296,10298,10301,10303],{"class":323,"line":473},[321,10289,9616],{"class":384},[321,10291,10292],{"class":1295},"button",[321,10294,10295],{"class":4065}," onClick",[321,10297,818],{"class":384},[321,10299,10300],{"class":355},"{()",[321,10302,4556],{"class":344},[321,10304,398],{"class":355},[321,10306,10307,10310,10312,10315,10317,10319,10321,10323,10325,10328],{"class":323,"line":479},[321,10308,10309],{"class":373}," setTodos",[321,10311,377],{"class":348},[321,10313,10314],{"class":380},"oldTodos",[321,10316,4556],{"class":344},[321,10318,6196],{"class":348},[321,10320,4153],{"class":384},[321,10322,10314],{"class":348},[321,10324,407],{"class":355},[321,10326,10327],{"class":348}," newTodo])",[321,10329,1404],{"class":355},[321,10331,10332,10335,10337,10339,10341],{"class":323,"line":512},[321,10333,10334],{"class":373}," setNewTodo",[321,10336,377],{"class":348},[321,10338,10159],{"class":431},[321,10340,238],{"class":348},[321,10342,1404],{"class":355},[321,10344,10345],{"class":323,"line":534},[321,10346,10347],{"class":355}," }}\n",[321,10349,10350],{"class":323,"line":552},[321,10351,10352],{"class":384}," >\n",[321,10354,10355],{"class":323,"line":891},[321,10356,10357],{"class":348}," Add Todo\n",[321,10359,10360,10362,10364],{"class":323,"line":897},[321,10361,9675],{"class":384},[321,10363,10292],{"class":1295},[321,10365,4110],{"class":384},[321,10367,10368,10370,10372],{"class":323,"line":902},[321,10369,9801],{"class":384},[321,10371,10176],{"class":1295},[321,10373,4110],{"class":384},[321,10375,10376,10378],{"class":323,"line":907},[321,10377,9810],{"class":348},[321,10379,1404],{"class":355},[321,10381,10382],{"class":323,"line":930},[321,10383,555],{"class":355},[17,10385,10386],{},[130,10387],{"alt":10388,"src":10389},"woah woah woah","https://media.giphy.com/media/RXKCMLmch5W2Q/giphy.gif",[17,10391,10392],{},"Yeah, that's a lot of new stuff to cover, I know. Let's break it down",[313,10394,10396],{"className":1387,"code":10395,"language":1389,"meta":240,"style":240},"const [newTodo, setNewTodo] = useState(\"\");\n",[171,10397,10398],{"__ignoreMap":240},[321,10399,10400,10402,10404,10406,10408,10410,10412,10414,10416,10418,10420,10422],{"class":323,"line":324},[321,10401,4209],{"class":344},[321,10403,6196],{"class":355},[321,10405,10143],{"class":348},[321,10407,407],{"class":355},[321,10409,10148],{"class":348},[321,10411,1290],{"class":355},[321,10413,2573],{"class":384},[321,10415,9737],{"class":373},[321,10417,377],{"class":348},[321,10419,10159],{"class":431},[321,10421,238],{"class":348},[321,10423,1404],{"class":355},[17,10425,10426,10427,10429,10430,10432],{},"This is just a new useState. Notice how this time I put ",[230,10428,10159],{}," as a first argument. The first argument in a useState function call defines the starting value of the state and is also used to determine its ",[189,10431,3811],{},". This can be used by us (the developers) to get IntelliSense on the state (autocompletion etc).",[313,10434,10436],{"className":1387,"code":10435,"language":1389,"meta":240,"style":240},"\u003Cinput onChange={(e) => { setNewTodo(e.target.value); }} value={newTodo} />;\n",[171,10437,10438],{"__ignoreMap":240},[321,10439,10440,10442,10444,10446,10448,10450,10452,10454,10456,10458,10460,10462,10464,10466,10468,10470,10472,10474,10476,10478,10480,10482,10484,10487],{"class":323,"line":324},[321,10441,3885],{"class":384},[321,10443,10239],{"class":1295},[321,10445,10242],{"class":4065},[321,10447,818],{"class":384},[321,10449,10247],{"class":355},[321,10451,6780],{"class":380},[321,10453,238],{"class":355},[321,10455,4556],{"class":344},[321,10457,3851],{"class":355},[321,10459,10148],{"class":373},[321,10461,6809],{"class":348},[321,10463,356],{"class":384},[321,10465,663],{"class":348},[321,10467,356],{"class":384},[321,10469,10268],{"class":348},[321,10471,3861],{"class":355},[321,10473,10273],{"class":355},[321,10475,3864],{"class":4065},[321,10477,818],{"class":384},[321,10479,2477],{"class":355},[321,10481,10143],{"class":348},[321,10483,1584],{"class":355},[321,10485,10486],{"class":384}," />",[321,10488,1404],{"class":355},[17,10490,10491,10492,10494],{},"This input does two things: it reads the state ",[189,10493,10143],{}," and uses it as value while also updating it with its new value (when a user types in the input, the state will change to reflect that change).",[313,10496,10498],{"className":1387,"code":10497,"language":1389,"meta":240,"style":240},"\u003Cbutton onClick={() => {\n setTodos(oldTodos => [...oldTodos, newTodo]);\n setNewTodo(\"\");\n}}\n>\n Add Todo\n\u003C/button>;\n",[171,10499,10500,10516,10539,10552,10557,10561,10566],{"__ignoreMap":240},[321,10501,10502,10504,10506,10508,10510,10512,10514],{"class":323,"line":324},[321,10503,3885],{"class":384},[321,10505,10292],{"class":1295},[321,10507,10295],{"class":4065},[321,10509,818],{"class":384},[321,10511,10300],{"class":355},[321,10513,4556],{"class":344},[321,10515,398],{"class":355},[321,10517,10518,10521,10523,10525,10527,10529,10531,10533,10535,10537],{"class":323,"line":241},[321,10519,10520],{"class":373}," setTodos",[321,10522,377],{"class":348},[321,10524,10314],{"class":380},[321,10526,4556],{"class":344},[321,10528,6196],{"class":348},[321,10530,4153],{"class":384},[321,10532,10314],{"class":348},[321,10534,407],{"class":355},[321,10536,10327],{"class":348},[321,10538,1404],{"class":355},[321,10540,10541,10544,10546,10548,10550],{"class":323,"line":248},[321,10542,10543],{"class":373}," setNewTodo",[321,10545,377],{"class":348},[321,10547,10159],{"class":431},[321,10549,238],{"class":348},[321,10551,1404],{"class":355},[321,10553,10554],{"class":323,"line":341},[321,10555,10556],{"class":355},"}}\n",[321,10558,10559],{"class":323,"line":362},[321,10560,4110],{"class":384},[321,10562,10563],{"class":323,"line":367},[321,10564,10565],{"class":348}," Add Todo\n",[321,10567,10568,10570,10572,10574],{"class":323,"line":401},[321,10569,4105],{"class":384},[321,10571,10292],{"class":1295},[321,10573,3896],{"class":384},[321,10575,1404],{"class":355},[17,10577,10578,10579,10581,10582,10584],{},"This button add the ",[189,10580,10143],{}," to the list and resets the input's text (i.e. ",[189,10583,10143],{},"). You might have noticed something...",[313,10586,10588],{"className":1387,"code":10587,"language":1389,"meta":240,"style":240},"setTodos(oldTodos => [...oldTodos, newTodo]);\nsetNewTodo(\"\");\n",[171,10589,10590,10613],{"__ignoreMap":240},[321,10591,10592,10595,10597,10599,10601,10603,10605,10607,10609,10611],{"class":323,"line":324},[321,10593,10594],{"class":373},"setTodos",[321,10596,377],{"class":348},[321,10598,10314],{"class":380},[321,10600,4556],{"class":344},[321,10602,6196],{"class":348},[321,10604,4153],{"class":384},[321,10606,10314],{"class":348},[321,10608,407],{"class":355},[321,10610,10327],{"class":348},[321,10612,1404],{"class":355},[321,10614,10615,10618,10620,10622,10624],{"class":323,"line":241},[321,10616,10617],{"class":373},"setNewTodo",[321,10619,377],{"class":348},[321,10621,10159],{"class":431},[321,10623,238],{"class":348},[321,10625,1404],{"class":355},[17,10627,10628],{},[130,10629],{"alt":10630,"src":10631},"different","https://media.giphy.com/media/IwX8XVO9mx3SmncyF5/giphy.gif",[17,10633,10634,10635,10637],{},"When you ",[189,10636,9973],{}," a state, you can either directly pass the new value as an argument, or use an arrow function to get a reference to the current state's value (useful for updating arrays).",[17,10639,10640],{},"And that's it for the useState hook !!",[12,10642,10644],{"id":10643},"useeffect","useEffect",[17,10646,10647,10648,10651,10652,10654],{},"Now that we know how to set state in a component, let's see how to replace a ",[171,10649,10650],{},"componentDidUpdate"," using hooks. the ",[171,10653,10650],{}," method of a class component was used to trigger effects based on state and props change.",[17,10656,10657,10658,10660],{},"Let's say we're building a photo search app and we need to update the search results based on the current value of an input field. As the user types in the input field, we want to update the result. We could do it in the ",[171,10659,10096],{}," callback, but that would make the rerender dependent on the onChange and the app would become laggy (at least it used to, don't quote me on that).",[17,10662,10663],{},"Let's setup our basic component:",[313,10665,10667],{"className":1387,"code":10666,"language":1389,"meta":240,"style":240},"function PhotoSearch() {\n const [searchValue, setSearchValue] = useState(\"\");\n const [photoResult, setPhotoResult] = useState([]);\n\n return (\n \u003Cdiv>\n \u003Cinput\n value={searchValue}\n onChange={(e) => { setSearchValue(e.target.value); }}\n />\n \u003Cdiv>\n {photoResult.map(e => (\n \u003Cimg src={e} />\n ))}\n \u003C/div>\n \u003C/div>\n );\n}\n",[171,10668,10669,10680,10708,10732,10736,10742,10750,10757,10770,10804,10809,10817,10835,10855,10862,10870,10878,10884],{"__ignoreMap":240},[321,10670,10671,10673,10676,10678],{"class":323,"line":324},[321,10672,1778],{"class":344},[321,10674,10675],{"class":373}," PhotoSearch",[321,10677,1455],{"class":355},[321,10679,398],{"class":355},[321,10681,10682,10684,10686,10689,10691,10694,10696,10698,10700,10702,10704,10706],{"class":323,"line":241},[321,10683,4890],{"class":344},[321,10685,6196],{"class":355},[321,10687,10688],{"class":348},"searchValue",[321,10690,407],{"class":355},[321,10692,10693],{"class":348}," setSearchValue",[321,10695,1290],{"class":355},[321,10697,2573],{"class":384},[321,10699,9737],{"class":373},[321,10701,377],{"class":348},[321,10703,10159],{"class":431},[321,10705,238],{"class":348},[321,10707,1404],{"class":355},[321,10709,10710,10712,10714,10717,10719,10722,10724,10726,10728,10730],{"class":323,"line":248},[321,10711,4890],{"class":344},[321,10713,6196],{"class":355},[321,10715,10716],{"class":348},"photoResult",[321,10718,407],{"class":355},[321,10720,10721],{"class":348}," setPhotoResult",[321,10723,1290],{"class":355},[321,10725,2573],{"class":384},[321,10727,9737],{"class":373},[321,10729,9740],{"class":348},[321,10731,1404],{"class":355},[321,10733,10734],{"class":323,"line":341},[321,10735,333],{"emptyLinePlaceholder":259},[321,10737,10738,10740],{"class":323,"line":362},[321,10739,6267],{"class":344},[321,10741,1831],{"class":348},[321,10743,10744,10746,10748],{"class":323,"line":367},[321,10745,9753],{"class":384},[321,10747,10176],{"class":1295},[321,10749,4110],{"class":384},[321,10751,10752,10754],{"class":323,"line":401},[321,10753,9616],{"class":384},[321,10755,10756],{"class":1295},"input\n",[321,10758,10759,10762,10764,10766,10768],{"class":323,"line":438},[321,10760,10761],{"class":4065}," value",[321,10763,818],{"class":384},[321,10765,2477],{"class":355},[321,10767,10688],{"class":348},[321,10769,555],{"class":355},[321,10771,10772,10775,10777,10779,10781,10783,10785,10787,10789,10791,10793,10795,10797,10799,10801],{"class":323,"line":455},[321,10773,10774],{"class":4065}," onChange",[321,10776,818],{"class":384},[321,10778,10247],{"class":355},[321,10780,6780],{"class":380},[321,10782,238],{"class":355},[321,10784,4556],{"class":344},[321,10786,3851],{"class":355},[321,10788,10693],{"class":373},[321,10790,6809],{"class":348},[321,10792,356],{"class":384},[321,10794,663],{"class":348},[321,10796,356],{"class":384},[321,10798,10268],{"class":348},[321,10800,3861],{"class":355},[321,10802,10803],{"class":355}," }}\n",[321,10805,10806],{"class":323,"line":473},[321,10807,10808],{"class":384}," />\n",[321,10810,10811,10813,10815],{"class":323,"line":479},[321,10812,9616],{"class":384},[321,10814,10176],{"class":1295},[321,10816,4110],{"class":384},[321,10818,10819,10821,10823,10825,10827,10829,10831,10833],{"class":323,"line":512},[321,10820,9625],{"class":355},[321,10822,10716],{"class":348},[321,10824,356],{"class":384},[321,10826,9641],{"class":373},[321,10828,377],{"class":348},[321,10830,6780],{"class":380},[321,10832,4556],{"class":344},[321,10834,1831],{"class":348},[321,10836,10837,10840,10842,10845,10847,10849,10851,10853],{"class":323,"line":534},[321,10838,10839],{"class":384}," \u003C",[321,10841,130],{"class":1295},[321,10843,10844],{"class":4065}," src",[321,10846,818],{"class":384},[321,10848,2477],{"class":355},[321,10850,6780],{"class":348},[321,10852,1584],{"class":355},[321,10854,4165],{"class":384},[321,10856,10857,10860],{"class":323,"line":552},[321,10858,10859],{"class":348}," ))",[321,10861,555],{"class":355},[321,10863,10864,10866,10868],{"class":323,"line":891},[321,10865,9675],{"class":384},[321,10867,10176],{"class":1295},[321,10869,4110],{"class":384},[321,10871,10872,10874,10876],{"class":323,"line":897},[321,10873,9801],{"class":384},[321,10875,10176],{"class":1295},[321,10877,4110],{"class":384},[321,10879,10880,10882],{"class":323,"line":902},[321,10881,9810],{"class":348},[321,10883,1404],{"class":355},[321,10885,10886],{"class":323,"line":907},[321,10887,555],{"class":355},[17,10889,10890,10891,10893,10894,238],{},"So, nothing new for now; we have to states, searchValue and photoResult, the former to store the input's value and the second to store the photos matching that search. Now let's use the ",[171,10892,10644],{}," hook to update the photoResult array with our search results (here I'm using a basic example, a real API would have more complex data formats (like ",[34,10895,10898],{"href":10896,"rel":10897},"https://unsplash.com/developers",[38],"unsplash",[313,10900,10902],{"className":1387,"code":10901,"language":1389,"meta":240,"style":240},"function PhotoSearch() {\n const [searchValue, setSearchValue] = useState(\"\");\n const [photoResult, setPhotoResult] = useState([]);\n\n useEffect(() => {\n if (searchValue) {\n fetchImages(searchValue).then((data) => {\n setPhotoResult(data);\n });\n }\n }, [searchValue]);\n\n return (\n \u003Cdiv>\n \u003Cinput\n value={searchValue}\n onChange={(e) => { setSearchValue(e.target.value); }}\n />\n \u003Cdiv>\n {photoResult.map(e => (\n \u003Cimg src={e} />\n ))}\n \u003C/div>\n \u003C/div>\n );\n}\n",[171,10903,10904,10914,10940,10962,10966,10979,10988,11012,11021,11030,11034,11044,11048,11054,11062,11068,11080,11112,11116,11124,11142,11160,11166,11174,11182,11188],{"__ignoreMap":240},[321,10905,10906,10908,10910,10912],{"class":323,"line":324},[321,10907,1778],{"class":344},[321,10909,10675],{"class":373},[321,10911,1455],{"class":355},[321,10913,398],{"class":355},[321,10915,10916,10918,10920,10922,10924,10926,10928,10930,10932,10934,10936,10938],{"class":323,"line":241},[321,10917,4890],{"class":344},[321,10919,6196],{"class":355},[321,10921,10688],{"class":348},[321,10923,407],{"class":355},[321,10925,10693],{"class":348},[321,10927,1290],{"class":355},[321,10929,2573],{"class":384},[321,10931,9737],{"class":373},[321,10933,377],{"class":348},[321,10935,10159],{"class":431},[321,10937,238],{"class":348},[321,10939,1404],{"class":355},[321,10941,10942,10944,10946,10948,10950,10952,10954,10956,10958,10960],{"class":323,"line":248},[321,10943,4890],{"class":344},[321,10945,6196],{"class":355},[321,10947,10716],{"class":348},[321,10949,407],{"class":355},[321,10951,10721],{"class":348},[321,10953,1290],{"class":355},[321,10955,2573],{"class":384},[321,10957,9737],{"class":373},[321,10959,9740],{"class":348},[321,10961,1404],{"class":355},[321,10963,10964],{"class":323,"line":341},[321,10965,333],{"emptyLinePlaceholder":259},[321,10967,10968,10971,10973,10975,10977],{"class":323,"line":362},[321,10969,10970],{"class":373}," useEffect",[321,10972,377],{"class":348},[321,10974,1455],{"class":355},[321,10976,4556],{"class":344},[321,10978,398],{"class":355},[321,10980,10981,10983,10986],{"class":323,"line":367},[321,10982,441],{"class":344},[321,10984,10985],{"class":348}," (searchValue) ",[321,10987,1732],{"class":355},[321,10989,10990,10993,10996,10998,11000,11002,11004,11006,11008,11010],{"class":323,"line":401},[321,10991,10992],{"class":373}," fetchImages",[321,10994,10995],{"class":348},"(searchValue)",[321,10997,356],{"class":384},[321,10999,5251],{"class":373},[321,11001,377],{"class":348},[321,11003,377],{"class":355},[321,11005,8085],{"class":380},[321,11007,238],{"class":355},[321,11009,4556],{"class":344},[321,11011,398],{"class":355},[321,11013,11014,11017,11019],{"class":323,"line":438},[321,11015,11016],{"class":373}," setPhotoResult",[321,11018,6260],{"class":348},[321,11020,1404],{"class":355},[321,11022,11023,11026,11028],{"class":323,"line":455},[321,11024,11025],{"class":355}," }",[321,11027,238],{"class":348},[321,11029,1404],{"class":355},[321,11031,11032],{"class":323,"line":473},[321,11033,476],{"class":355},[321,11035,11036,11039,11042],{"class":323,"line":479},[321,11037,11038],{"class":355}," },",[321,11040,11041],{"class":348}," [searchValue])",[321,11043,1404],{"class":355},[321,11045,11046],{"class":323,"line":512},[321,11047,333],{"emptyLinePlaceholder":259},[321,11049,11050,11052],{"class":323,"line":534},[321,11051,6267],{"class":344},[321,11053,1831],{"class":348},[321,11055,11056,11058,11060],{"class":323,"line":552},[321,11057,9753],{"class":384},[321,11059,10176],{"class":1295},[321,11061,4110],{"class":384},[321,11063,11064,11066],{"class":323,"line":891},[321,11065,9616],{"class":384},[321,11067,10756],{"class":1295},[321,11069,11070,11072,11074,11076,11078],{"class":323,"line":897},[321,11071,10761],{"class":4065},[321,11073,818],{"class":384},[321,11075,2477],{"class":355},[321,11077,10688],{"class":348},[321,11079,555],{"class":355},[321,11081,11082,11084,11086,11088,11090,11092,11094,11096,11098,11100,11102,11104,11106,11108,11110],{"class":323,"line":902},[321,11083,10774],{"class":4065},[321,11085,818],{"class":384},[321,11087,10247],{"class":355},[321,11089,6780],{"class":380},[321,11091,238],{"class":355},[321,11093,4556],{"class":344},[321,11095,3851],{"class":355},[321,11097,10693],{"class":373},[321,11099,6809],{"class":348},[321,11101,356],{"class":384},[321,11103,663],{"class":348},[321,11105,356],{"class":384},[321,11107,10268],{"class":348},[321,11109,3861],{"class":355},[321,11111,10803],{"class":355},[321,11113,11114],{"class":323,"line":907},[321,11115,10808],{"class":384},[321,11117,11118,11120,11122],{"class":323,"line":930},[321,11119,9616],{"class":384},[321,11121,10176],{"class":1295},[321,11123,4110],{"class":384},[321,11125,11126,11128,11130,11132,11134,11136,11138,11140],{"class":323,"line":947},[321,11127,9625],{"class":355},[321,11129,10716],{"class":348},[321,11131,356],{"class":384},[321,11133,9641],{"class":373},[321,11135,377],{"class":348},[321,11137,6780],{"class":380},[321,11139,4556],{"class":344},[321,11141,1831],{"class":348},[321,11143,11144,11146,11148,11150,11152,11154,11156,11158],{"class":323,"line":967},[321,11145,10839],{"class":384},[321,11147,130],{"class":1295},[321,11149,10844],{"class":4065},[321,11151,818],{"class":384},[321,11153,2477],{"class":355},[321,11155,6780],{"class":348},[321,11157,1584],{"class":355},[321,11159,4165],{"class":384},[321,11161,11162,11164],{"class":323,"line":984},[321,11163,10859],{"class":348},[321,11165,555],{"class":355},[321,11167,11168,11170,11172],{"class":323,"line":2068},[321,11169,9675],{"class":384},[321,11171,10176],{"class":1295},[321,11173,4110],{"class":384},[321,11175,11176,11178,11180],{"class":323,"line":2074},[321,11177,9801],{"class":384},[321,11179,10176],{"class":1295},[321,11181,4110],{"class":384},[321,11183,11184,11186],{"class":323,"line":2082},[321,11185,9810],{"class":348},[321,11187,1404],{"class":355},[321,11189,11190],{"class":323,"line":2087},[321,11191,555],{"class":355},[17,11193,11194],{},[189,11195,11196],{},"NB: fetchImages acts as a function that returns an array of links to images from an API",[17,11198,11199],{},"Let's break it down:",[17,11201,11202,11204,11205,11207,11208,11210],{},[171,11203,10644],{}," takes two arguments (the second being optional): the former is a callback function and the latter is a dependency array.\nThe callback function gets executed every time the values inside the dependency array change. In our example, we want to rerun that function each time the ",[171,11206,10688],{}," changes, so we add ",[171,11209,10688],{}," to the dependency array. Notice also the condition inside the callback:",[313,11212,11214],{"className":1387,"code":11213,"language":1389,"meta":240,"style":240},"if (searchValue)\n",[171,11215,11216],{"__ignoreMap":240},[321,11217,11218,11220],{"class":323,"line":324},[321,11219,4039],{"class":344},[321,11221,11222],{"class":348}," (searchValue)\n",[17,11224,11225],{},"Since the value could be empty after a user deletes its input or when the page first loads, we need to check before executing a code that depends on it.",[17,11227,11228,11229,356],{},"There are other quirks with useEffect, and you can read more about them on the ",[34,11230,11233],{"href":11231,"rel":11232},"https://reactjs.org/docs/hooks-effect.html",[38],"official React documentation",[12,11235,11237],{"id":11236},"final-words","Final words",[17,11239,11240],{},"These are just the two hooks you'll use the most. Others exist (useContext, useCallback, useMemo), and you can even make your own. We'll probably look at those another time, on another article, but let's stop here for the time being.",[1167,11242,11243],{},"html pre.shiki code .sIF4r, html code.shiki .sIF4r{--shiki-default:#C6A0F6}html pre.shiki code .s80kZ, html code.shiki .s80kZ{--shiki-default:#EED49F;--shiki-default-font-style:italic}html pre.shiki code .sXptk, html code.shiki .sXptk{--shiki-default:#8BD5CA}html pre.shiki code .slVFb, html code.shiki .slVFb{--shiki-default:#939AB7}html pre.shiki code .sFaBz, html code.shiki .sFaBz{--shiki-default:#CAD3F5}html pre.shiki code .siK8c, html code.shiki .siK8c{--shiki-default:#8AADF4;--shiki-default-font-style:italic}html pre.shiki code .s57MT, html code.shiki .s57MT{--shiki-default:#8AADF4}html pre.shiki code .s4s3B, html code.shiki .s4s3B{--shiki-default:#ED8796}html pre.shiki code .skVQi, html code.shiki .skVQi{--shiki-default:#EE99A0;--shiki-default-font-style:italic}html pre.shiki code .srmJT, html code.shiki .srmJT{--shiki-default:#EE99A0}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sfEIy, html code.shiki .sfEIy{--shiki-default:#939AB7;--shiki-default-font-style:italic}html pre.shiki code .sSZ1V, html code.shiki .sSZ1V{--shiki-default:#A6DA95}html pre.shiki code .swja1, html code.shiki .swja1{--shiki-default:#EED49F}",{"title":240,"searchDepth":241,"depth":241,"links":11245},[11246,11247,11248,11249],{"id":9519,"depth":241,"text":9520},{"id":9840,"depth":241,"text":9841},{"id":10643,"depth":241,"text":10644},{"id":11236,"depth":241,"text":11237},"Following up on the Beginner's guide to React, this article will cover the basic \"hooks\" that React offers.",{"cuid":11252,"cover_image":258},"ckytwsg0f01n62vs18lyr91cu","/articles/hook-line-sinker","2022-01-25T00:00:00.000Z","---\ntitle: \"Hook, line, and sinker\"\npublishDate: 2022-01-25\ncuid: ckytwsg0f01n62vs18lyr91cu\nsummary: \"A practical introduction to React hooks\"\ncover_image: ./images/og.png\nslug: hook-line-sinker\ntags:\n- javascript\n- reactjs\n- beginners\n- programming\nseries: \"React for Beginners\"\n\n---\n\nFollowing up on the [Beginner's guide to React](/articles/react-for-beginners), this article will cover the basic \"hooks\" that React offers.\n\n## What are hooks?\n\nHooks are a functionality introduced in React 16.8. They allow you to create stateful functional components.\n\n\n\nIf that last phrase made you have the same reaction as Al here, then this article's for you!\nLet me try to explain it this way. Before, if we wanted to have _state_ in a component, we'd need to use a `class component` that would look something like this:\n\n```js\nexport default class TodoList extends React.Component {\n state = {\n todos: []\n };\n\n render() {\n return (\n \u003Cul>\n {this.state.todos.map(e => \u003Cli>{e}\u003C/li>)}\n \u003C/ul>\n );\n }\n}\n```\n\nBut now there's no need to write something so long! With React hooks functional components can have state! Whereas before you would just use a functional component as a _stateless_ one (where data comes from _props_, basically), you can now give _state_ to those components. Let's take the previous example; using hooks, we can rewrite it like this:\n\n```js\nfunction TodoList() {\n const [todos] = useState([]);\n return (\n \u003Cul>\n {todos.map(e => \u003Cli>{e}\u003C/li>)}\n \u003C/ul>\n );\n}\n\n// Export the function here\n```\n\n_NB: I prefer to write functional components this way for readability, but you could also export the function directly._\n\nIs it just me or did that code become a bit more readable just now? If you're wondering about the `[todos]` part, I'll talk more about it in the next section. Speaking (or writing, such as it is) about that, let's dive into React hooks!\n\n## UseState\n\nthe `useState` hook replaces the `state` in a class component. It is used to define part of a component's state. Why do I say part of? Because you can (and _should_) use `useState` multiple times in a single component. This allows you to have more control over the state and, when used in conjunction with the next hook (spoiler), will allow you to rerun code or rerender of your app on a specific state change.\n\nLet's take the last bit of code and continue from there:\n\n```js\nfunction TodoList() {\n const [todos] = useState([]);\n return (\n \u003Cul>\n {todos.map(e => \u003Cli>{e}\u003C/li>)}\n \u003C/ul>\n );\n}\n```\n\nAt this moment, the component renders a list of _todos_, but you might notice a problem; we have no way to **set** those _todos_.\n\nTo do that, we need to understand the return values of `useState`: this function returns an array containing the state's value and a function to update said values. We can call it (and the value, for that matter) whatever we want, but let's stick to the conventions for now.\nLet's update the code we've written before:\n\n```js\nfunction TodoList() {\n const [todos, setTodos] = useState([]);\n return (\n \u003Cul>\n {todos.map(e => \u003Cli>{e}\u003C/li>)}\n \u003C/ul>\n );\n}\n```\n\nNow we have a function to set our todos. Let's add an input and a button to add todos. In the input, we'll listen to the `onChange` event to modify some other piece of state.\n\n```js\nfunction TodoList() {\n const [todos, setTodos] = useState([]);\n const [newTodo, setNewTodo] = useState(\"\");\n return (\n \u003Cdiv>\n \u003Cul>\n {todos.map(e => \u003Cli>{e}\u003C/li>)}\n \u003C/ul>\n \u003Cinput onChange={(e) => { setNewTodo(e.target.value); }} value={newTodo} />\n \u003Cbutton onClick={() => {\n setTodos(oldTodos => [...oldTodos, newTodo]);\n setNewTodo(\"\");\n }}\n >\n Add Todo\n \u003C/button>\n \u003C/div>\n );\n}\n```\n\n\n\nYeah, that's a lot of new stuff to cover, I know. Let's break it down\n\n```js\nconst [newTodo, setNewTodo] = useState(\"\");\n```\n\nThis is just a new useState. Notice how this time I put **\"\"** as a first argument. The first argument in a useState function call defines the starting value of the state and is also used to determine its _type_. This can be used by us (the developers) to get IntelliSense on the state (autocompletion etc).\n\n```js\n\u003Cinput onChange={(e) => { setNewTodo(e.target.value); }} value={newTodo} />;\n```\n\nThis input does two things: it reads the state _newTodo_ and uses it as value while also updating it with its new value (when a user types in the input, the state will change to reflect that change).\n\n```js\n\u003Cbutton onClick={() => {\n setTodos(oldTodos => [...oldTodos, newTodo]);\n setNewTodo(\"\");\n}}\n>\n Add Todo\n\u003C/button>;\n```\n\nThis button add the _newTodo_ to the list and resets the input's text (i.e. _newTodo_). You might have noticed something...\n\n```js\nsetTodos(oldTodos => [...oldTodos, newTodo]);\nsetNewTodo(\"\");\n```\n\n\n\nWhen you _set_ a state, you can either directly pass the new value as an argument, or use an arrow function to get a reference to the current state's value (useful for updating arrays).\n\nAnd that's it for the useState hook !!\n\n## useEffect\n\nNow that we know how to set state in a component, let's see how to replace a `componentDidUpdate` using hooks. the `componentDidUpdate` method of a class component was used to trigger effects based on state and props change.\n\nLet's say we're building a photo search app and we need to update the search results based on the current value of an input field. As the user types in the input field, we want to update the result. We could do it in the `onChange` callback, but that would make the rerender dependent on the onChange and the app would become laggy (at least it used to, don't quote me on that).\n\nLet's setup our basic component:\n\n```js\nfunction PhotoSearch() {\n const [searchValue, setSearchValue] = useState(\"\");\n const [photoResult, setPhotoResult] = useState([]);\n\n return (\n \u003Cdiv>\n \u003Cinput\n value={searchValue}\n onChange={(e) => { setSearchValue(e.target.value); }}\n />\n \u003Cdiv>\n {photoResult.map(e => (\n \u003Cimg src={e} />\n ))}\n \u003C/div>\n \u003C/div>\n );\n}\n```\n\nSo, nothing new for now; we have to states, searchValue and photoResult, the former to store the input's value and the second to store the photos matching that search. Now let's use the `useEffect` hook to update the photoResult array with our search results (here I'm using a basic example, a real API would have more complex data formats (like [unsplash](https://unsplash.com/developers))\n\n```js\nfunction PhotoSearch() {\n const [searchValue, setSearchValue] = useState(\"\");\n const [photoResult, setPhotoResult] = useState([]);\n\n useEffect(() => {\n if (searchValue) {\n fetchImages(searchValue).then((data) => {\n setPhotoResult(data);\n });\n }\n }, [searchValue]);\n\n return (\n \u003Cdiv>\n \u003Cinput\n value={searchValue}\n onChange={(e) => { setSearchValue(e.target.value); }}\n />\n \u003Cdiv>\n {photoResult.map(e => (\n \u003Cimg src={e} />\n ))}\n \u003C/div>\n \u003C/div>\n );\n}\n```\n\n_NB: fetchImages acts as a function that returns an array of links to images from an API_\n\nLet's break it down:\n\n`useEffect` takes two arguments (the second being optional): the former is a callback function and the latter is a dependency array.\nThe callback function gets executed every time the values inside the dependency array change. In our example, we want to rerun that function each time the `searchValue` changes, so we add `searchValue` to the dependency array. Notice also the condition inside the callback:\n\n```js\nif (searchValue)\n```\n\nSince the value could be empty after a user deletes its input or when the page first loads, we need to check before executing a code that depends on it.\n\nThere are other quirks with useEffect, and you can read more about them on the [official React documentation](https://reactjs.org/docs/hooks-effect.html).\n\n## Final words\n\nThese are just the two hooks you'll use the most. Others exist (useContext, useCallback, useMemo), and you can even make your own. We'll probably look at those another time, on another article, but let's stop here for the time being.\n",{"title":9506,"description":11250},"React for Beginners","hook-line-sinker","articles/hook-line-sinker","A practical introduction to React hooks",[269,3760,9502,9501],"5bOOJb2TU4RjTVAvci1_tNI1799H9TNJVr1nwSZNSUs",{"id":11264,"title":11265,"body":11266,"description":11270,"extension":256,"meta":11712,"navigation":259,"path":9514,"publishDate":11713,"rawbody":11714,"seo":11715,"series":11257,"slug":11716,"stem":11717,"summary":11718,"tags":11719,"__hash__":11720},"articles/articles/react-for-beginners.md","React for Beginners: Build a To-Do List",{"type":9,"value":11267,"toc":11698},[11268,11271,11275,11277,11286,11290,11319,11326,11329,11333,11340,11343,11364,11368,11371,11376,11380,11383,11387,11393,11397,11400,11403,11435,11438,11441,11448,11452,11458,11464,11474,11480,11484,11487,11493,11500,11510,11513,11519,11522,11528,11532,11544,11550,11557,11564,11567,11573,11576,11581,11588,11592,11603,11652,11658,11661,11664,11670,11677,11683,11690,11692,11695],[17,11269,11270],{},"So...\nYou heard of this framework called React and want to know what all the fuss is about ? Well, wander no more ! Let's go for a spin with ReactJS and build a simple application that will let us see why everyone is talking about it.",[12,11272,11274],{"id":11273},"installing-react","Installing React",[105,11276,5404],{"id":5403},[17,11278,11279,11280,11285],{},"Before installing React, make sure you have a correct development environment. I'm not talking about your IDE (Integrated Development Environment) - though I personally prefer Visual Studio Code, but whether or not you have NodeJS installed on your machine. If you do, that's great 👍! If you don't, I'd suggest using ",[34,11281,11284],{"href":11282,"rel":11283},"https://github.com/nvm-sh/nvm",[38],"nvm"," to install the latest LTS (long term support) version; you shouldn't need anything else for this guide.",[12,11287,11289],{"id":11288},"creating-a-project","Creating a Project",[17,11291,11292,11293,11296,11297,11300,11301,11304,11305,11308,11309,11312,11313,11315,11316,11318],{},"Now that we have a ",[230,11294,11295],{},"recent"," version on NodeJS, and consequently npm, we can start bootstrapping our new React project. After you get to know better the framework you can have your own starter template, but for today's guide let's just use ",[171,11298,11299],{},"create-react-app",". To start a new react project, just open up a terminal and type: ",[171,11302,11303],{},"npx create-react-app \u003CfolderName>"," where ",[171,11306,11307],{},"\u003CfolderName>"," is the name of the folder that will be created with your react project in it.\n",[230,11310,11311],{},"Here's a trick I found recently"," : If you insert ",[171,11314,356],{}," instead of a folder name (",[171,11317,356],{}," is the current directory), then the project will be instantiated in the current directory, without creating an addition subfolder.",[17,11320,11321,11322,11325],{},"Once that is done, you can move to that folder and run ",[171,11323,11324],{},"npm start"," to launch the development server. It should open a browser window to localhost:3000 and you should see the starter page.",[17,11327,11328],{},"Congrats ! You have created your first React project",[12,11330,11332],{"id":11331},"folder-structure","Folder Structure",[17,11334,11335,11339],{},[130,11336],{"alt":11337,"src":11338},"FolderStructure.PNG","https://cdn.hashnode.com/res/hashnode/image/upload/v1597411603872/6l9tHWSz8.png","\nThis is the usual folder structure generated by create-react-app",[17,11341,11342],{},"Let's now take a look at some particular files:",[21,11344,11345,11348,11358],{},[24,11346,11347],{},"src/App.js : this is the root component all your application will be rendered inside of. If you look at the code inside it, you will see that it's what you've seen when you launched your application",[24,11349,11350,11351,11354,11355,11357],{},"src/index.js : this is the file that actually renders the ",[171,11352,11353],{},"App"," component onto the page by attaching it to a ",[171,11356,10176],{}," with an id of \"root\".",[24,11359,11360,11361,11363],{},"public/index.html : the scheleton of the page your React component will live in: in here you can see the ",[171,11362,10176],{}," I mentioned above",[105,11365,11367],{"id":11366},"to-do","To Do",[17,11369,11370],{},"Now that we have had a look at how a React project is structured, we can start creating our To Do app.",[17,11372,11373],{},[230,11374,11375],{},"NB: for the sake of brevity, we will have everything happen inside one component. Perhaps in a following article we will advance this project to make it resemble more a real life use case",[5406,11377,11379],{"id":11378},"task-list","Task list",[17,11381,11382],{},"The first step is to create a list of tasks. This list will be updated every time a new items gets added or an item is removed. It will composed of a simple unordered list, with each list item corresponding to a task.",[12,11384,11386],{"id":11385},"creating-a-component","Creating a component",[17,11388,11389,11390,356],{},"For this guide, we will be using functional components and the latest addition to the React arsenal, hooks. To create a functional component you must first import React at the top of the file and create a function that returns some ",[171,11391,11392],{},"JSX",[105,11394,11396],{"id":11395},"what-is-jsx","What is JSX ?",[17,11398,11399],{},"From React's official documentation",[17,11401,11402],{},"Consider this variable declaration:",[313,11404,11406],{"className":1387,"code":11405,"language":1389,"meta":240,"style":240},"const element = \u003Ch1>Hello, world!\u003C/h1>;\n",[171,11407,11408],{"__ignoreMap":240},[321,11409,11410,11412,11415,11417,11419,11422,11424,11427,11429,11431,11433],{"class":323,"line":324},[321,11411,4209],{"class":344},[321,11413,11414],{"class":348}," element ",[321,11416,818],{"class":384},[321,11418,9262],{"class":384},[321,11420,11421],{"class":1295},"h1",[321,11423,3896],{"class":384},[321,11425,11426],{"class":348},"Hello, world!",[321,11428,4105],{"class":384},[321,11430,11421],{"class":1295},[321,11432,3896],{"class":384},[321,11434,1404],{"class":355},[17,11436,11437],{},"This funny tag syntax is neither a string nor HTML.",[17,11439,11440],{},"It is called JSX, and it is a syntax extension to JavaScript. We recommend using it with React to describe what the UI should look like. JSX may remind you of a template language, but it comes with the full power of JavaScript.",[17,11442,11443,11444],{},"You can learn more ",[34,11445,39],{"href":11446,"rel":11447},"https://reactjs.org/docs/introducing-jsx.html",[38],[105,11449,11451],{"id":11450},"lets-get-back-to-the-task-at-hand","Let's get back to the 'task' 🤣 at hand",[17,11453,11454,11455,11457],{},"Let's remove all the code inside the return of the ",[171,11456,11353],{}," component. It should now look like this:",[17,11459,11460],{},[130,11461],{"alt":11462,"src":11463},"empty app component","https://cdn.hashnode.com/res/hashnode/image/upload/v1597414689914/XlMMnaVw0.png",[17,11465,11466,11467,11469,11470,11473],{},"let's begin by adding a ",[171,11468,10176],{}," as the root of our component: ",[230,11471,11472],{},"All React element must have only one root element"," and inside of it let's add our unordered list",[17,11475,11476],{},[130,11477],{"alt":11478,"src":11479},"app with div and ul tag","https://cdn.hashnode.com/res/hashnode/image/upload/v1597414589017/FrdzYdWpa.png",[5406,11481,11483],{"id":11482},"lets-display-some-taks","Let's display some taks",[17,11485,11486],{},"Now that we have a list element, we need to add elements inside of it. We could add them one by one, but that is not the point of using React. Let's instead declare an array with some data in it:",[17,11488,11489],{},[130,11490],{"alt":11491,"src":11492},"ul with data no state","https://cdn.hashnode.com/res/hashnode/image/upload/v1597414874885/EJyk2JrSJ.png",[17,11494,11495,11496,11499],{},"Now that we have some data, all that is left is to display them to the user. Let me introduce you to one of your Javascript bestfriends: ",[230,11497,11498],{},"the map function",". It allows us to iterate over an array and return a value for each element (keeping it brief, of course). So, can you guess what we're going to use it for ?",[17,11501,11502,11506,11507,11509],{},[130,11503],{"alt":11504,"src":11505},"right you are","https://media.giphy.com/media/U56VoSyFD8MFcie2k8/giphy.gif"," We'll use ",[171,11508,9641],{}," to display our list items !",[17,11511,11512],{},"so why don't we do just that ?",[17,11514,11515],{},[130,11516],{"alt":11517,"src":11518},"app with items no state","https://cdn.hashnode.com/res/hashnode/image/upload/v1597421004860/ouR1gXFSE.png",[17,11520,11521],{},"If everything went well, then you should see 2 elements appear on your page.",[17,11523,11524],{},[130,11525],{"alt":11526,"src":11527},"congrats","https://media.giphy.com/media/XreQmk7ETCak0/giphy.gif",[5406,11529,11531],{"id":11530},"adding-a-task","Adding a task",[17,11533,11534,11535,11538,11539,9132,11541,11543],{},"In order to add a task, we need to introduce a ",[171,11536,11537],{},"Form"," tag. Within the form we'll have an ",[171,11540,10239],{},[171,11542,10292],{},". Clicking the button will submit the form.",[17,11545,11546],{},[130,11547],{"alt":11548,"src":11549},"adding form input","https://cdn.hashnode.com/res/hashnode/image/upload/v1597423178884/eKDC5Bv5N.png",[17,11551,11552,11553,11556],{},"If you try typing in something and then submitting the form you'll notice something strange... The page reloads! This is because the ",[171,11554,11555],{},"form"," is acting like a form, in that it sends a post request while reloading the page. But we don't want that, do we?",[17,11558,11559,11560,11563],{},"What we need to do is provide a substitute function that will run when the form gets submitted. We can do this with the ",[171,11561,11562],{},"onSubmit"," prop (think of props as passing values to another component from their parent).",[17,11565,11566],{},"The first thing we want to do is prevent the form from actually submitting (i.e. reloading the page). For that, we need to prevent the default behaviour associated with the event originating from the form. If this last sentence seems a bit complicated, it's not; we're simply doing this:",[17,11568,11569],{},[130,11570],{"alt":11571,"src":11572},"onSubmit preventDefault","https://cdn.hashnode.com/res/hashnode/image/upload/v1597490032250/qq8EIdkqv.png",[17,11574,11575],{},"If you try this out, you'll see that the form doesn't reload the page!",[11577,11578,11580],"h5",{"id":11579},"getting-the-tasks-name","Getting the task's name",[17,11582,11583,11584,11587],{},"Now we need to get the input's value in order to add it to the list. And it's in this section that you'll get to meet the ",[230,11585,11586],{},"one"," thing that I love about React: state!",[11577,11589,11591],{"id":11590},"react-state","React State",[17,11593,11594,11595,11599,11600,11602],{},"React state is a collection of variables that you define on a component basis, that forces the component showing them to the screen to re-render itself. It means that for example we can dynamically add rows to a table and it will update automatically (without us needing to manipulate the dom direclty). In order to do so, we'll also get to know a \"new\" concept in React, called hooks (more info ",[34,11596,39],{"href":11597,"rel":11598},"https://reactjs.org/docs/hooks-intro.html",[38],"). In this paricular guide, we'll only use the ",[171,11601,9847],{}," hook, which allows us to define a variable and a function to update said variable. The syntax is as follows:",[313,11604,11606],{"className":1387,"code":11605,"language":1389,"meta":240,"style":240},"const [value, setValue] = useState();\n\nsetValue(3); // updates the variable value\n",[171,11607,11608,11631,11635],{"__ignoreMap":240},[321,11609,11610,11612,11614,11616,11618,11621,11623,11625,11627,11629],{"class":323,"line":324},[321,11611,4209],{"class":344},[321,11613,6196],{"class":355},[321,11615,4100],{"class":348},[321,11617,407],{"class":355},[321,11619,11620],{"class":348}," setValue",[321,11622,1290],{"class":355},[321,11624,2573],{"class":384},[321,11626,9737],{"class":373},[321,11628,1455],{"class":348},[321,11630,1404],{"class":355},[321,11632,11633],{"class":323,"line":241},[321,11634,333],{"emptyLinePlaceholder":259},[321,11636,11637,11640,11642,11645,11647,11649],{"class":323,"line":248},[321,11638,11639],{"class":373},"setValue",[321,11641,377],{"class":348},[321,11643,11644],{"class":2597},"3",[321,11646,238],{"class":348},[321,11648,3861],{"class":355},[321,11650,11651],{"class":327}," // updates the variable value\n",[17,11653,11654,11655,11657],{},"you can also pass as an argument to ",[171,11656,9847],{}," the default value of the variable",[17,11659,11660],{},"In our case, we need to keep track of the input's value, and then add it to our table. For the task's id, I'll increment the length of task array by 1.",[17,11662,11663],{},"Our component should look like this, now:",[17,11665,11666],{},[130,11667],{"alt":11668,"src":11669},"App with onChange input","https://cdn.hashnode.com/res/hashnode/image/upload/v1597502204110/NI61Yls6T.png",[17,11671,11672,11673,11676],{},"All that is left is add the ",[171,11674,11675],{},"taskValue"," variable to our tasks array when the form is submitted:",[17,11678,11679],{},[130,11680],{"alt":11681,"src":11682},"image.png","https://cdn.hashnode.com/res/hashnode/image/upload/v1597502349666/Mj6V6f1TE.png",[17,11684,11685,11686,11689],{},"If you try and run this code, you'll notice that it doesn't really update the table the user sees. Can you guess why ? (hint: the table doesn't really ",[189,11687,11688],{},"react"," to changes)",[105,11691,3728],{"id":3727},[17,11693,11694],{},"Thanks for reading this article! If you have any questions, feel free to ask them in the comments below.",[1167,11696,11697],{},"html pre.shiki code .sIF4r, html code.shiki .sIF4r{--shiki-default:#C6A0F6}html pre.shiki code .sFaBz, html code.shiki .sFaBz{--shiki-default:#CAD3F5}html pre.shiki code .sXptk, html code.shiki .sXptk{--shiki-default:#8BD5CA}html pre.shiki code .s57MT, html code.shiki .s57MT{--shiki-default:#8AADF4}html pre.shiki code .slVFb, html code.shiki .slVFb{--shiki-default:#939AB7}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .siK8c, html code.shiki .siK8c{--shiki-default:#8AADF4;--shiki-default-font-style:italic}html pre.shiki code .s7Qn8, html code.shiki .s7Qn8{--shiki-default:#F5A97F}html pre.shiki code .sfEIy, html code.shiki .sfEIy{--shiki-default:#939AB7;--shiki-default-font-style:italic}",{"title":240,"searchDepth":241,"depth":241,"links":11699},[11700,11703,11704,11707],{"id":11273,"depth":241,"text":11274,"children":11701},[11702],{"id":5403,"depth":248,"text":5404},{"id":11288,"depth":241,"text":11289},{"id":11331,"depth":241,"text":11332,"children":11705},[11706],{"id":11366,"depth":248,"text":11367},{"id":11385,"depth":241,"text":11386,"children":11708},[11709,11710,11711],{"id":11395,"depth":248,"text":11396},{"id":11450,"depth":248,"text":11451},{"id":3727,"depth":248,"text":3728},{"cover_image":258},"2020-08-15T00:00:00.000Z","---\ntitle: \"React for Beginners: Build a To-Do List\"\npublishDate: 2020-08-15\ncover_image: ./images/og.png\nsummary: \"as with every web framework, the first step to mastery is a todo list\"\nslug: react-for-beginners\ntags:\n- tutorial\n- introduction\n- javascript\n- reactjs\n- beginners\n- programming\nseries: \"React for Beginners\"\n---\n\nSo...\nYou heard of this framework called React and want to know what all the fuss is about ? Well, wander no more ! Let's go for a spin with ReactJS and build a simple application that will let us see why everyone is talking about it.\n\n## Installing React\n\n### Prerequisites\n\nBefore installing React, make sure you have a correct development environment. I'm not talking about your IDE (Integrated Development Environment) - though I personally prefer Visual Studio Code, but whether or not you have NodeJS installed on your machine. If you do, that's great 👍! If you don't, I'd suggest using [nvm](https://github.com/nvm-sh/nvm) to install the latest LTS (long term support) version; you shouldn't need anything else for this guide.\n\n## Creating a Project\n\nNow that we have a **recent** version on NodeJS, and consequently npm, we can start bootstrapping our new React project. After you get to know better the framework you can have your own starter template, but for today's guide let's just use `create-react-app`. To start a new react project, just open up a terminal and type: `npx create-react-app \u003CfolderName>` where `\u003CfolderName>` is the name of the folder that will be created with your react project in it.\n**Here's a trick I found recently** : If you insert `.` instead of a folder name (`.` is the current directory), then the project will be instantiated in the current directory, without creating an addition subfolder.\n\nOnce that is done, you can move to that folder and run `npm start` to launch the development server. It should open a browser window to localhost:3000 and you should see the starter page.\n\nCongrats ! You have created your first React project\n\n## Folder Structure\n\n\nThis is the usual folder structure generated by create-react-app\n\nLet's now take a look at some particular files:\n\n- src/App.js : this is the root component all your application will be rendered inside of. If you look at the code inside it, you will see that it's what you've seen when you launched your application\n- src/index.js : this is the file that actually renders the `App` component onto the page by attaching it to a `div` with an id of \"root\".\n- public/index.html : the scheleton of the page your React component will live in: in here you can see the `div` I mentioned above\n\n### To Do\n\nNow that we have had a look at how a React project is structured, we can start creating our To Do app.\n\n**NB: for the sake of brevity, we will have everything happen inside one component. Perhaps in a following article we will advance this project to make it resemble more a real life use case**\n\n#### Task list\n\nThe first step is to create a list of tasks. This list will be updated every time a new items gets added or an item is removed. It will composed of a simple unordered list, with each list item corresponding to a task.\n\n## Creating a component\n\nFor this guide, we will be using functional components and the latest addition to the React arsenal, hooks. To create a functional component you must first import React at the top of the file and create a function that returns some `JSX`.\n\n### What is JSX ?\n\nFrom React's official documentation\n\nConsider this variable declaration:\n\n```js\nconst element = \u003Ch1>Hello, world!\u003C/h1>;\n```\n\nThis funny tag syntax is neither a string nor HTML.\n\nIt is called JSX, and it is a syntax extension to JavaScript. We recommend using it with React to describe what the UI should look like. JSX may remind you of a template language, but it comes with the full power of JavaScript.\n\nYou can learn more [here](https://reactjs.org/docs/introducing-jsx.html)\n\n### Let's get back to the 'task' 🤣 at hand\n\nLet's remove all the code inside the return of the `App` component. It should now look like this:\n\n\n\nlet's begin by adding a `div` as the root of our component: **All React element must have only one root element** and inside of it let's add our unordered list\n\n\n\n#### Let's display some taks\n\nNow that we have a list element, we need to add elements inside of it. We could add them one by one, but that is not the point of using React. Let's instead declare an array with some data in it:\n\n\n\nNow that we have some data, all that is left is to display them to the user. Let me introduce you to one of your Javascript bestfriends: **the map function**. It allows us to iterate over an array and return a value for each element (keeping it brief, of course). So, can you guess what we're going to use it for ?\n\n We'll use `map` to display our list items !\n\nso why don't we do just that ?\n\n\n\nIf everything went well, then you should see 2 elements appear on your page.\n\n\n\n#### Adding a task\n\nIn order to add a task, we need to introduce a `Form` tag. Within the form we'll have an `input` and a `button`. Clicking the button will submit the form.\n\n\n\nIf you try typing in something and then submitting the form you'll notice something strange... The page reloads! This is because the `form` is acting like a form, in that it sends a post request while reloading the page. But we don't want that, do we?\n\nWhat we need to do is provide a substitute function that will run when the form gets submitted. We can do this with the `onSubmit` prop (think of props as passing values to another component from their parent).\n\nThe first thing we want to do is prevent the form from actually submitting (i.e. reloading the page). For that, we need to prevent the default behaviour associated with the event originating from the form. If this last sentence seems a bit complicated, it's not; we're simply doing this:\n\n\n\nIf you try this out, you'll see that the form doesn't reload the page!\n\n##### Getting the task's name\n\nNow we need to get the input's value in order to add it to the list. And it's in this section that you'll get to meet the **one** thing that I love about React: state!\n\n##### React State\n\nReact state is a collection of variables that you define on a component basis, that forces the component showing them to the screen to re-render itself. It means that for example we can dynamically add rows to a table and it will update automatically (without us needing to manipulate the dom direclty). In order to do so, we'll also get to know a \"new\" concept in React, called hooks (more info [here](https://reactjs.org/docs/hooks-intro.html)). In this paricular guide, we'll only use the `useState` hook, which allows us to define a variable and a function to update said variable. The syntax is as follows:\n\n```js\nconst [value, setValue] = useState();\n\nsetValue(3); // updates the variable value\n```\n\nyou can also pass as an argument to `useState` the default value of the variable\n\nIn our case, we need to keep track of the input's value, and then add it to our table. For the task's id, I'll increment the length of task array by 1.\n\nOur component should look like this, now:\n\n\n\nAll that is left is add the `taskValue` variable to our tasks array when the form is submitted:\n\n\n\nIf you try and run this code, you'll notice that it doesn't really update the table the user sees. Can you guess why ? (hint: the table doesn't really _react_ to changes)\n\n### Conclusion\n\nThanks for reading this article! If you have any questions, feel free to ask them in the comments below.\n",{"title":11265,"description":11270},"react-for-beginners","articles/react-for-beginners","as with every web framework, the first step to mastery is a todo list",[3762,6028,269,3760,9502,9501],"fm2DAFao-H2bCq-qAmasY6hrq9NcW40jAUBbbgQAfSc",["Reactive",11722],{"$scolor-mode":11723,"$snuxt-seo-utils:routeRules":11726,"$ssite-config":11727},{"preference":11724,"value":11724,"unknown":259,"forced":11725},"system",false,{"head":-1,"seoMeta":-1},{"_priority":11728,"env":11732,"name":11733,"url":11734},{"name":11729,"env":11730,"url":11731},-5,-15,-4,"production","matteogassend.com","https://www.matteogassend.com/",["Set"],["ShallowReactive",11737],{"articles-index":-1},"/articles"]