[{"data":1,"prerenderedAt":10709},["ShallowReactive",2],{"all-blog-posts":3,"blog-build-personal-tools-solve-one-problem":10455},[4,120,172,453,592,972,1857,2245,2424,2522,2933,3308,3799,4098,4267,4510,5001,5187,5311,5813,5839,6769,6907,7136,7459,7994,8528,8735,9093,9319,9716,9916],{"id":5,"title":6,"author":7,"authorTitle":8,"body":9,"category":104,"createdAt":105,"description":106,"extension":107,"image":108,"meta":109,"navigation":110,"path":111,"proficiency":112,"published":110,"readingTime":113,"seo":114,"stem":115,"tags":116,"updatedAt":118,"__hash__":119},"blog/blog/adding-security-headers.md","How To Use Security Headers","Sachin Ghait","Lead Developer",{"type":10,"value":11,"toc":98},"minimark",[12,28,32,35,38,41,50,53,57,60,70,84,88,91],[13,14,15],"blockquote",{},[16,17,18,22,23,27],"p",{},[19,20,21],"strong",{},"TL;DR:"," HTTP security headers are a simple but effective way to harden your website against common attacks like XSS, clickjacking, and code injection. This post explains the key headers -- X-Frame-Options, X-XSS-Protection, Content-Security-Policy, Referrer-Policy, and more -- with a practical example of configuring them in a Netlify ",[24,25,26],"code",{},"_headers"," file. You can check your current score at securityheaders.com and aim for an A rating.",[29,30,6],"h2",{"id":31},"how-to-use-security-headers",[16,33,34],{},"With HTTP response headers, you can harden your website security and also prevent/mitigate attacks",[16,36,37],{},"Thanks to HTTP security headers, it is possible to be a few steps ahead, ensuring the security of our sites, our users and our data",[16,39,40],{},"HTTP security headers protect you against the types of attacks that your site is most likely to come across. These headers protect against XSS, code injection, clickjacking, etc.",[16,42,43,44],{},"You can check the score for your websites headers on below site. ",[45,46,47],"a",{"href":47,"rel":48},"https://securityheaders.com/",[49],"nofollow",[16,51,52],{},"If you have A in the score, then you are doing good.\nFor other scores securityheaders will suggest what things can be added.",[29,54,56],{"id":55},"example","Example",[16,58,59],{},"For hosting my site on netlify I have added headers as below",[61,62,67],"pre",{"className":63,"code":65,"language":66},[64],"language-text","[[headers]]\n  for = \"/*\"\n\n  [headers.values]\n    X-Frame-Options = \"DENY\"\n    X-XSS-Protection = \"1; mode=block\"\n    Content-Security-Policy = \"script-src 'self'\"\n    Referrer-Policy = \"same-origin\"\n    Permissions-Policy = \"fullscreen=(), geolocation=()\"\n    X-Content-Type-Options = \"nosniff\"\n\n    cache-control = '''\n    max-age=0,\n    no-cache,\n    no-store,\n    must-revalidate'''\n","text",[24,68,65],{"__ignoreMap":69},"",[16,71,72,73,76,77,80,81],{},"Some important headers to set are ",[24,74,75],{},"Content-Security-Policy, Permissions-Policy, Referrer-Policy, Strict-Transport-Security","\nMore explanation can ",[24,78,79],{},"Additional Information"," section at ",[45,82,47],{"href":47,"rel":83},[49],[29,85,87],{"id":86},"result","Result",[16,89,90],{},"This is the score after adding all required security headers",[16,92,93],{},[94,95],"img",{"alt":96,"src":97},"image alt text","/assets/securityHeaders.webp",{"title":69,"searchDepth":99,"depth":99,"links":100},2,[101,102,103],{"id":31,"depth":99,"text":6},{"id":55,"depth":99,"text":56},{"id":86,"depth":99,"text":87},"Frontend","2021-02-02T07:00:13.392Z","Learn how to improve your website security with HTTP security headers. Discover how to get an A score for your website with the help of this beginner-friendly guide.","md","/assets/http-security-headers.webp",{},true,"/blog/adding-security-headers","Beginner","5 min read",{"title":6,"description":106},"blog/adding-security-headers",[117],"developer",null,"ATaprwuA9E8MMstOgi6Qikp1cMb5R0nE3ZMJwhkdSvk",{"id":121,"title":122,"author":7,"authorTitle":8,"body":123,"category":161,"createdAt":162,"description":163,"extension":107,"image":164,"meta":165,"navigation":110,"path":166,"proficiency":112,"published":167,"readingTime":113,"seo":168,"stem":169,"tags":170,"updatedAt":118,"__hash__":171},"blog/blog/api-tasks-automation.md","Automate Rest API tasks using python requests module.",{"type":10,"value":124,"toc":159},[125,136,140,143,146,149],[13,126,127],{},[16,128,129,131,132,135],{},[19,130,21],{}," Most websites expose REST APIs that power their UI -- but these same APIs can be called directly from Python scripts to automate tasks. Using the ",[24,133,134],{},"requests"," module, you can build scripts that monitor bitcoin/product prices, fetch images from Nat Geo's photo of the day, and even set them as your desktop background. This post introduces the concept and walks through practical automation examples.",[137,138,122],"h1",{"id":139},"automate-rest-api-tasks-using-python-requests-module",[16,141,142],{},"Many websites use rest APIs to interact with their backend, (Others may use sockets or GRPC or something else). UI is a simple way to interact with Rest APIs ( simple to interact for a normal user ).",[16,144,145],{},"There are other ways these APIs can be used. They can be invoked directly from python(or any other language) scripts.",[16,147,148],{},"An API can be used to fetch some data or trigger some operation at server. I am listing down some examples, where I have used Rest APIs to automate some tasks.",[150,151,152,156],"ol",{},[153,154,155],"li",{},"Monitor bitcoin price or price of any item on shopping website.",[153,157,158],{},"Fetch images from nat geo photo of the day page and set it as desktop background.",{"title":69,"searchDepth":99,"depth":99,"links":160},[],"Developer","2021-07-12T07:00:13.392Z","Learn how to automate Rest API tasks using python's requests module. Discover the power of using APIs to interact with websites and the endless possibilities it offers. From monitoring prices to setting desktop backgrounds","/assets/python-requests.webp",{},"/blog/api-tasks-automation",false,{"title":122,"description":163},"blog/api-tasks-automation",[117],"MwgOvBM6Gic09GrXAFd7Cz6J0HYDB-zHJo0WdPx2VsM",{"id":173,"title":174,"author":7,"authorTitle":8,"body":175,"category":161,"createdAt":443,"description":444,"extension":107,"image":445,"meta":446,"navigation":110,"path":447,"proficiency":448,"published":110,"readingTime":113,"seo":449,"stem":450,"tags":451,"updatedAt":118,"__hash__":452},"blog/blog/block-ads-on-whole-network.md","Ad Blocker for Your Whole Network.",{"type":10,"value":176,"toc":434},[177,184,188,191,194,198,201,204,207,210,214,217,220,237,240,244,253,257,262,269,273,276,316,322,325,329,352,356,359,366,370,373,376,382,389,393,401,405,430],[13,178,179],{},[16,180,181,183],{},[19,182,21],{}," Browser ad blockers only protect one device at a time. Pi-hole is a network-level ad blocker that runs on a Raspberry Pi and acts as your DNS server, blocking ad-serving domains before they reach any device on your network. This is especially useful for protecting non-tech-savvy family members (kids, elderly parents) who might click on dangerous ads. The post covers how Pi-hole works, installation on Raspberry Pi, and configuring your router to use it as the DNS server.",[137,185,187],{"id":186},"ad-blocker-for-your-whole-network-️","Ad Blocker for Your Whole Network. 🛡️",[16,189,190],{},"The Online advertising market was valued at USD 304.0 billion in 2019 and is expected to reach USD 982.82 billion by 2025, at a CAGR of 21.6%. With increase in smartphone users these predictions look obvious.",[16,192,193],{},"Sometimes Ads can be a bit invasive and annoying. For mobile devices, there are different types of advertisements, including click to download, click to call, image text, banner ads and full screen ads.",[29,195,197],{"id":196},"why-i-needed-this","Why I needed this ? 🤷",[16,199,200],{},"There are some ads that simply advertise some content or product, which does not bother me. But there is also the other dangerous side to it. These ads show fake things or ask people to download something on their devices.",[16,202,203],{},"When there are non-tech savvy users (children or elderly parents) exposed to the ads, they can easily fall prey to these ads. They are more likely to click on anything.",[16,205,206],{},"These are few things you can do like installing ad-block-plus on browser, or other similar tools. But this handles it for just one device or rather browser. Doing this in all devices is a bit too much.",[16,208,209],{},"Recently I came across pi-hole a network-level advertisement and Internet tracker blocking application. Which makes it very easy to block ads on network-level.",[29,211,213],{"id":212},"how-pi-hole-works","How Pi-hole works ?",[16,215,216],{},"Pi-hole acts as a DNS server in your router network. So whenever there is any query for a domain, pihole checks if it is an ad serving website, and blocks the query if it is.\nIt can also act as a DHCP server.",[16,218,219],{},"Let's take look at an example",[150,221,222,225,228,231,234],{},[153,223,224],{},"open a Web browser.",[153,226,227],{},"type newswebsite.com into the address bar.",[153,229,230],{},"press Enter, query goes to pihole.",[153,232,233],{},"pihole detects the domain serves ads, pihole returns address of actual site, But instead of returning actual address of ad server pihole returns fake address.",[153,235,236],{},"So browser loads original website fully, buts ads are not present on the page.",[16,238,239],{},"NOTE: Since the ads were not downloaded in the first place, they do not need to be hidden from your view since they do not exist in the first place. Hence, no need for ad blocker extension.",[29,241,243],{"id":242},"what-all-is-required","What all is required.",[245,246,247,250],"ul",{},[153,248,249],{},"Raspberry Pi device (I have used Raspberry Pi zero w)",[153,251,252],{},"Access to your router",[29,254,256],{"id":255},"how-to-set-up-pihole-with-raspberry-pi","How to set up pihole with raspberry-pi. 🔨",[258,259,261],"h4",{"id":260},"setup-raspberry-pi","Setup Raspberry Pi",[16,263,264,265],{},"This is very easy setup just write raspbian OS to SD card.\nExplained in detail in this video.\n",[45,266,267],{"href":267,"rel":268},"https://www.youtube.com/watch?v=Hdm26W9dHK0",[49],[258,270,272],{"id":271},"install-pihole-on-raspberry-pi","Install pihole on Raspberry Pi",[16,274,275],{},"Installing pihole is quite easy, just run following command.\nIt will run the program and ask for some inputs like upstream DNS, ad-lists, and other configurations.",[61,277,286],{"className":278,"code":279,"highlights":280,"language":285,"meta":69,"style":69},"language-bash shiki shiki-themes github-light github-dark","$ curl -sSL https://install.pi-hole.net | bash\n",[281,282,283,284],1,3,4,5,"bash",[24,287,288],{"__ignoreMap":69},[289,290,294,298,302,306,309,313],"span",{"class":291,"line":281},[292,293],"line","highlight",[289,295,297],{"class":296},"sScJk","$",[289,299,301],{"class":300},"sZZnC"," curl",[289,303,305],{"class":304},"sj4cs"," -sSL",[289,307,308],{"class":300}," https://install.pi-hole.net",[289,310,312],{"class":311},"szBVR"," |",[289,314,315],{"class":296}," bash\n",[16,317,318],{},[94,319],{"alt":320,"src":321},"image pihole install","/assets/pihole-install-window.webp",[16,323,324],{},"I chose OpenDNS as upstream DNS, I will explain why OpenDNS in next section.",[258,326,328],{"id":327},"make-pihole-your-dns-server","Make Pihole your DNS server",[245,330,331],{},[153,332,333,336],{},[19,334,335],{},"You can do this in two ways",[150,337,338,345],{},[153,339,340,341],{},"Update DNS settings in your router with your raspberry-pi local address.\n",[94,342],{"alt":343,"src":344},"image pihole dns","/assets/router-dns-settings-pihole.webp",[153,346,347,348],{},"Disable DHCP on your router and enable DHCP in raspberry-pi, This way you get more control with pi-hile.\n",[94,349],{"alt":350,"src":351},"image pihole dhcp","/assets/pihole-dhcp.webp",[258,353,355],{"id":354},"bonus-create-opendns-account-and-set-level-of-web-content-filtering","BONUS: Create openDNS account and set level of web content filtering.",[16,357,358],{},"This is bonus thing along with blocking ads, As in previous steps we have set upstream DNS as OpenDNS. With OpenDNS we get some more features,\nLike blocking certain type of content on your network.",[16,360,361,362],{},"Categories are as below\n",[94,363],{"alt":364,"src":365},"image opendns webfiltering","/assets/opendns-wen-content-filter.webp",[29,367,369],{"id":368},"conclusion-️-what-i-observed-after-2-weeks-of-use","Conclusion ✔️ - What I observed after 2 weeks of use.",[16,371,372],{},"Pihole is working seamlessly, No issues so far. It does not require much compute, works quite fine with 15% memory usage. Also, As I am using raspberry-pi zero w, It's not consuming much power.",[16,374,375],{},"On average 30% - 45% daily unique requested domains are ads or tracking. Knowing that other devices in my network are not served any ads or not tracked, gives a relaxing feeling.",[16,377,378],{},[94,379],{"alt":380,"src":381},"image pihole stats","/assets/pihole-stats-daily.webp",[16,383,384,385],{},"And this is how you say no to Ads,\n",[94,386],{"alt":387,"src":388},"image No to ads","https://media1.giphy.com/media/l4FGIgsVPdoRd2wbS/giphy.gif?cid=790b7611da37642de1a3e196dd373a47a5aa2632e723bb14&rid=giphy.gif&ct=g",[29,390,392],{"id":391},"things-to-keep-in-mind","Things to keep in mind 🤨",[150,394,395,398],{},[153,396,397],{},"When you switch off pihole, make sure to revert DNS settings of your router. Otherwise, internet will not work on devices.",[153,399,400],{},"As with every software, pihole is not 100% accurate. Pihole only blocks ads which are in its adlist. So sometimes you will have to manually allow/disallow some domains",[29,402,404],{"id":403},"references-️","References 🖊️",[245,406,407,413,419,425],{},[153,408,409],{},[45,410,411],{"href":411,"rel":412},"https://pi-hole.net/",[49],[153,414,415],{},[45,416,417],{"href":417,"rel":418},"https://www.opendns.com/",[49],[153,420,421],{},[45,422,423],{"href":423,"rel":424},"https://www.mordorintelligence.com/industry-reports/online-advertising-market",[49],[153,426,427],{},[45,428,267],{"href":267,"rel":429},[49],[431,432,433],"style",{},"html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}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 .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":69,"searchDepth":99,"depth":99,"links":435},[436,437,438,439,440,441,442],{"id":196,"depth":99,"text":197},{"id":212,"depth":99,"text":213},{"id":242,"depth":99,"text":243},{"id":255,"depth":99,"text":256},{"id":368,"depth":99,"text":369},{"id":391,"depth":99,"text":392},{"id":403,"depth":99,"text":404},"2021-09-11T07:00:13.392Z","Stop annoying ads and protect your network with Pi-hole. This guide explains how to install Pi-hole on Raspberry-Pi, how it blocks ads, and how to make Pi-hole your DNS server.","/assets/block-ads.webp",{},"/blog/block-ads-on-whole-network","intermediate",{"title":174,"description":444},"blog/block-ads-on-whole-network",[117],"brl_Pda5mE4dm-aU1P7hbcRa-VsKUKOpxGcbvXI_zOo",{"id":454,"title":455,"author":7,"authorTitle":8,"body":456,"category":104,"createdAt":581,"description":582,"extension":107,"image":583,"meta":584,"navigation":110,"path":585,"proficiency":112,"published":110,"readingTime":586,"seo":587,"stem":588,"tags":589,"updatedAt":118,"__hash__":591},"blog/blog/block-website-crawlers.md","Block Google search bots from indexing your website.",{"type":10,"value":457,"toc":575},[458,469,472,475,478,485,494,497,500,503,506,511,518,524,529,532,543,549,555,562],[13,459,460],{},[16,461,462,464,465,468],{},[19,463,21],{}," Need to remove your website from Google search results? There are two approaches. For a temporary 6-month block, use Google Search Console to submit URL removal requests. For a permanent solution, modify your ",[24,466,467],{},"robots.txt"," file to disallow crawlers from indexing specific pages or the entire site. The post covers both methods, explains the difference between blocking indexing vs blocking crawling, and shows how to target specific bots like Googlebot.",[137,470,455],{"id":471},"block-google-search-bots-from-indexing-your-website",[16,473,474],{},"I recently come across a use case where I had to remove listing a website from google search. This was a rare use case so I looked for the ways we could achieve this.",[16,476,477],{},"There are 2 ways to do this task, listed below",[29,479,481,482],{"id":480},"_1-temporary-block-method","1. ",[19,483,484],{},"Temporary block method.",[16,486,487,488,493],{},"You can use ",[45,489,492],{"href":490,"rel":491},"https://search.google.com/",[49],"google search console"," to temporarily block website from being listed in google search.",[16,495,496],{},"It is a very simple UI that accepts the requests for blocking URL paths from your website.",[16,498,499],{},"You have an option to block a single webpage or whole website.",[16,501,502],{},"Webpage block or website block request lasts only for 6 months, After that your website will appear in google search.",[16,504,505],{},"Blocking a URL does not prevent Google from crawling your page, only from showing it in Search results.",[16,507,508],{},[94,509],{"alt":492,"src":510},"/assets/google-console.webp",[29,512,514,515],{"id":513},"_2-permanent-block-method","2. ",[19,516,517],{},"Permanent block method.",[16,519,520,521,523],{},"You can disallow crawlers to a certain part website or whole website by modifying your ",[24,522,467],{}," file.",[16,525,526,528],{},[24,527,467],{}," file is used to communicate with web crawlers. This file should be kept in root directory. You can configure this file to prevent crawlers from indexing webpages in your site.",[16,530,531],{},"Use cases for blocking crawlers can be,",[245,533,534,537,540],{},[153,535,536],{},"block indexing certain parts of your website that has private info",[153,538,539],{},"block indexing a website which is in maintenance mode",[153,541,542],{},"block indexing web pages intended for internal use or company use",[16,544,545,546,548],{},"To block all crawler bots from indexing all pages in your website, ",[24,547,467],{}," will look like this,",[61,550,553],{"className":551,"code":552,"language":66},[64],"User-agent: *\nDisallow: /\n",[24,554,552],{"__ignoreMap":69},[16,556,557,558,561],{},"In ",[24,559,560],{},"User-agent"," field you can add specific bots like Googlebot, Bingbot, etc.",[16,563,557,564,567,568,571,572],{},[24,565,566],{},"Disallow"," field you can add specific routes from your website like ",[24,569,570],{},"/private/"," or ",[24,573,574],{},"/private/blocked-page.html",{"title":69,"searchDepth":99,"depth":99,"links":576},[577,579],{"id":480,"depth":99,"text":578},"1. Temporary block method.",{"id":513,"depth":99,"text":580},"2. Permanent block method.","2021-08-01T07:00:13.392Z","Prevent your website from appearing in Google search results by temporarily using the Google Search Console or permanently by modifying your robots.txt file. Learn how to block all crawler bots or specific bots and pages in this post.","/assets/block-crawlers.webp",{},"/blog/block-website-crawlers","3 min read",{"title":455,"description":582},"blog/block-website-crawlers",[590],"frontend","v-9vZPZ7fAV8iXJwO5JUJu7d9Qm33aE0uX1P-n3d8bw",{"id":593,"title":594,"author":7,"authorTitle":8,"body":595,"category":161,"createdAt":959,"description":960,"extension":107,"image":961,"meta":962,"navigation":110,"path":963,"proficiency":964,"published":110,"readingTime":965,"seo":966,"stem":967,"tags":968,"updatedAt":118,"__hash__":971},"blog/blog/build-personal-tools-solve-one-problem.md","Use It First. Launch It Later.",{"type":10,"value":596,"toc":949},[597,604,607,610,613,616,619,622,626,629,632,635,646,653,656,659,662,666,673,679,682,685,691,696,699,705,711,715,718,721,724,727,741,744,750,755,758,763,768,772,775,785,788,791,794,805,808,811,817,822,827,832,836,839,865,868,872,875,878,881,884,890,895,899,902,905,908,925,928,932,937,940,943,946],[13,598,599],{},[16,600,601,603],{},[19,602,21],{}," The most useful tools I’ve built aren’t products, they’re small, personal tools I use every week. I built three: a stock exit validator, a media analysis/compression pipeline, and a habit-friction DNS blocker. Some are scripts, one is a simple local frontend, but all of them solve exactly one problem. I used them first, and only expanded them after they proved useful. With AI coding assistants, the cost of doing this has dropped from a weekend to an evening.",[16,605,606],{},"The most useful tools I’ve built aren’t products.",[16,608,609],{},"Some are scripts.\nSome are small local apps.\nNone of them were built to be launched.",[16,611,612],{},"They solve one specific problem, and that’s enough.",[16,614,615],{},"At work, we spend our time building complex systems. We design for scale, handle edge cases, and think in abstractions.",[16,617,618],{},"But outside of that, small inefficiencies quietly pile up. Repeated tasks. Minor annoyances. Things that take a few minutes each time, but add up over weeks.",[16,620,621],{},"The shift for me was simple:\nstop thinking in terms of side projects and start building tools I would actually use.",[29,623,625],{"id":624},"the-rule-use-it-first-expand-later","The Rule: Use It First. Expand Later.",[16,627,628],{},"Recently, I've been building small personal tools. Not side projects, tools. The difference matters.",[16,630,631],{},"A side project is something you want to grow. You add features, plan a roadmap, maybe even dream about launching it. Most side projects die because the ambition outgrows the motivation.",[16,633,634],{},"A personal tool solves one specific friction in your life. You build it, you use it, you're done. No landing page. No roadmap. No users.",[16,636,637,638],{},"The rule I follow: ",[19,639,640,641,645],{},"Zero ",[642,643,644],"em",{},"initial"," scope creep. Use it first, expand later.",[16,647,648,649,652],{},"If V1 takes more than two evenings to build, the scope is too big. Cut features until it fits. A tool starts with one user: you. If it proves useful repeatedly, ",[642,650,651],{},"then"," it earns more time, better UX, more features, maybe even sharing it with others. But the initial barrier to entry must remain microscopic.",[16,654,655],{},"If it works really well for you, consistently, over time, and you find yourself relying on it, that’s a signal.",[16,657,658],{},"At that point, if you feel it could genuinely help others, you can choose to share it or turn it into something more.",[16,660,661],{},"But that decision comes later, after it has proven its value in real use, not just in idea.",[29,663,665],{"id":664},"tool-1-the-exit-validator-execution-over-emotion","Tool 1: The Exit Validator, Execution Over Emotion",[16,667,668,669,672],{},"I invest in stocks. Like most retail investors, I often sell when the market is clearly heading downward. The problem isn't the core decision to sell; the problem is wondering if my ",[642,670,671],{},"execution timing"," was tight or if I left money on the table right before the drop.",[16,674,675,676],{},"I wanted an objective answer to one question: ",[19,677,678],{},"\"Given that I correctly identified a downward trend, was my execution speed correct, or was I late to the party?\"",[16,680,681],{},"So I built an analysis tool. It's not about torturing myself over unpredictable market noise, it's about evaluating my execution speed. I simply upload my trades as a CSV file. The tool runs entirely locally with absolutely no data sent over internet, and all the data is locally encrypted, so I never have to worry about security or privacy issues. It seamlessly compares my exit timing with the subsequent drop.",[16,683,684],{},"No portfolio tracker. No buy recommendations. No charts with 50 indicators. Just one question, answered with data.",[16,686,687],{},[94,688],{"alt":689,"src":690},"screenshot of exit validator showing post-sale price analysis for a stock","https://media4.giphy.com/media/v1.Y2lkPTc5MGI3NjExbnd2aHJscG9iMDl3aWV5ZWFtcml0N3p0ODNqYndlN2E3b2J2eGd4dyZlcD12MV9naWZzX3NlYXJjaCZjdD1n/3og0IOUOHXqXKbH36E/200.webp",[13,692,693],{},[16,694,695],{},"One question: was my execution timing right? Answered with data, not feelings.",[16,697,698],{},"The result was surprisingly useful, I discovered that most of my execution times were fine. The regret was mostly noise. Just seeing the data in a clean format removed the emotional second-guessing.",[16,700,701,704],{},[19,702,703],{},"Build cost:"," One evening talking to AI.",[16,706,707,710],{},[19,708,709],{},"Ongoing value:"," Every time I sell a stock, I check. Takes 10 seconds. Saves hours of emotional second-guessing.",[29,712,714],{"id":713},"tool-2-media-compression-pipeline-dodging-the-cloud-storage-tax","Tool 2: Media Compression Pipeline, Dodging the Cloud Storage Tax",[16,716,717],{},"I back up my photos and videos to a cloud service. Recently, I hit that dreaded notification: I was approaching my storage limit.",[16,719,720],{},"To keep my backups running, the service wanted me to automatically upgrade my subscription to the next tier. It wasn't a massive expense, but I hated the idea of paying a permanent \"storage tax\" month after month just because modern camera phones shoot unnecessarily massive files.",[16,722,723],{},"If I could drastically reduce the size of the files locally without a noticeable drop in quality, I could stay comfortably within my existing, cheaper cloud plan.",[16,725,726],{},"I built a simple shell script that:",[150,728,729,732,735,738],{},[153,730,731],{},"Iterates through my local media folders",[153,733,734],{},"Compresses photos and videos using high-efficiency settings I specifically chose",[153,736,737],{},"Visually verifies the integrity of the newly compressed files",[153,739,740],{},"Purges the massive originals (leaving only the optimized versions to sync to the cloud)",[16,742,743],{},"The result? I shrank my media footprint by over 40% in one go. I didn't upgrade my cloud subscription, and my backups fit perfectly in my existing plan again.",[16,745,746],{},[94,747],{"alt":748,"src":749},"terminal output showing compression pipeline processing folders in parallel","https://media3.giphy.com/media/v1.Y2lkPTc5MGI3NjExMmVieWd4aDhxZDRvdDd4M2t5aWVxNDA2eGdpOWMxZnU2dGF4djc0ZCZlcD12MV9naWZzX3NlYXJjaCZjdD1n/xUPGcq6cAnpcLz0lZm/200.webp",[13,751,752],{},[16,753,754],{},"Shrank my media footprint by over 40%. No permanent cloud storage tax.",[16,756,757],{},"The important thing: I didn't build a \"media management platform.\" There's no UI, no web interface, no database. I run it when storage gets low. Later, because it proved so useful, I added parallel processing to handle multiple folders simultaneously, and expanded it to PNG and WAV files too.",[16,759,760,762],{},[19,761,703],{}," One evening for V1, incremental improvements later.",[16,764,765,767],{},[19,766,709],{}," I run it whenever my cloud storage starts getting tight. It takes a few minutes and permanently saves me from subscription tier upgrades.",[29,769,771],{"id":770},"tool-3-shamirs-secret-sharing-engineering-a-dns-blocker","Tool 3: Shamir's Secret Sharing, Engineering a DNS Blocker",[16,773,774],{},"This one is my favorite because it's the most absurd application of serious cryptography to a silly human problem. It started from a small observation: I would mindlessly open distraction websites not because I needed to, but purely out of habit.",[16,776,777,778,781,782],{},"There's a principle from ",[19,779,780],{},"Atomic Habits"," by James Clear: ",[642,783,784],{},"If you add friction to bad habits, they become less likely.",[16,786,787],{},"I tried standard app blockers and screen time limits, but the workaround was always too easy, just click \"Ignore Limit\" and you're back in. I needed the workaround to be technically difficult. So instead of relying on willpower, I engineered friction using Shamir's Secret Sharing algorithm.",[16,789,790],{},"I blocked my distraction sites at the DNS level using a Pi-hole on my home network. Because it's a network-level block, persistent tokens and cookies don't matter, the connection simply drops. To disable the block, you have to log into the Pi-hole Admin panel.",[16,792,793],{},"So, I took my Pi-hole Admin password, used Shamir's Secret Sharing to split it into 3 shares, and deleted the original password:",[245,795,796,799,802],{},[153,797,798],{},"One share on my phone",[153,800,801],{},"One share on my laptop",[153,803,804],{},"One share in a password manager I deliberately made inconvenient to access",[16,806,807],{},"To log in and disable the Pi-hole block, I need to combine at least 2 shares. This means I can't impulsively unblock sites from my phone in bed, I have to get up, fetch my laptop, run the reconstruction script to decrypt the admin password, log into the Pi-hole, and disable the block.",[16,809,810],{},"By the time I've done all that effort, the impulse is gone.",[16,812,813],{},[94,814],{"alt":815,"src":816},"diagram showing Shamir's Secret Sharing splitting a password into 3 shares","https://media0.giphy.com/media/v1.Y2lkPTc5MGI3NjExNDRzZHRmNmg2cnhiZ2JmejlldWVpaDRwaHl4ZWkzeDdkbXR3cTBjMCZlcD12MV9naWZzX3NlYXJjaCZjdD1n/NdKVEei95yvIY/200.webp",[13,818,819],{},[16,820,821],{},"Cryptography designed for nuclear launch codes, applied to my Pi-hole admin password.",[16,823,824,826],{},[19,825,703],{}," A couple of hours, mostly understanding the cryptography, once I understood the cryptography concept, AI built it in 10 minutes.",[16,828,829,831],{},[19,830,709],{}," Fewer mindless opens, more intentional usage. The friction is enough to break the automatic habit loop.",[29,833,835],{"id":834},"the-pattern","The Pattern",[16,837,838],{},"Look at these three tools. They have nothing in common technically, but they follow the same pattern:",[150,840,841,847,853,859],{},[153,842,843,846],{},[19,844,845],{},"Identify a specific, recurring friction",", not a vague problem, a specific one.",[153,848,849,852],{},[19,850,851],{},"Resist the urge to build a \"full app\"",", no UI unless absolutely necessary, no database unless you need one.",[153,854,855,858],{},[19,856,857],{},"Build the smallest V1 that removes the friction",", use it repeatedly.",[153,860,861,864],{},[19,862,863],{},"Expand only if it proves useful",", if you encounter new friction later, add to it then. Don't build it upfront.",[16,866,867],{},"The anti-pattern is skipping step 3. The moment you jump straight to adding user accounts or thinking about a launch before you've even used it yourself, you've crossed the line from solving your problem to creating a new one.",[29,869,871],{"id":870},"why-this-matters-now-more-than-ever","Why This Matters Now More Than Ever",[16,873,874],{},"The cost of building small tools has dropped significantly.",[16,876,877],{},"Before AI coding assistants, building the Exit Validator would have meant researching APIs, setting up boilerplate, and debugging for a solid 3 hours. Hard to justify for something you use once a month.",[16,879,880],{},"With Claude Code, the same tool took 40 minutes. The AI handled the API research, the boilerplate, and most of the debugging.",[16,882,883],{},"The economic equation for personal tools has completely changed. The real bottleneck isn't effort anymore, it's clarity. Can you define the problem precisely enough, and can you stop once it's solved?",[16,885,886],{},[94,887],{"alt":888,"src":889},"before/after comparison showing time to build a personal tool with and without AI","https://media0.giphy.com/media/v1.Y2lkPTc5MGI3NjExMXlpdG5kdHljcTc1cXN5eWlkM3MyN2JjMHk2c2EzcTZ5aDNucDhqcCZlcD12MV9naWZzX3NlYXJjaCZjdD1n/AYkKBPkrKTOsU2YD3b/200.webp",[13,891,892],{},[16,893,894],{},"The cost of building personal tools dropped from a weekend to an evening",[29,896,898],{"id":897},"the-hidden-benefit-better-product-instincts","The Hidden Benefit: Better Product Instincts",[16,900,901],{},"Building tools for yourself sharpens instincts in a way side projects don't.",[16,903,904],{},"When you build something for yourself, you're the user and the developer. You feel every friction point. You cut unnecessary features faster. You optimize for usefulness, not perception.",[16,906,907],{},"These are exactly the skills that make you better at your day job:",[245,909,910,915,920],{},[153,911,912],{},[19,913,914],{},"Better problem definition",[153,916,917],{},[19,918,919],{},"Tighter scope control",[153,921,922],{},[19,923,924],{},"Faster execution",[16,926,927],{},"Every personal tool I've built has made me slightly better at building things for other people. It's practice, disguised as productivity.",[29,929,931],{"id":930},"your-turn","Your Turn",[16,933,934],{},[19,935,936],{},"What's one thing you do manually every week that annoys you?",[16,938,939],{},"Not a big life problem. A small, specific friction. Something you've just accepted as \"the way it is.\"",[16,941,942],{},"You have the skills to fix it. And with AI tools, you have the time.",[16,944,945],{},"Don't plan a product. Open your terminal, describe the problem to your AI assistant, and build the simplest possible solution. Use it.",[16,947,948],{},"If it works, keep it. If it proves valuable, improve it. And only then, if it genuinely deserves it, think about sharing it.",{"title":69,"searchDepth":99,"depth":99,"links":950},[951,952,953,954,955,956,957,958],{"id":624,"depth":99,"text":625},{"id":664,"depth":99,"text":665},{"id":713,"depth":99,"text":714},{"id":770,"depth":99,"text":771},{"id":834,"depth":99,"text":835},{"id":870,"depth":99,"text":871},{"id":897,"depth":99,"text":898},{"id":930,"depth":99,"text":931},"2026-03-19T00:00:00.000Z","I built a stock exit validator, a media compression pipeline, and a cryptographic doom scroll blocker. The rule is simple, use it first, expand later only if it really works for you.","/assets/use-it-first.webp",{},"/blog/build-personal-tools-solve-one-problem","beginner","7 min read",{"title":594,"description":960},"blog/build-personal-tools-solve-one-problem",[969,970,117],"productivity","tools","a5kwwHIld1EFFBS-p_5sQF7fIilcLTgwQ940mXTx5AU",{"id":973,"title":974,"author":7,"authorTitle":8,"body":975,"category":1845,"createdAt":1846,"description":1847,"extension":107,"image":1848,"meta":1849,"navigation":110,"path":1850,"proficiency":448,"published":110,"readingTime":113,"seo":1851,"stem":1852,"tags":1853,"updatedAt":118,"__hash__":1856},"blog/blog/building-mcp-server-employee-management.md","Building a Model Context Protocol (MCP) Server",{"type":10,"value":976,"toc":1831},[977,984,987,990,994,997,1002,1028,1032,1035,1067,1073,1077,1080,1103,1107,1110,1150,1153,1157,1160,1197,1200,1259,1262,1268,1272,1279,1295,1301,1307,1311,1314,1334,1337,1351,1354,1367,1370,1376,1380,1383,1388,1406,1413,1418,1442,1445,1596,1602,1606,1609,1612,1622,1628,1638,1644,1649,1655,1663,1667,1672,1700,1705,1744,1749,1778,1782,1785,1788,1805,1809,1825,1828],[13,978,979],{},[16,980,981,983],{},[19,982,21],{}," The Model Context Protocol (MCP) lets AI assistants like Claude interact with external systems in real time. This guide walks through building a complete MCP server in TypeScript with PostgreSQL that handles employee info lookups, leave balance checks, leave applications, and admin functions. You'll learn the MCP architecture, how to define tools with validation, connect to a database, and integrate with Claude Desktop.",[137,985,974],{"id":986},"building-a-model-context-protocol-mcp-server",[16,988,989],{},"The Model Context Protocol (MCP) is revolutionizing how AI assistants interact with external systems and data sources. Instead of being limited to their training data, AI models can now access real-time information and perform actions through MCP servers. In this guide, we'll build a MCP server for employee management that integrates with data sources like a PostgreSQL database.",[29,991,993],{"id":992},"what-is-mcp-and-why-should-you-care","What is MCP and Why Should You Care? 🤔",[16,995,996],{},"MCP is an open protocol that enables AI assistants like Claude to connect to external data sources and tools. Think of it as a bridge between AI and your applications - allowing the AI to read databases, call APIs, and perform complex operations on your behalf.",[16,998,999],{},[19,1000,1001],{},"Key Benefits:",[245,1003,1004,1010,1016,1022],{},[153,1005,1006,1009],{},[19,1007,1008],{},"Real-time Data Access",": AI can query live databases instead of relying on static training data",[153,1011,1012,1015],{},[19,1013,1014],{},"Action Execution",": Perform operations like creating records, sending emails, or triggering workflows",[153,1017,1018,1021],{},[19,1019,1020],{},"Extensibility",": Add new capabilities to AI assistants without retraining models",[153,1023,1024,1027],{},[19,1025,1026],{},"Security",": Controlled access with proper authentication and validation",[29,1029,1031],{"id":1030},"what-well-build-️","What We'll Build 🏗️",[16,1033,1034],{},"Our employee management MCP server will provide these powerful tools:",[150,1036,1037,1043,1049,1055,1061],{},[153,1038,1039,1042],{},[19,1040,1041],{},"get_employee_info"," - Retrieve detailed employee information",[153,1044,1045,1048],{},[19,1046,1047],{},"get_employee_leaves"," - Check leave balances and allocations",[153,1050,1051,1054],{},[19,1052,1053],{},"get_employee_leave_applications"," - View leave application history",[153,1056,1057,1060],{},[19,1058,1059],{},"apply_employee_leave"," - Submit leave requests with validation",[153,1062,1063,1066],{},[19,1064,1065],{},"get_all_employees"," - Admin function to list all employees",[16,1068,1069],{},[94,1070],{"alt":1071,"src":1072},"mcp architecture","/assets/mcp_arch.png",[29,1074,1076],{"id":1075},"prerequisites","Prerequisites 📋",[16,1078,1079],{},"Before we start, make sure you have:",[245,1081,1082,1088,1094,1100],{},[153,1083,1084,1087],{},[19,1085,1086],{},"Node.js"," (v18 or higher)",[153,1089,1090,1093],{},[19,1091,1092],{},"PostgreSQL"," (v12 or higher) or Docker",[153,1095,1096,1099],{},[19,1097,1098],{},"Claude Desktop"," app installed",[153,1101,1102],{},"Basic knowledge of TypeScript and SQL",[29,1104,1106],{"id":1105},"step-1-setting-up-the-project","Step 1: Setting Up the Project 🚀",[16,1108,1109],{},"Create the MCP server using the official TypeScript template:",[61,1111,1113],{"className":278,"code":1112,"language":285,"meta":69,"style":69},"npx @modelcontextprotocol/create-server employee-server\ncd employee-server\nnpm install pg @types/pg dotenv\n",[24,1114,1115,1126,1133],{"__ignoreMap":69},[289,1116,1117,1120,1123],{"class":292,"line":281},[289,1118,1119],{"class":296},"npx",[289,1121,1122],{"class":300}," @modelcontextprotocol/create-server",[289,1124,1125],{"class":300}," employee-server\n",[289,1127,1128,1131],{"class":292,"line":99},[289,1129,1130],{"class":304},"cd",[289,1132,1125],{"class":300},[289,1134,1135,1138,1141,1144,1147],{"class":292,"line":282},[289,1136,1137],{"class":296},"npm",[289,1139,1140],{"class":300}," install",[289,1142,1143],{"class":300}," pg",[289,1145,1146],{"class":300}," @types/pg",[289,1148,1149],{"class":300}," dotenv\n",[16,1151,1152],{},"This creates a well-structured project with all the necessary MCP server boilerplate and PostgreSQL dependencies.",[29,1154,1156],{"id":1155},"step-2-database-setup-with-docker","Step 2: Database Setup with Docker 🐳",[16,1158,1159],{},"If you have Docker, start a PostgreSQL container:",[61,1161,1163],{"className":278,"code":1162,"language":285,"meta":69,"style":69},"docker run --name postgres -e POSTGRES_PASSWORD=postgres -p 5432:5432 -d postgres\n",[24,1164,1165],{"__ignoreMap":69},[289,1166,1167,1170,1173,1176,1179,1182,1185,1188,1191,1194],{"class":292,"line":281},[289,1168,1169],{"class":296},"docker",[289,1171,1172],{"class":300}," run",[289,1174,1175],{"class":304}," --name",[289,1177,1178],{"class":300}," postgres",[289,1180,1181],{"class":304}," -e",[289,1183,1184],{"class":300}," POSTGRES_PASSWORD=postgres",[289,1186,1187],{"class":304}," -p",[289,1189,1190],{"class":300}," 5432:5432",[289,1192,1193],{"class":304}," -d",[289,1195,1196],{"class":300}," postgres\n",[16,1198,1199],{},"Create the database and tables:",[61,1201,1203],{"className":278,"code":1202,"language":285,"meta":69,"style":69},"docker exec -it postgres psql -U postgres -c \"CREATE DATABASE employee_management;\"\ndocker exec -i postgres psql -U postgres -d employee_management \u003C database/setup.sql\n",[24,1204,1205,1231],{"__ignoreMap":69},[289,1206,1207,1209,1212,1215,1217,1220,1223,1225,1228],{"class":292,"line":281},[289,1208,1169],{"class":296},[289,1210,1211],{"class":300}," exec",[289,1213,1214],{"class":304}," -it",[289,1216,1178],{"class":300},[289,1218,1219],{"class":300}," psql",[289,1221,1222],{"class":304}," -U",[289,1224,1178],{"class":300},[289,1226,1227],{"class":304}," -c",[289,1229,1230],{"class":300}," \"CREATE DATABASE employee_management;\"\n",[289,1232,1233,1235,1237,1240,1242,1244,1246,1248,1250,1253,1256],{"class":292,"line":99},[289,1234,1169],{"class":296},[289,1236,1211],{"class":300},[289,1238,1239],{"class":304}," -i",[289,1241,1178],{"class":300},[289,1243,1219],{"class":300},[289,1245,1222],{"class":304},[289,1247,1178],{"class":300},[289,1249,1193],{"class":304},[289,1251,1252],{"class":300}," employee_management",[289,1254,1255],{"class":311}," \u003C",[289,1257,1258],{"class":300}," database/setup.sql\n",[16,1260,1261],{},"The setup script creates 4 tables (employees, leave_types, leave_balances, leave_applications) and inserts sample data including 8 employees across different departments.",[16,1263,1264],{},[94,1265],{"alt":1266,"src":1267},"mcp employee db schema","/assets/mcp_emp_db_schema.png",[29,1269,1271],{"id":1270},"step-3-environment-configuration-️","Step 3: Environment Configuration ⚙️",[16,1273,1274,1275,1278],{},"Create a ",[24,1276,1277],{},".env"," file with your database credentials:",[61,1280,1282],{"className":278,"code":1281,"language":285,"meta":69,"style":69},"cp .env.example .env\n",[24,1283,1284],{"__ignoreMap":69},[289,1285,1286,1289,1292],{"class":292,"line":281},[289,1287,1288],{"class":296},"cp",[289,1290,1291],{"class":300}," .env.example",[289,1293,1294],{"class":300}," .env\n",[16,1296,1297,1298,1300],{},"Update the ",[24,1299,1277],{}," file with your database connection details:",[61,1302,1305],{"className":1303,"code":1304,"language":66},[64],"DB_HOST=localhost\nDB_PORT=5432\nDB_NAME=employee_management\nDB_USER=postgres\nDB_PASSWORD=postgres\n",[24,1306,1304],{"__ignoreMap":69},[29,1308,1310],{"id":1309},"step-4-building-the-server","Step 4: Building the Server 🔨",[16,1312,1313],{},"The project structure includes:",[245,1315,1316,1322,1328],{},[153,1317,1318,1321],{},[19,1319,1320],{},"Database layer",": Connection pooling and query utilities",[153,1323,1324,1327],{},[19,1325,1326],{},"Service layer",": Business logic for employee operations",[153,1329,1330,1333],{},[19,1331,1332],{},"MCP tools",": Tool definitions and request handlers",[16,1335,1336],{},"Build the TypeScript project:",[61,1338,1340],{"className":278,"code":1339,"language":285,"meta":69,"style":69},"npm run build\n",[24,1341,1342],{"__ignoreMap":69},[289,1343,1344,1346,1348],{"class":292,"line":281},[289,1345,1137],{"class":296},[289,1347,1172],{"class":300},[289,1349,1350],{"class":300}," build\n",[16,1352,1353],{},"Test the server connection:",[61,1355,1357],{"className":278,"code":1356,"language":285,"meta":69,"style":69},"node build/index.js\n",[24,1358,1359],{"__ignoreMap":69},[289,1360,1361,1364],{"class":292,"line":281},[289,1362,1363],{"class":296},"node",[289,1365,1366],{"class":300}," build/index.js\n",[16,1368,1369],{},"You should see: \"Database connection successful\" and \"Employee Management MCP server running on stdio\"",[16,1371,1372],{},[94,1373],{"alt":1374,"src":1375},"Server Running","/assets/mcp_running.png",[29,1377,1379],{"id":1378},"step-5-claude-desktop-integration","Step 5: Claude Desktop Integration 🤖",[16,1381,1382],{},"Configure Claude Desktop to use your MCP server. Edit the configuration file:",[16,1384,1385],{},[19,1386,1387],{},"macOS:",[61,1389,1391],{"className":278,"code":1390,"language":285,"meta":69,"style":69},"code ~/Library/Application\\ Support/Claude/claude_desktop_config.json\n",[24,1392,1393],{"__ignoreMap":69},[289,1394,1395,1397,1400,1403],{"class":292,"line":281},[289,1396,24],{"class":296},[289,1398,1399],{"class":300}," ~/Library/Application",[289,1401,1402],{"class":304},"\\ ",[289,1404,1405],{"class":300},"Support/Claude/claude_desktop_config.json\n",[16,1407,1408,1409,1412],{},"Note: If you select yes to ",[24,1410,1411],{},"Would you like to add this server to Claude Desktop"," option, you can skip above step.",[16,1414,1415],{},[19,1416,1417],{},"Windows:",[61,1419,1421],{"className":278,"code":1420,"language":285,"meta":69,"style":69},"code %AppData%\\Claude\\claude_desktop_config.json\n",[24,1422,1423],{"__ignoreMap":69},[289,1424,1425,1427,1430,1433,1436,1439],{"class":292,"line":281},[289,1426,24],{"class":296},[289,1428,1429],{"class":300}," %AppData%",[289,1431,1432],{"class":304},"\\C",[289,1434,1435],{"class":300},"laude",[289,1437,1438],{"class":304},"\\c",[289,1440,1441],{"class":300},"laude_desktop_config.json\n",[16,1443,1444],{},"Add your server configuration:",[61,1446,1450],{"className":1447,"code":1448,"language":1449,"meta":69,"style":69},"language-json shiki shiki-themes github-light github-dark","{\n  \"mcpServers\": {\n    \"employee-server\": {\n      \"command\": \"node\",\n      \"args\": [\"/absolute/path/to/employee-server/build/index.js\"],\n      \"env\": {\n        \"DB_HOST\": \"localhost\",\n        \"DB_PORT\": \"5432\",\n        \"DB_NAME\": \"employee_management\",\n        \"DB_USER\": \"postgres\",\n        \"DB_PASSWORD\": \"postgres\"\n      }\n    }\n  }\n}\n","json",[24,1451,1452,1458,1466,1473,1487,1501,1509,1522,1535,1548,1561,1572,1578,1584,1590],{"__ignoreMap":69},[289,1453,1454],{"class":292,"line":281},[289,1455,1457],{"class":1456},"sVt8B","{\n",[289,1459,1460,1463],{"class":292,"line":99},[289,1461,1462],{"class":304},"  \"mcpServers\"",[289,1464,1465],{"class":1456},": {\n",[289,1467,1468,1471],{"class":292,"line":282},[289,1469,1470],{"class":304},"    \"employee-server\"",[289,1472,1465],{"class":1456},[289,1474,1475,1478,1481,1484],{"class":292,"line":283},[289,1476,1477],{"class":304},"      \"command\"",[289,1479,1480],{"class":1456},": ",[289,1482,1483],{"class":300},"\"node\"",[289,1485,1486],{"class":1456},",\n",[289,1488,1489,1492,1495,1498],{"class":292,"line":284},[289,1490,1491],{"class":304},"      \"args\"",[289,1493,1494],{"class":1456},": [",[289,1496,1497],{"class":300},"\"/absolute/path/to/employee-server/build/index.js\"",[289,1499,1500],{"class":1456},"],\n",[289,1502,1504,1507],{"class":292,"line":1503},6,[289,1505,1506],{"class":304},"      \"env\"",[289,1508,1465],{"class":1456},[289,1510,1512,1515,1517,1520],{"class":292,"line":1511},7,[289,1513,1514],{"class":304},"        \"DB_HOST\"",[289,1516,1480],{"class":1456},[289,1518,1519],{"class":300},"\"localhost\"",[289,1521,1486],{"class":1456},[289,1523,1525,1528,1530,1533],{"class":292,"line":1524},8,[289,1526,1527],{"class":304},"        \"DB_PORT\"",[289,1529,1480],{"class":1456},[289,1531,1532],{"class":300},"\"5432\"",[289,1534,1486],{"class":1456},[289,1536,1538,1541,1543,1546],{"class":292,"line":1537},9,[289,1539,1540],{"class":304},"        \"DB_NAME\"",[289,1542,1480],{"class":1456},[289,1544,1545],{"class":300},"\"employee_management\"",[289,1547,1486],{"class":1456},[289,1549,1551,1554,1556,1559],{"class":292,"line":1550},10,[289,1552,1553],{"class":304},"        \"DB_USER\"",[289,1555,1480],{"class":1456},[289,1557,1558],{"class":300},"\"postgres\"",[289,1560,1486],{"class":1456},[289,1562,1564,1567,1569],{"class":292,"line":1563},11,[289,1565,1566],{"class":304},"        \"DB_PASSWORD\"",[289,1568,1480],{"class":1456},[289,1570,1571],{"class":300},"\"postgres\"\n",[289,1573,1575],{"class":292,"line":1574},12,[289,1576,1577],{"class":1456},"      }\n",[289,1579,1581],{"class":292,"line":1580},13,[289,1582,1583],{"class":1456},"    }\n",[289,1585,1587],{"class":292,"line":1586},14,[289,1588,1589],{"class":1456},"  }\n",[289,1591,1593],{"class":292,"line":1592},15,[289,1594,1595],{"class":1456},"}\n",[16,1597,1598,1601],{},[19,1599,1600],{},"Important:"," Use the absolute path to your project directory.",[29,1603,1605],{"id":1604},"step-6-testing-the-integration","Step 6: Testing the Integration 🧪",[16,1607,1608],{},"Restart Claude Desktop completely. You should now see the MCP tools available in the interface.",[16,1610,1611],{},"Test with these commands:",[245,1613,1614],{},[153,1615,1616,1617,1621],{},"\"Get information for employee ",[45,1618,1620],{"href":1619},"mailto:frank.miller@company.com","frank.miller@company.com","\"",[16,1623,1624],{},[94,1625],{"alt":1626,"src":1627},"Employee Info","/assets/mcp_emp_info.png",[245,1629,1630],{},[153,1631,1632,1633,1637],{},"\"What are the leave balances for ",[45,1634,1636],{"href":1635},"mailto:alice.johnson@company.com","alice.johnson@company.com","?\"",[16,1639,1640],{},[94,1641],{"alt":1642,"src":1643},"Employee Leaves","/assets/mcp_emp_leaves.png",[245,1645,1646],{},[153,1647,1648],{},"\"Show me all employees\"",[16,1650,1651],{},[94,1652],{"alt":1653,"src":1654},"Employee List","/assets/mcp_emp_list.png",[245,1656,1657],{},[153,1658,1659,1660,1662],{},"\"Apply for annual leave for ",[45,1661,1636],{"href":1635}," from 2024-09-01 to 2024-09-05\"",[29,1664,1666],{"id":1665},"troubleshooting-common-issues","Troubleshooting Common Issues 🔧",[16,1668,1669],{},[19,1670,1671],{},"Server not appearing in Claude Desktop:",[61,1673,1675],{"className":278,"code":1674,"language":285,"meta":69,"style":69},"# Check Claude Desktop logs\ntail -f ~/Library/Logs/Claude/mcp*.log\n",[24,1676,1677,1683],{"__ignoreMap":69},[289,1678,1679],{"class":292,"line":281},[289,1680,1682],{"class":1681},"sJ8bj","# Check Claude Desktop logs\n",[289,1684,1685,1688,1691,1694,1697],{"class":292,"line":99},[289,1686,1687],{"class":296},"tail",[289,1689,1690],{"class":304}," -f",[289,1692,1693],{"class":300}," ~/Library/Logs/Claude/mcp",[289,1695,1696],{"class":304},"*",[289,1698,1699],{"class":300},".log\n",[16,1701,1702],{},[19,1703,1704],{},"Database connection issues:",[61,1706,1708],{"className":278,"code":1707,"language":285,"meta":69,"style":69},"# Test PostgreSQL connection\npsql -h localhost -p 5432 -U postgres -d employee_management -c \"SELECT COUNT(*) FROM employees;\"\n",[24,1709,1710,1715],{"__ignoreMap":69},[289,1711,1712],{"class":292,"line":281},[289,1713,1714],{"class":1681},"# Test PostgreSQL connection\n",[289,1716,1717,1720,1723,1726,1728,1731,1733,1735,1737,1739,1741],{"class":292,"line":99},[289,1718,1719],{"class":296},"psql",[289,1721,1722],{"class":304}," -h",[289,1724,1725],{"class":300}," localhost",[289,1727,1187],{"class":304},[289,1729,1730],{"class":304}," 5432",[289,1732,1222],{"class":304},[289,1734,1178],{"class":300},[289,1736,1193],{"class":304},[289,1738,1252],{"class":300},[289,1740,1227],{"class":304},[289,1742,1743],{"class":300}," \"SELECT COUNT(*) FROM employees;\"\n",[16,1745,1746],{},[19,1747,1748],{},"Build errors:",[61,1750,1752],{"className":278,"code":1751,"language":285,"meta":69,"style":69},"# Clean and rebuild\nrm -rf build/\nnpm run build\n",[24,1753,1754,1759,1770],{"__ignoreMap":69},[289,1755,1756],{"class":292,"line":281},[289,1757,1758],{"class":1681},"# Clean and rebuild\n",[289,1760,1761,1764,1767],{"class":292,"line":99},[289,1762,1763],{"class":296},"rm",[289,1765,1766],{"class":304}," -rf",[289,1768,1769],{"class":300}," build/\n",[289,1771,1772,1774,1776],{"class":292,"line":282},[289,1773,1137],{"class":296},[289,1775,1172],{"class":300},[289,1777,1350],{"class":300},[29,1779,1781],{"id":1780},"conclusion","Conclusion ✅",[16,1783,1784],{},"Building an MCP server opens up powerful possibilities, So do keep this in mind when you are building your next application",[16,1786,1787],{},"MCP servers have the following advantages:",[245,1789,1790,1793,1796,1799,1802],{},[153,1791,1792],{},"Connect AI assistants to your systems",[153,1794,1795],{},"Integrate AI with existing databases",[153,1797,1798],{},"Implement complex business logic",[153,1800,1801],{},"Provide secure, validated operations",[153,1803,1804],{},"Create custom tools",[29,1806,1808],{"id":1807},"resources","Resources 📚",[245,1810,1811,1818],{},[153,1812,1813],{},[45,1814,1817],{"href":1815,"rel":1816},"https://modelcontextprotocol.io/",[49],"MCP Official Documentation",[153,1819,1820],{},[45,1821,1824],{"href":1822,"rel":1823},"https://claude.ai/download",[49],"Claude Desktop Download",[1826,1827],"hr",{},[431,1829,1830],{},"html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}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 .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}",{"title":69,"searchDepth":99,"depth":99,"links":1832},[1833,1834,1835,1836,1837,1838,1839,1840,1841,1842,1843,1844],{"id":992,"depth":99,"text":993},{"id":1030,"depth":99,"text":1031},{"id":1075,"depth":99,"text":1076},{"id":1105,"depth":99,"text":1106},{"id":1155,"depth":99,"text":1156},{"id":1270,"depth":99,"text":1271},{"id":1309,"depth":99,"text":1310},{"id":1378,"depth":99,"text":1379},{"id":1604,"depth":99,"text":1605},{"id":1665,"depth":99,"text":1666},{"id":1780,"depth":99,"text":1781},{"id":1807,"depth":99,"text":1808},"Backend","2025-06-07T14:30:00.000Z","Learn how to create a comprehensive MCP server with PostgreSQL integration for employee management. Build tools for employee info, leave management, and database operations with TypeScript and Claude Desktop integration.","/assets/MCP.webp",{},"/blog/building-mcp-server-employee-management",{"title":974,"description":1847},"blog/building-mcp-server-employee-management",[117,1854,1855],"ai","postgres","rxDy4TeL1tijiSKkfWejg_hv5Liq8k-v3cNA2FUdztU",{"id":1858,"title":1859,"author":7,"authorTitle":8,"body":1860,"category":161,"createdAt":2235,"description":2236,"extension":107,"image":2237,"meta":2238,"navigation":110,"path":2239,"proficiency":448,"published":110,"readingTime":2240,"seo":2241,"stem":2242,"tags":2243,"updatedAt":118,"__hash__":2244},"blog/blog/claude-code-browser-plugin-cross-context.md","How the Claude Code Browser Plugin Helped Me Debug Faster",{"type":10,"value":1861,"toc":2219},[1862,1869,1872,1878,1881,1890,1894,1897,1906,1911,1937,1941,1944,1964,1970,1973,1977,1980,1985,1988,1991,2012,2015,2019,2022,2025,2036,2039,2043,2072,2076,2079,2105,2108,2112,2118,2124,2130,2134,2166,2168,2171,2177,2181,2188,2191,2194,2196,2217],[13,1863,1864],{},[16,1865,1866,1868],{},[19,1867,21],{}," Debugging production issues means constantly switching between cloud dashboards and your terminal, losing context each time. The Claude Code browser plugin solves this by letting AI natively read and interpret visual data -- graphs, metrics, timestamps -- from your browser. You can then copy that distilled analysis directly into the Claude Code CLI to correlate infrastructure metrics with code changes. This post walks through a real database performance debugging scenario showing the full cross-context workflow.",[137,1870,1859],{"id":1871},"how-the-claude-code-browser-plugin-helped-me-debug-faster",[16,1873,1874],{},[94,1875],{"alt":1876,"src":1877},"lets debug","https://media1.giphy.com/media/v1.Y2lkPTc5MGI3NjExcjl4eGd6bG80cGlocjFtazhtZmd3a3lubGl2MmoxeWM4d2Rxb3prbiZlcD12MV9naWZzX3NlYXJjaCZjdD1n/26tPnAAJxXTvpLwJy/200.webp",[16,1879,1880],{},"I was staring at a database monitoring dashboard in our cloud console, trying to figure out why our database connections were spiking and performance was degraded. The graphs were all spiking — CPU, memory all hitting the roof. But translating what I saw visually into something actionable for my local debugging session felt like running two separate investigations.",[16,1882,1883,1884,1889],{},"That's when I realized the ",[45,1885,1888],{"href":1886,"rel":1887},"https://docs.anthropic.com/en/docs/claude-code/browser-tool",[49],"Claude Code browser plugin"," could do something really powerful — not just analyze what's on screen, but distill that complex visual data into accurate context that I could feed into my terminal where Claude Code CLI was already helping me dig through application and infrastructure code.",[29,1891,1893],{"id":1892},"why-claude-code-browser-plugin-was-needed","Why Claude Code Browser Plugin was needed? 🤔",[16,1895,1896],{},"The Claude Code browser plugin is a browser extension that gives Claude native access to interact with, analyze, and extract information from web pages. Think of it as Claude's eyes for the browser — it can read dashboards, interpret graphs, navigate complex UIs, and pull out structured data.",[16,1898,1899,1900,1905],{},"But the real magic happens when you combine it with the ",[45,1901,1904],{"href":1902,"rel":1903},"https://docs.anthropic.com/en/docs/claude-code/overview",[49],"Claude Code CLI",". The browser plugin isn't just about answering questions in the browser. It generates a distilled, accurate analysis of your screen that you can copy and pass directly into your CLI session. This cross-context workflow — bringing rich, parsed browser data into local debugging — is what makes it a game changer.",[16,1907,1908],{},[19,1909,1910],{},"Why should you care?",[245,1912,1913,1919,1925,1931],{},[153,1914,1915,1918],{},[19,1916,1917],{},"No more guessing or assuming"," when describing graphs and metrics to your AI",[153,1920,1921,1924],{},[19,1922,1923],{},"AI understands visual data"," natively, pulling out exact timestamps and patterns",[153,1926,1927,1930],{},[19,1928,1929],{},"Rich context bridge"," between what you see in the browser and what you debug locally",[153,1932,1933,1936],{},[19,1934,1935],{},"Faster root cause analysis"," by connecting accurate infrastructure metrics to code changes",[29,1938,1940],{"id":1939},"the-problem-death-by-context-switching","The Problem: Death by Context Switching 🔄",[16,1942,1943],{},"Here's the typical debugging flow most of us follow when something goes wrong in production:",[150,1945,1946,1949,1952,1955,1958,1961],{},[153,1947,1948],{},"Open the cloud console → stare at graphs",[153,1950,1951],{},"Try to mentally note the timestamps and patterns",[153,1953,1954],{},"Switch to terminal → grep through logs",[153,1956,1957],{},"Switch back to console → verify the timeline",[153,1959,1960],{},"Switch to IDE → check recent code changes",[153,1962,1963],{},"Repeat steps 1-5 about multiple times",[16,1965,1966],{},[94,1967],{"alt":1968,"src":1969},"context switching","https://i.giphy.com/Od6YZiM7acnu0.webp",[16,1971,1972],{},"Every context switch costs you mental energy. By the time you're back to the terminal, you've already forgotten half of what the graph was telling you. I've lost count of how many times I've gone back to the same dashboard just to re-read a metric I saw 2 minutes ago.",[29,1974,1976],{"id":1975},"my-debugging-story-the-connection-spike-mystery-️","My Debugging Story: The Connection Spike Mystery 🕵️",[16,1978,1979],{},"We had a production issue — our primary database was extremely slow, causing intermittent 500 errors. The usual suspects were checked: connection pool size, idle timeouts, query performance. Nothing obvious.",[1981,1982,1984],"h3",{"id":1983},"step-1-let-the-browser-plugin-analyze-the-dashboard","Step 1: Let the Browser Plugin Analyze the Dashboard",[16,1986,1987],{},"I had the cloud database monitoring page open. Instead of trying to screenshot and describe the patterns myself, I let the Claude Code browser plugin do its thing.",[16,1989,1990],{},"The plugin analyzed the dashboard and identified:",[245,1992,1993,1999,2005],{},[153,1994,1995,1998],{},[19,1996,1997],{},"CPU and memory usage both spiking"," to near-maximum capacity",[153,2000,2001,2004],{},[19,2002,2003],{},"High disk I/O"," causing CPU wait times as it fetched large amounts of data from disk",[153,2006,2007,2008,2011],{},"The pattern started ",[19,2009,2010],{},"3 days ago"," (not a gradual increase, suggesting a specific deployment)",[16,2013,2014],{},"Just this analysis alone saved me a good 20 minutes of squinting at graphs and trying to correlate timelines manually.",[1981,2016,2018],{"id":2017},"step-2-pass-the-context-to-claude-code-cli","Step 2: Pass the Context to Claude Code CLI",[16,2020,2021],{},"Here's where the cross-context magic happens. I took the browser plugin's distilled analysis — the exact metrics, timestamps, and patterns — and fed it directly into my prompt when I switched to my terminal running Claude Code CLI.",[16,2023,2024],{},"By providing the CLI with this rich, parsed context, it wasn't operating on my vague assumptions (\"the database seems slow lately\"). It had accurate, factual data:",[245,2026,2027,2030,2033],{},[153,2028,2029],{},"The exact timeline (3 days ago)",[153,2031,2032],{},"The precise pattern (15-minute intervals)",[153,2034,2035],{},"The specific metrics affected (connections, not memory)",[16,2037,2038],{},"Instead of me manually re-interpreting the graphs or risking confirmation bias, the CLI had exactly what it needed to connect the dots.",[1981,2040,2042],{"id":2041},"step-3-finding-the-root-cause","Step 3: Finding the Root Cause",[16,2044,2045,2046,2049,2050,2053,2054,2057,2058,2061,2062,2049,2065,2049,2068,2071],{},"With the browser context in hand, Claude Code CLI suggested running diagnostic queries directly against the database. We ran ",[24,2047,2048],{},"SHOW shared_buffers",", ",[24,2051,2052],{},"SHOW work_mem",", and ",[24,2055,2056],{},"SHOW effective_cache_size"," — and the values were shockingly low. Our cloud provider had shipped the managed database instance with stock PostgreSQL defaults, completely untuned for the 15GB machine it was running on. ",[24,2059,2060],{},"shared_buffers"," was set to 128MB instead of the recommended 3840MB. The database was constantly reading from disk instead of memory, explaining every spike in the dashboards. We updated our IaC config with properly tuned flags — ",[24,2063,2064],{},"shared_buffers=3840MB",[24,2066,2067],{},"effective_cache_size=10752MB",[24,2069,2070],{},"work_mem=32MB"," — applied them, and the database performance was immediately restored.",[29,2073,2075],{"id":2074},"how-the-cross-context-flow-works-️","How the Cross-Context Flow Works ⚙️",[16,2077,2078],{},"Let me break down what's actually happening in this workflow:",[150,2080,2081,2087,2093,2099],{},[153,2082,2083,2086],{},[19,2084,2085],{},"Browser Plugin captures context"," — It reads the current page, extracts visual data from graphs, tables, and UI elements.",[153,2088,2089,2092],{},[19,2090,2091],{},"Context is distilled"," — The raw visual data gets converted into structured, accurate information (metrics, timestamps, patterns).",[153,2094,2095,2098],{},[19,2096,2097],{},"You pass the context"," — You copy this rich, distilled analysis and feed it into your Claude Code CLI prompt.",[153,2100,2101,2104],{},[19,2102,2103],{},"AI connects the dots"," — Claude CLI combines this accurate browser data with your local codebase, logs, and git history.",[16,2106,2107],{},"While manual copy-pasting is involved, it is fundamentally different from reviewing dashboards yourself. Instead of feeding your CLI vague human assumptions, you are feeding it cold, hard, AI-parsed facts from your browser.",[29,2109,2111],{"id":2110},"other-ways-i-use-this-workflow-️","Other Ways I Use This Workflow 🛠️",[16,2113,2114,2117],{},[19,2115,2116],{},"Analyzing Cloud Cost Explorers","\nI use the browser plugin to navigate clunky cloud cost explorer UIs, find the right filters and graph configurations, and then pass those cost insights to the CLI to correlate with infrastructure changes in our infrastructure-as-code configs.",[16,2119,2120,2123],{},[19,2121,2122],{},"Debugging Observability Dashboards","\nSetting up observability dashboards in any tool involves navigating through a lot of configuration options. The browser plugin finds the right query builders and log pipeline configs, and that context helps the CLI generate the correct dashboard JSON configurations.",[16,2125,2126,2129],{},[19,2127,2128],{},"Navigating Cloud IAM Policies","\nEver tried to debug IAM permission issues in a cloud console? The nested roles, service accounts, and policy bindings are a maze. The browser plugin maps out the current state, and the CLI uses that to suggest the minimal permission changes needed.",[29,2131,2133],{"id":2132},"tips-for-getting-the-most-out-of-it","Tips for Getting the Most Out of It 💡",[150,2135,2136,2142,2148,2154,2160],{},[153,2137,2138,2141],{},[19,2139,2140],{},"Be specific with what you want analyzed"," — Don't just say \"look at this page.\" Tell the plugin to focus on specific graphs or metrics.",[153,2143,2144,2147],{},[19,2145,2146],{},"Use the CLI immediately after"," — The context is freshest right after the browser analysis. Don't wait too long to switch to the terminal.",[153,2149,2150,2153],{},[19,2151,2152],{},"Combine with local tools"," — The CLI can run commands, check git logs, and read files. Let it do the heavy lifting once it has the browser context.",[153,2155,2156,2159],{},[19,2157,2158],{},"Works great for complex UIs"," — Cloud consoles, monitoring dashboards, CI/CD pipelines — any UI that has too many buttons and tabs is perfect for this workflow.",[153,2161,2162,2165],{},[19,2163,2164],{},"Don't forget about hidden options"," — The browser plugin is surprisingly good at finding buried settings and options that you might miss manually.",[29,2167,1781],{"id":1780},[16,2169,2170],{},"The Claude Code browser plugin turned what used to be a frustrating context-switching exercise into a smooth, connected debugging workflow. Instead of relying on my own flawed visual memory when moving from browser to terminal, I let the AI distill the truth for me first.",[16,2172,2173,2174],{},"The key insight is simple: ",[19,2175,2176],{},"AI that can see what you see in the browser AND work with your code in the terminal is exponentially more useful than either capability alone.",[1981,2178,2180],{"id":2179},"future-improvements","Future Improvements 🚀",[16,2182,2183,2184,2187],{},"While this manual copy-paste workflow is incredible, the next logical step is full automation. In the future, we could build ",[19,2185,2186],{},"read-only MCP (Model Context Protocol) servers"," for these monitoring tools (cloud consoles, cost explorers, observability platforms). With an MCP server providing direct API access, the Claude Code CLI could pull these metrics and perform the initial analysis entirely on its own, without needing the browser plugin as an intermediary.",[16,2189,2190],{},"All this with only read only access. Don't give access to production databases or your cloud infra, Stay safe.",[16,2192,2193],{},"If you're spending too much time jumping between cloud dashboards and your local dev environment, give this cross-context workflow a try. Your future self debugging at 2 AM will thank you.",[29,2195,1808],{"id":1807},[245,2197,2198,2204,2210],{},[153,2199,2200],{},[45,2201,2203],{"href":1886,"rel":2202},[49],"Claude Code Browser Tool Docs",[153,2205,2206],{},[45,2207,2209],{"href":1902,"rel":2208},[49],"Claude Code CLI Overview",[153,2211,2212,2216],{},[45,2213,2215],{"href":1815,"rel":2214},[49],"MCP Protocol"," - For building your own integrations",[1826,2218],{},{"title":69,"searchDepth":99,"depth":99,"links":2220},[2221,2222,2223,2228,2229,2230,2231,2234],{"id":1892,"depth":99,"text":1893},{"id":1939,"depth":99,"text":1940},{"id":1975,"depth":99,"text":1976,"children":2224},[2225,2226,2227],{"id":1983,"depth":282,"text":1984},{"id":2017,"depth":282,"text":2018},{"id":2041,"depth":282,"text":2042},{"id":2074,"depth":99,"text":2075},{"id":2110,"depth":99,"text":2111},{"id":2132,"depth":99,"text":2133},{"id":1780,"depth":99,"text":1781,"children":2232},[2233],{"id":2179,"depth":282,"text":2180},{"id":1807,"depth":99,"text":1808},"2026-03-02T10:00:00.000Z","Learn how the Claude Code browser plugin bridges browser context to the CLI, enabling cross-context AI workflows that speed up debugging cloud infrastructure issues like database performance bottlenecks.","/assets/claude-code_browser_plugin.webp",{},"/blog/claude-code-browser-plugin-cross-context","6 min read",{"title":1859,"description":2236},"blog/claude-code-browser-plugin-cross-context",[117,1854,969],"nHnqZqIJy5GCXsAQL6Q8MNcfLUb2wnBtR5CiF4lYDIM",{"id":2246,"title":2247,"author":7,"authorTitle":8,"body":2248,"category":104,"createdAt":2414,"description":2415,"extension":107,"image":2416,"meta":2417,"navigation":110,"path":2418,"proficiency":448,"published":110,"readingTime":113,"seo":2419,"stem":2420,"tags":2421,"updatedAt":118,"__hash__":2423},"blog/blog/cloudfront-host-spa-website.md","Cloudfront for Hosting SPA (single page application)",{"type":10,"value":2249,"toc":2409},[2250,2265,2268,2275,2278,2285,2288,2294,2301,2304,2310,2313,2316,2322,2325,2328,2345,2352,2379,2385,2388,2398,2403],[13,2251,2252],{},[16,2253,2254,2256,2257,2260,2261,2264],{},[19,2255,21],{}," Hosting a Single Page Application on CloudFront? You'll hit 403 errors when users navigate to routes like ",[24,2258,2259],{},"/about"," because CloudFront looks for a literal file that doesn't exist in S3. The fix is configuring custom error responses to redirect 403/404 errors back to ",[24,2262,2263],{},"index.html"," with a 200 status code, letting your client-side router handle the routing. This post explains the MPA vs SPA difference, what CloudFront does, and the exact configuration needed.",[137,2266,2247],{"id":2267},"cloudfront-for-hosting-spa-single-page-application",[245,2269,2270],{},[153,2271,2272],{},[19,2273,2274],{},"What is Single Page Application ?",[16,2276,2277],{},"To understand Single Page Application we need to first understand what was the traditional way websites used to work. In older websites each page on website was requested separately.",[16,2279,2280,2281,2284],{},"This pattern is called as ",[24,2282,2283],{},"Multiple-page app(MPA)",".",[16,2286,2287],{},"In Multiple-page app(MPA) every change displays the data or submits data back to server requests rendering a new page from the server in the browser",[16,2289,2290,2293],{},[24,2291,2292],{},"Single page application(SPA)"," is an app that doesn't need to reload the page during its use and works with the browser. Think of all the apps like Google Maps Gmail a Facebook event GitHub all these are the examples of single page application.",[16,2295,2296,2297,2300],{},"The main advantage of single-page applications is its speed. The benefit of using single page applications is the ",[24,2298,2299],{},"user experience(UX)"," where the user doesn't have to wait a lot for page reloads or navigation",[16,2302,2303],{},"From developers perspective is very easy to debug a single page applications you can use special tools for build for angular react view of you can use Chrome console and monitor Network operations",[29,2305,2307],{"id":2306},"what-is-cloudfront",[19,2308,2309],{},"What is cloudfront ?",[16,2311,2312],{},"Amazon CloudFront is a web service that speeds up distribution of your static and dynamic web content, such as .html, .css, .js, and image files, to your users.",[16,2314,2315],{},"In case of SPA this delivers all the html files and JS bundles(heavy in size) quickly to client devices.",[29,2317,2319],{"id":2318},"what-are-the-challenges-while-hosting-a-spa-on-cloudfront",[19,2320,2321],{},"What are the challenges while hosting a SPA on Cloudfront ?",[16,2323,2324],{},"When you request for a resource from cloudfront it returns a cached version of that file to your browser.\nWhen you want to load an internal route/URL path on your website that only your SPA understands. Cloudfront can not return any response to that. Because it has no corresponding file for that resource",[16,2326,2327],{},"Ex.\nSteps in case of path matches file present in Cloudfront cache",[150,2329,2330,2336,2342],{},[153,2331,2332,2333],{},"Broweser requests ",[24,2334,2335],{},"hostname/index.html",[153,2337,2338,2339,2341],{},"Cloudfront has a ",[24,2340,2263],{}," file in cache",[153,2343,2344],{},"Returns file back to browser.",[16,2346,2347,2348,2351],{},"Steps in case of path matches file ",[24,2349,2350],{},"NOT"," present in Cloudfront cache",[150,2353,2354,2359,2365,2368,2373,2376],{},[153,2355,2332,2356],{},[24,2357,2358],{},"hostname/home",[153,2360,2361,2362],{},"Cloudfront has no file for ",[24,2363,2364],{},"home.html",[153,2366,2367],{},"Cloudfront checks with base server",[153,2369,2370,2371],{},"There is no file ",[24,2372,2364],{},[153,2374,2375],{},"Cloudfront gets no content response from base server",[153,2377,2378],{},"Cloud front sends file not found or AccessDenied response to browser.",[29,2380,2382],{"id":2381},"how-to-configure-cloudfront-to-deal-with-above-challenges",[19,2383,2384],{},"How to configure cloudfront to deal with above challenges",[16,2386,2387],{},"Above issue arises due to the behaviour of SPA, The internal routing is handled in client side.\nBut because the internal route/URL path is requested before the SPA has loaded in browser, cloudfront thinks that this is a unknown route and returns AccessDenied response.",[16,2389,2390,2391,2394,2395,2397],{},"The ",[24,2392,2393],{},"solution"," is to make browsers aware of the routing before requesting the internal routes.\nTo achieve this we can redirect the unknown route paths to homepage( ",[24,2396,2263],{},"). This makes sure we load the JS bundles necessary for internal routing.",[16,2399,2400],{},[94,2401],{"alt":96,"src":2402},"/assets/cloudfront-err-page-config.webp",[16,2404,2405,2406,2408],{},"With this config, Cloudfront will not return error but will respond with ",[24,2407,2263],{}," page.\nNow browser will load this page and handle the internal redirect in browser.",{"title":69,"searchDepth":99,"depth":99,"links":2410},[2411,2412,2413],{"id":2306,"depth":99,"text":2309},{"id":2318,"depth":99,"text":2321},{"id":2381,"depth":99,"text":2384},"2022-05-03T07:00:13.392Z","Amazon Cloudfront for faster distribution of your static and dynamic web content. Learn about the challenges faced while hosting an SPA on Cloudfront and how to configure it to handle internal routing in the browser.","/assets/template.webp",{},"/blog/cloudfront-host-spa-website",{"title":2247,"description":2415},"blog/cloudfront-host-spa-website",[590,2422],"aws","Yb_fhAuFzJwQSdbdi4bGaQflB4RKSAzF8xwu5mN7JlE",{"id":2425,"title":2426,"author":7,"authorTitle":8,"body":2427,"category":161,"createdAt":2513,"description":2514,"extension":107,"image":2515,"meta":2516,"navigation":110,"path":2517,"proficiency":112,"published":110,"readingTime":586,"seo":2518,"stem":2519,"tags":2520,"updatedAt":118,"__hash__":2521},"blog/blog/easily-remove-node-modules.md","Say goodbye to unused node modules in your computer.",{"type":10,"value":2428,"toc":2509},[2429,2436,2439,2446,2450,2453,2456,2473,2476,2479,2482,2489,2492,2495,2498,2501],[13,2430,2431],{},[16,2432,2433,2435],{},[19,2434,21],{}," Every JavaScript developer accumulates stale projects with bloated node_modules folders eating up disk space. Manually finding and deleting them takes hours. NPKill is an npm tool that scans your system, lists all node_modules directories with their sizes, and lets you delete them interactively with a single keystroke. It's a quick win for reclaiming gigabytes of storage in minutes.",[137,2437,2426],{"id":2438},"say-goodbye-to-unused-node-modules-in-your-computer",[16,2440,2441,2442,2445],{},"This is not a ",[24,2443,2444],{},"how-to"," post, rather discussion on how NPKill is\nuseful for developers (NodeJs or Javacript).",[29,2447,2449],{"id":2448},"problem","Problem",[16,2451,2452],{},"Every NodeJs or Javacript developer is aware that the node modules\ntakes up lot of space than actual code they write.",[16,2454,2455],{},"Number of projects you work on can easily increase if you are,",[245,2457,2458,2461,2464,2467,2470],{},[153,2459,2460],{},"Working on different projects in your org",[153,2462,2463],{},"Trying out any new small POCs",[153,2465,2466],{},"Cloning/forking some git repos to try it out",[153,2468,2469],{},"Contributing to open source projects/repos",[153,2471,2472],{},"Freelancer doing small repeatable projects",[16,2474,2475],{},"With time some projects can get stale, but they consume the\na lot of space due to node_modules.",[16,2477,2478],{},"It can get very tiring to find these folders and delete the\nnode_modules manually. It can easily take hours.",[29,2480,2481],{"id":2393},"Solution",[16,2483,2484,2485,2488],{},"To easily tackle above problem, I found a npm library ",[24,2486,2487],{},"NPKill"," that\nautomates this process",[16,2490,2491],{},"NPKill helps to list all node_module folders and you can easily navigate this list and delete the ones you dont need.",[16,2493,2494],{},"So your manual process that takes hours is reduced to just 5-10 minutes.",[16,2496,2497],{},"There is one thing to note, you have to run the command at root folder where your projects are, or in case of windows run at each drive level (ex C:/ or D:/).\nIt searches all child folders, does not search whole file system.",[16,2499,2500],{},"You can find out how to use it here.\nIt is very well documented.",[245,2502,2503],{},[153,2504,2505],{},[45,2506,2507],{"href":2507,"rel":2508},"https://npkill.js.org/",[49],{"title":69,"searchDepth":99,"depth":99,"links":2510},[2511,2512],{"id":2448,"depth":99,"text":2449},{"id":2393,"depth":99,"text":2481},"2021-05-05T07:00:13.392Z","Say goodbye to hours of manually deleting unused node modules with NPKill. This npm library automates the process and helps you save time in just a few minutes.","/assets/node-modules-app-performance_.webp",{},"/blog/easily-remove-node-modules",{"title":2426,"description":2514},"blog/easily-remove-node-modules",[117],"16IX3XBS2uuBytbMf7bwMBInwql7vhAO8kgmPPMYEr4",{"id":2523,"title":2524,"author":7,"authorTitle":8,"body":2525,"category":161,"createdAt":959,"description":2924,"extension":107,"image":2551,"meta":2925,"navigation":110,"path":2926,"proficiency":448,"published":167,"readingTime":2927,"seo":2928,"stem":2929,"tags":2930,"updatedAt":118,"__hash__":2932},"blog/blog/five-ai-tools-one-workflow.md","Five AI Tools, One Workflow - My Multi-Brain Development Stack",{"type":10,"value":2526,"toc":2913},[2527,2534,2537,2540,2543,2546,2552,2557,2561,2564,2567,2570,2573,2578,2583,2587,2590,2593,2596,2599,2604,2609,2613,2616,2619,2622,2625,2630,2635,2639,2642,2645,2648,2668,2671,2676,2681,2685,2688,2691,2711,2714,2719,2724,2728,2731,2734,2830,2833,2837,2840,2867,2870,2874,2877,2880,2883,2886,2890,2893,2907,2910],[13,2528,2529],{},[16,2530,2531,2533],{},[19,2532,21],{}," Using one AI tool for everything is like using a hammer for every job. I use five AI tools — NotebookLM for research, ChatGPT for brainstorming, Google Stitch for UI design, Claude Code for implementation, and the Superpowers plugin for quality gates. Each tool maps to a different phase of how I think and build. The workflow is the product, not any single tool.",[16,2535,2536],{},"I used to do everything in one AI tool. Research a library? ChatGPT. Write the code? ChatGPT. Debug it? ChatGPT. Design the UI? Also ChatGPT.",[16,2538,2539],{},"It worked, but it felt like I was asking a Swiss Army knife to be a power drill. The tool was decent at everything but great at nothing.",[16,2541,2542],{},"Over the past few months, I started mapping different AI tools to different phases of my work. Not because I wanted to use more tools — but because I noticed each tool has a sweet spot, a specific type of thinking it handles better than others.",[16,2544,2545],{},"Here's the workflow I landed on.",[16,2547,2548],{},[94,2549],{"alt":2550,"src":2551},"placeholder: diagram showing 5 phases - Research → Ideation → Design → Implementation → Discipline","/assets/placeholder.webp",[13,2553,2554],{},[16,2555,2556],{},"The five phases mapped to five tools",[29,2558,2560],{"id":2559},"phase-1-notebooklm-the-research-brain","Phase 1: NotebookLM — The Research Brain",[16,2562,2563],{},"Before I write a single line of code, I need to understand the problem space. Not just \"what API do I call\" but the deeper context — what has been tried before, what are the tradeoffs, what does the documentation actually say.",[16,2565,2566],{},"NotebookLM is perfect for this. I load up documentation, technical papers, or even existing codebases as sources. Then I have a conversation with that specific context.",[16,2568,2569],{},"The key difference from other AI tools: NotebookLM doesn't hallucinate as much because it's grounded in the sources you provide. When I ask \"how does this authentication flow work?\", it points me to the exact section in the docs.",[16,2571,2572],{},"I spend 15-20 minutes here before touching any other tool. This sounds slow, but it prevents the much more expensive mistake of building the wrong thing.",[16,2574,2575],{},[94,2576],{"alt":2577,"src":2551},"placeholder: screenshot of NotebookLM with loaded sources and a research conversation",[13,2579,2580],{},[16,2581,2582],{},"NotebookLM grounded in actual documentation, not general knowledge",[29,2584,2586],{"id":2585},"phase-2-chatgpt-the-rubber-duck","Phase 2: ChatGPT — The Rubber Duck",[16,2588,2589],{},"Once I understand the problem, I need to validate my approach. This is where ChatGPT shines — fast, low-friction, back-and-forth conversation.",[16,2591,2592],{},"I use it like a rubber duck that talks back. \"I'm thinking of structuring this as a pipeline with three stages — does that make sense?\" or \"What are the downsides of using WebSockets here instead of SSE?\"",[16,2594,2595],{},"ChatGPT is great for this because the conversation is lightweight. I'm not loading a codebase, I'm not setting up context. I'm just thinking out loud and getting quick pushback.",[16,2597,2598],{},"The goal of this phase isn't code — it's confidence. By the time I leave ChatGPT, I know what I'm going to build and roughly how.",[16,2600,2601],{},[94,2602],{"alt":2603,"src":2551},"placeholder: screenshot of a ChatGPT conversation exploring architectural tradeoffs",[13,2605,2606],{},[16,2607,2608],{},"Quick back-and-forth to validate an approach before committing",[29,2610,2612],{"id":2611},"phase-3-google-stitch-the-design-eye","Phase 3: Google Stitch — The Design Eye",[16,2614,2615],{},"This is the phase most developers skip entirely. They go straight from \"I know what to build\" to writing components. The result is UI that works but doesn't feel right.",[16,2617,2618],{},"Google Stitch lets me generate multiple visual variants of a UI concept in minutes. I describe what I want, and it produces real, styled designs — not wireframes, actual visual mockups.",[16,2620,2621],{},"The trick I use: I ask for 3-5 variations of the same screen. Different layouts, different color approaches, different information hierarchy. Then I pick the best elements from each.",[16,2623,2624],{},"This \"generate broadly, curate narrowly\" approach means my final UI is always better than what I would have designed in one shot. The cost of exploring 5 options is minutes, not days.",[16,2626,2627],{},[94,2628],{"alt":2629,"src":2551},"placeholder: screenshot showing multiple Stitch UI variants side by side",[13,2631,2632],{},[16,2633,2634],{},"Five variants of the same screen — the best design comes from having options",[29,2636,2638],{"id":2637},"phase-4-claude-code-the-builder","Phase 4: Claude Code — The Builder",[16,2640,2641],{},"Now it's time to actually build. This is where Claude Code takes over — full codebase context, subagent delegation, plan documents, git worktrees.",[16,2643,2644],{},"What makes Claude Code different from using AI in a chat window is that it operates inside your project. It reads your files, understands your patterns, and writes code that fits your codebase — not generic snippets you have to adapt.",[16,2646,2647],{},"I use a few specific patterns here:",[245,2649,2650,2656,2662],{},[153,2651,2652,2655],{},[19,2653,2654],{},"Plan documents"," before implementation — a markdown file describing what we're building and why, so the AI and I are aligned",[153,2657,2658,2661],{},[19,2659,2660],{},"Subagent delegation"," — I dispatch smaller models (like Sonnet) for broad exploration tasks (searching across files, understanding patterns) while keeping the main agent focused on architecture decisions",[153,2663,2664,2667],{},[19,2665,2666],{},"Git worktrees"," — for parallel feature development, I create isolated worktrees so multiple agents can work on different features without stepping on each other",[16,2669,2670],{},"The research from Phase 1, the validated approach from Phase 2, and the design from Phase 3 all feed into this phase. Claude Code isn't starting from zero — it's executing a plan that's already been thought through.",[16,2672,2673],{},[94,2674],{"alt":2675,"src":2551},"placeholder: screenshot of Claude Code working with plan document and subagents",[13,2677,2678],{},[16,2679,2680],{},"Claude Code executing a plan, not guessing what to build",[29,2682,2684],{"id":2683},"phase-5-superpowers-plugin-the-discipline-layer","Phase 5: Superpowers Plugin — The Discipline Layer",[16,2686,2687],{},"This is the phase I wish I had discovered earlier. Speed without discipline is how you ship bugs faster.",[16,2689,2690],{},"The Superpowers plugin for Claude Code adds quality gates to the workflow. It's a set of skills that enforce good engineering habits:",[245,2692,2693,2699,2705],{},[153,2694,2695,2698],{},[19,2696,2697],{},"Brainstorming before code"," — the plugin forces you to explore requirements and edge cases before implementation starts. No more \"let me just start coding and figure it out\"",[153,2700,2701,2704],{},[19,2702,2703],{},"Code review workflows"," — automated review that checks your implementation against the original plan, not just syntax",[153,2706,2707,2710],{},[19,2708,2709],{},"Verification before completion"," — before you can claim something is \"done\", the plugin requires you to run tests and confirm the output. Evidence before assertions",[16,2712,2713],{},"Without this layer, the speed of AI-assisted development becomes dangerous. You can write a feature in 20 minutes that has subtle bugs that take 2 hours to debug. The discipline layer catches those before they compound.",[16,2715,2716],{},[94,2717],{"alt":2718,"src":2551},"placeholder: screenshot of Superpowers enforcing a brainstorming step before implementation",[13,2720,2721],{},[16,2722,2723],{},"Superpowers forcing a brainstorm before I can start writing code",[29,2725,2727],{"id":2726},"why-the-workflow-matters-more-than-any-single-tool","Why the Workflow Matters More Than Any Single Tool",[16,2729,2730],{},"The real insight isn't about these five specific tools. It's about mapping different cognitive tasks to the right tool for each one.",[16,2732,2733],{},"Think about it this way:",[2735,2736,2737,2756],"table",{},[2738,2739,2740],"thead",{},[2741,2742,2743,2747,2750,2753],"tr",{},[2744,2745,2746],"th",{},"Phase",[2744,2748,2749],{},"Cognitive Task",[2744,2751,2752],{},"What You Need",[2744,2754,2755],{},"Tool",[2757,2758,2759,2774,2788,2802,2816],"tbody",{},[2741,2760,2761,2765,2768,2771],{},[2762,2763,2764],"td",{},"Research",[2762,2766,2767],{},"Understanding",[2762,2769,2770],{},"Grounded, source-based answers",[2762,2772,2773],{},"NotebookLM",[2741,2775,2776,2779,2782,2785],{},[2762,2777,2778],{},"Ideation",[2762,2780,2781],{},"Validating",[2762,2783,2784],{},"Fast, low-friction conversation",[2762,2786,2787],{},"ChatGPT",[2741,2789,2790,2793,2796,2799],{},[2762,2791,2792],{},"Design",[2762,2794,2795],{},"Visualizing",[2762,2797,2798],{},"Multiple visual options quickly",[2762,2800,2801],{},"Google Stitch",[2741,2803,2804,2807,2810,2813],{},[2762,2805,2806],{},"Building",[2762,2808,2809],{},"Implementing",[2762,2811,2812],{},"Full codebase context",[2762,2814,2815],{},"Claude Code",[2741,2817,2818,2821,2824,2827],{},[2762,2819,2820],{},"Quality",[2762,2822,2823],{},"Disciplining",[2762,2825,2826],{},"Enforced checkpoints",[2762,2828,2829],{},"Superpowers",[16,2831,2832],{},"If you use one tool for all five phases, you're asking it to be something it's not for at least three of them.",[29,2834,2836],{"id":2835},"what-this-looks-like-in-practice","What This Looks Like in Practice",[16,2838,2839],{},"Here's a recent example. I was building an ideas management feature for a personal project — a tab where I could capture thoughts, rate them with stars, and thread sub-thoughts underneath.",[150,2841,2842,2847,2852,2857,2862],{},[153,2843,2844,2846],{},[19,2845,2773],{}," — I loaded documentation about state management patterns and existing note-taking app architectures. Spent 15 minutes understanding what works.",[153,2848,2849,2851],{},[19,2850,2787],{}," — I described my concept and asked \"what's the simplest data model that supports nested threads with ratings?\" Got pushback on my initial approach, iterated to something cleaner.",[153,2853,2854,2856],{},[19,2855,2801],{}," — Generated 4 variants: ranked list, kanban board, mind map, and sticky notes. Picked ranked list for V1 but saved the kanban concept for later.",[153,2858,2859,2861],{},[19,2860,2815],{}," — Built the feature with a plan document, using subagents to scaffold the component structure while I focused on the data layer.",[153,2863,2864,2866],{},[19,2865,2829],{}," — Ran the code review skill to verify the implementation matched the plan. Caught two edge cases I'd missed.",[16,2868,2869],{},"Total time: about 2 hours for a feature that would have taken a full day with a single-tool approach.",[29,2871,2873],{"id":2872},"bonus-obsidian-as-the-reflection-layer","Bonus: Obsidian as the Reflection Layer",[16,2875,2876],{},"I want to mention one more tool that doesn't fit neatly into the five phases but ties everything together: Obsidian.",[16,2878,2879],{},"At the end of each day, I spend 5-10 minutes in my \"Mindful Dailies\" template — a quick journal of what I built, what worked, what didn't, and what I want to tackle tomorrow. It's not AI-powered, it's just markdown and thinking.",[16,2881,2882],{},"This reflection habit is what keeps the five-tool workflow from becoming a mindless process. It's where I notice patterns like \"I keep skipping Phase 2 and regretting it\" or \"the Stitch variants saved me from a bad design choice again.\"",[16,2884,2885],{},"The tools do the work. The reflection makes the work better over time.",[29,2887,2889],{"id":2888},"try-this","Try This",[16,2891,2892],{},"You don't need to adopt all five tools tomorrow. Start with this exercise:",[150,2894,2895,2898,2901,2904],{},[153,2896,2897],{},"Think about the last feature you built",[153,2899,2900],{},"Which cognitive phases did you go through? (research, ideation, design, building, review)",[153,2902,2903],{},"How many of those phases did you force through a single tool?",[153,2905,2906],{},"Where did you get stuck or produce subpar results?",[16,2908,2909],{},"That stuck point is where a specialized tool would have helped most. Start there.",[16,2911,2912],{},"💡 The best developers I know aren't the ones with the most powerful AI tool. They're the ones who know when to switch tools — because they understand that different types of thinking need different types of support.",{"title":69,"searchDepth":99,"depth":99,"links":2914},[2915,2916,2917,2918,2919,2920,2921,2922,2923],{"id":2559,"depth":99,"text":2560},{"id":2585,"depth":99,"text":2586},{"id":2611,"depth":99,"text":2612},{"id":2637,"depth":99,"text":2638},{"id":2683,"depth":99,"text":2684},{"id":2726,"depth":99,"text":2727},{"id":2835,"depth":99,"text":2836},{"id":2872,"depth":99,"text":2873},{"id":2888,"depth":99,"text":2889},"I mapped five AI tools to five cognitive phases of development — research, ideation, design, implementation, and discipline.",{},"/blog/five-ai-tools-one-workflow","8 min read",{"title":2524,"description":2924},"blog/five-ai-tools-one-workflow",[1854,2931,969],"workflow","OzxWm1XVyVP-FU1yvFgintnJjcDbGTvZIut29wiqtr8",{"id":2934,"title":2935,"author":7,"authorTitle":8,"body":2936,"category":3297,"createdAt":3298,"description":3299,"extension":107,"image":3300,"meta":3301,"navigation":110,"path":3302,"proficiency":448,"published":110,"readingTime":2240,"seo":3303,"stem":3304,"tags":3305,"updatedAt":118,"__hash__":3307},"blog/blog/gcp-suspend-resume-vm.md","Schedule Google Compute Engine Instances to Save Big Money.",{"type":10,"value":2937,"toc":3291},[2938,2945,2948,2959,2962,2966,2979,2982,2988,2992,3000,3003,3014,3023,3027,3042,3226,3246,3253,3260,3266,3275,3279,3288],[13,2939,2940],{},[16,2941,2942,2944],{},[19,2943,21],{}," If you only use a GCP Compute Engine VM for a few hours daily, you're overpaying. This guide shows how to automate VM start/stop (or suspend/resume) on a cron schedule using Cloud Scheduler, Pub/Sub, and Cloud Functions. When stopped, you only pay for disk and network -- not compute. Note that suspend/resume (like hibernate) only works on N2+ instances, not E2. The post covers the full setup including IAM permissions and function deployment.",[137,2946,2935],{"id":2947},"schedule-google-compute-engine-instances-to-save-big-money",[16,2949,2950,2951,2954,2955,2958],{},"Virtual machine is first type of compute for someone starting with the cloud. As per many reports 80-85% enterprise workload have migrated to cloud. ",[2952,2953],"br",{},"\nWith this increased cloud adoptions, individuals also thinking of shifting some heavy compute tasks on cloud VMs, rather than building a machine at home. As there is a benefit of ",[24,2956,2957],{},"pay-as-you-go"," model in cloud resources and no mainteance required.",[16,2960,2961],{},"In this article I’ll be showing you how you can save up money if you are using compute engine VMs for very few hours a day.",[29,2963,2965],{"id":2964},"gcp-compute-engine-pay-as-you-go","GCP compute engine - pay as you go",[16,2967,2968,2969,2971,2972,571,2975,2978],{},"With ",[24,2970,2957],{}," model you only pay for time you use a resource.\nIf you have a schedule like daily for 2-3 hours you will need to use a VM, you can keep it in ",[24,2973,2974],{},"stopped",[24,2976,2977],{},"suspended"," state.",[16,2980,2981],{},"GCP compute engine lets you start and stop a compute engine VM.\nIn this case you only pay minor charges for network resources and disk attached.\nIn GCP there is also an option to suspend and resume compute engine VM. Which is similar to hibernate on our physical machines.",[16,2983,2984,2987],{},[19,2985,2986],{},"NOTE:"," Right now as I am writing this article, E2 type compute engine instances do not support suspend/resume feature. So I had to choose N2 type VM. So keep this in mind if you want suspend/resume feature for your use-case.",[29,2989,2991],{"id":2990},"managing-state-using-cloud-scheduler-and-cloud-function","Managing state using cloud scheduler and cloud function",[16,2993,2994,2999],{},[45,2995,2998],{"href":2996,"rel":2997},"https://cloud.google.com/scheduler/docs/start-and-stop-compute-engine-instances-on-a-schedule",[49],"This gcp documentation "," show how you can use cloud functions along with pub-sub trigger and cloud scheduler to manage state of VM on a cron schedule.",[16,3001,3002],{},"These extra resources are required for this solution",[150,3004,3005,3008,3011],{},[153,3006,3007],{},"cloud function - logic to change VM state",[153,3009,3010],{},"pub-sub - acts as trigger for cloud function",[153,3012,3013],{},"cloud scheduler - publishes message to pub-sub when cron expression is matched.",[16,3015,3016,3017,3022],{},"VM price for month ranges from 15$-20$ per vCPU. cloud schedulerand pubsub costs 3-4 $ for this use case. This is peanuts amount as compared to VMs monthly cost.\nUse ",[45,3018,3021],{"href":3019,"rel":3020},"https://cloud.google.com/products/calculator",[49],"pricing calculator"," to calculate exact costs.",[29,3024,3026],{"id":3025},"implementation","Implementation",[16,3028,3029,3030,3033,3034,3037,3038,3041],{},"Cloud function has a ",[24,3031,3032],{},"pub-sub"," trigger.\n",[94,3035],{"alt":3032,"src":3036},"/assets/vm-auto-trigger.webp","\nCloud function code is as below, ",[24,3039,3040],{},"change_vm_state"," function is invoked when pub-sub message is received.",[61,3043,3048],{"className":3044,"code":3045,"highlights":3046,"language":3047,"meta":69,"style":69},"language-python shiki shiki-themes github-light github-dark","import base64\nimport googleapiclient.discovery\nimport sys\n\nPROJECT = \"GCP-PROJECT-NAME\"\nZONE = \"VM-ZONE\"\nINSTANCE = \"INSTANCE-NAME\"\n\ndef suspend(compute):\n    return compute.instances().suspend(project=PROJECT, zone=ZONE, instance=INSTANCE).execute()\n\ndef resume(compute):\n    return compute.instances().resume(project=PROJECT, zone=ZONE, instance=INSTANCE).execute()\n\ndef change_vm_state(event, context):\n    # Parse pubsub message\n    pubsub_message = base64.b64decode(event['data']).decode('utf-8')\n    # Pubsub message should not be empty\n    if pubsub_message == None:\n        print(\"No operation specified, choose either `suspend` or `resume`\")\n        sys.exit()\n    # Create compute client using googleapiclient.\n    # Suspend resume comes under beta APIs, so we need to pass `compute` and `beta` params.\n    compute = googleapiclient.discovery.build('compute', 'beta')\n\n    if pubsub_message == \"suspend\":\n        suspend(compute)\n    elif pubsub_message == \"resume\":\n        resume(compute)\n    else:\n        print(\"Invalid operation. choose either `suspend` or `resume`\")\n        sys.exit()\n",[281,282,283,284],"python",[24,3049,3050,3056,3061,3067,3073,3079,3084,3089,3093,3098,3103,3107,3112,3117,3121,3126,3132,3138,3144,3150,3156,3162,3168,3174,3180,3185,3191,3197,3203,3209,3215,3221],{"__ignoreMap":69},[289,3051,3053],{"class":3052,"line":281},[292,293],[289,3054,3055],{},"import base64\n",[289,3057,3058],{"class":292,"line":99},[289,3059,3060],{},"import googleapiclient.discovery\n",[289,3062,3064],{"class":3063,"line":282},[292,293],[289,3065,3066],{},"import sys\n",[289,3068,3070],{"class":3069,"line":283},[292,293],[289,3071,3072],{"emptyLinePlaceholder":110},"\n",[289,3074,3076],{"class":3075,"line":284},[292,293],[289,3077,3078],{},"PROJECT = \"GCP-PROJECT-NAME\"\n",[289,3080,3081],{"class":292,"line":1503},[289,3082,3083],{},"ZONE = \"VM-ZONE\"\n",[289,3085,3086],{"class":292,"line":1511},[289,3087,3088],{},"INSTANCE = \"INSTANCE-NAME\"\n",[289,3090,3091],{"class":292,"line":1524},[289,3092,3072],{"emptyLinePlaceholder":110},[289,3094,3095],{"class":292,"line":1537},[289,3096,3097],{},"def suspend(compute):\n",[289,3099,3100],{"class":292,"line":1550},[289,3101,3102],{},"    return compute.instances().suspend(project=PROJECT, zone=ZONE, instance=INSTANCE).execute()\n",[289,3104,3105],{"class":292,"line":1563},[289,3106,3072],{"emptyLinePlaceholder":110},[289,3108,3109],{"class":292,"line":1574},[289,3110,3111],{},"def resume(compute):\n",[289,3113,3114],{"class":292,"line":1580},[289,3115,3116],{},"    return compute.instances().resume(project=PROJECT, zone=ZONE, instance=INSTANCE).execute()\n",[289,3118,3119],{"class":292,"line":1586},[289,3120,3072],{"emptyLinePlaceholder":110},[289,3122,3123],{"class":292,"line":1592},[289,3124,3125],{},"def change_vm_state(event, context):\n",[289,3127,3129],{"class":292,"line":3128},16,[289,3130,3131],{},"    # Parse pubsub message\n",[289,3133,3135],{"class":292,"line":3134},17,[289,3136,3137],{},"    pubsub_message = base64.b64decode(event['data']).decode('utf-8')\n",[289,3139,3141],{"class":292,"line":3140},18,[289,3142,3143],{},"    # Pubsub message should not be empty\n",[289,3145,3147],{"class":292,"line":3146},19,[289,3148,3149],{},"    if pubsub_message == None:\n",[289,3151,3153],{"class":292,"line":3152},20,[289,3154,3155],{},"        print(\"No operation specified, choose either `suspend` or `resume`\")\n",[289,3157,3159],{"class":292,"line":3158},21,[289,3160,3161],{},"        sys.exit()\n",[289,3163,3165],{"class":292,"line":3164},22,[289,3166,3167],{},"    # Create compute client using googleapiclient.\n",[289,3169,3171],{"class":292,"line":3170},23,[289,3172,3173],{},"    # Suspend resume comes under beta APIs, so we need to pass `compute` and `beta` params.\n",[289,3175,3177],{"class":292,"line":3176},24,[289,3178,3179],{},"    compute = googleapiclient.discovery.build('compute', 'beta')\n",[289,3181,3183],{"class":292,"line":3182},25,[289,3184,3072],{"emptyLinePlaceholder":110},[289,3186,3188],{"class":292,"line":3187},26,[289,3189,3190],{},"    if pubsub_message == \"suspend\":\n",[289,3192,3194],{"class":292,"line":3193},27,[289,3195,3196],{},"        suspend(compute)\n",[289,3198,3200],{"class":292,"line":3199},28,[289,3201,3202],{},"    elif pubsub_message == \"resume\":\n",[289,3204,3206],{"class":292,"line":3205},29,[289,3207,3208],{},"        resume(compute)\n",[289,3210,3212],{"class":292,"line":3211},30,[289,3213,3214],{},"    else:\n",[289,3216,3218],{"class":292,"line":3217},31,[289,3219,3220],{},"        print(\"Invalid operation. choose either `suspend` or `resume`\")\n",[289,3222,3224],{"class":292,"line":3223},32,[289,3225,3161],{},[16,3227,3228,3229,3232,3233,3236,3237,3240,3241,2284],{},"This scripts require ",[24,3230,3231],{},"google-api-python-client==1.10.0"," library as dependancy.\nWe need to reference the ",[24,3234,3235],{},"beta"," APIs using this library.\nRead more on ",[24,3238,3239],{},"GCP beta APIs"," ",[45,3242,3245],{"href":3243,"rel":3244},"https://cloud.google.com/compute/docs/reference/rest/beta",[49],"here",[16,3247,3248,3249],{},"Keep in mind that you have to attach a service account which has compute engine permissions to start/stop/suspend/resume VM.\n",[94,3250],{"alt":3251,"src":3252},"sa","/assets/vm-auto-service-acc.webp",[16,3254,3255,3256],{},"Using cloud scheduler you can send a message to pubsub on a cron schedule.\n",[94,3257],{"alt":3258,"src":3259},"cloud-scheduler","/assets/vm-auto-cluod-scheduler.webp",[16,3261,3262,3263],{},"In next step you can define what message to send to pub-sub.\n",[94,3264],{"alt":3032,"src":3265},"/assets/vm-auto-pub-sub.webp",[16,3267,3268,3269,3271],{},"This way you dont have to remember to suspend the VM when you are done working. ",[2952,3270],{},[94,3272],{"alt":3273,"src":3274},"gif","https://media0.giphy.com/media/26xBzL5fpjhJ9dQNa/200.webp?cid=ecf05e47wnymqyqvko2pn52q3ieue2lyhw821z1hj56yy1dl&rid=200.webp&ct=g",[29,3276,3278],{"id":3277},"refrences","Refrences",[245,3280,3281],{},[153,3282,3283],{},[45,3284,3287],{"href":3285,"rel":3286},"https://www.youtube.com/watch?v=KkKcaFp0z1s&t=699s",[49],"VM feature comparizons for top 3 clouds",[431,3289,3290],{},"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 .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":69,"searchDepth":99,"depth":99,"links":3292},[3293,3294,3295,3296],{"id":2964,"depth":99,"text":2965},{"id":2990,"depth":99,"text":2991},{"id":3025,"depth":99,"text":3026},{"id":3277,"depth":99,"text":3278},"Cloud","2021-10-04T07:00:13.392Z","Maximize cost savings on your Google Compute Engine Instances with this guide on scheduling your VMs. Discover how to manage your virtual machine state, utilize the pay-as-you-go model, and implement a solution using cloud scheduler and cloud functions.","/assets/vm-auto-header.webp",{},"/blog/gcp-suspend-resume-vm",{"title":2935,"description":3299},"blog/gcp-suspend-resume-vm",[117,3306],"cloud","Vf6v1KXao0QXKnzOic6YZw8w0D6jsLlR4tETJBThoU4",{"id":3309,"title":3310,"author":7,"authorTitle":8,"body":3311,"category":3297,"createdAt":3789,"description":3790,"extension":107,"image":2551,"meta":3791,"navigation":110,"path":3792,"proficiency":112,"published":167,"readingTime":3793,"seo":3794,"stem":3795,"tags":3796,"updatedAt":118,"__hash__":3798},"blog/blog/generic-product-price-monitor.md","Product price monitor for any website.",{"type":10,"value":3312,"toc":3787},[3313,3323,3327,3330,3333,3336,3339,3345,3350,3353,3358,3361,3367,3395,3398,3415,3420,3426,3433,3439,3478,3492,3509,3518,3529,3532,3561,3567,3692,3697,3770,3785],[13,3314,3315],{},[16,3316,3317,3319,3320,3322],{},[19,3318,21],{}," Tired of manually checking product prices? This post walks through building a custom Python script that monitors prices on any website by scraping the DOM using XPath selectors and the ",[24,3321,134],{}," module. The script runs every 30 minutes, compares the current price against your target, and sends a Slack alert when the price drops. It's a practical alternative to browser extensions that require you to keep checking email notifications.",[137,3324,3326],{"id":3325},"monitor-product-price-for-any-website","Monitor product price for any website.",[16,3328,3329],{},"In this post I am going to discuss the custom solution I built to monitor the product price of any website.",[16,3331,3332],{},"I was searching for a good laptop for my sister. I had shortlisted 2 options. When looking at price fluctuations using browser extenstion, It seemed that price has chances of coming down.",[16,3334,3335],{},"The browser plugin had an option of sending email, But keep checking email for just this seemed to be a bit too much.",[16,3337,3338],{},"So I decided to make a custom script that will do the monitoring every 30 mins and send slack alert.",[16,3340,3341,3342,3344],{},"If you look at any HTML webpage it follows a Document Object Model (DOM) structure. ",[2952,3343],{},"\nHead section contains scripts and links to other required assets. And body contains elements on page. Every element has an abosulte path. We need to look for path of product price element.",[16,3346,3347],{},[289,3348,3349],{},"BASIC DOM IMAGE HTML AND BODY",[16,3351,3352],{},"To do that you can inspect that element by right clicking on it. Right click on element and copy Xpath.",[16,3354,3355],{},[289,3356,3357],{},"RIGHT CLICK ON PRICE IMAGE",[16,3359,3360],{},"Now we know the Xpath of element, Its just matter of making the request and searching the element from it.",[16,3362,3363,3364,3366],{},"For fetching the webpage you can use ",[24,3365,134],{}," module in python.",[61,3368,3371],{"className":3044,"code":3369,"highlights":3370,"language":3047,"meta":69,"style":69},"import requests\n\nurl = \"https://www.amazon.in/replace-with-valid-link\"\nwebpage = requests.get(url)\n",[281,282,283,284],[24,3372,3373,3379,3383,3389],{"__ignoreMap":69},[289,3374,3376],{"class":3375,"line":281},[292,293],[289,3377,3378],{},"import requests\n",[289,3380,3381],{"class":292,"line":99},[289,3382,3072],{"emptyLinePlaceholder":110},[289,3384,3386],{"class":3385,"line":282},[292,293],[289,3387,3388],{},"url = \"https://www.amazon.in/replace-with-valid-link\"\n",[289,3390,3392],{"class":3391,"line":283},[292,293],[289,3393,3394],{},"webpage = requests.get(url)\n",[16,3396,3397],{},"But if you save this webpage as html and look at this in any browser, It will not be the actual product page.",[61,3399,3402],{"className":3044,"code":3400,"highlights":3401,"language":3047,"meta":69,"style":69},"with open('product.html', 'w') as file:\n    file.write(wepage.text)\n",[281,282,283,284],[24,3403,3404,3410],{"__ignoreMap":69},[289,3405,3407],{"class":3406,"line":281},[292,293],[289,3408,3409],{},"with open('product.html', 'w') as file:\n",[289,3411,3412],{"class":292,"line":99},[289,3413,3414],{},"    file.write(wepage.text)\n",[16,3416,3417],{},[289,3418,3419],{},"AMAZON CAPTCHA PAGE",[16,3421,3422,3423,3425],{},"This shows how ecommerce websites or any other backend server analyse your requests. ",[2952,3424],{},"\nAs we have not mentioned any headers in our request, Server is not sure if request is coming from human behind browser, or a bot running the script.",[16,3427,3428,3429,3432],{},"Browsers add ",[24,3430,3431],{},"User-Agent"," header in the request to let server know request has come from a browser.",[16,3434,3435,3436,3438],{},"So the trick is to add ",[24,3437,3431],{}," in the request, to make serer believe request is from browser and get the webpage without any catpcha.",[61,3440,3443],{"className":3044,"code":3441,"highlights":3442,"language":3047,"meta":69,"style":69},"HEADERS = ({'User-Agent':\n            'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 \\\n            (KHTML, like Gecko) Chrome/44.0.2403.157 Safari/537.36',\\\n            'Accept-Language': 'en-US, en;q=0.5'})\n\nwebpage = requests.get(url, headers=HEADERS)\n",[281,282,283,284],[24,3444,3445,3451,3456,3462,3468,3473],{"__ignoreMap":69},[289,3446,3448],{"class":3447,"line":281},[292,293],[289,3449,3450],{},"HEADERS = ({'User-Agent':\n",[289,3452,3453],{"class":292,"line":99},[289,3454,3455],{},"            'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 \\\n",[289,3457,3459],{"class":3458,"line":282},[292,293],[289,3460,3461],{},"            (KHTML, like Gecko) Chrome/44.0.2403.157 Safari/537.36',\\\n",[289,3463,3465],{"class":3464,"line":283},[292,293],[289,3466,3467],{},"            'Accept-Language': 'en-US, en;q=0.5'})\n",[289,3469,3471],{"class":3470,"line":284},[292,293],[289,3472,3072],{"emptyLinePlaceholder":110},[289,3474,3475],{"class":292,"line":1503},[289,3476,3477],{},"webpage = requests.get(url, headers=HEADERS)\n",[16,3479,3480,3481,3483,3484,3487,3488,3491],{},"Now we have the actual webpage with product price info. ",[2952,3482],{},"\nTo parse the DOM structure, we can use ",[24,3485,3486],{},"BeautifulSoup"," and ",[24,3489,3490],{},"lxml",",",[61,3493,3496],{"className":3044,"code":3494,"highlights":3495,"language":3047,"meta":69,"style":69},"soup = BeautifulSoup(webpage.content, \"html.parser\")\ndom = etree.HTML(str(soup))\n",[281,282,283,284],[24,3497,3498,3504],{"__ignoreMap":69},[289,3499,3501],{"class":3500,"line":281},[292,293],[289,3502,3503],{},"soup = BeautifulSoup(webpage.content, \"html.parser\")\n",[289,3505,3506],{"class":292,"line":99},[289,3507,3508],{},"dom = etree.HTML(str(soup))\n",[16,3510,3511,3512,3514,3515,2284],{},"Now to find the product price element, we can use the XPATH that we copied previously. ",[2952,3513],{},"\nwe can strip extra characters like ",[24,3516,3517],{},"₹",[61,3519,3522],{"className":3044,"code":3520,"highlights":3521,"language":3047,"meta":69,"style":69},"price  = dom.xpath(xPath)[0].text.strip('₹')\n",[281,282,283,284],[24,3523,3524],{"__ignoreMap":69},[289,3525,3527],{"class":3526,"line":281},[292,293],[289,3528,3520],{},[16,3530,3531],{},"Now we have the price we can compare it with threshold and send slack(I preffer this) or email.",[61,3533,3536],{"className":3044,"code":3534,"highlights":3535,"language":3047,"meta":69,"style":69},"Fprice = float(price)\nFthresholdPrice = float(thresholdPrice)\nif Fprice \u003C FthresholdPrice:\n    alert_me(url,price)\n",[281,282,283,284],[24,3537,3538,3544,3549,3555],{"__ignoreMap":69},[289,3539,3541],{"class":3540,"line":281},[292,293],[289,3542,3543],{},"Fprice = float(price)\n",[289,3545,3546],{"class":292,"line":99},[289,3547,3548],{},"FthresholdPrice = float(thresholdPrice)\n",[289,3550,3552],{"class":3551,"line":282},[292,293],[289,3553,3554],{},"if Fprice \u003C FthresholdPrice:\n",[289,3556,3558],{"class":3557,"line":283},[292,293],[289,3559,3560],{},"    alert_me(url,price)\n",[16,3562,3563,3564],{},"For alerting I have written helper functions for email and slack as below.\n",[19,3565,3566],{},"Slack",[61,3568,3571],{"className":3044,"code":3569,"highlights":3570,"language":3047,"meta":69,"style":69},"def slack_alert(URL,price):\n    title =  f'Price fell down to {price}'\n    message = 'Buy it now here: '+URL\n    slack_msg = {\n        \"username\": \"BOT_NAME\",\n        \"icon_emoji\": \":tada:\",\n        \"channel\": \"SLACK_CHANNEL_NAME\",\n        \"attachments\": [\n                {\n                    \"color\": \"#75e403\",\n                    \"fields\": [\n                        {\n                            \"title\": title,\n                            \"value\": message,\n                            \"short\": \"false\",\n                        }\n                    ]}\n        ]}\n    byte_length = str(sys.getsizeof(slack_msg))\n    headers = {\"Content-Type\": \"application/json\",\n               \"Content-Length\":     byte_length}\n    response = requests.post(webhook_url, data=json.dumps(\n        slack_msg),    headers=headers)\n\n",[281,282,283,284],[24,3572,3573,3579,3584,3590,3596,3602,3607,3612,3617,3622,3627,3632,3637,3642,3647,3652,3657,3662,3667,3672,3677,3682,3687],{"__ignoreMap":69},[289,3574,3576],{"class":3575,"line":281},[292,293],[289,3577,3578],{},"def slack_alert(URL,price):\n",[289,3580,3581],{"class":292,"line":99},[289,3582,3583],{},"    title =  f'Price fell down to {price}'\n",[289,3585,3587],{"class":3586,"line":282},[292,293],[289,3588,3589],{},"    message = 'Buy it now here: '+URL\n",[289,3591,3593],{"class":3592,"line":283},[292,293],[289,3594,3595],{},"    slack_msg = {\n",[289,3597,3599],{"class":3598,"line":284},[292,293],[289,3600,3601],{},"        \"username\": \"BOT_NAME\",\n",[289,3603,3604],{"class":292,"line":1503},[289,3605,3606],{},"        \"icon_emoji\": \":tada:\",\n",[289,3608,3609],{"class":292,"line":1511},[289,3610,3611],{},"        \"channel\": \"SLACK_CHANNEL_NAME\",\n",[289,3613,3614],{"class":292,"line":1524},[289,3615,3616],{},"        \"attachments\": [\n",[289,3618,3619],{"class":292,"line":1537},[289,3620,3621],{},"                {\n",[289,3623,3624],{"class":292,"line":1550},[289,3625,3626],{},"                    \"color\": \"#75e403\",\n",[289,3628,3629],{"class":292,"line":1563},[289,3630,3631],{},"                    \"fields\": [\n",[289,3633,3634],{"class":292,"line":1574},[289,3635,3636],{},"                        {\n",[289,3638,3639],{"class":292,"line":1580},[289,3640,3641],{},"                            \"title\": title,\n",[289,3643,3644],{"class":292,"line":1586},[289,3645,3646],{},"                            \"value\": message,\n",[289,3648,3649],{"class":292,"line":1592},[289,3650,3651],{},"                            \"short\": \"false\",\n",[289,3653,3654],{"class":292,"line":3128},[289,3655,3656],{},"                        }\n",[289,3658,3659],{"class":292,"line":3134},[289,3660,3661],{},"                    ]}\n",[289,3663,3664],{"class":292,"line":3140},[289,3665,3666],{},"        ]}\n",[289,3668,3669],{"class":292,"line":3146},[289,3670,3671],{},"    byte_length = str(sys.getsizeof(slack_msg))\n",[289,3673,3674],{"class":292,"line":3152},[289,3675,3676],{},"    headers = {\"Content-Type\": \"application/json\",\n",[289,3678,3679],{"class":292,"line":3158},[289,3680,3681],{},"               \"Content-Length\":     byte_length}\n",[289,3683,3684],{"class":292,"line":3164},[289,3685,3686],{},"    response = requests.post(webhook_url, data=json.dumps(\n",[289,3688,3689],{"class":292,"line":3170},[289,3690,3691],{},"        slack_msg),    headers=headers)\n",[16,3693,3694],{},[19,3695,3696],{},"Email",[61,3698,3701],{"className":3044,"code":3699,"highlights":3700,"language":3047,"meta":69,"style":69},"def email_alert(URL, price):\n    server = smtplib.SMTP('smtp.gmail.com',587)\n    server.ehlo()\n    server.starttls()\n    server.ehlo()\n\n    server.login('your-email@gmail.com','GOOGLE_APP_PASSWORD')\n    subject = f'Price fell down to {price}'\n    body = 'Buy it now here: '+URL\n    msg = f\"Subject:{subject}\\n\\n{body}\"\n    server.sendmail('your-email@gmail.com','your-email@gmail.com',msg)\n    print('Email alert sent')\n    server.quit()\n",[281,282,283,284],[24,3702,3703,3709,3714,3720,3726,3731,3735,3740,3745,3750,3755,3760,3765],{"__ignoreMap":69},[289,3704,3706],{"class":3705,"line":281},[292,293],[289,3707,3708],{},"def email_alert(URL, price):\n",[289,3710,3711],{"class":292,"line":99},[289,3712,3713],{},"    server = smtplib.SMTP('smtp.gmail.com',587)\n",[289,3715,3717],{"class":3716,"line":282},[292,293],[289,3718,3719],{},"    server.ehlo()\n",[289,3721,3723],{"class":3722,"line":283},[292,293],[289,3724,3725],{},"    server.starttls()\n",[289,3727,3729],{"class":3728,"line":284},[292,293],[289,3730,3719],{},[289,3732,3733],{"class":292,"line":1503},[289,3734,3072],{"emptyLinePlaceholder":110},[289,3736,3737],{"class":292,"line":1511},[289,3738,3739],{},"    server.login('your-email@gmail.com','GOOGLE_APP_PASSWORD')\n",[289,3741,3742],{"class":292,"line":1524},[289,3743,3744],{},"    subject = f'Price fell down to {price}'\n",[289,3746,3747],{"class":292,"line":1537},[289,3748,3749],{},"    body = 'Buy it now here: '+URL\n",[289,3751,3752],{"class":292,"line":1550},[289,3753,3754],{},"    msg = f\"Subject:{subject}\\n\\n{body}\"\n",[289,3756,3757],{"class":292,"line":1563},[289,3758,3759],{},"    server.sendmail('your-email@gmail.com','your-email@gmail.com',msg)\n",[289,3761,3762],{"class":292,"line":1574},[289,3763,3764],{},"    print('Email alert sent')\n",[289,3766,3767],{"class":292,"line":1580},[289,3768,3769],{},"    server.quit()\n",[16,3771,3772,3773,3240,3778,3780,3781,2284],{},"To learn more on how to get google app password, check this ",[45,3774,3777],{"href":3775,"rel":3776},"https://www.youtube.com/watch?v=J4CtP1MBtOE",[49],"link",[2952,3779],{},"\nTo learn more on how slack webhooks are generated, check this ",[45,3782,3777],{"href":3783,"rel":3784},"https://www.youtube.com/watch?v=mCyf1gYkoMs",[49],[431,3786,3290],{},{"title":69,"searchDepth":99,"depth":99,"links":3788},[],"2021-09-17T07:00:13.392Z","Product price monitor for any website. Learn how to build a custom solution to monitor product prices on any website and receive Slack alerts",{},"/blog/generic-product-price-monitor","9 min read",{"title":3310,"description":3790},"blog/generic-product-price-monitor",[117,3797],"automate","kjm_2FOR6q6oKehdL0IOj3pRCwgwnFHSYNZUeIYPbqc",{"id":3800,"title":3801,"author":7,"authorTitle":8,"body":3802,"category":161,"createdAt":4088,"description":4089,"extension":107,"image":4090,"meta":4091,"navigation":110,"path":4092,"proficiency":112,"published":110,"readingTime":2240,"seo":4093,"stem":4094,"tags":4095,"updatedAt":118,"__hash__":4097},"blog/blog/git-bisect-to-find-buggy-commit.md","Git bisect is here to save your day",{"type":10,"value":3803,"toc":4081},[3804,3815,3818,3821,3825,3828,3832,3840,3844,3851,3856,3861,3966,3981,3986,3991,4010,4017,4022,4027,4045,4049,4054,4072,4075,4078],[13,3805,3806],{},[16,3807,3808,3810,3811,3814],{},[19,3809,21],{}," When a bug appears and you don't know which commit caused it, ",[24,3812,3813],{},"git bisect"," uses binary search to find the culprit in logarithmic time. You mark a known good commit and a known bad commit, and Git checks out the midpoint for you to test. After a few iterations, it identifies the exact breaking commit. This post walks through a practical example of finding a broken navigation link, including gotchas to watch out for.",[137,3816,3801],{"id":3817},"git-bisect-is-here-to-save-your-day",[16,3819,3820],{},"This blog discusses how we can use git bisect command to find commit that has introduced bug recently.",[29,3822,3824],{"id":3823},"what-is-git-bisect","What is git bisect ?",[16,3826,3827],{},"Git command that uses binary search to find the commit that introduced a bug.",[29,3829,3831],{"id":3830},"when-to-use-git-bisect","When to use git bisect ?",[150,3833,3834,3837],{},[153,3835,3836],{},"You can use git bisect to find out which commit caused the bug",[153,3838,3839],{},"You might be looking for the commit that introduced a particular fix/feature, I this caseyou can use the terms \"old\" and \"new\", respectively, in place of \"good\" and \"bad\". git bisect will report which commit introduced the feature/fix",[29,3841,3843],{"id":3842},"how-to-use-git-bisect","How to use git bisect ?",[16,3845,3846,3847,3850],{},"I will take very easy example of a button that is supposed to navigate user to ",[24,3848,3849],{},"/blog"," path.\nBut in recent commits this is broken.\nLets find out the commit which broke it.\nHere is the recent commit history",[16,3852,3853],{},[94,3854],{"alt":96,"src":3855},"/assets/git-bisect-commits.webp",[150,3857,3858],{},[153,3859,3860],{},"You need to tell git you want to start bisect, then you need to provide which is the bad commit (recent one most times) and which was the good commit.",[61,3862,3865],{"className":278,"code":3863,"highlights":3864,"language":285,"meta":69,"style":69},"$ git bisect start\n$ git bisect bad\n/* I have not provided any bad commit,\n   So git will consider latest as bad commit\n*/\n$ git bisect good db32414\n",[281,282,283,284],[24,3866,3867,3881,3892,3919,3944,3952],{"__ignoreMap":69},[289,3868,3870,3872,3875,3878],{"class":3869,"line":281},[292,293],[289,3871,297],{"class":296},[289,3873,3874],{"class":300}," git",[289,3876,3877],{"class":300}," bisect",[289,3879,3880],{"class":300}," start\n",[289,3882,3883,3885,3887,3889],{"class":292,"line":99},[289,3884,297],{"class":296},[289,3886,3874],{"class":300},[289,3888,3877],{"class":300},[289,3890,3891],{"class":300}," bad\n",[289,3893,3895,3898,3901,3904,3907,3910,3913,3916],{"class":3894,"line":282},[292,293],[289,3896,3897],{"class":296},"/*",[289,3899,3900],{"class":300}," I",[289,3902,3903],{"class":300}," have",[289,3905,3906],{"class":300}," not",[289,3908,3909],{"class":300}," provided",[289,3911,3912],{"class":300}," any",[289,3914,3915],{"class":300}," bad",[289,3917,3918],{"class":300}," commit,\n",[289,3920,3922,3925,3927,3930,3933,3936,3939,3941],{"class":3921,"line":283},[292,293],[289,3923,3924],{"class":296},"   So",[289,3926,3874],{"class":300},[289,3928,3929],{"class":300}," will",[289,3931,3932],{"class":300}," consider",[289,3934,3935],{"class":300}," latest",[289,3937,3938],{"class":300}," as",[289,3940,3915],{"class":300},[289,3942,3943],{"class":300}," commit\n",[289,3945,3947,3949],{"class":3946,"line":284},[292,293],[289,3948,1696],{"class":311},[289,3950,3951],{"class":1456},"/\n",[289,3953,3954,3956,3958,3960,3963],{"class":292,"line":1503},[289,3955,297],{"class":296},[289,3957,3874],{"class":300},[289,3959,3877],{"class":300},[289,3961,3962],{"class":300}," good",[289,3964,3965],{"class":300}," db32414\n",[150,3967,3968,3978],{"start":99},[153,3969,3970,3971,571,3974,3977],{},"Git bisect will pick a commit between those two endpoints and ask you whether the selected commit is \"good\" or \"bad\". Test if the bug is present or not and update same with ",[24,3972,3973],{},"git bisect bad",[24,3975,3976],{},"git bisect good"," command.",[153,3979,3980],{},"Keep following step 2. Git bisect will continue narrowing down the range until it finds the exact commit that introduced the change.",[16,3982,3983],{},[94,3984],{"alt":96,"src":3985},"/assets/git-bisect-log.webp",[150,3987,3988],{"start":283},[153,3989,3990],{},"Now you can check files changed in this commit and easily find out what caused it.",[61,3992,3995],{"className":278,"code":3993,"highlights":3994,"language":285,"meta":69,"style":69},"$ git show commitID\n",[281,282,283,284],[24,3996,3997],{"__ignoreMap":69},[289,3998,4000,4002,4004,4007],{"class":3999,"line":281},[292,293],[289,4001,297],{"class":296},[289,4003,3874],{"class":300},[289,4005,4006],{"class":300}," show",[289,4008,4009],{"class":300}," commitID\n",[16,4011,4012,4013,4016],{},"I can see this commit has changed the ",[24,4014,4015],{},"to"," path to incorrect value.",[16,4018,4019],{},[94,4020],{"alt":96,"src":4021},"/assets/git-bisect-buggy-commit.webp",[150,4023,4024],{"start":284},[153,4025,4026],{},"When you are done you need to tell git to stop the bisect process",[61,4028,4031],{"className":278,"code":4029,"highlights":4030,"language":285,"meta":69,"style":69},"$ git bisect reset\n",[281,282,283,284],[24,4032,4033],{"__ignoreMap":69},[289,4034,4036,4038,4040,4042],{"class":4035,"line":281},[292,293],[289,4037,297],{"class":296},[289,4039,3874],{"class":300},[289,4041,3877],{"class":300},[289,4043,4044],{"class":300}," reset\n",[29,4046,4048],{"id":4047},"gotchas","Gotchas",[150,4050,4051],{},[153,4052,4053],{},"There can be a case in git bisect when your commit is faling build process and you are not able to test if commit is bad or good, But you know this commit is nothing to do with the bug, You can skip this commit and move to next one.",[61,4055,4058],{"className":278,"code":4056,"highlights":4057,"language":285,"meta":69,"style":69},"$ git bisect skip\n",[281,282,283,284],[24,4059,4060],{"__ignoreMap":69},[289,4061,4063,4065,4067,4069],{"class":4062,"line":281},[292,293],[289,4064,297],{"class":296},[289,4066,3874],{"class":300},[289,4068,3877],{"class":300},[289,4070,4071],{"class":300}," skip\n",[29,4073,4074],{"id":1780},"Conclusion",[16,4076,4077],{},"If you use git bisect, you can save a lot of time (that you will spend into debugging).\nand narrow down the code you need to check to resolve bug.",[431,4079,4080],{},"html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}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 .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":69,"searchDepth":99,"depth":99,"links":4082},[4083,4084,4085,4086,4087],{"id":3823,"depth":99,"text":3824},{"id":3830,"depth":99,"text":3831},{"id":3842,"depth":99,"text":3843},{"id":4047,"depth":99,"text":4048},{"id":1780,"depth":99,"text":4074},"2021-03-10T07:00:13.392Z","Learn how to use Git Bisect to find the commit that introduced a bug in your code. A step by step guide with easy example, gotchas and conclusion. Save time in debugging with Git Bisect","/assets/git-bisect.webp",{},"/blog/git-bisect-to-find-buggy-commit",{"title":3801,"description":4089},"blog/git-bisect-to-find-buggy-commit",[117,4096],"git","iw4Z3Pt1BfXYvZ-M7oXBFsKCni4elZ6JFzbHR31bOdU",{"id":4099,"title":4100,"author":7,"authorTitle":8,"body":4101,"category":4255,"createdAt":4256,"description":4257,"extension":107,"image":4258,"meta":4259,"navigation":110,"path":4260,"proficiency":448,"published":110,"readingTime":965,"seo":4261,"stem":4262,"tags":4263,"updatedAt":118,"__hash__":4266},"blog/blog/git-template-files.md","Standardizing Git Workflow - Commit Templates, PR Templates, and More",{"type":10,"value":4102,"toc":4249},[4103,4110,4114,4117,4121,4124,4127,4141,4144,4150,4154,4157,4160,4174,4177,4183,4187,4190,4193,4207,4210,4216,4220,4223,4226,4234,4237,4243,4246],[13,4104,4105],{},[16,4106,4107,4109],{},[19,4108,21],{}," Git templates bring consistency to your development workflow. This post covers four types: commit message templates (structured format with summary, description, and references), PR templates (standardized review checklists), issue templates (separate formats for bugs vs features), and release templates (changelogs with categorized changes). Each section includes real examples and explains how templates save time, reduce back-and-forth, and improve team collaboration.",[137,4111,4113],{"id":4112},"standardizing-git-workflow-commit-templates-pr-templates-and-more","Standardizing Git Workflow: Commit Templates, PR Templates, and More",[16,4115,4116],{},"In Git, there are several templates available that can greatly improve your development workflow. Let's explore some of these templates and understand how they can save time and enhance collaboration.",[29,4118,4120],{"id":4119},"commit-templates-structured-commit-messages","Commit Templates: Structured Commit Messages",[16,4122,4123],{},"A commit template is a file that provides a predefined structure for your commit messages. By using commit templates, you can ensure consistent formatting and information in each commit message. This helps in better understanding the changes made and improves project maintainability.",[16,4125,4126],{},"Examples where commit templates save time include:",[150,4128,4129,4135],{},[153,4130,4131,4134],{},[19,4132,4133],{},"Feature Development",": When working on a feature, using a commit template allows you to provide clear and concise information about the purpose of the commit, the changes made, and any related references. This makes it easier for other developers to review and understand your commits.",[153,4136,4137,4140],{},[19,4138,4139],{},"Bug Fixes",": When fixing a bug, a commit template can prompt you to include details about the bug, steps to reproduce it, and the solution applied. This comprehensive information helps in tracking down the bug's source and verifying the fix.",[16,4142,4143],{},"example git commit template: (.git/.commit-msg-template)",[61,4145,4148],{"className":4146,"code":4147,"language":66},[64],"[Feature] Add user authentication\n\nSummary:\nProvide user authentication functionality for secure access to the application.\n\nDescription:\n- Implemented user registration and login endpoints.\n- Added password hashing for improved security.\n- Integrated authentication middleware for protecting sensitive routes.\n\nReferences: #123, #456\n",[24,4149,4147],{"__ignoreMap":69},[29,4151,4153],{"id":4152},"pr-templates-clear-pull-request-descriptions","PR Templates: Clear Pull Request Descriptions",[16,4155,4156],{},"PR templates provide a predefined structure for pull request descriptions. When creating a pull request, using a PR template ensures that you include essential information, such as a summary of the changes, the problem being addressed, the proposed solution, and any relevant context or documentation.",[16,4158,4159],{},"Examples where PR templates save time include:",[150,4161,4162,4168],{},[153,4163,4164,4167],{},[19,4165,4166],{},"Code Reviews",": A well-structured PR template assists reviewers by providing all the necessary information about the changes made. Reviewers can quickly understand the purpose of the PR, review the diff, and provide valuable feedback without spending extra time gathering information.",[153,4169,4170,4173],{},[19,4171,4172],{},"Change Management",": In projects with multiple stakeholders, PR templates help in efficient change management. By including details about the purpose, impact, and risks associated with the changes, stakeholders can make informed decisions and give appropriate approvals.",[16,4175,4176],{},"example PR template file: (.github/PULL_REQUEST_TEMPLATE.md)",[61,4178,4181],{"className":4179,"code":4180,"language":66},[64],"## Summary\nProvide a brief overview of the changes made in this pull request.\n\n## Problem\nDescribe the issue or problem this PR aims to solve.\n\n## Solution\nExplain the approach taken to address the problem.\n\n## Changes Made\nList the specific changes made in this PR.\n\n## Related Documentation\nInclude links or references to any relevant documentation.\n\n## Checklist\n- [ ] Code has been reviewed\n- [ ] Unit tests have been added/updated\n- [ ] Documentation has been updated\n\n",[24,4182,4180],{"__ignoreMap":69},[29,4184,4186],{"id":4185},"issue-templates-structured-issue-reporting","Issue Templates: Structured Issue Reporting",[16,4188,4189],{},"Issue templates define a predefined structure for creating new issues in your project's issue tracker. By providing specific sections, such as problem description, steps to reproduce, and expected behavior, issue templates help in capturing detailed and structured information when reporting bugs or suggesting enhancements.",[16,4191,4192],{},"Examples where issue templates save time include:",[150,4194,4195,4201],{},[153,4196,4197,4200],{},[19,4198,4199],{},"Bug Reporting",": By using an issue template, users can provide all the necessary information to reproduce a bug, including specific steps, environment details, and relevant logs. This reduces back-and-forth communication and allows developers to start investigating the issue promptly.",[153,4202,4203,4206],{},[19,4204,4205],{},"Feature Requests",": Issue templates can guide users to provide comprehensive details about their feature requests, such as use cases, expected behavior, and any related dependencies. This helps in better understanding the requirements and facilitates smoother implementation.",[16,4208,4209],{},"Example issue template (issue_template.md):",[61,4211,4214],{"className":4212,"code":4213,"language":66},[64],"## Description\nProvide a clear and concise description of the issue.\n\n## Steps to Reproduce\nOutline the steps to reproduce the issue, including any specific inputs or conditions.\n\n1. \n2. \n3. \n\n## Expected Behavior\nDescribe what you expected to happen.\n\n## Actual Behavior\nExplain what actually happened.\n\n## Additional Information\nInclude any additional information or context that may be relevant.\n",[24,4215,4213],{"__ignoreMap":69},[29,4217,4219],{"id":4218},"release-templates-consistent-release-documentation","Release Templates: Consistent Release Documentation",[16,4221,4222],{},"Release templates define the structure and content of release notes or release documentation. By using release templates, you can include relevant information, such as feature highlights, bug fixes, and breaking changes, in a standardized format.",[16,4224,4225],{},"Examples where release templates save time include:",[150,4227,4228],{},[153,4229,4230,4233],{},[19,4231,4232],{},"Release Notes",": With a release template, you can consistently document the changes made in each release, making it easier for users and stakeholders to understand the updates and enhancements introduced.",[16,4235,4236],{},"Example release template (release_template.md):",[61,4238,4241],{"className":4239,"code":4240,"language":66},[64],"## Version X.Y.Z\n\n### Features\n- List notable features or enhancements added in this release.\n\n### Bug Fixes\n- Summarize the bug fixes or issues resolved in this release.\n\n### Breaking Changes\n- Identify any breaking changes or backward-incompatible modifications made.\n\n### Known Issues\n- Highlight any known issues or limitations in this release.\n\n### Documentation Updates\n- Mention any updates made to the project's documentation.\n\n### Contributors\n- Recognize the contributors who made significant contributions to this release.\n",[24,4242,4240],{"__ignoreMap":69},[16,4244,4245],{},"By utilizing these templates, you can improve your Git workflow, promote consistency, and enhance collaboration within your development team. Whether it's structured commit messages, clear pull request descriptions, well-defined issue reporting, or consistent release documentation, these templates provide a framework for efficient and effective collaboration.",[16,4247,4248],{},"Remember to customize the templates according to your project's specific needs and conventions",{"title":69,"searchDepth":99,"depth":99,"links":4250},[4251,4252,4253,4254],{"id":4119,"depth":99,"text":4120},{"id":4152,"depth":99,"text":4153},{"id":4185,"depth":99,"text":4186},{"id":4218,"depth":99,"text":4219},"Development","2023-05-28T00:00:00.000Z","Learn how to enhance your Git workflow using commit templates, PR templates, issue templates, and release templates. Discover their uses and Examples where these templates save time and improve collaboration.","/assets/git-templates.webp",{},"/blog/git-template-files",{"title":4100,"description":4257},"blog/git-template-files",[4264,4265],"Git","Version Control","um97yPWNfmZaZ5QO4qVJu2Pecd2w5LsI-EHHEQUpGdI",{"id":4268,"title":4269,"author":7,"authorTitle":8,"body":4270,"category":4255,"createdAt":4498,"description":4499,"extension":107,"image":4500,"meta":4501,"navigation":110,"path":4502,"proficiency":448,"published":110,"readingTime":2240,"seo":4503,"stem":4504,"tags":4505,"updatedAt":118,"__hash__":4509},"blog/blog/golang-unit-testing.md","Efficient Unit Testing Approach for Go in VS Code",{"type":10,"value":4271,"toc":4490},[4272,4279,4282,4285,4289,4292,4325,4407,4414,4418,4421,4441,4445,4451,4455,4461,4464,4468,4471,4485,4488],[13,4273,4274],{},[16,4275,4276,4278],{},[19,4277,21],{}," Writing unit tests in Go can be streamlined significantly with the VS Code Go extension. It auto-generates table-driven test structures from your function signatures, provides a Test UI to run and debug individual tests or entire suites, and shows code coverage with color-coded highlights (green for covered, red for uncovered). This post walks through the full workflow from generating tests to analyzing coverage results.",[137,4280,4269],{"id":4281},"efficient-unit-testing-approach-for-go-in-vs-code",[16,4283,4284],{},"In software development, unit testing plays a vital role in ensuring code quality and reliability. If you're working with the Go programming language and utilizing the Visual Studio Code (VS Code) editor, you can streamline your unit testing workflow using the Go extension. This extension provides powerful features for generating unit tests, executing tests, and visualizing test coverage, making it easier and more efficient to test your Go code.",[29,4286,4288],{"id":4287},"generating-unit-tests-with-go-extension","Generating Unit Tests with Go Extension",[16,4290,4291],{},"To generate unit tests for your Go functions using the Go extension in VS Code, follow these steps:",[150,4293,4294,4297,4300,4311,4314,4317],{},[153,4295,4296],{},"Open the Go file in which you want to generate unit tests.",[153,4298,4299],{},"Place the cursor on the function declaration or its signature.",[153,4301,4302,4303,4306,4307,4310],{},"Open the Command Palette by pressing ",[24,4304,4305],{},"Ctrl+Shift+P"," (or ",[24,4308,4309],{},"Cmd+Shift+P"," on macOS).",[153,4312,4313],{},"Search for the \"Go: Generate unit tests for function\" command and select it.",[153,4315,4316],{},"Choose the test file where the generated tests should be placed or create a new file.",[153,4318,4319,4320,4324],{},"The extension will generate a table test structure with a placeholder test case. Add your test cases by providing input values and expected output.\n",[94,4321],{"alt":4322,"src":4323},"generate tests","/assets/generate-tests.png","\nHere's an example of a generated table test structure:",[61,4326,4330],{"className":4327,"code":4328,"language":4329,"meta":69,"style":69},"language-go shiki shiki-themes github-light github-dark","package main\n\nimport \"testing\"\n\nfunc Test_main(t *testing.T) {\n    tests := []struct {\n        name string\n    }{\n        // TODO: Add test cases.\n    }\n    for _, tt := range tests {\n        t.Run(tt.name, func(t *testing.T) {\n            main()\n        })\n    }\n}\n","go",[24,4331,4332,4337,4341,4346,4350,4355,4360,4365,4370,4375,4379,4384,4389,4394,4399,4403],{"__ignoreMap":69},[289,4333,4334],{"class":292,"line":281},[289,4335,4336],{},"package main\n",[289,4338,4339],{"class":292,"line":99},[289,4340,3072],{"emptyLinePlaceholder":110},[289,4342,4343],{"class":292,"line":282},[289,4344,4345],{},"import \"testing\"\n",[289,4347,4348],{"class":292,"line":283},[289,4349,3072],{"emptyLinePlaceholder":110},[289,4351,4352],{"class":292,"line":284},[289,4353,4354],{},"func Test_main(t *testing.T) {\n",[289,4356,4357],{"class":292,"line":1503},[289,4358,4359],{},"    tests := []struct {\n",[289,4361,4362],{"class":292,"line":1511},[289,4363,4364],{},"        name string\n",[289,4366,4367],{"class":292,"line":1524},[289,4368,4369],{},"    }{\n",[289,4371,4372],{"class":292,"line":1537},[289,4373,4374],{},"        // TODO: Add test cases.\n",[289,4376,4377],{"class":292,"line":1550},[289,4378,1583],{},[289,4380,4381],{"class":292,"line":1563},[289,4382,4383],{},"    for _, tt := range tests {\n",[289,4385,4386],{"class":292,"line":1574},[289,4387,4388],{},"        t.Run(tt.name, func(t *testing.T) {\n",[289,4390,4391],{"class":292,"line":1580},[289,4392,4393],{},"            main()\n",[289,4395,4396],{"class":292,"line":1586},[289,4397,4398],{},"        })\n",[289,4400,4401],{"class":292,"line":1592},[289,4402,1583],{},[289,4404,4405],{"class":292,"line":3128},[289,4406,1595],{},[16,4408,4409,4410,4413],{},"Replace the ",[24,4411,4412],{},"TODO: Add test cases."," comment with your actual test cases, providing different input values and asserting the expected output. Repeat this process for other functions you want to test.",[29,4415,4417],{"id":4416},"running-tests-and-viewing-coverage","Running Tests and Viewing Coverage",[16,4419,4420],{},"After writing your unit tests, you can use the Test UI provided by the Go extension in VS Code to run the tests and visualize the coverage results. Follow these steps:",[150,4422,4423,4426,4429,4432,4435,4438],{},[153,4424,4425],{},"Open the Go file containing your unit tests in VS Code.",[153,4427,4428],{},"Open the Command Palette.",[153,4430,4431],{},"Search for the \"Go: Toggle test coverage for this file\" command and select it.",[153,4433,4434],{},"The Test UI will open, displaying the test cases and their execution status.",[153,4436,4437],{},"Click the \"Run All Tests\" button to execute all the tests in the file.",[153,4439,4440],{},"Once the tests finish running, the coverage results will be displayed alongside the source code, with covered lines highlighted.",[1981,4442,4444],{"id":4443},"test-ui","Test UI",[16,4446,4447],{},[94,4448],{"alt":4449,"src":4450},"test main","/assets/test-main.png",[1981,4452,4454],{"id":4453},"view-coverage","View coverage",[16,4456,4457],{},[94,4458],{"alt":4459,"src":4460},"toggle coverage","/assets/toggle-coverage.png",[16,4462,4463],{},"Reviewing the coverage results allows you to identify areas of your code that may need additional testing or that lack adequate coverage.",[29,4465,4467],{"id":4466},"benefits-of-this-approach","Benefits of This Approach",[16,4469,4470],{},"Using the Go extension in VS Code for unit testing provides several benefits:",[150,4472,4473,4476,4479,4482],{},[153,4474,4475],{},"Efficient Test Generation: The \"Go: Generate unit tests for function\" command quickly generates test functions in a table format, reducing manual effort and ensuring consistent test structure.",[153,4477,4478],{},"Convenient Test Execution: The Test UI provided by the Go extension allows you to run your tests without leaving the editor, providing a seamless testing experience.",[153,4480,4481],{},"Coverage Visualization: The coverage results displayed in the editor help you understand which parts of your code are covered by tests, enabling you to identify areas that require more thorough testing.",[153,4483,4484],{},"Improved Code Quality: By adopting an efficient unit testing approach, you can catch bugs and issues early in the development process, resulting in higher code quality and more reliable software.",[16,4486,4487],{},"With the combination of the Go extension's test generation capabilities, the Test UI, and coverage visualization, you can streamline your unit testing workflow and ensure robust and well-tested Go code.",[431,4489,3290],{},{"title":69,"searchDepth":99,"depth":99,"links":4491},[4492,4493,4497],{"id":4287,"depth":99,"text":4288},{"id":4416,"depth":99,"text":4417,"children":4494},[4495,4496],{"id":4443,"depth":282,"text":4444},{"id":4453,"depth":282,"text":4454},{"id":4466,"depth":99,"text":4467},"2023-05-29T00:00:00.000Z","Learn how to streamline your unit testing workflow in Go using the Go extension for Visual Studio Code. Generate unit tests with ease and leverage the Test UI for convenient test execution and coverage visualization.","/assets/golang-unit-testing.webp",{},"/blog/golang-unit-testing",{"title":4269,"description":4499},"blog/golang-unit-testing",[4506,4507,4508],"Go","Testing","VS Code","UJo_tde1ZB6zPtjJsZpRiUBy4Gdd_uWqn2CSHHPw5Ms",{"id":4511,"title":4512,"author":7,"authorTitle":8,"body":4513,"category":104,"createdAt":4990,"description":4991,"extension":107,"image":4992,"meta":4993,"navigation":110,"path":4994,"proficiency":448,"published":110,"readingTime":4995,"seo":4996,"stem":4997,"tags":4998,"updatedAt":4999,"__hash__":5000},"blog/blog/how-to-create-nuxt-blog.md","Build a blog using Nuxt and Tailwind CSS",{"type":10,"value":4514,"toc":4977},[4515,4522,4525,4528,4532,4543,4547,4550,4554,4557,4586,4593,4610,4614,4626,4632,4656,4660,4667,4673,4680,4686,4690,4693,4770,4779,4790,4843,4847,4850,4877,4881,4884,4927,4931,4939,4943,4974],[13,4516,4517],{},[16,4518,4519,4521],{},[19,4520,21],{}," A walkthrough of how this blog was built using Nuxt, Tailwind CSS, and Tailblocks for pre-made UI components. Covers creating the Nuxt app, adding the content module for Markdown-based blog posts, styling with Tailwind, and displaying blog data dynamically. The post also covers generating a static build and deploying it to Netlify for free hosting with automatic deploys.",[137,4523,4512],{"id":4524},"build-a-blog-using-nuxt-and-tailwind-css",[16,4526,4527],{},"In this post I will walk you through how I created this blog",[29,4529,4531],{"id":4530},"frameworks-used","Frameworks used",[245,4533,4534,4537,4540],{},[153,4535,4536],{},"Nuxt",[153,4538,4539],{},"Tailwind CSS",[153,4541,4542],{},"Tailblocks",[1981,4544,4546],{"id":4545},"what-is-nuxt","What is Nuxt ?",[16,4548,4549],{},"Nuxt.js is a free and open source web application framework based on Vue.js, Node.js, Webpack and Babel.js.\nYou can also create Static sites and server side rendered webapp with nuxt.\nIt abstracts most of the complex configuration involved in managing things like asynchronous data, middleware, and routing.",[29,4551,4553],{"id":4552},"create-nuxt-app","Create nuxt app",[16,4555,4556],{},"Create nuxt app using below command",[61,4558,4561],{"className":278,"code":4559,"highlights":4560,"language":285,"meta":69,"style":69},"yarn create nuxt-app \u003Cproject-name>\n",[281,282,283,284],[24,4562,4563],{"__ignoreMap":69},[289,4564,4566,4569,4572,4575,4577,4580,4583],{"class":4565,"line":281},[292,293],[289,4567,4568],{"class":296},"yarn",[289,4570,4571],{"class":300}," create",[289,4573,4574],{"class":300}," nuxt-app",[289,4576,1255],{"class":311},[289,4578,4579],{"class":300},"project-nam",[289,4581,4582],{"class":1456},"e",[289,4584,4585],{"class":311},">\n",[16,4587,4588,4589],{},"You can choose to add nuxt content module while project create.\nOr you can add it manually ",[45,4590,4591],{"href":4591,"rel":4592},"https://content.nuxtjs.org/installation",[49],[61,4594,4597],{"className":278,"code":4595,"highlights":4596,"language":285,"meta":69,"style":69},"yarn add @nuxt/content\n",[281,282,283,284],[24,4598,4599],{"__ignoreMap":69},[289,4600,4602,4604,4607],{"class":4601,"line":281},[292,293],[289,4603,4568],{"class":296},[289,4605,4606],{"class":300}," add",[289,4608,4609],{"class":300}," @nuxt/content\n",[29,4611,4613],{"id":4612},"adding-ready-to-use-css-blocks-from-tailblocks","Adding ready to use CSS blocks from Tailblocks",[16,4615,4616,4617,4621,4622,4625],{},"Navigate to ",[45,4618,4619],{"href":4619,"rel":4620},"https://tailblocks.cc/",[49],"\nChoose any header footer and content blocks you like.\nAdd these blocks to nuxt ",[24,4623,4624],{},"layouts/default.vue"," file",[61,4627,4630],{"className":4628,"code":4629,"language":66},[64],"\u003Cdiv>\n    \u003CHeader>\u003C/Header>\n    \u003CNuxt/>\n    \u003CFooter>\u003C/Footer>\n\u003C/div>\n",[24,4631,4629],{"__ignoreMap":69},[16,4633,4634,4637,4638,3487,4641,4644,4645,3487,4648,4650,4651,4653,4654],{},[24,4635,4636],{},"\u003CNuxt/>"," will render pages as per routes.\nAdd ",[24,4639,4640],{},"pages/index.vue",[24,4642,4643],{},"pages/blog/index.vue"," they will serve on ",[24,4646,4647],{},"/",[24,4649,3849],{}," route.\nPut homepage content in ",[24,4652,4640],{}," and blog layout in ",[24,4655,4643],{},[29,4657,4659],{"id":4658},"writing-content","Writing Content",[16,4661,4662,4663,4666],{},"create a ",[24,4664,4665],{},"content/"," directory in your project:",[61,4668,4671],{"className":4669,"code":4670,"language":66},[64],"content/\n  blogs/\n    article-1.md\n    article-2.md\n  home.md\n",[24,4672,4670],{"__ignoreMap":69},[16,4674,4675,4676,4679],{},"In Each ",[24,4677,4678],{},"article.md"," file you can add your content as below",[61,4681,4684],{"className":4682,"code":4683,"language":66},[64],"---\ntitle: Introduction\ndescription: Learn how to use @nuxt/content.\n---\n## Markdown content\nmore details...\n",[24,4685,4683],{"__ignoreMap":69},[29,4687,4689],{"id":4688},"display-blogs-data","Display Blogs data",[16,4691,4692],{},"You need to fetch all posts as below and display in your vue template",[61,4694,4699],{"className":4695,"code":4696,"highlights":4697,"language":4698,"meta":69,"style":69},"language-js shiki shiki-themes github-light github-dark","async  asyncData({ $content }) {\n    const  posts  =  await  $content('blog').fetch()\n    return {\n        posts,\n    }\n},\n",[281,282,283,284],"js",[24,4700,4701,4713,4745,4754,4760,4765],{"__ignoreMap":69},[289,4702,4704,4707,4710],{"class":4703,"line":281},[292,293],[289,4705,4706],{"class":1456},"async  ",[289,4708,4709],{"class":296},"asyncData",[289,4711,4712],{"class":1456},"({ $content }) {\n",[289,4714,4715,4718,4721,4724,4727,4730,4733,4736,4739,4742],{"class":292,"line":99},[289,4716,4717],{"class":311},"    const",[289,4719,4720],{"class":304},"  posts",[289,4722,4723],{"class":311},"  =",[289,4725,4726],{"class":311},"  await",[289,4728,4729],{"class":296},"  $content",[289,4731,4732],{"class":1456},"(",[289,4734,4735],{"class":300},"'blog'",[289,4737,4738],{"class":1456},").",[289,4740,4741],{"class":296},"fetch",[289,4743,4744],{"class":1456},"()\n",[289,4746,4748,4751],{"class":4747,"line":282},[292,293],[289,4749,4750],{"class":311},"    return",[289,4752,4753],{"class":1456}," {\n",[289,4755,4757],{"class":4756,"line":283},[292,293],[289,4758,4759],{"class":1456},"        posts,\n",[289,4761,4763],{"class":4762,"line":284},[292,293],[289,4764,1583],{"class":1456},[289,4766,4767],{"class":292,"line":1503},[289,4768,4769],{"class":1456},"},\n",[16,4771,4772,4775,4776],{},[24,4773,4774],{},"posts"," variable will have all posts from that folder, you can loop over and display them in any format. There are various ready to use blocks here ",[45,4777,4619],{"href":4619,"rel":4778},[49],[16,4780,4781,4782,4785,4786,4789],{},"Further you can create a ",[24,4783,4784],{},"pages/blog/_slug.vue"," this will show content for route ",[24,4787,4788],{},"/blog/article-1",".\nYou need to fetch individual post as below and display in your vue template",[61,4791,4794],{"className":4695,"code":4792,"highlights":4793,"language":4698,"meta":69,"style":69},"async  asyncData({ $content, params }) {\n    const  post  =  await  $content('blog', params.slug).fetch()\n    return { post }\n},\n",[281,282,283,284],[24,4795,4796,4806,4830,4838],{"__ignoreMap":69},[289,4797,4799,4801,4803],{"class":4798,"line":281},[292,293],[289,4800,4706],{"class":1456},[289,4802,4709],{"class":296},[289,4804,4805],{"class":1456},"({ $content, params }) {\n",[289,4807,4808,4810,4813,4815,4817,4819,4821,4823,4826,4828],{"class":292,"line":99},[289,4809,4717],{"class":311},[289,4811,4812],{"class":304},"  post",[289,4814,4723],{"class":311},[289,4816,4726],{"class":311},[289,4818,4729],{"class":296},[289,4820,4732],{"class":1456},[289,4822,4735],{"class":300},[289,4824,4825],{"class":1456},", params.slug).",[289,4827,4741],{"class":296},[289,4829,4744],{"class":1456},[289,4831,4833,4835],{"class":4832,"line":282},[292,293],[289,4834,4750],{"class":311},[289,4836,4837],{"class":1456}," { post }\n",[289,4839,4841],{"class":4840,"line":283},[292,293],[289,4842,4769],{"class":1456},[29,4844,4846],{"id":4845},"create-a-static-build","Create a static build",[16,4848,4849],{},"After you have created the blog setup, static build can be generated by using below command",[61,4851,4854],{"className":278,"code":4852,"highlights":4853,"language":285,"meta":69,"style":69},"nuxt generate //or\nnpm run generate\n",[281,282,283,284],[24,4855,4856,4868],{"__ignoreMap":69},[289,4857,4859,4862,4865],{"class":4858,"line":281},[292,293],[289,4860,4861],{"class":296},"nuxt",[289,4863,4864],{"class":300}," generate",[289,4866,4867],{"class":300}," //or\n",[289,4869,4870,4872,4874],{"class":292,"line":99},[289,4871,1137],{"class":296},[289,4873,1172],{"class":300},[289,4875,4876],{"class":300}," generate\n",[29,4878,4880],{"id":4879},"bonus-hosting-on-netlify","Bonus: Hosting on netlify",[16,4882,4883],{},"You can easily host project from github on netlify.",[245,4885,4886,4895,4898,4901,4904,4907,4916,4924],{},[153,4887,4888,4889,4894],{},"Go to ",[45,4890,4893],{"href":4891,"rel":4892},"http://www.netlify.com/",[49],"www.netlify.com"," and login or signup.",[153,4896,4897],{},"Select “New site from git”.",[153,4899,4900],{},"Choose your provider (e.g. GitHub) — You may need to authenticate here.",[153,4902,4903],{},"Select the git repository you want to create a site from.",[153,4905,4906],{},"Select the branch you want to deploy from.",[153,4908,4909,4910],{},"Choose any commands that need to be run. — ",[642,4911,4912,4913],{},"in our case its ",[24,4914,4915],{},"npm run generate",[153,4917,4918,4919],{},"Choose the directory that you will publish from. It will contain files such as index.html. — ",[642,4920,4912,4921,2284],{},[24,4922,4923],{},"dist",[153,4925,4926],{},"Select “Build Site”.",[29,4928,4930],{"id":4929},"source-code","Source code",[245,4932,4933],{},[153,4934,4935],{},[45,4936,4937],{"href":4937,"rel":4938},"https://github.com/ssghait007/blog",[49],[29,4940,4942],{"id":4941},"useful-links","Useful links",[245,4944,4945,4951,4957,4963,4968],{},[153,4946,4947],{},[45,4948,4949],{"href":4949,"rel":4950},"https://nuxtjs.org/",[49],[153,4952,4953],{},[45,4954,4955],{"href":4955,"rel":4956},"https://content.nuxtjs.org/",[49],[153,4958,4959],{},[45,4960,4961],{"href":4961,"rel":4962},"https://tailwindcss.com/",[49],[153,4964,4965],{},[45,4966,4619],{"href":4619,"rel":4967},[49],[153,4969,4970],{},[45,4971,4972],{"href":4972,"rel":4973},"https://www.netlify.com/",[49],[431,4975,4976],{},"html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}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 .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}",{"title":69,"searchDepth":99,"depth":99,"links":4978},[4979,4982,4983,4984,4985,4986,4987,4988,4989],{"id":4530,"depth":99,"text":4531,"children":4980},[4981],{"id":4545,"depth":282,"text":4546},{"id":4552,"depth":99,"text":4553},{"id":4612,"depth":99,"text":4613},{"id":4658,"depth":99,"text":4659},{"id":4688,"depth":99,"text":4689},{"id":4845,"depth":99,"text":4846},{"id":4879,"depth":99,"text":4880},{"id":4929,"depth":99,"text":4930},{"id":4941,"depth":99,"text":4942},"2021-02-01T07:00:13.392Z","Learn how to build a blog using Nuxt and Tailwind CSS. In this post, I walk you through the process of creating a blog using Nuxt, Tailwind CSS, and Tailblocks. The post covers creating the Nuxt app, adding CSS blocks, writing content, displaying blog data, creating a static build, and hosting on Netlify.","/assets/nuxt.webp",{},"/blog/how-to-create-nuxt-blog","10 min read",{"title":4512,"description":4991},"blog/how-to-create-nuxt-blog",[590,117],"2026-03-10T00:00:00.000Z","XK4fsnbmNTAC8QAbHjaAs4_xtwiz8hTg3dvJSUTRt1k",{"id":5002,"title":5003,"author":7,"authorTitle":8,"body":5004,"category":4255,"createdAt":4256,"description":5178,"extension":107,"image":5179,"meta":5180,"navigation":110,"path":5181,"proficiency":448,"published":110,"readingTime":2240,"seo":5182,"stem":5183,"tags":5184,"updatedAt":118,"__hash__":5186},"blog/blog/ignore-files.md","Ignoring Files - A Guide to .gitignore, .dockerignore, and More",{"type":10,"value":5005,"toc":5172},[5006,5032,5036,5039,5043,5048,5054,5080,5084,5093,5098,5112,5116,5122,5127,5141,5145,5148],[13,5007,5008],{},[16,5009,5010,5012,5013,5016,5017,5020,5021,5024,5025,2053,5028,5031],{},[19,5011,21],{}," Ignore files are essential but often overlooked. This post covers the purpose and syntax of ",[24,5014,5015],{},".gitignore"," (keep repos clean, protect secrets), ",[24,5018,5019],{},".dockerignore"," (reduce image size, speed up builds), ",[24,5022,5023],{},".npmignore"," (control what gets published to npm), ",[24,5026,5027],{},".eslintignore",[24,5029,5030],{},".prettierignore"," (skip files from linting/formatting). Each section explains when and why to use them with practical examples.",[137,5033,5035],{"id":5034},"ignoring-files-a-guide-to-gitignore-dockerignore-and-more","Ignoring Files: A Guide to .gitignore, .dockerignore, and More",[16,5037,5038],{},"In software development, various ignore files help in managing version control systems, optimizing Docker image builds, and enhancing development workflows. Let's explore some of the important ignore files and understand their significance.",[29,5040,5042],{"id":5041},"gitignore-ignoring-files-in-git",".gitignore: Ignoring Files in Git",[16,5044,2390,5045,5047],{},[24,5046,5015],{}," file is used in Git repositories to specify files and directories that should be ignored and not committed to the repository. It helps keep the repository clean, avoids clutter, and improves version control efficiency.",[16,5049,5050,5051,5053],{},"By using a well-defined ",[24,5052,5015],{}," file, you can:",[245,5055,5056,5062,5068,5074],{},[153,5057,5058,5061],{},[19,5059,5060],{},"Exclude build artifacts:"," Prevent generated files, such as compiled binaries, object files, or log files, from cluttering your repository and bloating its size.",[153,5063,5064,5067],{},[19,5065,5066],{},"Ignore dependencies:"," Exclude third-party libraries or dependencies installed by package managers, as these can be easily installed again by other developers using dependency management tools.",[153,5069,5070,5073],{},[19,5071,5072],{},"Protect sensitive information:"," Ensure that sensitive data, such as API keys, passwords, or configuration files containing sensitive information, are not accidentally committed to the repository and exposed to unauthorized users.",[153,5075,5076,5079],{},[19,5077,5078],{},"Improve collaboration:"," By keeping unnecessary files out of the repository, you reduce merge conflicts and make collaboration with other developers smoother and more efficient.",[29,5081,5083],{"id":5082},"dockerignore-ignoring-files-in-docker",".dockerignore: Ignoring Files in Docker",[16,5085,5086,5087,5089,5090,5092],{},"Similar to ",[24,5088,5015],{},", the ",[24,5091,5019],{}," file is used in Docker projects to specify files and directories that should be excluded when building Docker images. It plays a crucial role in optimizing Docker image builds.",[16,5094,5095,5096,5053],{},"By using a well-structured ",[24,5097,5019],{},[245,5099,5100,5106],{},[153,5101,5102,5105],{},[19,5103,5104],{},"Reduce image size:"," Exclude files and directories that are not required during runtime, such as development-specific files, temporary files, or unnecessary build artifacts. This significantly reduces the size of the Docker image, resulting in faster image transfers and improved container startup times.",[153,5107,5108,5111],{},[19,5109,5110],{},"Improve build performance:"," Ignoring irrelevant files and directories reduces the build context size, which speeds up the Docker image build process. Docker only needs to process and transfer the files specified in the build context, resulting in faster and more efficient builds.",[29,5113,5115],{"id":5114},"npmignore-ignoring-files-in-nodejs-projects",".npmignore: Ignoring Files in Node.js Projects",[16,5117,5118,5119,5121],{},"In Node.js projects, the ",[24,5120,5023],{}," file specifies files and directories that should not be included when publishing a package to the npm registry. It allows you to exclude unnecessary files such as tests, documentation, or development-specific files from being distributed with the package.",[16,5123,5124,5125,5053],{},"By using a well-configured ",[24,5126,5023],{},[245,5128,5129,5135],{},[153,5130,5131,5134],{},[19,5132,5133],{},"Exclude development files:"," Remove test files, documentation, or source files that are not necessary for the package consumers.",[153,5136,5137,5140],{},[19,5138,5139],{},"Reduce package size:"," Exclude large or unnecessary files from being distributed with the package, resulting in a smaller package size and faster installation times.",[29,5142,5144],{"id":5143},"other-important-ignore-files","Other Important Ignore Files",[16,5146,5147],{},"In addition to the aforementioned ignore files, there are other important ones used in various contexts:",[245,5149,5150,5156,5162],{},[153,5151,5152,5155],{},[19,5153,5154],{},".eslintignore:"," Used in projects that utilize ESLint, a popular JavaScript linter. It lets you specify files and directories that should be ignored while linting.",[153,5157,5158,5161],{},[19,5159,5160],{},".prettierignore:"," Used with Prettier, a code formatter that helps ensure consistent code style across a project. It allows you to specify files and directories that should not be formatted by Prettier.",[153,5163,5164,5167,5168,5171],{},[19,5165,5166],{},".gitkeep:"," This file is used to preserve empty directories in a Git repository, as Git does not track empty directories by default. Adding a ",[24,5169,5170],{},".gitkeep"," file to an otherwise empty directory allows it to be committed to the repository.",{"title":69,"searchDepth":99,"depth":99,"links":5173},[5174,5175,5176,5177],{"id":5041,"depth":99,"text":5042},{"id":5082,"depth":99,"text":5083},{"id":5114,"depth":99,"text":5115},{"id":5143,"depth":99,"text":5144},"Learn about the usage and importance of ignore files such as .gitignore, .dockerignore, .npmignore, and more. Discover how these files help in managing version control, optimizing Docker image builds, and enhancing development workflows.","/assets/ignorefiles.webp",{},"/blog/ignore-files",{"title":5003,"description":5178},"blog/ignore-files",[4264,5185],"Docker","q1hHx2b0-B_sAmJy1b2OhrH-JmlOMwaxKOPtfkRiBeY",{"id":5188,"title":5189,"author":7,"authorTitle":8,"body":5190,"category":3297,"createdAt":5302,"description":5303,"extension":107,"image":5304,"meta":5305,"navigation":110,"path":5306,"proficiency":448,"published":110,"readingTime":113,"seo":5307,"stem":5308,"tags":5309,"updatedAt":118,"__hash__":5310},"blog/blog/lambda-in-vpc.md","Lambda Function In A VPC The Right Way.",{"type":10,"value":5191,"toc":5295},[5192,5199,5202,5205,5209,5212,5215,5218,5222,5225,5229,5232,5235,5249,5252,5255,5261,5269,5273,5284,5288],[13,5193,5194],{},[16,5195,5196,5198],{},[19,5197,21],{}," When you associate an AWS Lambda function with a VPC, it silently loses internet access -- all external API calls will time out. This happens because Lambda functions don't get public IPs, so they can't route through the Internet Gateway. The fix: place your Lambda in a private subnet and route its traffic through a NAT Gateway (which does have a public IP via an Elastic Network Interface). This post covers the problem, the networking explanation, and step-by-step setup with public/private subnets and route tables.",[137,5200,5189],{"id":5201},"lambda-function-in-a-vpc-the-right-way",[16,5203,5204],{},"In this post I have added my experience of working with lambda function in a VPC.",[29,5206,5208],{"id":5207},"when-you-associate-lambda-function-to-a-vpc-it-loses-internet-access","When you associate lambda function to a VPC, it loses internet access.",[16,5210,5211],{},"When I was debugging the aws lambda functions locally it was running properly and was able to make API calls to outside resources.",[16,5213,5214],{},"I was using AWS SAM cli to debug lambda function, which spins up a docker image and runs lambda function inside it.",[16,5216,5217],{},"But when I deployed lambda function to aws and associated it with a VPC, It was not able to call outside internet. All the calls to outside APIs were timed out. Then I researched a bit about this and found out its because how lambda functions work.",[29,5219,5221],{"id":5220},"why-lambda-functions-loses-internet-access-in-vpc","Why lambda functions loses internet access in VPC ?",[16,5223,5224],{},"Lambda function can not access internet when attached to a public subnet of your VPC, because Lambda functions do not have public IP addresses. You cannot send traffic to the internet, which happens via the VPC's Internet Gateway, unless you have a public IP.",[29,5226,5228],{"id":5227},"allow-lambda-to-get-access-to-internet-within-vpc","Allow lambda to get access to internet within VPC.",[16,5230,5231],{},"The way to access the internet is to route traffic through a NAT.\nNAT gateway has an elastic network interface (i.e. an IP address).\nSo NAT gateway can forward traffic to internet gateway and allow access to outside internet.",[16,5233,5234],{},"Steps to be followed are,",[150,5236,5237,5240,5243,5246],{},[153,5238,5239],{},"You need to create two subnets in your VPC (one public and one private).",[153,5241,5242],{},"Create a NAT gateway in public subnet",[153,5244,5245],{},"Create a lambda function and attach private subnet.",[153,5247,5248],{},"Update route table config of private subnet to route all unknown traffic to NAT gateway.",[16,5250,5251],{},"Now your lambda function can access outside internet.",[16,5253,5254],{},"Below diagram shows this setup. Lambda function can access SNS APIs, as traffic is routed through NAT and then internet gateway.",[16,5256,5257],{},[94,5258],{"alt":5259,"src":5260},"Example diagram","/assets/lambda-in-VPC.webp",[16,5262,5263,5264],{},"Read more about the solution ",[45,5265,5268],{"href":5266,"rel":5267},"https://aws.amazon.com/premiumsupport/knowledge-center/internet-access-lambda-function/",[49],"in this aws article",[29,5270,5272],{"id":5271},"keep-in-mind","Keep in mind.",[245,5274,5275,5278,5281],{},[153,5276,5277],{},"NAT gateway is not serverless solution and charges per hour and per GB processed. So keep data transfer of NAT gateway low.",[153,5279,5280],{},"Don't create NAT if lambda function only need to get access to internal VPC resources.",[153,5282,5283],{},"Lambda function can't be invoked from outside VPC, Invocations can come via AWS Lambda API, or API gateway or other internal aws triggers",[29,5285,5287],{"id":5286},"references","References",[245,5289,5290],{},[153,5291,5292],{},[45,5293,5266],{"href":5266,"rel":5294},[49],{"title":69,"searchDepth":99,"depth":99,"links":5296},[5297,5298,5299,5300,5301],{"id":5207,"depth":99,"text":5208},{"id":5220,"depth":99,"text":5221},{"id":5227,"depth":99,"text":5228},{"id":5271,"depth":99,"text":5272},{"id":5286,"depth":99,"text":5287},"2021-08-14T07:00:13.392Z","Discover the right way to use AWS Lambda function in a VPC. This post covers the reasons why a Lambda function loses internet access in a VPC, and explains how to route traffic through a NAT to allow access to the internet.","/assets/lambda-vpc.webp",{},"/blog/lambda-in-vpc",{"title":5189,"description":5303},"blog/lambda-in-vpc",[3306,2422],"UK1z9lWMhjdTXn8jQLKD9jI6ZaWNFZ4I0qqUDD1Tkwo",{"id":5312,"title":5313,"author":7,"authorTitle":8,"body":5314,"category":161,"createdAt":959,"description":5806,"extension":107,"image":2551,"meta":5807,"navigation":110,"path":5808,"proficiency":448,"published":167,"readingTime":3793,"seo":5809,"stem":5810,"tags":5811,"updatedAt":118,"__hash__":5812},"blog/blog/mining-ai-history-smarter-workflow.md","Mining My AI History to Build a Smarter Workflow",{"type":10,"value":5315,"toc":5786},[5316,5323,5326,5329,5332,5335,5339,5342,5346,5354,5357,5360,5365,5370,5374,5381,5392,5397,5402,5406,5409,5413,5416,5434,5437,5440,5444,5447,5461,5464,5468,5471,5478,5483,5488,5492,5495,5502,5505,5575,5578,5581,5586,5591,5595,5598,5601,5604,5608,5611,5614,5618,5621,5624,5628,5631,5636,5641,5645,5648,5686,5689,5692,5697,5702,5706,5709,5712,5719,5733,5736,5739,5743,5746,5778,5781,5784],[13,5317,5318],{},[16,5319,5320,5322],{},[19,5321,21],{}," After months of daily AI-assisted development, I realized my conversation history is a goldmine of data — about me. I used the claude-mem plugin and Claude Code's /insights command to mine my own work patterns, discovered recurring mistakes and preferences, and turned those into custom rules (CLAUDE.md) and skills that make Claude work like I do. Your AI usage data is the most personal dataset you'll ever have access to. Use it.",[16,5324,5325],{},"There's a phrase that gets thrown around a lot in tech: \"data is the new oil.\" Usually it's about user data, market data, analytics dashboards.",[16,5327,5328],{},"But here's something I never considered — I've been generating data about my own work habits every single day, and I was throwing it away.",[16,5330,5331],{},"Every conversation I have with Claude Code, every correction I make, every decision I explain — that's data. Data about how I think, how I make decisions, what mistakes I keep making, and what approaches work best for me.",[16,5333,5334],{},"I decided to mine it.",[29,5336,5338],{"id":5337},"the-two-sources-i-mined","The Two Sources I Mined",[16,5340,5341],{},"I had two sources of data about my own AI usage.",[1981,5343,5345],{"id":5344},"source-1-the-claude-mem-plugin","Source 1: The claude-mem Plugin",[16,5347,2390,5348,5353],{},[45,5349,5352],{"href":5350,"rel":5351},"https://github.com/anthropics/claude-mem",[49],"claude-mem plugin"," is a persistent memory system for Claude Code. It records observations across sessions — discoveries, decisions, bug fixes, feature implementations — all searchable and timestamped.",[16,5355,5356],{},"After months of use, I had hundreds of observations spanning multiple projects. Bug fixes in my compliance platform, design decisions in my blog, architecture choices in my fitness app, infrastructure work across AWS, GCP, and Azure.",[16,5358,5359],{},"This wasn't just a log. It was a searchable database of how I work.",[16,5361,5362],{},[94,5363],{"alt":5364,"src":2551},"placeholder: screenshot of claude-mem search results showing observations across projects",[13,5366,5367],{},[16,5368,5369],{},"Months of work decisions, searchable by keyword and date",[1981,5371,5373],{"id":5372},"source-2-the-insights-command","Source 2: The /insights Command",[16,5375,5376,5377,5380],{},"Claude Code has a ",[24,5378,5379],{},"/insights"," command that surfaces patterns in how you use the tool — what you ask for most, where you spend time, what types of tasks dominate your sessions.",[16,5382,5383,5384,5387,5388,5391],{},"Combined with claude-mem, this gave me two views: what I ",[19,5385,5386],{},"do"," (insights) and what I ",[19,5389,5390],{},"decide"," (memory).",[16,5393,5394],{},[94,5395],{"alt":5396,"src":2551},"placeholder: screenshot of /insights command output showing usage patterns",[13,5398,5399],{},[16,5400,5401],{},"Usage patterns I never would have noticed without looking at the data",[29,5403,5405],{"id":5404},"what-i-found-three-types-of-patterns","What I Found: Three Types of Patterns",[16,5407,5408],{},"I spent an evening going through my history. Not reading every conversation — searching for patterns. Here's what jumped out.",[1981,5410,5412],{"id":5411},"pattern-1-recurring-corrections","Pattern 1: Recurring Corrections",[16,5414,5415],{},"The most valuable discovery was the corrections I kept giving Claude. The same feedback, over and over, across different sessions:",[245,5417,5418,5421,5428,5431],{},[153,5419,5420],{},"\"Don't hardcode AWS account IDs — use dynamic references\"",[153,5422,5423,5424,5427],{},"\"Don't use ",[24,5425,5426],{},"gh pr edit"," — it triggers deprecation errors. Use the REST API\"",[153,5429,5430],{},"\"Dispatch subagents for broad exploration, don't do it in the main context\"",[153,5432,5433],{},"\"Don't create files that weren't explicitly requested\"",[16,5435,5436],{},"Each of these corrections cost me 30 seconds to type. But I was typing them multiple times a week. Across months, that's hours of repeating myself.",[16,5438,5439],{},"The pattern was clear: I had implicit rules in my head that I kept enforcing manually. Why not write them down once?",[1981,5441,5443],{"id":5442},"pattern-2-consistent-approaches","Pattern 2: Consistent Approaches",[16,5445,5446],{},"My memory logs showed that I approach work in a consistent way, whether I'm building a UI feature, fixing infrastructure, or debugging a distributed system:",[150,5448,5449,5452,5455,5458],{},[153,5450,5451],{},"Research first (load docs, understand context)",[153,5453,5454],{},"Plan before code (write a plan document)",[153,5456,5457],{},"Mockup before component (create HTML mockups for UI work)",[153,5459,5460],{},"Evaluate before scale (test quality on small sample first)",[16,5462,5463],{},"I wasn't doing this consciously — it's just how I work. But seeing it spelled out in the data made me realize I could encode these patterns into tools that enforce them automatically.",[1981,5465,5467],{"id":5466},"pattern-3-tool-switching-points","Pattern 3: Tool Switching Points",[16,5469,5470],{},"The data showed when I switch between tools and why. NotebookLM for research, ChatGPT for quick validation, Claude Code for implementation. Each switch happened at a predictable point in the workflow.",[16,5472,5473,5474,5477],{},"This pattern became the foundation for my ",[45,5475,5476],{"href":2926},"multi-tool workflow",". Without mining the data, I wouldn't have seen the pattern clearly enough to formalize it.",[16,5479,5480],{},[94,5481],{"alt":5482,"src":2551},"placeholder: diagram showing the three pattern types discovered from mining AI history",[13,5484,5485],{},[16,5486,5487],{},"Three types of patterns hiding in plain sight",[29,5489,5491],{"id":5490},"turning-patterns-into-rules-claudemd","Turning Patterns Into Rules: CLAUDE.md",[16,5493,5494],{},"The first actionable step was simple: write my recurring corrections into a file that Claude reads automatically.",[16,5496,5497,5498,5501],{},"Claude Code supports a ",[24,5499,5500],{},"CLAUDE.md"," file — a markdown file at the root of your project (or in your user home directory) that contains instructions Claude follows in every session. Think of it as your personal rulebook.",[16,5503,5504],{},"Here's a portion of what I added to my user-scope CLAUDE.md:",[61,5506,5510],{"className":5507,"code":5508,"language":5509,"meta":69,"style":69},"language-markdown shiki shiki-themes github-light github-dark","## General Rules\n- Do not create files that weren't explicitly requested\n- Keep explanations concise. Prefer action over lengthy explanation\n- For wide-level code exploration, dispatch a subagent with specific\n  instructions. Do not perform broad codebase exploration inline.\n\n## GitHub\n- Never use `gh pr edit` or `gh issue edit` — they trigger deprecation\n  errors. Use the REST API directly.\n\n## AWS / Infrastructure\n- Never hardcode AWS account IDs, regions, or credentials in code.\n  Always use dynamic references.\n","markdown",[24,5511,5512,5517,5522,5527,5532,5537,5541,5546,5551,5556,5560,5565,5570],{"__ignoreMap":69},[289,5513,5514],{"class":292,"line":281},[289,5515,5516],{},"## General Rules\n",[289,5518,5519],{"class":292,"line":99},[289,5520,5521],{},"- Do not create files that weren't explicitly requested\n",[289,5523,5524],{"class":292,"line":282},[289,5525,5526],{},"- Keep explanations concise. Prefer action over lengthy explanation\n",[289,5528,5529],{"class":292,"line":283},[289,5530,5531],{},"- For wide-level code exploration, dispatch a subagent with specific\n",[289,5533,5534],{"class":292,"line":284},[289,5535,5536],{},"  instructions. Do not perform broad codebase exploration inline.\n",[289,5538,5539],{"class":292,"line":1503},[289,5540,3072],{"emptyLinePlaceholder":110},[289,5542,5543],{"class":292,"line":1511},[289,5544,5545],{},"## GitHub\n",[289,5547,5548],{"class":292,"line":1524},[289,5549,5550],{},"- Never use `gh pr edit` or `gh issue edit` — they trigger deprecation\n",[289,5552,5553],{"class":292,"line":1537},[289,5554,5555],{},"  errors. Use the REST API directly.\n",[289,5557,5558],{"class":292,"line":1550},[289,5559,3072],{"emptyLinePlaceholder":110},[289,5561,5562],{"class":292,"line":1563},[289,5563,5564],{},"## AWS / Infrastructure\n",[289,5566,5567],{"class":292,"line":1574},[289,5568,5569],{},"- Never hardcode AWS account IDs, regions, or credentials in code.\n",[289,5571,5572],{"class":292,"line":1580},[289,5573,5574],{},"  Always use dynamic references.\n",[16,5576,5577],{},"This took 20 minutes to write. It saves me from repeating the same corrections every single day.",[16,5579,5580],{},"The important thing: I didn't invent these rules. I discovered them by looking at what I was already correcting. The data told me what the rules should be.",[16,5582,5583],{},[94,5584],{"alt":5585,"src":2551},"placeholder: screenshot of CLAUDE.md file with custom rules",[13,5587,5588],{},[16,5589,5590],{},"Rules I discovered from my own correction patterns",[29,5592,5594],{"id":5593},"turning-patterns-into-skills-custom-claude-skills","Turning Patterns Into Skills: Custom Claude Skills",[16,5596,5597],{},"Rules handle the \"don't do X\" patterns. But what about the \"always do Y\" patterns — my consistent approaches?",[16,5599,5600],{},"For those, I built custom Claude Code skills. A skill is a reusable prompt template that Claude follows when invoked. It's like a checklist that enforces a specific workflow.",[16,5602,5603],{},"Here are a few I created based on the patterns I discovered:",[1981,5605,5607],{"id":5606},"skill-pr-autofix","Skill: PR Autofix",[16,5609,5610],{},"From my memory logs, I noticed I follow the same pattern with every PR: create it, wait for CodeRabbit review, address comments, re-trigger review. I was doing this manually every time.",[16,5612,5613],{},"So I built a skill that automates the loop — it monitors the PR for review status, fixes issues, and re-triggers reviews until approved.",[1981,5615,5617],{"id":5616},"skill-debugging-template","Skill: Debugging Template",[16,5619,5620],{},"My debugging approach is consistent: reproduce the issue, check logs, form hypothesis, verify hypothesis, then fix. I kept explaining this process to Claude in different debugging sessions.",[16,5622,5623],{},"Now it's a skill. When I encounter a bug, I invoke the debugging skill and it follows my methodology automatically.",[1981,5625,5627],{"id":5626},"skill-research-first-planning","Skill: Research-First Planning",[16,5629,5630],{},"My \"research before code\" pattern became a skill that enforces documentation discovery before writing an implementation plan. It won't let me skip straight to coding.",[16,5632,5633],{},[94,5634],{"alt":5635,"src":2551},"placeholder: screenshot showing custom skills list in Claude Code",[13,5637,5638],{},[16,5639,5640],{},"Skills built from my own work patterns, not generic templates",[29,5642,5644],{"id":5643},"the-feedback-loop","The Feedback Loop",[16,5646,5647],{},"Here's where it gets interesting. The process of mining your AI history isn't a one-time thing. It's a loop:",[150,5649,5650,5656,5662,5668,5674,5680],{},[153,5651,5652,5655],{},[19,5653,5654],{},"Work"," — use AI tools normally across projects",[153,5657,5658,5661],{},[19,5659,5660],{},"Capture"," — claude-mem records observations automatically",[153,5663,5664,5667],{},[19,5665,5666],{},"Mine"," — periodically search for patterns (monthly works well)",[153,5669,5670,5673],{},[19,5671,5672],{},"Extract"," — turn patterns into CLAUDE.md rules or custom skills",[153,5675,5676,5679],{},[19,5677,5678],{},"Improve"," — the rules and skills make future work smoother",[153,5681,5682,5685],{},[19,5683,5684],{},"Repeat"," — new patterns emerge as your workflow evolves",[16,5687,5688],{},"Each cycle makes the AI a little smarter about you specifically. Not smarter in general — smarter about how you think, what you care about, and how you like to work.",[16,5690,5691],{},"After three cycles, the difference was noticeable. Fewer corrections per session. Less time explaining my preferences. More time building.",[16,5693,5694],{},[94,5695],{"alt":5696,"src":2551},"placeholder: diagram showing the feedback loop - work, capture, mine, extract, improve",[13,5698,5699],{},[16,5700,5701],{},"The self-improving feedback loop",[29,5703,5705],{"id":5704},"what-ai-learns-vs-what-you-learn","What AI Learns vs What You Learn",[16,5707,5708],{},"There's a deeper point here that goes beyond AI tools.",[16,5710,5711],{},"When you use an AI assistant, the AI learns about code — patterns, libraries, best practices. That's useful but generic. Every developer gets roughly the same code suggestions.",[16,5713,5714,5715,5718],{},"But when you mine your own AI history, ",[19,5716,5717],{},"you"," learn about yourself. You discover:",[245,5720,5721,5724,5727,5730],{},[153,5722,5723],{},"What mistakes you keep making (and can prevent)",[153,5725,5726],{},"What approaches consistently work for you (and can encode)",[153,5728,5729],{},"Where you waste time (and can optimize)",[153,5731,5732],{},"How your workflow actually works vs how you think it works",[16,5734,5735],{},"This is the most personal dataset you'll ever have access to. No one else has your exact combination of projects, decisions, preferences, and corrections. It's data about your thinking process, captured in real time, across months of real work.",[16,5737,5738],{},"The companies that win in tech are the ones that use their data well. The same applies to individual developers.",[29,5740,5742],{"id":5741},"getting-started","Getting Started",[16,5744,5745],{},"You don't need months of history to start. Here's what I'd suggest:",[150,5747,5748,5754,5760,5766,5772],{},[153,5749,5750,5753],{},[19,5751,5752],{},"Install claude-mem"," (or whatever persistent memory tool your AI supports) — start capturing observations now",[153,5755,5756,5759],{},[19,5757,5758],{},"After 2-3 weeks",", search your memory for the word \"don't\" or \"instead\" or \"not\" — these are your correction patterns",[153,5761,5762,5765],{},[19,5763,5764],{},"Write 5-10 rules"," into your CLAUDE.md based on what you find",[153,5767,5768,5771],{},[19,5769,5770],{},"After a month",", look for your consistent approaches — research-first? Plan-first? Test-first? Encode the strongest one as a custom skill",[153,5773,5774,5777],{},[19,5775,5776],{},"Review quarterly"," — your patterns evolve, your rules should too",[16,5779,5780],{},"The first time you start a session and Claude already knows your preferences without you explaining them — that's the moment it clicks.",[16,5782,5783],{},"💡 We talk about AI getting smarter. But the real unlock is when you use AI's memory of your work to get smarter about yourself. The data is already there. You just have to look at it.",[431,5785,3290],{},{"title":69,"searchDepth":99,"depth":99,"links":5787},[5788,5792,5797,5798,5803,5804,5805],{"id":5337,"depth":99,"text":5338,"children":5789},[5790,5791],{"id":5344,"depth":282,"text":5345},{"id":5372,"depth":282,"text":5373},{"id":5404,"depth":99,"text":5405,"children":5793},[5794,5795,5796],{"id":5411,"depth":282,"text":5412},{"id":5442,"depth":282,"text":5443},{"id":5466,"depth":282,"text":5467},{"id":5490,"depth":99,"text":5491},{"id":5593,"depth":99,"text":5594,"children":5799},[5800,5801,5802],{"id":5606,"depth":282,"text":5607},{"id":5616,"depth":282,"text":5617},{"id":5626,"depth":282,"text":5627},{"id":5643,"depth":99,"text":5644},{"id":5704,"depth":99,"text":5705},{"id":5741,"depth":99,"text":5742},"I mined my Claude Code memory logs to discover recurring patterns, then turned them into custom rules and skills that make my AI tools smarter about me.",{},"/blog/mining-ai-history-smarter-workflow",{"title":5313,"description":5806},"blog/mining-ai-history-smarter-workflow",[1854,969,2931],"dMpnl5N7W3pSvFB8XWin1v8fz3bpRHW_bZWCpHkDGCg",{"id":5814,"title":5815,"author":7,"authorTitle":8,"body":5816,"category":161,"createdAt":5830,"description":5831,"extension":107,"image":5832,"meta":5833,"navigation":110,"path":5834,"proficiency":112,"published":167,"readingTime":113,"seo":5835,"stem":5836,"tags":5837,"updatedAt":118,"__hash__":5838},"blog/blog/my-hactoberfest-experience.md","My hacktoberfest experience.",{"type":10,"value":5817,"toc":5828},[5818,5825],[13,5819,5820],{},[16,5821,5822,5824],{},[19,5823,21],{}," A personal reflection on participating in Hacktoberfest 2020 -- the annual open source contribution event. Covers the experience of finding projects to contribute to, navigating unfamiliar codebases, submitting pull requests, and the lessons learned about open source collaboration along the way.",[137,5826,5815],{"id":5827},"my-hacktoberfest-experience",{"title":69,"searchDepth":99,"depth":99,"links":5829},[],"2021-07-13T07:00:13.392Z","Discover my journey participating in hacktoberfest 2020, including my learnings and overall experience.","/assets/hacktober.webp",{},"/blog/my-hactoberfest-experience",{"title":5815,"description":5831},"blog/my-hactoberfest-experience",[3306,2422],"f05Wp6O5X9Q60YOSQ8ogjFNYrtRs41Het9u9_62TbdA",{"id":5840,"title":5841,"author":7,"authorTitle":8,"body":5842,"category":104,"createdAt":6760,"description":6761,"extension":107,"image":6762,"meta":6763,"navigation":110,"path":6764,"proficiency":448,"published":110,"readingTime":4995,"seo":6765,"stem":6766,"tags":6767,"updatedAt":118,"__hash__":6768},"blog/blog/notion-as-cms-demo.md","Use Notion as CMS for your website",{"type":10,"value":5843,"toc":6750},[5844,5851,5854,5857,5860,5863,5866,5870,5873,5876,5881,5887,5891,5894,5966,5970,5973,5976,5981,5987,5991,6005,6010,6016,6021,6025,6030,6033,6036,6042,6047,6051,6056,6062,6067,6071,6083,6089,6094,6097,6647,6651,6656,6662,6675,6681,6694,6700,6709,6715,6720,6728,6732,6735,6738,6741,6747],[13,5845,5846],{},[16,5847,5848,5850],{},[19,5849,21],{}," Notion can serve as a free headless CMS for any website. This post demonstrates the concept by building a team page for a startup where a hiring manager updates candidate statuses in a Notion board, and the website automatically reflects those changes. It covers creating a simple HTML site, integrating the Notion API to fetch database content at build time, and setting up Netlify CI/CD with build hooks to trigger redeployments when content changes.",[137,5852,5841],{"id":5853},"use-notion-as-cms-for-your-website",[16,5855,5856],{},"Notion is a note-taking software that I recently started using. It is very flexible and easy to use.",[16,5858,5859],{},"It can be a used as a writing repository, task management tool, a workout calendar, a database, and so much more. I manage my long term, short term goals, blog ideas scratch pad within notion.",[16,5861,5862],{},"Notion provides APIs which are good mix of REST and GraphQL. We will use these APIs, to fetch the pages and databases from notion. Because of this we can use notion as a CMS for any website.",[16,5864,5865],{},"Data fetched from the APIs will be added at built time to our demo website.",[29,5867,5869],{"id":5868},"create-a-simple-website","Create a simple website",[16,5871,5872],{},"To showcase a simple use case.\nLet's consider there is a hiring manager at small startup, And he/she manages the list of candidates in notion boards.\nBoard has sections ex. shortlisted, in process, hired candidates. Hiring manager moves the card of each candidate within respective section.",[16,5874,5875],{},"Names of the selected members should appear in team section on company website. I have created a one page website for this.",[16,5877,5878],{},[19,5879,5880],{},"Reference:",[16,5882,5883],{},[45,5884,5885],{"href":5885,"rel":5886},"https://tailwindcomponents.com/component/team-section-2",[49],[29,5888,5890],{"id":5889},"load-data-from-a-json-file","Load data from a json file",[16,5892,5893],{},"I modified this example to use data from a json file.\nMetadata of each team member like name, profilePic, jobtitle is loaded and shown on website.",[61,5895,5898],{"className":4695,"code":5896,"highlights":5897,"language":4698,"meta":69,"style":69},"import team from '../cms/team.json'\nexport default {\n  name: 'IndexPage',\n  data: () => ({\n    team: team\n  })\n}\n",[281,282,283,284],[24,5899,5900,5915,5925,5936,5951,5957,5962],{"__ignoreMap":69},[289,5901,5903,5906,5909,5912],{"class":5902,"line":281},[292,293],[289,5904,5905],{"class":311},"import",[289,5907,5908],{"class":1456}," team ",[289,5910,5911],{"class":311},"from",[289,5913,5914],{"class":300}," '../cms/team.json'\n",[289,5916,5917,5920,5923],{"class":292,"line":99},[289,5918,5919],{"class":311},"export",[289,5921,5922],{"class":311}," default",[289,5924,4753],{"class":1456},[289,5926,5928,5931,5934],{"class":5927,"line":282},[292,293],[289,5929,5930],{"class":1456},"  name: ",[289,5932,5933],{"class":300},"'IndexPage'",[289,5935,1486],{"class":1456},[289,5937,5939,5942,5945,5948],{"class":5938,"line":283},[292,293],[289,5940,5941],{"class":296},"  data",[289,5943,5944],{"class":1456},": () ",[289,5946,5947],{"class":311},"=>",[289,5949,5950],{"class":1456}," ({\n",[289,5952,5954],{"class":5953,"line":284},[292,293],[289,5955,5956],{"class":1456},"    team: team\n",[289,5958,5959],{"class":292,"line":1503},[289,5960,5961],{"class":1456},"  })\n",[289,5963,5964],{"class":292,"line":1511},[289,5965,1595],{"class":1456},[29,5967,5969],{"id":5968},"setup-cicd-using-netlify","Setup CICD using netlify",[16,5971,5972],{},"Netlify provides easy CICD integration with github.\nIntegrate netlify with your repo and Specify your build command and build output directory.",[16,5974,5975],{},"Follow the below given video to setup CICD for your github repo.",[16,5977,5978],{},[19,5979,5980],{},"Referance",[16,5982,5983],{},[45,5984,5985],{"href":5985,"rel":5986},"https://www.youtube.com/watch?v=4h8B080Mv4U",[49],[29,5988,5990],{"id":5989},"create-a-board-in-notion","Create a board in Notion",[245,5992,5993,5996,5999,6002],{},[153,5994,5995],{},"Create a board in notion.",[153,5997,5998],{},"Add few entries to it with relevant data, for this use case I have added name, jobTitle, profilePic, rank etc.",[153,6000,6001],{},"Keep entries in each category. (Later we can move these items to done, that would publish them on website)",[153,6003,6004],{},"Note down the database ID of this board. This will be used in API to query data.",[16,6006,6007],{},[24,6008,6009],{},"https://www.notion.so/{DB_ID}?v={VIEW_ID}",[16,6011,6012],{},[94,6013],{"alt":6014,"src":6015},"notion board","/assets/team_before.webp",[13,6017,6018],{},[16,6019,6020],{},"Names from the Done column should appear on website",[29,6022,6024],{"id":6023},"setup-notion-integration","Setup notion integration",[245,6026,6027],{},[153,6028,6029],{},"Create new integration in notion",[16,6031,6032],{},"This would give you a API key to use within your queries.",[16,6034,6035],{},"Make sure not to commit this to github (or any other SCM).",[16,6037,6038],{},[94,6039],{"alt":6040,"src":6041},"notion integration","/assets/notion_integration.webp",[13,6043,6044],{},[16,6045,6046],{},"Navigate to Notion developer --> my-integrations",[29,6048,6050],{"id":6049},"share-page-with-notion-integration","Share page with notion integration",[245,6052,6053],{},[153,6054,6055],{},"By using share page button, share you board with notion integration you created in last step.",[16,6057,6058],{},[94,6059],{"alt":6060,"src":6061},"share page","/assets/share_notion_page.webp",[13,6063,6064],{},[16,6065,6066],{},"This makes your content available to the integration, can be accessed using API now.",[29,6068,6070],{"id":6069},"script-to-fetch-notion-data-at-build-time","Script to fetch notion data at build time",[245,6072,6073],{},[153,6074,6075,6076,3487,6079,6082],{},"Add ",[24,6077,6078],{},"NOTION_API_KEY",[24,6080,6081],{},"NOTION_DB_ID"," in env variables.",[16,6084,6085],{},[94,6086],{"alt":6087,"src":6088},"netlify_env","/assets/netlify_env.webp",[13,6090,6091],{},[16,6092,6093],{},"Navigate to your site --> build and deploy --> environment",[16,6095,6096],{},"Below script fetches data from notion and writes to a json file.",[61,6098,6101],{"className":4695,"code":6099,"highlights":6100,"language":4698,"meta":69,"style":69},"var axios = require('axios')\nvar fs = require('fs')\n/* Filter entries with status `Done`, i.e.\nfetch only team members who are onboarded in company.\nOther statuses can be for candidates\nwho are in process of being hired or shortlisted\n*/\nvar data = JSON.stringify({\n  sorts: [\n    {\n      property: 'Status',\n      direction: 'ascending'\n    }\n  ],\n  filter: {\n    property: 'Status',\n    rich_text: {\n      equals: 'Done'\n    }\n  }\n})\nconst NOTION_API_KEY = process.env.NOTION_API_KEY\nconst NOTION_DB_ID = process.env.NOTION_DB_ID\nvar config = {\n  method: 'post',\n  url: `https://api.notion.com/v1/databases/${NOTION_DB_ID}/query`,\n  headers: {\n    Authorization: `Bearer ${NOTION_API_KEY}`,\n    'Notion-Version': '2022-02-22',\n    'Content-Type': 'application/json'\n  },\n  data: data\n}\n/* Make API call to fetch data, write to a json file.\nThis is the file that contains data of team\nmembers shown on the website.*/\naxios(config)\n  .then(function(response) {\n    let team = response.data.results.map(f => ({\n      name: f.properties.Name.title[0].text.content,\n      jobTitle: f.properties.jobTitle.rich_text[0].text.content,\n      profilePic: f.properties.profilePic.rich_text[0].text.content,\n      rank: f.properties.rank.rich_text[0].text.content\n    }))\n    team.sort((a, b) => a.rank - b.rank)\n    fs.writeFileSync('./cms/team.json', JSON.stringify(team, null, 2), 'utf-8')\n    console.log('Team data populated')\n  })\n  .catch(function(error) {\n    console.log(error)\n  })\n",[281,282,283,284],[24,6102,6103,6126,6144,6150,6156,6162,6167,6172,6192,6197,6202,6212,6220,6224,6229,6234,6243,6248,6256,6260,6264,6269,6286,6300,6311,6321,6336,6341,6356,6368,6378,6383,6388,6393,6399,6405,6411,6420,6442,6468,6480,6490,6500,6511,6517,6550,6592,6608,6613,6632,6642],{"__ignoreMap":69},[289,6104,6106,6109,6112,6115,6118,6120,6123],{"class":6105,"line":281},[292,293],[289,6107,6108],{"class":311},"var",[289,6110,6111],{"class":1456}," axios ",[289,6113,6114],{"class":311},"=",[289,6116,6117],{"class":296}," require",[289,6119,4732],{"class":1456},[289,6121,6122],{"class":300},"'axios'",[289,6124,6125],{"class":1456},")\n",[289,6127,6128,6130,6133,6135,6137,6139,6142],{"class":292,"line":99},[289,6129,6108],{"class":311},[289,6131,6132],{"class":1456}," fs ",[289,6134,6114],{"class":311},[289,6136,6117],{"class":296},[289,6138,4732],{"class":1456},[289,6140,6141],{"class":300},"'fs'",[289,6143,6125],{"class":1456},[289,6145,6147],{"class":6146,"line":282},[292,293],[289,6148,6149],{"class":1681},"/* Filter entries with status `Done`, i.e.\n",[289,6151,6153],{"class":6152,"line":283},[292,293],[289,6154,6155],{"class":1681},"fetch only team members who are onboarded in company.\n",[289,6157,6159],{"class":6158,"line":284},[292,293],[289,6160,6161],{"class":1681},"Other statuses can be for candidates\n",[289,6163,6164],{"class":292,"line":1503},[289,6165,6166],{"class":1681},"who are in process of being hired or shortlisted\n",[289,6168,6169],{"class":292,"line":1511},[289,6170,6171],{"class":1681},"*/\n",[289,6173,6174,6176,6179,6181,6184,6186,6189],{"class":292,"line":1524},[289,6175,6108],{"class":311},[289,6177,6178],{"class":1456}," data ",[289,6180,6114],{"class":311},[289,6182,6183],{"class":304}," JSON",[289,6185,2284],{"class":1456},[289,6187,6188],{"class":296},"stringify",[289,6190,6191],{"class":1456},"({\n",[289,6193,6194],{"class":292,"line":1537},[289,6195,6196],{"class":1456},"  sorts: [\n",[289,6198,6199],{"class":292,"line":1550},[289,6200,6201],{"class":1456},"    {\n",[289,6203,6204,6207,6210],{"class":292,"line":1563},[289,6205,6206],{"class":1456},"      property: ",[289,6208,6209],{"class":300},"'Status'",[289,6211,1486],{"class":1456},[289,6213,6214,6217],{"class":292,"line":1574},[289,6215,6216],{"class":1456},"      direction: ",[289,6218,6219],{"class":300},"'ascending'\n",[289,6221,6222],{"class":292,"line":1580},[289,6223,1583],{"class":1456},[289,6225,6226],{"class":292,"line":1586},[289,6227,6228],{"class":1456},"  ],\n",[289,6230,6231],{"class":292,"line":1592},[289,6232,6233],{"class":1456},"  filter: {\n",[289,6235,6236,6239,6241],{"class":292,"line":3128},[289,6237,6238],{"class":1456},"    property: ",[289,6240,6209],{"class":300},[289,6242,1486],{"class":1456},[289,6244,6245],{"class":292,"line":3134},[289,6246,6247],{"class":1456},"    rich_text: {\n",[289,6249,6250,6253],{"class":292,"line":3140},[289,6251,6252],{"class":1456},"      equals: ",[289,6254,6255],{"class":300},"'Done'\n",[289,6257,6258],{"class":292,"line":3146},[289,6259,1583],{"class":1456},[289,6261,6262],{"class":292,"line":3152},[289,6263,1589],{"class":1456},[289,6265,6266],{"class":292,"line":3158},[289,6267,6268],{"class":1456},"})\n",[289,6270,6271,6274,6277,6280,6283],{"class":292,"line":3164},[289,6272,6273],{"class":311},"const",[289,6275,6276],{"class":304}," NOTION_API_KEY",[289,6278,6279],{"class":311}," =",[289,6281,6282],{"class":1456}," process.env.",[289,6284,6285],{"class":304},"NOTION_API_KEY\n",[289,6287,6288,6290,6293,6295,6297],{"class":292,"line":3170},[289,6289,6273],{"class":311},[289,6291,6292],{"class":304}," NOTION_DB_ID",[289,6294,6279],{"class":311},[289,6296,6282],{"class":1456},[289,6298,6299],{"class":304},"NOTION_DB_ID\n",[289,6301,6302,6304,6307,6309],{"class":292,"line":3176},[289,6303,6108],{"class":311},[289,6305,6306],{"class":1456}," config ",[289,6308,6114],{"class":311},[289,6310,4753],{"class":1456},[289,6312,6313,6316,6319],{"class":292,"line":3182},[289,6314,6315],{"class":1456},"  method: ",[289,6317,6318],{"class":300},"'post'",[289,6320,1486],{"class":1456},[289,6322,6323,6326,6329,6331,6334],{"class":292,"line":3187},[289,6324,6325],{"class":1456},"  url: ",[289,6327,6328],{"class":300},"`https://api.notion.com/v1/databases/${",[289,6330,6081],{"class":304},[289,6332,6333],{"class":300},"}/query`",[289,6335,1486],{"class":1456},[289,6337,6338],{"class":292,"line":3193},[289,6339,6340],{"class":1456},"  headers: {\n",[289,6342,6343,6346,6349,6351,6354],{"class":292,"line":3199},[289,6344,6345],{"class":1456},"    Authorization: ",[289,6347,6348],{"class":300},"`Bearer ${",[289,6350,6078],{"class":304},[289,6352,6353],{"class":300},"}`",[289,6355,1486],{"class":1456},[289,6357,6358,6361,6363,6366],{"class":292,"line":3205},[289,6359,6360],{"class":300},"    'Notion-Version'",[289,6362,1480],{"class":1456},[289,6364,6365],{"class":300},"'2022-02-22'",[289,6367,1486],{"class":1456},[289,6369,6370,6373,6375],{"class":292,"line":3211},[289,6371,6372],{"class":300},"    'Content-Type'",[289,6374,1480],{"class":1456},[289,6376,6377],{"class":300},"'application/json'\n",[289,6379,6380],{"class":292,"line":3217},[289,6381,6382],{"class":1456},"  },\n",[289,6384,6385],{"class":292,"line":3223},[289,6386,6387],{"class":1456},"  data: data\n",[289,6389,6391],{"class":292,"line":6390},33,[289,6392,1595],{"class":1456},[289,6394,6396],{"class":292,"line":6395},34,[289,6397,6398],{"class":1681},"/* Make API call to fetch data, write to a json file.\n",[289,6400,6402],{"class":292,"line":6401},35,[289,6403,6404],{"class":1681},"This is the file that contains data of team\n",[289,6406,6408],{"class":292,"line":6407},36,[289,6409,6410],{"class":1681},"members shown on the website.*/\n",[289,6412,6414,6417],{"class":292,"line":6413},37,[289,6415,6416],{"class":296},"axios",[289,6418,6419],{"class":1456},"(config)\n",[289,6421,6423,6426,6428,6430,6433,6435,6439],{"class":292,"line":6422},38,[289,6424,6425],{"class":1456},"  .",[289,6427,651],{"class":296},[289,6429,4732],{"class":1456},[289,6431,6432],{"class":311},"function",[289,6434,4732],{"class":1456},[289,6436,6438],{"class":6437},"s4XuR","response",[289,6440,6441],{"class":1456},") {\n",[289,6443,6445,6448,6450,6452,6455,6458,6460,6463,6466],{"class":292,"line":6444},39,[289,6446,6447],{"class":311},"    let",[289,6449,5908],{"class":1456},[289,6451,6114],{"class":311},[289,6453,6454],{"class":1456}," response.data.results.",[289,6456,6457],{"class":296},"map",[289,6459,4732],{"class":1456},[289,6461,6462],{"class":6437},"f",[289,6464,6465],{"class":311}," =>",[289,6467,5950],{"class":1456},[289,6469,6471,6474,6477],{"class":292,"line":6470},40,[289,6472,6473],{"class":1456},"      name: f.properties.Name.title[",[289,6475,6476],{"class":304},"0",[289,6478,6479],{"class":1456},"].text.content,\n",[289,6481,6483,6486,6488],{"class":292,"line":6482},41,[289,6484,6485],{"class":1456},"      jobTitle: f.properties.jobTitle.rich_text[",[289,6487,6476],{"class":304},[289,6489,6479],{"class":1456},[289,6491,6493,6496,6498],{"class":292,"line":6492},42,[289,6494,6495],{"class":1456},"      profilePic: f.properties.profilePic.rich_text[",[289,6497,6476],{"class":304},[289,6499,6479],{"class":1456},[289,6501,6503,6506,6508],{"class":292,"line":6502},43,[289,6504,6505],{"class":1456},"      rank: f.properties.rank.rich_text[",[289,6507,6476],{"class":304},[289,6509,6510],{"class":1456},"].text.content\n",[289,6512,6514],{"class":292,"line":6513},44,[289,6515,6516],{"class":1456},"    }))\n",[289,6518,6520,6523,6526,6529,6531,6533,6536,6539,6541,6544,6547],{"class":292,"line":6519},45,[289,6521,6522],{"class":1456},"    team.",[289,6524,6525],{"class":296},"sort",[289,6527,6528],{"class":1456},"((",[289,6530,45],{"class":6437},[289,6532,2049],{"class":1456},[289,6534,6535],{"class":6437},"b",[289,6537,6538],{"class":1456},") ",[289,6540,5947],{"class":311},[289,6542,6543],{"class":1456}," a.rank ",[289,6545,6546],{"class":311},"-",[289,6548,6549],{"class":1456}," b.rank)\n",[289,6551,6553,6556,6559,6561,6564,6566,6569,6571,6573,6576,6579,6581,6584,6587,6590],{"class":292,"line":6552},46,[289,6554,6555],{"class":1456},"    fs.",[289,6557,6558],{"class":296},"writeFileSync",[289,6560,4732],{"class":1456},[289,6562,6563],{"class":300},"'./cms/team.json'",[289,6565,2049],{"class":1456},[289,6567,6568],{"class":304},"JSON",[289,6570,2284],{"class":1456},[289,6572,6188],{"class":296},[289,6574,6575],{"class":1456},"(team, ",[289,6577,6578],{"class":304},"null",[289,6580,2049],{"class":1456},[289,6582,6583],{"class":304},"2",[289,6585,6586],{"class":1456},"), ",[289,6588,6589],{"class":300},"'utf-8'",[289,6591,6125],{"class":1456},[289,6593,6595,6598,6601,6603,6606],{"class":292,"line":6594},47,[289,6596,6597],{"class":1456},"    console.",[289,6599,6600],{"class":296},"log",[289,6602,4732],{"class":1456},[289,6604,6605],{"class":300},"'Team data populated'",[289,6607,6125],{"class":1456},[289,6609,6611],{"class":292,"line":6610},48,[289,6612,5961],{"class":1456},[289,6614,6616,6618,6621,6623,6625,6627,6630],{"class":292,"line":6615},49,[289,6617,6425],{"class":1456},[289,6619,6620],{"class":296},"catch",[289,6622,4732],{"class":1456},[289,6624,6432],{"class":311},[289,6626,4732],{"class":1456},[289,6628,6629],{"class":6437},"error",[289,6631,6441],{"class":1456},[289,6633,6635,6637,6639],{"class":292,"line":6634},50,[289,6636,6597],{"class":1456},[289,6638,6600],{"class":296},[289,6640,6641],{"class":1456},"(error)\n",[289,6643,6645],{"class":292,"line":6644},51,[289,6646,5961],{"class":1456},[29,6648,6650],{"id":6649},"change-data-in-cms-and-trigger-build-to-see-new-data-added-to-website","Change data in CMS and trigger build to see new data added to website",[150,6652,6653],{},[153,6654,6655],{},"This is how the website looks initially.",[16,6657,6658],{},[94,6659],{"alt":6660,"src":6661},"website before","/assets/site_before.webp",[150,6663,6664],{"start":99},[153,6665,6666,6667,6670,6671,6674],{},"Suppose Hiring manager finalise to ",[19,6668,6669],{},"hire two new developers"," and that should show on your website. Move their entries to the ",[24,6672,6673],{},"Done"," column in notion.",[16,6676,6677],{},[94,6678],{"alt":6679,"src":6680},"team_after","/assets/team_after.webp",[150,6682,6683],{"start":282},[153,6684,6685,6686,6689,6690,6693],{},"Now ",[19,6687,6688],{},"trigger a new build"," in netlify to deploy these changes (with ",[24,6691,6692],{},"clear cache and deploy site"," option).",[16,6695,6696],{},[94,6697],{"alt":6698,"src":6699},"trigger deploy","/assets/netlify_deploy.webp",[150,6701,6702],{"start":283},[153,6703,6704,6705,6708],{},"After deploy the ",[19,6706,6707],{},"new members"," will show on the website",[16,6710,6711],{},[94,6712],{"alt":6713,"src":6714},"website after","/assets/site_after.webp",[13,6716,6717],{},[16,6718,6719],{},"Website has been deployed with new data fetched at build time.",[16,6721,6722,6723],{},"Full code for this can be found on ",[45,6724,6727],{"href":6725,"rel":6726},"https://github.com/ssghait007/notion-as-cms",[49],"github",[137,6729,6731],{"id":6730},"note","Note",[16,6733,6734],{},"This demo shows example to inject data at runtime, As this data is changed less frequently.",[16,6736,6737],{},"For use cases like product page, data will be changed rapidly .",[16,6739,6740],{},"You can use notion npm library to fetch this data while page loads in browser.",[16,6742,6743],{},[45,6744,6745],{"href":6745,"rel":6746},"https://www.npmjs.com/package/@notionhq/client",[49],[431,6748,6749],{},"html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}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 .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}",{"title":69,"searchDepth":99,"depth":99,"links":6751},[6752,6753,6754,6755,6756,6757,6758,6759],{"id":5868,"depth":99,"text":5869},{"id":5889,"depth":99,"text":5890},{"id":5968,"depth":99,"text":5969},{"id":5989,"depth":99,"text":5990},{"id":6023,"depth":99,"text":6024},{"id":6049,"depth":99,"text":6050},{"id":6069,"depth":99,"text":6070},{"id":6649,"depth":99,"text":6650},"2022-07-03T07:00:13.392Z","The post describes the step-by-step process of creating a simple website and loading data from a json file. Also, learn how to set up CICD using netlify, create a board in Notion, integrate it with your website, and fetch data from Notion at build time","/assets/notion-as-cms-header.webp",{},"/blog/notion-as-cms-demo",{"title":5841,"description":6761},"blog/notion-as-cms-demo",[590],"zQehq1Gzx6D39oFGaqpUEN27bQjb5qo_QtRnkya0Ku8",{"id":6770,"title":6771,"author":7,"authorTitle":8,"body":6772,"category":161,"createdAt":6896,"description":6897,"extension":107,"image":6898,"meta":6899,"navigation":110,"path":6900,"proficiency":6901,"published":110,"readingTime":113,"seo":6902,"stem":6903,"tags":6904,"updatedAt":118,"__hash__":6906},"blog/blog/port-sharing-os-and-docker.md","Understanding Port Sharing and SO_REUSEADDR in Docker",{"type":10,"value":6773,"toc":6890},[6774,6791,6794,6798,6801,6804,6808,6816,6819,6823,6831,6843,6847,6850,6856,6859,6862,6881,6884,6887],[13,6775,6776],{},[16,6777,6778,6780,6781,6784,6785,2053,6787,6790],{},[19,6779,21],{}," Running an app on port 8080 on your host and then exposing a Docker container on the same port won't throw an error -- Docker silently binds thanks to ",[24,6782,6783],{},"SO_REUSEADDR",". But requests may not reach the containerized app. This post explains how Docker's bridge networking, ",[24,6786,6783],{},[24,6788,6789],{},"SO_REUSEPORT"," interact to create this confusing behavior. The fix: kill the host process first, then restart the Docker container.",[137,6792,6771],{"id":6793},"understanding-port-sharing-and-so_reuseaddr-in-docker",[29,6795,6797],{"id":6796},"incident-running-application-in-docker-and-observing-weird-behavior","Incident: Running application in Docker and observing weird behavior",[16,6799,6800],{},"Recently I went through a weird incident that exposed me to some interesting networking concepts. I was running an application on my machine(host operating system), and it was listening on port 8080. Everything seemed to be working fine until I decided to run another instance of the same application inside a Docker container, exposing it on the same port (8080) as on my host machine.",[16,6802,6803],{},"To my surprise, Docker didn't throw any error or warning. Both instances of the application were seemingly running without any issues. However, when I tried to access the application through my browser, it didn't behave as expected. It seemed that the requests were not being handled by the application inside docker.",[29,6805,6807],{"id":6806},"understanding-the-problem-port-sharing-and-network-concepts","Understanding the Problem: Port Sharing and Network Concepts",[16,6809,6810,6811,3487,6813,6815],{},"As I started doing some research about this issue, I found about ",[24,6812,6783],{},[24,6814,6789],{},". They are Port sharing and network concepts in the context of Docker. When you run Docker containers, they create their own isolated network stack, including their own network interfaces, IP addresses, and port mappings. However, Docker provides a bridge network by default, enabling containers to communicate with each other and the host machine.",[16,6817,6818],{},"In the incident described above, both instances of the application were running (one on OS and one inside docker exposed on the same port), but they were sharing the same port (8080) on the host machine. This was an issue as they were trying to listen on the same port.",[29,6820,6822],{"id":6821},"the-role-of-so_reuseaddr","The Role of SO_REUSEADDR",[16,6824,6825,6826,3487,6828,6830],{},"Now, you might be wondering how the socket options ",[24,6827,6783],{},[24,6829,6789],{}," come into play. These options allow multiple sockets to bind to the same address and port combination.",[16,6832,2390,6833,6835,6836,6839,6840,6842],{},[24,6834,6783],{}," option, when enabled, allows the reuse of local addresses and ports. It allows a socket to bind to an address that is in the ",[24,6837,6838],{},"TIME_WAIT"," state, which is a state a socket enters after closing. By default, Docker containers use the ",[24,6841,6783],{}," option when binding to ports on the host machine.",[29,6844,6846],{"id":6845},"finding-a-solution","Finding a Solution",[16,6848,6849],{},"To resolve my issue I had to manually kill the process running on port(running on os), and then restart the docker application",[61,6851,6854],{"className":6852,"code":6853,"language":66},[64],"kill -9 $(lsof -ti:8080)\n",[24,6855,6853],{"__ignoreMap":69},[16,6857,6858],{},"One solution is to configure the Docker container to listen on different host port while mapping them to the corresponding container ports.",[16,6860,6861],{},"Here's an example of how you can achieve this using the Docker command-line interface:",[61,6863,6865],{"className":278,"code":6864,"language":285,"meta":69,"style":69},"docker run -p 8081:8080 [image_name]\n",[24,6866,6867],{"__ignoreMap":69},[289,6868,6869,6871,6873,6875,6878],{"class":292,"line":281},[289,6870,1169],{"class":296},[289,6872,1172],{"class":300},[289,6874,1187],{"class":304},[289,6876,6877],{"class":300}," 8081:8080",[289,6879,6880],{"class":1456}," [image_name]\n",[16,6882,6883],{},"Conclusion\nPort sharing can lead to conflicts when running multiple instances of an application on the same port. The incident highlights the importance of understanding network concepts and the role of socket options like SO_REUSEADDR.",[16,6885,6886],{},"💡 This also got me thinking when we are developing a web application and we restart the process while debugging we are using this same concept. By default, the ListenAndServe(golang) method will use the SO_REUSEADDR option on the underlying socket, allowing the server to bind to the same address and port again, even if it's in the TIME-WAIT state.",[431,6888,6889],{},"html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}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 .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":69,"searchDepth":99,"depth":99,"links":6891},[6892,6893,6894,6895],{"id":6796,"depth":99,"text":6797},{"id":6806,"depth":99,"text":6807},{"id":6821,"depth":99,"text":6822},{"id":6845,"depth":99,"text":6846},"2023-07-09","Learn about the problem of port sharing in Docker and how the SO_REUSEADDR option relates to it. Gain insights into network concepts and how they impact running multiple instances of an application on the same port. Find solutions to overcome port conflicts in Docker.","/assets/po.webp",{},"/blog/port-sharing-os-and-docker","Intermediate",{"title":6771,"description":6897},"blog/port-sharing-os-and-docker",[1169,6905],"networking","Q6QlKYaejEQWoBLwVzaf2HCMBXC1zUT_29sf-KT3QVA",{"id":6908,"title":6909,"author":7,"authorTitle":8,"body":6910,"category":161,"createdAt":443,"description":7128,"extension":107,"image":7129,"meta":7130,"navigation":110,"path":7131,"proficiency":448,"published":110,"readingTime":965,"seo":7132,"stem":7133,"tags":7134,"updatedAt":118,"__hash__":7135},"blog/blog/running-selenium-webdriver-on-raspberry-pi-zero.md","Selenium Webdriver on Raspberry Pi Zero W.",{"type":10,"value":6911,"toc":7122},[6912,6923,6926,6929,6936,6939,6943,6946,6982,6986,6989,6992,6995,6998,7004,7007,7040,7044,7047,7053,7056,7078,7081,7084,7087,7093,7095,7119],[13,6913,6914],{},[16,6915,6916,6918,6919,6922],{},[19,6917,21],{}," Want to run Selenium-based browser automation on a Raspberry Pi Zero W? Firefox's geckodriver dropped ARM support in 2018, so it's a dead end. The solution is using the ",[24,6920,6921],{},"chromium-chromedriver"," package that comes pre-built for ARM on Raspbian. This post covers the challenges of running browser automation on ARM hardware, the geckodriver vs chromedriver decision, and the full setup for headless Chromium with Selenium on Pi Zero W.",[137,6924,6909],{"id":6925},"selenium-webdriver-on-raspberry-pi-zero-w",[16,6927,6928],{},"I have earlier written a post on how selenium can be used to automate browser based tasks.",[16,6930,6931,6932,2284],{},"Find that post ",[45,6933,3245],{"href":6934,"rel":6935},"https://onthegoalways.com/blog/selenium-automate-browser-tasks",[49],[16,6937,6938],{},"I recently bought a raspberry-pi, So wanted to add some tasks like making changes in router settings on raspberry-pi device.",[29,6940,6942],{"id":6941},"raspberry-pi-zero-w-overview","Raspberry Pi zero w overview.",[16,6944,6945],{},"Raspberry-pi's raspbian OS is Linux based and its hardware is arm based.\nTo check this on your raspberry-pi device run below command.",[61,6947,6950],{"className":278,"code":6948,"highlights":6949,"language":285,"meta":69,"style":69},"$ uname -a\n\nLinux raspber 5.10.17+ #1414 Fri Apr 30 13:16:27 IST 2021 armv6l GNU/Linux\n",[281,282,283,284],[24,6951,6952,6963,6967],{"__ignoreMap":69},[289,6953,6955,6957,6960],{"class":6954,"line":281},[292,293],[289,6956,297],{"class":296},[289,6958,6959],{"class":300}," uname",[289,6961,6962],{"class":304}," -a\n",[289,6964,6965],{"class":292,"line":99},[289,6966,3072],{"emptyLinePlaceholder":110},[289,6968,6970,6973,6976,6979],{"class":6969,"line":282},[292,293],[289,6971,6972],{"class":296},"Linux",[289,6974,6975],{"class":300}," raspber",[289,6977,6978],{"class":300}," 5.10.17+",[289,6980,6981],{"class":1681}," #1414 Fri Apr 30 13:16:27 IST 2021 armv6l GNU/Linux\n",[29,6983,6985],{"id":6984},"what-challenges-i-faced","What challenges I faced ? 😞",[16,6987,6988],{},"I decided to go for firefox and geckodriver combination along with selenium. I thought this will be lightweight than chromium.",[16,6990,6991],{},"I did not find any suitable version of geckodriver for arm based machines. As geckodriver project has stopped supporting it since 2018.",[16,6993,6994],{},"I tried out some older versions of geckodriver shared online. But none of these work for me.",[16,6996,6997],{},"I also tried one geckodriver version built by someone following below process. And this as well didn't work.",[16,6999,7000],{},[45,7001,7002],{"href":7002,"rel":7003},"https://firefox-source-docs.mozilla.org/testing/geckodriver/ARM.html",[49],[16,7005,7006],{},"I was getting error for OS mismatch with all geckodriver executables I tried.",[61,7008,7011],{"className":278,"code":7009,"highlights":7010,"language":285,"meta":69,"style":69},"Firefox - OSError: [Errno 8] Exec format error\n",[281,282,283,284],[24,7012,7013],{"__ignoreMap":69},[289,7014,7016,7019,7022,7025,7028,7031,7034,7037],{"class":7015,"line":281},[292,293],[289,7017,7018],{"class":296},"Firefox",[289,7020,7021],{"class":300}," -",[289,7023,7024],{"class":300}," OSError:",[289,7026,7027],{"class":1456}," [Errno ",[289,7029,7030],{"class":300},"8]",[289,7032,7033],{"class":300}," Exec",[289,7035,7036],{"class":300}," format",[289,7038,7039],{"class":300}," error\n",[29,7041,7043],{"id":7042},"solution-️","Solution ✔️",[16,7045,7046],{},"I thought I have stuck dead-end and was about to keep this thing on the side.\nThen I found an article about chomium-chromedriver version supported by the Raspbian project.",[16,7048,7049],{},[45,7050,7051],{"href":7051,"rel":7052},"https://ivanderevianko.com/2020/01/selenium-chromedriver-for-raspberrypi",[49],[16,7054,7055],{},"Run below command to install chomium-chromedriver package",[61,7057,7060],{"className":278,"code":7058,"highlights":7059,"language":285,"meta":69,"style":69},"$ sudo apt-get install chromium-chromedriver\n",[281,282,283,284],[24,7061,7062],{"__ignoreMap":69},[289,7063,7065,7067,7070,7073,7075],{"class":7064,"line":281},[292,293],[289,7066,297],{"class":296},[289,7068,7069],{"class":300}," sudo",[289,7071,7072],{"class":300}," apt-get",[289,7074,1140],{"class":300},[289,7076,7077],{"class":300}," chromium-chromedriver\n",[16,7079,7080],{},"This installs both chromium browser and chromedriver on raspberry-pi. Chromium is required for selenium to run a browser in headless mode and chromedriver to connect and control that browser.",[16,7082,7083],{},"This package works awesome on raspberry-pi, I was able to get the selenium tasks\ninstalled, and run them from CLI commands.",[16,7085,7086],{},"Later on I used ngrok to access these from anywhere, more about that in below post",[16,7088,6931,7089,2284],{},[45,7090,3245],{"href":7091,"rel":7092},"https://onthegoalways.com/blog/using-ngrok-to-access-raspberry-pi-from-anywhere",[49],[29,7094,404],{"id":403},[245,7096,7097,7102,7107,7113],{},[153,7098,7099],{},[45,7100,7051],{"href":7051,"rel":7101},[49],[153,7103,7104],{},[45,7105,7002],{"href":7002,"rel":7106},[49],[153,7108,7109],{},[45,7110,7111],{"href":7111,"rel":7112},"https://raspberrypi.stackexchange.com/questions/63258/selenium-firefox-oserror-errno-8-exec-format-error",[49],[153,7114,7115],{},[45,7116,7117],{"href":7117,"rel":7118},"https://www.raspberrypi.org/forums/viewtopic.php?p=1076713",[49],[431,7120,7121],{},"html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}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 .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}",{"title":69,"searchDepth":99,"depth":99,"links":7123},[7124,7125,7126,7127],{"id":6941,"depth":99,"text":6942},{"id":6984,"depth":99,"text":6985},{"id":7042,"depth":99,"text":7043},{"id":403,"depth":99,"text":404},"Learn how to run Selenium Webdriver on Raspberry Pi Zero W with the help of chromium-chromedriver package. This guide walks you through the process of installing and using the package to automate browser-based tasks","/assets/selenium-on-raspberry-pi.webp",{},"/blog/running-selenium-webdriver-on-raspberry-pi-zero",{"title":6909,"description":7128},"blog/running-selenium-webdriver-on-raspberry-pi-zero",[117],"skd-9z_WfLiFAjsxckjWal7_jhGylGE9JstPUQNnqNk",{"id":7137,"title":7138,"author":7,"authorTitle":8,"body":7139,"category":1845,"createdAt":7449,"description":7450,"extension":107,"image":7451,"meta":7452,"navigation":110,"path":7453,"proficiency":7454,"published":110,"readingTime":965,"seo":7455,"stem":7456,"tags":7457,"updatedAt":118,"__hash__":7458},"blog/blog/selenium-automate-browser-tasks.md","Selenium - Easy Web Automation with Python.",{"type":10,"value":7140,"toc":7439},[7141,7148,7152,7155,7159,7162,7166,7169,7189,7192,7196,7201,7214,7219,7236,7241,7258,7275,7298,7320,7342,7344,7348,7366,7372,7376,7405,7409,7415,7422,7425,7437],[13,7142,7143],{},[16,7144,7145,7147],{},[19,7146,21],{}," Selenium lets you control a browser programmatically with Python, turning multi-step manual tasks into a single command. This post demonstrates automating an everyday task -- opening an online epaper that normally requires opening the browser, searching, clicking through pages, and zooming. With Selenium and a chromedriver, you script the entire flow. The walkthrough covers setup, element selection, clicking, and handling browser interactions.",[137,7149,7151],{"id":7150},"selenium-web-automation-made-easy","Selenium - Web automation made easy.",[16,7153,7154],{},"I went through a course on udemy ( Automate boring stuff with python ) back in 2018, from there I got that we can automate many tasks from daily life.",[29,7156,7158],{"id":7157},"what-is-selenium","What is selenium ?",[16,7160,7161],{},"Selenium is widely used in writing automated tests for web-applications.\nSelenium lets you run an automated instance of web browser( chrome, firefox ), and gives you APIs so you can control the browser and webpage.",[29,7163,7165],{"id":7164},"lets-use-selenium-to-automate-tasks","Lets use selenium to automate tasks !!",[16,7167,7168],{},"Lets take a simple example of reading an online epaper.\nSteps to get to last point are,",[150,7170,7171,7174,7177,7180,7183,7186],{},[153,7172,7173],{},"Open the browser.",[153,7175,7176],{},"Go to the epaper website ( readwhere.com in my case ).",[153,7178,7179],{},"Search for the newspaper name.",[153,7181,7182],{},"Click on \"Read Now\".",[153,7184,7185],{},"Skip sign in.",[153,7187,7188],{},"Maximize/zoom for better reading.",[16,7190,7191],{},"Doing this could easily take 3-5 minutes.\nWith selenium we can sum it up to a single command.",[29,7193,7195],{"id":7194},"code-walkthrough","Code walkthrough",[150,7197,7198],{},[153,7199,7200],{},"I have taken a link to the newspaper name directly so we do not have to search for the newspaper.",[61,7202,7207],{"className":7203,"code":7204,"highlights":7205,"language":7206,"meta":69,"style":69},"language-py shiki shiki-themes github-light github-dark","url = 'https://www.readwhere.com/newspaper/deshonnati/Akola-Main/557?refquery=deshonnati%20akola'\n",[281,282,283,284],"py",[24,7208,7209],{"__ignoreMap":69},[289,7210,7212],{"class":7211,"line":281},[292,293],[289,7213,7204],{},[150,7215,7216],{"start":99},[153,7217,7218],{},"Below snippet opens up a browser instance, you need to pass corresponding driver ( chrome-chromedriver, firefox-gecodriver )",[61,7220,7223],{"className":7203,"code":7221,"highlights":7222,"language":7206,"meta":69,"style":69},"browser = webdriver.Firefox(\n  executable_path=r'/usr/local/bin/geckodriver')\n",[281,282,283,284],[24,7224,7225,7231],{"__ignoreMap":69},[289,7226,7228],{"class":7227,"line":281},[292,293],[289,7229,7230],{},"browser = webdriver.Firefox(\n",[289,7232,7233],{"class":292,"line":99},[289,7234,7235],{},"  executable_path=r'/usr/local/bin/geckodriver')\n",[150,7237,7238],{"start":282},[153,7239,7240],{},"In next step, we provide this browser which url it should load.",[61,7242,7245],{"className":7203,"code":7243,"highlights":7244,"language":7206,"meta":69,"style":69},"browser.get(url) # URL of newspaper\nbrowser.maximize_window()\n",[281,282,283,284],[24,7246,7247,7253],{"__ignoreMap":69},[289,7248,7250],{"class":7249,"line":281},[292,293],[289,7251,7252],{},"browser.get(url) # URL of newspaper\n",[289,7254,7255],{"class":292,"line":99},[289,7256,7257],{},"browser.maximize_window()\n",[150,7259,7260],{"start":283},[153,7261,7262,7263,7266,7267,7270,7271,7274],{},"Then we tell browser to perform certain clicks.\nWith ",[24,7264,7265],{},"WebDriverWait"," we tell browser to wait until the element appears on the screen and is clickable(",[24,7268,7269],{},"EC.element_to_be_clickable",").\nWe search the element by its DOM path called XPath (",[24,7272,7273],{},"By.XPATH","), and then perform a click.",[61,7276,7279],{"className":7203,"code":7277,"highlights":7278,"language":7206,"meta":69,"style":69},"# Click on \"READ NOW\"\nWebDriverWait(browser, 6).until(EC.element_to_be_clickable(\n        (By.XPATH, '/html/body/div[4]/div/div/div[3]/div/div[2]/div[1]/div[3]/a'))).click()\n",[281,282,283,284],[24,7280,7281,7287,7292],{"__ignoreMap":69},[289,7282,7284],{"class":7283,"line":281},[292,293],[289,7285,7286],{},"# Click on \"READ NOW\"\n",[289,7288,7289],{"class":292,"line":99},[289,7290,7291],{},"WebDriverWait(browser, 6).until(EC.element_to_be_clickable(\n",[289,7293,7295],{"class":7294,"line":282},[292,293],[289,7296,7297],{},"        (By.XPATH, '/html/body/div[4]/div/div/div[3]/div/div[2]/div[1]/div[3]/a'))).click()\n",[61,7299,7302],{"className":7203,"code":7300,"highlights":7301,"language":7206,"meta":69,"style":69},"# Click on \"Skip Sign in\"\nWebDriverWait(browser, 6).until(EC.element_to_be_clickable(\n        (By.XPATH, '//*[@id=\"skip-area-id\"]'))).click()\n",[281,282,283,284],[24,7303,7304,7310,7314],{"__ignoreMap":69},[289,7305,7307],{"class":7306,"line":281},[292,293],[289,7308,7309],{},"# Click on \"Skip Sign in\"\n",[289,7311,7312],{"class":292,"line":99},[289,7313,7291],{},[289,7315,7317],{"class":7316,"line":282},[292,293],[289,7318,7319],{},"        (By.XPATH, '//*[@id=\"skip-area-id\"]'))).click()\n",[61,7321,7324],{"className":7203,"code":7322,"highlights":7323,"language":7206,"meta":69,"style":69},"# Click on \"Zoom + button\"\nWebDriverWait(browser, 6).until(EC.element_to_be_clickable(\n        (By.XPATH, '/html/body/div[7]/div[1]/div[1]/div[4]/div/button[1]'))).click()\n",[281,282,283,284],[24,7325,7326,7332,7336],{"__ignoreMap":69},[289,7327,7329],{"class":7328,"line":281},[292,293],[289,7330,7331],{},"# Click on \"Zoom + button\"\n",[289,7333,7334],{"class":292,"line":99},[289,7335,7291],{},[289,7337,7339],{"class":7338,"line":282},[292,293],[289,7340,7341],{},"        (By.XPATH, '/html/body/div[7]/div[1]/div[1]/div[4]/div/button[1]'))).click()\n",[29,7343,4048],{"id":4047},[1981,7345,7347],{"id":7346},"how-to-find-xpath-of-the-browser-element","How to find xpath of the browser element.",[150,7349,7350,7356],{},[153,7351,7352,7353,2284],{},"Right click on the element, select ",[24,7354,7355],{},"inspect element",[153,7357,7358,7359,7362,7363],{},"In the elements tab right click on the element and select ",[24,7360,7361],{},"copy",", then select ",[24,7364,7365],{},"Copy XPath",[16,7367,7368],{},[94,7369],{"alt":7370,"src":7371},"image","/assets/find-xpath.webp",[1981,7373,7375],{"id":7374},"run-the-browser-in-headless-invisible-mode","Run the browser in headless ( invisible ) mode.",[61,7377,7380],{"className":7203,"code":7378,"highlights":7379,"language":7206,"meta":69,"style":69},"options = Options()\noptions.headless = True\nbrowser = webdriver.Firefox(options=options,\n   executable_path=r'/usr/local/bin/geckodriver')\n",[281,282,283,284],[24,7381,7382,7388,7393,7399],{"__ignoreMap":69},[289,7383,7385],{"class":7384,"line":281},[292,293],[289,7386,7387],{},"options = Options()\n",[289,7389,7390],{"class":292,"line":99},[289,7391,7392],{},"options.headless = True\n",[289,7394,7396],{"class":7395,"line":282},[292,293],[289,7397,7398],{},"browser = webdriver.Firefox(options=options,\n",[289,7400,7402],{"class":7401,"line":283},[292,293],[289,7403,7404],{},"   executable_path=r'/usr/local/bin/geckodriver')\n",[29,7406,7408],{"id":7407},"end-result","End Result",[16,7410,7411],{},[94,7412],{"alt":7413,"src":7414},"screengrab","https://raw.githubusercontent.com/ssghait007/pyclone/master/images/sg.gif",[16,7416,7417,7418],{},"Find the complete code ",[45,7419,3245],{"href":7420,"rel":7421},"https://github.com/ssghait007/pyclone/blob/e967c5c72047f056c73f4ed129653145b9f4a720/pyclone/__main__.py#L31",[49],[16,7423,7424],{},"Some other ideas you can try out.",[245,7426,7427,7434],{},[153,7428,7429,7430,7433],{},"Change config on router ( block/unblock certain domains, Limit speed on certain devices ). You will need to supply username and password for router in input boxes(",[24,7431,7432],{},"sendkeys()"," can be used for that). Make sure you are not publishing these username and password in public repos.",[153,7435,7436],{},"Clock in/out i.e. time booking for employees on company website. ( Again make sure to not disclose username and password on public repos, pass it from environment variables )",[431,7438,3290],{},{"title":69,"searchDepth":99,"depth":99,"links":7440},[7441,7442,7443,7444,7448],{"id":7157,"depth":99,"text":7158},{"id":7164,"depth":99,"text":7165},{"id":7194,"depth":99,"text":7195},{"id":4047,"depth":99,"text":4048,"children":7445},[7446,7447],{"id":7346,"depth":282,"text":7347},{"id":7374,"depth":282,"text":7375},{"id":7407,"depth":99,"text":7408},"2021-04-13T07:00:13.392Z","Automate browser-based tasks with Python's Selenium module. Learn how to control a browser with code and perform tasks with ease.","/assets/selenium.webp",{},"/blog/selenium-automate-browser-tasks","advanced",{"title":7138,"description":7450},"blog/selenium-automate-browser-tasks",[117,3797],"F60DgR3Iwu_z_KbNo5LPaOsIeEMEPyUhjj-TL5SAqxU",{"id":7460,"title":7461,"author":7,"authorTitle":8,"body":7462,"category":161,"createdAt":7983,"description":7984,"extension":107,"image":7985,"meta":7986,"navigation":110,"path":7987,"proficiency":448,"published":110,"readingTime":965,"seo":7988,"stem":7989,"tags":7990,"updatedAt":118,"__hash__":7993},"blog/blog/shamirs-secret-sharing.md","Breaking Bad Habits with Shamir's Secret Sharing",{"type":10,"value":7463,"toc":7964},[7464,7471,7477,7480,7483,7486,7490,7493,7496,7502,7506,7509,7532,7535,7539,7546,7550,7591,7595,7613,7616,7622,7626,7643,7646,7652,7656,7662,7668,7674,7680,7684,7688,7714,7718,7738,7742,7745,7851,7855,7861,7867,7873,7879,7883,7915,7917,7920,7923,7926,7932,7935,7961],[13,7465,7466],{},[16,7467,7468,7470],{},[19,7469,21],{}," Inspired by Atomic Habits' principle of making bad habits harder, this post uses Shamir's Secret Sharing to fight doom scrolling addiction. The idea: split your Pi-hole admin password into 8 shares hidden across digital and physical locations, requiring any 4 to reconstruct the original. Each individual share reveals nothing. The post explains the cryptographic algorithm (polynomial interpolation over finite fields), walks through practical usage with code examples, and shares creative ideas for where to hide your shares.",[16,7472,7473],{},[94,7474],{"alt":7475,"src":7476},"You can see me","https://media2.giphy.com/media/v1.Y2lkPTc5MGI3NjExOXdodGJiZXRrbWt6N2tvOGxsaDRmbDd4aXJuc205ZHhyNDhyZmk5ZCZlcD12MV9naWZzX3NlYXJjaCZjdD1n/5x0XvhK2zDgKHt0D86/200.webp",[16,7478,7479],{},"I had a problem. I kept turning off my PiHole's social media blocking to mindlessly scroll through feeds. It was too easy - just login to the admin panel, disable restrictions, and boom - instant access to my time-wasting websites.",[16,7481,7482],{},"After reading Atomic Habits, I realized I needed to make bad habits harder to do. That's when I discovered Shamir's Secret Sharing scheme - a cryptographic technique that lets you split a secret into multiple pieces where you only need some of them to reconstruct it.",[16,7484,7485],{},"My solution? Split my PiHole admin password into 8 chunks and hide them across different digital and physical locations. Now when I want to disable restrictions, I need to really think about whether it's worth the effort of collecting 4 pieces.",[29,7487,7489],{"id":7488},"what-is-shamirs-secret-sharing","What is Shamir's Secret Sharing",[16,7491,7492],{},"Shamir's Secret Sharing is a cryptographic algorithm that divides a secret into N shares, where any K shares can reconstruct the original secret, but K-1 or fewer shares reveal nothing about the secret.",[16,7494,7495],{},"The beauty is in the mathematics - it uses polynomial interpolation over finite fields. But you don't need to understand the math to use it effectively.",[16,7497,7498],{},[94,7499],{"alt":7500,"src":7501},"Shamir Secret Sharing Concept","/assets/shamir-concept-diagram.webp",[29,7503,7505],{"id":7504},"how-it-works-in-practice","How It Works in Practice",[16,7507,7508],{},"Let's say you want to split your master password into 8 shares, requiring any 4 shares to reconstruct it:",[150,7510,7511,7520,7526],{},[153,7512,7513,7516,7517],{},[19,7514,7515],{},"Split Phase",": Your password gets converted into shares like ",[24,7518,7519],{},"1-abc123def456",[153,7521,7522,7525],{},[19,7523,7524],{},"Distribution",": You store each share in different locations or give them to different people",[153,7527,7528,7531],{},[19,7529,7530],{},"Recovery",": When you need the password, collect any 4 shares and reconstruct the original",[16,7533,7534],{},"The key insight: each individual share is completely useless. Someone with 3 shares has exactly the same information as someone with 0 shares.",[29,7536,7538],{"id":7537},"building-a-practical-tool","Building a Practical Tool",[16,7540,7541,7542,7545],{},"I built a simple bash implementation that uses the ",[24,7543,7544],{},"ssss"," (Shamir's Secret Sharing Scheme) library. Here's how it works:",[1981,7547,7549],{"id":7548},"installation","Installation",[61,7551,7553],{"className":278,"code":7552,"language":285,"meta":69,"style":69},"# macOS\nbrew install ssss\n\n# Linux\nsudo apt install ssss\n",[24,7554,7555,7560,7570,7574,7579],{"__ignoreMap":69},[289,7556,7557],{"class":292,"line":281},[289,7558,7559],{"class":1681},"# macOS\n",[289,7561,7562,7565,7567],{"class":292,"line":99},[289,7563,7564],{"class":296},"brew",[289,7566,1140],{"class":300},[289,7568,7569],{"class":300}," ssss\n",[289,7571,7572],{"class":292,"line":282},[289,7573,3072],{"emptyLinePlaceholder":110},[289,7575,7576],{"class":292,"line":283},[289,7577,7578],{"class":1681},"# Linux\n",[289,7580,7581,7584,7587,7589],{"class":292,"line":284},[289,7582,7583],{"class":296},"sudo",[289,7585,7586],{"class":300}," apt",[289,7588,1140],{"class":300},[289,7590,7569],{"class":300},[1981,7592,7594],{"id":7593},"split-a-password","Split a Password",[61,7596,7598],{"className":278,"code":7597,"language":285,"meta":69,"style":69},"# Use the unified CLI tool\n./shamir.sh split\n",[24,7599,7600,7605],{"__ignoreMap":69},[289,7601,7602],{"class":292,"line":281},[289,7603,7604],{"class":1681},"# Use the unified CLI tool\n",[289,7606,7607,7610],{"class":292,"line":99},[289,7608,7609],{"class":296},"./shamir.sh",[289,7611,7612],{"class":300}," split\n",[16,7614,7615],{},"This prompts you to enter your password (hidden input) and generates 8 shares like:",[61,7617,7620],{"className":7618,"code":7619,"language":66},[64],"1-abcd1234efgh5678\n2-ijkl9012mnop3456  \n3-qrst7890uvwx1234\n4-yzab5678cdef9012\n5-ghij3456klmn7890\n6-opqr1234stuv5678\n7-wxyz9012abcd3456\n8-efgh7890ijkl1234\n",[24,7621,7619],{"__ignoreMap":69},[1981,7623,7625],{"id":7624},"reconstruct-the-password","Reconstruct the Password",[61,7627,7629],{"className":278,"code":7628,"language":285,"meta":69,"style":69},"# Use the unified CLI tool  \n./shamir.sh reconstruct\n",[24,7630,7631,7636],{"__ignoreMap":69},[289,7632,7633],{"class":292,"line":281},[289,7634,7635],{"class":1681},"# Use the unified CLI tool  \n",[289,7637,7638,7640],{"class":292,"line":99},[289,7639,7609],{"class":296},[289,7641,7642],{"class":300}," reconstruct\n",[16,7644,7645],{},"The reconstruction script has a nice ritual-like interface:",[61,7647,7650],{"className":7648,"code":7649,"language":66},[64],"🔐 Password Reconstruction Ritual 🔐\n====================================\n\nPreparing to unlock your secret...\nI need exactly 4 shares to proceed.\n\n🔑 Enter share 1/4: 2-ijkl9012mnop3456\n   ✓ Share 1 accepted\n🔑 Enter share 2/4: 5-ghij3456klmn7890\n   ✓ Share 2 accepted\n🔑 Enter share 3/4: 7-wxyz9012abcd3456\n   ✓ Share 3 accepted\n🔑 Enter share 4/4: 1-abcd1234efgh5678\n   ✓ Share 4 accepted\n\n🔮 Combining the fragments...\n   Invoking ancient mathematics...\n   Reconstructing the secret...\n\n🎉 SUCCESS! 🎉\n==============\n\nYour password has been revealed:\n🔓 [your original password] 🔓\n",[24,7651,7649],{"__ignoreMap":69},[29,7653,7655],{"id":7654},"real-world-use-cases","Real-World Use Cases",[16,7657,7658,7661],{},[19,7659,7660],{},"Breaking Bad Habits (My Use Case)","\nSplit your PiHole admin password to make social media access harder. Store shares in different locations - some in password managers, others as physical notes. The friction of collecting shares makes you reconsider impulse decisions.",[16,7663,7664,7667],{},[19,7665,7666],{},"Team Password Management","\nSplit critical system passwords among team leads. No single person can access production, but any 3 out of 5 leads can recover passwords during emergencies.",[16,7669,7670,7673],{},[19,7671,7672],{},"Personal Backup Strategy","\nStore shares in different locations: cloud storage, physical notes, trusted family members. Even if you lose access to some shares, you can still recover your passwords.",[16,7675,7676,7679],{},[19,7677,7678],{},"Corporate Compliance","\nMeet security requirements that mandate multiple approvals for sensitive access. The cryptographic guarantee means you can't cheat the system.",[29,7681,7683],{"id":7682},"security-considerations","Security Considerations",[1981,7685,7687],{"id":7686},"distribution-strategy","Distribution Strategy",[245,7689,7690,7696,7702,7708],{},[153,7691,7692,7695],{},[19,7693,7694],{},"Never store all shares together"," - defeats the entire purpose",[153,7697,7698,7701],{},[19,7699,7700],{},"Different storage methods"," - mix digital and physical storage",[153,7703,7704,7707],{},[19,7705,7706],{},"Geographic distribution"," - store shares in different locations",[153,7709,7710,7713],{},[19,7711,7712],{},"Access control"," - limit who has access to each share",[1981,7715,7717],{"id":7716},"threat-model","Threat Model",[245,7719,7720,7726,7732],{},[153,7721,7722,7725],{},[19,7723,7724],{},"Insider threats",": Even malicious insiders can't access secrets with insufficient shares",[153,7727,7728,7731],{},[19,7729,7730],{},"Data breaches",": Compromising one storage location doesn't reveal the secret",[153,7733,7734,7737],{},[19,7735,7736],{},"Key person risk",": System doesn't break if some people leave or become unavailable",[29,7739,7741],{"id":7740},"advanced-features","Advanced Features",[16,7743,7744],{},"You can customize the scheme for different security needs:",[61,7746,7748],{"className":278,"code":7747,"language":285,"meta":69,"style":69},"# High security: 10 shares, need 7 to reconstruct\necho \"$PASSWORD\" | ssss-split -t 7 -n 10\n\n# Lower threshold: 6 shares, need 2 to reconstruct\necho \"$PASSWORD\" | ssss-split -t 2 -n 6\n\n# Enterprise: 15 shares, need 8 to reconstruct\necho \"$PASSWORD\" | ssss-split -t 8 -n 15\n",[24,7749,7750,7755,7785,7789,7794,7818,7822,7827],{"__ignoreMap":69},[289,7751,7752],{"class":292,"line":281},[289,7753,7754],{"class":1681},"# High security: 10 shares, need 7 to reconstruct\n",[289,7756,7757,7760,7763,7766,7768,7770,7773,7776,7779,7782],{"class":292,"line":99},[289,7758,7759],{"class":304},"echo",[289,7761,7762],{"class":300}," \"",[289,7764,7765],{"class":1456},"$PASSWORD",[289,7767,1621],{"class":300},[289,7769,312],{"class":311},[289,7771,7772],{"class":296}," ssss-split",[289,7774,7775],{"class":304}," -t",[289,7777,7778],{"class":304}," 7",[289,7780,7781],{"class":304}," -n",[289,7783,7784],{"class":304}," 10\n",[289,7786,7787],{"class":292,"line":282},[289,7788,3072],{"emptyLinePlaceholder":110},[289,7790,7791],{"class":292,"line":283},[289,7792,7793],{"class":1681},"# Lower threshold: 6 shares, need 2 to reconstruct\n",[289,7795,7796,7798,7800,7802,7804,7806,7808,7810,7813,7815],{"class":292,"line":284},[289,7797,7759],{"class":304},[289,7799,7762],{"class":300},[289,7801,7765],{"class":1456},[289,7803,1621],{"class":300},[289,7805,312],{"class":311},[289,7807,7772],{"class":296},[289,7809,7775],{"class":304},[289,7811,7812],{"class":304}," 2",[289,7814,7781],{"class":304},[289,7816,7817],{"class":304}," 6\n",[289,7819,7820],{"class":292,"line":1503},[289,7821,3072],{"emptyLinePlaceholder":110},[289,7823,7824],{"class":292,"line":1511},[289,7825,7826],{"class":1681},"# Enterprise: 15 shares, need 8 to reconstruct\n",[289,7828,7829,7831,7833,7835,7837,7839,7841,7843,7846,7848],{"class":292,"line":1524},[289,7830,7759],{"class":304},[289,7832,7762],{"class":300},[289,7834,7765],{"class":1456},[289,7836,1621],{"class":300},[289,7838,312],{"class":311},[289,7840,7772],{"class":296},[289,7842,7775],{"class":304},[289,7844,7845],{"class":304}," 8",[289,7847,7781],{"class":304},[289,7849,7850],{"class":304}," 15\n",[29,7852,7854],{"id":7853},"common-mistakes-to-avoid","Common Mistakes to Avoid",[16,7856,7857,7860],{},[19,7858,7859],{},"Storing shares together"," - I've seen people save all shares in the same password manager. This completely defeats the purpose.",[16,7862,7863,7866],{},[19,7864,7865],{},"Not testing reconstruction"," - Always verify you can reconstruct your secret before relying on the shares.",[16,7868,7869,7872],{},[19,7870,7871],{},"Ignoring share protection"," - Each share should be stored securely. They're not the original secret, but they're still sensitive.",[16,7874,7875,7878],{},[19,7876,7877],{},"Poor documentation"," - Keep track of which shares you have and where they're stored, otherwise recovery becomes impossible.",[29,7880,7882],{"id":7881},"implementation-tips","Implementation Tips",[150,7884,7885,7891,7897,7903,7909],{},[153,7886,7887,7890],{},[19,7888,7889],{},"Start simple"," - Begin with a (4,8) scheme (4 needed, 8 total) for most use cases",[153,7892,7893,7896],{},[19,7894,7895],{},"Test thoroughly"," - Always verify reconstruction works before deploying",[153,7898,7899,7902],{},[19,7900,7901],{},"Document everything"," - Keep clear records of your sharing scheme",[153,7904,7905,7908],{},[19,7906,7907],{},"Regular audits"," - Periodically verify you can still access your shares",[153,7910,7911,7914],{},[19,7912,7913],{},"Have a backup plan"," - Consider multiple sharing schemes for critical secrets",[29,7916,4074],{"id":1780},[16,7918,7919],{},"Shamir's Secret Sharing solves a fundamental problem in security: how to protect secrets while ensuring availability. It's not just theoretical cryptography - it's a practical tool that can improve your security posture immediately.",[16,7921,7922],{},"The beauty is in its simplicity once implemented. You get mathematical guarantees about security while solving real business problems around access control and availability.",[16,7924,7925],{},"Whether you're breaking bad habits like I did with social media, managing team passwords, backing up personal credentials, or meeting enterprise compliance requirements, Shamir's Secret Sharing provides an elegant solution that balances security with practicality.",[16,7927,7928],{},[94,7929],{"alt":7930,"src":7931},"You can't see me","https://media0.giphy.com/media/v1.Y2lkPTc5MGI3NjExOXdodGJiZXRrbWt6N2tvOGxsaDRmbDd4aXJuc205ZHhyNDhyZmk5ZCZlcD12MV9naWZzX3NlYXJjaCZjdD1n/3o6ZtjDNG2UXy7B3xK/200.webp",[29,7933,7934],{"id":1807},"Resources",[245,7936,7937,7945,7953],{},[153,7938,7939,7944],{},[45,7940,7943],{"href":7941,"rel":7942},"https://github.com/ssghait007/ssss",[49],"Complete Implementation"," - Ready-to-use bash scripts",[153,7946,7947,7952],{},[45,7948,7951],{"href":7949,"rel":7950},"https://www.geeksforgeeks.org/computer-networks/shamirs-secret-sharing-algorithm-cryptography/",[49],"ssss in details"," - Core implementation details",[153,7954,7955,7960],{},[45,7956,7959],{"href":7957,"rel":7958},"https://cs.jhu.edu/~sdoshi/crypto/papers/shamirturing.pdf",[49],"Technical Paper"," - Original Shamir paper",[431,7962,7963],{},"html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}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 .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}",{"title":69,"searchDepth":99,"depth":99,"links":7965},[7966,7967,7968,7973,7974,7978,7979,7980,7981,7982],{"id":7488,"depth":99,"text":7489},{"id":7504,"depth":99,"text":7505},{"id":7537,"depth":99,"text":7538,"children":7969},[7970,7971,7972],{"id":7548,"depth":282,"text":7549},{"id":7593,"depth":282,"text":7594},{"id":7624,"depth":282,"text":7625},{"id":7654,"depth":99,"text":7655},{"id":7682,"depth":99,"text":7683,"children":7975},[7976,7977],{"id":7686,"depth":282,"text":7687},{"id":7716,"depth":282,"text":7717},{"id":7740,"depth":99,"text":7741},{"id":7853,"depth":99,"text":7854},{"id":7881,"depth":99,"text":7882},{"id":1780,"depth":99,"text":4074},{"id":1807,"depth":99,"text":7934},"2025-08-16T07:00:13.392Z","How I used cryptography to break my doom scrolling addiction. Learn to split sensitive passwords into multiple shares, making it harder to give in to impulses while keeping recovery possible when truly needed.","/assets/shamirs-secret-sharing.webp",{},"/blog/shamirs-secret-sharing",{"title":7461,"description":7984},"blog/shamirs-secret-sharing",[117,7991,7992],"cryptography","security","sSkW3XA5saEf12AeW70vusawnOYkCxsFiNvtIGiMhZE",{"id":7995,"title":7996,"author":7,"authorTitle":8,"body":7997,"category":161,"createdAt":8519,"description":8520,"extension":107,"image":8521,"meta":8522,"navigation":110,"path":8523,"proficiency":112,"published":110,"readingTime":2927,"seo":8524,"stem":8525,"tags":8526,"updatedAt":118,"__hash__":8527},"blog/blog/steganography.md","Steganography - The Art of Hiding Data in Plain Sight",{"type":10,"value":7998,"toc":8510},[7999,8010,8013,8023,8026,8030,8033,8036,8040,8043,8075,8079,8082,8088,8091,8096,8099,8106,8111,8114,8118,8126,8130,8151,8155,8260,8264,8366,8455,8459,8476,8480,8491,8493,8507],[13,8000,8001],{},[16,8002,8003,8005,8006,8009],{},[19,8004,21],{}," Inspired by a Mr. Robot episode where RSA keys are hidden inside an image, this post explores steganography -- the practice of concealing messages within ordinary-looking media. It covers five types: text, image, audio, video, and network steganography, explaining how each works at a high level. Includes a hands-on Python example using the ",[24,8007,8008],{},"cryptosteganography"," library to hide and extract secret messages from images, plus a list of free steganography tools.",[137,8011,7996],{"id":8012},"steganography-the-art-of-hiding-data-in-plain-sight",[16,8014,8015,8016,8019,8020,2284],{},"I was watching a web-show(",[24,8017,8018],{},"Mr-Robot",") on a weekend. It's a story of how a guy hacks into data servers of big conglomerate, and encrypts all data with a cryptographic keys. After the hack was successful he stores the RSA keys to decrypt this data ",[24,8021,8022],{},"inside an image",[16,8024,8025],{},"This got me interested in the fact that we can store secret messages and keys. So I researched on this topic and got some basic methods of how it is practically achieved.",[29,8027,8029],{"id":8028},"what-is-steganography","What is Steganography",[16,8031,8032],{},"Steganography is a very ancient method, It is the practice of concealing a message within another message or a physical object.",[16,8034,8035],{},"The ancient form was like, sending messages on paper with invisible ink.",[29,8037,8039],{"id":8038},"types-of-steganography","Types of Steganography",[16,8041,8042],{},"Over years its evolved and now used digitally with various forms of media like",[150,8044,8045,8051,8057,8063,8069],{},[153,8046,8047,8050],{},[19,8048,8049],{},"Text"," - hide data inside another text, ex. embed a word every 5th word of paragraph.",[153,8052,8053,8056],{},[19,8054,8055],{},"Image"," - hide data inside image, replace pixels info with secret data in such a way there is minimal/un-noticable changes to image.",[153,8058,8059,8062],{},[19,8060,8061],{},"Audio"," - hide data in audio file, ex. add hidden data by modify audio in imperceptible way.",[153,8064,8065,8068],{},[19,8066,8067],{},"Video"," - hide data in video file. As videos have large size, its is very easy to hide any data/file type inside it.",[153,8070,8071,8074],{},[19,8072,8073],{},"Network"," - hide data in network protocols ex. hide secret in header or payload or mixing it in both.",[29,8076,8078],{"id":8077},"methods-used-in-image-steganography","Methods used in Image Steganography",[16,8080,8081],{},"There are many methods used in image steganography, I have listed very basic and simple ones.",[16,8083,8084,8087],{},[19,8085,8086],{},"LSB"," (Least significant bit)",[16,8089,8090],{},"It is a technique in which least significant bit of pixel data is replaced with data bit. It's very simple method and difference in images are undetected by naked eye.",[16,8092,8093],{},[19,8094,8095],{},"DCT",[16,8097,8098],{},"This technique involves changing values of quantized DCT coefficients.",[16,8100,8101,8102,2284],{},"Read more on DCT ",[45,8103,3245],{"href":8104,"rel":8105},"https://www.youtube.com/watch?v=Q2aEzeMDHMA",[49],[16,8107,8108],{},[19,8109,8110],{},"JSTEG",[16,8112,8113],{},"It is a steganography algorithm based on LSB replacement method for hiding data in DCT coefficients of JPEG images. The algorithm replaces the LSB of DCT coefficients by bits of the secret message to be hidden",[29,8115,8117],{"id":8116},"simple-example-in-python","Simple example in python",[16,8119,8120,8121,8125],{},"I am using python library (",[45,8122,8008],{"href":8123,"rel":8124},"https://pypi.org/project/cryptosteganography/",[49],") for demonstrating this.",[258,8127,8129],{"id":8128},"install-the-package","Install the package",[61,8131,8134],{"className":278,"code":8132,"highlights":8133,"language":285,"meta":69,"style":69},"\npip3 install cryptosteganography\n\n",[281,282,283,284],[24,8135,8136,8141],{"__ignoreMap":69},[289,8137,8139],{"class":8138,"line":281},[292,293],[289,8140,3072],{"emptyLinePlaceholder":110},[289,8142,8143,8146,8148],{"class":292,"line":99},[289,8144,8145],{"class":296},"pip3",[289,8147,1140],{"class":300},[289,8149,8150],{"class":300}," cryptosteganography\n",[258,8152,8154],{"id":8153},"hide-data-inside-cover-image","Hide data inside cover image",[61,8156,8159],{"className":7203,"code":8157,"highlights":8158,"language":7206,"meta":69,"style":69},"\nfrom cryptosteganography import CryptoSteganography\n\n\n\n# Initialise package with password key\n\ncrypto_steganography = CryptoSteganography('password key')\n\n\n\n\n\nmessage = 'Super secret message. That I want to send secretly to someone. No one  in middle should be able to read this'\n\n\n\n# Hide message inside output image(`output_stego_image.png`).\n\ncrypto_steganography.hide(\n\n    'cover_image.png', 'output_stego_image.png', message)\n\n",[281,282,283,284],[24,8160,8161,8166,8171,8176,8181,8186,8191,8195,8200,8204,8208,8212,8216,8220,8225,8229,8233,8237,8242,8246,8251,8255],{"__ignoreMap":69},[289,8162,8164],{"class":8163,"line":281},[292,293],[289,8165,3072],{"emptyLinePlaceholder":110},[289,8167,8168],{"class":292,"line":99},[289,8169,8170],{},"from cryptosteganography import CryptoSteganography\n",[289,8172,8174],{"class":8173,"line":282},[292,293],[289,8175,3072],{"emptyLinePlaceholder":110},[289,8177,8179],{"class":8178,"line":283},[292,293],[289,8180,3072],{"emptyLinePlaceholder":110},[289,8182,8184],{"class":8183,"line":284},[292,293],[289,8185,3072],{"emptyLinePlaceholder":110},[289,8187,8188],{"class":292,"line":1503},[289,8189,8190],{},"# Initialise package with password key\n",[289,8192,8193],{"class":292,"line":1511},[289,8194,3072],{"emptyLinePlaceholder":110},[289,8196,8197],{"class":292,"line":1524},[289,8198,8199],{},"crypto_steganography = CryptoSteganography('password key')\n",[289,8201,8202],{"class":292,"line":1537},[289,8203,3072],{"emptyLinePlaceholder":110},[289,8205,8206],{"class":292,"line":1550},[289,8207,3072],{"emptyLinePlaceholder":110},[289,8209,8210],{"class":292,"line":1563},[289,8211,3072],{"emptyLinePlaceholder":110},[289,8213,8214],{"class":292,"line":1574},[289,8215,3072],{"emptyLinePlaceholder":110},[289,8217,8218],{"class":292,"line":1580},[289,8219,3072],{"emptyLinePlaceholder":110},[289,8221,8222],{"class":292,"line":1586},[289,8223,8224],{},"message = 'Super secret message. That I want to send secretly to someone. No one  in middle should be able to read this'\n",[289,8226,8227],{"class":292,"line":1592},[289,8228,3072],{"emptyLinePlaceholder":110},[289,8230,8231],{"class":292,"line":3128},[289,8232,3072],{"emptyLinePlaceholder":110},[289,8234,8235],{"class":292,"line":3134},[289,8236,3072],{"emptyLinePlaceholder":110},[289,8238,8239],{"class":292,"line":3140},[289,8240,8241],{},"# Hide message inside output image(`output_stego_image.png`).\n",[289,8243,8244],{"class":292,"line":3146},[289,8245,3072],{"emptyLinePlaceholder":110},[289,8247,8248],{"class":292,"line":3152},[289,8249,8250],{},"crypto_steganography.hide(\n",[289,8252,8253],{"class":292,"line":3158},[289,8254,3072],{"emptyLinePlaceholder":110},[289,8256,8257],{"class":292,"line":3164},[289,8258,8259],{},"    'cover_image.png', 'output_stego_image.png', message)\n",[258,8261,8263],{"id":8262},"extract-data-from-stego-image","Extract data from stego image",[61,8265,8268],{"className":7203,"code":8266,"highlights":8267,"language":7206,"meta":69,"style":69},"\n# retrieve_message.py\n\nfrom cryptosteganography import CryptoSteganography\n\n\n\n# Initialise package with password key\n\ncrypto_steganography = CryptoSteganography('password key')\n\n\n\n\n\n# Extract the message from stego image.\n\nsecret = crypto_steganography.retrieve('output_stego_image.png')\n\n\n\nprint(secret)\n\n",[281,282,283,284],[24,8269,8270,8275,8280,8285,8290,8295,8299,8303,8307,8311,8315,8319,8323,8327,8331,8335,8340,8344,8349,8353,8357,8361],{"__ignoreMap":69},[289,8271,8273],{"class":8272,"line":281},[292,293],[289,8274,3072],{"emptyLinePlaceholder":110},[289,8276,8277],{"class":292,"line":99},[289,8278,8279],{},"# retrieve_message.py\n",[289,8281,8283],{"class":8282,"line":282},[292,293],[289,8284,3072],{"emptyLinePlaceholder":110},[289,8286,8288],{"class":8287,"line":283},[292,293],[289,8289,8170],{},[289,8291,8293],{"class":8292,"line":284},[292,293],[289,8294,3072],{"emptyLinePlaceholder":110},[289,8296,8297],{"class":292,"line":1503},[289,8298,3072],{"emptyLinePlaceholder":110},[289,8300,8301],{"class":292,"line":1511},[289,8302,3072],{"emptyLinePlaceholder":110},[289,8304,8305],{"class":292,"line":1524},[289,8306,8190],{},[289,8308,8309],{"class":292,"line":1537},[289,8310,3072],{"emptyLinePlaceholder":110},[289,8312,8313],{"class":292,"line":1550},[289,8314,8199],{},[289,8316,8317],{"class":292,"line":1563},[289,8318,3072],{"emptyLinePlaceholder":110},[289,8320,8321],{"class":292,"line":1574},[289,8322,3072],{"emptyLinePlaceholder":110},[289,8324,8325],{"class":292,"line":1580},[289,8326,3072],{"emptyLinePlaceholder":110},[289,8328,8329],{"class":292,"line":1586},[289,8330,3072],{"emptyLinePlaceholder":110},[289,8332,8333],{"class":292,"line":1592},[289,8334,3072],{"emptyLinePlaceholder":110},[289,8336,8337],{"class":292,"line":3128},[289,8338,8339],{},"# Extract the message from stego image.\n",[289,8341,8342],{"class":292,"line":3134},[289,8343,3072],{"emptyLinePlaceholder":110},[289,8345,8346],{"class":292,"line":3140},[289,8347,8348],{},"secret = crypto_steganography.retrieve('output_stego_image.png')\n",[289,8350,8351],{"class":292,"line":3146},[289,8352,3072],{"emptyLinePlaceholder":110},[289,8354,8355],{"class":292,"line":3152},[289,8356,3072],{"emptyLinePlaceholder":110},[289,8358,8359],{"class":292,"line":3158},[289,8360,3072],{"emptyLinePlaceholder":110},[289,8362,8363],{"class":292,"line":3164},[289,8364,8365],{},"print(secret)\n",[61,8367,8370],{"className":278,"code":8368,"highlights":8369,"language":285,"meta":69,"style":69},"\n$ python3 retrieve_message.py\n\nSuper secret message. That I want to send secretly to someone. No one in middle should be able to read this\n\n",[281,282,283,284],[24,8371,8372,8377,8387,8392],{"__ignoreMap":69},[289,8373,8375],{"class":8374,"line":281},[292,293],[289,8376,3072],{"emptyLinePlaceholder":110},[289,8378,8379,8381,8384],{"class":292,"line":99},[289,8380,297],{"class":296},[289,8382,8383],{"class":300}," python3",[289,8385,8386],{"class":300}," retrieve_message.py\n",[289,8388,8390],{"class":8389,"line":282},[292,293],[289,8391,3072],{"emptyLinePlaceholder":110},[289,8393,8395,8398,8401,8404,8407,8409,8412,8415,8418,8421,8423,8426,8429,8432,8435,8438,8441,8444,8447,8449,8452],{"class":8394,"line":283},[292,293],[289,8396,8397],{"class":296},"Super",[289,8399,8400],{"class":300}," secret",[289,8402,8403],{"class":300}," message.",[289,8405,8406],{"class":300}," That",[289,8408,3900],{"class":300},[289,8410,8411],{"class":300}," want",[289,8413,8414],{"class":300}," to",[289,8416,8417],{"class":300}," send",[289,8419,8420],{"class":300}," secretly",[289,8422,8414],{"class":300},[289,8424,8425],{"class":300}," someone.",[289,8427,8428],{"class":300}," No",[289,8430,8431],{"class":300}," one",[289,8433,8434],{"class":300}," in",[289,8436,8437],{"class":300}," middle",[289,8439,8440],{"class":300}," should",[289,8442,8443],{"class":300}," be",[289,8445,8446],{"class":300}," able",[289,8448,8414],{"class":300},[289,8450,8451],{"class":300}," read",[289,8453,8454],{"class":300}," this\n",[29,8456,8458],{"id":8457},"free-available-tools","Free available tools",[245,8460,8461,8464,8467,8470,8473],{},[153,8462,8463],{},"Openstego",[153,8465,8466],{},"Steghide",[153,8468,8469],{},"SSuite Piscel",[153,8471,8472],{},"Xiao Steganography",[153,8474,8475],{},"Hide’N’Send",[29,8477,8479],{"id":8478},"uses","Uses",[245,8481,8482,8485,8488],{},[153,8483,8484],{},"embed copyright messages in media files",[153,8486,8487],{},"send secret info to someone",[153,8489,8490],{},"It's very popular in cyber crimes, Hence very important for white hat hackers.",[29,8492,5287],{"id":5286},[245,8494,8495,8501],{},[153,8496,8497],{},[45,8498,8499],{"href":8499,"rel":8500},"https://www.youtube.com/watch?v=xepNoHgNj0w",[49],[153,8502,8503],{},[45,8504,8505],{"href":8505,"rel":8506},"https://www.youtube.com/watch?v=TWEXCYQKyDc",[49],[431,8508,8509],{},"html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}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 .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":69,"searchDepth":99,"depth":99,"links":8511},[8512,8513,8514,8515,8516,8517,8518],{"id":8028,"depth":99,"text":8029},{"id":8038,"depth":99,"text":8039},{"id":8077,"depth":99,"text":8078},{"id":8116,"depth":99,"text":8117},{"id":8457,"depth":99,"text":8458},{"id":8478,"depth":99,"text":8479},{"id":5286,"depth":99,"text":5287},"2021-06-19T07:00:13.392Z","Learn about Steganography, a method of concealing messages in plain sight by using various digital forms of media like text, image, audio, video, and network steganography. Get an example of image steganography using the Python library cryptosteganography and a list of free tools available.","/assets/stegano.webp",{},"/blog/steganography",{"title":7996,"description":8520},"blog/steganography",[117],"KzARBMc2H9NVARhzOJoSxsEjp3l-WgSGsaW96-Yk-vA",{"id":8529,"title":8530,"author":7,"authorTitle":8,"body":8531,"category":161,"createdAt":8726,"description":8727,"extension":107,"image":8728,"meta":8729,"navigation":110,"path":8730,"proficiency":112,"published":110,"readingTime":965,"seo":8731,"stem":8732,"tags":8733,"updatedAt":118,"__hash__":8734},"blog/blog/useful-slack-tips.md","The complete guide for Slack productivity.",{"type":10,"value":8532,"toc":8717},[8533,8540,8543,8546,8549,8553,8599,8603,8606,8609,8612,8619,8623,8626,8629,8649,8652,8656,8659,8663,8666,8669,8672,8676,8679,8682,8686,8689,8692],[13,8534,8535],{},[16,8536,8537,8539],{},[19,8538,21],{}," Slack is more than a chat app -- knowing its features well can save you significant time daily. This post covers 7 productivity tips: keyboard shortcuts (Cmd+K, Cmd+F, arrow keys), grouping channels into custom sections, slash commands for quick actions, muting noisy channels, powerful search for messages/files/people, using saved items as a bookmark system, and integrating apps like GitHub, Jira, and Google Calendar directly into your workspace.",[137,8541,8530],{"id":8542},"the-complete-guide-for-slack-productivity",[16,8544,8545],{},"Slack has become much more than a communication platform.\nThere are tons of features and integrations in slack.",[16,8547,8548],{},"As a developer you should know how to use your tools efficiently.\nBelow 7 tips and tricks will help you do more and save time.",[29,8550,8552],{"id":8551},"_1-use-shortcuts-listing-mac-shortcuts","1. Use shortcuts (listing mac shortcuts)",[245,8554,8555,8568,8576,8583],{},[153,8556,8557,8560,8561,571,8564,8567],{},[24,8558,8559],{},"cmd"," + ",[24,8562,8563],{},"[",[24,8565,8566],{},"]"," - Navigate between old/new chats",[153,8569,8570,8560,8572,8575],{},[24,8571,8559],{},[24,8573,8574],{},"k"," - Search for channel, people, files",[153,8577,8578,8560,8580,8582],{},[24,8579,8559],{},[24,8581,6462],{}," - Search messages or files in a chat/channel",[153,8584,8585,8588,8589,8591,8592,8594,8595],{},[24,8586,8587],{},"Up Arrow"," - Edit last message you sent",[2952,8590],{},"You can find and remember other shortcuts if you have any actions you do repeatedly.",[2952,8593],{},"Find more shortcuts ",[45,8596,3245],{"href":8597,"rel":8598},"https://slack.com/intl/en-in/help/articles/201374536-Slack-keyboard-shortcuts-and-commands",[49],[29,8600,8602],{"id":8601},"_2-group-channels-create-new-section","2. Group channels (create new section)",[16,8604,8605],{},"As you get added to more and more channels it gets difficult to keep track whats going on where.",[16,8607,8608],{},"Its like a messy table, You can group the channels by sections.\nYou can name these section anything(ex. Org, Team, Project channels)",[16,8610,8611],{},"It visually organises the conversations, and helps you get to important messages first.",[16,8613,8614,8615],{},"Read further ",[45,8616,3245],{"href":8617,"rel":8618},"https://slack.com/intl/en-gb/help/articles/360043207674-Organise-your-sidebar-with-customised-sections",[49],[29,8620,8622],{"id":8621},"_3-use-slash-commands","3. Use slash commands",[16,8624,8625],{},"Slash commands are very easy and quick way to do actions or run commands in slack",[16,8627,8628],{},"The ones I mostly use are,",[245,8630,8631,8637,8643],{},[153,8632,8633,8636],{},[24,8634,8635],{},"/remind"," - Remind someone or channel to do something at certain time.",[153,8638,8639,8642],{},[24,8640,8641],{},"/away"," - Toggle your online status.",[153,8644,8645,8648],{},[24,8646,8647],{},"/giphy"," - Post a GIF on chat/channel",[16,8650,8651],{},"There are other slash commands of slack apps that you installed.",[29,8653,8655],{"id":8654},"_4-mute-channels","4. Mute channels",[16,8657,8658],{},"When slack channels and messages get out of hand, its better to mute some channels which do not require your attention immediately.\nYou can check these at end of day or when you get free time",[29,8660,8662],{"id":8661},"_5-find-channel-people-or-files-browser","5. Find channel, people or files (browser)",[16,8664,8665],{},"Along with global search for channels, people and files,\nSlack offers separate browser for each of these entities.",[16,8667,8668],{},"Channel browser, File browser, People and user groups, and App browser.",[16,8670,8671],{},"These are very helpful if you are looking specifically for one thing, Ex. a file you shared 3 months back.",[29,8673,8675],{"id":8674},"_6-saved-items-for-revisiting-messages","6. Saved Items for revisiting messages",[16,8677,8678],{},"Some messages need to be revisited frequently, It is likely these messages gets lost in chat with newer messages coming in daily.",[16,8680,8681],{},"Its gets very easy to save these messages and find them under saved messages, ( Just like bookmarks in browsers )",[29,8683,8685],{"id":8684},"_7-apps-and-integrations","7. Apps and Integrations",[16,8687,8688],{},"Slack apps are very useful for specific purpose.",[16,8690,8691],{},"There are many apps available in slack app library, these apps will boost your collaboration and productivity.",[245,8693,8694,8705],{},[153,8695,8696,8697],{},"Google calendar\n",[150,8698,8699,8702],{},[153,8700,8701],{},"This app will remind you of meetings 1 min before.",[153,8703,8704],{},"It gives summary of meetings for the day, So you can plan the day",[153,8706,8707,8708],{},"Github\n",[150,8709,8710],{},[153,8711,8712,8713,8716],{},"This app helps get updates about that project. Run",[24,8714,8715],{},"/github subscribe [owner/repo]"," in any conversation to start receiving updates. You will get notifications for Pull Requests, New commits, Issues. This saves time in code reviews.",{"title":69,"searchDepth":99,"depth":99,"links":8718},[8719,8720,8721,8722,8723,8724,8725],{"id":8551,"depth":99,"text":8552},{"id":8601,"depth":99,"text":8602},{"id":8621,"depth":99,"text":8622},{"id":8654,"depth":99,"text":8655},{"id":8661,"depth":99,"text":8662},{"id":8674,"depth":99,"text":8675},{"id":8684,"depth":99,"text":8685},"2021-05-09T07:00:13.392Z","The complete guide to boosting Slack productivity with tips and tricks for developers. 7 tips including shortcuts, grouping channels, using slash commands, muting channels, finding channels, people or files, saved items and apps and integrations.","/assets/slack_logo.webp",{},"/blog/useful-slack-tips",{"title":8530,"description":8727},"blog/useful-slack-tips",[969],"uCYFzWjhv_uxUntFHiTgAPYbTX1fzHZ9JEH4FhyPU18",{"id":8736,"title":8737,"author":7,"authorTitle":8,"body":8738,"category":161,"createdAt":443,"description":9085,"extension":107,"image":9086,"meta":9087,"navigation":110,"path":9088,"proficiency":448,"published":110,"readingTime":2927,"seo":9089,"stem":9090,"tags":9091,"updatedAt":118,"__hash__":9092},"blog/blog/using-ngrok-to-access-raspberry-pi-from-anywhere.md","How to use ngrok to access your raspberry pi from anywhere.",{"type":10,"value":8739,"toc":9077},[8740,8747,8750,8753,8761,8765,8768,8771,8782,8786,8797,8804,8806,8812,8815,8819,8824,8841,8844,8849,8884,8889,8909,8912,8915,8921,8925,8928,8960,8993,8996,9002,9006,9009,9036,9062,9065,9068,9074],[13,8741,8742],{},[16,8743,8744,8746],{},[19,8745,21],{}," After setting up Pi-hole and Selenium automations on a Raspberry Pi, the next challenge was accessing them remotely -- but the router didn't support port forwarding. Ngrok solves this by creating secure tunnels from the internet to your local device. This post shows how to set up ngrok on a Raspberry Pi to expose both the Pi-hole web admin panel (HTTP tunnel) and SSH access (TCP tunnel), so you can manage your Pi from anywhere on any network.",[137,8748,8737],{"id":8749},"how-to-use-ngrok-to-access-your-raspberry-pi-from-anywhere",[16,8751,8752],{},"I have posted some blog posts regarding setting up pihole on raspberry-pi, and running selenium based tasks on raspberry-pi.",[16,8754,8755,8756,2284],{},"Check these here in ",[45,8757,8760],{"href":8758,"rel":8759},"https://onthegoalways.com/blog",[49],"blog posts section",[29,8762,8764],{"id":8763},"what-was-my-use-case","What was my use case ? 🤷",[16,8766,8767],{},"After putting these tasks as CLI commands on raspberry-pi, I wanted more accessibility for these. Like running these even if I am not in local network (at home).",[16,8769,8770],{},"There are two things that I wanted to access.",[150,8772,8773,8779],{},[153,8774,8775,8776],{},"Pihole web interface that runs on pihole's address at ",[24,8777,8778],{},"http://192.168.0.X/admin/",[153,8780,8781],{},"SSH into pihole device to run the CLI commands that I made for some automations.",[29,8783,8785],{"id":8784},"challenges","Challenges ✨",[16,8787,8788,8789,8792,8793,8796],{},"In many routers these are sections where you can configure ",[24,8790,8791],{},"remote management"," or do ",[24,8794,8795],{},"port-forwarding",". But my browser did not have these settings available.",[16,8798,8799,8800,8803],{},"It had a section to configure ",[24,8801,8802],{},"port-triggering",", which is similar to port-forwarding but just inside the WAN network. That meant I can not access these things from mobile network or outside WAN.",[29,8805,7043],{"id":7042},[16,8807,8808,8809,2284],{},"Because of all these restrictions of my router I decided to use ",[24,8810,8811],{},"ngrok",[16,8813,8814],{},"Ngrok is a useful utility to create secure tunnels to locally hosted applications using a reverse proxy. It is a utility to expose any locally hosted application over the web.",[29,8816,8818],{"id":8817},"setting-up-ngrok","Setting up ngrok 🔨",[150,8820,8821],{},[153,8822,8823],{},"log on to your device",[61,8825,8828],{"className":278,"code":8826,"highlights":8827,"language":285,"meta":69,"style":69},"$ ssh pi@192.168.0.X\n",[281,282,283,284],[24,8829,8830],{"__ignoreMap":69},[289,8831,8833,8835,8838],{"class":8832,"line":281},[292,293],[289,8834,297],{"class":296},[289,8836,8837],{"class":300}," ssh",[289,8839,8840],{"class":300}," pi@192.168.0.X\n",[16,8842,8843],{},"It will prompt for password, Enter password and log into the device.",[150,8845,8846],{"start":99},[153,8847,8848],{},"Download ngrok and unzip it.",[61,8850,8853],{"className":278,"code":8851,"highlights":8852,"language":285,"meta":69,"style":69},"$ wget https://bin.equinox.io/c/4VmDzA7iaHb/ngrok-stable-linux-arm.tgz\n\n$ tar -xvzf ngrok-stable-linux-arm.tgz\n",[281,282,283,284],[24,8854,8855,8866,8870],{"__ignoreMap":69},[289,8856,8858,8860,8863],{"class":8857,"line":281},[292,293],[289,8859,297],{"class":296},[289,8861,8862],{"class":300}," wget",[289,8864,8865],{"class":300}," https://bin.equinox.io/c/4VmDzA7iaHb/ngrok-stable-linux-arm.tgz\n",[289,8867,8868],{"class":292,"line":99},[289,8869,3072],{"emptyLinePlaceholder":110},[289,8871,8873,8875,8878,8881],{"class":8872,"line":282},[292,293],[289,8874,297],{"class":296},[289,8876,8877],{"class":300}," tar",[289,8879,8880],{"class":304}," -xvzf",[289,8882,8883],{"class":300}," ngrok-stable-linux-arm.tgz\n",[150,8885,8886],{"start":282},[153,8887,8888],{},"Create a ngrok account. Where you will get an authtoken.",[61,8890,8893],{"className":278,"code":8891,"highlights":8892,"language":285,"meta":69,"style":69},"$ ./ngrok authtoken YOUR_NGROK_TOKEN\n",[281,282,283,284],[24,8894,8895],{"__ignoreMap":69},[289,8896,8898,8900,8903,8906],{"class":8897,"line":281},[292,293],[289,8899,297],{"class":296},[289,8901,8902],{"class":300}," ./ngrok",[289,8904,8905],{"class":300}," authtoken",[289,8907,8908],{"class":300}," YOUR_NGROK_TOKEN\n",[16,8910,8911],{},"This token removes the 8-hour limit of ngrok tunnel.\nIn free version, you get 1 tunnel with unique URL to access local app.",[16,8913,8914],{},"Now you are all setup for using ngrok…!!",[16,8916,8917],{},[94,8918],{"alt":8919,"src":8920},"GIF","https://media2.giphy.com/media/YPKFBSrq0EoqPJGqTn/giphy.gif?cid=ecf05e47h1aqkyltlm8m1s2b36679g2xmsjub98gaymgx2l4&rid=giphy.gif&ct=g",[29,8922,8924],{"id":8923},"using-ngrok-to-access-pihole-web-interface","Using ngrok to access pihole web interface",[16,8926,8927],{},"Use below command to create a tunnel for port 80. On port 80 there is pihole web interface. We will be able to access this from a unique ngrok URL.",[61,8929,8932],{"className":278,"code":8930,"highlights":8931,"language":285,"meta":69,"style":69},"$ /home/pi/ngrok/ngrok http 80 --log=stdout > /home/pi/ngrok.log &\n",[281,282,283,284],[24,8933,8934],{"__ignoreMap":69},[289,8935,8937,8939,8942,8945,8948,8951,8954,8957],{"class":8936,"line":281},[292,293],[289,8938,297],{"class":296},[289,8940,8941],{"class":300}," /home/pi/ngrok/ngrok",[289,8943,8944],{"class":300}," http",[289,8946,8947],{"class":304}," 80",[289,8949,8950],{"class":304}," --log=stdout",[289,8952,8953],{"class":311}," >",[289,8955,8956],{"class":300}," /home/pi/ngrok.log",[289,8958,8959],{"class":1456}," &\n",[16,8961,8962,8963,8965,8968,8969,8971,8974,8975,8977,8980,8981,8983,8986,8987,8989,8992],{},"Breakdown of command",[2952,8964],{},[24,8966,8967],{},"/home/pi/ngrok/ngrok"," --> this is absolute location ngrok executable",[2952,8970],{},[24,8972,8973],{},"http"," --> protocol to make tunnel for",[2952,8976],{},[24,8978,8979],{},"80"," --> port number of web app",[2952,8982],{},[24,8984,8985],{},"--log=stdout > /home/pi/ngrok.log"," --> Log output file",[2952,8988],{},[24,8990,8991],{},"&"," --> runs ngrok in background mode",[16,8994,8995],{},"Below is snapshot of how you can access web portal via ngrok tunnel.",[16,8997,8998],{},[94,8999],{"alt":9000,"src":9001},"ngrok http example","/assets/ngrok-http-access.webp",[29,9003,9005],{"id":9004},"using-ngrok-to-ssh-into-raspberry-pi","Using ngrok to ssh into raspberry-pi",[16,9007,9008],{},"Use below command to create a tunnel for port 22. Using port 22 we can ssh into the device. Using unique URL provided by ngrok, we can ssh into raspberry-pi from anywhere.",[61,9010,9013],{"className":278,"code":9011,"highlights":9012,"language":285,"meta":69,"style":69},"$ /home/pi/ngrok/ngrok tcp 22 --log=stdout > /home/pi/ngrok.log &\n",[281,282,283,284],[24,9014,9015],{"__ignoreMap":69},[289,9016,9018,9020,9022,9025,9028,9030,9032,9034],{"class":9017,"line":281},[292,293],[289,9019,297],{"class":296},[289,9021,8941],{"class":300},[289,9023,9024],{"class":300}," tcp",[289,9026,9027],{"class":304}," 22",[289,9029,8950],{"class":304},[289,9031,8953],{"class":311},[289,9033,8956],{"class":300},[289,9035,8959],{"class":1456},[16,9037,8962,9038,9040,8968,9042,9044,8974,9047,9049,9052,9053,9055,8986,9057,9059,9061],{},[2952,9039],{},[24,9041,8967],{},[2952,9043],{},[24,9045,9046],{},"tcp",[2952,9048],{},[24,9050,9051],{},"22"," --> port number of ssh",[2952,9054],{},[24,9056,8985],{},[2952,9058],{},[24,9060,8991],{}," --> runs ngrok in background mode.",[16,9063,9064],{},"Any ssh app like putty(Windows) or juiceSSH(mobile) can be used to connect to raspberry-pi",[16,9066,9067],{},"Below is snapshot of how ssh access from juiceSSH.",[16,9069,9070],{},[94,9071],{"alt":9072,"src":9073},"juiceSSH example","/assets/juicessh.webp",[431,9075,9076],{},"html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}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 .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}",{"title":69,"searchDepth":99,"depth":99,"links":9078},[9079,9080,9081,9082,9083,9084],{"id":8763,"depth":99,"text":8764},{"id":8784,"depth":99,"text":8785},{"id":7042,"depth":99,"text":7043},{"id":8817,"depth":99,"text":8818},{"id":8923,"depth":99,"text":8924},{"id":9004,"depth":99,"text":9005},"The post describes how you can access raspberry-pi from anywhere in two modes (web portal and ssh).","/assets/ngrok.webp",{},"/blog/using-ngrok-to-access-raspberry-pi-from-anywhere",{"title":8737,"description":9085},"blog/using-ngrok-to-access-raspberry-pi-from-anywhere",[117],"h32jNmtVOhObLRGATr0s3Q6EJQXYt6qLDtAvV5Zp2HE",{"id":9094,"title":9095,"author":7,"authorTitle":8,"body":9096,"category":161,"createdAt":9310,"description":9311,"extension":107,"image":9312,"meta":9313,"navigation":110,"path":9314,"proficiency":448,"published":110,"readingTime":2240,"seo":9315,"stem":9316,"tags":9317,"updatedAt":118,"__hash__":9318},"blog/blog/vscodium.md","Meet VSCodium. A Visual Studio Code Alternative.",{"type":10,"value":9097,"toc":9303},[9098,9105,9108,9111,9115,9118,9122,9129,9136,9142,9146,9158,9222,9227,9244,9252,9256,9262,9272,9278,9284,9286,9289,9292,9295,9301],[13,9099,9100],{},[16,9101,9102,9104],{},[19,9103,21],{}," When Microsoft builds VS Code, it adds telemetry and tracking to the binary. VSCodium is a community-driven project that takes the same open-source VS Code codebase, builds it without telemetry, and distributes it under the MIT license. You get the same editor, same extension support, and same features -- just without the tracking. This post explains why VSCodium exists and walks through installation on Windows using Chocolatey.",[137,9106,9095],{"id":9107},"meet-vscodium-a-visual-studio-code-alternative",[16,9109,9110],{},"This post describes how and why I switched to VSCodium from VSCode.",[29,9112,9114],{"id":9113},"what-is-vscodium","What is VSCodium ?",[16,9116,9117],{},"VSCodium is a community-driven, freely-licensed binary distribution of Microsoft’s editor VSCode.\nIn simple words you can download VSCode binary open source build, instead of downloading from Microsoft.",[29,9119,9121],{"id":9120},"why-i-chose-vscodium-over-vscode","Why I chose VSCodium over VSCode",[16,9123,9124,9125],{},"When Microsoft build VSCOde binary, some telemetry and tracking is added,\nRead more in detail on below link\n",[45,9126,9127],{"href":9127,"rel":9128},"https://vscodium.com/#why",[49],[16,9130,9131,9132,9135],{},"VSCodium project exists so that you dont have to download from Microsofts VSCode download page.\nVSCodium project has build scripts, that clone the VSCode repo and create build.\nThese binaries are licensed under the MIT license. ",[19,9133,9134],{},"Telemetry is disabled.","\nThese builds can be downloaded from Github releases",[16,9137,9138],{},[45,9139,9140],{"href":9140,"rel":9141},"https://github.com/VSCodium/vscodium/releases",[49],[29,9143,9145],{"id":9144},"steps-to-install-vscodium-windows","Steps to install VSCodium (Windows)",[150,9147,9148],{},[153,9149,9150,9151,9155,9157],{},"Install chocolatey\nHead over to chocolatey website ",[45,9152,9153],{"href":9153,"rel":9154},"https://chocolatey.org/install",[49],[2952,9156],{},"Copy below command and run in powershell (run as admin)",[61,9159,9162],{"className":278,"code":9160,"highlights":9161,"language":285,"meta":69,"style":69},"Set-ExecutionPolicy Bypass -Scope Process -Force;\n[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072;\niex ((New-Object System.Net.WebClient).DownloadString('https://chocolatey.org/install.ps1'))\n",[281,282,283,284],[24,9163,9164,9185,9201],{"__ignoreMap":69},[289,9165,9167,9170,9173,9176,9179,9182],{"class":9166,"line":281},[292,293],[289,9168,9169],{"class":296},"Set-ExecutionPolicy",[289,9171,9172],{"class":300}," Bypass",[289,9174,9175],{"class":304}," -Scope",[289,9177,9178],{"class":300}," Process",[289,9180,9181],{"class":304}," -Force",[289,9183,9184],{"class":1456},";\n",[289,9186,9187,9190,9193,9196,9198],{"class":292,"line":99},[289,9188,9189],{"class":1456},"[System.Net.ServicePointManager]",[289,9191,9192],{"class":304},":",[289,9194,9195],{"class":300},":SecurityProtocol",[289,9197,6279],{"class":300},[289,9199,9200],{"class":1456}," [System.Net.ServicePointManager]::SecurityProtocol -bor 3072;\n",[289,9202,9204,9207,9210,9213,9216,9219],{"class":9203,"line":282},[292,293],[289,9205,9206],{"class":296},"iex",[289,9208,9209],{"class":1456}," ((New-Object ",[289,9211,9212],{"class":300},"System.Net.WebClient",[289,9214,9215],{"class":1456},").DownloadString(",[289,9217,9218],{"class":296},"'https://chocolatey.org/install.ps1'",[289,9220,9221],{"class":1456},"))\n",[150,9223,9224],{"start":99},[153,9225,9226],{},"Run below command to install VSCodium with chocolatey",[61,9228,9231],{"className":278,"code":9229,"highlights":9230,"language":285,"meta":69,"style":69},"choco install vscodium\n",[281,282,283,284],[24,9232,9233],{"__ignoreMap":69},[289,9234,9236,9239,9241],{"class":9235,"line":281},[292,293],[289,9237,9238],{"class":296},"choco",[289,9240,1140],{"class":300},[289,9242,9243],{"class":300}," vscodium\n",[150,9245,9246],{"start":282},[153,9247,9248,9249],{},"Or you can download exe file directly from link below\n",[45,9250,9140],{"href":9140,"rel":9251},[49],[29,9253,9255],{"id":9254},"migrating-settings-from-vscode-to-vscodium","Migrating settings from VSCode to VSCodium",[16,9257,9258],{},[45,9259,9260],{"href":9260,"rel":9261},"https://github.com/VSCodium/vscodium/blob/master/DOCS.md#migrating",[49],[16,9263,9264,9265,9268,9269],{},"Your settings are stored in json file ",[24,9266,9267],{},"settings.josn"," in location ",[24,9270,9271],{},"%APPDATA%\\Code\\User",[16,9273,9274,9275],{},"Keep a backup of this file and Copy this file to ",[24,9276,9277],{},"%APPDATA%\\VSCodium\\User",[16,9279,9280,9281],{},"Same can be done for ",[24,9282,9283],{},"keybindings.json",[29,9285,4074],{"id":1780},[16,9287,9288],{},"I liked the performace of VSCodium. Noticed some performace boost than VSCode.",[16,9290,9291],{},"After Checking in control panel found the size of installation for VSCode is 3MB more than VSCodium.\nSo some extra things must be there in VSCode package.",[16,9293,9294],{},"There are some caveats with VSCodium like some extensions might not be directly installed from VSCodium,\nBut those can be install using vsix files. Downloading directly from marketplace and then install by command",[61,9296,9299],{"className":9297,"code":9298,"language":66},[64],"code --install-extension myextension.vsix\n",[24,9300,9298],{"__ignoreMap":69},[431,9302,6889],{},{"title":69,"searchDepth":99,"depth":99,"links":9304},[9305,9306,9307,9308,9309],{"id":9113,"depth":99,"text":9114},{"id":9120,"depth":99,"text":9121},{"id":9144,"depth":99,"text":9145},{"id":9254,"depth":99,"text":9255},{"id":1780,"depth":99,"text":4074},"2021-02-16T07:00:13.392Z","Discover VSCodium, a community-driven alternative to Visual Studio Code. Learn why I switched from VSCode and how to install VSCodium on Windows","/assets/vscodium.webp",{},"/blog/vscodium",{"title":9095,"description":9311},"blog/vscodium",[117],"YMrphq8JVLIJCHLigLuUf_Mzj5chhNhFQ7vdkFLWgHY",{"id":9320,"title":9321,"author":7,"authorTitle":8,"body":9322,"category":104,"createdAt":9707,"description":9708,"extension":107,"image":9709,"meta":9710,"navigation":110,"path":9711,"proficiency":6901,"published":110,"readingTime":113,"seo":9712,"stem":9713,"tags":9714,"updatedAt":118,"__hash__":9715},"blog/blog/vue-ffmpeg-wasm.md","Convert Video to GIF with FFmpeg",{"type":10,"value":9323,"toc":9700},[9324,9335,9338,9341,9345,9348,9352,9390,9397,9401,9406,9457,9462,9482,9487,9571,9576,9672,9679,9683,9688,9690,9697],[13,9325,9326],{},[16,9327,9328,9330,9331,9334],{},[19,9329,21],{}," FFmpeg is a powerful multimedia processing tool, and thanks to WebAssembly, you can now run it entirely in the browser with no server-side processing needed. This post shows how to integrate ",[24,9332,9333],{},"@ffmpeg/ffmpeg"," (the WASM build) into a Vue.js app to convert video files to GIFs client-side. It covers importing the library, loading the WASM script, running native FFmpeg commands in the browser, and handling the file input/output flow.",[137,9336,9321],{"id":9337},"convert-video-to-gif-with-ffmpeg",[16,9339,9340],{},"This post describes how to use Ffmpeg directly in browser, and use native commands.\nFfmpeg loads web assembly script in browser, and gives APIs that we can consume.",[29,9342,9344],{"id":9343},"what-is-ffmpeg","What is Ffmpeg ?",[16,9346,9347],{},"FFmpeg is a free and open-source software project consisting of a large suite of libraries and programs for handling video, audio, and other multimedia files and streams.",[29,9349,9351],{"id":9350},"how-to-add-ffmpegwasm-to-vue-app","How to add ffmpeg.wasm to vue app",[61,9353,9356],{"className":278,"code":9354,"highlights":9355,"language":285,"meta":69,"style":69},"# Use npm\nnpm  install @ffmpeg/ffmpeg\n# Use yarn\nyarn  add @ffmpeg/ffmpeg\n",[281,282,283,284],[24,9357,9358,9364,9374,9380],{"__ignoreMap":69},[289,9359,9361],{"class":9360,"line":281},[292,293],[289,9362,9363],{"class":1681},"# Use npm\n",[289,9365,9366,9368,9371],{"class":292,"line":99},[289,9367,1137],{"class":296},[289,9369,9370],{"class":300},"  install",[289,9372,9373],{"class":300}," @ffmpeg/ffmpeg\n",[289,9375,9377],{"class":9376,"line":282},[292,293],[289,9378,9379],{"class":1681},"# Use yarn\n",[289,9381,9383,9385,9388],{"class":9382,"line":283},[292,293],[289,9384,4568],{"class":296},[289,9386,9387],{"class":300},"  add",[289,9389,9373],{"class":300},[16,9391,9392,9393],{},"or you can use CDN directly, read more on ",[45,9394,9395],{"href":9395,"rel":9396},"https://ffmpegwasm.github.io/#demo",[49],[29,9398,9400],{"id":9399},"usage","Usage",[150,9402,9403],{},[153,9404,9405],{},"Import ffmpeg as below",[61,9407,9410],{"className":4695,"code":9408,"highlights":9409,"language":4698,"meta":69,"style":69},"const { createFFmpeg, fetchFile } = FFmpeg\nconst ffmpeg = createFFmpeg({ log: true })\n",[281,282,283,284],[24,9411,9412,9436],{"__ignoreMap":69},[289,9413,9415,9417,9420,9423,9425,9428,9431,9433],{"class":9414,"line":281},[292,293],[289,9416,6273],{"class":311},[289,9418,9419],{"class":1456}," { ",[289,9421,9422],{"class":304},"createFFmpeg",[289,9424,2049],{"class":1456},[289,9426,9427],{"class":304},"fetchFile",[289,9429,9430],{"class":1456}," } ",[289,9432,6114],{"class":311},[289,9434,9435],{"class":1456}," FFmpeg\n",[289,9437,9438,9440,9443,9445,9448,9451,9454],{"class":292,"line":99},[289,9439,6273],{"class":311},[289,9441,9442],{"class":304}," ffmpeg",[289,9444,6279],{"class":311},[289,9446,9447],{"class":296}," createFFmpeg",[289,9449,9450],{"class":1456},"({ log: ",[289,9452,9453],{"class":304},"true",[289,9455,9456],{"class":1456}," })\n",[150,9458,9459],{"start":99},[153,9460,9461],{},"Load Load ffmpeg.wasm-core script in browser environment",[61,9463,9466],{"className":4695,"code":9464,"highlights":9465,"language":4698,"meta":69,"style":69},"await ffmpeg.load()\n",[281,282,283,284],[24,9467,9468],{"__ignoreMap":69},[289,9469,9471,9474,9477,9480],{"class":9470,"line":281},[292,293],[289,9472,9473],{"class":311},"await",[289,9475,9476],{"class":1456}," ffmpeg.",[289,9478,9479],{"class":296},"load",[289,9481,4744],{"class":1456},[150,9483,9484],{"start":282},[153,9485,9486],{},"Use following file command to do file operations in browser, all data is bound to browser and will be lost on page refresh",[61,9488,9491],{"className":4695,"code":9489,"highlights":9490,"language":4698,"meta":69,"style":69},"// ffmpeg.FS(method, ...args)\n\n// Write file using below command\nffmpeg.FS('writeFile', 'test.mp4', await fetchFile(this.video))\n\n// Read file (already available in FS memory)\nffmpeg.FS('readFile', 'out.gif')\n",[281,282,283,284],[24,9492,9493,9499,9503,9509,9543,9548,9553],{"__ignoreMap":69},[289,9494,9496],{"class":9495,"line":281},[292,293],[289,9497,9498],{"class":1681},"// ffmpeg.FS(method, ...args)\n",[289,9500,9501],{"class":292,"line":99},[289,9502,3072],{"emptyLinePlaceholder":110},[289,9504,9506],{"class":9505,"line":282},[292,293],[289,9507,9508],{"class":1681},"// Write file using below command\n",[289,9510,9512,9515,9518,9520,9523,9525,9528,9530,9532,9535,9537,9540],{"class":9511,"line":283},[292,293],[289,9513,9514],{"class":1456},"ffmpeg.",[289,9516,9517],{"class":296},"FS",[289,9519,4732],{"class":1456},[289,9521,9522],{"class":300},"'writeFile'",[289,9524,2049],{"class":1456},[289,9526,9527],{"class":300},"'test.mp4'",[289,9529,2049],{"class":1456},[289,9531,9473],{"class":311},[289,9533,9534],{"class":296}," fetchFile",[289,9536,4732],{"class":1456},[289,9538,9539],{"class":304},"this",[289,9541,9542],{"class":1456},".video))\n",[289,9544,9546],{"class":9545,"line":284},[292,293],[289,9547,3072],{"emptyLinePlaceholder":110},[289,9549,9550],{"class":292,"line":1503},[289,9551,9552],{"class":1681},"// Read file (already available in FS memory)\n",[289,9554,9555,9557,9559,9561,9564,9566,9569],{"class":292,"line":1511},[289,9556,9514],{"class":1456},[289,9558,9517],{"class":296},[289,9560,4732],{"class":1456},[289,9562,9563],{"class":300},"'readFile'",[289,9565,2049],{"class":1456},[289,9567,9568],{"class":300},"'out.gif'",[289,9570,6125],{"class":1456},[150,9572,9573],{"start":283},[153,9574,9575],{},"Run ffmpeg command, as ffmpeg native cli.",[61,9577,9580],{"className":4695,"code":9578,"highlights":9579,"language":4698,"meta":69,"style":69},"await ffmpeg.run(\n  '-i',\n  'test.mp4',\n  '-t',\n  '5',\n  '-ss',\n  '5',\n  '-f',\n  'gif',\n  'out.gif'\n)\n// -t ==> total time of gif\n// -ss ⇒ starting seconds or offset\n",[281,282,283,284],[24,9581,9582,9595,9602,9610,9618,9626,9633,9639,9646,9653,9658,9662,9667],{"__ignoreMap":69},[289,9583,9585,9587,9589,9592],{"class":9584,"line":281},[292,293],[289,9586,9473],{"class":311},[289,9588,9476],{"class":1456},[289,9590,9591],{"class":296},"run",[289,9593,9594],{"class":1456},"(\n",[289,9596,9597,9600],{"class":292,"line":99},[289,9598,9599],{"class":300},"  '-i'",[289,9601,1486],{"class":1456},[289,9603,9605,9608],{"class":9604,"line":282},[292,293],[289,9606,9607],{"class":300},"  'test.mp4'",[289,9609,1486],{"class":1456},[289,9611,9613,9616],{"class":9612,"line":283},[292,293],[289,9614,9615],{"class":300},"  '-t'",[289,9617,1486],{"class":1456},[289,9619,9621,9624],{"class":9620,"line":284},[292,293],[289,9622,9623],{"class":300},"  '5'",[289,9625,1486],{"class":1456},[289,9627,9628,9631],{"class":292,"line":1503},[289,9629,9630],{"class":300},"  '-ss'",[289,9632,1486],{"class":1456},[289,9634,9635,9637],{"class":292,"line":1511},[289,9636,9623],{"class":300},[289,9638,1486],{"class":1456},[289,9640,9641,9644],{"class":292,"line":1524},[289,9642,9643],{"class":300},"  '-f'",[289,9645,1486],{"class":1456},[289,9647,9648,9651],{"class":292,"line":1537},[289,9649,9650],{"class":300},"  'gif'",[289,9652,1486],{"class":1456},[289,9654,9655],{"class":292,"line":1550},[289,9656,9657],{"class":300},"  'out.gif'\n",[289,9659,9660],{"class":292,"line":1563},[289,9661,6125],{"class":1456},[289,9663,9664],{"class":292,"line":1574},[289,9665,9666],{"class":1681},"// -t ==> total time of gif\n",[289,9668,9669],{"class":292,"line":1580},[289,9670,9671],{"class":1681},"// -ss ⇒ starting seconds or offset\n",[16,9673,9674,9675],{},"Read more on ffmpeg commands on ",[45,9676,9677],{"href":9677,"rel":9678},"https://ffmpeg.org/ffmpeg.html",[49],[29,9680,9682],{"id":9681},"how-app-will-look-like","How app will look like",[16,9684,9685],{},[94,9686],{"alt":96,"src":9687},"/assets/ffmpeg-mp4-to-gif.webp",[29,9689,4074],{"id":1780},[16,9691,9692,9693],{},"I found ffmpeg-wasm was really helpful, I have explored just one use case, that's like exploring tip of iceberg.\nI will try out more use cases and keep updating in my GitHub repo.\n",[45,9694,9695],{"href":9695,"rel":9696},"https://github.com/ssghait007/ffmpeg-wasm-poc",[49],[431,9698,9699],{},"html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}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 .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}",{"title":69,"searchDepth":99,"depth":99,"links":9701},[9702,9703,9704,9705,9706],{"id":9343,"depth":99,"text":9344},{"id":9350,"depth":99,"text":9351},{"id":9399,"depth":99,"text":9400},{"id":9681,"depth":99,"text":9682},{"id":1780,"depth":99,"text":4074},"2021-02-06T07:00:13.392Z","Convert Video to GIF with FFmpeg in the browser using Ffmpeg's web assembly script. Learn how to import Ffmpeg in a Vue app, load its script, and run native commands to convert a video file to a GIF","/assets/ffmpeg-wasm.webp",{},"/blog/vue-ffmpeg-wasm",{"title":9321,"description":9708},"blog/vue-ffmpeg-wasm",[590],"Jey2M0UK-tZ8NNd2H5ENq4WVqylJZXeJ7qki0IT_Hgw",{"id":9717,"title":9718,"author":7,"authorTitle":8,"body":9719,"category":104,"createdAt":105,"description":9909,"extension":107,"image":108,"meta":9910,"navigation":110,"path":9911,"proficiency":112,"published":110,"readingTime":113,"seo":9912,"stem":9913,"tags":9914,"updatedAt":118,"__hash__":9915},"blog/blog/what-is-cors.md","What is CORS and how to deal with issues related to it ?",{"type":10,"value":9720,"toc":9885},[9721,9732,9736,9740,9743,9747,9750,9754,9757,9761,9765,9768,9772,9775,9779,9783,9786,9790,9793,9797,9800,9804,9808,9811,9815,9818,9822,9825,9829,9840,9844,9867,9871],[13,9722,9723],{},[16,9724,9725,9727,9728,9731],{},[19,9726,21],{}," CORS (Cross-Origin Resource Sharing) is a browser security mechanism that blocks web pages from making requests to different domains unless the server explicitly allows it via HTTP headers like ",[24,9729,9730],{},"Access-Control-Allow-Origin",". This post explains why CORS exists (preventing unauthorized cross-domain requests), how the preflight mechanism works, the key CORS headers you can configure, and practical solutions for common CORS errors developers hit during local development and API integrations.",[29,9733,9735],{"id":9734},"what-is-cors","What is CORS ?",[1981,9737,9739],{"id":9738},"introduction-to-cross-origin-resource-sharing","Introduction to Cross-Origin Resource Sharing:",[16,9741,9742],{},"CORS is a security feature in web browsers that prevents web pages from sending requests to domains other than the one that delivered them. It enables secure cross-domain data transfer and communication.",[1981,9744,9746],{"id":9745},"purpose-of-cors-in-web-browsers","Purpose of CORS in Web Browsers:",[16,9748,9749],{},"The purpose of CORS is to stop malicious websites from submitting unauthorized requests to a website and potentially revealing confidential information or causing other security issues.",[1981,9751,9753],{"id":9752},"how-cors-works-adding-http-headers","How CORS works: adding HTTP Headers:",[16,9755,9756],{},"CORS works by adding HTTP headers to server responses to enable queries to other domains. If a web page served from example.com wants to send a request to api.example.com, for example, the server at api.example.com can add an Access-Control-Allow-Origin header in its response to signify that example.com is authorized to make queries to the API.",[29,9758,9760],{"id":9759},"how-cors-protects-your-website","How CORS Protects Your Website?",[1981,9762,9764],{"id":9763},"malicious-websites-and-unauthorized-requests","Malicious Websites and Unauthorized Requests:",[16,9766,9767],{},"By preventing web pages from sending requests to domains other than the one that delivered them, CORS protects your website from malicious websites attempting to submit unauthorized requests.",[1981,9769,9771],{"id":9770},"securing-confidential-information-and-preventing-security-issues","Securing Confidential Information and Preventing Security Issues:",[16,9773,9774],{},"By stopping malicious websites from accessing confidential information or causing other security issues, CORS helps to keep your website and its users secure.",[29,9776,9778],{"id":9777},"cors-headers-for-restricted-access","CORS Headers for Restricted Access",[1981,9780,9782],{"id":9781},"utilizing-additional-cors-headers","Utilizing Additional CORS Headers:",[16,9784,9785],{},"To further restrict access to resources, additional CORS headers can be utilized.",[1981,9787,9789],{"id":9788},"indicating-permitted-http-methods-and-http-headers","Indicating Permitted HTTP Methods and HTTP Headers:",[16,9791,9792],{},"To indicate which HTTP methods and HTTP headers are permitted for a certain resource, headers such as Access-Control-Allow-Methods and Access-Control-Allow-Headers can be used.",[1981,9794,9796],{"id":9795},"using-access-control-allow-methods-and-access-control-allow-headers","Using Access-Control-Allow-Methods and Access-Control-Allow-Headers:",[16,9798,9799],{},"By using these headers, the server can control which HTTP methods and headers are permitted, providing an additional layer of security for the website and its resources.",[29,9801,9803],{"id":9802},"cors-and-its-impact-on-developers","CORS and Its Impact on Developers",[1981,9805,9807],{"id":9806},"irritation-caused-by-cors","Irritation Caused by CORS:",[16,9809,9810],{},"Despite being a crucial security component, CORS can often irritate developers.",[1981,9812,9814],{"id":9813},"understanding-the-servers-defense-mechanism","Understanding the Server's Defense Mechanism:",[16,9816,9817],{},"It's important to understand that the server is simply trying to defend itself and its users when encountering CORS problems.",[1981,9819,9821],{"id":9820},"resolving-cors-problems-through-server-configuration-or-proxy-utilization","Resolving CORS Problems through Server Configuration or Proxy Utilization:",[16,9823,9824],{},"CORS problems can typically be resolved by configuring the server to provide the necessary headers with the request or by utilizing a proxy. This allows developers to work around CORS restrictions while still maintaining the security provided by CORS.",[29,9826,9828],{"id":9827},"common-issues-with-cors-faced-by-devs","Common issues with CORS faced by devs",[150,9830,9831,9834,9837],{},[153,9832,9833],{},"CORS issues when doing AJAX queries: CORS failures are most typically experienced when executing AJAX requests using JavaScript. If you send an AJAX request to another domain and that domain's server is not configured to allow requests from your domain, you may get a CORS error in your browser's console. To fix this issue, you must confirm that the server is configured to accept requests from your domain. This is typically accomplished via the server's responses including an Access-Control-Allow-Origin header.",[153,9835,9836],{},"CORS faults that are encountered when making requests from a local development environment: You could have CORS issues while working locally on a web application and sending requests to a distant server. This can happen if the server isn't configured to allow requests from your local development environment. To fix this issue, you might need to set up the server to accept requests from your local development environment or use a tool like ngrok to expose your local development environment to the internet.",[153,9838,9839],{},"CORS issues may arise when making calls to APIs that use rate restriction. To prevent users from making an excessive number of queries, certain APIs implement rate limiting. When making requests to an API that has rate limitation enabled and exceeding the allotted number of requests, you may receive a CORS error. To fix this issue, you will need to check that you are not making too many API calls, or you may need to employ caching or other methods to reduce the number of requests you are making.",[29,9841,9843],{"id":9842},"debugging-cors-issues-for-frontend-devs","Debugging CORS issues for frontend devs",[245,9845,9846,9849,9852,9855,9858,9861,9864],{},[153,9847,9848],{},"🔎 Use browser dev tools to inspect network requests and responses",[153,9850,9851],{},"🚨 Check browser console for error messages related to CORS",[153,9853,9854],{},"💻 Try running the request from a different browser to see if it's a browser-specific issue",[153,9856,9857],{},"🔍 Check the server response headers for Access-Control-Allow-Origin header value",[153,9859,9860],{},"🔗 Use a CORS proxy to make the request and bypass CORS restrictions",[153,9862,9863],{},"💬 Verify that the request URL and parameters are correct.",[153,9865,9866],{},"🚨 Try CORS proxy: Try using a CORS proxy to bypass the same-origin policy and see if the CORS error persists.",[29,9868,9870],{"id":9869},"debugging-cors-issues-for-backend-devs","Debugging CORS issues for backend devs",[245,9872,9873,9876,9879,9882],{},[153,9874,9875],{},"Check server response headers: Verify that the server is sending the correct CORS headers in its responses (e.g. Access-Control-Allow-Origin).",[153,9877,9878],{},"Enable CORS logging: Enable logging in the server code to track the flow of CORS requests and debug any issues.",[153,9880,9881],{},"Check API documentation: Review the API documentation to ensure that the API supports CORS and check the API's CORS configuration.",[153,9883,9884],{},"Test API with cURL: Test the API with cURL from the command line to see if the server is correctly responding to CORS requests.",{"title":69,"searchDepth":99,"depth":99,"links":9886},[9887,9892,9896,9901,9906,9907,9908],{"id":9734,"depth":99,"text":9735,"children":9888},[9889,9890,9891],{"id":9738,"depth":282,"text":9739},{"id":9745,"depth":282,"text":9746},{"id":9752,"depth":282,"text":9753},{"id":9759,"depth":99,"text":9760,"children":9893},[9894,9895],{"id":9763,"depth":282,"text":9764},{"id":9770,"depth":282,"text":9771},{"id":9777,"depth":99,"text":9778,"children":9897},[9898,9899,9900],{"id":9781,"depth":282,"text":9782},{"id":9788,"depth":282,"text":9789},{"id":9795,"depth":282,"text":9796},{"id":9802,"depth":99,"text":9803,"children":9902},[9903,9904,9905],{"id":9806,"depth":282,"text":9807},{"id":9813,"depth":282,"text":9814},{"id":9820,"depth":282,"text":9821},{"id":9827,"depth":99,"text":9828},{"id":9842,"depth":99,"text":9843},{"id":9869,"depth":99,"text":9870},"CORS is a security feature that prevents malicious websites from accessing confidential information. CORS adds HTTP headers to server responses, enabling cross-domain queries. Common issues faced by developers include AJAX failures, problems with local development, and API rate restrictions.",{},"/blog/what-is-cors",{"title":9718,"description":9909},"blog/what-is-cors",[117,590],"IrbIInnZ7LFXN6m-p3RVEw6EZeZiHvA0NiZ04X2wOcY",{"id":9917,"title":9918,"author":9919,"authorTitle":9920,"body":9921,"category":104,"createdAt":10443,"description":10444,"extension":107,"image":10445,"meta":10446,"navigation":110,"path":10447,"proficiency":112,"published":110,"readingTime":10448,"seo":10449,"stem":10450,"tags":10451,"updatedAt":118,"__hash__":10454},"blog/blog/youtube-cleaner-firefox-extension.md","Build a YouTube Cleaner Firefox Extension","Claude Assistant","Technical Writer",{"type":10,"value":9922,"toc":10434},[9923,9930,9934,9937,9941,9944,9961,9965,9968,9974,9980,10112,10116,10341,10345,10403,10407,10410,10426,10428,10431],[13,9924,9925],{},[16,9926,9927,9929],{},[19,9928,21],{}," Inspired by Atomic Habits' principle of making bad habits harder, this post walks through building a Firefox extension that removes YouTube Shorts and other distracting elements from the page. It uses a MutationObserver to handle dynamically loaded content, ensuring elements are removed even as YouTube loads new sections. The tutorial covers the full project structure including manifest.json, the content script, and how to test and install the extension locally.",[29,9931,9933],{"id":9932},"how-i-built-a-firefox-extension-to-break-my-youtube-shorts-addiction","How I Built a Firefox Extension to Break My YouTube Shorts Addiction",[16,9935,9936],{},"Ever caught yourself mindlessly scrolling through YouTube Shorts for hours? That was me. After reading \"Atomic Habits\" by James Clear, I learned a powerful principle: to break bad habits, make them difficult to do. Instead of relying purely on willpower, I decided to use my coding skills to remove the temptation entirely. Here's how I built a Firefox extension that removes Shorts and other distracting elements from YouTube, making it harder for my brain to fall into those addictive patterns.",[29,9938,9940],{"id":9939},"key-features","Key Features",[16,9942,9943],{},"This extension offers several useful features to improve your YouTube browsing:",[245,9945,9946,9949,9952,9955,9958],{},[153,9947,9948],{},"Automatically removes distracting rich section elements",[153,9950,9951],{},"Cleans up the navigation by removing Shorts links",[153,9953,9954],{},"Works seamlessly across all YouTube pages",[153,9956,9957],{},"Handles dynamic content loading",[153,9959,9960],{},"Maintains high performance with minimal overhead",[29,9962,9964],{"id":9963},"project-structure","Project Structure",[16,9966,9967],{},"Let's start by setting up our project structure. Create a new directory with these files:",[61,9969,9972],{"className":9970,"code":9971,"language":66},[64],"youtube-cleaner/\n├── manifest.json    # Extension configuration\n├── cleaner.js      # Main cleaning script\n├── icon48.png      # Extension icon (48x48)\n├── icon96.png      # Extension icon (96x96)\n└── README.md       # Documentation\n",[24,9973,9971],{"__ignoreMap":69},[16,9975,2390,9976,9979],{},[24,9977,9978],{},"manifest.json"," file is crucial - it defines your extension's properties:",[61,9981,9983],{"className":1447,"code":9982,"language":1449,"meta":69,"style":69},"{\n    \"manifest_version\": 2,\n    \"name\": \"YouTube Cleaner\",\n    \"version\": \"1.0\",\n    \"description\": \"Removes unwanted elements from YouTube\",\n    \"icons\": {\n        \"48\": \"icon48.png\",\n        \"96\": \"icon96.png\"\n    },\n    \"content_scripts\": [{\n        \"matches\": [\"*://*.youtube.com/*\"],\n        \"js\": [\"cleaner.js\"]\n    }]\n}\n",[24,9984,9985,9989,10000,10012,10024,10036,10043,10055,10065,10070,10078,10090,10103,10108],{"__ignoreMap":69},[289,9986,9987],{"class":292,"line":281},[289,9988,1457],{"class":1456},[289,9990,9991,9994,9996,9998],{"class":292,"line":99},[289,9992,9993],{"class":304},"    \"manifest_version\"",[289,9995,1480],{"class":1456},[289,9997,6583],{"class":304},[289,9999,1486],{"class":1456},[289,10001,10002,10005,10007,10010],{"class":292,"line":282},[289,10003,10004],{"class":304},"    \"name\"",[289,10006,1480],{"class":1456},[289,10008,10009],{"class":300},"\"YouTube Cleaner\"",[289,10011,1486],{"class":1456},[289,10013,10014,10017,10019,10022],{"class":292,"line":283},[289,10015,10016],{"class":304},"    \"version\"",[289,10018,1480],{"class":1456},[289,10020,10021],{"class":300},"\"1.0\"",[289,10023,1486],{"class":1456},[289,10025,10026,10029,10031,10034],{"class":292,"line":284},[289,10027,10028],{"class":304},"    \"description\"",[289,10030,1480],{"class":1456},[289,10032,10033],{"class":300},"\"Removes unwanted elements from YouTube\"",[289,10035,1486],{"class":1456},[289,10037,10038,10041],{"class":292,"line":1503},[289,10039,10040],{"class":304},"    \"icons\"",[289,10042,1465],{"class":1456},[289,10044,10045,10048,10050,10053],{"class":292,"line":1511},[289,10046,10047],{"class":304},"        \"48\"",[289,10049,1480],{"class":1456},[289,10051,10052],{"class":300},"\"icon48.png\"",[289,10054,1486],{"class":1456},[289,10056,10057,10060,10062],{"class":292,"line":1524},[289,10058,10059],{"class":304},"        \"96\"",[289,10061,1480],{"class":1456},[289,10063,10064],{"class":300},"\"icon96.png\"\n",[289,10066,10067],{"class":292,"line":1537},[289,10068,10069],{"class":1456},"    },\n",[289,10071,10072,10075],{"class":292,"line":1550},[289,10073,10074],{"class":304},"    \"content_scripts\"",[289,10076,10077],{"class":1456},": [{\n",[289,10079,10080,10083,10085,10088],{"class":292,"line":1563},[289,10081,10082],{"class":304},"        \"matches\"",[289,10084,1494],{"class":1456},[289,10086,10087],{"class":300},"\"*://*.youtube.com/*\"",[289,10089,1500],{"class":1456},[289,10091,10092,10095,10097,10100],{"class":292,"line":1574},[289,10093,10094],{"class":304},"        \"js\"",[289,10096,1494],{"class":1456},[289,10098,10099],{"class":300},"\"cleaner.js\"",[289,10101,10102],{"class":1456},"]\n",[289,10104,10105],{"class":292,"line":1580},[289,10106,10107],{"class":1456},"    }]\n",[289,10109,10110],{"class":292,"line":1586},[289,10111,1595],{"class":1456},[29,10113,10115],{"id":10114},"development-process","Development Process",[150,10117,10118,10139,10323],{},[153,10119,10120,10123],{},[19,10121,10122],{},"Local Testing",[245,10124,10125,10131,10134],{},[153,10126,4616,10127,10130],{},[24,10128,10129],{},"about:debugging"," in Firefox",[153,10132,10133],{},"Click \"This Firefox\" > \"Load Temporary Add-on\"",[153,10135,10136,10137,4625],{},"Select your ",[24,10138,9978],{},[153,10140,10141,10144,10145,10148,10149],{},[19,10142,10143],{},"Creating the Cleaner Script","\nThe ",[24,10146,10147],{},"cleaner.js"," file handles the main functionality:",[61,10150,10154],{"className":10151,"code":10152,"language":10153,"meta":69,"style":69},"language-javascript shiki shiki-themes github-light github-dark","const removeUnwantedElements = () => {\n  // Remove rich section renderers\n  document.querySelectorAll('ytd-rich-section-renderer')\n    .forEach(el => el.remove());\n  \n  // Remove Shorts links\n  document.querySelectorAll('a[href^=\"/shorts\"]')\n    .forEach(el => el.parentElement.remove());\n};\n\n// Handle dynamic content\nconst observer = new MutationObserver(removeUnwantedElements);\nobserver.observe(document.body, {\n  childList: true,\n  subtree: true\n});\n","javascript",[24,10155,10156,10172,10177,10192,10216,10221,10226,10239,10258,10263,10267,10272,10290,10301,10310,10318],{"__ignoreMap":69},[289,10157,10158,10160,10163,10165,10168,10170],{"class":292,"line":281},[289,10159,6273],{"class":311},[289,10161,10162],{"class":296}," removeUnwantedElements",[289,10164,6279],{"class":311},[289,10166,10167],{"class":1456}," () ",[289,10169,5947],{"class":311},[289,10171,4753],{"class":1456},[289,10173,10174],{"class":292,"line":99},[289,10175,10176],{"class":1681},"  // Remove rich section renderers\n",[289,10178,10179,10182,10185,10187,10190],{"class":292,"line":282},[289,10180,10181],{"class":1456},"  document.",[289,10183,10184],{"class":296},"querySelectorAll",[289,10186,4732],{"class":1456},[289,10188,10189],{"class":300},"'ytd-rich-section-renderer'",[289,10191,6125],{"class":1456},[289,10193,10194,10197,10200,10202,10205,10207,10210,10213],{"class":292,"line":283},[289,10195,10196],{"class":1456},"    .",[289,10198,10199],{"class":296},"forEach",[289,10201,4732],{"class":1456},[289,10203,10204],{"class":6437},"el",[289,10206,6465],{"class":311},[289,10208,10209],{"class":1456}," el.",[289,10211,10212],{"class":296},"remove",[289,10214,10215],{"class":1456},"());\n",[289,10217,10218],{"class":292,"line":284},[289,10219,10220],{"class":1456},"  \n",[289,10222,10223],{"class":292,"line":1503},[289,10224,10225],{"class":1681},"  // Remove Shorts links\n",[289,10227,10228,10230,10232,10234,10237],{"class":292,"line":1511},[289,10229,10181],{"class":1456},[289,10231,10184],{"class":296},[289,10233,4732],{"class":1456},[289,10235,10236],{"class":300},"'a[href^=\"/shorts\"]'",[289,10238,6125],{"class":1456},[289,10240,10241,10243,10245,10247,10249,10251,10254,10256],{"class":292,"line":1524},[289,10242,10196],{"class":1456},[289,10244,10199],{"class":296},[289,10246,4732],{"class":1456},[289,10248,10204],{"class":6437},[289,10250,6465],{"class":311},[289,10252,10253],{"class":1456}," el.parentElement.",[289,10255,10212],{"class":296},[289,10257,10215],{"class":1456},[289,10259,10260],{"class":292,"line":1537},[289,10261,10262],{"class":1456},"};\n",[289,10264,10265],{"class":292,"line":1550},[289,10266,3072],{"emptyLinePlaceholder":110},[289,10268,10269],{"class":292,"line":1563},[289,10270,10271],{"class":1681},"// Handle dynamic content\n",[289,10273,10274,10276,10279,10281,10284,10287],{"class":292,"line":1574},[289,10275,6273],{"class":311},[289,10277,10278],{"class":304}," observer",[289,10280,6279],{"class":311},[289,10282,10283],{"class":311}," new",[289,10285,10286],{"class":296}," MutationObserver",[289,10288,10289],{"class":1456},"(removeUnwantedElements);\n",[289,10291,10292,10295,10298],{"class":292,"line":1580},[289,10293,10294],{"class":1456},"observer.",[289,10296,10297],{"class":296},"observe",[289,10299,10300],{"class":1456},"(document.body, {\n",[289,10302,10303,10306,10308],{"class":292,"line":1586},[289,10304,10305],{"class":1456},"  childList: ",[289,10307,9453],{"class":304},[289,10309,1486],{"class":1456},[289,10311,10312,10315],{"class":292,"line":1592},[289,10313,10314],{"class":1456},"  subtree: ",[289,10316,10317],{"class":304},"true\n",[289,10319,10320],{"class":292,"line":3128},[289,10321,10322],{"class":1456},"});\n",[153,10324,10325,10327],{},[19,10326,4507],{},[245,10328,10329,10332,10335,10338],{},[153,10330,10331],{},"Visit YouTube.com",[153,10333,10334],{},"Verify that rich sections and Shorts links are removed",[153,10336,10337],{},"Check that the cleaning persists while browsing",[153,10339,10340],{},"Monitor console for any errors",[29,10342,10344],{"id":10343},"publishing-your-extension","Publishing Your Extension",[150,10346,10347,10387],{},[153,10348,10349,10352,10357],{},[19,10350,10351],{},"Prepare for Submission",[245,10353,10354],{},[153,10355,10356],{},"Create a ZIP file of your extension:",[61,10358,10360],{"className":278,"code":10359,"language":285,"meta":69,"style":69},"zip -r youtube-cleaner.zip * -x \".*\" -x \"__MACOSX\"\n",[24,10361,10362],{"__ignoreMap":69},[289,10363,10364,10367,10370,10373,10376,10379,10382,10384],{"class":292,"line":281},[289,10365,10366],{"class":296},"zip",[289,10368,10369],{"class":304}," -r",[289,10371,10372],{"class":300}," youtube-cleaner.zip",[289,10374,10375],{"class":304}," *",[289,10377,10378],{"class":304}," -x",[289,10380,10381],{"class":300}," \".*\"",[289,10383,10378],{"class":304},[289,10385,10386],{"class":300}," \"__MACOSX\"\n",[153,10388,10389,10392],{},[19,10390,10391],{},"Submit to Mozilla",[245,10393,10394,10397,10400],{},[153,10395,10396],{},"Create an account on Mozilla's Add-on Developer Hub",[153,10398,10399],{},"Submit your extension for review",[153,10401,10402],{},"Provide necessary documentation and screenshots",[29,10404,10406],{"id":10405},"debugging-tips","Debugging Tips",[16,10408,10409],{},"If you encounter issues:",[245,10411,10412,10415,10418,10423],{},[153,10413,10414],{},"Use Firefox Developer Tools (F12)",[153,10416,10417],{},"Check the Console tab for errors",[153,10419,10420,10421],{},"Reload the extension through ",[24,10422,10129],{},[153,10424,10425],{},"Verify your content script matches patterns",[29,10427,4074],{"id":1780},[16,10429,10430],{},"Building a YouTube Cleaner extension is an excellent way to learn browser extension development. This project teaches you about manifest configuration, content scripts, DOM manipulation, and handling dynamic content. As you develop the extension, you'll gain valuable experience that can be applied to more complex extension projects in the future.",[431,10432,10433],{},"html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}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 .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}",{"title":69,"searchDepth":99,"depth":99,"links":10435},[10436,10437,10438,10439,10440,10441,10442],{"id":9932,"depth":99,"text":9933},{"id":9939,"depth":99,"text":9940},{"id":9963,"depth":99,"text":9964},{"id":10114,"depth":99,"text":10115},{"id":10343,"depth":99,"text":10344},{"id":10405,"depth":99,"text":10406},{"id":1780,"depth":99,"text":4074},"2024-11-19T08:00:00.392Z","Learn how to create a Firefox extension that removes unwanted elements from YouTube for a cleaner browsing experience. Perfect for developers looking to get started with browser extension development.","/assets/yt-shorts-remove.webp",{},"/blog/youtube-cleaner-firefox-extension","4 min read",{"title":9918,"description":10444},"blog/youtube-cleaner-firefox-extension",[117,10452,10453],"firefox","extension","uLV7G0oLir3vnna0JuYFqekOWWax1JQ9wvcTZVpKvwg",{"id":593,"title":594,"author":7,"authorTitle":8,"body":10456,"category":161,"createdAt":959,"description":960,"extension":107,"image":961,"meta":10706,"navigation":110,"path":963,"proficiency":964,"published":110,"readingTime":965,"seo":10707,"stem":967,"tags":10708,"updatedAt":118,"__hash__":971},{"type":10,"value":10457,"toc":10696},[10458,10464,10466,10468,10470,10472,10474,10476,10478,10480,10482,10484,10490,10494,10496,10498,10500,10502,10506,10510,10512,10514,10518,10522,10524,10528,10532,10534,10536,10538,10540,10542,10552,10554,10558,10562,10564,10568,10572,10574,10576,10582,10584,10586,10588,10596,10598,10600,10604,10608,10612,10616,10618,10620,10638,10640,10642,10644,10646,10648,10650,10654,10658,10660,10662,10664,10666,10680,10682,10684,10688,10690,10692,10694],[13,10459,10460],{},[16,10461,10462,603],{},[19,10463,21],{},[16,10465,606],{},[16,10467,609],{},[16,10469,612],{},[16,10471,615],{},[16,10473,618],{},[16,10475,621],{},[29,10477,625],{"id":624},[16,10479,628],{},[16,10481,631],{},[16,10483,634],{},[16,10485,637,10486],{},[19,10487,640,10488,645],{},[642,10489,644],{},[16,10491,648,10492,652],{},[642,10493,651],{},[16,10495,655],{},[16,10497,658],{},[16,10499,661],{},[29,10501,665],{"id":664},[16,10503,668,10504,672],{},[642,10505,671],{},[16,10507,675,10508],{},[19,10509,678],{},[16,10511,681],{},[16,10513,684],{},[16,10515,10516],{},[94,10517],{"alt":689,"src":690},[13,10519,10520],{},[16,10521,695],{},[16,10523,698],{},[16,10525,10526,704],{},[19,10527,703],{},[16,10529,10530,710],{},[19,10531,709],{},[29,10533,714],{"id":713},[16,10535,717],{},[16,10537,720],{},[16,10539,723],{},[16,10541,726],{},[150,10543,10544,10546,10548,10550],{},[153,10545,731],{},[153,10547,734],{},[153,10549,737],{},[153,10551,740],{},[16,10553,743],{},[16,10555,10556],{},[94,10557],{"alt":748,"src":749},[13,10559,10560],{},[16,10561,754],{},[16,10563,757],{},[16,10565,10566,762],{},[19,10567,703],{},[16,10569,10570,767],{},[19,10571,709],{},[29,10573,771],{"id":770},[16,10575,774],{},[16,10577,777,10578,781,10580],{},[19,10579,780],{},[642,10581,784],{},[16,10583,787],{},[16,10585,790],{},[16,10587,793],{},[245,10589,10590,10592,10594],{},[153,10591,798],{},[153,10593,801],{},[153,10595,804],{},[16,10597,807],{},[16,10599,810],{},[16,10601,10602],{},[94,10603],{"alt":815,"src":816},[13,10605,10606],{},[16,10607,821],{},[16,10609,10610,826],{},[19,10611,703],{},[16,10613,10614,831],{},[19,10615,709],{},[29,10617,835],{"id":834},[16,10619,838],{},[150,10621,10622,10626,10630,10634],{},[153,10623,10624,846],{},[19,10625,845],{},[153,10627,10628,852],{},[19,10629,851],{},[153,10631,10632,858],{},[19,10633,857],{},[153,10635,10636,864],{},[19,10637,863],{},[16,10639,867],{},[29,10641,871],{"id":870},[16,10643,874],{},[16,10645,877],{},[16,10647,880],{},[16,10649,883],{},[16,10651,10652],{},[94,10653],{"alt":888,"src":889},[13,10655,10656],{},[16,10657,894],{},[29,10659,898],{"id":897},[16,10661,901],{},[16,10663,904],{},[16,10665,907],{},[245,10667,10668,10672,10676],{},[153,10669,10670],{},[19,10671,914],{},[153,10673,10674],{},[19,10675,919],{},[153,10677,10678],{},[19,10679,924],{},[16,10681,927],{},[29,10683,931],{"id":930},[16,10685,10686],{},[19,10687,936],{},[16,10689,939],{},[16,10691,942],{},[16,10693,945],{},[16,10695,948],{},{"title":69,"searchDepth":99,"depth":99,"links":10697},[10698,10699,10700,10701,10702,10703,10704,10705],{"id":624,"depth":99,"text":625},{"id":664,"depth":99,"text":665},{"id":713,"depth":99,"text":714},{"id":770,"depth":99,"text":771},{"id":834,"depth":99,"text":835},{"id":870,"depth":99,"text":871},{"id":897,"depth":99,"text":898},{"id":930,"depth":99,"text":931},{},{"title":594,"description":960},[969,970,117],1774498724783]