<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:media="http://search.yahoo.com/mrss/"><channel><title><![CDATA[woodies11.dev]]></title><description><![CDATA[Hello World! - Dev, Design, Productivity!]]></description><link>https://blog.woodies11.dev/</link><image><url>https://blog.woodies11.dev/favicon.png</url><title>woodies11.dev</title><link>https://blog.woodies11.dev/</link></image><generator>Ghost 2.9</generator><lastBuildDate>Thu, 05 Oct 2023 19:38:50 GMT</lastBuildDate><atom:link href="https://blog.woodies11.dev/rss/" rel="self" type="application/rss+xml"/><ttl>60</ttl><item><title><![CDATA[Testing Various ATK kits in Thailand]]></title><description><![CDATA[ทดลองเปรียบเทียบชุดตรวจ COVID-19 แบบ ATK ที่ขายอยู่ในไทย

Test and compare various common COVID-19 ATK kits available in Thailand.]]></description><link>https://ghost.woodies11.dev/testing-various-atk-kits-in-thailand/</link><guid isPermaLink="false">Ghost__Post__62c68a59ed3fd804e0d29a21</guid><dc:creator><![CDATA[Romson Preechawit]]></dc:creator><pubDate>Thu, 07 Jul 2022 07:54:39 GMT</pubDate><media:content url="https://ghost.woodies11.dev/content/images/2022/07/3F812833-B237-4D3D-A139-5CB9D2A10D48.jpeg" medium="image"/><content:encoded><![CDATA[<img src="https://ghost.woodies11.dev/content/images/2022/07/3F812833-B237-4D3D-A139-5CB9D2A10D48.jpeg" alt="Testing Various ATK kits in Thailand"/><p>มีสรุปภาษาไทยอยู่ล้างสุดนะครับ (Summary in Thai below)</p><p><br>Never thought I would be writing a post like this but I just found out this morning that I (finally) caught COVID-19, despite already having 4 doeses of vaccines.</br></p><p>As a born-to-be an engineer/math-and-science student major, I have always wonder how effective the various ATK kits I have accumulated really are. So, I took this opportunity to test them out.</p><p><br>All tests were carried out as per each ATK's instruction and at Bangkok's normal indoor temperature of around ~30°C.</br></p><p>Here are the results:</p><figure class="kg-card kg-image-card"><img src="https://ghost.woodies11.dev/content/images/2022/07/C473DAD1-99D3-4CB2-9E1B-714E8C49BC3C.jpeg" class="kg-image" alt="Testing Various ATK kits in Thailand" loading="lazy" width="1284" height="2282"/></figure><figure class="kg-card kg-image-card"><img src="https://ghost.woodies11.dev/content/images/2022/07/FF0A1D3F-77FE-4EC2-A798-463CA11B343E.jpeg" class="kg-image" alt="Testing Various ATK kits in Thailand" loading="lazy" width="1284" height="2282"/></figure><figure class="kg-card kg-image-card"><img src="https://ghost.woodies11.dev/content/images/2022/07/F4700368-1E8A-422D-B979-BEA7804F027E.jpeg" class="kg-image" alt="Testing Various ATK kits in Thailand" loading="lazy" width="1284" height="2282"/></figure><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://ghost.woodies11.dev/content/images/2022/07/48C62294-3F60-487B-B656-61555A6BCFAF.jpeg" class="kg-image" alt="Testing Various ATK kits in Thailand" loading="lazy" width="1284" height="2282"><figcaption>Gica by Testsealabs - Using Saliva</figcaption></img></figure><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://ghost.woodies11.dev/content/images/2022/07/99FBBA49-8862-4A3E-9630-1BD4774C6334.jpeg" class="kg-image" alt="Testing Various ATK kits in Thailand" loading="lazy" width="1284" height="2282"><figcaption>Gica by Testsealabs - Using Nasal</figcaption></img></figure><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://ghost.woodies11.dev/content/images/2022/07/461FB4D2-7B30-4EF6-8E12-4686A9FC08F8.jpeg" class="kg-image" alt="Testing Various ATK kits in Thailand" loading="lazy" width="1284" height="2282"><figcaption>CoviFind - the only one unable to detect the virus</figcaption></img></figure><h2 id="summary">Summary:</h2><p>Most test kits actually perform much better than I expected. Even the cheaper ones like GICA by Testsealabs were able to detect the present of COVID-19 accurately.</p><p>The only one that fail is the CoviFind, which was given to me for free by some company I can no longer remember. Now this might be a one off faulty test kit but I don't have anymore of that kit to retest.</p><p>I also notice that the saliva method produces much clearer lines compared to nasal swab. I am still waiting for the lan result to come back to know which strain I caught so I will have to come and update this post later.</p><h2 id="%E0%B8%AA%E0%B8%A3%E0%B8%B8%E0%B8%9B%E0%B8%A0%E0%B8%B2%E0%B8%A9%E0%B8%B2%E0%B9%84%E0%B8%97%E0%B8%A2">สรุปภาษาไทย</h2><p>สรุปคือมีตัวเดียวที่ตรวจแล้วไม่เจอนะครับ คือตัว CoviFind ทั้งนี้ทั้งนั้น อันนี้มีชุดตรวจของยี่ห้อนี้แค่ตัวเดียว เลยไม่สามารถตรวจซ้ำได้ว่าเป็นที่ชุดตรวจชุดนั้นมีปัญหาเท่านั้นรึเปล่า</p><p>นอกจากนี้ ยังสังเกตุว่า ตัวมี่ตรวจโดยใช้น้ำลายได้เส้นที่เห็นชัดเจนกว่าครับ</p><p>กำลังรอผล lab อยู่ว่าเป็นสายพันธุ์ไหนที่ติดมา ถ้ารู้จะมาอัพเดตเพิ่มนะครับ</p>]]></content:encoded></item><item><title><![CDATA[Café Review: Sheep in the City - Lad Phrao]]></title><description><![CDATA[Sheep in the City is a small café near MRT Lad Phrao. They serve very nice food and coffee at very competitive price. The café have a nice environment suitable for working.]]></description><link>https://ghost.woodies11.dev/sheep-in-the-city-lad-phrao/</link><guid isPermaLink="false">Ghost__Post__60445a7b74a4873722686551</guid><category><![CDATA[cafe]]></category><dc:creator><![CDATA[Romson Preechawit]]></dc:creator><pubDate>Sun, 07 Mar 2021 05:24:46 GMT</pubDate><media:content url="https://ghost.woodies11.dev/content/images/2021/03/IMG_7908.jpeg" medium="image"/><content:encoded><![CDATA[<h2 id="coffee">Coffee</h2><img src="https://ghost.woodies11.dev/content/images/2021/03/IMG_7908.jpeg" alt="Café Review: Sheep in the City - Lad Phrao"/><p>Coffee here are quite good, especially for the price. At the time of writing, a glass of Ice Americano will run you 65 THB and it's about the side of a Grande. I enjoy the coffee here more than Starbucks and actually look forward to having a sip each time. It is nothing special compared to some coffee specialty shop like Red Diamond and such though.</p><h2 id="food-and-snack">Food and Snack</h2><figure class="kg-card kg-image-card"><img src="https://ghost.woodies11.dev/content/images/2021/03/IMG_7915.jpeg" class="kg-image" alt="Café Review: Sheep in the City - Lad Phrao" loading="lazy" width="4032" height="3024"/></figure><p>They also serve very tasty and well-priced food here. Expect to pay around ~129-189 THB for an enjoyable plate here. The food look hygenic and well prepared. </p><p>You can definitely have a proper meal here while continuing to work. In fact, I have already been ordering breakfast/lunch from this place way before I came in for the first time.</p><h2 id="services">Services</h2><p>The staffs are very friendly and overall service is good.</p><h1 id="working-here">Working Here</h1><figure class="kg-card kg-image-card"><img src="https://ghost.woodies11.dev/content/images/2021/03/IMG_7910.jpeg" class="kg-image" alt="Café Review: Sheep in the City - Lad Phrao" loading="lazy" width="4032" height="3024"/></figure><p>The café is on the small side. It is separated into two floor with an open space in the middle of the second floor to give room to breath.</p><h2 id="tables">Tables</h2><p>Most of the tables and chairs are at an okay height for working. Only a few of them are short coffee table that force you to crouch down. With that said, there aren't many seats available here. On the first floor, there are two double-tables (can fit 4-6 people) suitable for working. On the second floor, there are three single-tables, one double-table, a long desk that can probably fit 8-10 people, and a long couter that can fit around 6-8 people. Around 3-4 shorter tables good for chilling out but not very good for working are also available.</p><h2 id="lighting">Lighting</h2><p>Lighting is on the darker side but most table have sufficient light for working.</p><h2 id="noise-level">Noise Level</h2><p>The café have some music playing in the background but it is not too loud. Since the café is next to a main road, you can also here cars driving by but they are soft enough that it likely won't be annoying to you.</p><p>This café is also very popular among food delivery services (Grab Food, Line Man, etc.) so you will here them coming and going quite often. If you are easily distracted, sitting on the second floor with a good headphone is recommended.</p><p>Overall, the noise level is totally acceptable for work to me. Nevertheless, having a conference call here is probably not a good idea due to the background music.</p><h2 id="power-outlets">Power Outlets</h2><figure class="kg-card kg-image-card"><img src="https://ghost.woodies11.dev/content/images/2021/03/IMG_7921.jpeg" class="kg-image" alt="Café Review: Sheep in the City - Lad Phrao" loading="lazy" width="4032" height="3024"/></figure><p>There are abundant power outlets available if you sit wall-facing couter style seats. These seems to be designed specifically for working.</p><p>Other than that, there are no outlets around any of the tables. That said, some tables are not too far from the wall so your laptop's charger can probably still reach the outlets. You just need to be careful about people tripping on them (oh how I miss MagSafe).</p><h2 id="wifi">WiFi</h2><ul><li><strong>Download: </strong>194 Mbps, <strong>Upload: </strong>142 Mbps</li><li><strong>Test Device:</strong> MacBook Pro M1 (2020)</li><li><strong>WiFi Type:</strong> 802.11ac</li><li><strong>Security:</strong> WPA2 Personal - SSID/Password, No Captive Portal</li></ul><figure class="kg-card kg-image-card"><img src="https://ghost.woodies11.dev/content/images/2021/03/Sheep-In-The-City---WiFi.png" class="kg-image" alt="Café Review: Sheep in the City - Lad Phrao" loading="lazy" width="1024" height="1442"/></figure><h1 id="location-">Location:</h1><!--kg-card-begin: html--><iframe src="https://www.google.com/maps/embed?pb=!1m18!1m12!1m3!1d3874.542959483437!2d100.57055131516307!3d13.806403090312138!2m3!1f0!2f0!3f0!3m2!1i1024!2i768!4f13.1!3m3!1m2!1s0x30e29dbbd9dcef8b%3A0x5549bf83e0faab94!2sSheep%20in%20the%20city!5e0!3m2!1sth!2sth!4v1615093968161!5m2!1sth!2sth" width="600" height="450" style="border:0;" allowfullscreen="" loading="lazy"/><!--kg-card-end: html--><h2 id="public-transport">Public Transport</h2><p>The café is about 5 minutes walk from MRT Lad Phrao.</p><h2 id="parking">Parking</h2><p>I never come here by car. The shop itself doesn't seem to have a parking space. If you really want to, you can park at the MRT and walk here.</p>]]></content:encoded></item><item><title><![CDATA[Apple M1 Macs for Web Development - What's working and what's isn't (so far)?]]></title><description><![CDATA[The transition to Apple's ARM CPU for developers may not be as smooth as it is for other users. In this post, I am going to keep a log of the problems I encounter setting up and using my M1 MacBook Pro for Web Development.]]></description><link>https://ghost.woodies11.dev/my-list-of-m1-developer-tools-compatibility-so-far/</link><guid isPermaLink="false">Ghost__Post__6001cf5074a487372268646f</guid><category><![CDATA[dev]]></category><dc:creator><![CDATA[Romson Preechawit]]></dc:creator><pubDate>Sat, 16 Jan 2021 12:36:13 GMT</pubDate><media:content url="https://images.unsplash.com/photo-1555066931-4365d14bab8c?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=MXwxMTc3M3wwfDF8c2VhcmNofDE2fHxjb2RlfGVufDB8fHw&amp;ixlib=rb-1.2.1&amp;q=80&amp;w=2000" medium="image"/><content:encoded><![CDATA[<img src="https://images.unsplash.com/photo-1555066931-4365d14bab8c?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MXwxMTc3M3wwfDF8c2VhcmNofDE2fHxjb2RlfGVufDB8fHw&ixlib=rb-1.2.1&q=80&w=2000" alt="Apple M1 Macs for Web Development - What's working and what's isn't (so far)?"/><p>I recently got my M1 MacBook Pro and while I do love it very much, setting up some of the tools for development is, unfortunately, not as straight forward as other programs normally tested by reviewers. In this post, I am going to log down some of the programs I found so far setting up my Mac for my typical web development work flow.</p><p>I have to add that I am only using this device for web development <strong>as a hobby. </strong>I have another machines for my work. As such, the things I am running here may not cover everything that most people doing web development professionally would do.</p><p>I would say, if this will be your one and only work machine, wait for a bit more before jumping in.</p><h1 id="what-work-">What work:</h1><p>As of 16 Jan 2020:</p><ul><li>Homebrew is supported (2.6.0)</li><li>Node 15+ is supported but you may need to build it from source (<code>nvm</code> will automatically do it for you).</li></ul><h1 id="what-are-broken-">What are broken:</h1><p>As of 16 Jan 2020:</p><h2 id="gatsby-ymmv">Gatsby - YMMV</h2><p>It seems like <code>sharp</code> package is not compatible due to its dependency being incompatible.</p><p>You may use homebrew to manually install <code>libvips</code> by running the command:</p><pre><code class="language-sh"> brew install vips</code></pre><p>This will allow <code>sharp</code> to be installed via <code>npm</code> or <code>yarn</code>. However, running <code>gatsby develop</code> in the project still generate compile time errors.</p><p>Updating <code>gatsby</code> to <code>2.30.3</code> and <code>gatsby-plugin-sharp</code> to <code>2.12.2</code> allow the code to run but produces different runtime errors, causing the server to crash upon first navigation/request. Updating other dependencies doesn't solve the error.</p><figure class="kg-card kg-image-card"><img src="https://ghost.woodies11.dev/content/images/2021/01/image-26.png" class="kg-image" alt="Apple M1 Macs for Web Development - What's working and what's isn't (so far)?" loading="lazy" width="1682" height="630"/></figure>]]></content:encoded></item><item><title><![CDATA[Debug Angular Performance Better with These Tools and Techniques]]></title><description><![CDATA[What do you do when your Angular app is starting to slow down but you are not quite sure what's the cause? Here are some tools and techniques that may help you find the culprit that is slowing down your app!]]></description><link>https://ghost.woodies11.dev/debug-angular-performance-better-with-these-tools/</link><guid isPermaLink="false">Ghost__Post__5ff9a0ae74a4873722686252</guid><category><![CDATA[dev]]></category><category><![CDATA[angular]]></category><category><![CDATA[tutorials]]></category><dc:creator><![CDATA[Romson Preechawit]]></dc:creator><pubDate>Sat, 09 Jan 2021 13:00:38 GMT</pubDate><media:content url="https://ghost.woodies11.dev/content/images/2021/01/Angular-Performance.png" medium="image"/><content:encoded><![CDATA[<img src="https://ghost.woodies11.dev/content/images/2021/01/Angular-Performance.png" alt="Debug Angular Performance Better with These Tools and Techniques"/><p>Angular's change detection mechanism was very fastinating when it first came out. Nevertheless, it can be very troublesome to debug, especially when your application get more complex.</p><p>I recently moved to a new project and was tasked with improving the performance of a particular page/feature in the app that is starting to get really slow.</p><p>This was quite a challenge as I have no knowledge about the application's architectures and designs prior to joining. The codebase has also been through many hands, some of which have already moved to another assignment or company.</p><p>After some investigating, I was able to find and optimize various codes that causes the degradation, and was able to achieve five to ten times improvement in cycle time for that particular feature.</p><p>What I am about to outlined in this post are the tools and techniques that has helped me scoped down the culprits that plague our app's performance.</p><p><strong>Note: </strong>I am only going to talk about the tools I used to <em>discover </em>improvement areas. I won't go into the detail of the techniques and refactoring I did to actually <em>fix </em>the issues. That will get very long and will likely be another post of its own.</p><h1 id="chrome-s-profiling-tools">Chrome's Profiling Tools</h1><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://ghost.woodies11.dev/content/images/2021/01/image-1.png" class="kg-image" alt="Debug Angular Performance Better with These Tools and Techniques" loading="lazy" width="1458" height="990"><figcaption>Chrome DevTools Performance tab - picture shown as an example only.</figcaption></img></figure><p>I will admit outright, I have no idea how to make sense of that <strong>Performance </strong>tab in Chrome. I know it can give me paint time and all, but I couldn't find a way to dig up <strong>what </strong>cause the slow down. The call stacks mostly trace back to Angular's own inner working like <code>zone.js</code> or <code>core.js</code> which are not very helpful.</p><p>Maybe I am missing something here but the profiling tool was not something I used a lot on this optimization journey.</p><h1 id="mornitoring-angular-s-change-detection-time">Mornitoring Angular's Change Detection Time</h1><p>The first thing I attempt to do was to get some quantifier matrices for how bad the performance currently is. A good starting point is to measure how long each change detection (CD) cycle took.</p><p>One way to do this is to <em>patch</em> the function call to trigger CD and have it log some time to the console. We will look into <code>ng.profiler.timeChangeDetection()</code> later on in the post. I don't want to call that function over and over though so the patching method is what I used.</p><p>In my <code>AppModule</code>, I first inject <code>ApplicationRef</code> in the <code>constructor</code> and patch the <code>tick()</code> function with some logging code:</p><pre><code class="language-ts">export class AppModule {
    constructor(applicationRef: ApplicationRef) {

        if (isDevMode()) {
            // First, store the original tick function
            const originalTick = applicationRef.tick

            applicationRef.tick = function () {
            	// Save start time
                const windowsPerfomance = window.performance
                const before = windowsPerfomance.now()
                
                // Run the original tick() function
                const returnValue = originalTick.apply(this, arguments)
                
                // Save end time, calculate the delta, then log to console
                const after = windowsPerfomance.now()
                const runTime = after - before
                window.console.log('[Profiler] CHANGE DETECTION TIME', runTime, 'ms')
                return returnValue

            }
        }
    }
}</code></pre><p>Now, each time the CD run, we will see how long each cycle took printed out in the console.</p><figure class="kg-card kg-image-card"><img src="https://ghost.woodies11.dev/content/images/2021/01/image-2.png" class="kg-image" alt="Debug Angular Performance Better with These Tools and Techniques" loading="lazy" width="1252" height="284"/></figure><p>Keep in mind that the act of logging to console itself, as well as the various dev tools and processes that are attached by the inspector already cause the app to slow down quite a bit. Many tasks that are running on a machine at any particular time can also affect these number significantly. So, these should be used to compare relative performance improvement rather than as an absolute measure of an app's performance.</p><figure class="kg-card kg-image-card"><img src="https://ghost.woodies11.dev/content/images/2021/01/image-3.png" class="kg-image" alt="Debug Angular Performance Better with These Tools and Techniques" loading="lazy" width="1390" height="836"/></figure><p>You can see our little app took around 120ms per cycle to run on my top-of-the-line <strong>8-Cores Core i9 16" MacBook Pro </strong>with nothing else running on it. Not to mention it run <strong>5 cycles</strong> of change detection per key input! That is around half a second of lag per keystroke. On a typical Core i7 or i5 machine, this can go up to ~250-500 ms per cycle. That made me want to rage comment on the site... except... my browser will freeze if I type too fast in there.</p><figure class="kg-card kg-image-card"><img src="https://ghost.woodies11.dev/content/images/2021/01/image-5.png" class="kg-image" alt="Debug Angular Performance Better with These Tools and Techniques" loading="lazy" width="1284" height="454"/></figure><p>For comparison, most other pages take hardly a millisecond and only occationally go up to a few milliseconds at worst.</p><p>From my experience, apps that run at around 50ms per CD with the inspector open will drop to around 1~2ms with it closed. That is hardly noticable in most cases.</p><h1 id="helper-decorator">Helper Decorator</h1><p>Now, I'll be honest, what you are about to see is rather dull 😂.</p><p>It is also the one of the less helpful tools I used, but it can flag some issues in certain scenarios so I will put it here anyway.</p><p>The second tool I used to investigate our performance issues is the good old <code>console.log</code> function. To get better understanding of who the functions wihtin the app interact with each other, and what function may potetially be causing the performance issues, I simply add <code>console.log("FUNCTION NAME")</code> to each the function in a component... <strong>EACH AND EVERY FUNCTIONS!</strong></p><p>Well... I definitely did not do that manually though. That would be insane and go against my "proactively lazy" character that I want to take.</p><p>Instead, I wrote a simple <code>@Decorator()</code> that does that for me.</p><pre><code class="language-ts">import { isDevMode } from '@angular/core'

/**
 * Use to patch all functions/methods in a class and make them print out run time
 * in ms to the console.
 *
 * This decorator will only patch functions declared in the target class.
 * It will **not** patch functions reside in the **base class**.
 * Dynamically created functions or functions received from the outside as input
 * may also not be patched.
 *
 * Keep in mind that the act of printing stuffs to the console itself will slow down
 * some function a little. This could add up if that function is called multiple times in a loop.
 * Callbacks may also not be tracked so functions that rely on
 * callbacks to do heavy lifting may appear to take very little time
 * here.
 *
 * @param threshold allow filtering log to only those that took more than the threshold (in ms)
 */
export function ProfileClassToConsole({ prefix = '', threshold = 0 } = {}) {

    return function (target: Function) {

        // Guard to skip patching
        if (!isDevMode()) {
            return
        }

        // Loop through all property of the class
        for (const propName of Object.keys(target.prototype)) {

            const descriptor = Object.getOwnPropertyDescriptor(target.prototype, propName)

            // If not a function, skip
            if (!(descriptor.value instanceof Function)) {
                continue
            }

            const windowsPerfomance = window.performance
            const fn = descriptor.value

            descriptor.value = function (...args: any[]): any {

                const before = windowsPerfomance.now()

                const result = fn.apply(this, args)

                const after = windowsPerfomance.now()
                const runTime = after - before
                if (runTime &gt; threshold) {
                    console.log(prefix, target.name, ': ', propName, 'took', runTime, 'ms')
                }

                return result

            }

            Object.defineProperty(target.prototype, propName, descriptor)

        }
    }
}</code></pre><p>I also made a similar one for sole function. It is very similar to above code so you can like adapt it for individual funtion yourself.</p><p>Now I can use the decorator like this:</p><pre><code class="language-ts">// highlight-next-line
@ProfileClassToConsole()
@Component({
	// ...
})
export class MyComponent {
	// ...
}</code></pre><figure class="kg-card kg-image-card"><img src="https://ghost.woodies11.dev/content/images/2021/01/image-6.png" class="kg-image" alt="Debug Angular Performance Better with These Tools and Techniques" loading="lazy" width="1604" height="1036"/></figure><p>I logged the time here as well just in case. However, you should take these time even more with a grain of salt than the CD time itself. Many things will escape this logging method. The main one being callbacks such as those called by observables. Just because a function finish calling and returning doesn't mean the side-effect it triggered has finished. You also need to put this decorator on a lot of components if you want to get anywhere near complete CD time.</p><p>Instead, this is useful for inspecting how your functions interact with each other and which events trigger which functions to run.</p><p>One of the beginner mistake, for example, is calling long running function in the HTML. These functions will be called every single time a change detection run. We had a page in the app that has these pattern in an <code>*ngFor</code> block that result in a few functions being run 30 times a cycle. These will become very obvious when using this decorator.</p><h1 id="angular-debug-tools">Angular Debug Tools</h1><p>Probably the most powerful tool on this list is Angular's own Debug Tools.</p><p><em>A quick note, these tools seems to differ quite a bit from version to version. At the time I worked on these optimization, we were on Angular version 7. The exact code may differ if you are on different version.</em></p><p>To enable these tools, you need to add a few line of code in your <code>src/main.ts</code>.</p><pre><code class="language-ts">import { ApplicationRef, isDevMode } from '@angular/core'
import { enableDebugTools } from '@angular/platform-browser'
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'

import { AppModule } from './app/app.module'

// ... other bootstrap code

platformBrowserDynamic()
    .bootstrapModule(AppModule)
    .then(moduleRef =&gt; {

        if (isDevMode()) {
            // Enable console debug tools
            const appRef = moduleRef.injector.get(ApplicationRef)
            const componentRef = appRef.components[0]

            enableDebugTools(componentRef)
        }
    })
    .catch(err =&gt; console.error(err))
</code></pre><p/><h2 id="ng-profiler-timechangedetection-">ng.profiler.timeChangeDetection()</h2><p>One of the tool enabled is <code>ng.profiler.timeChangeDetection()</code>. This funtion basically run the app's root CD a few time and print out average time required.</p><p>In your browser's (I'm using Chrome here) Dev Console, type <code>ng.profiler.timeChangeDetection()</code>:</p><figure class="kg-card kg-image-card"><img src="https://ghost.woodies11.dev/content/images/2021/01/image-9.png" class="kg-image" alt="Debug Angular Performance Better with These Tools and Techniques" loading="lazy" width="1612" height="352"/></figure><p>Combining this with the patch we did to <code>tick()</code> function in the first step result in this:</p><figure class="kg-card kg-image-card"><img src="https://ghost.woodies11.dev/content/images/2021/01/image-7.png" class="kg-image" alt="Debug Angular Performance Better with These Tools and Techniques" loading="lazy" width="1586" height="706"/></figure><p>As you can see, I don't normally use this function too much as my patch already give me CD time.</p><p/><h2 id="ng-probe-or-ng-getcomponent-">ng.probe() or ng.getComponent()</h2><p>This, here, is the real deal! Angular dev tool provide us with a function that allwo us to tap into a component's state and infomation at runtime, as well as altering some of its state.</p><p>Again, we were on <strong>Angular 7</strong> when I worked on these so <code>ng.probe().componentInstance</code> is what I used. If you are on <strong>version 9</strong> and above with <strong>Ivy </strong>compiler, the function has changed to <code>ng.getComponent()</code>.</p><p>In Chrome—and like most modern browsers, I know Safari work similarly—if you select something in the inspector, you may notice it says <code>== $0</code> at the end.</p><figure class="kg-card kg-image-card"><img src="https://ghost.woodies11.dev/content/images/2021/01/image-10.png" class="kg-image" alt="Debug Angular Performance Better with These Tools and Techniques" loading="lazy" width="1116" height="832"/></figure><p>The inspector will in fact automatically store whatever you select in this speciall <code>$0</code> variable. There are also <code>$1</code> that store the <em>previously </em>selected element, <code>$2</code> that store another element back, and so on. We will only use <code>$0</code> 99% of the time though.</p><figure class="kg-card kg-image-card"><img src="https://ghost.woodies11.dev/content/images/2021/01/image-11.png" class="kg-image" alt="Debug Angular Performance Better with These Tools and Techniques" loading="lazy" width="818" height="322"/></figure><p>If you use the inspector to select any <em><strong>Angular Component </strong></em>(e.g. <code>&lt;app-some-component&gt;</code> tag) and pass it to <code>ng.probe()</code>, it will return something interesting:</p><figure class="kg-card kg-image-card"><img src="https://ghost.woodies11.dev/content/images/2021/01/image-13.png" class="kg-image" alt="Debug Angular Performance Better with These Tools and Techniques" loading="lazy" width="1594" height="1074"/></figure><p><em>Note, with <code>ng.probe()</code>, you can actually select any of the component's child node, as long as that node is not an Angular Component itself, and <code>ng.probe()</code> will find the right Angular Component. With <code>ng.getComponent()</code> however, you will need to select the Angular Component's selector tag itself.</em></p><h3 id="componentinstance">componentInstance</h3><p>Now, this <code>DebugElement</code> object here has a bunch of useful things. The one we are going to focus on right now is its <code>componentInstance</code>.</p><figure class="kg-card kg-image-card"><img src="https://ghost.woodies11.dev/content/images/2021/01/image-14.png" class="kg-image" alt="Debug Angular Performance Better with These Tools and Techniques" loading="lazy" width="1586" height="374"/></figure><p>As you may guess, this object give us access to the current component instance and all of its current state. This is like putting a breakpoint in your component and inspect its state, but one you can tap into anytime. No more "oh! I missed that breakpoint!!!".</p><p>Not only does this give you access to its state, but it also give you the ability to make change and run any function within the instance.</p><p>For example, I can do:</p><figure class="kg-card kg-image-card"><img src="https://ghost.woodies11.dev/content/images/2021/01/image-15.png" class="kg-image" alt="Debug Angular Performance Better with These Tools and Techniques" loading="lazy" width="1598" height="782"/></figure><p>This change may not reflect on the app right away because changing a component's state this way does not trigger automatic change detection. However, if you do anything that trigger change detection (such as running <code>ng.profiler.timeChangeDetection()</code> or just click some stuffs on your app), you will see the change being reflected right away!</p><figure class="kg-card kg-image-card"><img src="https://ghost.woodies11.dev/content/images/2021/01/image-16.png" class="kg-image" alt="Debug Angular Performance Better with These Tools and Techniques" loading="lazy" width="299" height="120"/></figure><p>Nevertheless, if that particular property is decorated with <code>@Input()</code>, it may get replaced with the old value from the <code>@Input()</code> during CD so you may never see any change happening, unfortunately.</p><p>You can also execute a method within the instance:</p><figure class="kg-card kg-image-card"><img src="https://ghost.woodies11.dev/content/images/2021/01/image-23.png" class="kg-image" alt="Debug Angular Performance Better with These Tools and Techniques" loading="lazy" width="1134" height="374"/></figure><p>This method doesn't return anything but you get the point.</p><h3 id="injecting-changedetectorref">Injecting ChangeDetectorRef</h3><p>To get the most out of <code>ng.probe()</code> for performance debugging, you can inject <code>ChangeDetectorRef</code> into your component like this:</p><pre><code class="language-ts">export class MyComponent {
	// ...
    constructor(
        public cdRef: ChangeDetectorRef
    ) { /* ... */ }
}
</code></pre><p>This will allow us to access the component's change detector at runtime.</p><figure class="kg-card kg-image-card"><img src="https://ghost.woodies11.dev/content/images/2021/01/image-24.png" class="kg-image" alt="Debug Angular Performance Better with These Tools and Techniques" loading="lazy" width="1580" height="392"/></figure><p>You can use <code>detach()</code> and <code>reattach()</code> to manually turn CD off and on for a particular component, enabling you to somewhat isolate the effect of a component's change detection cycle on overall app's performance.</p><p>For example, if detaching a component's CD reduce the overall CD time by <code>50ms</code> then you can say that the component or some of its children is costing around <code>50ms</code> of time to complete its CD.</p><p>I also wrote a snippet that you can paste into the console:</p><pre><code class="language-js">const cd = () =&gt; {
    const wp = window.performance
    const before = wp.now()
    ng.probe($0).componentInstance.cdRef.detectChanges()
    const after = wp.now()
    const runTime = after - before
    console.log('CD Took', runTime)
}</code></pre><figure class="kg-card kg-image-card"><img src="https://ghost.woodies11.dev/content/images/2021/01/image-25.png" class="kg-image" alt="Debug Angular Performance Better with These Tools and Techniques" loading="lazy" width="1590" height="760"/></figure><p>This will allow you to type <code>cd()</code> into in console and it will print out how long that component's CD cycle took to run.</p><p><strong>Again, </strong>you will need to change the function to <code>ng.getComponent()</code> if you are using Ivy. That snippet also expect the component to have a <code>ChangeDetectorRef</code> named <code>cdRef</code> expose for it to use. There might be a better way to trigger component level CD in other version of Angular but I couldn't seem to find it for version 7.</p><h1 id="conclusion">Conclusion</h1><p>With all those tools under your belt, it's a matter of going through each of the suspected component and try to isolate their CD performance to lock down on area of improvement. As for how to actually optimize the CD time of a component once you found what is causing the slow down... that will be long enough to be a post of its own so stay tune for when that drop!</p><p>As you can see, the method I used is rather... hacky I would say. I'm sure there are better ways to go about this. If you know of a more elegant solution, please feel free to enlight me by dropping that in the comment or reach out via any of the contact channels provided. Thanks!</p>]]></content:encoded></item><item><title><![CDATA[How to fix Gatsby, Ghost, and Netlify Cache Busting and Service Worker Issues]]></title><description><![CDATA[In this post, I discuss an issue when Gatsby does not refresh and display new content upon Ghost CMS update, and how to solve such an issue.]]></description><link>https://ghost.woodies11.dev/how-to-fix-gatsby-ghost-and-netlify-cache-busting-issues/</link><guid isPermaLink="false">Ghost__Post__5feb4fd574a487372268619d</guid><category><![CDATA[dev]]></category><category><![CDATA[gatsby]]></category><category><![CDATA[react]]></category><category><![CDATA[tutorials]]></category><dc:creator><![CDATA[Romson Preechawit]]></dc:creator><pubDate>Tue, 29 Dec 2020 16:38:19 GMT</pubDate><media:content url="https://ghost.woodies11.dev/content/images/2020/12/Gatsby_SW_Issue.png" medium="image"/><content:encoded><![CDATA[<img src="https://ghost.woodies11.dev/content/images/2020/12/Gatsby_SW_Issue.png" alt="How to fix Gatsby, Ghost, and Netlify Cache Busting and Service Worker Issues"/><p>Ever since I started making this blog, one issue stand out to me in particular. Whenever I publish a new post or edit a published one, the new changes won't be seen by someone who have visited the site previously, unless they refresh their browser <strong><em>twice.</em></strong></p><p>At first, this seems like a caching issues, and technically speaking it is. However, most Google search point me to adding these magic lines to your <code>static/_headers file</code> to disable caching for these resources.</p><pre><code class="language-sh"># This is recommended to prevent the caching of the service worker file itself
/sw.js   # Gatsby's default service worker file path
  Cache-Control: no-cache

/*.html
  Cache-Control: no-cache

/public/page-data/*.json
  Cache-Control: no-cache</code></pre><p>That, however, did not resolve my issues.</p><p>My blog doesn't have much repeating visitors yet so I kind of left it as is.</p><p>As I write more and more though, I finally come down to digging deeper into the issue and found a fix!</p><h1 id="gastby-plugin-offline">gastby-plugin-offline</h1><p><code>gastby-plugin-offline</code> (<a href="https://www.gatsbyjs.com/plugins/gatsby-plugin-offline/">docs</a>) is a nice little plugin that allow our Gatsby site to work offline. It does that by utilizing Service Worker—specifically <a href="https://developers.google.com/web/tools/workbox">Workbox</a>—that Gatsby already use by default to cache our site for offline use.</p><p>The problem is, the default <code>runtimeCaching</code> used by <code>gastby-plugin-offline</code> uses <code>StaleWhileRevalidate</code> handling method for our <code>page-date.json</code> files.</p><pre><code class="language-js">runtimeCaching: [
    {
      // Use cacheFirst since these don't need to be revalidated (same RegExp
      // and same reason as above)
      urlPattern: /(\.js$|\.css$|static\/)/,
      handler: `CacheFirst`,
    },
    // highlight-start
    {
      // page-data.json files, static query results and app-data.json
      // are not content hashed
      urlPattern: /^https?:.*\/page-data\/.*\.json/,
      handler: `StaleWhileRevalidate`,
    },
    // highlight-end
    {
      // Add runtime caching of various other page resources
      urlPattern: /^https?:.*\.(png|jpg|jpeg|webp|svg|gif|tiff|js|woff|woff2|json|css)$/,
      handler: `StaleWhileRevalidate`,
    },
    {
      // Google Fonts CSS (doesn't end in .css so we need to specify it)
      urlPattern: /^https?:\/\/fonts\.googleapis\.com\/css/,
      handler: `StaleWhileRevalidate`,
    },
  ],</code></pre><p>We need to change this to <code>NetworkFirst</code> to tell SW to always prioritize fetching new data unless we are offline.</p><p>To override the default configuration, you can pass a <code>workboxConfig</code> object as an <code>options</code> to the plugin. Update the entry in your <code>gatsby-config.js</code> like this:</p><pre><code class="language-js">// gatsby-config.js

// Your gatsby config's plugins array
plugins: [
    // ... ,
    {
        resolve: `gatsby-plugin-offline`,
        options: {
            workboxConfig: {
                runtimeCaching: [
                    {
                        // Use cacheFirst since these don't need to be revalidated (same RegExp
                        // and same reason as above)
                        urlPattern: /(\.js$|\.css$|static\/)/,
                        handler: `CacheFirst`,
                    },
                    {
                        // page-data.json files, static query results and app-data.json
                        // are not content hashed
                        urlPattern: /^https?:.*\/page-data\/.*\.json/,
                        // highlight-next-line
                        handler: `NetworkFirst`,
                    },
                    {
                        // Add runtime caching of various other page resources
                        urlPattern: /^https?:.*\.(png|jpg|jpeg|webp|svg|gif|tiff|js|woff|woff2|json|css)$/,
                        handler: `StaleWhileRevalidate`,
                    },
                    {
                        // Google Fonts CSS (doesn't end in .css so we need to specify it)
                        urlPattern: /^https?:\/\/fonts\.googleapis\.com\/css/,
                        handler: `StaleWhileRevalidate`,
                    },
                ],
            },
        },
    },
        
]</code></pre><p>Now, this will cause more bandwidth to be consumed for the users since they will always have to fetch new <code>page-data.json</code> for every pages, even if changes are usually rare and far between. Nevertheless, these <code>page-data.json</code> files are almost always very small, and other assets like images will still be cached so that shouldn't be much of a problem. <code>NetworkFirst</code> will also still allow our site to function when offline.</p><h1 id="note">Note</h1><p>The default behaviour, <code>StaleWhileRevalidate</code> is supposed to tell the SW to display site using what is stored in cache while it silently fetch any new changes and update its cache. Then, upon next visit, present the newly updated version while repeating the silent fetching again.</p><p>Apparently, Gatsby will also refresh itself upon any navigation in the site, if changes are present. For some reason though, these behaviour do not work for me. Thus, why I resorted to simply switching to <code>NetworkFirst</code> instead.</p><p>For more information about how each handling method works for service workers, Ondrej Polesny has done an amazing work explaining these in <a href="https://www.freecodecamp.org/news/how-to-enable-offline-mode-for-gatsby-site/">his blog post right here</a>. You should go check that out!</p>]]></content:encoded></item><item><title><![CDATA[How I setup freshly installed macOS Big Sur for Web Development]]></title><description><![CDATA[I recently do a fresh installation of the new macOS Big Sur on my old MacBook Pro so I decided to write this post, more as a step-by-step guide on how I should setup my mac for web development.]]></description><link>https://ghost.woodies11.dev/how-i-setup-a-new-macos-for-web-development/</link><guid isPermaLink="false">Ghost__Post__5feae6f7f7192e1e45140e9f</guid><category><![CDATA[dev]]></category><category><![CDATA[tutorials]]></category><dc:creator><![CDATA[Romson Preechawit]]></dc:creator><pubDate>Tue, 29 Dec 2020 09:53:00 GMT</pubDate><media:content url="https://ghost.woodies11.dev/content/images/2020/12/Setup-Big-Sur-for-Web-Dev-2.png" medium="image"/><content:encoded><![CDATA[<blockquote>Updated as of <strong>Jan 16, 2021</strong>. I recently received my M1 MacBook Pro and set it up exactly as this post described. Pretty much all of the steps completed without any problem (except <code>nvm</code> taking quite long to install node because it has to compile the whole thing from source instead of using pre-built version). However, some of the <code>npm</code> dependencies refuse to install correctly. I will make another post to update on these soon.</blockquote><img src="https://ghost.woodies11.dev/content/images/2020/12/Setup-Big-Sur-for-Web-Dev-2.png" alt="How I setup freshly installed macOS Big Sur for Web Development"/><p>While the new M1 MacBook are very exciting and are what I have been waiting to upgrade to, a configured version (e.g. with 16 GB of RAM) will take a bit more time to ship in my country. </p><p>Plus, I don't know how I feel about not being to connect multiple monitors natively. Not to mention the hardest part, deciding between an Air or a Pro...</p><p>So, to squeeze the last drop of life of my trusty, now 7 years old, 2014 top-of-the-line 15" MacBook Pro with Retina Display that is now running very slow and basically have none-existing battery life, I decided to do a fresh installation of macOS again to see if this could maybe last a few more months until the shipping time for those new MacBook's drop slightly.</p><p>Here is how I set it up!</p><h1 id="1password">1Password</h1><figure class="kg-card kg-image-card"><img src="https://ghost.woodies11.dev/content/images/2020/12/Screen-Shot-2563-12-29-at-15.30.38-1.png" class="kg-image" alt="How I setup freshly installed macOS Big Sur for Web Development" loading="lazy" width="2584" height="1672"/></figure><p>I am a big fan of password managers and have been using <a href="https://1password.com">1Password</a> even before they turn to a subscription model (good times, I know)... and no, I'm not being paid to say that.</p><figure class="kg-card kg-image-card"><img src="https://ghost.woodies11.dev/content/images/2020/12/image-1.png" class="kg-image" alt="How I setup freshly installed macOS Big Sur for Web Development" loading="lazy" width="1192" height="644"/></figure><p>1Password is even more powerful on Big Sur, with better OS integration, like it's iOS/iPad OS counterpart.</p><p>Naturally, it is the first app I downloaded. It will make setting up my other accounts a lot easier.</p><h1 id="spotify">Spotify</h1><p>Setting up a new machine/OS can take a while. Better put some music on!</p><h1 id="visual-studio-code">Visual Studio Code</h1><p><a href="https://code.visualstudio.com">VS Code</a> has been my IDE of chioce for almost everything for a while now. I'm still impress this is made by Microsoft, considering it's brother—Visual Studio Professional/Enterprise still require multiple keystrokes to comment out a line of code out-of-the-box.</p><figure class="kg-card kg-image-card"><img src="https://ghost.woodies11.dev/content/images/2020/12/image-2.png" class="kg-image" alt="How I setup freshly installed macOS Big Sur for Web Development" loading="lazy" width="1034" height="676"/></figure><p>I used to use the Insider version just to get Extensions and Settings sync, but those has since become generally available in the normal version of Code.</p><p>I might do another post about the extensions that I use. Honestly though, with auto-sync and stuffs, I kinda take many of them for granted.</p><h1 id="my-terminal">My Terminal</h1><figure class="kg-card kg-image-card"><img src="https://ghost.woodies11.dev/content/images/2020/12/Screen-Shot-2020-07-23-at-20.34-1.png" class="kg-image" alt="How I setup freshly installed macOS Big Sur for Web Development" loading="lazy" width="1200" height="608"/></figure><p>I already wrote a post some time ago on how I setup my terminal. It's a bit long to put here so you should go <a href="https://blog.woodies11.dev/how-i-set-up-my-terminal-oh-my-zsh-powerline9k-iterm-2/">check it out</a>.</p><h1 id="connect-to-my-synology-nas-and-setup-time-machine">Connect to my Synology NAS and setup Time Machine</h1><figure class="kg-card kg-image-card"><img src="https://ghost.woodies11.dev/content/images/2020/12/image-6.png" class="kg-image" alt="How I setup freshly installed macOS Big Sur for Web Development" loading="lazy" width="2064" height="1096"/></figure><p>I have a NAS at home that I keep my backups and files shared across my PC and laptops. Since the NAS has already been setup and correctly broadcast appropriate protocols to my local network, this is just a matter of putting in credentials.</p><p>Same goes for setting up Time Machine.</p><figure class="kg-card kg-image-card"><img src="https://ghost.woodies11.dev/content/images/2020/12/image-7.png" class="kg-image" alt="How I setup freshly installed macOS Big Sur for Web Development" loading="lazy" width="1560" height="1136"/></figure><h1 id="dev-tools">Dev Tools</h1><p>If you follow the <a href="https://blog.woodies11.dev/how-i-set-up-my-terminal-oh-my-zsh-powerline9k-iterm-2/">How I setup my Terminal</a> post, the basics like GIT and stuffs should already been taken care of.</p><h2 id="homebrew">Homebrew</h2><figure class="kg-card kg-image-card"><img src="https://ghost.woodies11.dev/content/images/2020/12/image-3.png" class="kg-image" alt="How I setup freshly installed macOS Big Sur for Web Development" loading="lazy" width="2560" height="1614"/></figure><p>Install <a href="https://brew.sh">Homebrew</a> Package Manager so we can use it to install other things.</p><h2 id="nvm-and-node">NVM and Node</h2><p>I use <a href="https://github.com/nvm-sh/nvm">nvm</a> to manage my Node versions.</p><p><code>nvm</code> is useful if you want to switch between multiple versions of node, maybe because each projects require different version as a dependencies.</p><p>As of Jan 16, 2021, the command I used to install <code>nvm</code> on macOS Big Sur is this command below:</p><pre><code class="language-sh">curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.37.2/install.sh | zsh</code></pre><p>Once installed, you can use:</p><pre><code class="language-sh">nvm install [version] </code></pre><p>and</p><pre><code>nvm use [version]</code></pre><p>to install and use a specific version of Node.</p><figure class="kg-card kg-image-card"><img src="https://ghost.woodies11.dev/content/images/2020/12/Screen-Shot-2563-12-29-at-15.52.29.png" class="kg-image" alt="How I setup freshly installed macOS Big Sur for Web Development" loading="lazy" width="1814" height="1208"/></figure><p>If you only going to use one version of Node or you mainly develop your app in a container (and probably don't need my help telling you how to install node), then you can just download and install Node package from the <a href="https://nodejs.org/en/download/">official website</a>.</p><h2 id="yarn">Yarn</h2><p><a href="https://classic.yarnpkg.com/en/docs/install/#mac-stable">Full Instructions</a></p><pre><code>brew install yarn</code></pre><p>I use both <code>npm</code> and <code>yarn</code>. Honestly, the performance feels about the same nowadays (I just run it and go do something else anyway). I just prefer <code>yarn add</code> over remembering to add <code>-save</code> flag to <code>npm</code>...</p><p>My project at work still use <code>npm</code> though.</p><h2 id="global-angular-cli">Global Angular CLI</h2><p>I am an Angular developer, so I will install the latest version of <a href="https://cli.angular.io">Angular CLI.</a></p><pre><code>yarn global add @angular/cli</code></pre><p>or</p><pre><code>npm install -g @angular/cli</code></pre><h2 id="react">React</h2><p>I'm pretty new with React but as far as I know, React doesn't really need any special CLI to be installed globally (or one exist that I should have known but don't). Well... it also depends on who you setup your React project.</p><h2 id="docker">Docker</h2><p>I don't use Docker that much yet so I won't install it on this laptop as it cost too much battery to leave running. It is easy enough to install following <a href="https://www.docker.com/products/docker-desktop">this</a> instruction though. Also, Docker still doesn't support the new Apple M1 chip in production build at the time of writting this.</p><h1 id="tweak-safari-a-bit">Tweak Safari a bit</h1><p>Enable <strong>Show full website address </strong>so I can actually see which <code>localhost:[PORT]</code> I am on.</p><figure class="kg-card kg-image-card"><img src="https://ghost.woodies11.dev/content/images/2020/12/image-8.png" class="kg-image" alt="How I setup freshly installed macOS Big Sur for Web Development" loading="lazy" width="1808" height="1050"/></figure><p>While you are at that, don't forget to check <strong>Show Develop menu in menu bar, </strong>to enable developer tools.</p><p>I also disable <strong>Open "safe" files after downloading.</strong></p><figure class="kg-card kg-image-card"><img src="https://ghost.woodies11.dev/content/images/2021/01/image.png" class="kg-image" alt="How I setup freshly installed macOS Big Sur for Web Development" loading="lazy" width="1808" height="1118"/></figure><p>I also turn <strong>Show Status Bar </strong>on in <strong>View &gt; Show Status Bar</strong></p><figure class="kg-card kg-image-card"><img src="https://ghost.woodies11.dev/content/images/2021/01/image-27.png" class="kg-image" alt="How I setup freshly installed macOS Big Sur for Web Development" loading="lazy" width="834" height="442"/></figure><p>This give you a preview of the link's URL before you click on them.</p><figure class="kg-card kg-image-card"><img src="https://ghost.woodies11.dev/content/images/2021/01/image-29.png" class="kg-image" alt="How I setup freshly installed macOS Big Sur for Web Development" loading="lazy" width="1112" height="490"/></figure><h1 id="download-chrome">Download Chrome</h1><figure class="kg-card kg-image-card"><img src="https://ghost.woodies11.dev/content/images/2020/12/Screen-Shot-2563-12-29-at-16.08.05.png" class="kg-image" alt="How I setup freshly installed macOS Big Sur for Web Development" loading="lazy" width="2560" height="1614"/></figure><p>While I mainly love and use Safari for day to day browsing, I sometime jump to <a href="https://www.google.com/chrome/">Chrome</a> for its amazing development tools (at the expense of reduced battery life...).</p><h2 id="dev-extensions">Dev Extensions</h2><p>My extensions are auto-sync.</p><p>I don't have a lot installed. These are the three main ones related to developments:</p><ul><li>Angury - <a href="https://augury.rangle.io/">Official Website</a>, <a href="https://chrome.google.com/webstore/detail/augury/elgalmkoelokbchhkhacckoklkejnhcd">Chrome Store</a></li><li>Redux DevTools - <a href="https://chrome.google.com/webstore/detail/redux-devtools/lmhkpmbekcpmknklioeibfkpmmfibljd?hl=en">Chrome Store</a></li><li>React Developer Tools - <a href="https://chrome.google.com/webstore/detail/react-developer-tools/fmkadmapgofadopljbjfkapdkoienihi?hl=en">Chrome Store</a></li></ul><h1 id="configure-finder">Configure Finder</h1><p>I like to see my status bar and path so I enable the two options in <strong>View</strong>.</p><figure class="kg-card kg-image-card"><img src="https://ghost.woodies11.dev/content/images/2020/12/image-5.png" class="kg-image" alt="How I setup freshly installed macOS Big Sur for Web Development" loading="lazy" width="1802" height="1058"/></figure><p>You can press <code>CMD + Shift + .</code> to show/hide hidden files.</p><p>I also like for it to show my folders on the sidebar, as well as showing something predictable on launch (Recents annoy me cause it look disorganized).</p><figure class="kg-card kg-image-card"><img src="https://ghost.woodies11.dev/content/images/2020/12/Screen-Shot-2563-12-29-at-16.31.42.png" class="kg-image" alt="How I setup freshly installed macOS Big Sur for Web Development" loading="lazy" width="850" height="976"/></figure><figure class="kg-card kg-image-card"><img src="https://ghost.woodies11.dev/content/images/2020/12/Screen-Shot-2563-12-29-at-16.31.56.png" class="kg-image" alt="How I setup freshly installed macOS Big Sur for Web Development" loading="lazy" width="828" height="1362"/></figure><h1 id="disable-automatic-spaces-arrangement">Disable Automatic Spaces Arrangement</h1><p>I hate it when macOS switch my spaces around. I like my IDE to always be in the middle space. The left space to my IDE is for testing/previewing/displaying the output of whatever I am developing. The right is for web browsing, opening stackoverflow, and referencing stuffs. Next to that will sometime be Spotify.</p><p>To prevent the OS from ruining my muscle memory, I would go to <strong>System Preferences &gt; Mission Control </strong>and uncheck <strong>Automatically rearrange Spaces based on most recent use.</strong></p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://ghost.woodies11.dev/content/images/2021/01/Screen-Shot-2564-01-20-at-01.14.08-1.png" class="kg-image" alt="How I setup freshly installed macOS Big Sur for Web Development" loading="lazy" width="1560" height="1360"><figcaption>In <strong>System Preferences, </strong>go to <strong>Mission Control.</strong></figcaption></img></figure><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://ghost.woodies11.dev/content/images/2021/01/Screen-Shot-2564-01-20-at-01.15.50.png" class="kg-image" alt="How I setup freshly installed macOS Big Sur for Web Development" loading="lazy" width="1560" height="1210"><figcaption>Uncheck <strong>Automatically rearrange Spaces based on most recent use</strong> so macOS won't mess with the order of your spaces.</figcaption></img></figure><h1 id="other-stuffs">Other Stuffs</h1><p>I mainly use <a href="https://todoist.com">Todoist</a> for my task management, so that is one of the few other programs I downloaded (it does have a very good web version, but I like to keep it a separate app so I can quickly switch back and forth between it and other apps).</p><p>Another is a journalling app called <a href="https://dayoneapp.com">Day One</a> that I use to keep my diary.</p><p>Also, let's not forget <a href="https://zoom.us">Zoom</a>, because... you know... it's 2020 and quarantine... (I hope this whole thing would already be a thing of a past one day when you read this).</p><p>Others like <a href="http://figma.com">Figma</a> and <a href="http://app.youneedabudget.com">YNAB</a> work perfectly fine in a browser (oh the beauty of modern web!).</p>]]></content:encoded></item><item><title><![CDATA[How to PROPERLY implement ControlValueAccessor - Angular Form]]></title><description><![CDATA[How to create your own FormControl (ControlValueAccessor) for your Angular application, and common pitfall to avoid.]]></description><link>https://ghost.woodies11.dev/how-to-properly-implement-controlvalueaccessor/</link><guid isPermaLink="false">Ghost__Post__5f6becd44e433422bb91bb8b</guid><category><![CDATA[dev]]></category><category><![CDATA[angular]]></category><category><![CDATA[tutorials]]></category><category><![CDATA[web]]></category><dc:creator><![CDATA[Romson Preechawit]]></dc:creator><pubDate>Thu, 24 Sep 2020 01:12:35 GMT</pubDate><media:content url="https://ghost.woodies11.dev/content/images/2020/09/ReactiveFormsCVA.png" medium="image"/><content:encoded><![CDATA[<img src="https://ghost.woodies11.dev/content/images/2020/09/ReactiveFormsCVA.png" alt="How to PROPERLY implement ControlValueAccessor - Angular Form"/><p>The <code><a href="https://angular.io/api/forms/ControlValueAccessor">ControlValueAccessor</a></code> (CVA) interface exposes functions that allow Angular's forms, both template driven and reactive, to communicate with a custom form control component.</p><figure class="kg-card kg-image-card"><img src="https://ghost.woodies11.dev/content/images/2020/09/View-Model.png" class="kg-image" alt="How to PROPERLY implement ControlValueAccessor - Angular Form" loading="lazy"/></figure><h1 id="let-s-create-our-own-cva">Let's create our own CVA</h1><p>For this example, I am going to implement a basic star rating input where the user can click on the stars to give rating. I know, I know... another one of these star rating things but I do want to avoid having a native HTML <code>&lt;input&gt;</code> in here for now because those have their own best practices. Let's focus on the very basics of CVA for this post.</p><p>I want to focus on <code>ControlValueAccessor</code> implementation so I will skill the detail of the UI Implementation. You can refer to the code below:</p><pre><code class="language-ts">// star-rating.component.ts

import { Component, Input } from '@angular/core';

@Component({
  selector: 'app-star-rating',
  templateUrl: './star-rating.component.html',
  styleUrls: ['./star-rating.component.scss'],
})
export class StarRatingComponent {
  @Input() numberOfStar = 5;

  currentRating: number;

  // *ngFor can't iterate on a number
  // so we are going to covert it to an array of [0...n-1]
  public toIterableArray(n) {
    return Array.from(Array(n).keys());
  }

  // highlight-start
  setRating(n) {
    this.currentRating = n;
  }
  // highlight-end

}
</code></pre><pre><code class="language-scss">// star-rating.component.scss

ul.star-rating {
    list-style: none;
    color: black;
    padding: 0;
    margin: 0;

    &amp;:hover {
      color: rgb(42, 190, 235);

      &amp; li:hover ~ li {
        color: black;
      }
    }

    li {
      font-size: 3em;
      transition-duration: 300ms;
      cursor: pointer;
      display: inline;
    }

    .star-rating--highlight {
        color: orange;
    }
}</code></pre><pre><code class="language-html">&lt;!-- star-rating.component.html --&gt;

&lt;ul class="star-rating"&gt;
  &lt;li
    *ngFor="let i of toIterableArray(numberOfStar)"
    (click)="setRating(i + 1)"
    [ngClass]="{ 'star-rating--highlight': currentRating &gt; i }"
  &gt;
    ☆
  &lt;/li&gt;
&lt;/ul&gt;
</code></pre><p>With this, whenever the user click on a star, <code>setRating(n)</code> will be called with <code>n</code> being the rating the user selected.</p><figure class="kg-card kg-image-card"><img src="https://ghost.woodies11.dev/content/images/2020/09/chrome_XXytGj3uC1.png" class="kg-image" alt="How to PROPERLY implement ControlValueAccessor - Angular Form" loading="lazy"/></figure><p>In order to get the component to talk to Angular's Form though, we need to implement a few more logics.</p><h2 id="import-and-implement-the-controlvalueaccessor">Import and implement the <code>ControlValueAccessor</code></h2><p>Import the <code>ControlValueAccessor</code> interface from <code>@angular/forms</code> and implements it in your component:</p><pre><code class="language-ts">import { ControlValueAccessor } from '@angular/forms';

// ...
export class StarRatingComponent implements ControlValueAccessor {

}</code></pre><h2 id="implement-functions-for-angular-to-register-its-callbacks">Implement functions for Angular to register its callbacks</h2><p>The first two functions we are going to implement are very straight forward:</p><p><code>registerOneChange()</code> and <code>registerOneTouched()</code>.</p><p><code>registerOnChange()</code><strong> </strong>allows Angular to pass its own <code>OnChange()</code> callback to your CVA. We can then use this callback to ask Angular to update its <strong>model</strong>–the form object–whenever our value change <strong>resulting from the user's interaction with the view</strong>. Our job, therefore, is to save this callback and call it at appropriate time.</p><p>The same goes for <code>registerOnTouched()</code>.</p><pre><code class="language-ts">export class StarRatingComponent implements ControlValueAccessor {
  // ...
  
  // Save the callbacks, make sure to have a default so your app
  // doesn't crash when one isn't (yet) registered
  private onChange = (v: any) =&gt; {};
  private onTouched = () =&gt; {};

  registerOnChange(fn: any): void {
    this.onChange = fn;
  }
  registerOnTouched(fn: any): void {
    this.onTouched = fn;
  }
}</code></pre><p>Now, you will need to call these functions whenever a user interact with the UI in a way that will require the model to change. In our example, this happen when the user click on a star and set a new rating.</p><p>In our <code>setRating(n)</code> function, let's call the callbacks once a new rating is set.</p><pre><code class="language-ts">  setRating(n) {
    this.currentRating = n;

    // Notify Angular to update its model
    this.onChange(this.currentRating);
    this.onTouched();

  }</code></pre><p>With this, every time we click on a star, Angular will update the form's value automatically.</p><p>For now, let's implement an empty <code>writeValue()</code> function. We will come back to it in a moment.</p><pre><code class="language-ts">export class StarRatingComponent implements ControlValueAccessor {

  // ...
  
  writeValue(obj: any): void {
    return;
  }
}</code></pre><h3 id="connecting-our-custom-component-to-reactive-form">Connecting our custom component to Reactive Form</h3><p>To see how this work, let quickly create an connect a Reactive Form in our AppComponent.</p><pre><code class="language-ts">// app.module.ts

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';

import { AppComponent } from './app.component';
import { StarRatingComponent } from './components/mycustominput/star-rating.component';
// highlight-next-line
import { ReactiveFormsModule } from '@angular/forms';

@NgModule({
  declarations: [AppComponent, StarRatingComponent],
  imports: [
    BrowserModule, 
    // highlight-next-line
    ReactiveFormsModule
  ],
  providers: [],
  bootstrap: [AppComponent],
})
export class AppModule {}

</code></pre><pre><code class="language-html">&lt;!-- app.component.html --&gt;

&lt;div&gt;
  &lt;app-star-rating&gt;&lt;/app-star-rating&gt;
&lt;/div&gt;
&lt;button (click)="submit()"&gt;Submit&lt;/button&gt;

</code></pre><pre><code class="language-ts">// app.component.ts

import { Component, OnInit } from '@angular/core';
import { FormControl, FormGroup } from '@angular/forms';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss'],
})
export class AppComponent implements OnInit {
  title = 'cva-example';

  public form: FormGroup;

  ngOnInit(): void {
    this.form = new FormGroup({
      rating: new FormControl(),
    });
  }

  submit() {
    console.log(this.form.value);
  }
}
</code></pre><p>This add a button that will output the value of our form to the console when click.</p><figure class="kg-card kg-image-card"><img src="https://ghost.woodies11.dev/content/images/2020/09/chrome_9LSw19JT4T.png" class="kg-image" alt="How to PROPERLY implement ControlValueAccessor - Angular Form" loading="lazy"/></figure><p/><p>It doesn't talk to our stars yet though so the value will always be <code>null</code> no matter what the star is at.</p><p>Let's solve that by binding our Reactive Form to the view.</p><p>As with your regular FormControl, you want to bind your root <code>form</code> object to <code>formGroup</code> and give your control its <code>formControlName</code>.</p><pre><code class="language-html">&lt;!-- app.component.html --&gt;

&lt;div [formGroup]="form"&gt;
  &lt;app-star-rating formControlName="rating"&gt;&lt;/app-star-rating&gt;
&lt;/div&gt;
&lt;button (click)="submit()"&gt;Submit&lt;/button&gt;</code></pre><p>Once you hit save, Angular will complain about not being able to find an accessor <code>Error: No value accessor for form control with name: 'rating'</code></p><figure class="kg-card kg-image-card"><img src="https://ghost.woodies11.dev/content/images/2020/09/chrome_fargGYurIL.png" class="kg-image" alt="How to PROPERLY implement ControlValueAccessor - Angular Form" loading="lazy"/></figure><p>This is because we haven't register the new form control to the app yet.</p><p>We need to go back to <code>star-rating.component.ts</code> and let Angular know how to provide it.</p><pre><code class="language-ts">// star-rating.component.ts

// highlight-next-line
import { Component, forwardRef, Input } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';

@Component({
  selector: 'app-star-rating',
  templateUrl: './star-rating.component.html',
  styleUrls: ['./star-rating.component.scss'],
  // highlight-start
  providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      useExisting: forwardRef(() =&gt; StarRatingComponent),
      multi: true,
    },
  ],
  // highlight-end
})
export class StarRatingComponent implements ControlValueAccessor {
	// ...
}</code></pre><p>With that done, our form model should now be in sync with our stars!</p><figure class="kg-card kg-image-card"><img src="https://ghost.woodies11.dev/content/images/2020/09/chrome_CgFPrITY0q.png" class="kg-image" alt="How to PROPERLY implement ControlValueAccessor - Angular Form" loading="lazy"/></figure><h2 id="what-about-writevalue-v-">What about writeValue(v)?</h2><p>Our new component seems to be working fine so far, and we haven't really done anything with the <code>writeValue(v)</code> component yet.</p><p>To make it clear what that function is suppose to do, let's create a new button and write some logic for it.</p><pre><code class="language-html">&lt;!-- app.component.html --&gt;

&lt;div [formGroup]="form"&gt;
  &lt;app-star-rating formControlName="rating"&gt;&lt;/app-star-rating&gt;
&lt;/div&gt;
&lt;button (click)="submit()"&gt;Submit&lt;/button&gt;
// highligh-next-line
&lt;button (click)="load()"&gt;Load Rating&lt;/button&gt;</code></pre><pre><code class="language-ts">// app.component.ts

export class AppComponent implements OnInit {
  // ...
  
  load() {
    this.form.get('rating').patchValue(5);
  }

}</code></pre><p>In a real app, you might have the user's current rating store in a database somewhere and you would want to load these ratings when the user return to your app.</p><p>For this example, <code>load()</code> will simulate a saved rating of <code>5</code> which we will patch into our form.</p><p>Now, if you click on the newly create load button, you will see... nothing!</p><figure class="kg-card kg-image-card"><img src="https://ghost.woodies11.dev/content/images/2020/09/chrome_X0OP9h1D5D.png" class="kg-image" alt="How to PROPERLY implement ControlValueAccessor - Angular Form" loading="lazy"/></figure><p>Try submitting the form though and you will see that our value were indeed patched correctly.</p><figure class="kg-card kg-image-card"><img src="https://ghost.woodies11.dev/content/images/2020/09/chrome_QaOYNkROCa.png" class="kg-image" alt="How to PROPERLY implement ControlValueAccessor - Angular Form" loading="lazy"/></figure><p>By now, you probably realize what the problem is.</p><figure class="kg-card kg-image-card"><img src="https://ghost.woodies11.dev/content/images/2020/09/chrome_XIa9cMHU8X.png" class="kg-image" alt="How to PROPERLY implement ControlValueAccessor - Angular Form" loading="lazy"/></figure><p>Our component communication currently only work <strong>one-way.</strong> Our stars can update the model's value, but our <strong>programmatic change</strong> to the model's value <strong>doesn't get reflect back on the UI!</strong></p><p>This is where <code>writeValue(v)</code> comes in.</p><p>From <a href="https://angular.io/api/forms/ControlValueAccessor#writevalue">Angular's API Docs:</a></p><ul><li><code>writeValue(obj: any):void</code> - This method is called by the forms API to write to the view when programmatic changes from <strong>model to view</strong> are requested.</li></ul><p><code>writeValue()</code> is called whenever a change is <strong>programmatically</strong> made to the model. It is to notify the view that it needs to update the UI. (It will not be called if the change in value is made from UI interaction, i.e. from the view itself firing <code>OnChange()</code>).</p><p>Let's go back to our StarRatingComponent, this time properly implementing the <code>writeValue()</code> function.</p><pre><code class="language-ts">// star-rating.component.ts
export class StarRatingComponent implements ControlValueAccessor {
  // ...
  writeValue(rating: any): void {
    this.currentRating = rating
  }
}</code></pre><p>With that, our UI now properly reflect the change!</p><figure class="kg-card kg-image-card"><img src="https://ghost.woodies11.dev/content/images/2020/09/chrome_wHPjPLa7nZ.png" class="kg-image" alt="How to PROPERLY implement ControlValueAccessor - Angular Form" loading="lazy"/></figure><h3 id="common-pitfall">Common Pitfall</h3><p>I have seen way too many tutorials on the Internet telling you to call <code>this.onChange()</code> within the <code>writeValue()</code> method. You <strong>don't</strong> want to do this. It can cause some nasty infinite loop!</p><pre><code class="language-ts">// DON'T DO THIS!!!

const _value: any
public set value(v): void {
	if (v) {
    	this._value = v
        // highlight-start
        this.onChange(v)
        this.onTouched()
        // highlight-end
    }
}

writeValue(v) {
	// The setter will trigger `onChange()` (and `onTouched()`)
    // and that will cause infinite loopin some cases
	this.value = v
}</code></pre><p/><p>Going back to <a href="https://angular.io/api/forms/ControlValueAccessor#writevalue">Angular's API Docs:</a></p><ul><li><code>writeValue(obj: any):void</code> - This method is called by the forms API to write to the view when programmatic changes from <strong>model to view</strong> are requested.</li><li><code>registerOnChange(fn: any):void</code> - This method is called by the forms API on initialization to update the form model when values propagate from the <strong>view to the model</strong>.</li></ul><p>The key is <strong>model to view </strong>and <strong>view to model.</strong></p><p>The <code>writeValue()</code> method is reserved for cases where the form's value is changed <strong>outside </strong>the CVA. It is called by Angular so that the CVA can update its UI to reflect the change. </p><p>If you then call <code>onChange()</code> again, you would be passing the same value back to the form. Depending on how your form is setup, it can then register that change and pass it back to <code>writeValue()</code> again and bam! You got yourself an infinite loop!!! (A common case are when you use Reactive Form with NgRx. The form would trigger an action to a reducer which update the store. The store then propagate the update back to the form which pass it on the the CVA to update its view).</p><p>P.S. I don't actually like the name <code>writeValue(v)</code> so much. I think it's kind of misleading. Maybe something like <code>updateViewFromModel(v)</code> or just <code>updateView(v)</code> might be better.</p><h2 id="setdisabledstate">setDisabledState?</h2><p>The <code>ControlValueAccessor</code> also provide an optional method <code><strong>setDisabledState</strong>(isDisabled: boolean)?: void</code> for us to decide whether we need to implement this or not.</p><p>As the name suggest, this function is called by Angular whenever the <code>disabled</code> state of the FormControl changed.</p><p>If your CVA encapsulate a normal HTML <code>&lt;input&gt;</code>, you will need to pass this <code>isDisabled</code> by setting an appropriate <code>disabled</code> attribute on your <code>&lt;input&gt;</code> element.</p><p>In our case, since we are not using any of those, we can simply block the user's input when disabled and apply some styling.</p><pre><code class="language-ts">// star-rating.component.ts

// ...
export class StarRatingComponent implements ControlValueAccessor {

  // highlight-next-line
  isDisabled = false;

  setRating(n) {
  	// highlight-start
    if (this.isDisabled) {
      return;
    }
    this.currentRating = n;
    // highlight-end

    // Notify Angular to update its model
    this.onChange(this.currentRating);
    this.onTouched();
  }

  // highlight-start
  setDisabledState(isDisabled: boolean) {
    this.isDisabled = isDisabled;
  }
  // highlight-end
}
</code></pre><pre><code class="language-scss">// star-rating.component.scss
ul.star-rating {
    list-style: none;
    color: black;
    padding: 0;
    margin: 0;

	// highlight-next-line
    &amp;:not(.is-disabled):hover {
      color: rgb(42, 190, 235);

      &amp; li:hover ~ li {
        color: black;
      }
    }

    li {
      font-size: 3em;
      transition-duration: 300ms;
      cursor: pointer;
      display: inline;
    }

    .star-rating--highlight {
        color: orange;
    }
}

// highlight-start
ul.star-rating.is-disabled {
    opacity: 50%;
}
// highlight-end</code></pre><figure class="kg-card kg-image-card"><img src="https://ghost.woodies11.dev/content/images/2020/09/chrome_34cS5UpKYy.png" class="kg-image" alt="How to PROPERLY implement ControlValueAccessor - Angular Form" loading="lazy"/></figure><p>We do <strong>not </strong>want to block <code>writeValue(v)</code> when disabled though as the UI should still reflect changes to the model even when disabled (i.e. <code>load()</code> function should still update the number of star highlighted, regardless of whether the control is disabled or not). Whether the model can be changed or not <em>outside the view</em> is not up to the view to decide.</p><p/><p>Here is the final code for <code>star-rating.component.ts</code> for your reference:</p><pre><code class="language-ts">import { Component, forwardRef, Input } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';

@Component({
  selector: 'app-star-rating',
  templateUrl: './star-rating.component.html',
  styleUrls: ['./star-rating.component.scss'],
  providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      useExisting: forwardRef(() =&gt; StarRatingComponent),
      multi: true,
    },
  ],
})
export class StarRatingComponent implements ControlValueAccessor {
  @Input() numberOfStar = 5;

  currentRating: number;
  isDisabled = false;

  // *ngFor can't iterate on a number
  // so we are going to covert it to an array of [0...n-1]
  public toIterableArray(n) {
    return Array.from(Array(n).keys());
  }

  setRating(n) {
    if (this.isDisabled) {
      return;
    }
    this.currentRating = n;

    // Notify Angular to update its model
    this.onChange(this.currentRating);
    this.onTouched();
  }

  // Save the callbacks, make sure to have a default so your app
  // doesn't crash when one isn't (yet) registered
  private onChange = (v: any) =&gt; {};
  private onTouched = () =&gt; {};

  constructor() {}

  writeValue(rating: any): void {
    this.currentRating = rating;
  }
  registerOnChange(fn: any): void {
    this.onChange = fn;
  }
  registerOnTouched(fn: any): void {
    this.onTouched = fn;
  }

  setDisabledState(isDisabled: boolean) {
    this.isDisabled = isDisabled;
  }
}
</code></pre>]]></content:encoded></item><item><title><![CDATA[How I set up my Terminal (Oh-my-zsh + Powerline9k + iTerm 2)]]></title><description><![CDATA[This post show how I set up my Terminal (zsh, oh-my-zsh, iTerm 2/Terminal) on every machines I use for development.]]></description><link>https://ghost.woodies11.dev/how-i-set-up-my-terminal-oh-my-zsh-powerline9k-iterm-2/</link><guid isPermaLink="false">Ghost__Post__5f159f224e433422bb91b9da</guid><category><![CDATA[dev]]></category><category><![CDATA[tutorials]]></category><dc:creator><![CDATA[Romson Preechawit]]></dc:creator><pubDate>Thu, 23 Jul 2020 13:31:45 GMT</pubDate><media:content url="https://ghost.woodies11.dev/content/images/2020/07/Terminal-Setup.png" medium="image"/><content:encoded><![CDATA[<img src="https://ghost.woodies11.dev/content/images/2020/07/Terminal-Setup.png" alt="How I set up my Terminal (Oh-my-zsh + Powerline9k + iTerm 2)"/><p>Us developers spend lots and lots of time staring at our terminals (along with our IDE, of course). It makes sense to customize the way they look and feel to our liking. After all, having a colorful and good looking shell window can increase ones' productivity and dev skill by up to <strong>500% </strong>which translate to over <strong>9000+ business values</strong>!!!! (Just kidding, it was just a fun procrastinating thing to do while having an insomnia 😅).</p><p>Some plugins like the git plugin do have functional benefits for making sure that I am on the right branch and reminding me to push my code though.</p><h1 id="setting-up-zsh">Setting Up ZSH</h1><h2 id="zsh">ZSH</h2><p>Many UNIX systems, including Ubuntu and macOS—prior to macOS Catalina, come with <code>bash</code> as the default shell. <code>zsh</code> offer most of the features <code>bash</code> has and much more.</p><p>The most important quality-of-life improvement for me is its <strong>amazing autocomplete and approximate autocorrect functionality. </strong>Say, I execute an <code>ssh</code> command with a bunch of parameters a couple weeks back and want to run the same command again weeks later, in <code>bash</code> I either have to type the whole thing again (and trying to remember the destination's IP Address) or scroll through a bunch of commands history. With <code>zsh</code> I can type <code>ssh </code> then press arrow up and <code>zsh</code> will only show commands in the history starting with <code>ssh </code>.</p><p>Its folder name autocompletion, command lookup and such are also much better. The list of features <code>zsh</code> has is too long to list here so you will just have to try it yourself!</p><p>For the most part, <code>zsh</code> will function much the same as <code>bash</code> as far as compatability go. <strong>However, it does have some little quirks here and there </strong>that you may encouter (e.g. different behavior with very long <code>curl</code> command or having to use <code>source script.sh</code> instead of <code>. script.sh</code>).</p><h3 id="macos">macOS</h3><p>Since macOS Catalina, Apple has replaced the default shell—<code>bash</code> shell—with <code>zsh</code>. </p><p>For users running older version of macOS, go to <strong>System Preferences &gt; Users &amp; Group </strong>then right click on your user then click <strong>Advance.</strong></p><figure class="kg-card kg-image-card"><img src="https://ghost.woodies11.dev/content/images/2020/07/Screen-Shot-2020-07-20-at-20.51.35-1.png" class="kg-image" alt="How I set up my Terminal (Oh-my-zsh + Powerline9k + iTerm 2)" loading="lazy"/></figure><p>In the pop-up window, change the <strong>Login shell </strong>to <code>/bin/zsh</code>.</p><figure class="kg-card kg-image-card"><img src="https://ghost.woodies11.dev/content/images/2020/07/Screen-Shot-2020-07-20-at-20.51.47-1.png" class="kg-image" alt="How I set up my Terminal (Oh-my-zsh + Powerline9k + iTerm 2)" loading="lazy"/></figure><p/><h2 id="windows-10">Windows 10</h2><p>For Windows 10, I strongly recommend using <a href="https://docs.microsoft.com/en-us/windows/wsl/install-win10">WSL 2</a>. It will allow you to run actual Linux shell on Windows. Instructions on how to configure <code>zsh</code> on WSL 2 depends on the distro.</p><p>For Ubuntu based distro, you can use these commands to install <code>zsh</code>:</p><pre><code class="language-sh">sudo apt-get install wget curl git
sudo apt install zsh</code></pre><p>and this command to make it the default shell:</p><pre><code class="language-sh">chsh -s $(which zsh)</code></pre><h2 id="linux">Linux</h2><p>For those of you Linux users... well I'm sure you already know what you are doing. 😂</p><h2 id="oh-my-zsh">OH-MY-ZSH</h2><p><code>oh-my-zsh</code> is an open-source framework for managing zsh configurations. We will be using it mainly to set our themeing options here.</p><p>Ref: <a href="https://ohmyz.sh/#install">https://ohmyz.sh/#install</a></p><pre><code class="language-sh">sh -c "$(curl -fsSL https://raw.github.com/ohmyzsh/ohmyzsh/master/tools/install.sh)"</code></pre><h2 id="powerline9k-theme">Powerline9k Theme</h2><p>Powerline9k is the theme I will be using with my shell. You can find more themes by searching for "oh-my-zsh themes".</p><p>Ref: <a href="https://github.com/Powerlevel9k/powerlevel9k/wiki/Install-Instructions#option-2-install-for-oh-my-zsh">https://github.com/Powerlevel9k/powerlevel9k/wiki/Install-Instructions#option-2-install-for-oh-my-zsh</a></p><pre><code class="language-sh">git clone https://github.com/bhilburn/powerlevel9k.git ~/.oh-my-zsh/custom/themes/powerlevel9k</code></pre><h2 id="powerline-fonts">Powerline Fonts</h2><p>The Powerline9k theme (and many other themes) uses some special characters (e.g. the arrow looking character) that are non-standard. These font packs are designed specifically for uses with themes like these. You can install them using the quick install command below or manually by visiting this <a href="https://github.com/powerline/fonts">link</a>.</p><p>Ref: <a href="https://github.com/powerline/fonts">https://github.com/powerline/fonts</a></p><pre><code class="language-sh"># clone
git clone https://github.com/powerline/fonts.git --depth=1
# install
cd fonts
./install.sh
# clean-up a bit
cd ..
rm -rf fonts</code></pre><p>This will:</p><ol><li>Clone/download the fonts from the official GitHub repo.</li><li><code>cd</code> into the downloaded folder and execute the installation script <code>install.sh</code>.</li><li>Delete the downloaded folder.</li></ol><h2 id="iterm-2">iTerm 2</h2><p>For macOS users, I recommend using <a href="https://iterm2.com">iTerm 2</a>, which you can download <a href="https://iterm2.com">here</a>. It supports a bunch more features than the default Terminal.app, but more importantly, richer color!</p><h3 id="settings-">Settings:</h3><h4 id="appearance">Appearance</h4><p>This setting dictate how the app window will look.</p><p><strong>iTerm 2 &gt; Preferences &gt; Appearance &gt; Theme: <em>Minimal</em></strong></p><figure class="kg-card kg-image-card"><img src="https://ghost.woodies11.dev/content/images/2020/07/Screen-Shot-2020-07-20-at-21.14.11.png" class="kg-image" alt="How I set up my Terminal (Oh-my-zsh + Powerline9k + iTerm 2)" loading="lazy"/></figure><h4 id="profiles-">Profiles:</h4><p>This setting is where you adjust the look of your actual console (font family, font size, text color, background color, etc.).</p><p>For my exact profile, download <a href="https://gist.github.com/woodies11/670756a8057907e0dbbac3b588a391ab">this</a> JSON file and import it into iTerm 2 using <strong>Other Actions... &gt; Import JSON Profiles... </strong>and select the file you have just downloaded.</p><figure class="kg-card kg-image-card"><img src="https://ghost.woodies11.dev/content/images/2020/07/Screen-Shot-2020-07-23-at-20.19.26.png" class="kg-image" alt="How I set up my Terminal (Oh-my-zsh + Powerline9k + iTerm 2)" loading="lazy"/></figure><p/><h1 id="windows-10-terminal">Windows 10 Terminal</h1><p>For Windows 10 users, I recommend using the new <a href="https://www.microsoft.com/en-us/p/windows-terminal/9n0dx20hk701?activetab=pivot:overviewtab">Terminal</a> app for Windows released by Microsoft.</p><h1 id="-zshrc">.zshrc</h1><p>The <code>~/.zshrc</code> is a script that will run everytime <code>zsh</code> start. This is where you can configure your <code>zsh</code> and <code>oh-my-zsh</code> to your liking.</p><p>Below is my current <code>~/.zshrc</code>:</p><pre><code class="language-sh"># Path to your oh-my-zsh installation.
export ZSH="/Users/woods/.oh-my-zsh"

# Set name of the theme to load --- if set to "random", it will
# load a random theme each time oh-my-zsh is loaded, in which case,
# to know which specific one was loaded, run: echo $RANDOM_THEME
# See https://github.com/robbyrussell/oh-my-zsh/wiki/Themes

// highlight-next-line
# 1.
ZSH_THEME="powerlevel9k/powerlevel9k"

# ...

# git plugin
plugins=(git)

source $ZSH/oh-my-zsh.sh

# ========================================================================
# My own customization:
# ========================================================================

// highlight-next-line
# 2.
# POWERLEVEL9K
# --------------------------

DEFAULT_USER=woods
VIRTUAL_ENV_DISABLE_PROMPT=1

# These configurations are for the 
# powerlevel9k theme's promt

POWERLEVEL9K_ANACONDA_BACKGROUND=yellow
POWERLEVEL9K_DIR_SHOW_WRITABLE=true
POWERLEVEL9K_ALWAYS_SHOW_USER=true
POWERLEVEL9K_LEFT_PROMPT_ELEMENTS=(anaconda virtualenv context dir vcs)
POWERLEVEL9K_RIGHT_PROMPT_ELEMENTS=(status root_indicator background_jobs history time)

# How many levels to show (2: "../parent/current")
POWERLEVEL9K_SHORTEN_DIR_LENGTH=2

// highlight-next-line
# 3.
# Shortcuts:
# --------------------------
alias code='open -a "Visual Studio Code"'
alias code-insiders='open -a "Visual Studio Code - Insiders"'</code></pre><ol><li>This is where you can select the theme you want to use. The theme I use here is the <code>powerlevel9k</code> that I installed earlier.</li><li>These lines are specifically for the <code>powerlevel9k</code> theme. They tell the theme what to display on left and right promts and such.</li><li>These are general shortcut I create to open a file in <strong>Visual Studio Code/Visual Studio Code - Insiders. </strong>You can set more <code>alias</code> here as you wish.</li></ol>]]></content:encoded></item><item><title><![CDATA[Initializing an NPM project + Basic Scripting]]></title><description><![CDATA[This post will walk you through the basic of getting around NPM project, from initializing a project and using custom scripts.]]></description><link>https://ghost.woodies11.dev/npm-basics/</link><guid isPermaLink="false">Ghost__Post__5f0d57de4e433422bb91b9a7</guid><category><![CDATA[dev]]></category><category><![CDATA[nodejs]]></category><category><![CDATA[notes]]></category><dc:creator><![CDATA[Romson Preechawit]]></dc:creator><pubDate>Tue, 14 Jul 2020 07:00:22 GMT</pubDate><media:content url="https://ghost.woodies11.dev/content/images/2020/07/Node-JS-Cover-01-2.png" medium="image"/><content:encoded><![CDATA[<img src="https://ghost.woodies11.dev/content/images/2020/07/Node-JS-Cover-01-2.png" alt="Initializing an NPM project + Basic Scripting"/><p>To initialize an NPM project, <code>cd</code> into the root of your project directory and run:</p><pre><code class="language-bash">npm init
</code></pre><ul><li><code>package name</code> is your project's name (default to folder name).</li><li><code>entry point</code> is the file that should be run to start the application (e.g. <code>app.js</code> ,<code>index.js</code>, etc.)</li><li>By default, no script will be generated. You need to write the <code>start</code> yourself.</li></ul><h1 id="npm-scripts">NPM Scripts</h1><p>To write additional  <strong>npm scripts,</strong> go into the newly generated <code>package.json</code> file and edit the <code>"scripts": {}</code> object.</p><pre><code class="language-json">{
  "name": "my_node_package",
  "version": "1.0.0",
  "description": "Just a simple node project.",
  "main": "./app.js",
	// highlight-start
  "scripts": {
    "start": "node ./app.js",
    "test": "echo \\"Error: no test specified\\" &amp;&amp; exit 1",
    "foo": "echo bar"
  },
	// highlight-end
  "author": "Romson Preechawit (Woods)",
  "license": ""
}
</code></pre><h2 id="to-run-the-script-do-">To run the script, do:</h2><pre><code class="language-bash">npm run foo
</code></pre><h3 id="npm-start">npm start</h3><p><code>start</code> is the only script that can be run using the shorthand <code>npm start</code> without adding the <code>run</code> command. Other scripts need to use the full <code>npm run &lt;SCRIPT_NAME&gt;</code> syntax.</p><pre><code class="language-bash">npm start
</code></pre><h2 id="passing-arguments-to-an-npm-scripts">Passing arguments to an NPM scripts</h2><p>To pass arguments to a script, do:</p><pre><code class="language-bash">npm run &lt;SCRIPT_NAME&gt; -- &lt;ARGUMENTS&gt;
</code></pre><p>For example, in the case of <code>"build": "ng build"</code>, we can do:</p><pre><code class="language-bash">npm run build -- --prod
</code></pre><h1 id="npm-dependencies-management">NPM Dependencies Management</h1><pre><code class="language-bash">npm install &lt;PACKAGE_NAME&gt;</code></pre><p>Common options:</p><ul><li><code>--save-dev</code> only save this in development dependencies</li><li><code>--save</code> will save for production as well</li><li><code>-g</code> will install the package globally. You normally don't want to do this as the dependencies should be encapsulated within the project itself.</li></ul><p>For example:</p><pre><code class="language-bash">npm install nodemon --save-dev</code></pre><p>This will add these line in our <code>package.json</code></p><pre><code class="language-json">"devDependencies": {
    "nodemon": "^2.0.4"
}</code></pre>]]></content:encoded></item><item><title><![CDATA[Syntax Highlight Codes on your Ghost CMS Powered Gatsby Blog with PrismJS]]></title><description><![CDATA[This post will walk you through how to set up code syntax highlighting using PrismJS on a Ghost CMS backed Gatsby blog.]]></description><link>https://ghost.woodies11.dev/syntax-highlight-your-ghost-gatsby-blog-with-prism-js/</link><guid isPermaLink="false">Ghost__Post__5f05dfca4e433422bb91b8cc</guid><category><![CDATA[dev]]></category><category><![CDATA[gatsby]]></category><category><![CDATA[web]]></category><category><![CDATA[tutorials]]></category><dc:creator><![CDATA[Romson Preechawit]]></dc:creator><pubDate>Wed, 08 Jul 2020 15:31:33 GMT</pubDate><media:content url="https://ghost.woodies11.dev/content/images/2020/07/Gatsby-PrismJS-Cover-1.png" medium="image"/><content:encoded><![CDATA[<img src="https://ghost.woodies11.dev/content/images/2020/07/Gatsby-PrismJS-Cover-1.png" alt="Syntax Highlight Codes on your Ghost CMS Powered Gatsby Blog with PrismJS"/><p>This guide was written based on:</p><ul><li>Gatsby Ghost Blog build on top of <code>gatsby-starter-ghost</code> version <code>1.0.0</code></li></ul><p>with these dependencies (partial):</p><pre><code class="language-js">"@tryghost/helpers": "1.1.22",
"@tryghost/helpers-gatsby": "1.0.25",
"gatsby": "2.19.23",
"gatsby-rehype-prismjs": "^1.1.2",
"gatsby-source-filesystem": "2.1.48",
"gatsby-source-ghost": "4.0.4",
"gatsby-transformer-rehype": "^1.7.3",
"prismjs": "^1.20.0",
"react": "16.13.0",</code></pre><h1 id="1-install-require-packages">1. Install Require Packages</h1><p>First, we need to install the required packages. They are:</p><ul><li><code>gatsby-transformer-rehype</code></li><li><code>gatsby-rehype-prismjs</code></li></ul><p>Install them using:</p><pre><code class="language-bash">yarn add gatsby-transformer-rehype gatsby-rehype-prismjs</code></pre><p>or</p><pre><code class="language-bash">npm install --save gatsby-transformer-rehype gatsby-rehype-prismjs</code></pre><h1 id="2-configure-gatsby-config-js">2. Configure gatsby-config.js</h1><p>Next, we need to configure and add these new plugins to <code>gatsby-config.js</code> so that Gatsby is aware and make use of them.</p><pre><code class="language-js">module.exports = {
    //...
    plugins: [
        // ...
        
        // 1. - Add transformer for HTML sources.
        {
            resolve: `gatsby-transformer-rehype`,
            options: {
                // 2. - Ensure these only apply to type
                filter: (node) =&gt;
                    node.internal.type === `GhostPost` ||
                    node.internal.type === `GhostPage`,
                plugins: [
                    {
                        // 3. - Add syntax highlight for code block.
                        resolve: `gatsby-rehype-prismjs`,
                    },
                ],
            },
        }
    ]
}</code></pre><h1 id="3-configure-query">3. Configure Query</h1><h2 id="fragment-js">fragment.js</h2><p>The two plugins will now parse our HTML and add</p><pre><code class="language-graphql">childHtmlRehype {
	html
}</code></pre><p>to our <code>GhostPost</code> and <code>GhostPage</code> GraphQL.</p><p>To make Gatsby aware of these new properties, we need to add it to our fragments definition.</p><p>In our <code>./src/utils/fragments.js</code> file, find the line defining <code>fragment GhostPostFields on GhostPost</code> and add the above <code>childHtmlRehype</code> to it.</p><p>It should look something like:</p><pre><code class="language-js">// Used for single posts
export const ghostPostFields = graphql`
    fragment GhostPostFields on GhostPost {

        # ...

		# Content
        plaintext
        html
		// highlight-start
        childHtmlRehype {
            html
        }
		// highlight-end
		
		# ...

	}
`;</code></pre><p>You can do the same for <code>GhostPage</code>.</p><h2 id="post-js">post.js</h2><p>Now that Gatsby are aware of the transformed HTML, we need to switch our <code>post.js</code> to use this newly added properties instead of the normal <code>html</code>.</p><p>In <code>./src/templates/post.js</code>, apply following edit:</p><pre><code class="language-js">Post.propTypes = {
    data: PropTypes.shape({
        ghostPost: PropTypes.shape({
            codeinjection_styles: PropTypes.object,
            title: PropTypes.string.isRequired,
            html: PropTypes.string.isRequired,
            // highlight-start
            childHtmlRehype: PropTypes.shape({
                html: PropTypes.string.isRequired,
            }),
            // highlight-end
            feature_image: PropTypes.string,
            primary_author: PropTypes.shape({
                name: PropTypes.string.isRequired,
                profile_image: PropTypes.string,
                slug: PropTypes.string.isRequired,
            }).isRequired,
        }).isRequired,
    }).isRequired,
    location: PropTypes.object.isRequired,
};</code></pre><p>Now, replace all <code>post.html</code> in your JSX with <code>post.childHtmlRehype.html</code>.</p><pre><code class="language-jsx">{/* The main post content */}
&lt;section
    className="content-body load-external-scripts"
    dangerouslySetInnerHTML={{
    // highlight-next-line
    	__html: post.childHtmlRehype.html,
    }}
/&gt;</code></pre><h2 id="page-js">page.js</h2><p>Repeat the same steps as <code>post.js</code> above.</p><h1 id="4-select-theme">4. Select Theme</h1><p>Now you can select which CSS theme to use. Go to your <code>gatsby-browser.js</code> and add the following require:</p><pre><code class="language-js">// gatsby-browser.js
require("prismjs/themes/prism-solarizedlight.css")</code></pre><p>You can find all the default themes that ship with PrismJS <a href="https://github.com/PrismJS/prism/tree/1d5047df37aacc900f8270b1c6215028f6988eb1/themes">here</a>. Visit their official <a href="https://prismjs.com">website</a> for theme preview.</p><p>Finally, restart your Gatsby Development server and enjoy beautifully highlighted and clean looking code! Don't forget to write and share more content to the world!!! :)</p>]]></content:encoded></item><item><title><![CDATA[WSL 2 - localhost problem]]></title><description><![CDATA[WSL 2 come with a new architecture, supporting faster linux file access and implementing all core features. However, the new architecture also create a few  new problems, especially with networking.]]></description><link>https://ghost.woodies11.dev/wsl-2-localhost-problem/</link><guid isPermaLink="false">Ghost__Post__5efa97184e433422bb91b803</guid><category><![CDATA[dev]]></category><category><![CDATA[windows]]></category><category><![CDATA[wsl]]></category><category><![CDATA[network]]></category><dc:creator><![CDATA[Romson Preechawit]]></dc:creator><pubDate>Tue, 30 Jun 2020 01:47:16 GMT</pubDate><media:content url="https://images.unsplash.com/photo-1518432031352-d6fc5c10da5a?ixlib=rb-1.2.1&amp;q=80&amp;fm=jpg&amp;crop=entropy&amp;cs=tinysrgb&amp;w=2000&amp;fit=max&amp;ixid=eyJhcHBfaWQiOjExNzczfQ" medium="image"/><content:encoded><![CDATA[<img src="https://images.unsplash.com/photo-1518432031352-d6fc5c10da5a?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=2000&fit=max&ixid=eyJhcHBfaWQiOjExNzczfQ" alt="WSL 2 - localhost problem"/><p>WSL 2 now run on its own light weight virtual machine instead of being part of Windows System.</p><p>This create a problem where the VM now has its own network interface and its own IP Address.</p><p>From what I read, WSL 2 suppose to have a feature called Localhost Forwarding that will allow Windows to automatically forward <code>localhost</code> request to WSL 2 VM.</p><p>In many case, this forwarded request will have a different <em>destination</em> or <em>host</em> than <code>localhost</code> (most likely to be WSL 2's IP Address). As such, you will need to configure your server to bind to WSL 2's IP Address or to <code>0.0.0.0</code> (accept any IP) instead of <code>localhost</code>.</p><p>Unfortunately, <a href="http://localhost">localhost</a> forwarding doesn't seem to work on my machine so I have to type out my WSL 2's IP address inside my browser instead of <code>localhost</code> but otherwise, this is an okay workaround for now.</p><p>You can find your WSL 2's IP Address with (Ubuntu-20.04 LTS):</p><pre><code class="language-bash">ip addr list | grep inet
</code></pre><p><em>Apparently, <code>ifconfig</code> has been deprecated in favor of the <code>ip</code> (iproute2) command in newer distros.</em></p><h2 id="a-few-examples">A few examples</h2><p>For gatsby, you need to pass <code>-H 0.0.0.0</code> as an argument to your <code>gatsby develop</code> command.</p><pre><code class="language-bash">yarn run dev -H 0.0.0.0
</code></pre><p>For your own node server, make sure the <code>listen()</code> method bind to <code>0.0.0.0</code> .</p><pre><code class="language-js">server.listen(3000, '0.0.0.0')
</code></pre><h1 id="accessing-from-other-machines">Accessing from other machines</h1><p>It seems like accessing WSL 2 servers from other devices on the same network also will not work without configuring Windows to forward such request to WSL 2 manually. </p><p>I think this is because the connection between WSL 2 and Windows is being done over a virtual network interface, and thus only exist and accessible within the Windows host itself. For external devices to connect, they need to send their requests to Windows' 'real' IP address on that network and have Windows forward those requests through the virtual interface to WSL 2.</p><p>If you run <code>ipconfig</code> in PowerShell, you will likely see a few virtual Ethernet adapter (vEthernet) will IP on different subnet.</p><figure class="kg-card kg-image-card"><img src="https://ghost.woodies11.dev/content/images/2020/06/InkedWindowsTerminal_88mben3aLO_LI.jpg" class="kg-image" alt="WSL 2 - localhost problem" loading="lazy"/></figure><p>I haven't had any needs to do this yet but will add how to configure this when I get to it.</p>]]></content:encoded></item><item><title><![CDATA[Atomic Habits - Book Review]]></title><description><![CDATA[A quick review of one of my favorite self-help book of recent time, Atomic Habits by James Clear.]]></description><link>https://ghost.woodies11.dev/the-atomic-habits-book-review/</link><guid isPermaLink="false">Ghost__Post__5ea093854e433422bb91b722</guid><category><![CDATA[books]]></category><category><![CDATA[reviews]]></category><category><![CDATA[productivity]]></category><dc:creator><![CDATA[Romson Preechawit]]></dc:creator><pubDate>Wed, 22 Apr 2020 18:58:41 GMT</pubDate><media:content url="https://ghost.woodies11.dev/content/images/2020/04/Atomic-Habits-Cover.png" medium="image"/><content:encoded><![CDATA[<img src="https://ghost.woodies11.dev/content/images/2020/04/Atomic-Habits-Cover.png" alt="Atomic Habits - Book Review"/><p>I first read this book back in 2018. It was around the end of the year and I was all hyped up with the upcoming <em>New Year Resolutions Festival.</em> I was extremely skeptic at first since I found most self-help book that came out recently to either: full of quick-fixes nonsense, or repeat words and ideas that has already been repeated a thousands time before. But this book was different. A rare gem among self-help books of recent time.</p><p>Recently, I picked the book up again and the same magic still applied. I read through about a quarter of the book in that one sitting. </p><p>Now that I have a blog, I thought I might try doing a review of this book.</p><h1 id="main-theme-atomic-habits">Main Theme - <em>Atomic</em> Habits</h1><p>The main theme of the books is about how you can create and/or change your habits.</p><p>It first introduce you to the idea of a “small wins” or “1% improvement”. Hence, the name <em>Atomic</em> Habit. The author—James Clear—illustrated how a 1% different compound over a period of time basically become deciding factor in where you will end up in life. He has done an amazing job conveying these concepts with easy to understand examples.</p><h1 id="practical-step-by-step-guides">Practical Step-by-Step Guides</h1><p>Later in the book, James gives us a bunch of very practical, steps-by-steps guides on how to design and change your habits.</p><p>He started by giving us a new paradigm on habit changes and goal setting. Specifically, how it is much more powerful to focus on <em>identity changes</em> rather than <em>outcome changes</em>. The book argue that there is a huge psychological different between someone who say “I’m trying to loss weight so I cannot drink that soda” vs “I’m a health-conscious person, so I will not drink that soda”.</p><p>Subsequently, the book break down habits into its four fundamental components; cue, craving, response, and reward (similar to cue, routine, and reward, if you read The Power of Habits). James then go into detail on how to basically <em>engineer</em> each of the components so that you can most effectively create or destroy a habit.</p><h1 id="what-i-like-most">What I Like Most</h1><p>Those guides mentioned above are what I like most about this book. The book is <strong><em>extremely practical.</em></strong> </p><p>Many self-help books talk a lot about new and amazing perspective, ideas, theory, and such. These books, while inspiring, still left you to wonder “well, so how can I apply this to my life?”. </p><p>In fact, I feel this book is like the perfect complimentary to the book The Power of Habits. The Power of Habits did a great job introducing you to the idea and important of habits, and the story Charles gave are very inspiring. Yet, after reading that book, I’m still not quite sure how to implement those habits changes myself. Atomic Habit teaches me exactly that, and in a very detailed, very practical step-by-steps guides.</p><h1 id="conclusion">Conclusion</h1><p>Overall, I think this is one of the best self-help book of recent time. It is a book definitely worth checking out.</p><p>If you can, I would recommend reading The Power of Habits first before going into this book. However, if you must pick either one, I would pick this one.</p><p>If you have read the book, leave comments below on whether you like it or not, and what's the most profound thing you learnt from it. I would love to read all of these!</p><p>Also, if you are interested, I am going to do a book summarize of this book so stay tuned!</p>]]></content:encoded></item><item><title><![CDATA[First-impression iPad Mouse Support on iPadOS 13.4 (iPad Pro 10.5)]]></title><description><![CDATA[In this post, I am taking a look at the new and improved mouse support on iPadOS 13.4]]></description><link>https://ghost.woodies11.dev/first-impression-ipad-mouse-support-on-ios-134-ipad-pro-105/</link><guid isPermaLink="false">Ghost__Post__5e75d51d4e433422bb91b6a2</guid><category><![CDATA[dev]]></category><category><![CDATA[reviews]]></category><category><![CDATA[iOS]]></category><category><![CDATA[thoughts]]></category><category><![CDATA[apps]]></category><dc:creator><![CDATA[Romson Preechawit]]></dc:creator><pubDate>Sat, 21 Mar 2020 09:01:30 GMT</pubDate><media:content url="https://ghost.woodies11.dev/content/images/2020/03/DSC01468.jpeg" medium="image"/><content:encoded><![CDATA[<img src="https://ghost.woodies11.dev/content/images/2020/03/DSC01468.jpeg" alt="First-impression iPad Mouse Support on iPadOS 13.4 (iPad Pro 10.5)"/><p>Apple surprise us this morning when they release the new iPad Pro, new MacBook Air, and new Mac Mini. They also release a new accessory for the iPad Pro, the new Magic Keyboard.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://ghost.woodies11.dev/content/images/2020/03/Image-20-3-20-1-02-AM.jpeg" class="kg-image" alt="First-impression iPad Mouse Support on iPadOS 13.4 (iPad Pro 10.5)" loading="lazy"><figcaption>Credit: www.apple.com</figcaption></img></figure><p>With that new accessory comes the long awaited full mouse support.</p><h1 id="what-s-it-like">What’s it like?</h1><p>In this first-impression, I will be using the iPad Pro 10.5” running iPadOS 13.4 with a Logitech MX Master 2S and a Logitech K380 keyboard.</p><figure class="kg-card kg-image-card"><img src="https://ghost.woodies11.dev/content/images/2020/03/Image-20-3-20-1-07-AM.png" class="kg-image" alt="First-impression iPad Mouse Support on iPadOS 13.4 (iPad Pro 10.5)" loading="lazy"/></figure><p>Once upgraded, you can connect to any Bluetooth mouse from your Bluetooth setting, just like any other Bluetooth accessories.</p><p>Unlike previous version of iPadOS 13, the new cursor is much smaller and smarter. It will actually adapt and change appearance based on where it is on the screen.</p><figure class="kg-card kg-image-card"><img src="https://ghost.woodies11.dev/content/images/2020/03/2020-03-21-15.05.35.gif" class="kg-image" alt="First-impression iPad Mouse Support on iPadOS 13.4 (iPad Pro 10.5)" loading="lazy"/></figure><p>You can select texts just like you normally do on a computer. Right click will usually bring up the contextual menu or activate<strong> Haptic Touch </strong>functions<strong>.</strong></p><figure class="kg-card kg-image-card"><img src="https://ghost.woodies11.dev/content/images/2020/03/image.png" class="kg-image" alt="First-impression iPad Mouse Support on iPadOS 13.4 (iPad Pro 10.5)" loading="lazy"/></figure><figure class="kg-card kg-image-card"><img src="https://ghost.woodies11.dev/content/images/2020/03/image-1.png" class="kg-image" alt="First-impression iPad Mouse Support on iPadOS 13.4 (iPad Pro 10.5)" loading="lazy"/></figure><p>Moving the cursor to the bottom of the screen <strong>quickly </strong>bring up the Dock.</p><figure class="kg-card kg-image-card"><img src="https://ghost.woodies11.dev/content/images/2020/03/2020-03-21-15.55.50.gif" class="kg-image" alt="First-impression iPad Mouse Support on iPadOS 13.4 (iPad Pro 10.5)" loading="lazy"/></figure><p>Clicking on the left side of the status bar bring down the <strong>Notification Center.</strong></p><figure class="kg-card kg-image-card"><img src="https://ghost.woodies11.dev/content/images/2020/03/2020-03-21-15.57.15.gif" class="kg-image" alt="First-impression iPad Mouse Support on iPadOS 13.4 (iPad Pro 10.5)" loading="lazy"/></figure><p>Clicking on the right side activate <strong>Control Center</strong>.</p><figure class="kg-card kg-image-card"><img src="https://ghost.woodies11.dev/content/images/2020/03/2020-03-21-15.57.53.gif" class="kg-image" alt="First-impression iPad Mouse Support on iPadOS 13.4 (iPad Pro 10.5)" loading="lazy"/></figure><h1 id="reduce-pointer-animations">Reduce Pointer Animations</h1><p>One thing that annoy me right away is how the cursor sort of “snap” to buttons and screen elements (see the animation below). This look really at first, but become irritating to use very fast.</p><figure class="kg-card kg-image-card"><img src="https://ghost.woodies11.dev/content/images/2020/03/2020-03-21-15.08.34.gif" class="kg-image" alt="First-impression iPad Mouse Support on iPadOS 13.4 (iPad Pro 10.5)" loading="lazy"/></figure><p>First off, your cursor disappear when this happen.</p><p>Second, it feels like the cursor “jumps” to the buttons. This combined with the fact that the cursor is hidden, make it really easy to lost track of where the cursor is.</p><p>These make it really hard to preciously control the cursor. It is even more problematic for larger icons/buttons since it take longer for the cursor to “escape” the icons’ hold.</p><figure class="kg-card kg-image-card"><img src="https://ghost.woodies11.dev/content/images/2020/03/2020-03-21-15.16.47.gif" class="kg-image" alt="First-impression iPad Mouse Support on iPadOS 13.4 (iPad Pro 10.5)" loading="lazy"/></figure><p>Though, I only have a few hours with the new OS so I am not sure if this is something I will get used to.</p><p>Luckily, I don’t need to find out. There is actually an option to disable this behavior. Go to <strong>Settings &gt; Accessibility &gt; Pointer Control </strong><u>(Only visible while a mouse is connected)</u> and <strong><u>turn off</u></strong> <strong>Pointer Animations.</strong></p><figure class="kg-card kg-image-card"><img src="https://ghost.woodies11.dev/content/images/2020/03/image-3.png" class="kg-image" alt="First-impression iPad Mouse Support on iPadOS 13.4 (iPad Pro 10.5)" loading="lazy"/></figure><figure class="kg-card kg-image-card"><img src="https://ghost.woodies11.dev/content/images/2020/03/IMG_1681.png" class="kg-image" alt="First-impression iPad Mouse Support on iPadOS 13.4 (iPad Pro 10.5)" loading="lazy"/></figure><p>This makes the cursor act much like what you would have on a computer.</p><figure class="kg-card kg-image-card"><img src="https://ghost.woodies11.dev/content/images/2020/03/2020-03-21-15.09.02.gif" class="kg-image" alt="First-impression iPad Mouse Support on iPadOS 13.4 (iPad Pro 10.5)" loading="lazy"/></figure><h1 id="other-quirks">Other Quirks</h1><h2 id="going-home">Going Home</h2><p>Another little quirk with this new mouse support is the fact that there is no way to go Home with the mouse, unless you have the newer iPad Pro models that have Face ID (and no home button).</p><p>You can click on the left side of the status bar to get to Control Center, and click on the left (the clock) to get to Notification Center. To go home, you would click on the <strong>Home Bar</strong> at the bottom and it will take you home. However, older or non-Pro iPads that still have Touch-ID <strong>doesn’t have this Home Bar at the bottom.</strong> The best way I found to go home is to set a custom button on my mouse to act as a home button, press shortcut keys on a keyboard, or just use the physical Home button. There might be other way to go home but I have not figure that out yet. Please, let me know in the comment if you know how to go home with a mouse on a non-Pro iPad.</p><h2 id="mouse-and-on-screen-keyboard">Mouse and On-Screen Keyboard</h2><p>The last mildly annoying thing was the fact that my mouse (the Logitech MX Master 2S) also have custom buttons on it, which a lot of Bluetooth mice do nowadays. These buttons seem to be recognized by the OS to be some kind of keyboard. Now, here is the problem, because the OS thought there is also a keyboard being attached to it, it refuses to show the on-screen keyboard while this mouse is connected to it!</p><p>This means that I can’t type while a mouse is connected, unless I also have an external keyboard connected to the iPad at the same time!</p><p>This is something that should be easily fixable with a software update. Add a button to show/hide the on-screen keyboard like what we have on Windows 10, for example. I really hope Apple would fix this in the next OS update.</p><h1 id="teamviewer">TeamViewer</h1><p>As of March 25, 2020, TeamViewer has not been updated to support the mouse feature yet. Things like scrolling with the scroll-wheel still doesn’t work. </p><p>On Windows, clicking and selecting items do work quite well. Much better than with touch alone. On macOS though, the click actions is extremely finicky and hard to get right. Dragging work just fine, but single click rarely seems to work. This seems like a TeamViewer on macOS issues though, not the iPad itself. Hopefully, this is something that will get fixed soon.</p><p>It would be a dream to have your powerful computer running at home and remote access in to do your work from an iPad, anywhere in the world. I am positive that, once the company update their application to support mouse, this will be an amazing setup to use!</p><h1 id="conclusion">Conclusion</h1><p>My first impression with this new mouse support for the iPad is still somewhat of a mixed experience. I feel like there are still quite a bit of quirk here and there that will need to be patched out for this to be truly useful. For a lot of people, I don’t think this is a must. Unless you are doing a lot of things that require a mouse, you won’t gain much by having this kind of mouse support (surely, <strong>most </strong>people don’t go back and forth to edit their emails that often to required a mouse). For those that do a lot of task that need a mouse (e.g. video editing, editing long and complex documents), I still think it is better to just get a laptop.</p><p>Nevertheless, I still think this is definitely a step in the right direction for iPads and I’m really excited to see what to come.</p>]]></content:encoded></item><item><title><![CDATA[How to make your own local URL Shortening Server]]></title><description><![CDATA[In this post, I am going to emulate a link shortening utility being employed in my work's intranet.]]></description><link>https://ghost.woodies11.dev/how-to-make-your-own-local-url-shortening-server/</link><guid isPermaLink="false">Ghost__Post__5e738dbe4e433422bb91b5b9</guid><category><![CDATA[dev]]></category><category><![CDATA[network]]></category><category><![CDATA[tutorials]]></category><dc:creator><![CDATA[Romson Preechawit]]></dc:creator><pubDate>Thu, 19 Mar 2020 15:55:30 GMT</pubDate><media:content url="https://images.unsplash.com/photo-1555952494-efd681c7e3f9?ixlib=rb-1.2.1&amp;q=80&amp;fm=jpg&amp;crop=entropy&amp;cs=tinysrgb&amp;w=2000&amp;fit=max&amp;ixid=eyJhcHBfaWQiOjExNzczfQ" medium="image"/><content:encoded><![CDATA[<img src="https://images.unsplash.com/photo-1555952494-efd681c7e3f9?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=2000&fit=max&ixid=eyJhcHBfaWQiOjExNzczfQ" alt="How to make your own local URL Shortening Server"/><p>So, my corporate have this amazing tool call <code>goto/</code> that allow anyone to shorten any URL into their own choice of keywords. For example, I could type <code>goto/kitten</code> into the URL bar at the top and have it redirect to <a href="https://www.reddit.com/r/cats/">this</a> Cats Subreddit (<a href="https://www.reddit.com/r/cats/">https://www.reddit.com/r/cats/</a>).</p><p>This is very useful for URL paths that are under long subdomains or with very long query/path. Something you found quite often in the corporate world, ridden by many intranet URL's and IP's to remember.</p><p>The problem though, arise when I came home to my personal computer. I became so used to the tool that I occasionally type these <code>goto/links</code> at home too!</p><p>To solve this problem, I set out to try recreate the same function myself!</p><h1 id="redirect-server-using-node-js-and-express">Redirect Server using Node JS and Express</h1><p>Now, I used Node JS and Express before, but I am no expert in it.</p><p>Here is a very basic script I used:</p><pre><code class="language-js">// index.js
var express = require('express');
var app = express();

app.get('/udm', function(req, res){
    res.redirect('https://www.udemy.com/home/my-courses/learning/')
})

app.listen(80, () =&gt; {
    console.log('Start listening on port 80')
})</code></pre><p>To achieve the same effect as the ones at my work, I need the redirect to go through without me specifying my server's port (e.g. <code>goto:3000/test</code> doesn't look pretty). As such, the server need to listen on port 80, the default HTTP port. Browsers are sending request to IP addresses through this port by default.</p><p>After running this server, I can go to <code>http://localhost/udm</code> and have it correctly redirect me to Udemy page.</p><figure class="kg-card kg-image-card"><img src="https://ghost.woodies11.dev/content/images/2020/03/2020-03-19-22.39.15.gif" class="kg-image" alt="How to make your own local URL Shortening Server" loading="lazy"/></figure><p>Now, to have <code>goto</code> be the redirect keyword instead of <code>localhost</code>, we need to tell the OS to redirect <code>goto</code> to <code>localhost</code>. To do this, we can edit the <code>hosts</code> file located at <code>/etc/hosts</code> ( <code>c:\Windows\System32\Drivers\etc\hosts</code> on Windows). Note that you will need to have root or admin permission to complete this step.</p><p>Add this entry to the file then save.</p><pre><code>127.0.0.1       goto</code></pre><figure class="kg-card kg-image-card"><img src="https://ghost.woodies11.dev/content/images/2020/03/Screen-Shot-2020-03-19-at-22.42.58.png" class="kg-image" alt="How to make your own local URL Shortening Server" loading="lazy"/></figure><p>What this does is telling the OS to redirect names on the right to addresses on the left of each entry. In this case, it tells the OS to redirect any request sent to <code>goto</code> to <code>127.0.0.1</code> or our <code>localhost</code>.</p><p>With this, our <code>goto/udm</code> should now work.</p><figure class="kg-card kg-image-card"><img src="https://ghost.woodies11.dev/content/images/2020/03/2020-03-19-22.44.58.gif" class="kg-image" alt="How to make your own local URL Shortening Server" loading="lazy"/></figure><p/><h1 id="conclusion">Conclusion</h1><p>With this simple script, it allow me to:</p><ol><li>Shorten any URL with my own custom keywords.</li><li><strong>More or less verify the theory I have for how the one at my work functions.</strong> Freeing me from the itch I have everytime I use it!</li></ol><h1 id="improvement">Improvement</h1><p/><ol><li>Move the script to a proper server on the network.</li></ol><p>Right now, this would only work on my laptop, and I need to have that Node JS server running on it all the time. What I will try to do next is to containerize this app then move it to run on my Raspberry Pi that is currently <a href="https://blog.woodies11.dev/whats-on-my-home-network/">running Pi Hole on my local network.</a></p><p>2. Implement better way to add new entries.</p><p>The version at my work also have a web interface that anyone can go on to register a new shorten link. Right now, all links are hard coded to each <code>/&lt;endpoints&gt;</code>. Ideally, this should be changed to a wildcard, then have the URL be parsed by the script. There should then be a lookup table of some sort that the app can look up from.</p>]]></content:encoded></item><item><title><![CDATA[Drawing Coffee Presses (French Presses) Illustration using Sketch for Mac | Time-lapse]]></title><description><![CDATA[Drawing Coffee Presses (French Presses) Illustration using Sketch for Mac | Time-lapse]]></description><link>https://ghost.woodies11.dev/drawing-coffee-presses-french-presses-illustration-using-sketch-for-mac-time-lapse/</link><guid isPermaLink="false">Ghost__Post__5e72490d4e433422bb91b5a0</guid><category><![CDATA[arts]]></category><category><![CDATA[design]]></category><category><![CDATA[video]]></category><dc:creator><![CDATA[Romson Preechawit]]></dc:creator><pubDate>Wed, 18 Mar 2020 16:17:22 GMT</pubDate><media:content url="https://ghost.woodies11.dev/content/images/2020/03/Thumbnail-FrenchPresses.jpg" medium="image"/><content:encoded><![CDATA[<figure class="kg-card kg-embed-card"><iframe width="480" height="270" src="https://www.youtube.com/embed/TAahDtcy-AY?feature=oembed" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen=""/></figure>]]></content:encoded></item><item><title><![CDATA[[Part 3] How to set up your own Ghost + Gatsby blog (with Digital Ocean and Netlify)]]></title><description><![CDATA[Finally, in this post, we will look at how to host our Gatsby generated static sites on Netlify for free, and how to set up a webhook to automatically regenerate and update our site each time our content change.]]></description><link>https://ghost.woodies11.dev/how-to-setup-ghost-netlify-part-3/</link><guid isPermaLink="false">Ghost__Post__5e6a5d154e433422bb91b431</guid><category><![CDATA[tutorials]]></category><category><![CDATA[dev]]></category><category><![CDATA[web]]></category><dc:creator><![CDATA[Romson Preechawit]]></dc:creator><pubDate>Sat, 14 Mar 2020 05:24:57 GMT</pubDate><media:content url="https://ghost.woodies11.dev/content/images/2020/03/Frame-1.png" medium="image"/><content:encoded><![CDATA[<img src="https://ghost.woodies11.dev/content/images/2020/03/Frame-1.png" alt="[Part 3] How to set up your own Ghost + Gatsby blog (with Digital Ocean and Netlify)"/><p/><p>Finally, in this post, we will look at how to host our Gatsby generated static sites on Netlify for free, and how to set up a webhook to automatically regenerate and update our site each time our content change.</p><p>Before we get to Netlify though, let's look at how the final achitecture/workflow will work.</p><figure class="kg-card kg-image-card"><img src="https://ghost.woodies11.dev/content/images/2020/03/Screen-Shot-2020-03-12-at-23.18.35.png" class="kg-image" alt="[Part 3] How to set up your own Ghost + Gatsby blog (with Digital Ocean and Netlify)" loading="lazy"/></figure><ol><li>Each time any contents on our site changed, Ghost will automatically send a POST request to Netlify, notifying that a rebuild should be made.</li><li>Netlify then pull our Gatsby source code from our Git repository.</li><li>CI/CD pipeline run <code>gatsby build</code>.</li><li>During build process, Gatsby pull contents from our Ghost CMS to generate the final static sites.</li><li>Netlify then publishes the final result to its CDN.</li><li>The CDN then serve these to users upon visit. </li></ol><h1 id="git-repository">Git Repository</h1><p>In the previous parts (<a href="https://blog.woodies11.dev/how-to-setup-ghost-netlify-part-1">Part 1</a> and <a href="https://blog.woodies11.dev/how-to-setup-ghost-netlify-part-2">Part 2</a>), we took care of the CMS and our Gatsby Source code. The one thing that is missing apart from the web host, Netlify, is a Git repo.</p><p>We need our code to sit somewhere that Netlify can pull, and our local machine just won't do. So, we need to push our code to a repository first. There is a lot of repo to choose from and a lot of tutorials to teach you how to do this so I won't go into that detail here.</p><h1 id="netlify">Netlify</h1><p>With Git repo out of the way, we will now set up automatic build process and host our site on Netlify.</p><p>First, you want to <a href="https://app.netlify.com/signup">sign up</a> for a free Netlify account.</p><figure class="kg-card kg-image-card"><img src="https://ghost.woodies11.dev/content/images/2020/03/Screen-Shot-2020-03-12-at-23.04.44.png" class="kg-image" alt="[Part 3] How to set up your own Ghost + Gatsby blog (with Digital Ocean and Netlify)" loading="lazy"/></figure><p>Once that isd done, click on <strong>New site from Git.</strong></p><figure class="kg-card kg-image-card"><img src="https://ghost.woodies11.dev/content/images/2020/03/970E8100-66CD-4F6E-BB19-47813C0B0F73.png" class="kg-image" alt="[Part 3] How to set up your own Ghost + Gatsby blog (with Digital Ocean and Netlify)" loading="lazy"/></figure><p>Pick the Git provider that your site's source code is hosted. We will be using BitBuck in this tutorial.</p><figure class="kg-card kg-image-card"><img src="https://ghost.woodies11.dev/content/images/2020/03/76450745-08C9-40A1-AA0E-10ACA6103278.png" class="kg-image" alt="[Part 3] How to set up your own Ghost + Gatsby blog (with Digital Ocean and Netlify)" loading="lazy"/></figure><p>Authenticate with your Git provider then select the correct repository.</p><figure class="kg-card kg-image-card"><img src="https://ghost.woodies11.dev/content/images/2020/03/5801AEBC-B866-4B6D-8387-846970968374.png" class="kg-image" alt="[Part 3] How to set up your own Ghost + Gatsby blog (with Digital Ocean and Netlify)" loading="lazy"/></figure><p>Make sure the <strong>Branch to deploy </strong>is set to <code>master</code>.</p><p>Set <strong>Build command</strong> to <code>gatsby build</code>. This is the command that Netlify will run each time a build is requested.</p><p>Next, set the <strong>Publish directory </strong>to <code>public/</code>. This tell Netlify where your final static files (the result of previos build command) is located at. These are the files that Netlify will publish to its CDN.</p><p>Finally, click on <strong>Show advanced </strong>to configure our API keys.</p><figure class="kg-card kg-image-card"><img src="https://ghost.woodies11.dev/content/images/2020/03/259BBA01-DF21-449B-8C93-1A944FD2187A.png" class="kg-image" alt="[Part 3] How to set up your own Ghost + Gatsby blog (with Digital Ocean and Netlify)" loading="lazy"/></figure><p>Add two variable called <code>GHOST_API_URL</code> and <code>GHOST_CONTENT_API_KEY</code>, then put the values you obtained from Ghost's Integration we set up in the previous step in each respective value field.</p><p>Finally, click <strong>Deploy site.</strong></p><figure class="kg-card kg-image-card"><img src="https://ghost.woodies11.dev/content/images/2020/03/09239286-7A5C-4DD3-800E-2C8DAFAA2C08.png" class="kg-image" alt="[Part 3] How to set up your own Ghost + Gatsby blog (with Digital Ocean and Netlify)" loading="lazy"/></figure><p>Wait awhile while the your site is being build. If everything goes smoothly, you will see the status turned to <strong>Published. </strong>Once your site finished building, you can click on the URL below your site's name to go to your hosted site. Note that, Netlify will likely generate some random subdomain for you initially. We will look at how to change this later.</p><figure class="kg-card kg-image-card"><img src="https://ghost.woodies11.dev/content/images/2020/03/32FE81F0-33A5-4B0A-94CC-07B68D947BAF.png" class="kg-image" alt="[Part 3] How to set up your own Ghost + Gatsby blog (with Digital Ocean and Netlify)" loading="lazy"/></figure><p>You should see that your site is now live and accessible from the Internet!</p><figure class="kg-card kg-image-card"><img src="https://ghost.woodies11.dev/content/images/2020/03/Screen-Shot-2020-03-18-at-22.15.32.png" class="kg-image" alt="[Part 3] How to set up your own Ghost + Gatsby blog (with Digital Ocean and Netlify)" loading="lazy"/></figure><h1 id="setting-up-webhook-for-ci-cd">Setting up Webhook for CI/CD</h1><p>Finally, we do not want to have to manually tell Netlify to rebuild our Gatsby site everytime we create or modify a blog post. Fortunately, Netlify provide a webhook that we can call to automate this process.</p><p/><p>First, go to <strong>Site setting &gt; Build &amp; deploy</strong></p><figure class="kg-card kg-image-card"><img src="https://ghost.woodies11.dev/content/images/2020/03/A674CD30-ABDA-4A05-ABEC-0ED0F0F7D662.png" class="kg-image" alt="[Part 3] How to set up your own Ghost + Gatsby blog (with Digital Ocean and Netlify)" loading="lazy"/></figure><figure class="kg-card kg-image-card"><img src="https://ghost.woodies11.dev/content/images/2020/03/15090CCE-3F64-49E5-9668-A1C0255783AF.png" class="kg-image" alt="[Part 3] How to set up your own Ghost + Gatsby blog (with Digital Ocean and Netlify)" loading="lazy"/></figure><p>Scroll down to <strong>Build hooks </strong>section and click on <strong>Add build hook.</strong></p><figure class="kg-card kg-image-card"><img src="https://ghost.woodies11.dev/content/images/2020/03/A7C83F81-8F86-499C-A532-BC7060E22A42.png" class="kg-image" alt="[Part 3] How to set up your own Ghost + Gatsby blog (with Digital Ocean and Netlify)" loading="lazy"/></figure><p>Name it whatever you want, then click <strong>Save</strong>.</p><figure class="kg-card kg-image-card"><img src="https://ghost.woodies11.dev/content/images/2020/03/26974D73-3725-49AF-A36B-0626BCBC2F9C.png" class="kg-image" alt="[Part 3] How to set up your own Ghost + Gatsby blog (with Digital Ocean and Netlify)" loading="lazy"/></figure><p>This will give you a URL. Select and <strong>copy </strong>it.</p><figure class="kg-card kg-image-card"><img src="https://ghost.woodies11.dev/content/images/2020/03/0905B61C-C44B-4D7D-B118-59CEC2638788.png" class="kg-image" alt="[Part 3] How to set up your own Ghost + Gatsby blog (with Digital Ocean and Netlify)" loading="lazy"/></figure><p>Now, go back to your Ghost admin page and click on the <strong>Integrations </strong>you set up for Netlify. Then, click on <strong>+ Add webhook.</strong></p><figure class="kg-card kg-image-card"><img src="https://ghost.woodies11.dev/content/images/2020/03/E9CB86D0-AB43-42DB-B7C6-E12817A12916.png" class="kg-image" alt="[Part 3] How to set up your own Ghost + Gatsby blog (with Digital Ocean and Netlify)" loading="lazy"/></figure><p>Name it however you like.</p><p>Set <strong>Event </strong>to <code>Site changed (rebuild)</code>. As the name suggest, this event is triggered everything something change on our Ghost site (create, modify, publish, unpublish, etc.).</p><p>Finally, set <strong>Target URL </strong>to be the URL you obtained from Netlify Build hook in the previous step.</p><p>Now, your Ghost CMS will send a <code>POST</code> request to the target URL each time something changed on our site. This <code>POST</code> request will then trigger a rebuild on Netlify, allowing it to pull new contents and update our site automatically!</p><figure class="kg-card kg-image-card"><img src="https://ghost.woodies11.dev/content/images/2020/03/BEAE89CC-A8CC-401B-BDA3-02F60635D362.png" class="kg-image" alt="[Part 3] How to set up your own Ghost + Gatsby blog (with Digital Ocean and Netlify)" loading="lazy"/></figure><p>Note that, this hook only handle the changes made to content on Ghost. It does not handle changes to the gatsby source code itself. Luckily, Netlify already handle that part of the CI/CD for you. Each time you push a new commit to <code>master</code>, Netlify will also pull the new code and trigger a rebuild automatically. Pretty code isn't it!</p><p>Lastly, we will look at how to get your own domain name and set that up in Netlify!</p>]]></content:encoded></item><item><title><![CDATA[What to write in your blog?]]></title><description><![CDATA[What should you write in your blog? Who are you to even write anything?]]></description><link>https://ghost.woodies11.dev/what-to-write-in-your-blog/</link><guid isPermaLink="false">Ghost__Post__5e67bdad4e433422bb91b411</guid><category><![CDATA[thoughts]]></category><category><![CDATA[productivity]]></category><dc:creator><![CDATA[Romson Preechawit]]></dc:creator><pubDate>Tue, 10 Mar 2020 16:21:20 GMT</pubDate><media:content url="https://images.unsplash.com/photo-1483546363825-7ebf25fb7513?ixlib=rb-1.2.1&amp;q=80&amp;fm=jpg&amp;crop=entropy&amp;cs=tinysrgb&amp;w=2000&amp;fit=max&amp;ixid=eyJhcHBfaWQiOjExNzczfQ" medium="image"/><content:encoded><![CDATA[<img src="https://images.unsplash.com/photo-1483546363825-7ebf25fb7513?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=2000&fit=max&ixid=eyJhcHBfaWQiOjExNzczfQ" alt="What to write in your blog?"/><p>I used to think “who am I to write stuffs”? I am not <em>that</em> good at anything. I know programming, but really only the basics compared to others legends out there. I mean, all I can really do is Google a bunch of how-to’s and put stuffs together. Anyone can do that right? Right???</p><p>Then, I came across this quote from somewhere...</p><blockquote>What is obvious to you may not be obvious to others.</blockquote><p>I am sure this mean something else in its original context. But I will apply it here anyway.</p><p>You see, the things you know may feel like very basic, very easy things for you, but that is <strong>because</strong> you <em><strong>already know it</strong></em>.</p><h1 id="know-one-really-know-everything">Know one really know everything</h1><p>I still need to Google and read a brunch of tutorials and documents to put this blog together. But I am able to do so with ease because I know <em>basic</em> networking, <em>basic</em> Linux commands, <em>basic</em> macOS commands, <em>basic</em> web development, and <em>basic</em> design theory. These are all <em>basic</em> knowledge. Yet, they are <em>basic</em> of many broad topics and not many know all these <em>basics</em> all in one person.</p><h1 id="additional-value-are-created-even-by-simply-combining-existing-things-together">Additional value are created, even by simply combining existing things together</h1><p>What would you do if I were to ask you; go build a car from scratch. You cannot use any existing parts. You need to invent everything on your own.</p><p>Would you be able to do it? Maybe you will. But that will at least take a very long time.</p><p>Our entire infrastructures are all build by combining existing technology and concepts together to create better things. So, don’t feel bad if you can only put this and that from someone else post together (as long as you are not straight up copy or <em>plagiarize</em> of course). After all, everyone does that.</p><h1 id="just-write-">Just write...</h1><p>As with anything, it takes time to get good. You may not know your style at first. In fact, I am just writing a bunch of posts right now. Some of which are not even remotely related. Eventually though, you will find what you really enjoy writing. Eventually, the blog will build and play itself out. So, just take the first step and write! Create something great!</p><p>If you are interested in making building your own blog, I made a tutorial on how to launch your own blog right <a href="https://blog.woodies11.dev/how-to-setup-ghost-netlify-part-1/">here</a>. Feel free to check that out. <a href="https://medium.com/">Medium</a> is also a good platform if you just want to write and start writing right away. Either way, have fun and enjoy!</p>]]></content:encoded></item><item><title><![CDATA[Testing out iA Writer iOS and Ghost workflow]]></title><description><![CDATA[Just a simple post to test out the integration between iA Writer, a minimal markdown editor on iOS and macOS, with Ghost CMS.]]></description><link>https://ghost.woodies11.dev/testing-out-ia-writer-ios-and-ghost-workflow/</link><guid isPermaLink="false">Ghost__Post__5e67186c4e433422bb91b3cf</guid><category><![CDATA[reviews]]></category><category><![CDATA[productivity]]></category><category><![CDATA[iOS]]></category><category><![CDATA[software]]></category><category><![CDATA[apps]]></category><dc:creator><![CDATA[Romson Preechawit]]></dc:creator><pubDate>Tue, 10 Mar 2020 04:39:44 GMT</pubDate><media:content url="https://images.unsplash.com/photo-1508780709619-79562169bc64?ixlib=rb-1.2.1&amp;q=80&amp;fm=jpg&amp;crop=entropy&amp;cs=tinysrgb&amp;w=2000&amp;fit=max&amp;ixid=eyJhcHBfaWQiOjExNzczfQ" medium="image"/><content:encoded><![CDATA[<img src="https://images.unsplash.com/photo-1508780709619-79562169bc64?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=2000&fit=max&ixid=eyJhcHBfaWQiOjExNzczfQ" alt="Testing out iA Writer iOS and Ghost workflow"/><p>This is just a simple test to see how punishing to Ghost directly from <a href="https://apps.apple.com/th/app/ia-writer/id775737172">iA Writer </a>on iPad would work.</p><p>I wanted to use <a href="https://apps.apple.com/th/app/ulysses/id1225571038">Ulysses</a>, but hate the subscription model. I don’t feel like I will write enough to justify spending a few bucks a month on a writing tool (I only write as a hobby after all).</p><p>All you need to do is set up Ghost Integration and add your Admin API ky to iA Writer. Then, you can just hit Publish after you finish writing.</p><figure class="kg-card kg-image-card"><img src="https://ghost.woodies11.dev/content/images/2020/03/36AD729F-4D2F-42FD-9B5C-F4C7FFA7F539.png" class="kg-image" alt="Testing out iA Writer iOS and Ghost workflow" loading="lazy"/></figure><p>This will take you to Ghost's editor page with your content! You can then edit your post and/or publish it to your blog.</p><figure class="kg-card kg-image-card"><img src="https://ghost.woodies11.dev/content/images/2020/03/88D8EB4F-2DAB-46DA-A5A3-2BB2019F6E4E.png" class="kg-image" alt="Testing out iA Writer iOS and Ghost workflow" loading="lazy"/></figure><blockquote>Note that this is only a <strong>one-way and one-time </strong>upload. Changes you make in the Ghost editor will not sync back to iA Writer. Any changes you made in the writer after you update will also not be sync to Ghost.</blockquote><h1 id="a-bit-more-about-ia-writer">A bit more about iA Writer</h1><p>I first used iA Writer on macOS since it was originally released on the Mac back in 2011. It was the app that introduced me to Markdown. In fact, I was learning and using the Markdown syntax for a long while without even knowing what it is.</p><p>Back then, iA Writer was one of the first, if not the first, writing app to introduce “focus mode”. In this mode, the app highlight, by default, only the sentence the caret (the little I indicating where you are typing on) is on. This greatly save my English-as-a-second language self from writing overly long, stupidly complex, and completely unnecessary sentences that many of us, non-native users of English, love to do times and times again. We just never learn to start a new sentence.</p><p>Seeing each sentence being highlighted as you type make it very clear if a sentence is getting too long.</p><p>The blur effect applied to unfocused text in earlier version of the app is also a complete joy to look at. Something that made my never ending amount of essay assignments much more bearable.</p>]]></content:encoded></item><item><title><![CDATA[Drawing Pour Over Coffee Set Illustration using Sketch for Mac | Time-lapse Art]]></title><description><![CDATA[Drawing pour over coffee set in Sketch | Timelapse]]></description><link>https://ghost.woodies11.dev/drawing-pour-over-coffee-set-illustration-using-sketch-for-mac-time-lapse-art/</link><guid isPermaLink="false">Ghost__Post__5e66fb624e433422bb91b3b2</guid><category><![CDATA[arts]]></category><category><![CDATA[design]]></category><category><![CDATA[video]]></category><dc:creator><![CDATA[Romson Preechawit]]></dc:creator><pubDate>Tue, 10 Mar 2020 02:30:56 GMT</pubDate><media:content url="https://ghost.woodies11.dev/content/images/2020/03/C4F718F9-8816-4392-BEAB-C64AFAAED698.jpeg" medium="image"/><content:encoded><![CDATA[<figure class="kg-card kg-embed-card"><iframe width="480" height="270" src="https://www.youtube.com/embed/1ckBerLailw?feature=oembed" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen=""/></figure>]]></content:encoded></item><item><title><![CDATA[[Part 2] How to set up your own Ghost + Gatsby blog (with Digital Ocean and Netlify)]]></title><description><![CDATA[In this part, we will be looking at how to connect Gatsby to our Ghost CMS.]]></description><link>https://ghost.woodies11.dev/how-to-setup-ghost-netlify-part-2/</link><guid isPermaLink="false">Ghost__Post__5e6647f34e433422bb91b2cc</guid><category><![CDATA[dev]]></category><category><![CDATA[tutorials]]></category><category><![CDATA[web]]></category><dc:creator><![CDATA[Romson Preechawit]]></dc:creator><pubDate>Mon, 09 Mar 2020 16:40:03 GMT</pubDate><media:content url="https://ghost.woodies11.dev/content/images/2020/03/Screen-Shot-2020-03-09-at-23.37.21-1.png" medium="image"/><content:encoded><![CDATA[<img src="https://ghost.woodies11.dev/content/images/2020/03/Screen-Shot-2020-03-09-at-23.37.21-1.png" alt="[Part 2] How to set up your own Ghost + Gatsby blog (with Digital Ocean and Netlify)"/><p>In this part, we will be looking at how to connect Gatsby to our Ghost CMS.</p><p>If you haven't completed <a href="https://blog.woodies11.dev/how-to-setup-ghost-netlify-part-1">Part 1</a> yet, we look at how to host our own Ghost CMS in a Digital Ocean droplet there.</p><h1 id="ghost-s-gatsby-starter-project">Ghost's Gatsby Starter Project</h1><p>Ghost provides a convenient starter Gatsby project for us to used. You can visit the official <a href="https://github.com/TryGhost/gatsby-starter-ghost">GitHub</a> page or clone the repo using this command:</p><pre><code class="language-sh">git clone https://github.com/TryGhost/gatsby-starter-ghost.git</code></pre><p>We will need to do a few configuration here to get our Gatsby connected to our Ghost in Digital Ocean.</p><h2 id="add-ghost-json-to-gitignore">Add .ghost.json to .gitignore</h2><figure class="kg-card kg-image-card"><img src="https://ghost.woodies11.dev/content/images/2020/03/Screen-Shot-2020-03-09-at-23.28.01.png" class="kg-image" alt="[Part 2] How to set up your own Ghost + Gatsby blog (with Digital Ocean and Netlify)" loading="lazy"/></figure><p>First, we will add <code>.ghost.json</code> file to <code>.gitignore</code> so git will ignore this file from commits (make sure hidden files are visible). The file will be where we store our API keys, and it is generally a bad practice to commit these keys to a repo. Instead, we will use <strong>ENVIRONMENTAL VARIABLES </strong>to store our keys.</p><p>You can either open <code>.gitignore</code> file in a text editor and add the entry that way or run this command:</p><pre><code class="language-sh">echo "\n.ghost.json" &gt;&gt; .gitignore</code></pre><p>Optionally, you can create a copy of the file to keep as template: (<code>copy</code> for Windows)</p><pre><code class="language-sh">cp .ghost.json .ghost.json.template</code></pre><p>The default configuration that come with the starter project is pointing to a Ghost instance created specifically for starter projects. Thus, you can leave it as is for reference. Otherwise, I would remove the values and only leave keys for the template.</p><p><strong>Leave the original </strong><code>.ghost.json</code><strong> as is if you want to test or develop Gatsby locally before hosting it on Netlify.</strong></p><h2 id="node-js">Node JS</h2><p>If you want to run Gatsby on your local machine, either to test or develop locally, make sure you have Node JS and <code>npm</code> or <code>yarn</code> installed first.</p><h2 id="install-dependencies">Install dependencies</h2><p>Make sure you are in the project's root directory. Then run:</p><pre><code class="language-sh">npm install</code></pre><p>Once that is done, run:</p><pre><code class="language-sh">npm run dev</code></pre><p>This will pull content from <a href="https://gatsby.ghost.io">gatsby.ghost.io</a> which is an instance of Ghost hosted for the purpose of this demo project.</p><figure class="kg-card kg-image-card"><img src="https://ghost.woodies11.dev/content/images/2020/03/Screen-Shot-2020-03-09-at-23.15.15.png" class="kg-image" alt="[Part 2] How to set up your own Ghost + Gatsby blog (with Digital Ocean and Netlify)" loading="lazy"/></figure><p>Once completed, you can go to <a href="http://localhost:8000">http://localhost:8000</a> to see your Gatsby project.</p><figure class="kg-card kg-image-card"><img src="https://ghost.woodies11.dev/content/images/2020/03/Screen-Shot-2020-03-09-at-23.16.12.png" class="kg-image" alt="[Part 2] How to set up your own Ghost + Gatsby blog (with Digital Ocean and Netlify)" loading="lazy"/></figure><p>Now, let's configure this to pull from the Ghost instance that you set up in <a href="https://ghost.woodies11.dev/how-to-setup-ghost-netlify-part-1">Part 1</a>.</p><p>First, login to the Ghost admin page of your Ghost instance.</p><p>Then, click on <strong>Integrations &gt; + Add custom integration</strong></p><figure class="kg-card kg-image-card"><img src="https://ghost.woodies11.dev/content/images/2020/03/Screen-Shot-2020-03-09-at-23.19.31.png" class="kg-image" alt="[Part 2] How to set up your own Ghost + Gatsby blog (with Digital Ocean and Netlify)" loading="lazy"/></figure><p>Name it however you like. I will use <strong>Local Dev </strong>here because I will only use this key for local development.</p><figure class="kg-card kg-image-card"><img src="https://ghost.woodies11.dev/content/images/2020/03/Screen-Shot-2020-03-09-at-23.22.01.png" class="kg-image" alt="[Part 2] How to set up your own Ghost + Gatsby blog (with Digital Ocean and Netlify)" loading="lazy"/></figure><p>Take note of your <strong>Content API Key </strong>and <strong>API URL.</strong></p><figure class="kg-card kg-image-card"><img src="https://ghost.woodies11.dev/content/images/2020/03/Screen-Shot-2020-03-09-at-23.35.21.png" class="kg-image" alt="[Part 2] How to set up your own Ghost + Gatsby blog (with Digital Ocean and Netlify)" loading="lazy"/></figure><p>In your project folder, open up the <code>.ghost.json</code> file (again, make sure hidden files are shown or use command line to open it).</p><p>You should see something like:</p><figure class="kg-card kg-image-card"><img src="https://ghost.woodies11.dev/content/images/2020/03/Screen-Shot-2020-03-09-at-23.28.46.png" class="kg-image" alt="[Part 2] How to set up your own Ghost + Gatsby blog (with Digital Ocean and Netlify)" loading="lazy"/></figure><p>As you can guess, you want to replace both <code>apiUrl</code> and <code>contentApiKey</code> with the values you get from your Ghost's admin page.</p><p>Do that then save the file.</p><p>Finally, run the command below again to see the final result.</p><pre><code class="language-sh">npm run dev</code></pre><p> It should now pull from your own Ghost instance.</p><figure class="kg-card kg-image-card"><img src="https://ghost.woodies11.dev/content/images/2020/03/Screen-Shot-2020-03-09-at-23.37.21.png" class="kg-image" alt="[Part 2] How to set up your own Ghost + Gatsby blog (with Digital Ocean and Netlify)" loading="lazy"/></figure><p>Congratulation! You have successfully integrated Gatsby with Ghost!</p><h1 id="next-hosting-">Next, hosting...</h1><p>In <a href="https://blog.woodies11.dev/how-to-setup-ghost-netlify-part-3">Part 3</a>, we will look at how we could host this site on Netlify and create a webhook to tell Netlify CI/CD to rebuild our site each time content is updated on Ghost.</p>]]></content:encoded></item><item><title><![CDATA[What’s on my Home Network]]></title><description><![CDATA[I have been building and expanding the capability of my home network for the past few months. Here is what I have accumulated so far!]]></description><link>https://ghost.woodies11.dev/whats-on-my-home-network/</link><guid isPermaLink="false">Ghost__Post__5e64f2ff4e433422bb91b169</guid><category><![CDATA[dev]]></category><category><![CDATA[network]]></category><dc:creator><![CDATA[Romson Preechawit]]></dc:creator><pubDate>Sun, 08 Mar 2020 14:58:50 GMT</pubDate><media:content url="https://ghost.woodies11.dev/content/images/2020/03/DSC01443.jpeg" medium="image"/><content:encoded><![CDATA[<img src="https://ghost.woodies11.dev/content/images/2020/03/DSC01443.jpeg" alt="What’s on my Home Network"/><p/><h3 id="1-modem">1. Modem</h3><p>I reuse an all-in-one D-Link Modem/Router to replace my ISP's Modem. Nothing fancy. It just get the job done.</p><h3 id="2-tp-link-ac1200-router">2. <a href="https://www.amazon.com/TP-Link-AC1200-Smart-WiFi-Router/dp/B01IUDUJE0/ref=sr_1_2?keywords=tp+link+c1200&amp;qid=1583674561&amp;s=electronics&amp;sr=1-2">TP-Link AC1200 Router</a></h3><figure class="kg-card kg-image-card"><img src="https://ghost.woodies11.dev/content/images/2020/03/IMG_7833.jpg" class="kg-image" alt="What’s on my Home Network" loading="lazy"/></figure><p>A budget wireless AC router that perform really well for the price. I will likely upgrade to a better Wi-Fi 6 or Wi-Fi AX router once those become more prominent. Otherwise, this router does the job for now.</p><h3 id="3-tp-link-8-ports-gigabit-switch">3. <a href="https://www.amazon.com/Ethernet-Splitter-Optimization-Unmanaged-TL-SG108/dp/B00A121WN6/ref=sr_1_3?crid=F8HVVT9O3F4Q&amp;keywords=tp+link+8+port+gigabit+switch&amp;qid=1583674492&amp;s=electronics&amp;sprefix=tp+link+8+port+gi%2Celectronics%2C405&amp;sr=1-3">TP-Link 8-Ports Gigabit Switch</a></h3><figure class="kg-card kg-image-card"><img src="https://ghost.woodies11.dev/content/images/2020/03/DSC01444.jpeg" class="kg-image" alt="What’s on my Home Network" loading="lazy"/></figure><p>I never thought I would need to have a switch at home in 2020. After all, practically anything is now wireless enabled.</p><p>That is, until I got my next item.</p><h3 id="4-synology-ds218-nas">4. <a href="https://www.amazon.com/Synology-bay-DiskStation-DS218-Diskless/dp/B077PJX8TH">Synology DS218 NAS</a></h3><figure class="kg-card kg-image-card"><img src="https://ghost.woodies11.dev/content/images/2020/03/DSC01392-3.jpeg" class="kg-image" alt="What’s on my Home Network" loading="lazy"/></figure><p>Apple Time Machine has saves me from a brunch of headaches more time than I can count. For the part few years, I have been backing up to my WD My Book 2TB through USB. It was a very simple process. Everyday when I am back from college, I would dock my laptop at my dock and plug the HDD in. TimeMachine would continue to back up to the drive automatically.</p><p>I have since moved to a new condominium and accomulate a few more devices. I also graduated and now working full time. It is sometime hard to remember to plug the hard drive in when I have multiple laptops that I switch around to all the time.</p><p>I also want to share files quickly between my Gaming PC and my MacBook Pro.</p><p>Plus, I just love the idea of a NAS. I have always wanted to play around with these networking stuffs.</p><p>So, I decided that it is time to invest in something more powerful. I wanted to start out with something simple. I tried repurpose an old laptop to run FreeNAS before but honestly, with something as important as data storage device, I rather have something that "just work" than saving a few bucks and having to spend time troubleshoot later when something go wrong. Thus, I decided to go with a basic Synology D218 2-bays NAS with a 6 TB Seagate IronWolf drive (I do plan to add a second later for RAID 1). My local store did not have the D218+ model, so the standard one is what I ended up getting. I will probably make the jump later on to a 4 or 5 bays one so this will do for now.</p><h3 id="5-raspberry-pi-3b-newer-model-available-with-pi-hole-running-on-raspbian-lite">5. <a href="https://www.amazon.com/Raspberry-Model-2019-Quad-Bluetooth/dp/B07TD42S27/ref=sxin_2_ac_d_pm?ac_md=4-0-VW5kZXIgJDUw-ac_d_pm&amp;cv_ct_cx=raspberry+pi+4&amp;keywords=raspberry+pi+4&amp;pd_rd_i=B07TD42S27&amp;pd_rd_r=2d71be08-6d83-4ac1-b3e6-35ee0a9fc23c&amp;pd_rd_w=zsPEo&amp;pd_rd_wg=ipBi6&amp;pf_rd_p=0e223c60-bcf8-4663-98f3-da892fbd4372&amp;pf_rd_r=7B8ZMNWH4Y0TVRTRQ1PA&amp;psc=1&amp;qid=1583674606">Raspberry Pi 3b+</a> (newer model available) with Pi Hole running on Raspbian Lite</h3><figure class="kg-card kg-image-card"><img src="https://ghost.woodies11.dev/content/images/2020/03/Screen-Shot-2020-03-05-at-11.23.08.png" class="kg-image" alt="What’s on my Home Network" loading="lazy"/></figure><p>This is a recent addition to my network. I stumbled across the Pi-Hole project a few weeks ago and got hooked with the idea of having a dashboard that I can monitor my devices network activities (for absolutely no reason apart from because I can).<br/></p><p>It also have the added benefit of being a DNS sink hole, allowing me to block ads and websites at a network level.</p><h3 id="6-hue-bridge-for-philips-hue">6. <a href="https://www.amazon.com/Philips-Hue-Starter-HomeKit-Assistant/dp/B07XH4KDR5/ref=sr_1_1_sspa?crid=2467A8KQWNWCJ&amp;keywords=philips+hue+starter+kit&amp;qid=1583674651&amp;sprefix=Philip+hue+star%2Caps%2C402&amp;sr=8-1-spons&amp;psc=1&amp;spLa=ZW5jcnlwdGVkUXVhbGlmaWVyPUEyREZRMzRZMVlGM1VLJmVuY3J5cHRlZElkPUEwOTIyNzI1MjFVTVNZRTNaVk9MRSZlbmNyeXB0ZWRBZElkPUEwNjE2NDYxTVlaOE5INVJKS09YJndpZGdldE5hbWU9c3BfYXRmJmFjdGlvbj1jbGlja1JlZGlyZWN0JmRvTm90TG9nQ2xpY2s9dHJ1ZQ==">Hue Bridge for Philips Hue</a></h3><p>The Philips Hue zigbee bridge is required to control Philips Hue lights around my condo. Nothing fancy here.</p><h3 id="7-other-wired-wireless-clients">7. Other wired/wireless clients</h3><p>You know... my smartphones, iPad, laptops, PC, Smart TV, Chromecast, etc.</p><h1 id="the-cupboard-">The Cupboard:</h1><figure class="kg-card kg-image-card"><img src="https://ghost.woodies11.dev/content/images/2020/03/IMG_7755.jpeg" class="kg-image" alt="What’s on my Home Network" loading="lazy"/></figure><p>Originally, these devices just sit next to my router. My router is right next to my desk, and in the middle of my living room though. Those HDD sounds from the NAS eventually drive me crazy so I moved all these devices into an empty cupboard away from my desk. </p><p>I will probably get myself a rack someday. For now, they all live here.</p>]]></content:encoded></item><item><title><![CDATA[[Part 1] How to set up your own Ghost + Gatsby blog (with Digital Ocean and Netlify)]]></title><description><![CDATA[How to set up your own fully automated static blog using Ghost and Gatsby with the JAMstack!]]></description><link>https://ghost.woodies11.dev/how-to-setup-ghost-netlify-part-1/</link><guid isPermaLink="false">Ghost__Post__5e64b7d74e433422bb91b12a</guid><category><![CDATA[tutorials]]></category><category><![CDATA[dev]]></category><category><![CDATA[web]]></category><dc:creator><![CDATA[Romson Preechawit]]></dc:creator><pubDate>Sun, 08 Mar 2020 09:16:48 GMT</pubDate><media:content url="https://ghost.woodies11.dev/content/images/2020/03/Screen-Shot-2020-03-08-at-16.25.58.png" medium="image"/><content:encoded><![CDATA[<h1 id="ghost-on-digital-ocean-droplet-jamstack-">Ghost on Digital Ocean Droplet (JAMstack)</h1><img src="https://ghost.woodies11.dev/content/images/2020/03/Screen-Shot-2020-03-08-at-16.25.58.png" alt="[Part 1] How to set up your own Ghost + Gatsby blog (with Digital Ocean and Netlify)"/><p>For now, I am hosting the site on the smallest, and cheapest, Digital Ocean droplet.</p><p>First, you will want to <a href="https://m.do.co/c/a24e0451905d">create a Digital Ocean account</a> (disclosure: referral link).</p><figure class="kg-card kg-image-card"><img src="https://ghost.woodies11.dev/content/images/2020/03/Screen-Shot-2020-03-08-at-15.43.15.png" class="kg-image" alt="[Part 1] How to set up your own Ghost + Gatsby blog (with Digital Ocean and Netlify)" loading="lazy"/></figure><p>Then, select <strong>Create &gt; Droplets</strong>.</p><figure class="kg-card kg-image-card"><img src="https://ghost.woodies11.dev/content/images/2020/03/Screen-Shot-2020-03-08-at-15.40.14.png" class="kg-image" alt="[Part 1] How to set up your own Ghost + Gatsby blog (with Digital Ocean and Netlify)" loading="lazy"/></figure><p>We will be using a template already provided by Digital Ocean, so click on <strong>Marketplace </strong>then <strong>See all Marketplace Apps </strong>to search all apps.</p><figure class="kg-card kg-image-card"><img src="https://ghost.woodies11.dev/content/images/2020/03/Screen-Shot-2020-03-08-at-15.45.00.png" class="kg-image" alt="[Part 1] How to set up your own Ghost + Gatsby blog (with Digital Ocean and Netlify)" loading="lazy"/></figure><p>Search and select <strong>Ghost</strong>.</p><figure class="kg-card kg-image-card"><img src="https://ghost.woodies11.dev/content/images/2020/03/Screen-Shot-2020-03-08-at-15.45.23.png" class="kg-image" alt="[Part 1] How to set up your own Ghost + Gatsby blog (with Digital Ocean and Netlify)" loading="lazy"/></figure><p>Click on <strong>Create Ghost Droplet</strong>.</p><figure class="kg-card kg-image-card"><img src="https://ghost.woodies11.dev/content/images/2020/03/Screen-Shot-2020-03-08-at-15.45.29.png" class="kg-image" alt="[Part 1] How to set up your own Ghost + Gatsby blog (with Digital Ocean and Netlify)" loading="lazy"/></figure><p>Choose the configuration you want. Since we will be using a static site generator to generate and host the site elsewhere, the cheapest plan should work. </p><p>This is the beauty of this setup! The CMS, and thus the droplet, is only access during rebuild while another hosting server or a CDN take care of actual user load. Therefore, the CMS server can be really small!</p><figure class="kg-card kg-image-card"><img src="https://ghost.woodies11.dev/content/images/2020/03/Screen-Shot-2020-03-08-at-15.45.48.png" class="kg-image" alt="[Part 1] How to set up your own Ghost + Gatsby blog (with Digital Ocean and Netlify)" loading="lazy"/></figure><p>Pick your datacenter location. Again, since our web hosting will take care of the actual task of serving the site, this doesn't matter much. I live in Thailand so I picked Singapore. (Though, I should have picked one closer to where my CI/CD build pipeline will take place...).</p><figure class="kg-card kg-image-card"><img src="https://ghost.woodies11.dev/content/images/2020/03/Screen-Shot-2020-03-08-at-15.45.55.png" class="kg-image" alt="[Part 1] How to set up your own Ghost + Gatsby blog (with Digital Ocean and Netlify)" loading="lazy"/></figure><p>Pick your authentication method. I recommend using SSH keys if possible. If you have never add an SSH key to your Digital Ocean account, click on <strong>New SSH Key </strong>and follow the instruction to add one.</p><figure class="kg-card kg-image-card"><img src="https://ghost.woodies11.dev/content/images/2020/03/Screen-Shot-2020-03-08-at-15.46.01.png" class="kg-image" alt="[Part 1] How to set up your own Ghost + Gatsby blog (with Digital Ocean and Netlify)" loading="lazy"/></figure><p>Finally, click <strong>Create Droplet </strong>and wait a bit while Digital Ocean is setting up your droplet for the first time.</p><figure class="kg-card kg-image-card"><img src="https://ghost.woodies11.dev/content/images/2020/03/Screen-Shot-2020-03-08-at-15.46.19.png" class="kg-image" alt="[Part 1] How to set up your own Ghost + Gatsby blog (with Digital Ocean and Netlify)" loading="lazy"/></figure><p>Once your droplet is ready, you will get a <strong>Public IP Address</strong> that you can use to <strong>SSH </strong>into the droplet.</p><figure class="kg-card kg-image-card"><img src="https://ghost.woodies11.dev/content/images/2020/03/Screen-Shot-2020-03-08-at-16.01.38.png" class="kg-image" alt="[Part 1] How to set up your own Ghost + Gatsby blog (with Digital Ocean and Netlify)" loading="lazy"/></figure><h2 id="configuring-ghost-for-the-first-time">Configuring Ghost for the First Time</h2><p>You will need to SSH into your droplet once to finish installation.</p><p>On Windows, you can use <a href="https://www.putty.org">PuTTY</a> to establish an ssh connection.</p><p>On a UNIX system (macOS, Linux), <code>ssh</code> should come standard with your shell.</p><h3 id="ssh-using-password">SSH using password</h3><p>If you selected <strong>One-time password </strong>as your <strong>authentication method </strong>previously, you can use:</p><pre><code class="language-sh">ssh root@123.123.123.123</code></pre><p>Where <code>123.123.123.123</code> is replaced with your <strong>Public IP Address.</strong></p><p>If prompt to trust the certificate, type <code>yes</code> and hit enter.</p><p>It will then ask for password sent to your email. Enter that in.</p><h3 id="ssh-using-ssh-key">SSH using SSH Key</h3><p>If you selected <strong>SSH Keys </strong>as your <strong>authentication method, </strong>use the following command instead:</p><pre><code class="language-sh">ssh -i /path/to/my/key root@123.123.123.123</code></pre><p>Where <code>/path/to/my/key</code> is the path to your <em>private key</em> associated with the <em>public key </em>you configured for this droplet.</p><h3 id="auto-installation">Auto Installation</h3><p>Once connected, the shell should automatically finish the installation for you.</p><p>You will be asked to supply a <strong>domain </strong>name. Do so if you already have one, otherwise, enter <code>http://&lt;YOUR DROPLET PUBLIC IP&gt;</code>. </p><p>Once that is complete, visit <code>http://&lt;YOUR DROPLET PUBLIC IP&gt;/ghost</code>, or <code>https://&lt;YOUR DOMAIN NAME&gt;/ghost</code> if you have a domain, to set up Admin for Ghost for the first time.</p><figure class="kg-card kg-image-card"><img src="https://ghost.woodies11.dev/content/images/2020/03/Screen-Shot-2020-03-09-at-23.34.35.png" class="kg-image" alt="[Part 1] How to set up your own Ghost + Gatsby blog (with Digital Ocean and Netlify)" loading="lazy"/></figure><figure class="kg-card kg-image-card"><img src="https://ghost.woodies11.dev/content/images/2020/03/Screen-Shot-2020-03-09-at-23.34.40-1.png" class="kg-image" alt="[Part 1] How to set up your own Ghost + Gatsby blog (with Digital Ocean and Netlify)" loading="lazy"/></figure><figure class="kg-card kg-image-card"><img src="https://ghost.woodies11.dev/content/images/2020/03/Screen-Shot-2020-03-09-at-23.34.45.png" class="kg-image" alt="[Part 1] How to set up your own Ghost + Gatsby blog (with Digital Ocean and Netlify)" loading="lazy"/></figure><figure class="kg-card kg-image-card"><img src="https://ghost.woodies11.dev/content/images/2020/03/Screen-Shot-2020-03-09-at-23.42.15.png" class="kg-image" alt="[Part 1] How to set up your own Ghost + Gatsby blog (with Digital Ocean and Netlify)" loading="lazy"/></figure><h3 id="updating-ghost-s-base-url-after-the-fact-">Updating Ghost's base URL after the fact...</h3><p>For those who are impatience (like me) and could not wait until you get your domain name set up and A name to propagete, you can update Ghost's URL after the first installation by following <a href="https://ghost.org/faq/change-configured-site-url/">this guide</a>.</p><p>Note that, the default installation for Ghost in Digital Ocean's droplet is at <code>/var/www/ghost</code>. You will also need to switch to <code>ghost-mgr</code> user using <code>sudo -i -u ghost-mgr</code>.</p><h1 id="set-up-gatsby">Set up Gatsby</h1><p>To be continue in <a href="https://blog.woodies11.dev/how-to-setup-ghost-netlify-part-2">Part 2</a>...</p>]]></content:encoded></item><item><title><![CDATA[First Commit!]]></title><description><![CDATA[New blog, resurrected!

After loooooooong procrastination, I finally got the time to resurrect my Ghost blog.]]></description><link>https://ghost.woodies11.dev/my-first-blog-post/</link><guid isPermaLink="false">Ghost__Post__5e64863792369c3aae7627b8</guid><category><![CDATA[dev]]></category><dc:creator><![CDATA[Romson Preechawit]]></dc:creator><pubDate>Sun, 08 Mar 2020 05:51:37 GMT</pubDate><media:content url="https://images.unsplash.com/photo-1489533119213-66a5cd877091?ixlib=rb-1.2.1&amp;q=80&amp;fm=jpg&amp;crop=entropy&amp;cs=tinysrgb&amp;w=2000&amp;fit=max&amp;ixid=eyJhcHBfaWQiOjExNzczfQ" medium="image"/><content:encoded><![CDATA[<img src="https://images.unsplash.com/photo-1489533119213-66a5cd877091?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=2000&fit=max&ixid=eyJhcHBfaWQiOjExNzczfQ" alt="First Commit!"/><p/><figure class="kg-card kg-image-card"><img src="https://ghost.woodies11.dev/content/images/2020/03/Screen-Shot-2020-03-08-at-12.49.42.png" class="kg-image" alt="First Commit!" loading="lazy"/></figure><p>After loooooooong procrastination, I finally got the time to resurrect my Ghost blog. A couple years ago, I rent a shared Web Hosting server to host my portfolio while looking for a job (actually, the real intent was to host a website just for fun...).</p><p>I also got a free domain name from <a href="http://namecheap.com/">NameCheap</a> as a student. That free domain came with free a year of free <a href="https://ghost.org">Ghost</a> instance! That's the first time I got to know of <a href="https://ghost.org">Ghost</a>, and I instantly fall in love! Having used Wordpress before, Ghost is just <em>elegant. </em>It reminded me a lot of journaling in <a href="https://dayoneapp.com">Day One</a> (which I still try to do everyday).</p><h1 id="resurrected-jamstack">Resurrected - JAMstack</h1><p>I didn't really know what I was doing back then (and still don't know much now lol).</p><p>Ghost was already all setup. So, I was only using what's there. Otherwise, I my perfectionist would kick in and I would try to find <strong>the best way to host a blog</strong> and never actually get started.</p><p>As it turned out though, I <em>hate</em><strong> </strong>the idea of shared web hosting. I feel like those shared plan really restricted what I can do. Not being able to take control of my backend feel bad!</p><p>Anyway, so I ended up trying something different this time.</p><p>Upon doing a bit of research, I stumbled upon the <a href="https://jamstack.org">JAMstack</a> (<strong>J</strong>avaScript, <strong>A</strong>PI, <strong>M</strong>arkup). Basically, it leverage CI/CD process to continuously build a <strong>static-site </strong>with data source from a headless <strong>CMS</strong>, and serve them, usually through <strong>CDN</strong>.</p><p>The beauty of this setup is that you get the best of both world!</p><h2 id="static-sites">Static-sites</h2><p>Static-sites are faster, more scalable, and more secure than most dynamic websites. Blogging is one of the best use case for static-sites. After all, you wouldn't normally serve a version of a blog page to one user, then another slightly customized version to another. </p><p>Since all user will eventuall get the same version of the page, why not just render that page and store it somewhere instead of pulling from database and rerender it every time?</p><p>Nevertheless, static-sites used to be hard to manage and maintain. Static-site generators like Jekyll, and, in this case, Gatsby, helped leviate a lot of those burden of the old-days. But they still require quite a bit of coding knowledge to use.</p><h2 id="cms">CMS</h2><p>A CMS provide ways to create, update, and maintain your content without needing to touch any of the actual code. Wordpress has been the go-to CMS for years. Ghost is a more recent althernative with heavier focus on just being a blogging platform.</p><h3 id="the-api-">The API!</h3><p>While Ghost itself provide its own, pretty robust, frontend, its Ghost API allow for a much greather possibilities. One could, as we have done here, replace the default frontend altogether in favor of another prefered Web Framework of choice.</p><p>With the JAMstack, one would publish new content to the CMS as you would normally do. A webhook would then trigger a rebuild process. The rebuild process usually involve the framework of choice refetching data from the CMS, build all those into static-source, the publish the updated static-site to your web hosting target.</p><p>This allow you to have a fully automatic update and deployment of your static sites!</p><h1 id="how-to-set-up-your-own-">How to set up your own!</h1><p>Since the tutorial become so long, I have separated it out into another <a href="https://ghost.woodies11.dev/how-to-setup-ghost-netlify">post</a>.</p>]]></content:encoded></item></channel></rss>