[{"id":"content:0.index.md","path":"/","dir":"","title":"Open Source Developer","description":"Open source developer, contributing to the Vue, Nuxt, and Vite ecosystems.","keywords":[],"body":" Harlan Wilton Hey, I'm Harlan. An open-source developer from Sydney, Australia . Core team member of Nuxt ,\n VueUse and\n UnJS . Author of Unlighthouse , Unhead and Nuxt SEO . Recent Personal Updates "},{"id":"content:1.blog.md","path":"/blog","dir":"","title":"Blog","description":"This is a directory of the articles I have written and published that are sometimes updated.","keywords":[],"body":" "},{"id":"content:2.projects.md","path":"/projects","dir":"","title":"Projects","description":"This is a directory of the open-source packages and tools I've released that are actively maintained.","keywords":[],"body":" This is a directory of the open-source packages and tools I've released that are actively maintained. You can find all of my projects on GitHub . "},{"id":"content:3.sponsors.md","path":"/sponsors","dir":"","title":"Sponsors","description":"Through the support of my amazing sponsors I'm able to dedicate a lot of my professional time to working on open-source.","keywords":["Send some love"],"body":" Through my amazing sponsors, I'm able to dedicate a lot of my time to working on open-source. Thank you to all of them! Send some love Enjoy my work? Heck yeah! You can show your support by following my Twitter ,\njoining my Discord \nor paying for my coffee addiction ."},{"id":"content:4.talks.md","path":"/talks","dir":"","title":"Talks","description":"This is a directory of the talks I've given either in video or slide format.","keywords":["Videos","Slides"],"body":" Videos Supercharged SEO head management for Nuxt + Practical SEO tips: Nuxt Nation 2022 Slides VueFes 2023 - Getting your head around your Nuxt Nation 2022 - Supercharged SEO head management for Nuxt + Practical SEO tips Laravel Meetup 2022 - To Vite and Beyond: A history and future of bundling"},{"id":"content:5.meet.md","path":"/meet","dir":"","title":"Meet","description":"Let's meet up virtually or in real life.","keywords":["IRL Schedule"],"body":" Let's meet Meeting new people is one of my favourite things about open-source. I love chatting about open-source, business,\ncraft-beer or anything. If you want to chat, feel free free to book a video call with me. Otherwise,\njust jump in my Discord and say hi. IRL Schedule I'm around Sydney, Australia for the next couple of months. Hit me up if you feel like catching up!"},{"id":"content:6.hire.md","path":"/hire","dir":"","title":"Hire","description":"This is a directory of the articles I have written and published that are sometimes updated.","keywords":[],"body":" When not creating open-source, I work for myself as a developer for hire. I specialise in building sites that: Users and search engines love Scale to complex architecture requirements I'm passionate about UX, SEO and performance and my stack is Nuxt and Laravel. My standard rate is $120 USD / hour. Email me any exciting opportunities you have."},{"id":"content:blog:0.2023-february.md","path":"/blog/2023-february","dir":"blog","title":"February 2023: GitHub Sponsor perks and firing a freelance client","description":"I share my updates for February covering my open-source development, financials and personal updates.","keywords":["GitHub Sponsors Perk Updates","Month Review"],"body":" Hello, my amazing sponsors! Welcome to first ever newsletter. I have some exciting things to share with you. GitHub Sponsors Perk Updates Going forward, I'll be improving your sponsor perks in three ways. Early Access Monthly Newsletter At the start of each month, I'll be sending a newsletter (just like this one). This will cover my achievements, financials, and plans for the month ahead. I'll post these publicly towards the end of the month on my blog. New Discord Role / Channels I have a Discord server that I use to provide support to many of my open-source packages. If you have any technical issues you'd like help on, feel free to ask here. I have the following new perks within discord: New role: \"sponsor\" New discussion channel: 💎・priority-support. Link to join: https://discord.gg/FC8FgHuC Bi-monthly Video Call Every 2 months, I'm going to schedule a time to do a video call with any sponsors who would like to join. In it, you can ask me any questions or just hang out, no pressure smile 30-60 minutes Held in discord (link above) First call is scheduled for 9pm AEDT, calendar invite link Month Review Achievements My personal, open-source and freelancing achievements. OSS ⭐ 3431 GitHub stars (+231) Unhead v1.1 release, many performance and security improvements Nuxt OG Image Satori Support and OG Image Playground Personal 🍴 I started the month with a 120-hour fast (5 days)! Went pretty well, and lost 4kg ✈️ Gave notice to move out of our apartment at the end of March Sponsorship Impressions (sites only) The analytics of people who see your generous sponsorship on my doc / personal sites. 👨 5.7k visitors (-22)\n👀 26.3k views (+4k) Financials How sustainable is my current open-source work My financial goal for February was saving $10k, as I have some travel plans coming up. I planned to spend 50% of my time on open-source. I had a ~100-hour project booked with one of my clients. Around 40 hours into the project it was clear that I couldn't work with them anymore on it. I decided to fire them and ended up refunding them 40 hours of work to be done with it. While this was quite a hit, the peace of mind of not having to work with them anymore on it was worth it. I'll avoid any bad-mouthing, I'll just say that they weren't pleasant to work with. OSS ⌛ 111 hrs 💸 A$617.06 A$5.55 / hour Freelancing ⌛ 78.25 hrs (+ 35.25 hrs) 💸 A$3015 (- $517.5) A$38.53 / hour (-$42.67) March plans By the end of this month, I'll be starting my travels for 3 months in Asia, going from Thailand, South Korea, and Indonesia. During this time, I'll continue to work on open-source, just while sitting on a beach drinking a coconut. Until then I have some goals: Significantly improve the Unhead performance in Nuxt (feat: server/client only composables, feat(head): improved Unhead integration) Docs site for Nuxt SEO Kit Make Nuxt OG Image an official Nuxt module, have a few bugs to fix Land a few more Nuxt PRs Summary I hope you've enjoyed the first newsletter! Again, please join me for the first sponsors call on the 7th if you're interested. Best wishes to all of you and I hope you have an amazing month ahead."},{"id":"content:blog:0.my-open-source-journey.md","path":"/blog/my-open-source-journey","dir":"blog","title":"Introduction: My open-source journey","description":"Discover how you can build a modern package development through how I created the Unlighthouse project.","keywords":["My Open Source Journey"],"body":" My Open Source Journey At the end of 2020, I finished up my full-time job at a startup . I wanted to explore other ways of working,\nwith the idea of building a SaaS product. Though, without an idea, I was passionate about and no audience. It felt like a waste of time. Instead, I decided to focus on learning as much as I could within the Vue ecosystem. I started building my old blog \nin the earliest version of VitePress and did an article comparing Vite to webpack . In setting up Twitter and sharing that article, I was hooked. I started writing more, contributing issues and building my own Vue plugin . Through these I grew my audience and started focusing on Nuxt modules and joined the WindiCSS team. Since then, I've been working on all sorts of projects. While progress has been slow while I've been learning, I'm proud of what I've accomplished,\nand I'm excited to share more with the community."},{"id":"content:blog:1.2023-march.md","path":"/blog/2023-march","dir":"blog","title":"March 2023: Planning a year of travel and moving out","description":"I share my updates for March covering my open-source development, financials and personal updates.","keywords":["Personal Updates","Numbers","Work Updates / Financials","April plans","Final thoughts"],"body":" Personal Updates Moving out Since becoming a freelancer and focusing on open-source, my days are mostly relaxed. I can choose what I want to work\non and actively avoid any hard deadlines. March was the complete opposite. Deadlines on moving out, on planning and preparing for a year of travel, finishing off freelancing commitments\nand staying on top of my open-source work. But it all got done. As of the end of March, my girlfriend (Alina) and I handed over the keys to our leafy-green apartment in Canberra, Australia.\nWith no where in particular to now call home. Travel We started our travel last week, landing in Bangkok, Thailand. Bangkok is a hectic city, not recommended for relaxing.\nBut I enjoyed the city, the food and the people. It has been interesting trying to work on open-source while travelling. Any down-time I have I whip out the laptop. Thailand's internet has been surprisingly good which as made that easier. As of writing this we're in Ko Chang, Thailand. Ko Chang is a dense mountainous jungle island with deep-blue water paired with complex orange gradient sunsets. We're staying in a bungalow near the beach for $30 AUD a night. Numbers ⭐ 3580 GitHub stars (+149) Work Updates / Financials With the cost of living in Thailand much cheaper than Australia, I'm spending less money while travelling than I would be\nif I was still living in Canberra. Open-Source ⌛ 88 hrs (-23 hrs) 💸 $787 AUD (+$168) $8.94 AUD / hour (+$3.39 / hour) Most of my open-source time last month went into Nuxt modules and Nuxt itself. My current focus is trying to get all the Nuxt SEO Kit modules to a v2 and release v2 of Nuxt SEO Kit. Main projects 24 hours Nuxt - working on better Unhead integration, a couple of minor bugs and triage 22 hours nuxt-simple-sitemap - v2 with many SSR improvements and bug fixes. Is now closer to being feature-complete. 14 hours Unhead - working towards a v2 which will significantly reduce the bundle size. 12 hours nuxt-og-image - v2 is out under a beta tag, with many bug fixes and improvements. Still want to add a couple of features for the v2 release. 3 hours nuxt-simple-robots - v2 is out and now feature-complete. Freelancing ⌛ 6 hrs (-72.25 hrs) 💸 $645 AUD (-$2370) $107 AUD / hour (+$68 / hour) Didn't have too much freelancing to do. Finished off some important projects and now have a bit of a break. April plans Figuring out what I can actually finish while travelling is a challenge. I'm trying to keep my goals small and achievable. I would like to get Nuxt SEO Kit v2 out by the end of April. This will be quite a bit of work as each dependent module\nneeds to be updated to v2, which I have many plans for. It also includes a proper documentation site. I would also like to get the Unhead v2 update out. The bundle size improvements will be a win for Nuxt and the Vue ecosystem. Final thoughts I'm really enjoying travelling and working while travelling. I hope to sustain it as long as I can and I can't thank you,\nthe sponsors, enough for your support. While the amount I can get done will take a hit, I'm still dedicated to providing as much value\nas I can for your contributions. Please get in touch with my if you have any questions, feedback or suggestions. Would love to chat!"},{"id":"content:blog:2.2023-april.md","path":"/blog/2023-april","dir":"blog","title":"April 2023: Thai New Year and Nuxt SEO Progress","description":"April was spent traveling within Thailand while working through a bunch of bugs in my Nuxt SEO modules.","keywords":["Personal Updates","Work Updates","Numbers / Financials","May plans","Final thoughts"],"body":" Personal Updates A Month In Thailand We spent all of April in Thailand, 38 days total.\nWe got lucky to sneak into Thailand just before they removed the 45-day visa on arrival. As you'll know from the last update , we started the month in Ko Chang. For those interested in our route afterward: Chanthaburi - A small and quiet town with charm, known for its gemstones. Pattaya - Caught up with some friends here to celebrate Thai New Year. Krabi - Amazing rock formations. Would highly recommend mangrove canoeing. Ko Phi Phi - Tiny scenic island but very touristy. Think bucket cocktails for $4. Khao Sok - Jungle, caves, lakes, monkeys and waterfalls. One of my favourite places in Thailand. Chiang Mai - Super-chill and relaxed city. Great food, great people, great vibes. Pai - Quite touristy with a hippy vibe. The surrounding nature is amazing. Thai New Year 🇹🇭 We were lucky enough to be in Thailand for Songkran. Songkran is a festival to mark the start of the Buddhist New Year.\nWater is thrown to symbolize the washing away of sins and bad luck. We spent the holiday hanging out with our friends and the Thai locals,\nthrowing icy-cold water at passers-by on the street. Work Updates Nuxt SEO Kit v2 Module Progress Getting v2 of Nuxt SEO Kit has been weighing on me for a while. Making progress towards it meant solving some particularly annoying bugs. Most of my time was spent on nuxt-og-image . It has been a challenging module. Debugging WASM code, sifting through nitro code and testing on a number of cloud providers. I went through 30 beta releases and things are looking better. The rest went into nuxt-simple-sitemap . In March, I decided to add support for i18n. I18n is complicated. While there is still some more work to do it's a lot more stable and the improvements will be incremental. Numbers / Financials ⭐ 3779 GitHub stars (+199) Open-Source ⌛ 70.75 hrs (-17.25 hrs) 💸 $1,013 AUD (+$226) - Had a payout from the Windi CSS project as it was sunset. $14.32 AUD / hour (+$5.38 / hour) - Highest yet! Although this was mostly a one-off. Main projects 34 hours nuxt-og-image 16.5 hours nuxt-simple-sitemap 6 hours Nuxt Freelancing ⌛ 2.25 hrs (-4.75 hrs) 💸 $225 AUD (-$420) $100 AUD / hour (-$7 / hour) April's focus was on open-source work. I had two organic leads come in.\nI decided to push the meetings to May where I had more availability while traveling. May plans In April, I wanted to get Nuxt SEO Kit v2 out. This was optimistic and unrealistic, unfortunately. May's plan is to continue the work towards it.\nThe estimate for a Nuxt SEO Kit v2 stable release is still a few months away. Final thoughts It was a really fun month in Thailand, I had a good balance of work and traveling.\nIt's never satisfying just working on bugs and issues, there's not much to show for it. But this is the way open-source works it seems.\nPeriods of rapid development and then periods of slow but steady maintenance to get things stable. Thanks for reading and thanks to my amazing sponsors."},{"id":"content:blog:4.2023-may.md","path":"/blog/2023-may","dir":"blog","title":"May 2023: South Korea Mountains and Unlighthouse Goes Viral","description":"May was spent traveling to South Korea and Indonesia while working on Unlighthosue going viral.","keywords":["Personal Updates","Work Updates","Numbers / Financials","June plans","Final thoughts"],"body":" Personal Updates South Korea Travel In May, we finished up our time in Thailand and traveled to South Korea for 2 weeks. Seoul - Amazing city with so much to do. The culture is very unique and the mountains surrounding the city are beautiful. Busan - Coastal city, kind of reminded me of the Gold Coast in Australia. Jeju - Island off the south coast of South Korea. Beautiful beaches and hiking. We really loved the food, especially the Korean BBQ and bipimbap. They had surprisingly good coffee, craft beer and pastries, which I partook in generously. Hiking Bukhansan Mountain in Seoul and Hallasan Mountain in Jeju were definitely the highlights. I would love to return one day and explore more of the country. Indonesia Travel After South Korea, we flew to Jakarta, Indonesia with a quick stop over in Singapore.\nWe'll spend the next month here, until mid-June when we head back to Australia. We're taking Indonesia pretty slow.\nIt's nice to have a bit of a break after the busy itinerary in South Korea and Thailand. Yogyakarta - University town, lots of culture, art and history. Hostels here have a really nice, social vibe. Bali - Ubud and Canggu.\nCanggu is very touristy, but the food is worth it.\nMelbourne Cafe tier.\nUbud is quite touristy too, but you can see some\namazing sights around like rice fields, temples and waterfalls. Gili Islands - Good snorkeling and chilling. (currently here) Flores - Planning lots of hikes for here. We're looking forward to Komodo National Park and Kelimutu volcano. Work Updates Unlighthouse Goes Viral Last month I had a DM from someone telling me that they had put Unlighthouse front of Jeff from Fireship. I thought that was pretty cool in of itself, and I didn't think much more about it. Next thing I knew, though, the Unlighthouse stars started going bananas. I did some quick research and found the culprit. Very cool. And with that, I had a massive influx of issues to deal with through GitHub and my Discord. I started working through them, I would solve one, and two more would appear.\nIt took me the rest of my May to get control of them. It wasn't a bad problem to have though.\nI'm grateful to have Unlighthouse being used by so many people, and I'm glad it's helping people improve their sites. It's given me a lot of ideas for how to improve it, and I'm excited to work on it more in the future. Numbers / Financials ⭐ 5407 GitHub stars (+1628) Open-Source ⌛ 84 hrs (+13.25 hrs) 💸 $744 AUD (-$269) - Payout from Windi CSS the month before. $8.85 AUD / hour (-$5.46 / hour) Main projects 27 hours Unlighthouse Some important improvements around reporting, authentication, how the chrome binary is used and Docker. 23 hours nuxt-seo-kit Extracting the breadcrumbs and site config logic into separate modules in preparation of v2. These will be released soon. 8 hours nuxt-og-image Getting the final issues solved so the v2 release can happen. 7.5 hours nuxt-simple-sitemap Some outstanding issues around runtime sitemaps and i18n. Freelancing ⌛ 8.25 hrs (+0.75 hrs) 💸 $1,025 AUD (+$800) $124.24 AUD / hour (+24.24 / hour) I started some SEO work for a new client. The work was to fix site-wide technical issues from Google Lighthouse.\nThis is exactly why I originally built Unlighthouse,\nso it was perfect. June plans I'll be landing back in Australia toward the middle of June. I look forward to having a few weeks of routine.\nI'll use this time to focus on getting Nuxt SEO Kit v2 released. Final thoughts I'm really grateful to see the growth in Unlighthouse this month,\nand I'm excited to finally release what I've been working on the for the last few months. Thanks as always to my amazing sponsors.\nSee you next month.\n."},{"id":"content:blog:5.2023-june.md","path":"/blog/2023-june","dir":"blog","title":"June 2023: Flores, Australia and preparing for major releases","description":"Traveling to Flores, Indonesia and coming back home to Australia while preparing for major releases.","keywords":["Personal Updates","Work Updates","Numbers / Financials","Final thoughts"],"body":" Personal Updates Flores We spent the last week of our South East Asia trip in Flores, an island in Indonesia, east of Bali. Flores is best known for its Komodo dragons and the glowing lakes of Mt Kelimutu volcano. We did a road trip from East (Maumere) to West (Labuan Bajo), weaving along \"spaghetti roads\" through the mountains and\ndense jungle. We passed tiny villages as we went that offered a unique glimpse into the life of the Floresian and the Manggarai people. Nearly everyone lived off the land, with rice being grown as the main source of food. Occasionally we'd see someone roasting their coffee beans which I also had a go at. When inspecting the jungle, we'd always see something growing:\nmangoes, bananas, passion-fruit, cocoa, vanilla pods, taro, etc. Our highlights were: Kelimutu volcano, which has 3 crater lakes at the top. These lakes change colour over time, and we were lucky enough to see them in their blue, green and black state. Rinca Island, seeing the Komodo dragons in their natural habitat. Bena Traditional Village, walking amongst amazing thatched roof houses. Australia: A wedding and camping After Flores, we flew back to Australia for a wedding. It's winter time here, so it was a bit of a shock to the system after 3 months of high heat and humidity. After the wedding, we took a trip to the Warrumbungles with some friends. The Warrumbungles is a national park north-east of Sydney which is best known for its amazing night sky views. We camped there for a few days, doing some hikes and exploring the area. Afterwards we spent time in the Blue Mountains and Sydney catching up with family and friends,\nand planning the Europe leg of our trip. Work Updates June was an important month of work,\ngetting things ready for the next major releases of my Nuxt modules. The main blocker was Nuxt OG Image which had continued to plague me with issues. With some luck and many hours of work, I was able to get it to a state where it's mostly working correctly on all\nmajor runtimes and was able to unblock myself. This was a huge relief, and I'm looking forward to pushing out all Nuxt SEO modules in the coming months. Numbers / Financials ⭐ 6112 GitHub stars (+705) Open-Source ⌛ 87 hrs (+3 hrs) 💸 $867 AUD (+$123) $10.36 AUD / hour (+$1.51 / hour) Main projects Nuxt Modules Audit (13.5 hours) Some preliminary work to audit the state of Nuxt modules and authors. Still need some time to finish this off. Nuxt SEO Modules (45 hours) Fixing outstanding issues to unblock work on the new majors deveolopment. Mostly involved\nNuxt OG Image, Nuxt Simple Sitemap and Nuxt Simple Robots. Work also going into the initial versions of the new Nuxt Site Config module. 6 hours Unlighthouse Adding new CSV reporting feature and fixing some minor bugs. Freelancing ⌛ 10.25 hrs (+1.5 hrs) 💸 $1,025 AUD $100.00 AUD / hour (-24.24 / hour) Small bit of adhoc bug fixing work for one of my clients. Fun with AWS SNS notifications... Final thoughts It's really nice ce to be back in Australia and to have some more time to dedicate to work.\nThere are exciting things planned for July, some have already been released, and there's much more to come. Thanks as always to my amazing sponsors.\nSee you next month!"},{"id":"content:blog:6.2023-july.md","path":"/blog/2023-july","dir":"blog","title":"July 2023: Nuxt Release Week, Turning 29 and Exploring Greece","description":"What I got up to in July in my open-source work and my travels.","keywords":["Open-source Metrics","What I Got Up To","Other Small Wins","August Plan"],"body":" Open-source Metrics What I Got Up To Nuxt Release Week While working and travelling it's difficult to build momentum. I have a constant pulse of things requiring attention: GitHub issues,\n Discord support questions, client issues etc. I like helping people, it's rewarding in of-itself. But going for long periods\nwithout releasing new projects is demotivating. That's why I was excited for the start of July. I had an entire week free in Sydney to focus on shipping. With this excitement, I naively announced that I'd get something out every day on Twitter. The v2 release of Nuxt SEO Kit has been something bouncing around in my mind for many months, so I decided to focus on that. It started off really fun with overwhelmingly positive feedback. I even received my first Pizza sponsorship.\nAn extra special shout out to Omar McPizza Each night I was staying up progressively later and later. By the Friday release at 7am, I was done. I had burnt the candle at both ends and was ready to enjoy the weekend. I wasn't able to get Nuxt SEO Kit v2 out, but I was happy with what I had achieved. A month of work finished in one week. Even though it was a grind, it was really fun, and I'm grateful to the community\nfor their support. Turning 29 On the 10th July, my girlfriend and I flew to Greece, starting the Europe leg of our travels. This also happened to be one of my more unique birthdays. I spent the entire day in airplanes and airports, ending\nwith a premium sleep on the floor of Singapore airport. Being 28 was a good time with many memories and accomplishments while still trying to find my feet in open-source. I'm especially proud of joining team Nuxt, and I'm excited for the year ahead and have plenty of exciting projects to release Exploring Greece We got to Athens just as they were in a heat wave, going from the Australian winter to 40 degree heat. Our plan was to get to Istanbul, exploring what we could on the way, with an obligatory Greek island stop. We had a lot of fun in Athens, especially at the archaeological sites and museums. This was also our introduction to authentic Greek food, which has become our new favourite cuisine. After Athens, we travelled to the beautiful Greek island Naxos. We really liked exploring Halki village, the Old Town and beaches. After Naxos, we headed inland to Meteora, which is known for its stunning rocky outcrops and monasteries. It was quite impressive. Struggling with the heat, we decided to chill in Thessanoliki for a few days. The food, bars and museums were top tier. Our final stop in Greece was the charming town of Xanthi, where we spent time wandering the picturesque cobblestone streets. Other Small Wins VueFes Japan : I'll be speaking at VueFes Japan on the 28th October, would love to see you there! Nuxt Scripts : Started working on Nuxt Script in collaboration with the Google Aurora team Nuxt Link Checker : Released v2 with Live Inspections. August Plan Release Nuxt SEO Kit v2 (hopefully) Nuxt Scripts private beta 🚌 Türkiye, Bulgaria, North Macedonia, Albania, Montenegro"},{"id":"content:blog:7.2023-august.md","path":"/blog/2023-august","dir":"blog","title":"August 2023: Türkiye, Bulgaria, North Macedonia and Albania","description":"What I got up to in August in my open-source work and my travels.","keywords":["Open-source","🇹🇷 Türkiye","🇧🇬 Bulgaria","🇲🇰 North Macedonia","🇦🇱 Albania","September Plan"],"body":" In August me and my girlfriend continued travelling, getting to 4 new countries: Türkiye, Bulgaria, North Macedonia and Albania. A typically morning was waking up and trying to piece together the details of where we even where, we were moving fast\nafter Greece. We did a quick detour in Türkiye, then the many cramped and sweaty bus getting around the Balkans. I was surprised to still manage to get 109 hours to work on open-source, but I was starting to feel the burnout of\nmanaging work and travel. Open-source The highlight of the month was getting out Unhead v1.3 which included\nsome significant performance improvements. I continued work on Nuxt Scripts, trying to figure out the scope of the project and how it would work. The rest of my time went to maintenance of Nuxt SEO . 🇹🇷 Türkiye Our first day in İstanbul was massive, exploring all the stunning Mosque's. You can easily hit up 5 mosques and only walked\na few kms. We stayed in a locals neighbourhood and hit up the family-run restaurants. I didn't know at that point, but I'd be eating\nsome form of \"kebab\" and drinking Turkish coffee every day for the next month. Safe to say, I also took advantage of the Turkish sweets. After being thoroughly mosque'd out, we headed to the famous \"balloon\" town Göreme within the Cappadocia region. Göreme is stunningly scenic little town that is an Instagram tourist haven and in turn is a bit of a tourist trap.\nBut we still enjoyed it! We did a full-day hike through Rose valley, coming across\nimpressively well-endowed rock formations, historical abandoned cave churches and a friendly dog\nwho guided us. This was one of my favourite hikes, we had the trail almost entirely to ourselves, it was scenic and exploring the caves with\nour new dog friend was fun. The balloons were expensive, so I decided to skip. But I did hear good things and the ground view\nitself was pretty impressive. 🇧🇬 Bulgaria Bulgaria was the start of\nthe Balkans for us, which meant I was in for a lot of cheese pies and more kebabs. We did a quick couple of days in Plovid, which had a nice old town and some interesting Roman ruins. Then we were\noff to the capital city Sofia. It had some interesting architecture and a nice park. From Sofia, we went to do Seven Rila Lakes hike, which was one of our highlights. The hike is a chair lift, followed by a 3-hour ascent to the peek. Along the way you'll see 7 lakes, each at a different altitude. It was a fun hike, everyone had brought their dogs along and had picnics at the lakes. You get to the peak, and it's a stunning view - if the clouds aren't blocking it. 🇲🇰 North Macedonia We spent a couple of days in Skopje, a city characterised by an excess of modern statues and monuments. Many of them\nartificially made to look historical. A nice day trip from Skopje is Matka Canyon, you can do some nice hiking, lake swims and kayaking. The rest of our time in North Macedonia was spent in Ohrid, a beautiful lake town. It seems to be the holiday destination\nof choice for anyone in the Balkans who doesn't have easy access to the coast. Every night we were there, there was a festival on the lake, with fireworks, music and rides. 🇦🇱 Albania We didn't know much about Albania beforehand, we had heard good things from other travellers though and we were excited.\nThey told us about the friendly locals, and it was beautiful nature. Our first stop was Gjirokastër, Albania. Gjirokastër is a UNESCO world heritage site, with a classic old town and castle.\nWe got a good workout climbing up to the castle, which had some nice views. We also did a day trip to the Blue Eye, which is a natural spring that is a stunning blue colour. You can jump from\na few metres up into the water, which was fun. From there, we hit the coastal town of Himarë. Himarë is one of the remaining chill beach towns in the Balkans, that\nhasn't been completely overrun with tourism development. Our last stop for Albania was the Northern town of Shkodër. From Shkodër\nyou can go and stay in the Albanian Alp town of Theth. We spent 4 days in Theth, doing the Volbona pass hike and chasing waterfalls. It was really cosy and relaxing to be surrounded by\nthe mountains, it was a good recovery from the fast pace of the Balkans. September Plan 🚌 Montenegro, Bosnia & Herzegovina, Croatia, Slovenia, Hungary, Czechia"},{"id":"content:blog:how-the-heck-does-vite-work.md","path":"/blog/how-the-heck-does-vite-work","dir":"blog","title":"How Does Vite Work - A Comparison to webpack","description":"A deep-dive into the comparisons between the earliest Vite version and webpack. Discover what I learnt digging into internals and how I, correctly, guessed Vite was the next big thing.","keywords":["A Recap on Vite","Vite vs webpack","Understanding webpack","Understanding Vite","Production Builds","Summary","Getting started with Vite","Thanks for reading"],"body":" Note: This article was written for the alpha of Vite. I'd recommend reading Patak's great article on The Vite Ecosystem . In rebuilding my old Nuxt.js personal site, I wanted to challenge myself to learn the latest tech, the unknown. The unknown was the new project by Evan You: ⚡ Vite (/veet/). Called Fast, for the 🇫🇷 Frenchies. I'll be comparing how Vite works to the standard webpack config using webpack-dev-server , which all major Vue frameworks\nare using. We'll be looking at how Vite no-bundling works, by first looking at how webpack's bundling works and what the difference is. Afterwards I'll give you some\nrecommendations for setting up Vite for yourself. Vite could the next best thing in tooling, currently, it's still in a pre-release stage though so be careful out there 🐛. A Recap on Vite Vite is a web development build tool which supports Vue, React and Preact. It's an experimental new direction in how build tools can work with a greenfield ecosystem. Vite's core functionality is similar to webpack + webpack-dev-server with some core improvements\non developer experience: ⌛ Less time waiting for your app to start, regardless of app size 🔥 Hot module reloading (HMR) that is basically instant, regardless of app size 🔨 On-demand compilation 🙅‍♂️ Zero configuration for numerous pre-processors out of the box 📜 Esbuild powered typescript / jsx (super quick) Speed Example To give you a quick idea on how much faster it is, the below comparison is for Vue CLI which uses webpack. The bigger your app\nis the more noticeable the speed difference will be. \n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n Build Time Dev Server Start Time Dev Page Load Time Vue CLI 5.14s 2568ms 320ms Vite 2.39s 232ms ️ 379ms New Vue 3 project / 10 components / no Babel / 2nd run, in development. Vite vs webpack The main functional difference you'll notice with Vite and your webpack app, is how code is served in development and which modules are supported. Don't worry if the below terms don't make sense to you, we'll be exploring them below. webpack (Nuxt.js / Vue CLI / etc) Supported Modules: ES Modules , CommonJS and AMD Modules Dev Server: Bundled modules served via webpack-dev-server using Express.js web server Production Build: webpack Vite Supported Modules: ES Modules Dev Server: Native-ES-Modules, served via Vite using a Koa web server Production build: Rollup Check out Mozilla's article on ES Modules if they're new to you. Understanding webpack To understand how Vite works, it's best to look at how webpack works first. Even with its popularly, understanding webpack can be intimidating, so I'll try to keep it simple. webpack is versatile in what you can do with it, but at its core, it will: Starting with an entry file, build a tree of your dependencies: all the imports, exports, requires from your code/files Transform / compile modules: think transpiling js for older browsers, turning SCSS into CSS Use algorithms to sort, rewrite and concatenate code Optimise webpack In Development Assuming you're using one of the main Vue frameworks, when you start your app in development, it is going to do a few things: Bundle all of your code Start the webpack-dev-server, the Express.js web server which will serve the bundled code Setup sockets which will handle the Hot Module Reloading As you may notice with your own apps, the bigger they grow, the longer you have to wait to start coding. Bundling in development is quicker because you don't need to do as much with the code, however,\nas your app grows, it will become painfully slow, especially on older machines. webpack Component Example I created a default Vue 3 Vue CLI project, which has an entry App.vue file using the HelloWorld.vue component.\nLet's see how this component gets to my browser. HelloWorld.vue component: < script >\n export default {\n props : {\n msg : String\n }\n }\n \n \n < template >\n < div class = \" hello \" >\n < h1 > {{ msg }} \n \n \n \n < style scoped >\n h1 {\n color : green ;\n }\n \n When I start my app and visit localhost I get the following HTML from the Express.js server. \n < html lang = \" en \" >\n < head >\n < meta charset = \" utf-8 \" >\n < meta http-equiv = \" X-UA-Compatible \" content = \" IE=edge \" >\n < meta name = \" viewport \" content = \" width=device-width,initial-scale=1.0 \" >\n \n < body >\n < div id = \" app \" >\n < script type = \" text/javascript \" src = \" /js/chunk-vendors.js \" >\n < script type = \" text/javascript \" src = \" /js/app.js \" >\n \n \n You'll notice we have 2 script files there: chunk-vendor.js and app.js . On inspecting them you'd see a lot of gibberish looking code.\nit helps to use the webpack-bundle-analyzer to see how it works visually. chunk-vendors.js These are third-party modules, usually coming from node_modules . The two main libraries in here are Vue itself and sockjs which is used for HMR. app.js This is all the code for my application. It contains components, assets, etc. You'll notice that for an SFC it splits\nit into multiple modules. Taking a quick look at the app.js file, we can find some of the HelloWorld component code. As you can see in the above image,\nall parts of the SFC are separate modules: the wrapper, CSS, template, js. The wrapper module is defining and importing the other models, some beautiful code. /***/ \" ./src/components/HelloWorld.vue \" :\n /*!***************************************!*\\\n !*** ./src/components/HelloWorld.vue ***!\n \\***************************************/\n /*! exports provided: default */\n /***/ ( function ( module , __webpack_exports__ , __webpack_require__ ) {\n \n \" use strict \" ;\n eval ( \" __webpack_require__.r(__webpack_exports__); \\n /* harmony import */ var _HelloWorld_vue_vue_type_template_id_469af010_scoped_true__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./HelloWorld.vue?vue&type=template&id=469af010&scoped=true */ \\\" ./src/components/HelloWorld.vue?vue&type=template&id=469af010&scoped=true \\\" ); \\n /* harmony import */ var _HelloWorld_vue_vue_type_script_lang_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./HelloWorld.vue?vue&type=script&lang=js */ \\\" ./src/components/HelloWorld.vue?vue&type=script&lang=js \\\" ); \\n /* empty/unused harmony star reexport *//* harmony import */ var _HelloWorld_vue_vue_type_style_index_0_id_469af010_scoped_true_lang_css__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./HelloWorld.vue?vue&type=style&index=0&id=469af010&scoped=true&lang=css */ \\\" ./src/components/HelloWorld.vue?vue&type=style&index=0&id=469af010&scoped=true&lang=css \\\" ); \\n\\n\\n\\n\\n\\n _HelloWorld_vue_vue_type_script_lang_js__WEBPACK_IMPORTED_MODULE_1__[ \\\" default \\\" ].render = _HelloWorld_vue_vue_type_template_id_469af010_scoped_true__WEBPACK_IMPORTED_MODULE_0__[ \\\" render \\\" ] \\n _HelloWorld_vue_vue_type_script_lang_js__WEBPACK_IMPORTED_MODULE_1__[ \\\" default \\\" ].__scopeId = \\\" data-v-469af010 \\\"\\n /* hot reload */ \\n if (true) { \\n _HelloWorld_vue_vue_type_script_lang_js__WEBPACK_IMPORTED_MODULE_1__[ \\\" default \\\" ].__hmrId = \\\" 469af010 \\\"\\n const api = __VUE_HMR_RUNTIME__ \\n module.hot.accept() \\n if (!api.createRecord('469af010', _HelloWorld_vue_vue_type_script_lang_js__WEBPACK_IMPORTED_MODULE_1__[ \\\" default \\\" ])) { \\n api.reload('469af010', _HelloWorld_vue_vue_type_script_lang_js__WEBPACK_IMPORTED_MODULE_1__[ \\\" default \\\" ]) \\n } \\n \\n module.hot.accept(/*! ./HelloWorld.vue?vue&type=template&id=469af010&scoped=true */ \\\" ./src/components/HelloWorld.vue?vue&type=template&id=469af010&scoped=true \\\" , function(__WEBPACK_OUTDATED_DEPENDENCIES__) { /* harmony import */ _HelloWorld_vue_vue_type_template_id_469af010_scoped_true__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./HelloWorld.vue?vue&type=template&id=469af010&scoped=true */ \\\" ./src/components/HelloWorld.vue?vue&type=template&id=469af010&scoped=true \\\" ); \\n (() => { \\n api.rerender('469af010', _HelloWorld_vue_vue_type_template_id_469af010_scoped_true__WEBPACK_IMPORTED_MODULE_0__[ \\\" render \\\" ]) \\n })(__WEBPACK_OUTDATED_DEPENDENCIES__); }.bind(this)) \\n\\n } \\n\\n _HelloWorld_vue_vue_type_script_lang_js__WEBPACK_IMPORTED_MODULE_1__[ \\\" default \\\" ].__file = \\\" src/components/HelloWorld.vue \\\"\\n\\n /* harmony default export */ __webpack_exports__[ \\\" default \\\" ] = (_HelloWorld_vue_vue_type_script_lang_js__WEBPACK_IMPORTED_MODULE_1__[ \\\" default \\\" ]); \" );\n The main takeaway here is that within the app.js file contains all modules for my app. webpack does let you chunk the bundles how you like, for Nuxt.js it chunks routes individually. The more chunks though,\nthe more requests and more potential blocking js. You may see the problem here, we have multiple monolith files that need to be generated anytime we want to use our app.\nWhen we change a file for HMR, we need to regenerate the entire file. Understanding Vite Vite doesn't set out to be a new bundler. Rather, it's a pre-configured build environment using the Rollup\nbundler and a tool for local development. Vite In Development Vite makes the assumption that developers are going to be using the latest browser versions, so it can safely rely on the\nlatest JS functionality straight from the browser - in other words, no babel transpiling! When you start Vite for the first time pre-optimisations will be done on your node_modules , then Koa ,\na light-weight node web server starts to serve your app. There is no bundling or compiling needed to start the dev server, so it's damn quick (< 300ms). When you open your Vite app you'll be served the index.html from the server. The browser is going to read the index.html \nand know how to parse the Native-ES-Module code. < script type = \" module \" > import \" /vite/client \" \n < div id = \" app \" >\n < script type = \" module \" src = \" /@app/index.js \" >\n Parsing the Native-ES-Module means it will read the export and import lines from your code. It will convert those\nlines into HTTP requests back to the server, where it will again read the export and import lines and make new requests. It will keep going through like this with your dependencies recursively, in a waterfall process, until everything has been resolved. Vite Component Example Let's take a look at how these requests are working in the browser. After I open my app at http://localhost:3000 , the browser has fetched the following index.js file from the web server: import ' /@theme/styles/main.scss?import ' ;\n import Layout from ' /@theme/Layout.vue ' ;\n import NotFound from ' /@theme/NotFound.vue ' ;\n import CardPost from ' /@theme/components/CardPost.vue ' ;\n \n const theme = {\n Layout,\n NotFound,\n enhanceApp ({ app , }) {\n app. component ( ' CardPost ' , CardPost)\n }\n };\n export default theme;\n Normally, in webpack, you would have to transpile this code to something legacy browsers can understand. Newer browsers know what to do with it, see es6 module dynamic import . Let's drill into that highlighted line which is requesting the CardPost SFC. The browser will turn that import into a request for http://localhost:3000/@theme/components/CardPost.vue . < script >\n import posts from ' ../../posts '\n \n export default {\n props : {\n postIndex : {\n type : Number,\n required : true ,\n }\n },\n computed : {\n post () {\n return posts[ this .postIndex]\n }\n }\n }\n \n \n < template >\n < div class = \" card-post \" >\n ...\n \n \n \n < style lang = \" scss \" scoped >\n .card-post {\n ...\n }\n \n Once the web server gets this request, it will need to compile the CardPost.vue file to javascript and send it back. Vite has many\noptimisations around the Vue compiling so this takes no time. Let's see what comes through: import posts from ' /.vitepress/posts.ts '\n \n import ' /@theme/components/CardPost.vue?type=style&index=0 '\n import { render as __render } from ' /@theme/components/CardPost.vue?type=template '\n \n const __script = {\n props : {\n postIndex : {\n type : Number,\n required : true ,\n }\n },\n computed : {\n post () {\n return posts[ this .postIndex]\n }\n }\n }\n __script.__scopeId = ' data-v-287b4794 '\n __script.render = __render\n __script.__hmrId = ' /@theme/components/CardPost.vue '\n typeof __VUE_HMR_RUNTIME__ !== ' undefined ' && __VUE_HMR_RUNTIME__. createRecord (__script.__hmrId, __script)\n __script.__file = ' /home/harlan/sites/new.harlanzw.com/app/.vitepress/theme/components/CardPost.vue '\n export default __script\n Cool, so quite a bit going on here. The main thing to note here is how it's split up the SFC into different modules which\nwill need separate requests to fetch. It hasn't bundled these imports into the SFC or some other monolith file. Dependencies: /.vitepress/posts.ts Template: /@theme/components/CardPost.vue?type=template Stylesheet: /@theme/components/CardPost.vue?type=style&index=0 If you're curious, this is what the style component response looks like, some nifty for sure. import { updateStyle } from ' /vite/client '\n \n const css = ' .card-post[data-v-287b4794] { \\n position: relative; \\n } \\n .card-post .prose[data-v-287b4794] { \\n max-width: 100% !important; \\n } \\n .card-post__link[data-v-287b4794] { \\n position: absolute; \\n left: 0; \\n top: 0; \\n width: 100%; \\n height: 100%; \\n content: \" \"; \\n z-index: 1; \\n } \\n .card-post__content[data-v-287b4794] { \\n background-color: white; \\n z-index: 1; \\n } \\n .card-post__effect[data-v-287b4794] { \\n z-index: -1; \\n content: \" \"; \\n height: 30px; \\n width: 100%; \\n position: absolute; \\n background-color: #059669; \\n transition: 0.2s; \\n opacity: 0; \\n top: 30px; \\n } \\n .card-post:hover .card-post__effect[data-v-287b4794] { \\n top: -5px; \\n opacity: 1; \\n transform: rotate(0.25deg); \\n } '\n updateStyle ( ' 287b4794-0 ' , css)\n export default css\n You can see how the above allows the Hot Module Replacement to work efficiently. When you have a module that is changed,\nsay the styles within a component, instead of reloading the entire component tree, only the style module needs to be replaced. You can also imagine with the above, where Vite slows down. Imagine hundreds of HTTP requests which rely on nested HTTP requests, recursively.\nFortunately, there are optimisation to avoid this situation after the first load. The server will return a 304\nUnmodified HTTP Status code for modules which haven't changed, meaning they will use the browser's cached version of the file. Vite scales well for any app size because it only needs to request the modules for the route you're on. Production Builds Since Vite is using Rollup, pre-configured, you'd expect a similar output from Vite as webpack. Vite does boast a quicker\nbuilder and potentially a smaller artifact size, as Rollup is a more efficient bundler than webpack. The main gotcha is that Vite can still only support ES Modules in the production build, meaning you can't have any dependencies\nwhich don't have ES Module exports. Vite is also pre-configured to handle your build as a universal app. A universal app is built using a client (virtual browser)\nand a server (node). Allowing it to pre-render the HTML pages, so robot crawlers can fetch your page content without executing\njs and speeding up the initial load for users. That means SEO friendly static sites out of the box 🎉. Summary While I haven't touched on a lot of the complexities of Vite and webpack, I've tried to show you the main difference, how\nbundling and no-bundling look in action. Hopefully you've seen why Vite is promising alternative. There is so much potential in the ecosystem at the moment, watch this space, given 12-months we could see an explosion of Vite related projects. If you want to find out more about Vite, I'd watch Evan's talk on Vite & VitePress . Getting started with Vite I'd recommend just spinning up bare-bones Vite to get a feel for it. It's really easy, takes less than a minute. npm init vite-app\n Once you are sold, it's worth checking out the ecosystem before you build. Recommendations You shouldn't be looking to replace Vue CLI or webpack with Vite for existing projects yet, but it may be valuable to check out for new smaller scoped projects. The Vite ecosystem isn't that mature yet, the two main projects I'd recommend checking out are VitePress and Vitesse . If you are in need of a documentation site then VitePress is awesome, you can follow the VuePress documentation to fill in any gaps. VitePress abstracts away\nthe Vite configuration, which will be limiting for non-documentation sites. Otherwise, I'd choose Vitesse as it's going to give you more flexible on customising your app. Vitesse offers a pre-configured vite.config.js , so you can easily\nstrip anything out you don't need to add whatever you'd like to it. If you like my blog (VitePress + TailwindCSS), then you're more than welcome to clone it . Thanks for reading If you like the technical side of Vue and Laravel, I'll be posting regular articles on this site. The best\nway to keep up to date is by following me @harlan_zw . html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}"},{"id":"content:blog:modern-package-development.md","path":"/blog/modern-package-development","dir":"blog","title":"Learning Modern Package Development: Monorepos and Backends - Part 1","description":"Discover how you can build a modern package development through how I created the Unlighthouse project.","keywords":["Introduction","Background: Why build Unlighthouse?","Deciding on the stack","Monorepo","Server","Client","Putting It Together - Part 2","Conclusion"],"body":" This article is a work in progress. Feel free to read it, but some sections are incomplete. Introduction Unlighthouse is an open-source package I built to scan your entire site using Google Lighthouse. Building it was chaotic, with day-long bugs, constant refactoring and endless documentation reading. Through building it, I learnt modern development practices, making use of the vast ecosystem of packages and tools. Background: Why build Unlighthouse? As a freelancer I keep on top of my clients organic growth with Google Search Console. Was a day like any other, looking at one of my clients' dashboard. Seemingly out of nowhere, I saw the trend of page position, clicks and page views in free fall. My clients' income was based on organic traffic, not good. Isolating the reason for the falling page rank wasn't easy. The site had issues, but what was causing the free fall? There was no easy way to know. To diagnose the issue, I used Google Lighthouse. I went through all pages of the site, noticing quite a number of issues. I spent a couple of days\nfixing them all up and improving the general performance of the site. What happened next? Things started turning around. I was able to invert the graph. Organic growth doubled in the next few months. Happy client. Now that was out of the way, how could I make it easier to stay on top of the health of the sites I manage? Deciding on the stack I needed to build a tool that would run Google Lighthouse on an entire site with just the home page URL. I had a plan of attack for the build. The backend would be build using Typescript and Node. The frontend client would be built using Vue and Vite. But how would I be able to design this in a way that was easy to build and maintain? I had seen the amazing work coming out of the UnJS ecosystem and knew that they could solve some of my problems. With that, the package would be known as Un (inspired by Unjs) Lighthouse . Keeping a keen eye on other modern packages coming out, I took some of the best practices and tools I saw implemented. The stack was split into three core parts: Monorepo: containing the dependencies to build, test and deploy the code Frontend: displaying searchable, filtering and sortable results Backend: generating the frontend, running the scans and providing an API for the frontend Monorepo Implementing a monorepo is a keystone for a large project which will ship multiple packages. It allows you to\ngroup up logic, dependencies and documentation into a single repository. PNPM PNPM is the new kid on the block of node package managers and has gained a large following quickly, for good reason. It is the most performant package manager and has first class support for monorepos. There are many benefits to using a monorepo for a package. My personal favourite is it allows me to easily isolate logic and dependencies for your package, letting you write simpler code. Allowing end users to pull any specific part of your package that they want to use. Vitest Vitest is also the new kid on the block of testing. It's original aim was to be a testing framework specifically for Vite, but it has ended up being a possible replacement for Jest entirely. Vitest makes writing your logic and tests a breeze and I'd recommend checking it out for any project. Unbuild This package is described as a \"A unified javascript build system\". In reality, it's a minimal config way to build your package code to ESM and CJS. One of the amazing features of unbuild is stubbing. This allows you can run source code from your dist folder, meaning it transpiles just-in-time. This allows you to completely cut out the build step when you're iterating and testing integrations on your package. It's as simple as unbuild --stub . import { defineBuildConfig } from ' unbuild '\n \n export default defineBuildConfig ({\n entries : [\n { input : ' src/index ' },\n { input : ' src/process ' , outDir : ' dist/process ' , builder : ' mkdist ' , declaration : false },\n ],\n })\n GitHub Repo Link Server Lighthouse Binary Unlighthouse wouldn't be possible if Google hadn't published Lighthouse as its own NPM binary . To make Unlighthouse fast, I combined the binary with the package puppeteer-cluster , which allows for multi-threaded lighthouse scans. Unctx It's amazing that a simple pattern like composition has evaded Node packages for so long. With the introduction of Vue 3, composition became cool. And with that, unctx is composition for your own package. unctx allows you to define a scope where there's only a single instance of something that is globally accessible. This is incredibly useful for building packages, as you no longer need to be juggling core state. You can build your logic out as composables that interact with the core. import { createContext } from ' unctx '\n \n const engineContext = createContext < UnlighthouseContext >()\n \n export const useUnlighthouse = engineContext.use as () => UnlighthouseContext\n \n export async function createUnlighthouse ( userConfig : UserConfig , provider ?: Provider ) {\n // ...\n engineContext. set (ctx, true )\n }\n unctx GitHub Repo Hookable For Nuxt.js users, you might be familiar with the concept of frameworks hooks. A way for you to modify or do something with the internal logic of Nuxt. Building a package, I knew that this was a useful feature, not just for end-users, but for me as a way to organise logic. Having a core which is hookable means you can avoid baking logic in that may be better suited elsewhere. For example, I wanted to make sure that Unlighthouse didn't start for integrations until they visited the page. I simply set a hook for it to start only when they visit the client. hooks. hookOnce ( ' visited-client ' , () => {\n ctx. start ()\n })\n Hookable GitHub Repo Unconfig Unconfig is a universal solution for loading configurations. This let me allow the package to load in a configuration from unlighthouse.config.ts or a custom path, with barely any code. import { loadConfig } from ' unconfig '\n \n const configDefinition = await loadConfig < UserConfig >({\n cwd : userConfig.root,\n sources : [\n {\n files : [\n ' unlighthouse.config ' ,\n // may provide the config file as an argument\n ... (userConfig.configFile ? [userConfig.configFile] : []),\n ],\n // default extensions\n extensions : [ ' ts ' , ' js ' ],\n },\n ],\n })\n if (configDefinition.sources?.[ 0 ]) {\n configFile = configDefinition.sources[ 0 ]\n userConfig = defu (configDefinition.config, userConfig)\n }\n Unconfig GitHub Repo ufo Dealing with URLs in Node isn't nice. For Unlighthouse I needed to deal with many URLS, I needed to make sure they were standardised no matter how they were formed. This meant using the ufo package heavily. The slash trimming came in handy and the origin detection. export const trimSlashes = ( s : string ) => withoutLeadingSlash ( withoutTrailingSlash (s))\n const site = new $URL (url).origin\n ufo GitHub Repo Unrouted I needed an API for the client to communicate with the Node server to fetch the status of the scan and submit re-scans. The current JS offerings were a bit lackluster. I wanted something that just worked and had a nice way to use it. I ended up building unrouted as a way to solve that. group ( ' /api ' , () => {\n group ( ' /reports ' , () => {\n post ( ' /rescan ' , () => {\n // ...\n return true\n })\n \n post ( ' /:id/rescan ' , () => {\n const report = useReport ()\n const { worker } = useUnlighthouse ()\n \n if (report)\n worker. requeueReport (report)\n })\n })\n \n get ( ' __launch ' , () => {\n const { file } = useQuery <{ file : string }>()\n if ( ! file) {\n setStatusCode ( 400 )\n return false\n }\n const path = file. replace (resolvedConfig.root, '' )\n const resolved = join (resolvedConfig.root, path)\n logger. info ( `Launching file in editor: \\`${ path }\\` ` )\n launch (resolved)\n })\n \n get ( ' ws ' , req => ws. serve (req))\n \n get ( ' reports ' , () => {\n const { worker } = useUnlighthouse ()\n \n return worker. reports (). filter ( r => r.tasks.inspectHtmlTask === ' completed ' )\n })\n \n get ( ' scan-meta ' , () => createScanMeta ())\n })\n Unrouted GitHub Repo Client The code that what went into building the package. Vue 3 / Vite client The beloved Vite was to be used to make the development of the client as easy and fast as possible. Vue v3 used to make use of the vast collection of utilities available at VueUse . Putting It Together - Part 2 Part 2 of this article will be coming soon where I go over some technical feats in putting together the above packages. Conclusion Thanks for reading Part 1. I hope you at least found it interesting or some of the links useful. html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}"},{"id":"content:blog:nuxt-3-migration-cheatsheet.md","path":"/blog/nuxt-3-migration-cheatsheet","dir":"blog","title":"Nuxt 3 Migration Simplified: A Cheat Sheet","description":"A simplified cheat sheet for migrating from Nuxt 2 to Nuxt 3. Includes a list of all the changes you need to make to your Nuxt 2 project to get it working with Nuxt 3.","keywords":["Introduction","Resources","Pre-migration","Migration","Post Migration","Conclusion"],"body":" Introduction Upgrading to Nuxt 3 from Nuxt 2 can be a little intimidating. There are a ton of changes to keep track of, and it's easy to feel like you're lost in a maze of code. But don't worry, I'm here to help! I've started this cheat sheet\nto simplify the upgrade process for myself and for others who are brave enough to tackle it. It's a work in progress, and not intended to be exhaustive. I'll be updating it as I migrate more sites. Just remember that the migration process will take some time, patience, and persistence, depending on the size of your project and your prior experience. But hey, at least you'll learn a bunch. And if you're just looking to upgrade some modules, I've written an upgrade guide for 23 of them. Let's do this! Resources Links Docs Nuxt 3 - Docs Nuxt 3 - Official Migration Guide Nuxt 3 - Migration guide discussion Vue 3 - Migration Guide Tech UnJS – A collection of packages which make up the Nuxt core. VueUse – Essential Vue Composition API utilities. Vite.js and Rollup – Backbone of Nuxt 3 bundling. Nuxt 3 Examples Nuxt Examples - Minimal scoped examples NuxtBnB - Complex app Nuxt Movies - Complex app harlanzw.com - @nuxt/content app Education Free: NuxtNation 2022 YouTube playlist Paid: Mastering Nuxt Getting help Discord : create a post in #nuxt3-help if you get stuck Twitter Nuxt Community Post on StackOverflow with the tag Nuxt , kissu may help you if you're lucky Reddit Nuxt.js Need quicker help? Try providing a reproduction repo .\n \nAt a minimal, provide the output of npx nuxi info . Pre-migration Before you start upgrading, it's important to familiarise yourself with the new features and changes in Nuxt 3 and prepare your\nproject for the upgrade. 1. Get familiar with Nuxt 3 Learn the key differences between Nuxt 2 and Nuxt 3 and the new technologies that power it. Nuxt 3 Architecture While there are many similarities to Nuxt 2, the underlying architecture is completely new, more complex and more powerful. Start by learning the differences between: Nuxt (core context) Nuxt App (app context) Nitro (server engine) Nuxi (command line interface) You may want to re-familiarise yourself with the Rendering modes (SSR, SPA, Static). Daniel Roe's talk What happens when you start Nuxt 3 is a great place to start. Read the official migration guide Having a rough understanding of the migration process will allow you\navoid common pitfalls. Keep this open as you'll reference this as you continue on with the migration process. Play with Nuxt 3 Create a new fresh Nuxt 3 project using nuxt.new , play around with it. This will help you get a feel for the new features and how they work. Learn Vue 3 / Composition API Vue 3 is a major upgrade from Vue 2. You will need to learn the new syntax and features to be able to use Nuxt 3, particularly\nthe Composition API . Learn the basics of TypeScript Whether you like TypeScript or not is irrelevant. Nuxt 3 is written in TypeScript, trying to avoid it will only make your life harder. You don't need to be an expert, but you should be able to read and understand the Nuxt 3 code. You can learn the basics with TypeScript in 5 minutes . Learn about modules: CJS and ESM Nuxt 3 requires ECMAScript modules, so you'll need to convert your CommonJS modules. See the Nuxt ES modules page. Many of the issues you'll come across are due to using dependencies which are CJS.\nSo understanding the difference between them can be critical. Node modules can be a challenging subject, if you need further guidance see this article . 2. Prepare your Nuxt 2 app Before you start migrating, you should prepare your Nuxt 2 app to make the upgrade process easier. Upgrade Nuxt 2 If you are on an earlier version, the first step is upgrading. The latest Nuxt 2 uses Vue 2.7 , which supports the Composition API. npm i nuxt@^2.15.4\n # yarn add nuxt@^2.15.4\n Upgrade Modules Upgrade your modules to the latest version that supports Nuxt 2. This will make the upgrade process easier. Be careful not to upgrade modules which may only support Nuxt 3 in future versions. Enable component auto-imports Nuxt 3 has auto-imports enabled by default, if you're not using this feature in Nuxt 2, you should enable it to identify\nany component paths that need to be updated. All manual component imports should be removed. See Components Discovery for more information. export default {\n components : true\n }\n Audit your dependencies Having hard dependencies in your project that aren't compatible with Nuxt 3 / Vue 3 is going to massively slow you down. You should go through all your node dependencies, checking the following: Vue plugin: Supports Vue 3 Nuxt module: Supports Nuxt 3 or has an upgrade path. See Migration - 2. modules and the Nuxt 3 modules . Other: Scan with bundlephobia to find ESM support / alternatives. You can use CJS dependencies, but you'll need to transpile them. This is a good opportunity\nto drop any dependencies you don't need, migrate to dependencies which have better maintenance and / or are smaller. Migration This cheat sheet requires a fresh Nuxt 3 project and migrating things over one by one. In each step of migrating, you should test to see if everything is working.\nIf there are bugs, then you will be able to debug them easily. 1. Create the new app The first step is creating the new apps boilerplate and migrate over safe config from nuxt.config.ts . Create a Fresh Nuxt 3 App You can do so with npx nuxi init my-app . This will create a new Nuxt 3 project. Create Boilerplate By default, Nuxt 3 does not provide page routing and layouts. To make the migration easier, you'll be modifying the files to be Nuxt 2 compatible. To enable them we update app.vue with NuxtLayout and NuxtPage . < template >\n < div >\n < NuxtLayout >\n < NuxtPage />\n \n \n \n Create an empty default layout file that will be updated later. < template >\n < div >\n < slot / >\n \n \n Create a basic index page, so we can see our changes. < template >\n < div >\n < h1 >hello world\n \n \n Migrate Runtime Config / env Follow the Migrate Runtime Config doc. The env key in nuxt.config.ts is no longer supported, you should use runtime config exclusively. // Nuxt 2\n export default {\n privateRuntimeConfig : {\n apiKey : process.env. NUXT_API_KEY || ' super-secret-key '\n },\n publicRuntimeConfig : {\n websiteURL : ' https://public-data.com '\n },\n env : {\n NUXT_API_KEY : process.env. NUXT_API_KEY\n }\n }\n // Nuxt 3\n export default defineNuxtConfig ({\n runtimeConfig : {\n apiKey : process.env. NUXT_API_KEY || ' super-secret-key ' ,\n // Warning: `public` is exposed on client and server\n public : {\n websiteURL : ' https://public-data.com '\n }\n }\n })\n You can safely ignore TypeScript issues this causes for now. Migrate head If you previously had head configuration in your nuxt.config.ts , you should migrate it to app.head config. If you're using hid in your head config, unless you have a good reason, you should remove it. Read @vueuse/head v1 release notes \nto learn more. Note: Tags with href and src will not resolve relative links / aliases.\nYou will need to reference absolute links from the public directory. If you'd like to use aliases, you can use the nuxt-unhead module. // nuxt 2\n head: {\n meta: [\n { hid : ' description ' , name : ' description ' , content : ' My custom description ' },\n ]\n }\n // nuxt 3\n app: {\n head: {\n meta: [\n { name : ' description ' , content : ' My custom description ' }\n ]\n }\n }\n Copy Static files In Nuxt 3 the /static folder has been renamed to /public .\nMake the public directory and copy all of these files over. Copy CSS / Assets In Nuxt 3, the assets folder serves the same function.\nSimply copy over your assets' folder. Once done, update the css field. css: [\n ' ~/assets/main.css ' ,\n ]\n Most pre-processors will work with no extra config. You will just need to install the dependencies. For example, if you see an error like: Preprocessor dependency \" sass \" not found. Did you install it?\n You will need to run npm install -D sass . If you're having issues with linked images within CSS,\nyou may need to use absolute links to the images in the public directory. /* Nuxt 2 - we have the file: assets/images/logo.png */\n body {\n background-image : url ( ' ~/assets/images/logo.png ' );\n }\n /* Nuxt 3 - we have the file: public/images/logo.png */\n body {\n background-image : url ( ' /images/logo.png ' );\n }\n You may have some node_module dependencies in your css entries, install each of these as needed. 2. Modules Module migration can be a challenging part of the migration process. You will need to migrate each module one by one and test your app after each module. Nuxt 2 modules are not compatible with Nuxt 3 out of the box, so many modules are not supported yet. I have tried to provide an\nupgrade path for the most popular modules which are not supported. Copy over modules In Nuxt 3 all modules should belong under the modules key. You will need to copy over your modules from the buildModules and modules fields. export default defineNuxtConfig ({\n modules : [\n // your modules\n ]\n })\n Remove redundant modules The following modules can be safely removed as functionality they provide is redundant. @nuxtjs/typescript @nuxtjs/typescript-runtime @nuxtjs/composition-api @nuxtjs/dotenv nuxt-build-optimisations / nuxt-webpack-optimisations Remove Modules No Longer Recommended @nuxtjs/axios and @nuxt/http are not recommended with Nuxt 3. It's best practice to use the $fetch / useFetch API. If are determined to use $http you can use nuxt-alt/http . See the Nuxt 3 and Axios discussion for additional context. Use alternative modules @nuxtjs/auth Official @nuxt/auth support is a work-in-progress . In the meantime, you can use: sidebase/nuxt-auth nuxt-alt/auth @nuxtjs/supabase @nuxtjs/pwa The community nuxt-pwa-module module seems to be the best option. A core module for PWA seems to be undecided, if it goes ahead there's no ETA. @nuxtjs/i18n Official support is in beta, check the docs . @nuxtjs/proxy This feature is coming to nitro route rules. You can track it here: https://github.com/unjs/nitro/issues/113 . In the meantime, you can use the nuxt-alt/proxy module. @nuxtjs/eslint-module The Nuxt 2 version of this module is called @nuxtjs/eslint-config , you should remove this. Instead, follow the documentation for @nuxt/eslint-config . @nuxtjs/sitemap Not currently supporting Nuxt 3. Instead, you can use: nuxt-simple-sitemap or nuxt-seo-kit . @funken-studio/sitemap-nuxt-3 @nuxtjs/robots Not currently supporting Nuxt 3. Instead, you can use: nuxt-simple-robots or nuxt-seo-kit . Alternatively, create your own robots.txt file at public/robots.txt . User-agent: *\n Disallow: /\n OR you can generate robots dynamically. export default defineEventHandler (\n e => `User-agent: * \\n Disallow: / \\n Sitemap: https://YOUR_SITE/sitemap_index.xml`\n )\n @nuxtjs/vuetify Official support is coming soon. You can use nuxt-alt/vuetify in the meantime. @nuxtjs/device Not supporting Nuxt 3. Create your own plugin to detect the device instead, you can use the ua-parser-js package directly. import UAParser from ' ua-parser-js '\n \n export function useDevice () {\n const parser = new UAParser ()\n const { os, browser, device } = parser. getResult ()\n \n return {\n os,\n browser,\n device,\n isMobile : device.type === ' mobile ' ,\n isDesktop : device.type === ' desktop ' ,\n isTablet : device.type === ' tablet ' ,\n // etc\n }\n }\n < script lang = \" ts \" setup >\n const { isDesktop, browser } = useDevice ()\n \n @nuxtjs/moment Not supporting Nuxt 3. Create your own plugin to use Moment instead. import moment from ' moment '\n import ' moment/locale/en '\n \n export default defineNuxtPlugin (( nuxtApp ) => {\n moment. locale ( ' en ' )\n moment.tz. setDefault ( ' UTC ' )\n return {\n provide : {\n moment,\n }\n }\n })\n < script setup lang = \" ts \" >\n // access moment via $moment\n const { $moment } = useNuxtApp ()\n \n @nuxtjs/fontawesome I'd recommend using nuxt-icon . Alternatively,\nsee the Using FontAwesome in Nuxt 3 discussion for plugin code sample. @nuxtjs/dayjs See Nuxt3 support for plugin code sample. @nuxtjs/toast See Examples of using the @nuxtjs/toast or any vue toast plugin? discussion for plugin code sample. @nuxtjs/gtm You can create your own code to handle gtm. export default defineNuxtConfig ({\n app : {\n head : {\n script : [\n {\n innerHTML : `(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':\n new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],\n j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=\n 'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);\n })(window,document,'script','dataLayer','YOUR_GTM_ID');`\n },\n ],\n },\n }\n })\n bootstrap-vue See nuxt3 starter? issue for code sample. Work in progress. @nuxtjs/google-analytics See Vue Plugins from the docs for an example of how to use Google Analytics with Nuxt 3. Alternatively,\nsee this comment \non how you can implement it with cookie control. Upgrade supported modules If the module you're using supports Nuxt 3, it's important to check the migration notes.\nIt's likely that the upgrade will include some\nbreaking changes. 3. State management If you're using Vuex 3, you will need to upgrade your state management, as Vue 3 does not support it. Consider if you need a state management library Nuxt 3 ships with its own statement management with the useState composable. If you're using a state management library, you should consider if you need it. Additionally, if you are dealing with data client side, you can always reference global refs. This isn't recommended for\nserver-side rendering as state is shared between users. import { ref } from ' vue '\n \n // You can import this counter from any file and the data will stay in sync\n export const counter = ref ( 0 )\n \n export function useCounter () {\n return {\n counter,\n increment () {\n counter.value ++\n },\n decrement () {\n counter.value --\n }\n }\n }\n Otherwise, migrate to Pinia or Vuex 4 Pinia is the recommended state management solution for Nuxt 3, but you can use Vuex 4 if you prefer. 4. Components Components will only be parsed if they are imported. This means it's safe to copy over all our components and mixin files from Nuxt 2. Copy components The folders have the same name and will be auto-imported. You don't need to add anything to nuxt.config.ts . If your components aren't being found,\nit may be because component auto-imports are done with path-prefixing by default,\nsee Component Names . Follow Component Options Migration See the Component Options Migration guide for more information. 5. Pages and dependencies You will be copying over your pages one by one and their dependencies. As you copy each page, you will run into errors. These errors may be related to breaking changes between Nuxt 2 and 3 or to missing dependencies. For example, you'll have missing layout, plugin and potentially component dependencies. It's important you reference the Nuxt 3 Migration Guide as you go.\nRemember to ask for help in the community channels if you get stuck. Home Page Start with the home page, pages/index.vue .\nThis will give you some momentum and help you get started. Copy static route pages You'll start with routes which aren't dynamic which tend to be less complex and easier. For example: pages/about.vue , pages/contact.vue , etc. Copy dynamic route pages Nuxt 3 uses a new file system routing system, which means that all our pages need to be converted to the new format. See Dynamic Routes for more information. 6. Server Routes / Middleware Nuxt 3 uses a completely new server engine and HTTP framework.\nThis means that all our server routes and middleware need to be converted to the new format. Migrate server routes See Migrate Server Routes . You'll likely need to completely rewrite your server routes. Migrate server middleware See Migrate Server Routes . You'll likely need to completely rewrite your server middleware. Post Migration 1. Recommendations Migrate to Composition API While this is not required, it is highly recommended.\nThe Composition API is a much better way to write Vue code and Nuxt provides\nbetter support for it. Tips: vue-composition-convertor may give you a head start ChatGPT is pretty good at converting Vue 2 options API to Vue 3 composition API Migrate everything to TypeScript Nuxt 3 is TypeScript first, it is the best practice to have all of your code in TypeScript. If you're using the Options API, you can simply add lang=\"ts\" to your Vue SFCs. Otherwise, if you are using the options API, you\ncan make use of the defineNuxtComponent function. < script lang = \" ts \" >\n export default defineNuxtComponent ({\n props : {\n name : {\n type : String,\n required : true\n }\n },\n mounted () {\n console. log ( this .name)\n }\n })\n \n Load static assets from an absolute path In Nuxt 2 it was common to load everything from ~/assets , however with Nuxt 3 the default is to use absolute paths. This way the bundler does not need to parse them. This will save you time when migrating and improve your build performance. Simply move images which aren't going to change to static and load them from / instead of ~/assets . Use Vue Macros To make the migration from Vue 2 as simple as possible, you can make use of vue-macros . It provides a number of useful build-time macros, which speeds up the time to convert the code. < script setup lang = \" ts \" >\n const { modelValue, count } = defineModel <{\n modelValue : string\n count : number\n }>()\n \n console. log (modelValue.value)\n modelValue.value = ' newValue '\n \n Switch to UnJS / VueUse where appropriate The VueUse and UnJS packages all support Nuxt 2 and Nuxt 3. If you can migrate to using them over other dependencies,\nyou'll save yourself a lot of time. 2. Testing Testing in Nuxt 3 is still under development. There is some documentation around it, but it is fairly minimal. Migrate to Vitest Nuxt 3 uses Vitest for testing. If you're using Jest or Mocha, you'll need to migrate. See the Why Vitest guide for more information. Use @nuxt/test-utils Follow the documentation on the Testing page, to get started. Add vitest-environment-nuxt (optional) The vitest-environment-nuxt package will likely take over as the official testing environment for Nuxt 3. It's still a work-in-progress with minimal documentation, but you may consider using it. Conclusion You likely still have some migration pain ahead of you, but you're well on your way, congratulations. Feel free to reach out to me directly on Twitter or Discord with any feedback or questions you may have. Keep in mind\nthat this migration guide does not intend to be exhaustive and is being actively updated. This post took me many hours to put together. Please consider sponsoring me if this has helped you. html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}"},{"id":"content:blog:nuxt3-hydration-node-mismatch.md","path":"/blog/nuxt3-hydration-node-mismatch","dir":"blog","title":"Nuxt 3 \"Hydration Mismatch\" Errors","description":"Learn about the causes and solutions for the Hydration Children Mismatch error in Nuxt 3, including the use of client-only components, the component, and the @nuxtjs/html-validator module.","keywords":["The \"Hydration Mismatch\" Error","Solving the Hydration Mismatch Error","Conclusion"],"body":" The \"Hydration Mismatch\" Error A hydration mismatch error is a disagreement between the server and the client on what the Vue rendered HTML should look like. When it\noccurs when you see one of the following errors in your browser console: [Vue Warn]: Hydration node mismatch...\n [Vue Warn]: Hydration children mismatch...\n How Server-Side Rendering Works When a user requests your Nuxt 3 site, Nuxt has one important job: Responding with the full rendered HTML of the page and all client-side scripts needed to bring it to life. It does this by getting Vue to run its server-side rendering (SSR) code. This code is responsible for taking your Vue components and turning them into HTML. When Vue renders the HTML, it is rendering DOM nodes in a way that may not match how a browser itself would render them. For example, it's quite possible to generate invalid HTML\nwith Vue whereas the browser would complain. How Client-Side Hydration Works When the user's browser receives the HTML, it loads the client-side scripts. These scripts are responsible for bringing initializing your Vue app,\nrendering all of your components with the goal of producing the same HTML that the server rendered. In doing so, it has the full state needed to create new interactivity. Consequences of a Hydration Mismatch When a hydration mismatch occurs, the client-side Vue app can have quite unpredictable results. Leading to interactivity not working,\nor even worse, the page not working at all. Most commonly, this error will lead to a poor user experience and SEO issues. Solving the Hydration Mismatch Error There are a number of ways to solve this error. The first step is to identify the cause of the mismatch. This can be quite tricky in Nuxt 3 as the error messages can sometimes\nbe quite cryptic. Ensure Pages Have a Single Root Element As a quick check, if your error is pointing to a page component, it's possible that the error is from multiple root elements. While not technically required in Vue 3, it's required in Nuxt 3 to have a root element in your page components. Bad - Has 2 root elements < template >\n < div >\n < h1 >My Page\n \n < div >Hello \n \n Good - 1 root element < template >\n < div >\n < h1 >My Page\n < div >Hello \n \n \n Check the HTML Because the trigger for the error is the HTML not matching, this can be a good starting point to debugging. There are many ways to produce invalid HTML in Vue. It's not always intuitive to know these what is invalid though. The easiest way to check the HTML is to use the @nuxtjs/html-validator module. This module will validate the HTML and\nthrow an error if it is invalid. Use Client Only Components If you notice that the error is coming from a component that is only needed on the client-side, you can wrap it with component, alternatively.\nyou can prefix your component name with .client.vue . For example MyWidget.client.vue . This is quite common components which use code coming from a third-party library. For example, a component which uses a Google Maps library. In these instances, the third-party library does not have support to server-side render. < template >\n < client-only >\n < MyWidget />\n \n \n Conclusion Hydration mismatch errors can be quite frustrating and tricky to debug. Having a better understanding of the root cause of the error\nand how to solve it can help you avoid these issues in the future. html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}"},{"id":"content:blog:scale-your-vue-components.md","path":"/blog/scale-your-vue-components","dir":"blog","title":"Scaling Your Vue Components for Mid-Large Size Apps","description":"Working on a mid-large size app usually means hundreds of components. How do you make sure these components will scale?","keywords":["100+ component club","Solving component scaling with rules","Example: Newsletter Sign Up","Example: Forum Thread","Extra and optional rules","Thanks for reading"],"body":" One of the key pieces in scaling your Vue app is having good component architecture. How are components named? What folder hierarchy should you use? How is component code scoped? The cost of not having clear answers to these simple questions increases as your app grows. My previous role was the tech lead at a startup. Growing pains were frequent with pivots, design changes and new features. All pushing our total component count up. My below suggestions are what I came up with to solve our scaling issues. Your project will have its own requirements. 100+ component club Let's assume once you hit 100+ components, then you are a mid-size app and you will be feeling your own growing pains. Are you in the club? Run the following in your component folder: # cd app/components\n COMPONENTS = $( ls -lR ** / * .vue | wc -l ) && echo -e \" You have ${ COMPONENTS } components. \"\n Problems you may have Difficult to remember which component to use where Code is being repeated Monolithic components New components are being built instead of leveraging existing ones Inconsistent emits and props between components with the same functionality Technical debt is being ignored because it is too painful Solving component scaling with rules Good code adheres to a set of rules. You either follow existing rules (syntax and conventions) or create\nnew ones and make sure others follow them (documentation and code reviews). Rule 0. Have good dev processes There is no substitute for a good development process. You need to be following best continuous delivery, documentation and communication practices.\nThe rest of the rules will not help you if you are not functioning like a well-oiled machine. Rule 1. Know the style guide You should be familiar with the official Vue.js Style Guide .\nIt gives you clear, concise instructions on what you should and shouldn't do.\nYou should set up eslint-plugin-vue with the recommended rules. module . exports = {\n extends : [\n // ...\n ' plugin:vue/vue3-recommended ' ,\n // 'plugin:vue/recommended' // Use this if you are using Vue.js 2.x.\n ],\n }\n Rule 2. Use a component naming convention The bane of developers lives: how to name something. You can address that by having an easy-to-follow convention on how to name a component. The convention also tells you where to put the component in your folder hierarchy. {prefix} - {namespace} {?-class} Prefix Base components (a.k.a. presentational, dumb, or pure components) that apply app-specific styling and conventions should all begin with a specific prefix, such as Base, App, or V. A short prefix for all your components is preferable to the above. Using a prefix avoids conflicts with HTML tags and third-party components. It also gives you scoped IDE autocompletion and more reusable components. Prefixing becomes especially important when working with a component library ( Vuetify , VueStrap , etc) or third-party components\n( algolia , google maps , etc). You should use something which relates to your app, for example, I use h as the prefix because my site is harlanzw.com. < template >\n < p >Please enter your email to subscribe\n \n < v-text-field label = \" Your Email \" />\n \n < h-button >Submit\n \n You can use many prefixes for your components to help you with scoping code. < template >\n \n < the-header >\n \n < v-img src = \" logo.png \" />\n \n < h-button >Sign In\n \n < main >\n < the-sidebar />\n < the-content v-html = \" content \" />\n \n < the-footer />\n \n Namespace Child components that are tightly coupled with their parent should include the parent component name as a prefix. The style guide recommends starting the component name with the parent component. I've found using a namespace after the prefix instead is more flexible. Namespaces avoid conflicts, improve IDE autocompletion and define the scope of the component. You should map namespaces to a folder, this way you can group components, making them easier to find and use. An example of a namespace is Field , for all our field components (text field, textarea, search, etc.). components/\n | - Field/ # namespace\n | --- HFieldText.vue\n | --- HFieldTextarea.vue\n | --- HFieldSearch.vue\n | --- HFieldAutocomplete.vue\n | --- HFieldCheckbox.vue\n You can then create conventions that components in a namespace should follow. For example these components should all have a :value prop and $emit('input', value) . Class (optional) Component names should start with the highest-level (often most general) words and end with descriptive modifying words. The final part of the convention is, in fact, the name of the component. Thinking of it as a class name makes the distinction between the namespace easier. You still want to follow the above style guide rule, our class names should be\ngeneral to descriptive. The class should be optional. Namespaces can provide a default component to reduce the name of common components. Imagine you have a project with a few buttons. Most of the time you want to use the default button, you shouldn't\nneed to name it HButtonDefault.vue . components/\n | - Button/ # namespace\n | --- HButton.vue # The namespaces default component \n | --- HButtonCallToAction.vue # A call to action button\n | --- HButtonSubmitForm.vue # A button to submit forms\n Recommendations on naming the class: Describe the application function of the component, rather than what it looks like.\n ❌ HButtonRainbowFlashing.vue ✅ HButtonCallToAction.vue Choose to be verbose if it adds clarity to the scope.\n ❌ HProfileUser.vue ✅ HProfileAuthenticatedUsersCard.vue Prefer full words over abbreviations. From the style guide . Rule 3. Separate component scopes Defining scopes for how components behave will guide you in staying DRY. There are many ways to set this up. A good starting point is a scope for \"shared\" (a.k.a. base, presentational or dumb) components and \"app\" (a.k.a single-instance). components/\n | - app # Contains application logic\n | - shared # Does not contain application logic\n You could also pull out your \"shared\" components into their own npm package. When creating new components it's natural to couple application logic in. With this setup, you'll think about component scopes more and how code can be re-used. \"Shared\" Folder - Base Components These components are re-usable and include form inputs, buttons, dialogues and modals. They should never contain application logic or state data. You should be aiming to build your own \"UI kit\" from these components. Copy-pasting your shared folder into a new project should work out of the box (assuming you handle dependencies). \"App\" Folder - App components App components do contain application logic and state data. If you were to copy+paste an app component into a new project, it should not work. Example: Newsletter Sign Up This exists as two \"app\" components, they contain logic for validation and posting to an API. They both contain \"shared\" components. components/\n # application component scope\n | - app/ \n | -- Newsletter # namespace\n | --- HNewsletterForm.vue # validates and posts data\n | --- HNewsletterCard.vue # handles successful form post\n # shared component scope\n | - shared/ \n | -- Alert/\n | --- HAlertSuccess.vue\n | -- Button/ \n | --- HButton.vue \n | -- Card/\n | --- HCard.vue\n | -- Form\n | --- HForm.vue\n | -- Field/\n | --- HFieldEmail.vue\n < template >\n < h-form @ submit = \" submit \" >\n < h-field-email\n v-model = \" email \"\n label = \" Enter your email \"\n />\n < h-button type = \" submit \" >\n Subscribe\n \n \n \n < template >\n < h-card >\n < div class = \" pl-3 \" >\n < h2 >Keep up to date\n < h-newsletter-form\n v-if= \" ! success \"\n @ submit = \" success = true \"\n />\n < h-alert-success\n v-else\n >\n Thanks for signing up :)\n \n \n \n \n Example: Forum Thread Now imagine you want to build a forum thread page. A user can see comments, upvote comments and post their own comment. Using F as our component prefix, let's look at what you need. components/\n # application component scope\n | - app/ \n | -- Thread # namespace\n | --- FThread.vue # Wraps the entire thread\n | --- FThreadPost.vue # A single post / reply\n | --- FThreadFormReply.vue # Form to submit a reply\n | -- Field/\n | --- FFieldComment.vue # Comment box for posts\n | -- Button/\n | --- FButtonUpvote.vue # The thumbs up button\n # shared component scope\n | - shared/ \n | -- Img/\n | --- FImgAvatar.vue # Users photos\n | -- Field/\n | --- FFieldWYSIWYG.vue # Comment box for posts\n | -- Card/\n | --- FCard.vue # Gives posts a 'card' look\n | -- Button/\n | --- FButton.vue # Reply button for the post box\n < template >\n < f-thread-post\n v-for= \" posts as post \"\n :key= \" post . id \"\n :post= \" post \"\n / >\n < f - thread - reply @ submit = \" addPost \" />\n \n < template >\n < f-card >\n < div class = \" p-3 border-b-2 border-gray-500 flex \" >\n < f-img-avatar : src = \" post.author.avatar \" />\n < span > {{ post.author.name }} \n < span > {{ post.publishedAgo }} \n \n < div class = \" p-3 border-b-2 border-gray-500 prose \" v-html = \" post.content \" / >\n < div class = \" p-3 \" >\n < f-button-upvote\n : upvotes = \" post.upvotes \"\n class = \" border-r-2 border-gray-500 pr-3 \"\n @ click = \" upvote \"\n />\n \n \n \n < template >\n < f-form @ submit = \" submitComment \" >\n < f-field-comment\n label = \" Write a reply \"\n />\n < div class = \" flex \" >\n < p >Please make sure you've read our Forum Rules before replying.\n < f-button type = \" submit \" >\n Reply\n \n \n \n \n Extra and optional rules Use An Automatic Component Importer Being tied to import paths once you have a few hundred components is going to slow you down. Using an automatic component imports will clean up your code. You'll be free to tinker with the directory structure of your components in any way you want. Typescript Components The value of types, when you're working with objects is too good to pass up. Will save you hours down the line in developer experience. As a starting point, I'd try and get your shared components using Typescript. < script lang = \" ts \" >\n import type { PropType } from ' vue '\n import { defineComponent } from ' vue '\n import type { Post } from ' ./types '\n \n export default defineComponent ({\n props : {\n post : {\n type : Object as PropType < Post >,\n required : true\n }\n },\n })\n \n Components have \"one job\" Every component should have one job, any code in the component that isn't achieving that job shouldn't be there. You should be thinking when you create a component what it's one core function is. You can limit yourself with this mindset, but it's worth keeping in mind as you go. Create component demo pages Using a package like Storybook is a great idea, but it comes with overhead and when you're starting out it can be a bit overkill. As a starting point, you can create pages under a /demo prefix and throw your components on it.\nYou want an easy way to find components and classes that are available. Here is a rough demo page as an example: Massive Monster UI Demo . Keep it as basic as you want. Mixins and composables This one should be pretty obvious and there are enough articles elsewhere on using these. You want to pull out common logic from components and put them in either mixins or composables. Check out VueUse for some ideas on what that could look like. Thanks for reading If you like the technical side of Vue and Laravel, I'll be posting regular articles on this site. The best\nway to keep up to date is by following me @harlan_zw or signing up for the newsletter below. html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}"},{"id":"content:blog:vue-automatic-component-imports.md","path":"/blog/vue-automatic-component-imports","dir":"blog","title":"Building a Vue Auto Component Importer - A Better Dev Experience","description":"Components magically being imported into your app is the latest developer experience trend in Vue. Why does it exist and how does it work?","keywords":["Why Automatic Component Imports?","Fundamental: How Does webpack Load Vue Files?","Building an Automatic Component Importer","Problems With Automatic Component Imports","Other Compile-Time \"Upgrades\"","Conclusion","Thanks for reading"],"body":" When first learning Vue, you are taught you need to import and add components to components in the script block. < script >\n import HelloWorld from ' @/components/HelloWorld.vue '\n \n export default {\n components : {\n HelloWorld\n }\n }\n \n \n < template >\n < HelloWorld />\n \n However, there's been a recent trend to \"upgrade\" the Vue developer experience (DX), having components magically import themselves\nat compile-time. < template >\n < HelloWorld />\n \n In the wild, you can find auto component imports in most popular Vue frameworks, as part of the core or a plugin. Nuxt Components Vuetify Chakra Vue CLI (built by me) Vite This article will look at: why automatic component imports exist, how you can easily build our own auto component importer using\na Webpack loader and what the performance cost of using them has on your app. Finally, we'll look at some other compile-time DX upgrades that are possible. Why Automatic Component Imports? The why that comes first to my mind, is the developer experience is great. No more confusion or typos on import paths,\nrefactoring becomes easier and there's less code overall. The unintuitive but equally great advantage is found in the problem that this feature first solved. The UI framework Vuetify is a huge library of over 80 components, coming in at 99.4KB \nfor their scripts. As far as I know, they were the first to introduce automatic component imports. Problem: UI Framework Bloat One of the complaints you'll hear about using a UI framework over something simple like TailwindCSS ,\nis the bloat it will add to your app. This is a valid concern. It's unlikely your application is going to need half the components that a UI framework has to offer. Forcing\nbrowsers to download code that will never run, dead code, is not ideal. Additionally, this component bloat can make import paths harder to work with and further scope for issues to pop up. So, how do Vuetify and other UI frameworks overcome their inherent bloat? Solution: webpack Optimisations As is the way, webpack is here to magically solve our problems with tree shaking and code splitting optimisations. If tree shaking is new to you, you can think of it as an optimisation to remove code that isn't explicitly used. Banishing\n'dead' code to the shadow realm. The tree shaking optimisation requires ES2015 module syntax, (i.e. import and export ) and a production build. The code can't be compiled\nto CommonJS modules (i.e. require ) for it to work. So how does all this relate to automatic component imports? With Vuetify handling the imports of your components ( à la carte as they call it), they\ncan ensure webpack optimisations are running out of the box for your app with their component library. The A la carte system enables you to pick and choose which components to import, drastically lowering your build size. This will also make code-splitting more effective, as webpack will only load the components required for that chunk to be displayed. Fundamental: How Does webpack Load Vue Files? Before we jump into building our own automatic component importer, we'll need to have a basic understanding of how webpack loads Vue files. When you request a resource (such as a file) in webpack, it pushes the request through a pipeline of webpack loaders to resolve the output. A webpack loader is a piece of code which will transform a resource from one thing into another, it has an input and output . For example, the raw-loader will read a file and give you the string contents.\nThe input is a path to a file in your filesystem, the output is the string contents of the file. import txt from ' raw-loader!./hello.txt '\n \n // txt=HelloWorld\n The vue-loader is the loader for .vue files. The loader compiles and bundles your component Single File Component (SFC) into code\nthat the browser can understand and run. Vue Loader in Action Let's take a look at an example of input and output from the vue-loader. Input: App.vue This is the default entry file for Vue CLI with Vue 3. < script >\n import HelloWorld from ' ./components/HelloWorld.vue '\n \n export default {\n name : ' App ' ,\n components : {\n HelloWorld\n }\n }\n \n \n < template >\n < img alt = \" Vue logo \" src = \" ./assets/logo.png \" >\n < HelloWorld msg = \" Welcome to Your Vue.js App \" />\n \n \n < style >\n #app {\n font-family : Avenir, Helvetica , Arial , sans-serif ;\n -webkit-font-smoothing : antialiased ;\n -moz-osx-font-smoothing : grayscale ;\n text-align : center ;\n color : #2c3e50 ;\n margin-top : 60 px ;\n }\n \n Output: App.vue Internally, the loader parses this code using the compiler, getting an SFC descriptor object that is used to create the\nfinal string output of the loader. import { render } from ' ./App.vue?vue&type=template&id=7ba5bd90 '\n import script from ' ./App.vue?vue&type=script&lang=js '\n \n import ' ./App.vue?vue&type=style&index=0&id=7ba5bd90&lang=css '\n \n export * from ' ./App.vue?vue&type=script&lang=js '\n script.render = render\n script.__file = ' src/App.vue '\n \n export default script\n Note: I've removed the Hot Module Reloading (HMR) code for simplicity here. The output of the loader isn't that important to understand, just know that the vue-loader has an in and out function. The output\nis usually parsed to another loader such as babel-loader before being chunked. Building an Automatic Component Importer If you have some spare time, I'd encourage you to join along. You can use Vue CLI with the Vue 3 preset. vue create auto-component-importer -p __default_vue_3__\n To begin, let's remove the manual import from the entry SFC, like so: New App.vue < script >\n export default {\n name : ' App ' ,\n }\n \n \n < template >\n < img alt = \" Vue logo \" src = \" ./assets/logo.png \" >\n < HelloWorld msg = \" Welcome to Your Vue.js App \" />\n \n \n < style >\n #app {\n font-family : Avenir, Helvetica , Arial , sans-serif ;\n -webkit-font-smoothing : antialiased ;\n -moz-osx-font-smoothing : grayscale ;\n text-align : center ;\n color : #2c3e50 ;\n margin-top : 60 px ;\n }\n \n When we load our App.vue , the HelloWorld doesn't work, as expected. Our goal is to get it to work without touching the Vue code. Step 1. Modify the webpack Configuration We need to make sure the loader we'll be making is going to run after the vue-loader. module . exports = {\n chainWebpack : ( config ) => {\n config.module\n .rules\n . get ( ' vue ' )\n . use ( ' components ' )\n . loader (require. resolve ( ' ./imports-loader ' ))\n . before ( ' vue-loader ' )\n . end ()\n }\n }\n If you'd like to see the raw webpack config example, open the below. \n webpack.config.js example module . exports = {\n // ...\n module : {\n rules : [\n {\n test : / \\.vue $ / ,\n loader : ' vue-loader '\n }\n ]\n }\n }\n Knowing that webpack loaders are loaded from bottom to top, we would modify the configuration as so: module . exports = {\n // ...\n module : {\n rules : [\n {\n test : / \\.vue $ / ,\n use : [\n {\n loader : require. resolve ( ' ./imports-loader ' ),\n },\n {\n loader : ' vue-loader ' ,\n }\n ]\n },\n ]\n }\n }\n Normally a webpack would handle this configuration changing for you. Now we create the loader called imports-loader.js in your apps root directory. We're going to make sure we only run it for the virtual SFC module. module . exports = function loader ( source ) {\n // only run for the virtual SFC\n if ( this .resourceQuery)\n return source\n \n console. log (source)\n return source\n }\n The source variable is the output of the vue-loader, the Output: App.vue . Here we can now change anything about how our components work by modifying the vue-loader output. Step 2. Dumb Compile-Time Import As a proof of concept, let's try to import the HelloWorld.vue component so our New App.vue works. At this stage, we can just append the import code on to the source . module . exports = function loader ( source ) {\n // only run for the virtual SFC\n if ( this .resourceQuery)\n return source\n \n return ` ${ source }\n import HelloWorld from \"@/components/HelloWorld.vue\"\n script.components = Object.assign({ HelloWorld }, script.components)\n `\n }\n Your App.vue now knows what the HelloWorld component is and works. Try it yourself. Note: This is a dumb solution, as it will be modifying HelloWorld.vue to also import itself. Step 3. Making it smart A smarter solution would give us the ability to add components to our component folder and use them straight away without\nany imports. a. Scan components The first step in making it smarter is we need to create a map of the components files we want to automatically import. We recursively iterate over the components folder and do some mapping. const base = ' ./src/components/ '\n const fileComponents = ( await globby ( ' *.vue ' , { cwd : base })). map (( c ) => {\n const name = path. parse (c).name\n const shortPath = path. resolve (base). replace (path. resolve ( ' ./src ' ), ' @ ' )\n return {\n name,\n import : `import ${ name } from \" ${ shortPath } / ${ c } \"`\n }\n })\n // [ { name: 'HelloWorld', import: 'import HelloWorld from \"@/components/HelloWorld.vue\"' } ]\n b. Find the template tags To understand what components are being used, we need to have our new loader to compile the SFC