# Building DailyWage: A Real-Time Salary Counter with a Corgi Companion - URL: https://easonchang.com/posts/dailywage - Date: 2026-07-29T00:00:00.000Z - Language: en - Description: DailyWage is a free iOS app from Aburi Studio that shows your salary ticking up every 100 ms, with a pixel corgi companion, tap-to-save goals, and widgets. No backend — your data never leaves your device. ![DailyWage — watch your salary tick](/images/dailywage/dailywage-og-en.png) ## Introduction **[DailyWage](https://dailywage.aburi.app)** is the third product [Carol](https://carolhsiao.com) and I built at **[Aburi Studio](https://aburistudio.com)** — the smallest and most playful one, and weirdly, the one with some of the most interesting engineering. It answers a question every office worker has asked at 3 p.m. on a Wednesday: "How much have I actually earned *so far today*?" (In Chinese it's called **天天開薪** — a pun on 天天開心, "happy every day".) ## Demo - **Website:** [dailywage.aburi.app](https://dailywage.aburi.app) - **App Store:** [free download](https://apps.apple.com/app/id6758375748) (iOS; Android is on the roadmap) - **Languages:** English / Traditional Chinese ## Why we created this project People work for their goals and dreams, but the daily grind makes it easy to want to give up. Your salary is a number that appears at the end of the month — too far removed from these three hours this afternoon. What if you could *see* time turning into money, every single second? Would the workday feel a little more worth it? So we built a small, comforting tool: enter your monthly salary and working hours, and it updates your earnings **every 100 milliseconds**, four decimal places and all, while a pixel-art corgi keeps you company. Outside working hours and on weekends, the corgi sleeps. ## Key Features - **Real-time earnings counter:** ticks every 100 ms while you watch your money grow. - **Tap-to-save goals:** create goals — "Trip to Japan", an emergency fund — and tap a card to allocate today's earnings into it — long-press to allocate continuously. Completed goals get stamped with a traditional ink-seal animation. - **Monthly rhythm:** a monthly progress bar fills as the month goes on, and goals marked as fixed expenses (rent!) reset automatically on the 1st. - **Widgets:** today's earnings and monthly progress tick along on your home screen and lock screen — with working, sleeping, and celebrating corgi variants. - **100% private:** no accounts, no cloud. All financial data stays on your device, and the app is completely free with no ads. ## Technologies DailyWage is an Expo (React Native) app with SwiftUI widgets — and deliberately **no backend**: everything stays on your device. My favorite development detail: you can't QA a watch-your-salary-all-day app in real time, so dev mode can run the clock at 720× — a full 24-hour workday plays out in two minutes. ## Where it's at DailyWage is [live on the App Store](https://apps.apple.com/app/id6758375748), completely free with no ads, and Android is on the roadmap. If your workday could use a corgi and a satisfying number going up, give it a try — and if it makes you smile at 3 p.m. on a Wednesday, it did its job. DailyWage is part of a small family of tools we're building at Aburi Studio around time and money — alongside **CoreHour**, a minimalist time-boxing app ([read about it here](/posts/corehour)), and **FireFree**, a FIRE planning app ([read about it here](/posts/firefree)). If you have feedback, find me on [X](https://x.com/easondev) or [email me](mailto:eason@easonchang.com) — I read everything. --- # 打造「天天開薪」:柯基陪你上班、薪水一秒一秒進帳的療癒小 App - URL: https://easonchang.com/zh-TW/posts/dailywage - Date: 2026-07-29T00:00:00.000Z - Language: zh-TW - Description: 天天開薪(DailyWage)是我們在 Aburi Studio 打造的免費 iOS App:每 100 毫秒更新你今天賺到的薪水,一隻像素柯基陪你上班,還能輕觸把收入分配進存錢目標。沒有後端,資料完全不離開你的手機。 ![天天開薪 — 看著薪水一秒一秒進帳](/images/dailywage/dailywage-og-zh-TW.png) ## 簡介 **[天天開薪](https://dailywage.aburi.app)**(DailyWage)是我和 [Carol](https://carolhsiao.com) 在 **[Aburi Studio](https://aburistudio.com)** 打造的第三個產品——最小、最玩心大發,但意外地,也是工程細節最有趣的一個。 它回答每個上班族週三下午三點都問過的問題:「到現在為止,我今天到底賺了多少錢?」名字是「天天開心」的諧音——我們希望上班的煩悶裡,至少有個東西讓你每天開薪(心)😆 ## Demo - **官網:** [dailywage.aburi.app](https://dailywage.aburi.app) - **App Store:** [免費下載](https://apps.apple.com/app/id6758375748)(iOS;Android 在路線圖上) - **語言:** 繁體中文 / English ## 為什麼我們要開發這個專案 我們發現大家是為了目標跟夢想而上班,但上班帶來的煩悶跟厭倦感,卻讓人很想放棄。 薪水是月底才出現的一個數字,跟今天下午這三個小時的關係太遙遠了。如果能*看見*時間正在變成錢——每一秒都在——上班會不會有動力一點? 於是我們做了一個療癒小工具:輸入月薪和上班時間,它就**每 100 毫秒**更新一次你今天賺到的錢,小數點後四位都給你看,旁邊一隻像素柯基陪你上班。下班時間和週末,柯基會睡覺。 ## 主打功能 - **即時薪水計算:** 每 100 毫秒跳動一次,看著自己賺到的錢一點一點長大。 - **輕觸存錢目標:** 建立「日本旅遊」、緊急預備金等目標,點一下卡片就把今天賺到的錢分配進去,長按還能連續分配。目標達成時會蓋上一顆篆刻印章動畫。 - **每月的節奏:** 本月累積進度條慢慢填滿;標成「每月固定支出」的目標(房租!)每月 1 號自動歸零。 - **桌面小工具:** 今日收入和本月進度直接在主畫面與鎖定畫面上跳動,柯基也有上班、睡覺、慶祝三種型態。 - **100% 隱私:** 沒有帳號、沒有雲端,所有財務資料只存在你的裝置上,完全免費、沒有廣告。 ## 技術架構 天天開薪是用 Expo(React Native)打造的 App,搭配 SwiftUI 小工具,而且**刻意不做後端**——所有資料只存在你的裝置上。我最喜歡的開發細節:「盯著薪水跳一整天」沒辦法用真實時間 QA,所以開發者模式可以把時鐘加速 720 倍,24 小時的工作日兩分鐘演完。 ## 目前進度 天天開薪已經在 [App Store 上架](https://apps.apple.com/app/id6758375748),完全免費、沒有廣告,Android 版在路線圖上。 如果你的上班日需要一隻柯基和一個持續上升的數字,來試試看——如果它讓你在週三下午三點笑出來,它的任務就完成了。 天天開薪是我們在 Aburi Studio 圍繞「時間與金錢」打造的一系列工具之一——另外還有極簡時間塊 App **CoreHour**([介紹文在這](/posts/corehour))和 FIRE 財務規劃工具 **FireFree**([介紹文在這](/posts/firefree))。 有任何回饋,歡迎在 [X](https://x.com/easondev_tw) 上找我,或直接[寄信給我](mailto:eason@easonchang.com)——每一封我都會看。 --- # FireFree: A FIRE Planning App That Calms Your Money Anxiety - URL: https://easonchang.com/posts/firefree - Date: 2026-01-15T00:00:00.000Z - Language: en - Description: FireFree is the FIRE planning app we built at Aburi Studio: no expense logging, no bank linking — just track your net worth and see the day you reach financial freedom. Free to sign up, with a no-account demo mode. ![FireFree — Calm your money anxiety](/images/firefree/firefree-og-en.png) ## Introduction The second product we're building at **[Aburi Studio](https://aburistudio.com)** is the one closest to my own life: **[FireFree](https://firefree.app)**, a FIRE (Financial Independence, Retire Early) planning app. It answers one deceptively simple question: "If I keep living the way I live now, when can I stop working?" ## Demo - **Website:** [firefree.app](https://firefree.app) - **Demo mode:** no sign-up needed — enter the full product from the landing page, with six preset financial scenarios to explore - **Languages:** full English / Traditional Chinese support ## Why we created this project FIRE sounds like a dream until you turn it into numbers. Then it becomes something much more useful: a plan. [Carol](https://carolhsiao.com) and I have been planning our own financial future, and spreadsheets only take you so far: they don't show momentum, and they don't answer the only question that really matters — *when?* We also noticed that money anxiety rarely comes from not knowing what you spent last Tuesday. It comes from **not being able to see the future**. You save a few hundred dollars a month with no idea whether it's enough; your banking app shows a cold balance and nothing tells you how long it would last. So FireFree deliberately rejects two things: **no expense logging** (update your assets once a month, in minutes) and **no bank linking** (enter rough numbers yourself — your data stays yours). What you get in return is a map of your future. ## Key Features - **FIRE forecast:** your income, expenses, assets, and debts, with compound growth and inflation applied — drawn as the curve where your net worth crosses your FIRE number, with your projected FIRE age on it. Based on the classic 4% rule; multiplier, withdrawal rate, and inflation are all adjustable. - **Asset tracking without bookkeeping:** savings, investments, real assets, and debts, each with value history. Watching the curve climb is genuinely satisfying. - **Life goals you can toggle:** add "buy a home" or "one big trip a year", flip each goal on and off, and watch your FIRE date shift. Abstract trade-offs become a visible curve. - **Automatic stock price sync:** link holdings to tickers (US and Taiwan markets) and daily jobs keep valuations fresh. - **Multi-currency:** accounts in USD, TWD, EUR… converted into one display currency. - **Privacy mode:** one eye icon turns every amount into `$•,•••.••` — share your screen without sharing your net worth. ## Technologies FireFree is a Next.js web app — a PWA that works offline — backed by Supabase, and all the FIRE math lives in one shared TypeScript package so every screen shows exactly the same numbers. The deeper engineering stories deserve their own posts someday. ## Where it's at FireFree is live and open for free sign-ups — there's a free plan you can stay on. An iOS app is in the works. If FIRE is on your mind — whether you're deep into the movement or just want to know your number — [playing with the demo](https://firefree.app) is the fastest way in: no account, pick a scenario, explore the full product. FireFree is part of a small family of tools we're building at Aburi Studio around time and money — alongside **CoreHour**, a minimalist time-boxing app ([read about it here](/posts/corehour)), and **DailyWage**, a real-time salary counter ([read about it here](/posts/dailywage)). If you have feedback, find me on [X](https://x.com/easondev) or [email me](mailto:eason@easonchang.com) — I read everything. --- # FireFree:告別理財焦慮的 FIRE 財務規劃工具 - URL: https://easonchang.com/zh-TW/posts/firefree - Date: 2026-01-15T00:00:00.000Z - Language: zh-TW - Description: FireFree 是我們在 Aburi Studio 打造的 FIRE 財務規劃工具:不用記帳、不用串接銀行,就能追蹤淨資產、預測你達成財務自由的那一天。已開放免費註冊,也可以直接玩 Demo 模式。 ![FireFree — 告別理財焦慮](/images/firefree/firefree-og-zh-TW.png) ## 簡介 我們在 **[Aburi Studio](https://aburistudio.com)** 打造的第二個產品,也是和我自己的人生最相關的一個:**[FireFree](https://firefree.app)**,一個 FIRE(財務獨立、提早退休)財務規劃工具。 它想回答一個看似簡單的問題:「照現在這樣過日子,我到底哪一天可以不用再為錢工作?」 ## Demo - **官網:** [firefree.app](https://firefree.app) - **Demo 模式:** 不用註冊,從官網就能進入完整產品,內建六種財務情境隨你玩 - **語言:** 繁體中文 / English 完整支援 ## 為什麼我們要開發這個專案 FIRE 在化成數字之前,聽起來像個遙遠的夢。但一旦變成數字,它就成了更有用的東西:一個計畫。 我和 [Carol](https://carolhsiao.com) 一直在規劃自己的財務未來,但試算表能做的有限:它看不出動能,也回答不了唯一真正重要的問題——*什麼時候?* 我們也發現,對錢的焦慮很少來自「不知道上週二花了多少」,而是來自**看不見未來**。每個月省下幾百塊,卻不知道到底夠不夠退休;打開銀行 App 只有冰冷的餘額,沒有人告訴你這筆錢能撐多久。 所以 FireFree 刻意放棄了兩件事:**不記帳**(每月花幾分鐘更新一次資產就好),**不串接銀行**(數字自己填大概的就行,資料完全屬於你)。換來的是一張未來的地圖。 ## 主打功能 - **財務自由預測:** 根據收支、資產與負債,套上複利與通膨,畫出淨資產曲線與 FIRE 目標曲線的交會點,直接標出你預計幾歲 FIRE。計算基礎是經典的 4% 法則,倍數、提領率、通膨率全部可調。 - **不用記帳的資產追蹤:** 存款、投資、實體資產、負債四大類,每筆都有歷史紀錄,看著淨資產慢慢攀升很有成就感。 - **可以開關的人生目標:** 新增「買房」、「每年一趟大旅行」,在圖表上開開關關,看 FIRE 日期怎麼移動。抽象的取捨變成看得見的曲線。 - **股價自動同步:** 持股綁定代號(支援美股與台股),每天自動更新市值。 - **多幣別支援:** USD、TWD、EUR 帳戶通通換算成同一個顯示幣別。 - **隱私模式:** 點一下眼睛圖示,全 App 金額變成 `$•,•••.••`,開會投影不怕曝光淨資產。 ## 技術架構 FireFree 的 Web 版用 Next.js 打造(支援離線的 PWA),後端是 Supabase;所有 FIRE 數學都放在同一個共用的 TypeScript package 裡,每個畫面顯示的數字保證一致。更深入的工程故事,之後再另外寫文章分享。 ## 目前進度 FireFree 已經上線,開放免費註冊,也有免費方案可以一直用。iOS 版開發中。 如果你也在想 FIRE 這件事——無論你已經深入研究、還是只想知道自己的「FIRE 數字」——[直接玩玩 Demo](https://firefree.app) 是最快的方式:不用註冊,選一個財務情境就能體驗完整產品。 FireFree 是我們在 Aburi Studio 圍繞「時間與金錢」打造的一系列工具之一——另外還有極簡時間塊 App **CoreHour**([介紹文在這](/posts/corehour))和薪水即時計算器 **天天開薪**([介紹文在這](/posts/dailywage))。 有任何回饋,歡迎在 [X](https://x.com/easondev_tw) 上找我,或直接[寄信給我](mailto:eason@easonchang.com)——每一封我都會看。 --- # Building CoreHour: A Time-Boxing App Around the 80/20 Rule - URL: https://easonchang.com/posts/corehour - Date: 2025-09-01T00:00:00.000Z - Language: en - Description: CoreHour is a minimalist time-boxing app from Aburi Studio that helps you focus on the 20% of tasks that drive 80% of results. What it does, why we built it, and where it's headed. ![CoreHour — Do less. Achieve more.](/images/corehour/corehour-og.png) ## Introduction For the past while, [Carol](https://carolhsiao.com) and I have been building products together under **[Aburi Studio](https://aburistudio.com)**, our two-person studio based in Calgary. The first product I want to introduce here is **[CoreHour](https://corehour.app)** — a minimalist time-boxing app built around one idea: the 80/20 rule. Its philosophy is printed right on the landing page: **"Do Less. Achieve More."** ## Demo - **Website:** [corehour.app](https://corehour.app) - **Web app:** [app.corehour.app](https://app.corehour.app) — 7-day free trial, no credit card - **iOS app:** in the works ## Why we created this project Most productivity tools are great at collecting tasks, and terrible at telling you a harder truth: not all tasks deserve your time. A day can be completely full and still accomplish nothing that matters. The Pareto principle says roughly 20% of your tasks drive 80% of your results. The hard part is *seeing* which 20% that is — and actually protecting time for it. We were our own victims: growing task lists, stalled important work. So CoreHour asks you to do just three things: set your focus, timebox your day, and track your focus hours. ## Key Features - **Plan your day visually:** drag and drop tasks into a timeline of 30-minute blocks, with color-coded categories, so you can see your entire day at a glance. - **Mark what's critical:** flag your high-impact tasks, and track how much of your time goes to them versus busy work. - **Automatic conflict handling:** overlapping time boxes get trimmed, split, or merged automatically — no manual shuffling. - **Learn from your own data:** a daily focus-hours goal, streaks, and a weekly breakdown of where your hours really went. - **Stay out of your way:** a clean, thoughtfully crafted interface with light and dark modes. One detail I especially like: the streak has three states, not two. Because CoreHour is a *planner*, qualifying days count backwards and **forwards** — future days can already be planned. If today isn't planned yet but yesterday kept the streak alive, you're in a "planning" state and the UI nudges you instead of showing a broken streak. That's it. No project management ceremony, no team features you'll never use. It's a daily planning tool for one person: you. ## Technologies Under the hood, CoreHour is a Next.js web app and an Expo-based iOS app sharing one Supabase backend, with subscriptions handled by RevenueCat. I'll save the deeper engineering stories for future posts. ## Where it's at CoreHour is live at **[corehour.app](https://corehour.app)**, with a 7-day trial that needs no credit card. An iOS app is in the works. If time boxing or the 80/20 rule ever appealed to you, I'd love for you to try it and tell me what's missing. CoreHour is part of a small family of tools we're building at Aburi Studio around time and money — alongside **FireFree**, a FIRE planning app ([read about it here](/posts/firefree)), and **DailyWage**, a real-time salary counter ([read about it here](/posts/dailywage)). If you have feedback, find me on [X](https://x.com/easondev) or [email me](mailto:eason@easonchang.com) — I read everything. --- # 打造 CoreHour:圍繞 80/20 法則設計的時間塊 App - URL: https://easonchang.com/zh-TW/posts/corehour - Date: 2025-09-01T00:00:00.000Z - Language: zh-TW - Description: CoreHour 是我們在 Aburi Studio 打造的極簡時間塊(Time Boxing)App,幫助你專注在驅動 80% 成果的那 20% 任務上。介紹它能做什麼、為什麼做它,以及接下來的計畫。 ![CoreHour — 做得更少,完成更多](/images/corehour/corehour-og.png) ## 簡介 這陣子,我和 [Carol](https://carolhsiao.com) 一起以 **[Aburi Studio](https://aburistudio.com)** 的名義打造產品——這是我們兩人在加拿大 Calgary 成立的小工作室。第一個想在這裡介紹的產品是 **[CoreHour](https://corehour.app)**:一款圍繞著「80/20 法則」設計的極簡時間塊(Time Boxing)App。 它的哲學直接印在官網上:**「Do Less. Achieve More.」** ## Demo - **官網:** [corehour.app](https://corehour.app) - **Web 版:** [app.corehour.app](https://app.corehour.app),7 天免費試用、不用信用卡 - **iOS 版:** 開發中 ## 為什麼我們要開發這個專案 大多數生產力工具都很擅長「收集任務」,卻不擅長告訴你一個更殘酷的事實:不是所有任務都值得你的時間。行程可以排得滿滿的,卻沒完成任何真正重要的事。 帕雷托法則(Pareto Principle)說:大約 20% 的任務,驅動了 80% 的成果。難的是*看見*那 20% 是什麼——並且真的為它保留時間。 我們自己就是受害者:待辦清單越長越長,重要的事卻一直卡著。所以 CoreHour 只要求你做三件事:設定焦點、把一天裝進時間塊、追蹤專注時數。 ## 主打功能 - **視覺化規劃你的一天:** 把任務拖放進 30 分鐘一格的時間軸,搭配顏色分類,一眼看懂整天的安排。 - **標記關鍵任務:** 把高影響力的任務標記出來,追蹤你的時間花在關鍵工作還是瞎忙上。 - **自動衝突處理:** 時間塊重疊時自動截短、切割或合併,不用手動喬位置。 - **從自己的數據中學習:** 每日專注時數目標、連續紀錄(streak)、一週時間分配分析。 - **不打擾你:** 乾淨、細緻的介面,支援明亮與暗黑模式。 有個細節我特別喜歡:streak 有三種狀態,不是兩種。因為 CoreHour 是「規劃」工具,合格天數往回也**往前**算——未來的日子可以先排好。今天還沒達標但昨天有守住時,你會處在「規劃中」狀態,介面會提醒你把今天排好,而不是給你看一條斷掉的 streak。 就這樣。沒有繁瑣的專案管理流程,也沒有你永遠用不到的團隊功能。它是為一個人設計的每日規劃工具:就是你。 ## 技術架構 CoreHour 的 Web 版用 Next.js 打造,iOS 版用 Expo,背後共用同一套 Supabase 後端,訂閱金流則交給 RevenueCat。更深入的工程故事,之後再另外寫文章分享。 ## 目前進度 CoreHour 已經上線,網址是 **[corehour.app](https://corehour.app)**,7 天試用不用信用卡。iOS 版開發中。 如果你曾對時間塊或 80/20 法則感興趣,非常歡迎你試用,然後告訴我還缺什麼。 CoreHour 是我們在 Aburi Studio 圍繞「時間與金錢」打造的一系列工具之一——另外還有 FIRE 財務規劃工具 **FireFree**([介紹文在這](/posts/firefree))和薪水即時計算器 **天天開薪**([介紹文在這](/posts/dailywage))。 有任何回饋,歡迎在 [X](https://x.com/easondev_tw) 上找我,或直接[寄信給我](mailto:eason@easonchang.com)——每一封我都會看。 --- # Equation Pyramid - A Math Puzzle Game Inspired by The Devil's Plan - URL: https://easonchang.com/posts/equation-pyramid - Date: 2025-07-27T00:00:00.000Z - Language: en - Description: Equation Pyramid is an open-source 3D math puzzle game inspired by Netflix's The Devil's Plan: arrange arithmetic operations to form valid equations. Built with Next.js and React Three Fiber — playable right in your browser. ![Equation Pyramid - Math Puzzle Game](/images/projects/equation-pyramid.jpg) ## Introduction **Equation Pyramid** is an interactive math puzzle game inspired by the Netflix reality show *The Devil's Plan*: given a set of numbers, arrange arithmetic operations to form valid equations. It's an open-source project [Carol](https://carolhsiao.com) and I built together — she designed the game experience and interface, and I turned it into a 3D game you can play right in the browser. ## Demo & Source Code - **Play now:** [equation-pyramid.carolhsiao.com](https://equation-pyramid.carolhsiao.com) - **Source code:** [github.com/carolhsiaoo/equation-pyramid](https://github.com/carolhsiaoo/equation-pyramid) No download needed — it runs in your browser. ## Why we created this project After watching *The Devil's Plan*, we were hooked on the Equation Pyramid game: simple rules (form equations from numbers), deep strategy (the same numbers combine in many ways), and dangerously addictive. The problem: there was nowhere to play it online. So we built one ourselves — and open-sourced it so anyone who loves the game can play it in the browser. ## Key Features - **Strategic challenge:** arrange arithmetic operations into valid equations — every position matters. - **Instant validation:** equations are checked automatically, so you know right away whether you got it. - **3D pyramid:** the pyramid comes alive as you solve, making the puzzle feel immersive. - **Any device:** works on desktop, tablet, and phone. ## Technologies Equation Pyramid is a web game built with [Next.js](https://nextjs.org/), TypeScript, and [React Three Fiber](https://docs.pmnd.rs/react-three-fiber/), deployed on Vercel. If you're curious about 3D rendering in React or game logic, the whole codebase is on GitHub — dig in. ## Closing Ready to test your math instincts? Head to [equation-pyramid.carolhsiao.com](https://equation-pyramid.carolhsiao.com) and play. If you enjoy it, a star on [GitHub](https://github.com/carolhsiaoo/equation-pyramid) makes our day — and if you find a bug or have an idea, open an issue or leave a comment below! --- # Equation Pyramid - 靈感來自《魔鬼的計謀》的數學解謎遊戲 - URL: https://easonchang.com/zh-TW/posts/equation-pyramid - Date: 2025-07-27T00:00:00.000Z - Language: zh-TW - Description: Equation Pyramid 是一款受 Netflix《魔鬼的計謀》啟發的開源 3D 數學解謎遊戲:用加減乘除排列出正確的等式。使用 Next.js 與 React Three Fiber 打造,直接在瀏覽器就能玩。 ![Equation Pyramid - 數學解謎遊戲](/images/projects/equation-pyramid.jpg) ## 簡介 **Equation Pyramid** 是一款互動式數學解謎遊戲,靈感來自 Netflix 實境秀《魔鬼的計謀》(The Devil's Plan):在一組數字裡,用加減乘除排列出正確的等式。 這是我和 [Carol](https://carolhsiao.com) 一起打造的開源專案——她負責遊戲體驗與介面設計,我負責把它實作成能在瀏覽器裡直接玩的 3D 遊戲。 ## Demo & 程式碼 - **立即遊玩:** [equation-pyramid.carolhsiao.com](https://equation-pyramid.carolhsiao.com) - **原始碼:** [github.com/carolhsiaoo/equation-pyramid](https://github.com/carolhsiaoo/equation-pyramid) 不用下載,打開瀏覽器就能玩。 ## 為什麼我們要開發這個專案 看完《魔鬼的計謀》之後,我們完全被 Equation Pyramid 這個遊戲迷住:規則簡單(用數字組出等式)、策略很深(同一組數字有很多種組合)、而且非常上癮。 問題是:網路上找不到地方玩。所以我們決定自己做一個,並且把它開源,讓喜歡這個遊戲的人都能直接在瀏覽器裡玩。 ## 主打功能 - **策略挑戰:** 運用加減乘除排列出正確等式,每個數字擺放的位置都很重要。 - **即時驗證:** 自動檢查等式是否成立,立刻知道答對還是答錯。 - **3D 金字塔:** 金字塔會隨著你的解題進度動起來,解謎的過程更有沉浸感。 - **跨裝置:** 桌機、平板、手機都能玩。 ## 技術架構 Equation Pyramid 是用 [Next.js](https://nextjs.org/)、TypeScript 與 [React Three Fiber](https://docs.pmnd.rs/react-three-fiber/) 打造的網頁遊戲,部署在 Vercel 上。對 React 3D 渲染或遊戲邏輯有興趣的話,整份程式碼都在 GitHub 上,歡迎直接翻原始碼。 ## 結語 準備好挑戰你的數學直覺了嗎?打開 [equation-pyramid.carolhsiao.com](https://equation-pyramid.carolhsiao.com) 就能直接玩。 喜歡的話,歡迎到 [GitHub](https://github.com/carolhsiaoo/equation-pyramid) 給我們一顆星星;發現 bug 或有新點子,也歡迎開 issue,或直接在下面留言告訴我! --- # FocusZone - A visionOS Focus App with AI Distraction Detection - URL: https://easonchang.com/posts/focuszone - Date: 2024-09-15T00:00:00.000Z - Language: en - Description: FocusZone is a focus and productivity app for Apple Vision Pro: a Pomodoro timer paired with AI distraction detection (phone appearance, noise, head movement) that guards your attention inside immersive spaces. ![The FocusZone welcome card floating in an immersive space](/images/focuszone/focuszone-welcome.png) ## Introduction **FocusZone** is a focus and productivity app we built for Apple Vision Pro: run a Pomodoro timer inside an immersive space, with AI watching for distractions so your attention stays on the work. It's an exploration of spatial computing that [Carol](https://carolhsiao.com) and I did together — she designed the spatial UI/UX, and I handled the visionOS implementation. ## Demo & Source Code - **Source code:** [github.com/eason-dev/FocusZone](https://github.com/eason-dev/FocusZone) - **Platform:** Apple Vision Pro (visionOS 2) ## Why we created this project When Vision Pro launched, we immediately saw its potential for focused work: immersive environments naturally block out physical distractions, and its rich sensors understand your working context. But traditional productivity apps weren't designed for spatial computing. We wanted a focus tool that actually uses what Vision Pro can do — not just a timer floating in 3D, but a device that actively helps you notice when you've drifted. ## Key Features - **Spatial Pomodoro timer:** the timer floats in your immersive space — always visible, never in the way. - **AI distraction detection:** your phone entering your view, a sudden spike of noise, frequent head turning — FocusZone gives you a gentle nudge back to work, never a punishment. - **Immersive environments:** from a minimal void to natural scenes, pick the space that puts you in flow. - **Focus analytics:** track focus hours and distraction patterns to learn what actually breaks your concentration. ![The Pomodoro timer floating in space](/images/focuszone/focuszone-timer.png) ![Customizing focus duration and AI detection: head movement, phone, sound](/images/focuszone/focuszone-customize.png) ## Technologies FocusZone is built with SwiftUI and RealityKit on visionOS 2. Distraction detection runs entirely on-device — nothing leaves your headset. ## Closing FocusZone is an open-source exploration — the code lives on [GitHub](https://github.com/eason-dev/FocusZone). If you have a Vision Pro, or you're curious about productivity in spatial computing, browse the code, open an issue, or leave a comment below! --- # FocusZone - 結合 AI 干擾偵測的 visionOS 專注 App - URL: https://easonchang.com/zh-TW/posts/focuszone - Date: 2024-09-15T00:00:00.000Z - Language: zh-TW - Description: FocusZone 是為 Apple Vision Pro 打造的專注生產力 App:番茄鐘搭配 AI 干擾偵測(手機出現、噪音、頭部轉動),在沉浸式空間裡幫你守住專注力。 ![FocusZone 的歡迎畫面,漂浮在沉浸式空間中](/images/focuszone/focuszone-welcome.png) ## 簡介 **FocusZone** 是我們為 Apple Vision Pro 打造的專注生產力 App:在沉浸式空間裡跑番茄鐘,並用 AI 偵測干擾,幫你把注意力留在工作上。 這是我和 [Carol](https://carolhsiao.com) 對空間計算(Spatial Computing)的一次探索——她設計了整套空間 UI/UX,我負責 visionOS 上的實作。 ## Demo & 程式碼 - **原始碼:** [github.com/eason-dev/FocusZone](https://github.com/eason-dev/FocusZone) - **平台:** Apple Vision Pro(visionOS 2) ## 為什麼我們要開發這個專案 Vision Pro 發布時,我們立刻看到它在「專注工作」上的潛力:沉浸式環境天生能擋掉實體干擾,豐富的感測器則能感知你的工作情境。 但傳統的生產力 App 並不是為空間計算設計的。我們想做一個真正發揮 Vision Pro 能力的專注工具——不只是把計時器搬進 3D 空間,而是讓裝置主動幫你發現分心。 ## 主打功能 - **空間番茄鐘:** 計時器漂浮在沉浸空間中,隨時看得到、卻不干擾你。 - **AI 干擾偵測:** 手機出現在視野裡、環境突然出現噪音、頭部頻繁轉動——FocusZone 會溫和提醒你回到工作上,而不是懲罰你。 - **沉浸式環境:** 從極簡空間到自然場景,挑一個最能讓你進入心流的環境。 - **專注分析:** 追蹤專注時數與干擾模式,了解到底是什麼打斷了你。 ![漂浮在空間中的番茄鐘計時器](/images/focuszone/focuszone-timer.png) ![自訂專注時長與 AI 偵測項目:頭部移動、手機偵測、聲音偵測](/images/focuszone/focuszone-customize.png) ## 技術架構 FocusZone 使用 SwiftUI 與 RealityKit 在 visionOS 2 上開發,干擾偵測直接在裝置上完成,不會把資料傳到任何地方。 ## 結語 FocusZone 是一個開源的探索專案,原始碼都在 [GitHub](https://github.com/eason-dev/FocusZone) 上。如果你也有 Vision Pro,或對空間計算的生產力應用感興趣,歡迎翻翻程式碼、開 issue 跟我們聊聊,或直接在下面留言! --- # Timez - Time Zone Converter: Convert time between different timezones - URL: https://easonchang.com/posts/timezone-converter - Date: 2024-03-06T00:00:00.000Z - Language: en - Description: Easily convert time between different timezones and compare overlapping time periods ![Time Zone Converter project screenshot](/images/project-timezone-converter/timezone-converter-screenshot.png) ## Introduction "Timez" project is a timezone converter web app that allows users to convert time between different timezones and compare overlapping time periods. I created this project with [Carol Hsiao](https://carolhsiao.com). She designed the whole UI and I implemented it using Next.js and Tailwind CSS. ## Demo & Source Code "Timez" a open-source project, you can check the demo and source code here: - **Demo:** [timez.eason.ch](https://timez.eason.ch) - **Source Code:** [github.com/eason-dev/timezone-converter](https://github.com/eason-dev/timezone-converter) ## Why we created this project We created this project because we just moved from Taiwan to Canada. The timezone offset between Taiwan (UTC+8) and Canada(UTC-7) is 15 hours. We found it difficult for our parents and friends in Taiwan to find a good time to call us. So we wanted to create a tool that makes it easy to convert time between different timezones and compare overlapping time periods. That's why We created the Time Zone Converter. ## Key Features - **Time Zone Conversion:** Convert time between any two time zones effortlessly. - **Overlap Comparison:** Compare overlapping time periods to find the best meeting times. - **Dark Mode Support:** A user-friendly interface that supports dark mode, making it easy on the eyes during late-night planning sessions. ## Technologies I used my favorite tech stack to build this project: - [Next.js v14](https://nextjs.org/): React.js meta-framework for server-side rendering and efficient routing - [TypeScript](https://www.typescriptlang.org/): Static type checking for JavaScript - [Tailwind CSS](https://tailwindcss.com/): Utility-first CSS framework for rapid UI development - [shadcn/ui](https://ui.shadcn.com/): Beautiful and functional UI components - [luxon](https://moment.github.io/luxon/): Date and time manipulation library - [@vvo/tzdb](https://github.com/vvo/tzdb): Timezone data library - [@dnd-kit](https://dndkit.com/): Intuitive drag and drop functionality for user interaction - [Vercel](https://vercel.com/): Website hosting - [Figma](https://www.figma.com/): UI/UX design ## Screenshots ![Time Zone Converter project demo](/images/project-timezone-converter/timezone-converter-demo.gif) ## Conclusion We hope you find the Time Zone Converter helpful for managing time across different time zones. If you have any feedback or suggestions, please feel free to contact us by leave a comment below. ## References Prior art and inspiration for this project: - [Time Zone Converter - Time Difference Calculator](https://www.timeanddate.com/worldclock/converter.html) - [Timezone Converter, Meeting Planner - Timezone Wizard](https://timezonewizard.com/) - [Every Time Zone: time zone converter, compare time zone difference and find best time for a meeting with one click](https://everytimezone.com/) - [Time zone overlap](https://timezoneoverlap.com/?z=Pacific%2FGuadalcanal%2CAmerica%2FLos_Angeles%2CAmerica%2FNew_York%2CAustralia%2FSydney) - [Remocle - Timezone converter & visualizer](https://remocle.com/) --- # Timez - Time Zone Converter 時區轉換工具 - URL: https://easonchang.com/zh-TW/posts/timezone-converter - Date: 2024-03-06T00:00:00.000Z - Language: zh-TW - Description: 輕鬆在不同時區之間轉換時間並比較重疊時間段 ![時區轉換工具頁面截圖](/images/project-timezone-converter/timezone-converter-screenshot.png) ## 專案簡介 「Timez - Time Zone Converter 時區轉換工具」是一個網頁應用程式,幫助你輕鬆在不同時區之間轉換時間並比較重疊時間段。 我與 [Carol Hsiao](https://carolhsiao.com) 一起開發了這個專案,她用 Figma 設計了整個 UI,而我使用 Next.js 和 Tailwind CSS 將它實作出來。 ## Demo & 程式碼 Timez 是一個開源專案,你可以在這裡玩玩 Demo 網站和程式碼: - **Demo:**[timez.eason.ch](https://timez.eason.ch) - **程式碼:**[github.com/eason-dev/timezone-converter](https://github.com/eason-dev/timezone-converter) ## 為什麼我們要開發這個專案 我們最近剛從台灣搬到加拿大。台灣(UTC+8)和加拿大(UTC-7)之間有 15 小時的時差。 我們發現在台灣的家人和朋友常常不知道現在在加拿大是幾點,很難找到一個合適的時間來打電話給我們。 因此我們想開發一個小工具,幫助他們更容易在不同時區之間轉換時間、和比較重疊的時間段。 這就是為什麼我們決定開發「Time Zone Converter 時區轉換工具」。 ## 主打功能 - **時區轉換:**輕鬆地在任何時區之間轉換時間。 - **晝夜時間比較:**比較重疊的晝夜時間段,以找到最合適的溝通時間。 - **深色模式支持:**UI 支援深色模式! ## 技術架構 我使用了我最喜歡的 Tech Stack 來開發這個專案: - [Next.js v14](https://nextjs.org/):開發體驗極佳的 React.js Framework,支援 Server Side Rendering 以利於 SEO - [TypeScript](https://www.typescriptlang.org/):加上型別的 JavaScript,讓長期維護更容易 - [Tailwind CSS](https://tailwindcss.com/):用於快速 UI 開發的 CSS 框架 - [shadcn/ui](https://ui.shadcn.com/):美觀且功能全面的 UI 元件 - [luxon](https://moment.github.io/luxon/):用來轉換日期和時間的程式庫 - [@vvo/tzdb](https://github.com/vvo/tzdb):時區列表的程式庫 - [@dnd-kit](https://dndkit.com/):實現可拖曳(Drag and Drop)的介面 - [Vercel](https://vercel.com/):網站架設平台 ## 畫面截圖 ![時區轉換工具操作流程 Gif](/images/project-timezone-converter/timezone-converter-demo.gif) ## 結語 我們希望這個小工具能幫助到你,讓你更輕鬆地在不同時區之間轉換時間,並找到最合適的溝通時間。 如果你有任何建議或問題,歡迎在下方留言讓我們知道,謝謝你的支持和閱讀! --- # Balancing and Improving Readability of Web Titles with react-wrap-balancer - URL: https://easonchang.com/posts/react-wrap-balancer - Date: 2023-02-04T00:00:00.000Z - Language: en - Description: Implement Balance Text with react-wrap-balancer, balancing the width of each line of text for improved visual comfort. Lightweight and easy to use > If you want to balance the width of each line of text in your React web page, making it visually more comfortable, you can use the [react-wrap-balancer](https://react-wrap-balancer.vercel.app/) package to achieve the "Balance Text" effect. > > I will tell you to the features and technical limitations of react-wrap-balancer, and teach you how to install and use it! ## What is the issue with a normal article title? When the text of a web page is too long, it will automatically wrap to the second or third line. For example, the title of [my article](/posts/darkmode-tailwindcss) has three lines: ![Before](/images/2023-02-04-react-wrap-balancer/before.jpg) However, you will find that the third line only has 5 characters, which is visually very unbalanced compared to the first two lines. At this point, you might instinctively want to modify the title by deleting some redundant words. For example, after deleting the words "Dark Mode" the effect is as follows, looking much better: ![Remove some words](/images/2023-02-04-react-wrap-balancer/remove-words.jpg) However, because web pages usually did RWD (responsive web design), the maximum width of the title is different when reading on different devices with different widths. For example, when the reader uses a Surface Duo tablet (screen width of 540px) to read, the unbalanced third line appears again! ![View in Surface Duo tablet](/images/2023-02-04-react-wrap-balancer/view-in-surface-duo.jpg) It's hard to rewrite the title to a perfect length that maintains visual balance in all cases, because there are too many possibilities for devices! Since changing the content is not feasible, what can we do? ## Balance Text You can use the technique of **Balance Text** to dynamically adjust the width of the text block and balance the number of characters on each line. The effect after applying is shown below, the title is more balanced and easier to read! ![After](/images/2023-02-04-react-wrap-balancer/after.jpg) --- ## Implementing balanced text in React with react-wrap-balancer In a React.js project, you can use the **react-wrap-balancer** package to implement balanced text. > Links of react-wrap-balancer: > > Website: [https://react-wrap-balancer.vercel.app/](https://react-wrap-balancer.vercel.app/) > > Github: [https://github.com/shuding/react-wrap-balancer](https://github.com/shuding/react-wrap-balancer) It has the following features: - Extremely small bundle size, only 0.95 kB (Gzipped) - Does not cause [layout shifts (CLS)](https://web.dev/cls/) and does not harm SEO score - Supports Server-Side Render, [Next.js 13 app directory](https://beta.nextjs.org/docs/routing/fundamentals#the-app-directory), and React Server Component - Requires React ≥ 18.0.0 or later, and does not support IE 11 ### react-wrap-balancer Demo There are several interactive demos on the [react-wrap-balancer website](https://react-wrap-balancer.vercel.app/), you can go there and try it out. It is most suitable for use in **titles**, balancing the width of each line to make it more visually comfortable: ![Demo title](/images/2023-02-04-react-wrap-balancer/demo-title.gif) It can also be used in **Chinese and other languages**, and also supports **left-aligned text**: ![Demo Chinese left aligned title](/images/2023-02-04-react-wrap-balancer/demo-chinese-left-align-title.gif) In addition to titles, it is also suitable for use in **Tooltip or Toast text**: ![Demo tooltip](/images/2023-02-04-react-wrap-balancer/demo-tooltip.gif) Or you can apply it to **entire paragraphs**! It is very flexible to use: ![Demo paragraph](/images/2023-02-04-react-wrap-balancer/demo-paragraph.gif) ### Install react-wrap-balancer Installing react-wrap-balancer is very simple, just enter the command and install it using npm: ```bash npm install react-wrap-balancer ``` Next, you can evaluate your scenario, whether you will display multiple balanced texts at the same time on the screen. If so, it is recommended to use the `` of react-wrap-balancer, wrapping the entire React App. Although not necessary, doing so can improve overall performance and reduce HTML size. If you are using Create-React-App, please modify `App.js`, and if you are using Next.js, please change it in `_app.js`: ```tsx:_app.tsx showLineNumbers {3,5} import { Provider as ReactWrapBalancerProvider } from "react-wrap-balancer"; ; ``` ### Use react-wrap-balancer Assuming you want to apply it to the following React component: ```tsx:PageTitle.tsx showLineNumbers export default function PageTitle({ children }) { return (

{children}

); } ``` > Note:This is a simple `

` element with TailwindCSS that I actually use in my blog as post titles. Just import the `` from react-wrap-balancer and wrap the text, and done: ```tsx:PageTitle.tsx showLineNumbers {1,6} import Balancer from "react-wrap-balancer"; export default function PageTitle({ children }) { return (

{children}

); } ``` The effect is as shown as following: ![After](/images/2023-02-04-react-wrap-balancer/after.jpg) It's super easy to use! ## Performance Evaluation The more `` used in a screen, the more exponential the impact on performance will be. This is because each time a `` is calculated, it may affect other `` and require recalculation. The author's performance measurement results are shown in the following image: ![Benchmark](/images/2023-02-04-react-wrap-balancer/benchmark.jpg) - When the page uses **100** ``, the total calculation time is approximately **0.025 seconds** and there is no noticeable effect. - When used with **1000** ``, the total calculation time is approximately **1 second**, which affects the rendering speed of the screen. - When used with **5000** ``, the total calculation time grows exponentially to **35 seconds**, which is excessive. Therefore, it is recommended to only use `` on elements that are truly needed, such as **titles** or **elements that need user interaction to be displayed**, such as **tooltips**. ## Future Possibilities of "Balance Text" **Adobe** and **The New York Times** also have similar projects implementing "Balance Text" effect: - Adobe: [https://github.com/adobe/balance-text](https://github.com/adobe/balance-text) - NYTimes: [https://github.com/nytimes/text-balancer](https://github.com/nytimes/text-balancer) There is currently a CSS proposal in progress, hoping to natively support this effect in CSS: [https://drafts.csswg.org/css-text-4/#text-wrap](https://drafts.csswg.org/css-text-4/#text-wrap)。 Therefore, in the near future, when major browsers are supported, we can easily achieve the "Balance Text" effect using the following method: ```css:style.css {2} .balance-text { text-wrap: balance; } ``` ## Reference - [react-wrap-balancer Website](https://react-wrap-balancer.vercel.app/) - [react-wrap-balancer Github](https://github.com/shuding/react-wrap-balancer) The Gif animation and benchmark image in this article are taken from [react-wrap-balancer Website](https://react-wrap-balancer.vercel.app/)。 --- # 讓網頁標題文字更均衡、好讀,使用 react-wrap-balancer - URL: https://easonchang.com/zh-TW/posts/react-wrap-balancer - Date: 2023-02-04T00:00:00.000Z - Language: zh-TW - Description: 使用 react-wrap-balancer 在 React.js 網頁實作 Balance Text,平衡每一行文字寬度,讓視覺更舒服。輕量化簡單易用,支援 React 18 及 Next.js SSR > 如果你想讓 React 網頁一段文字的每行寬度均衡、視覺更舒服,可以使用 [react-wrap-balancer](https://react-wrap-balancer.vercel.app/) 套件,實現「Balance Text 均衡文字」效果。 > > 我將向你介紹 react-wrap-balancer 的特點及技術限制,並教你如何安裝與使用! ## 正常的文章標題有什麼問題? 網頁的文字太長時,會自動換行到第二、第三行。 例如我的[這篇文章](/posts/darkmode-tailwindcss)標題有三行: ![Before](/images/2023-02-04-react-wrap-balancer/before.jpg) 但你會發現第三行僅有 5 個字,跟前兩行相比視覺上非常不均衡。 此時你可能直覺地想要修改標題,刪掉一些贅字。 例如把「Dark Mode」文字刪掉後,效果如下,看起來好多了: ![Remove some words](/images/2023-02-04-react-wrap-balancer/remove-words.jpg) 但因為通常網頁都會做 RWD 響應式設計處理,在不同寬度的裝置閱讀時,標題的最大寬度也不同。 例如當讀者使用 Surface Duo 平板(螢幕寬度 540px)閱讀時,不平衡的第三行又出現了! ![View in Surface Duo tablet](/images/2023-02-04-react-wrap-balancer/view-in-surface-duo.jpg) 你很難硬把標題改出一個完美的長度,在所有情況下保持視覺均衡,因為裝置可能性太多了! 既然修改內容不可行,那怎麼辦呢? ## Balance Text 均衡文字 你可以使用 **「Balance Text 均衡文字」** 的技術,動態調整文字區塊寬度,平衡每一行的字數。 套用後的效果如下,標題更均衡、更好讀了! ![After](/images/2023-02-04-react-wrap-balancer/after.jpg) --- ## 使用 react-wrap-balancer 在 React 實作均衡文字 在 React.js 專案內,你可以使用 **react-wrap-balancer** 套件來實作均衡文字。 > 官網:[https://react-wrap-balancer.vercel.app/](https://react-wrap-balancer.vercel.app/) > > Github:[https://github.com/shuding/react-wrap-balancer](https://github.com/shuding/react-wrap-balancer) 它有以下特點: - Bundle size 極小,只有 0.95 kB(Gzipped) - 不會造成版面位移([CLS](https://web.dev/cls/)),不傷害 SEO 分數 - 支援 Server-Side Render、[Next.js 13 app directory](https://beta.nextjs.org/docs/routing/fundamentals#the-app-directory)、React Server Component - 需要 React ≥ 18.0.0 以上,並且不支援 IE 11 ### react-wrap-balancer 效果 Demo [react-wrap-balancer 官網](https://react-wrap-balancer.vercel.app/)有數個互動式 demo,你可以先上去玩看看效果, 它最適合用在**標題**,平衡每一行的寬度,讓視覺更舒服: ![Demo title](/images/2023-02-04-react-wrap-balancer/demo-title.gif) 也可以用在**中文等各種語系**上,也支援**置左對齊**的文字: ![Demo Chinese left aligned title](/images/2023-02-04-react-wrap-balancer/demo-chinese-left-align-title.gif) 除了標題,它也適合用在 **Tooltip 或 Toast 文字**上: ![Demo tooltip](/images/2023-02-04-react-wrap-balancer/demo-tooltip.gif) 或是套用**整個文字段落**也可以!使用上非常彈性: ![Demo paragraph](/images/2023-02-04-react-wrap-balancer/demo-paragraph.gif) ### 安裝 react-wrap-balancer react-wrap-balancer 安裝非常簡單,輸入指令,使用 npm 安裝即可: ```bash npm install react-wrap-balancer ``` 接著你可以評估一下你的使用情境,你是否會在畫面上同時顯示多組均衡文字。 如果會的話,建議使用 react-wrap-balancer 的 ``,包住整個 React App。 雖然非必要,但這麼做可以提升整體效能、和減少 HTML 大小。 若你使用 Create-React-App 的話請修改 `App.js`,而使用 Next.js 的話則改在 `_app.js` 裡: ```tsx:_app.tsx showLineNumbers {3,5} import { Provider as ReactWrapBalancerProvider } from "react-wrap-balancer"; ; ``` ### 使用 react-wrap-balancer 假設你想套用它在下面這個 React component: ```tsx:PageTitle.tsx showLineNumbers export default function PageTitle({ children }) { return (

{children}

); } ``` > Note:這是我實際用在部落格內文標題的 component,使用 TailwindCSS 的簡單 `

` 元素 那只要 import react-wrap-balancer 的 ``,把文字包住,就完成了: ```tsx:PageTitle.tsx showLineNumbers {1,6} import Balancer from "react-wrap-balancer"; export default function PageTitle({ children }) { return (

{children}

); } ``` 效果就如同下圖這樣: ![After](/images/2023-02-04-react-wrap-balancer/after.jpg) 使用上超級簡單! ## 渲染效能評估 一個畫面用到越多 `` 時,對效能的影響會是指數性成長。 因為每個 `` 算完時,可能會影響到其他 `` 而需要重算。 作者的效能量測結果如下圖: ![Benchmark](/images/2023-02-04-react-wrap-balancer/benchmark.jpg) 當頁面用到 **100 個** `` 時,整體計算時間大約是 **0.025 秒**,體感不會察覺到。 當用到 **1000 個** `` 時,整體計算時間大約是 **1 秒**,就會影響到畫面渲染速度了。 當用到 **5000 個** `` 時,整體計算時間會指數成長到 **35 秒**,非常誇張。 所以建議只在 **真正需要的元素** 才套用 ``,例如 **標題**、或 **Tooltip 等需要使用者操作才會顯示的元素** 上。 ## 「Balance Text 均衡文字」未來可能性 Adobe 和紐約時報也有類似的專案在實作「Balance Text 均衡文字」效果: - Adobe:[https://github.com/adobe/balance-text](https://github.com/adobe/balance-text) - 紐約時報:[https://github.com/nytimes/text-balancer](https://github.com/nytimes/text-balancer) 而目前也有 CSS 提案正在進行中,希望在 CSS 原生支援這效果:[https://drafts.csswg.org/css-text-4/#text-wrap](https://drafts.csswg.org/css-text-4/#text-wrap)。 因此也許在不久的將來,當各大瀏覽器支援後,我們就能用下列方式輕鬆實現「Balance Text 均衡文字」效果了: ```css:style.css {2} .balance-text { text-wrap: balance; } ``` ## Reference - [react-wrap-balancer 官網](https://react-wrap-balancer.vercel.app/) - [react-wrap-balancer Github](https://github.com/shuding/react-wrap-balancer) 本文 Gif 動畫及 Benchmark 圖片擷取自 [react-wrap-balancer 官網](https://react-wrap-balancer.vercel.app/)。 --- # Summary and Review: The Birth of a Dazzling Tech Blog - Modern Next.js Blog Series #30 - URL: https://easonchang.com/posts/modern-nextjs-blog-summary - Date: 2022-10-15T00:00:00.000Z - Language: en - Description: Summarizing this 30-day journey of building a dazzling personal blog from scratch with modern front-end technologies, revisiting the learned content, implemented features, and used technologies. > This article is also published at [it 邦幫忙 2022 iThome Ironman Contest](https://ithelp.ithome.com.tw/articles/10308942) This is the 30th article in the series "**Building a Dazzling Personal Blog from Scratch Using Modern Technologies like Next.js, ContentLayer, i18next, and More**". Over these 30 days, we have gone from nothing to creating a dazzling tech blog together using modern technology. ## Show me the code & demo! I've open-sourced the outcome of these 30 days into the "[Next.js Tailwind Contentlayer Blog Starter](https://github.com/eason-dev/nextjs-tailwind-contentlayer-blog-starter)" template project on GitHub. ![Next.js Tailwind Contentlayer Blog Starter](https://i.imgur.com/oNtgi9e.png) ### Github repo You can see all the code by visiting the repo URL below: https://github.com/eason-dev/nextjs-tailwind-contentlayer-blog-starter ![Github repo](https://i.imgur.com/vYRUaL3.jpg) Feel free to use it to build your own tech blog. Just click the green "Use this template" or "Fork" button! I'd also appreciate a "Star" to support me if you find this series helpful! If you discover any bugs or want to add more features, feel free to open an issue or PR to collaborate! ### Demo You can play around with it here: [https://nextjs-tailwind-contentlayer-blog-starter.vercel.app/](https://nextjs-tailwind-contentlayer-blog-starter.vercel.app/) > My personal blog [https://easonchang.com/](https://easonchang.com/) is also developed using this template. > The repo is here: [https://github.com/eason-dev/easonchang.com-next](https://github.com/eason-dev/easonchang.com-next). > It contains many more articles related to front-end development, feel free to check it out! ## Looking back at these 30 days, what have we achieved Together, we started from scratch and used the most modern front-end technologies to build a dazzling personal blog from the ground up, covering everything from basic environment setup to final website deployment and maintenance. ### Review of the Series This 30-day journey was divided into three phases: During days 1 to 13, we created a blog with basic functionality that looked good. From day 14 to 19, after completing code blocks, SEO handling, sitemap, and RSS, we met all the conditions for a tech blog. And finally, from day 20 to 30, we continued to add more dazzling and practical features to make our blog stand out! The 30-day itinerary is as follows: (Links to the specific posts within the series) ### Review of Front-end Technologies Used - [Next.js](https://nextjs.org/): A modern full-stack framework - [Vercel](https://vercel.com/home): Website hosting - [Contentlayer](https://github.com/contentlayerdev/contentlayer): Markdown/MDX article processing - [Tailwind CSS](https://tailwindcss.com/): UI styling - [next-i18next](https://github.com/i18next/next-i18next): Multilingual support - [next-seo](https://github.com/garmeeh/next-seo): SEO meta tags - [kbar](https://github.com/timc1/kbar): Command palette - [giscus](https://github.com/giscus/giscus): Comment system - [nprogress](https://github.com/rstacruz/nprogress): Page transition progress bar - [TypeScript](https://www.typescriptlang.org/): More robust JavaScript - [ESLint](https://eslint.org/), [Prettier](https://prettier.io/): Code formatting ### Review of Implemented Features In addition to basic blog functionalities, we also implemented the following cool features: - Writing posts by editing local Markdown / MDX files - Excellent web performance - SEO-friendly - Dark mode - Multilingual support in Chinese and English - Command Palette - Comment system - Page transition progress bar - RSS Feed - Sitemap - Readable code blocks ## Future Prospects I will continue to maintain the "[Next.js Tailwind Contentlayer Blog Starter](https://github.com/eason-dev/nextjs-tailwind-contentlayer-blog-starter)" project, as my personal blog ([easonchang.com](https://easonchang.com/)) is developed based on it. I have many more ideas, and my blog will continue to evolve, as will this project! Below are some features planned for the future, which might continue into articles 31, 32...: (List of planned features) ## About Me I am [Eason Chang](https://easonchang.com/about), a full-stack engineer who loves creating challenges for myself. If you found this series helpful or wish to follow more articles, feel free to follow me through various channels for further discussions and exchanges! > Personal blog: https://easonchang.com/ > > Facebook: https://www.facebook.com/easondev > > Twitter (X): https://x.com/easondev > > Github: https://github.com/eason-dev > > LinkedIn: https://www.linkedin.com/in/easonchang101/ --- # 總結與回顧:一個炫砲技術部落格的誕生 - Modern Next.js Blog 系列 #30 - URL: https://easonchang.com/zh-TW/posts/modern-nextjs-blog-summary - Date: 2022-10-15T00:00:00.000Z - Language: zh-TW - Description: 總結這系列 30 天內從零開始、用現代前端技術打造的炫砲個人部落格,回顧學到的內容、實作出的功能、及用到的技術 > 本文同步發佈於 [it 邦幫忙 2022 iThome 鐵人賽](https://ithelp.ithome.com.tw/articles/10308942) 這是「**從零開始打造炫砲個人部落格,使用 Next.js、ContentLayer、i18next 等現代技術**」系列第 30 篇文章。 在這 30 天裡,我們一起從無到有、使用現代技術、打造出了一個炫砲的技術部落格。 ## Show me the code & demo! 我把這 30 天的成果開源成「[Next.js Tailwind Contentlayer Blog Starter](https://github.com/eason-dev/nextjs-tailwind-contentlayer-blog-starter)」template 專案,並放在 Github 上。 ![Next.js Tailwind Contentlayer Blog Starter](https://i.imgur.com/oNtgi9e.png) ### Github repo 你可以點下面 repo 網址,看到所有程式碼: https://github.com/eason-dev/nextjs-tailwind-contentlayer-blog-starter ![Github repo](https://i.imgur.com/vYRUaL3.jpg) 歡迎使用它來建立你自己的技術部落格,點綠色「Use this template」或「Fork」按鈕就行了! 也希望你能點個「Star」支持我,如果這系列有幫助到你的話! 發現任何 bug 或想加入更多 feature 的話,也歡迎開 issue 和 PR 一起合作! ### Demo 你可以在這裡玩玩它: [https://nextjs-tailwind-contentlayer-blog-starter.vercel.app/](https://nextjs-tailwind-contentlayer-blog-starter.vercel.app/) > 我自己的個人部落格 [https://easonchang.com/](https://easonchang.com/) 也是用這套模板開發的。 > Repo 在這兒:[https://github.com/eason-dev/easonchang.com-next](https://github.com/eason-dev/easonchang.com-next) 。 > 裡面還有更多前端開發相關技術文章,歡迎來逛逛! ## 回顧這 30 天,我們做到了什麼 我們一起在 30 天內從零開始,用最現代的前端技術,從最基礎的環境配置、到最後的網站部署及維護,做出了一個炫砲個人部落格。 ### 回顧系列內容 這 30 天我們經歷了 3 階段迭代: 第 1 ~ 13 天,我們先做出了一個有基本功能、且不錯看的部落格。 第 14 ~ 19 天,做完程式碼區塊、SEO 處理、sitemap、RSS 後,完成一個技術部落格該有的一切條件了。 而最後第 20 ~ 30 天,我們繼續加入更多炫砲且實用的功能,讓我們的部落格脫穎而出! 這 30 天目錄如下: 1. [「從零開始打造炫砲個人部落格」系列簡介](/posts/modern-nextjs-blog-intro) 2. [簡介前端開發生態&建立 Next.js 專案](/posts/frontend-intro-create-nextjs) 3. [將 Next.js 專案部署上 Vercel 平台](/posts/nextjs-to-vercel) 4. [ESLint、Prettier、TypeScript 等 Next.js 專案基礎設定](/posts/nextjs-dev-env) 5. [Markdown 簡介 & 安裝 ContentLayer](/posts/markdown-contentlayer) 6. [首頁功能實作,加入 Contentlayer 文章列表](/posts/contentlayer-post-list) 7. [文章內頁功能實作,渲染 Markdown 文章內容](/posts/contentlayer-post-content) 8. [讓 Contentlayer 文章支援 MDX](/posts/contentlayer-mdx) 9. [安裝 Tailwind CSS 和相關 ESLint、Prettier 設定](/posts/tailwindcss-setup) 10. [Dark Mode 深色模式支援,使用 Tailwind CSS、next-themes](/posts/darkmode-tailwindcss) 11. [全站樣式切版](/posts/blog-site-style) 12. [首頁樣式切版](/posts/blog-homepage-style) 13. [文章內頁樣式切版](/posts/blog-post-page-style) 14. [加入程式碼 Syntax Highlighting,使用 rehype-prism-plus](/posts/rehype-prism-plus-syntax-highlighting) 15. [加入程式碼區塊標題,使用 rehype-code-titles](/posts/rehype-code-titles) 16. [加入程式碼區塊「複製按鈕」,使用客製化 MDX 元件](/posts/code-copy-button) 17. [加入 Open Graph、LD-JSON 等 SEO meta data](/posts/next-seo-meta-data) 18. [使用 next-sitemap 生成 Sitemap](/posts/next-sitemap) 19. [使用 feed 生成 RSS Feed](/posts/rss-feed) 20. [為內文小標題加入 anchor 錨點連結](/posts/post-heading-anchor) 21. [強化內文連結換頁速度、加入外部連結 icon](/posts/post-custom-link) 22. [圖片效能最佳化,使用 Next.js Image、plaiceholder、客製 MDX 元件](/posts/post-custom-image) 23. [使用 nprogress 加入換頁進度條](/posts/nprogress) 24. [在 MDX 文章側邊加入目錄](/posts/post-side-toc) 25. [使用 giscus 在 Next.js 加入留言系統](/posts/giscus-comment-system) 26. [使用 kbar 加入 Command Palette 指令面板](/posts/kbar-command-palette) 27. [在 kbar Command Palette 實作文章搜尋](/posts/kbar-post-search) 28. [使用 next-i18next 實作中英文多語系](/posts/next-i18next-multilingual) 29. [在 Next.js Contentlayer blog 實作舊路徑轉址](/posts/contentlayer-redirection) 30. 總結與回顧:一個炫砲技術部落格的誕生(You are here) ### 回顧使用的前端技術 - [Next.js](https://nextjs.org/):現代全端框架 - [Vercel](https://vercel.com/home):網站託管 - [Contentlayer](https://github.com/contentlayerdev/contentlayer):Markdown/MDX 文章處理 - [Tailwind CSS](https://tailwindcss.com/):UI 樣式 - [next-i18next](https://github.com/i18next/next-i18next):多語系 - [next-seo](https://github.com/garmeeh/next-seo):SEO meta tags - [kbar](https://github.com/timc1/kbar):指令面板 - [giscus](https://github.com/giscus/giscus):留言系統 - [nprogress](https://github.com/rstacruz/nprogress):換頁進度條 - [TypeScript](https://www.typescriptlang.org/):更扎實的 JavaScript - [ESLint](https://eslint.org/), [Prettier](https://prettier.io/):統一程式碼格式 ### 回顧實作出的功能 除了基本部落格功能,我們也實作下列這些酷炫 feature: - 編輯 local Markdown / MDX 檔案來寫作 - 網頁效能極佳 - SEO 友善 - 深色模式 - 中文、英文多語系 - 指令面板 Command Palette - 留言系統 - 換頁進度條 - RSS Feed - Sitemap - 好讀的程式碼區塊 ## 未來展望 我會持續維護「[Next.js Tailwind Contentlayer Blog Starter](https://github.com/eason-dev/nextjs-tailwind-contentlayer-blog-starter)」專案,畢竟我的個人部落格([easonchang.com](https://easonchang.com/))就是基於它開發的。 我還有很多想做的,我的部落格會持續進化,這份專案也會! 下面簡單列出一些未來預計加入的功能,應該會繼續寫成第 31、32... 篇文章: 1. 文章 OG image 自動產生:預計使用 Vercel 最新推出的 @vercel/og 搭配 Vercel Edge Function 來實作,參見 [Introducing OG Image Generation: Fast, dynamic social card images at the Edge - Vercel](https://vercel.com/blog/introducing-vercel-og-image-generation-fast-dynamic-social-card-images) 2. 可編輯的程式碼區塊:預計使用 [Sandpack](https://sandpack.codesandbox.io/) 搭配這篇教學文([Building a Next-Level Code Playground / Sandbox / REPL with Sandpack](https://www.joshwcomeau.com/react/next-level-playground/)) 3. 加入更多頁面(About、Projects、Resume) 4. 快速生成文章 .mdx 檔:預計使用 [Hygen](https://www.hygen.io/) code generator 5. 加入 Analytics:預計使用 Google Analytics 4 6. 串接 Google Search Console 7. 讓 RSS 分語系 8. 改善寫作體驗 9. 加入推薦文章區塊 10. 加入 Storybook 11. 還有很多...! ## 關於我 我是 [Eason Chang](https://easonchang.com/about),最愛挖坑給自己跳的全端工程師。 如果這系列文章對你有幫助,或是想追蹤後續更多文章,歡迎透過各種方式追蹤我,一起交流切磋! > 個人部落格:https://easonchang.com/ > > Facebook:https://www.facebook.com/easondev > > Twitter (X):https://x.com/easondev > > Github:https://github.com/eason-dev > > LinkedIn:https://www.linkedin.com/in/easonchang101/ --- # Implementing Old Path Redirection in Next.js Contentlayer Blog - Modern Next.js Blog Series #29 - URL: https://easonchang.com/posts/contentlayer-redirection - Date: 2022-10-14T00:00:00.000Z - Language: en - Description: Implementing redirection for each MDX article's old path within Next.js's getStaticProps > This article is also published at [it 邦幫忙 2022 iThome Ironman Contest](https://ithelp.ithome.com.tw/articles/10308729) This is the last implementation article of this series, where we'll implement a small but important feature: old path redirection! > The code changes for this article are as follows: > https://github.com/eason-dev/nextjs-tailwind-contentlayer-blog-starter/compare/day28-i18next...day29-old-post-path-redirect --- ## Old Path Redirection in Next.js Contentlayer Blog After running a blog for some time, you might want to adjust the URLs of articles for various reasons, such as: > Redirecting [https://easonchang.com/2016/12/13/grid-note](https://easonchang.com/2016/12/13/grid-note) > > to [https://easonchang.com/posts/grid-note](https://easonchang.com/posts/grid-note) In Next.js, there are two ways to implement redirection (redirect): 1. List all redirection rules in next.config.js, refer to the official documentation "[Data Fetching: getStaticProps | Next.js](https://nextjs.org/docs/api-reference/data-fetching/get-static-props#redirect)" 2. In getStaticProps or getServerSideProps, return redirect based on custom conditions, refer to the official documentation "[Data Fetching: getStaticProps | Next.js](https://nextjs.org/docs/api-reference/data-fetching/get-static-props#redirect)" We want the redirection rules to be customizable for each article, written inside the .mdx file of the article. Therefore, we need to analyze all articles with Contentlayer to know all redirection rules. After attempting, method 1 is not feasible because you cannot import Contentlayer's allPosts in next.config.js. Thus, this article will use method 2, deciding whether to redirect within getStaticProps. ### Implementing Old Path Redirection First, modify `contentlayer.config.ts` to add a redirectFrom attribute to Post. This allows setting individual redirection rules in each article's .mdx file: ```ts // ... export const Post = defineDocumentType(() => ({ // ... fields: { // ... // Add redirectFrom redirectFrom: { type: "list", of: { type: "string" }, }, }, // ... })); // ... ``` Add `src/lib/getAllRedirects.ts` to get all paths that need to be redirected: ```ts import { allPosts } from "@/lib/contentLayerAdapter"; import { unifyPath } from "@/lib/unifyPath"; export type Redirect = { source: string; destination: string; permanent: boolean; }; export const getAllRedirects = () => { const redirects: Redirect[] = []; allPosts.forEach((post) => { const allRedirectFrom = post.redirectFrom?.map((from) => unifyPath(from)) || []; allRedirectFrom.forEach((from) => { redirects.push({ source: from, destination: post.path, permanent: false, }); }); }); return redirects; }; export const allRedirects = getAllRedirects(); ``` Add `src/lib/unifyPath.ts` to normalize paths for redirection: ```ts // Discard leading and trailing slashes export const unifyPath = (path: string): string => { return "/" + path.replace(/\/$/, "").replace(/^\//, ""); }; ``` Add `src/lib/stringifyCatchAllDynamicRoute.ts` to convert route to string for easier processing later: ```ts export const stringifyCatchAllDynamicRoute = ( route: string | string[] | undefined ): string => { if (!route) return ""; if (Array.isArray(route)) return route.join("/"); return route; }; ``` Finally, modify all pages that use dynamic routes, currently only the article detail page `[slug].tsx` needs modification. Modify `src/pages/posts/[slug].tsx`: ```tsx // ... import { allRedirects } from "@/lib/getAllRedirects"; import { unifyPath } from "@/lib/unifyPath"; // ... export const getStaticProps: GetStaticProps = async ( context ) => { const { slug } = context.params!; const locale = context.locale!; // Add the following redirection logic // Handle redirect logic; const path = unifyPath("/posts/" + slug); const matchedRedirectRule = allRedirects.find((rule) => rule.source === path); if (matchedRedirectRule) { return { redirect: { destination: matchedRedirectRule.destination, permanent: matchedRedirectRule.permanent, }, }; } // ... }; // ... ``` And add `src/pages/[...pathToRedirectFrom].tsx` to catch all other paths: ```tsx import { GetStaticPaths, GetStaticProps } from "next"; import { ParsedUrlQuery } from "querystring"; import { allRedirects, Redirect } from "@/lib/getAllRedirects"; import { stringifyCatchAllDynamicRoute } from "@/lib/stringifyCatchAllDynamicRoute"; import { unifyPath } from "@/lib/unifyPath"; interface Params extends ParsedUrlQuery { pathToRedirectFrom: string | string[]; } export const getStaticPaths: GetStaticPaths = () => { return { paths: [], fallback: "blocking", }; }; export const getStaticProps: GetStaticProps = (context) => { const { pathToRedirectFrom } = context.params!; // Handle redirect logic const path = unifyPath(stringifyCatchAllDynamicRoute(pathToRedirectFrom)); const matchedRedirectRule: Redirect | undefined = allRedirects.find( (rule) => rule.source === path ); if (matchedRedirectRule) { return { redirect: { destination: matchedRedirectRule.destination, permanent: matchedRedirectRule.permanent, }, }; } return { notFound: true, }; }; const NullComponent = () => null; export default NullComponent; ``` ### Setting Redirection Rules for Articles Now, you can use the redirectFrom attribute added to Contentlayer Post to specify redirection rules for each article. For example, I set `content/posts/20220904-custom-link-demo.mdx` as follows: ```markdown --- // ... type: Post slug: custom-link-demo redirectFrom: - /old-custom-link/ - /2022/08/01/custom-link/ - /posts/old-custom-link/ --- Content... ``` The `slug` defines the current path of the article, which will be http://localhost:3000/posts/custom-link-demo The `redirectFrom` specifies the following 3 old paths, all redirecting to the latest article path: - http://localhost:3000/old-custom-link/ - http://localhost:3000/2022/08/01/custom-link/ - http://localhost:3000/posts/old-custom-link/ ## Results That's it! By running `pnpm dev` and browsing to the old paths you've set, you'll see that you're directly redirected to the correct article path, still able to browse the article smoothly! > The code changes for this article are as follows: > https://github.com/eason-dev/nextjs-tailwind-contentlayer-blog-starter/compare/day28-i18next...day29-old-post-path-redirect ## References - [Data Fetching: getStaticProps | Next.js](https://nextjs.org/docs/api-reference/data-fetching/get-static-props#redirect) ## Conclusion & Next Article Congratulations on successfully implementing old path redirection in your Next.js Contentlayer blog! And congratulations on completing all the implementations in this series! In the next article, which is also the last one, we'll summarize and review these 30 articles and the dazzling blog we've built together! --- # 在 Next.js Contentlayer blog 實作舊路徑轉址 - Modern Next.js Blog 系列 #29 - URL: https://easonchang.com/zh-TW/posts/contentlayer-redirection - Date: 2022-10-14T00:00:00.000Z - Language: zh-TW - Description: 在 Next.js 的 getStaticProps 裡,實作每篇 MDX 文章的舊路徑轉址 > 本文同步發佈於 [it 邦幫忙 2022 iThome 鐵人賽](https://ithelp.ithome.com.tw/articles/10308729) 這是本系列最後一篇實作文,最後來實作一個微小但重要的功能:舊路徑轉址! > 這篇修改的程式碼如下: > https://github.com/eason-dev/nextjs-tailwind-contentlayer-blog-starter/compare/day28-i18next...day29-old-post-path-redirect --- ## Next.js Contentlayer blog 舊路徑轉址 部落格經營一段時間後,你可能因為各種原因,想要調整文章網址,例如: > 把 [https://easonchang.com/2016/12/13/grid-note](https://easonchang.com/2016/12/13/grid-note) > > 轉址到 [https://easonchang.com/posts/grid-note](https://easonchang.com/posts/grid-note) 在 Next.js 要實作轉址(redirect)有兩種方式: 1. 在 next.config.js 羅列出所有轉址規則,參考官方文件「[Data Fetching: getStaticProps | Next.js](https://nextjs.org/docs/api-reference/data-fetching/get-static-props#redirect)」 2. 在 getStaticProps 或 getServerSideProps 裡根據自訂條件,回傳 redirect,參考官方文件「[Data Fetching: getStaticProps | Next.js](https://nextjs.org/docs/api-reference/data-fetching/get-static-props#redirect)」 這裡我們希望轉址規則是每篇文章都能自訂,寫在文章 .mdx 檔案裡面,所以要搭配 Contentlayer 分析所有文章設定後,才能知道所有轉址規則。 我在嘗試後發現方法 1 行不通,因為沒辦法在 next.config.js 裡 import Contentlayer 提供的 allPosts。 所以我們這篇文章會採用方法 2,在 getStaticProps 裡決定是否 redirect。 ### 實作舊路徑轉址 首先修改 `contentlayer.config.ts`,在 Post 新增 redirectFrom 屬性,這樣就能在每篇文章 .mdx 裡設定各自的轉址規則: ```ts // ... export const Post = defineDocumentType(() => ({ // ... fields: { // ... // 新增 redirectFrom redirectFrom: { type: "list", of: { type: "string" }, }, }, // ... })); // ... ``` 新增 `src/lib/getAllRedirects.ts` 來取得所有需轉址的路徑: ```ts import { allPosts } from "@/lib/contentLayerAdapter"; import { unifyPath } from "@/lib/unifyPath"; export type Redirect = { source: string; destination: string; permanent: boolean; }; export const getAllRedirects = () => { const redirects: Redirect[] = []; allPosts.forEach((post) => { const allRedirectFrom = post.redirectFrom?.map((from) => unifyPath(from)) || []; allRedirectFrom.forEach((from) => { redirects.push({ source: from, destination: post.path, permanent: false, }); }); }); return redirects; }; export const allRedirects = getAllRedirects(); ``` 新增 `src/lib/unifyPath.ts` 統一轉址 URL 格式: ```ts // Discard leading and trailing slashes export const unifyPath = (path: string): string => { return "/" + path.replace(/\/$/, "").replace(/^\//, ""); }; ``` 新增 `src/lib/stringifyCatchAllDynamicRoute.ts` 將 route 轉成 string 方便後續處理: ```ts export const stringifyCatchAllDynamicRoute = ( route: string | string[] | undefined ): string => { if (!route) return ""; if (Array.isArray(route)) return route.join("/"); return route; }; ``` 最後修改所有用到動態參數 [Dynamic Routes](https://nextjs.org/docs/routing/dynamic-routes) 的 pages,目前只有文章內頁 `[slug].tsx` 需要修改。 修改 `src/pages/posts/[slug].tsx`: ```tsx // ... import { allRedirects } from "@/lib/getAllRedirects"; import { unifyPath } from "@/lib/unifyPath"; // ... export const getStaticProps: GetStaticProps = async ( context ) => { const { slug } = context.params!; const locale = context.locale!; // 新增下面這段重導向規則 // Handle redirect logic const path = unifyPath("/posts/" + slug); const matchedRedirectRule = allRedirects.find((rule) => rule.source === path); if (matchedRedirectRule) { return { redirect: { destination: matchedRedirectRule.destination, permanent: matchedRedirectRule.permanent, }, }; } // ... }; // ... ``` 以及新增 `src/pages/[...pathToRedirectFrom].tsx`,捕捉所有其他路徑: ```tsx import { GetStaticPaths, GetStaticProps } from "next"; import { ParsedUrlQuery } from "querystring"; import { allRedirects, Redirect } from "@/lib/getAllRedirects"; import { stringifyCatchAllDynamicRoute } from "@/lib/stringifyCatchAllDynamicRoute"; import { unifyPath } from "@/lib/unifyPath"; interface Params extends ParsedUrlQuery { pathToRedirectFrom: string | string[]; } export const getStaticPaths: GetStaticPaths = () => { return { paths: [], fallback: "blocking", }; }; export const getStaticProps: GetStaticProps = (context) => { const { pathToRedirectFrom } = context.params!; // Handle redirect logic const path = unifyPath(stringifyCatchAllDynamicRoute(pathToRedirectFrom)); const matchedRedirectRule: Redirect | undefined = allRedirects.find( (rule) => rule.source === path ); if (matchedRedirectRule) { return { redirect: { destination: matchedRedirectRule.destination, permanent: matchedRedirectRule.permanent, }, }; } return { notFound: true, }; }; const NullComponent = () => null; export default NullComponent; ``` ### 設定各文章轉址規則 現在就能使用前面在 Contentlayer Post 新增的 redirectFrom 屬性來指定轉址規則。 例如我把 `content/posts/20220904-custom-link-demo.mdx` 設定如下: ```markdown --- // ... type: Post slug: custom-link-demo redirectFrom: - /old-custom-link/ - /2022/08/01/custom-link/ - /posts/old-custom-link/ --- Content... ``` `slug` 定義了文章現在的路徑,會是 http://localhost:3000/posts/custom-link-demo 而 `redirectFrom` 指定了下列 3 條舊路徑,都會導向最新文章路徑: - http://localhost:3000/old-custom-link/ - http://localhost:3000/2022/08/01/custom-link/ - http://localhost:3000/posts/old-custom-link/ ## 成果 這樣就完成了!使用 `pnpm dev`,瀏覽你設定過的舊路徑,就能看你會直接重導向到正確的文章路徑,依然能順利瀏覽文章! > 這篇修改的程式碼如下: > https://github.com/eason-dev/nextjs-tailwind-contentlayer-blog-starter/compare/day28-i18next...day29-old-post-path-redirect ## References - [Data Fetching: getStaticProps | Next.js](https://nextjs.org/docs/api-reference/data-fetching/get-static-props#redirect) ## 小結&下一篇 恭喜你成功在 Next.js Contentlayer blog 實作舊路徑轉址了! 也恭喜你完成這系列文章所有實作了! 下一篇,也就是最後一篇文章,我們會來總結與回顧這 30 篇文章,以及我們一同實作出的炫砲部落格! --- # Implementing Multilingual Support with next-i18next in a Next.js Contentlayer Blog - Modern Next.js Blog Series #28 - URL: https://easonchang.com/posts/next-i18next-multilingual - Date: 2022-10-13T00:00:00.000Z - Language: en - Description: Implementing multilingual support for English and Chinese in a Next.js Contentlayer blog, including a language switch button and Command Palette > This article is also published at [it 邦幫忙 2022 iThome Ironman Contest](https://ithelp.ithome.com.tw/articles/10308315) Let's complete the last key feature of this series: "i18next support for English and Chinese," making our blog internationally friendly! The final effect is as follows: ![Chinese](https://i.imgur.com/GQxxTPA.jpg) ![English](https://i.imgur.com/7Ko9i3i.jpg) ![Command Palette](https://i.imgur.com/eci7bg3.jpg) > The code changes for this article are as follows: > https://github.com/eason-dev/nextjs-tailwind-contentlayer-blog-starter/compare/day27-search-post...day28-i18next > Note: > > If your site does not plan to support multiple languages, you can skip this article. > > It will not affect the reading and implementation of the remaining two articles. --- ## Multilingual Support in Next.js Next.js natively supports multilingual routing, allowing for different languages based on the path, as follows: - [https://easonchang.com/](https://easonchang.com/) points to the Chinese version of the site - [https://easonchang.com/en](https://easonchang.com/en) points to the English version of the site Or based on subdomains, for example: - example.com for the English version of the site - example.fr for the French version of the site For more details, refer to the official document: [Advanced Features: Internationalized Routing | Next.js](https://nextjs.org/docs/advanced-features/i18n-routing). For actual multilingual string handling, additional packages such as [next-i18next](https://github.com/i18next/next-i18next) or [next-intl](https://github.com/amannn/next-intl) are needed. Here, we will use the most popular [next-i18next](https://github.com/i18next/next-i18next) for implementation. ## Implementing English and Chinese Multilingual Support with next-i18next ### Installing next-i18next Enter the command to install the package: ```shell pnpm add next-i18next ``` Add `next-i18next.config.js`: ```js module.exports = { i18n: { locales: ["en", "zh-TW"], defaultLocale: "zh-TW", }, }; ``` Modify `next.config.mjs` to enable next-i18next: ```js // ... import i18nConfig from "./next-i18next.config.js"; const { i18n } = i18nConfig; const nextConfig = withContentlayer({ // ... i18n, }); export default nextConfig; ``` Modify `src/pages/_app.tsx` to wrap the entire App with `appWithTranslation`: ```tsx // ... import { appWithTranslation } from "next-i18next"; import nextI18nConfig from "../../next-i18next.config"; // ... export default appWithTranslation(MyApp, nextI18nConfig); ``` ### Adding Language Files To display different text in next-i18next based on language, JSON files defining each i18n key in different languages are needed under `public/locales//.json`. We will add two locales, `en` and `zh-TW`, and two namespaces: `common` for site-wide use and `indexPage` for the homepage. Therefore, we need to add the following four files: - `public/locales/en/common.json` - `public/locales/en/indexPage.json` - `public/locales/zh-TW/common.json` - `public/locales/zh-TW/indexPage.json` The content of the key and value can be adjusted according to your site content. Add `public/locales/en/common.json`: ```json { "copied": "Copied!", "table-of-contents": "Table of contents", "home": "Home", "posts": "Posts", "search": "Search", "search-posts": "Search Posts", "next-article": "Next Article", "previous-article": "Previous Article", "published-time": "Published time", "toggle-theme": " Toggle theme", "dark-mode": "Dark mode", "light-mode": "Light mode", "page": "Page", "operation": "Operation", "toggle-language": "Toggle language", "english": "English", "chinese": "中文" } ``` ### Using Language Files in Pages Modify `src/pages/index.tsx`: ```tsx import { useTranslation } from "next-i18next"; import { serverSideTranslations } from "next-i18next/serverSideTranslations"; // ... export const getStaticProps: GetStaticProps = async (context) => { const locale = context.locale!; // ... return { props: { ...(await serverSideTranslations(locale, ["indexPage", "common"])), // ... }, }; }; // ... ``` ### Adding a Language Switch Button in the Header Next, add a language toggle button as shown in the image: ![Language toggle button](https://i.imgur.com/blXVtA3.jpg) Add `src/components/LanguageSwitch.tsx`: ```tsx /* eslint-disable jsx-a11y/anchor-is-valid */ import Link from "next/link"; import { useRouter } from "next/router"; const LanguageSwitch = () => { const router = useRouter(); const { pathname, query } = router; const nextLocale = router.locale === "en" ? "zh-TW" : "en"; return ( {router.locale === "en" ? "中文" : "English"} ); }; export default LanguageSwitch; ``` Modify `src/components/Header.tsx` to display `LanguageSwitch`: ```tsx import LanguageSwitch from "@/components/LanguageSwitch"; // ... export default function Header() { return (
// ...
// ... // Add LanguageSwitch
); } ``` ### Adding Language Switch Option to Command Palette In [Article 26](/posts/kbar-command-palette), we added a Command Palette allowing readers to perform various operations quickly with their keyboard. Switching languages is an important action, so let's add it to the Command Palette. The effect is shown in the image: ![Command Palette](https://i.imgur.com/eci7bg3.jpg) > Similar to [Article 27 on implementing post search in Command Palette](/posts/kbar-post-search), there was a technical issue. > > If the language switch action is added directly into the `` **actions** array, the `changeLocale` function is only initialized once when the site mounts, remembering the URL from the first page. > After navigating to the second page and calling `changeLocale`, although the language can be switched, it forces a redirection back to the first page. > > Therefore, a workaround is used here. In each page, we dynamically add each page's action using [useRegisterActions](https://kbar.vercel.app/docs/concepts/actions), so `changeLocale` is re-initialized on every page. Modify `src/components/CommandPalette/CommandPalette.tsx` to add the language section: ```tsx import { // ... LanguageIcon, } from "@heroicons/react/24/outline"; import { useTranslation } from "next-i18next"; // ... export default function CommandPalette({ children }: Props) { const { t } = useTranslation(["common"]); // ... const actions = [ // ... // - Language toggle { id: "language", name: t("toggle-language"), keywords: "change toggle locale language translation 切換 更換 語言 語系 翻譯", icon: , section: t("operation"), }, ]; // ... } // ... ``` Add `src/components/CommandPalette/useCommandPaletteLocaleActions.tsx` to dynamically add language switching actions: ```tsx import { useRegister Actions } from "kbar"; import { useRouter } from "next/router"; import { useTranslation } from "next-i18next"; export const useCommandPaletteLocaleActions = () => { const router = useRouter(); const { pathname, asPath, query } = router; const { t } = useTranslation(["common"]); const changeLocale = (locale: string) => { router.push({ pathname, query }, asPath, { locale: locale }); }; useRegisterActions( [ { id: "language-english", name: "English", keywords: "locale language translation english 語言 語系 英文 英語", perform: () => changeLocale("en"), icon: EN, parent: "language", section: t("operation"), }, { id: "language-chinese", name: "中文", keywords: "locale language translation traditional chinese taiwanese 語言 語系 翻譯 中文 台灣 繁體", perform: () => changeLocale("zh-TW"), icon: , parent: "language", section: t("operation"), }, ], [asPath] ); }; ``` Wrap each page with `` to call `useCommandPaletteLocaleActions` on every page. Modify `src/pages/index.tsx`: ```tsx // ... const Home: NextPage = ({ posts, commandPalettePosts }) => { // ... return // ...; }; // ... ``` Modify `src/pages/posts/[slug].tsx`: ```tsx // ... const PostPage: NextPage = ( { // ... } ) => { // ... return // ...; }; // ... ``` This successfully adds the language switching action to the Command Palette. ### Supporting Multilingual Comments with giscus The [giscus](https://giscus.app/en) comment system added in [Article 25](/posts/giscus-comment-system) also supports multiple languages. We can pass the `locale` to ``'s `lang` to synchronize the comment section's language with the site's displayed language. Modify `src/components/Comment.tsx`: ```tsx import { useRouter } from "next/router"; // ... const Comment = () => { // ... const { locale } = useRouter(); return (
); }; // ... ``` ### Converting All Page and Component Texts to i18n Keys Finally, convert all text in each page and component to i18n keys to complete the multilingual setup. > Many files are modified here, and the method is the same for all, so only 2 examples are listed. The complete changes can be seen in this commit: > https://github.com/eason-dev/nextjs-tailwind-contentlayer-blog-starter/commit/988eec15c02172c3de1b1de88630afc5bc5e5397 Modify `src/components/Header.tsx`, converting text to translated text using the `t` function: ```tsx import { useTranslation } from 'next-i18next'; // ... export default function Header() { const { t } = useTranslation(['common']); return (
// ...
{headerConfigs.navLinks.map((link) => ( {t(link.title)} // <-- Modified this line ))}
// ...
); } ``` Modify `src/components/CustomPre.tsx`, similarly converting text using the `t` function: ```tsx // ... function CustomPre({ children, className, ...props }: Props) { // ... return (
        
{t('copied')} // <-- Modified this line
{children}
); } // ... ``` ## Results That's it! Run `pnpm dev`, go to the website, click the language toggle button in the header, or switch languages using the Command Palette. You'll be able to switch between English and Chinese languages and see the site in different languages! The URL will also change based on the language: - Chinese: http://localhost:3000/ - English: http://localhost:3000/en The final effect is as follows: ![Chinese](https://i.imgur.com/GQxxTPA.jpg) ![English](https://i.imgur.com/7Ko9i3i.jpg) ![Command Palette](https://i.imgur.com/eci7bg3.jpg) > The code changes for this article are as follows: > https://github.com/eason-dev/nextjs-tailwind-contentlayer-blog-starter/compare/day27-search-post...day28-i18next ## References - [i18next/next-i18next: The easiest way to translate your NextJs apps.](https://github.com/i18next/next-i18next) - [Advanced Features: Internationalized Routing | Next.js](https://nextjs.org/docs/advanced-features/i18n-routing) ## Next Article Congratulations on successfully adding multilingual support with next-i18next! The next article, which is the last implementation piece of the 30-day series, will add a small but important feature: old path redirection! --- # 使用 next-i18next 實作中英文多語系 - Modern Next.js Blog 系列 #28 - URL: https://easonchang.com/zh-TW/posts/next-i18next-multilingual - Date: 2022-10-13T00:00:00.000Z - Language: zh-TW - Description: 在 Next.js Contentlayer 部落格中,使用 next-i18next 實作中英文多語系,包含語系切換按鈕及 Command Palette > 本文同步發佈於 [it 邦幫忙 2022 iThome 鐵人賽](https://ithelp.ithome.com.tw/articles/10308315) 讓我們來完成這個系列最後一塊重點功能:「i18next 中英文多語系支援」,讓部落格走向國際! 最終效果如下: ![Chinese](https://i.imgur.com/GQxxTPA.jpg) ![English](https://i.imgur.com/7Ko9i3i.jpg) ![Command Palette](https://i.imgur.com/eci7bg3.jpg) > 這篇修改的程式碼如下: > https://github.com/eason-dev/nextjs-tailwind-contentlayer-blog-starter/compare/day27-search-post...day28-i18next > Note: > > 如果你的網站沒有打算支援多語系,這篇文章可以忽略不看。 > > 不影響剩餘 2 篇文章的閱讀與實作。 --- ## Next.js 的 多語系支援 Next.js 內建就支援了多語系路由,可以做到根據 path 切分不同語系,效果如下: - [https://easonchang.com/](https://easonchang.com/) 指到中文版網站 - [https://easonchang.com/en](https://easonchang.com/en) 指到英文版網站 或是根據 subdomain 切分語系,例如: - example.com 英文版網站 - example.fr 法文版網站 詳細可參考官方文件:[Advanced Features: Internationalized Routing | Next.js](https://nextjs.org/docs/advanced-features/i18n-routing)。 而實際處理多語系字串,要搭配額外套件,像是 [next-i18next](https://github.com/i18next/next-i18next) 或 [next-intl](https://github.com/amannn/next-intl) 等。 這邊我們使用最熱門的 [next-i18next](https://github.com/i18next/next-i18next) 來實作。 ## 使用 next-i18next 實作中英文多語系 ### 安裝 next-i18next 輸入指令安裝套件: ```shell pnpm add next-i18next ``` 新增 `next-i18next.config.js`: ```js module.exports = { i18n: { locales: ["en", "zh-TW"], defaultLocale: "zh-TW", }, }; ``` 修改 `next.config.mjs`,啟用 next-i18next: ```js // ... import i18nConfig from "./next-i18next.config.js"; const { i18n } = i18nConfig; /** @type {import('next').NextConfig} */ const nextConfig = withContentlayer({ // ... i18n, }); export default nextConfig; ``` 修改 `src/pages/_app.tsx`,用 appWithTranslation 包住整個 App: ```tsx // ... import { appWithTranslation } from "next-i18next"; import nextI18nConfig from "../../next-i18next.config"; // ... // Explicitly pass nextI18nConfig to suppress i18next console warning // `react-i18next:: You will need to pass in an i18next instance by using initReactI18next` // Ref: https://github.com/i18next/next-i18next/issues/718#issuecomment-1190468800 export default appWithTranslation(MyApp, nextI18nConfig); ``` ### 加入中英文語系檔案 想在 next-i18next 裡讓一段文字的中英文不同,需要在 `public/locales//.json` 路徑新增 JSON 檔,定義每個 i18n key 在不同語系呈現的文字。 我們這裡會新增 `en` 和 `zh-TW` 這兩個語系,以及分成兩個 namespace:全站共用的 `common` 和首頁專屬的 `indexPage`。 因此我們需要新增下面四個檔案: - `public/locales/en/common.json` - `public/locales/en/indexPage.json` - `public/locales/zh-TW/common.json` - `public/locales/zh-TW/indexPage.json` 內容的 key, value 可以依照你的網站內容調整。 新增 `public/locales/en/common.json`: ```json { "copied": "Copied!", "table-of-contents": "Table of contents", "home": "Home", "posts": "Posts", "search": "Search", "search-posts": "Search Posts", "next-article": "Next Article", "previous-article": "Previous Article", "published-time": "Published time", "toggle-theme": "Toggle theme", "dark-mode": "Dark mode", "light-mode": "Light mode", "page": "Page", "operation": "Operation", "toggle-language": "Toggle language", "english": "English", "chinese": "中文" } ``` 新增 `public/locales/en/indexPage.json`: ```json { "latest-posts": "Latest Posts", "intro-title": "Hey,I am Iron Man ?", "intro-1": "I'm Tony Stark,not Stank!", "intro-2": "I'm rich and have saved the world lots of times.", "intro-3": "I have aliens, purple things, and purple aliens." } ``` 新增 `public/locales/zh-TW/common.json`: ```json { "copied": "已複製!", "table-of-contents": "目錄", "home": "首頁", "posts": "文章", "search": "搜尋", "search-posts": "搜尋文章", "next-article": "下一篇文章", "previous-article": "上一篇文章", "published-time": "發佈時間", "toggle-theme": "切換主題", "dark-mode": "暗黑模式", "light-mode": "明亮模式", "page": "頁面", "operation": "操作", "toggle-language": "切換語言", "english": "English", "chinese": "中文" } ``` 新增 `public/locales/zh-TW/indexPage.json`: ```json { "latest-posts": "最新文章", "intro-title": "Hey,I am Iron Man ?", "intro-1": "我是 Tony Stark,不是 Stank!", "intro-2": "老子很有錢,拯救過很多次世界。", "intro-3": "我討厭外星人、紫色的東西、和紫色外星人。" } ``` ### 在各頁面引用語系檔案 修改 `src/pages/index.tsx`: ```tsx import { useTranslation } from "next-i18next"; import { serverSideTranslations } from "next-i18next/serverSideTranslations"; // ... export const getStaticProps: GetStaticProps = async (context) => { const locale = context.locale!; // ... return { props: { ...(await serverSideTranslations(locale, ["indexPage", "common"])), // ... }, }; }; // ... ``` 新增 `src/configs/i18nConfigs.ts`: ```ts export const LOCALES = ["en", "zh-TW"]; export const DEFAULT_LOCALE = "zh-TW"; ``` 修改 `src/pages/posts/[slug].tsx`: ```tsx import { serverSideTranslations } from "next-i18next/serverSideTranslations"; import { ParsedUrlQuery } from "querystring"; import { LOCALES } from "@/configs/i18nConfigs"; // ... interface Params extends ParsedUrlQuery { slug: string; } export const getStaticPaths: GetStaticPaths = () => { const paths: string[] = []; LOCALES.forEach((locale) => { paths.push(...allPosts.map((post) => `/${locale}${post.path}`)); }); return { paths, fallback: false, }; }; export const getStaticProps: GetStaticProps = async ( context ) => { const { slug } = context.params!; const locale = context.locale!; // ... return { props: { ...(await serverSideTranslations(locale, ["common"])), // ... }, }; }; // ... ``` 這樣就能在各元件內用 **next-i18next** 的 `useTranslation` 的 `t` function 來根據語系顯示不同文字了,範例如下: ```js const { t } = useTranslation(["indexPage", "common"]);

{t("home")}

; // en 顯示 'Home' // zh-TW 顯示 '首頁' ``` ### 在 Header 加入語系切換按鈕 接著來加入下圖這個語系切換按鈕: ![Language toggle button](https://i.imgur.com/blXVtA3.jpg) 新增 `src/components/LanguageSwitch.tsx`: ```tsx /* eslint-disable jsx-a11y/anchor-is-valid */ import Link from "next/link"; import { useRouter } from "next/router"; const LanguageSwitch = () => { const router = useRouter(); const { pathname, query } = router; const nextLocale = router.locale === "en" ? "zh-TW" : "en"; return ( {router.locale === "en" ? "??" : "??"} ); }; export default LanguageSwitch; ``` 修改 `src/components/Header.tsx`,顯示 LanguageSwitch: ```tsx import LanguageSwitch from "@/components/LanguageSwitch"; // ... export default function Header() { return (
// ...
// ... // 加入 LanguageSwitch
); } ``` ### 在 Command Palette 指令面板加入語系切換選單 在[第 26 篇](/posts/kbar-command-palette)我們加入了 Command Palette 指令面板,讓讀者能用鍵盤快速執行各種操作。 切換語系也是一個很重要的動作,讓我們把它加入 Command Palette 吧。 效果如下圖: ![Command Palette](https://i.imgur.com/eci7bg3.jpg) > 這邊和[第 27 篇在 Command Palette 實作文章搜尋](/posts/kbar-post-search)一樣有遇到技術問題。 > > 如果把切換語系的 action 寫進 `` 的 **actions** array,`changeLocale` 函式只會在全站 mount 時初始化一次,會記憶住第一頁 **useRouter** 的網址。 > 進到第二頁後呼叫 `changeLocale` 時,雖然能切換語系,但會被強制導回第一頁。 > > 因此這邊採用迂迴的方法,在每個 page 使用 [useRegisterActions](https://kbar.vercel.app/docs/concepts/actions),動態加入每個頁面的 action,`changeLocale` 才會在每個頁面重新初始化。 修改 `src/components/CommandPalette/CommandPalette.tsx`,加入 language section: ```tsx import { // ... LanguageIcon, } from "@heroicons/react/24/outline"; import { useTranslation } from "next-i18next"; // ... export default function CommandPalette({ children }: Props) { const { t } = useTranslation(["common"]); // ... const actions = [ // ... // - Language toggle { id: "language", name: t("toggle-language"), keywords: "change toggle locale language translation 切換 更換 語言 語系 翻譯", icon: , section: t("operation"), }, ]; // ... } // ... ``` 新增 `src/components/CommandPalette/useCommandPaletteLocaleActions.tsx`: ```tsx import { useRegisterActions } from "kbar"; import { useRouter } from "next/router"; import { useTranslation } from "next-i18next"; export const useCommandPaletteLocaleActions = () => { const router = useRouter(); const { pathname, asPath, query } = router; const { t } = useTranslation(["common"]); const changeLocale = (locale: string) => { router.push({ pathname, query }, asPath, { locale: locale }); }; useRegisterActions( [ { id: "language-english", name: "English", keywords: "locale language translation english 語言 語系 英文 英語", perform: () => changeLocale("en"), icon: ??, parent: "language", section: t("operation"), }, { id: "language-chinese", name: "中文", keywords: "locale language translation traditional chinese taiwanese 語言 語系 翻譯 中文 台灣 繁體", perform: () => changeLocale("zh-TW"), icon: ??, parent: "language", section: t("operation"), }, ], [asPath] ); }; ``` 新增 `src/components/LayoutPerPage.tsx`,用來在每個頁面呼叫 `useCommandPaletteLocaleActions`: ```tsx import { useCommandPaletteLocaleActions } from "@/components/CommandPalette/useCommandPaletteLocaleActions"; type Props = { children: React.ReactNode; }; const LayoutPerPage = ({ children }: Props) => { useCommandPaletteLocaleActions(); return <>{children}; }; export default LayoutPerPage; ``` 接著將每個頁面用 `` 包住。 修改 `src/pages/index.tsx`: ```tsx // ... const Home: NextPage = ({ posts, commandPalettePosts }) => { // ... return // ...; }; // ... ``` 修改 `src/pages/posts/[slug].tsx`: ```tsx // ... const PostPage: NextPage = ( { // ... } ) => { // ... return // ...; }; // ... ``` 這樣就成功把切換語系 action 加入 Command Palette 了。 ### 讓 giscus 留言區塊支援多語系 在[第 25 篇](/posts/giscus-comment-system)加入的 [giscus](https://giscus.app/zh-TW) 留言板也支援多語系,我們可以把 locale 傳給 `` 的 `lang`,來讓留言板語言與網站顯示的語言一致。 修改 `src/components/Comment.tsx`: ```tsx import { useRouter } from "next/router"; // ... const Comment = () => { // ... const { locale } = useRouter(); return (
); }; // ... ``` ### 把所有頁面和元件文字換成 i18n key 最後把每個頁面和元件的文字換成 i18n key,就完成多語系處理了。 > 這邊改動檔案很多,寫法都一樣,這邊只列 2 個當參考,完整改動可以看這支 commit: > https://github.com/eason-dev/nextjs-tailwind-contentlayer-blog-starter/commit/988eec15c02172c3de1b1de88630afc5bc5e5397 修改 `src/components/Header.tsx`,把 CustomLink 內的文字用 `t` function 轉換成翻譯後文字: ```tsx import { useTranslation } from 'next-i18next'; // ... export default function Header() { const { t } = useTranslation(['common']); return (
// ...
{headerConfigs.navLinks.map((link) => ( {t(link.title)} // <-- 修改這一行 ))}
// ...
); } ``` 修改 `src/components/CustomPre.tsx`,一樣把文字改成 `t` function: ```tsx // ... function CustomPre({ children, className, ...props }: Props) { // ... return (
        
{t('copied')} // <-- 修改這一行
{children}
); } // ... ``` ## 成果 這樣就完成了!使用 `pnpm dev`,進去網站後按下 Header 的切換語系按鈕,或用 Command Palette 切換語系,就能在中英文語系切換,看到不同語系的網站了! 網址也會根據語系不同: - 中文:http://localhost:3000/ - 英文:http://localhost:3000/en 最終效果如下: ![Chinese](https://i.imgur.com/GQxxTPA.jpg) ![English](https://i.imgur.com/7Ko9i3i.jpg) ![Command Palette](https://i.imgur.com/eci7bg3.jpg) > 這篇修改的程式碼如下: > https://github.com/eason-dev/nextjs-tailwind-contentlayer-blog-starter/compare/day27-search-post...day28-i18next ## References - [i18next/next-i18next: The easiest way to translate your NextJs apps.](https://github.com/i18next/next-i18next) - [Advanced Features: Internationalized Routing | Next.js](https://nextjs.org/docs/advanced-features/i18n-routing) ## 下一篇 恭喜你成功使用 next-i18next 加入多語系了! 下一篇是 30 天系列最後一篇實作,最後來加入一個微小但重要的功能:舊路徑轉址! --- # Implementing Article Search in kbar Command Palette - Modern Next.js Blog Series #27 - URL: https://easonchang.com/posts/kbar-post-search - Date: 2022-10-12T00:00:00.000Z - Language: en - Description: Enabling article search in the Command Palette, allowing navigation directly to specific article pages > This article is also published at [it 邦幫忙 2022 iThome Ironman Contest](https://ithelp.ithome.com.tw/articles/10307994) This is the 27th article in the "Modern Blog 30 Days" series. After adding the Command Palette in the previous article, we continue to expand its functionality, enabling article search within the Command Palette and allowing navigation directly to specific article pages! The final effect is as follows: ![Command palette search post light](https://i.imgur.com/76PWNBj.jpg) ![Command palette search post dark](https://i.imgur.com/9L1tBBS.jpg) The code changes for this article are as follows: https://github.com/eason-dev/nextjs-tailwind-contentlayer-blog-starter/compare/day26-command-palette...day27-search-post --- ## Implementing Article Search Feature in kbar Command Palette Ideally, to expand the options in [kbar](https://github.com/timc1/kbar), you only need to modify the actions array in CommandPalette.tsx, and importing the list of allPosts provided by Contentlayer should suffice. However, due to various technical limitations, we need to implement it in a more complicated way. If you find a simpler method, feel free to share it with me! The technical limitations encountered, related to Next.js, Contentlayer, and kbar, are as follows: - Contentlayer cannot directly import **allPosts** in `CommandPalette.tsx`; it can only be imported in Next.js pages' **getStaticProps**. - Next.js currently does not support writing **getStaticProps** in `_app.tsx` for site-wide use, so **getStaticProps** in each page needs to be modified. - Dynamically adding Command Palette options on each page requires using kbar's [useRegisterActions](https://kbar.vercel.app/docs/concepts/actions). Thus, our implementation direction is: 1. Add `getCommandPalettePosts` in **getStaticProps** of all pages to retrieve all articles. 2. Add `useCommandPalettePostActions` in components of all pages to dynamically add all articles to Command Palette options using **useRegisterActions**. 3. Modify the CommandPalette component, adding a "Search Articles" action, gathering all article options from **2.** for better usability. Let's start implementing! Add `src/components/CommandPalette/getCommandPalettePosts.ts`: ```ts import { allPostsNewToOld } from "@/lib/contentLayerAdapter"; export type PostForCommandPalette = { slug: string; title: string; path: string; }; export const getCommandPalettePosts = (): PostForCommandPalette[] => { const commandPalettePosts = allPostsNewToOld.map((post) => ({ slug: post.slug, title: post.title, path: post.path, })); return commandPalettePosts; }; ``` Add `src/components/CommandPalette/useCommandPalettePostActions.tsx`: ```tsx import { useRegisterActions } from "kbar"; import { useRouter } from "next/router"; import { PostForCommandPalette } from "./getCommandPalettePosts"; export const useCommandPalettePostActions = ( posts: PostForCommandPalette[] ): void => { const router = useRouter(); useRegisterActions( posts.map((post) => ({ id: post.slug, name: post.title, perform: () => router.push(post.path), section: "Search Articles", parent: "search-posts", })), [] ); }; ``` Next, modify each Next.js page to include the logic of `getCommandPalettePosts` and `useCommandPalettePostActions`. Modify `src/pages/index.tsx`: ```tsx // ... import { getCommandPalettePosts, PostForCommandPalette, } from "@/components/CommandPalette/getCommandPalettePosts"; import { useCommandPalettePostActions } from "@/components/CommandPalette/useCommandPalettePostActions"; // ... type Props = { posts: PostForIndexPage[]; commandPalettePosts: PostForCommandPalette[]; }; export const getStaticProps: GetStaticProps = () => { const commandPalettePosts = getCommandPalettePosts(); // ... return { props: { posts, commandPalettePosts } }; }; const Home: NextPage = ({ posts, commandPalettePosts }) => { useCommandPalettePostActions(commandPalettePosts); // ... }; // ... ``` Modify `src/pages/posts/[slug].tsx`: ```tsx // ... import { getCommandPalettePosts, PostForCommandPalette, } from "@/components/CommandPalette/getCommandPalettePosts"; import { useCommandPalettePostActions } from "@/components/CommandPalette/useCommandPalettePostActions"; // ... type Props = { post: PostForPostPage; prevPost: RelatedPostForPostLayout; nextPost: RelatedPostForPostLayout; commandPalettePosts: PostForCommandPalette[]; }; // ... export const getStaticProps: GetStaticProps = ({ params }) => { const commandPalettePosts = getCommandPalettePosts(); // ... return { props: { post, prevPost, nextPost, commandPalettePosts, }, }; }; const PostPage: NextPage = ({ post, prevPost, nextPost, commandPalettePosts, }) => { useCommandPalettePostActions(commandPalettePosts); // ... }; // ... ``` Finally, modify `src/components/CommandPalette/CommandPalette.tsx` to add the Search section in the actions array: ```tsx import { MagnifyingGlassIcon, // ... } from "@heroicons/react/24/outline"; // ... export default function CommandPalette({ children }: Props) { // ... const actions = [ // Page section // ... // Add this section // Search section // - Search posts { id: "search-posts", name: "Articles", keywords: "search find posts writing words blog articles thoughts 搜尋 尋找 文章 寫作 部落格", icon: , section: "Search", }, // Operation section // - Theme toggle // ... ]; return ( {children} ); } // ... ``` ## Results That's it! Use `pnpm dev`, enter the website, press `Ctrl + K` (Windows) or `Cmd + K` (Mac), or click the Command icon in the top-right corner to open the Command Palette. You'll see the "Search Articles" operation available for execution! The final effect is as follows: ![Command palette search post light](https://i.imgur.com/76PWNBj.jpg) ![Command palette search post dark](https://i.imgur.com/9L1tBBS.jpg) The code changes for this article are as follows: https://github.com/eason-dev/nextjs-tailwind-contentlayer-blog-starter/compare/day26-command-palette...day27-search-post ## References - [timc1/kbar: fast, portable, and extensible cmd+k interface for your site](https://github.com/timc1/kbar) - [kbar - command+k interface for your site](https://kbar.vercel.app/docs/concepts/actions) ## Next Article Congratulations on successfully adding article search functionality to the Command Palette! In the next article, we finally complete the last key feature of this series: "i18next multilingual support"! --- # 在 kbar Command Palette 實作文章搜尋 - Modern Next.js Blog 系列 #27 - URL: https://easonchang.com/zh-TW/posts/kbar-post-search - Date: 2022-10-12T00:00:00.000Z - Language: zh-TW - Description: 讓 Command Palette 指令面板能搜尋所有文章,並跳轉到特定文章內頁 > 本文同步發佈於 [it 邦幫忙 2022 iThome 鐵人賽](https://ithelp.ithome.com.tw/articles/10307994) 這是「Modern Blog 30 天」系列第 27 篇文章。 上一篇加完了 Command Palette 指令面板,這篇我們來繼續擴充它,讓 Command Palette 能搜尋所有文章並跳轉到特定文章內頁! 最終效果如下: ![Command palette search post light](https://i.imgur.com/76PWNBj.jpg) ![Command palette search post dark](https://i.imgur.com/9L1tBBS.jpg) 這篇修改的程式碼如下: https://github.com/eason-dev/nextjs-tailwind-contentlayer-blog-starter/compare/day26-command-palette...day27-search-post --- ## 實作 kbar Command Palette 文章搜尋功能 理想上要擴充 [kbar](https://github.com/timc1/kbar) 的選項,只需要修改 CommandPalette.tsx 裡 actions array 就好,import Contentlayer 提供的 allPosts 文章列表放進去應該就行了。 但這邊遇到各種技術限制,所以要改用比較複雜的方式實現。如果你有找到更簡單的方法,歡迎跟我說! 遇到的技術限制如下,跟 Next.js、Contentlayer、kbar 有關: - Contentlayer 沒辦法直接在 `CommandPalette.tsx` 裡 import **allPosts**,只能在 Next.js pages 的 **getStaticProps** 裡 import - Next.js 目前還不支援在 `_app.tsx` 裡寫 **getStaticProps** 給全站用,因此每個頁面的 **getStaticProps** 都要修改 - 在各頁面動態加入 Command Palette 選項,需要使用 kbar 提供的 [useRegisterActions](https://kbar.vercel.app/docs/concepts/actions) 所以最終我們的實作方向是: 1. 新增 `getCommandPalettePosts` 並放在所有頁面的 **getStaticProps** 裡,取得所有文章 2. 新增 `useCommandPalettePostActions` 並放在所有頁面的 component 內,利用 **useRegisterActions** 動態加入所有文章到 Command Palette 選項 3. 修改 CommandPalette component,加入一個「搜尋文章」的 action,集結所有 **2.** 的文章選項,比較好用 讓我們開始實作吧! 新增 `src/components/CommandPalette/getCommandPalettePosts.ts`: ```ts import { allPostsNewToOld } from "@/lib/contentLayerAdapter"; export type PostForCommandPalette = { slug: string; title: string; path: string; }; export const getCommandPalettePosts = (): PostForCommandPalette[] => { const commandPalettePosts = allPostsNewToOld.map((post) => ({ slug: post.slug, title: post.title, path: post.path, })); return commandPalettePosts; }; ``` 新增 `src/components/CommandPalette/useCommandPalettePostActions.tsx`: ```tsx import { useRegisterActions } from "kbar"; import { useRouter } from "next/router"; import { PostForCommandPalette } from "./getCommandPalettePosts"; export const useCommandPalettePostActions = ( posts: PostForCommandPalette[] ): void => { const router = useRouter(); useRegisterActions( posts.map((post) => ({ id: post.slug, name: post.title, perform: () => router.push(post.path), section: "搜尋文章", parent: "search-posts", })), [] ); }; ``` 接著修改每個 Next.js 頁面,加入 `getCommandPalettePosts` 和 `useCommandPalettePostActions` 邏輯。 修改 `src/pages/index.tsx`: ```tsx // ... import { getCommandPalettePosts, PostForCommandPalette, } from "@/components/CommandPalette/getCommandPalettePosts"; import { useCommandPalettePostActions } from "@/components/CommandPalette/useCommandPalettePostActions"; // ... type Props = { posts: PostForIndexPage[]; commandPalettePosts: PostForCommandPalette[]; }; export const getStaticProps: GetStaticProps = () => { const commandPalettePosts = getCommandPalettePosts(); // ... return { props: { posts, commandPalettePosts } }; }; const Home: NextPage = ({ posts, commandPalettePosts }) => { useCommandPalettePostActions(commandPalettePosts); // ... }; // ... ``` 修改 `src/pages/posts/[slug].tsx`: ```tsx // ... import { getCommandPalettePosts, PostForCommandPalette, } from "@/components/CommandPalette/getCommandPalettePosts"; import { useCommandPalettePostActions } from "@/components/CommandPalette/useCommandPalettePostActions"; // ... type Props = { post: PostForPostPage; prevPost: RelatedPostForPostLayout; nextPost: RelatedPostForPostLayout; commandPalettePosts: PostForCommandPalette[]; }; // ... export const getStaticProps: GetStaticProps = ({ params }) => { const commandPalettePosts = getCommandPalettePosts(); // ... return { props: { post, prevPost, nextPost, commandPalettePosts, }, }; }; const PostPage: NextPage = ({ post, prevPost, nextPost, commandPalettePosts, }) => { useCommandPalettePostActions(commandPalettePosts); // ... }; // ... ``` 最後修改 `src/components/CommandPalette/CommandPalette.tsx`,在 actions array 加入 Search section: ```tsx import { MagnifyingGlassIcon, // ... } from "@heroicons/react/24/outline"; // ... export default function CommandPalette({ children }: Props) { // ... const actions = [ // Page section // ... // 加入這個 section // Search section // - Search posts { id: "search-posts", name: "文章", keywords: "search find posts writing words blog articles thoughts 搜尋 尋找 文章 寫作 部落格", icon: , section: "搜尋", }, // Operation section // - Theme toggle // ... ]; return ( {children} ); } // ... ``` ## 成果 這樣就完成了!使用 `pnpm dev`,進去網站裡按下 `Ctrl + K` (Windows) 或 `Cmd + K` (Mac),或是點右上角的 Command icon,開啟 Command Palette 後,就會看到多出「搜尋文章」的操作可以執行了! 最終效果如下: ![Command palette search post light](https://i.imgur.com/76PWNBj.jpg) ![Command palette search post dark](https://i.imgur.com/9L1tBBS.jpg) 這篇修改的程式碼如下: https://github.com/eason-dev/nextjs-tailwind-contentlayer-blog-starter/compare/day26-command-palette...day27-search-post ## References - [timc1/kbar: fast, portable, and extensible cmd+k interface for your site](https://github.com/timc1/kbar) - [kbar - command+k interface for your site](https://kbar.vercel.app/docs/concepts/actions) ## 下一篇 恭喜你成功在 Command Palette 指令面板加入文章搜尋功能了! 下一篇我們終於要來完成這個系列最後一塊重點功能:「i18next 多語系支援」了! --- # Adding a Command Palette with kbar - Modern Next.js Blog Series #26 - URL: https://easonchang.com/posts/kbar-command-palette - Date: 2022-10-11T00:00:00.000Z - Language: en - Description: Installing the kbar package to implement a Command Palette, and styling it with Tailwind CSS - Translation: AI-assisted > This article is also published at [it 邦幫忙 2022 iThome Ironman Contest](https://ithelp.ithome.com.tw/articles/10307584) This is the 26th article in the "Modern Blog 30 Days" series. After adding the comment system in the previous article, we continue with another cool feature: the "Command Palette"! The final effect is as follows: ![Command palette toggle button](https://i.imgur.com/km4HREb.jpg) ![Command palette light](https://i.imgur.com/XxGq0nS.jpg) ![Command palette dark](https://i.imgur.com/cw7GbKR.jpg) The code changes for this article are as follows: https://github.com/eason-dev/nextjs-tailwind-contentlayer-blog-starter/compare/day25-giscus-comment...day26-command-palette --- ## Command Palette The Command Palette is a UI design element that has become very popular recently. You can see it in many apps and websites. For example: - Spotlight on Mac, invoked by pressing `Cmd + Space`, or its replacements [Alfred](https://www.alfredapp.com/) and [Raycast](https://www.raycast.com/) - The file search box in VSCode with `Cmd + P`, or the command input box with `Cmd + Shift + P` - Notion with `Cmd + /` - [Vercel Dashboard](https://vercel.com/dashboard) with `Cmd + K` - [React Docs (beta)](https://beta.reactjs.org/) with `Cmd + K` After pressing a specific shortcut, a search box pops up in the center of the screen, where you can type text to search the entire site or quickly perform various operations. Recently, many open-source packages have appeared that let us implement a Command Palette on our website. Here we use [kbar](https://github.com/timc1/kbar). ### Installing @heroicons/react We will assign an icon to each option in the Command Palette. Here we consistently use Heroicons made by the Tailwind CSS team ([official website](https://heroicons.com/), [Github repo](https://github.com/tailwindlabs/heroicons#react)). Enter the command to install it: ```shell pnpm add @heroicons/react ``` ### Installing @tailwindcss/line-clamp Later, when styling the Command Palette, we also want option text that is too long to be truncated to a single line, avoiding a broken layout. This effect can be achieved with CSS's [-webkit-line-clamp](https://developer.mozilla.org/en-US/docs/Web/CSS/-webkit-line-clamp). Tailwind CSS also offers the official `@tailwindcss/line-clamp` plugin for this truncation effect ([official website](https://tailwindcss.com/docs/plugins#line-clamp), [Github repo](https://github.com/tailwindlabs/tailwindcss-line-clamp)). Enter the command to install it: ```shell pnpm add -D @tailwindcss/line-clamp ``` Then modify `tailwind.config.js` to enable it: ```js // ... /** @type {import('tailwindcss').Config} */ module.exports = { // ... plugins: [ require("@tailwindcss/typography"), // 加入 @tailwindcss/line-clamp require("@tailwindcss/line-clamp"), ], }; ``` ### Installing kbar Next, install kbar, the core of our Command Palette ([official website](https://kbar.vercel.app/), [Github repo](https://github.com/timc1/kbar)): ```shell pnpm add kbar ``` ### Implementing the Command Palette We implement it with kbar, and style it with Tailwind CSS as usual. The styling code here is modified from this article: [How to implement command palette with Kbar and Tailwind CSS | by Oz Hashimoto | Prototypr](https://blog.prototypr.io/how-to-implement-command-palette-with-kbar-and-tailwind-css-71ea0e3f99c1). Add `src/components/CommandPalette/index.ts`: ```ts import CommandPalette from "./CommandPalette"; export default CommandPalette; ``` Add `src/components/CommandPalette/CommandPalette.tsx`: (If your website has more pages, or you want more executable operations, you can expand the `actions` array inside.) ```tsx // template come from: // https://blog.prototypr.io/how-to-implement-command-palette-with-kbar-and-tailwind-css-71ea0e3f99c1 import { HomeIcon, LightBulbIcon, MoonIcon, SunIcon, } from "@heroicons/react/24/outline"; import { ActionId, ActionImpl, KBarAnimator, KBarPortal, KBarPositioner, KBarProvider, KBarResults, Priority, useMatches, } from "kbar"; import { useRouter } from "next/router"; import { useTheme } from "next-themes"; import React, { forwardRef, useMemo } from "react"; import { KBarSearch } from "./KBarSearch"; type Props = { children: React.ReactNode; }; export default function CommandPalette({ children }: Props) { const router = useRouter(); const { setTheme } = useTheme(); const actions = [ // Page section { id: "home", name: "首頁", keywords: "home homepage index 首頁", perform: () => router.push("/"), icon: , section: { name: "頁面", priority: Priority.HIGH, }, }, // Operation section // - Theme toggle { id: "theme", name: "切換主題", keywords: "change toggle theme mode color 切換 更換 顏色 主題 模式", icon: , section: "操作", }, { id: "theme-light", name: "明亮模式", keywords: "theme light white mode color 顏色 主題 模式 明亮 白色", perform: () => setTheme("light"), icon: , parent: "theme", section: "操作", }, { id: "theme-dark", name: "暗黑模式", keywords: "theme dark black mode color 顏色 主題 模式 暗黑 黑色 深夜", perform: () => setTheme("dark"), icon: , parent: "theme", section: "操作", }, ]; return ( {children} ); } function CommandBar() { return ( ); } function RenderResults() { const { results, rootActionId } = useMatches(); return ( typeof item === "string" ? (
{item}
) : ( ) } /> ); } interface ResultItemProps { action: ActionImpl; active: boolean; currentRootActionId: ActionId; } type Ref = HTMLDivElement; // eslint-disable-next-line react/display-name const ResultItem = forwardRef( ( { action, active, currentRootActionId, }: { action: ActionImpl; active: boolean; currentRootActionId: ActionId; }, ref: React.Ref ) => { const ancestors = useMemo(() => { if (!currentRootActionId) return action.ancestors; const index = action.ancestors.findIndex( (ancestor) => ancestor.id === currentRootActionId ); // +1 removes the currentRootAction; e.g. // if we are on the "Set theme" parent action, // the UI should not display "Set theme… > Dark" // but rather just "Dark" return action.ancestors.slice(index + 1); }, [action.ancestors, currentRootActionId]); return (
{action.icon && action.icon}
{ancestors.length > 0 && ancestors.map((ancestor) => ( {ancestor.name} ))} {action.name}
{action.subtitle && ( {action.subtitle} )}
{action.shortcut?.length ? (
{action.shortcut.map((sc) => ( {sc} ))}
) : null}
); } ); ``` Next, because kbar currently cannot handle Chinese text input, we need to customize our own KBarSearch component as a workaround. The code is taken from this reply in a kbar issue: [can't input chinese · Issue #237 · timc1/kbar](https://github.com/timc1/kbar/issues/237#issuecomment-1253691644). Add `src/components/CommandPalette/KBarSearch.tsx`: ```tsx // Custom KBarSearch component to fix cannot input Chinese issue // A replacement of KBarSearch component from kbar // import { KBarSearch } from 'kbar'; // Copied from: https://github.com/timc1/kbar/issues/237#issuecomment-1253691644 import { useKBar, VisualState } from "kbar"; import React, { useState } from "react"; export const KBAR_LISTBOX = "kbar-listbox"; export const getListboxItemId = (id: number) => `kbar-listbox-item-${id}`; export function KBarSearch( props: React.InputHTMLAttributes & { defaultPlaceholder?: string; } ) { const { query, searchQuery, actions, currentRootActionId, activeIndex, showing, options, } = useKBar((state) => ({ searchQuery: state.searchQuery, currentRootActionId: state.currentRootActionId, actions: state.actions, activeIndex: state.activeIndex, showing: state.visualState === VisualState.showing, })); const [search, setSearch] = useState(searchQuery); const ownRef = React.useRef(null); const { defaultPlaceholder, ...rest } = props; React.useEffect(() => { query.setSearch(""); ownRef.current!.focus(); return () => query.setSearch(""); }, [currentRootActionId, query]); React.useEffect(() => { query.setSearch(search); }, [query, search]); const placeholder = React.useMemo((): string => { const defaultText = defaultPlaceholder ?? "Type a command or search…"; return currentRootActionId && actions[currentRootActionId] ? actions[currentRootActionId].name : defaultText; }, [actions, currentRootActionId, defaultPlaceholder]); return ( { props.onChange?.(event); setSearch(event.target.value); options?.callbacks?.onQueryChange?.(event.target.value); }} onKeyDown={(event) => { props.onKeyDown?.(event); if (currentRootActionId && !search && event.key === "Backspace") { const parent = actions[currentRootActionId].parent; query.setCurrentRootAction(parent); } }} /> ); } ``` Finally, modify `src/pages/_app.tsx` by wrapping the entire App with the `` component: ```tsx // ... import CommandPalette from '@/components/CommandPalette'; // ... function MyApp({ Component, pageProps }: AppProps) { // ... return ( // 用 包住整個 App // ... ); } export default MyApp; ``` This successfully adds the Command Palette using kbar. Press `Cmd + K` on the page to open it. ### Adding a Command Palette Button to the Navigation However, ordinary users will never notice that we added a Command Palette, so we also need a toggle button in the navigation, letting users trigger it manually and discover its existence. Add `src/components/CommandPaletteToggle.tsx`: ```tsx import { useKBar } from "kbar"; export default function CommandPaletteToggle() { const { query } = useKBar(); return ( ); } ``` Modify `src/components/Header.tsx` to add ``: ```tsx import CommandPaletteToggle from "@/components/CommandPaletteToggle"; // ... export default function Header() { return (
// ...
// ... // 加入
); } ``` ## Results That's it! Use `pnpm dev`, enter the website, press `Ctrl + K` (Windows) or `Cmd + K` (Mac), or click the Command icon in the top-right corner to open the Command Palette. There are currently three operations available: navigating to the homepage, switching to dark theme, and switching to light theme. The final effect is as follows: ![Command palette toggle button](https://i.imgur.com/km4HREb.jpg) ![Command palette light](https://i.imgur.com/XxGq0nS.jpg) ![Command palette dark](https://i.imgur.com/cw7GbKR.jpg) The code changes for this article are as follows: https://github.com/eason-dev/nextjs-tailwind-contentlayer-blog-starter/compare/day25-giscus-comment...day26-command-palette ## References - [timc1/kbar: fast, portable, and extensible cmd+k interface for your site](https://github.com/timc1/kbar) - [How to implement command palette with Kbar and Tailwind CSS | by Oz Hashimoto | Prototypr](https://blog.prototypr.io/how-to-implement-command-palette-with-kbar-and-tailwind-css-71ea0e3f99c1) - [星箭廣播 141 集——神力搜尋框:解決「這個功能在哪裡?」的介面設計(同場加映好用 App 推薦) | Star Rocket Blog](https://blog.starrocket.io/posts/star-rocket-podcast-ep141-what-is-power-bar-and-why-we-like-it/) - [The History of Command Palettes: How Typing Commands Became The Norm Again | Capiche](https://capiche.com/e/consumer-dev-tools-command-palette) - [can't input chinese · Issue #237 · timc1/kbar](https://github.com/timc1/kbar/issues/237#issuecomment-1253691644) ## Troubleshooting In the earlier `src/components/CommandPalette/KBarSearch.tsx`, we used TypeScript's [Non-null assertion operator](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-0.html#non-null-assertion-operator). If you see a TypeScript ESLint warning there, you can modify `.eslintrc.js` to turn off this rule: ```js module.exports = { // ... overrides: [ { files: "**/*.{ts,tsx}", // ... rules: { // 加入下面這行關掉 warning "@typescript-eslint/no-non-null-assertion": "off", }, }, ], }; ``` ## Next Article Congratulations on successfully adding the Command Palette, giving the website another dazzling feature that lets readers operate the site quickly. In the next article, we continue to expand it, enabling it to search all articles and navigate directly to specific article pages! --- # 使用 kbar 加入 Command Palette 指令面板 - Modern Next.js Blog 系列 #26 - URL: https://easonchang.com/zh-TW/posts/kbar-command-palette - Date: 2022-10-11T00:00:00.000Z - Language: zh-TW - Description: 安裝 kbar 套件來實作 Command Palette 指令面板,並使用 Tailwind CSS 美化樣式 > 本文同步發佈於 [it 邦幫忙 2022 iThome 鐵人賽](https://ithelp.ithome.com.tw/articles/10307584) 這是「Modern Blog 30 天」系列第 26 篇文章。 上一篇加完了留言系統,這篇我們繼續加入另一個酷炫功能:「Command Palette 指令面板」! 最終效果如下: ![Command palette toggle button](https://i.imgur.com/km4HREb.jpg) ![Command palette light](https://i.imgur.com/XxGq0nS.jpg) ![Command palette dark](https://i.imgur.com/cw7GbKR.jpg) 這篇修改的程式碼如下: https://github.com/eason-dev/nextjs-tailwind-contentlayer-blog-starter/compare/day25-giscus-comment...day26-command-palette --- ## Command Palette 指令面板 Command Palette 指令面板是一個最近非常流行的 UI 設計元素,在許多 App 或網頁都可以看到它。 例如: - Mac 按下 `Cmd + 空白鍵` 就能叫出的 Spotlight,或取代 Spotlight 的 [Alfred](https://www.alfredapp.com/) 和 [Raycast](https://www.raycast.com/) - VSCode 按下 `Cmd + P` 的檔案搜尋框、或按下 `Cmd + Shift + P` 的指令輸入框 - Notion 按下 `Cmd + /` - [Vercel Dashboard](https://vercel.com/dashboard) 按下 `Cmd + K` - [React Docs (beta)](https://beta.reactjs.org/) 按下 `Cmd + K` 按下特定快捷鍵後,就會在畫面中央跳出一個搜尋框,裡面可以輸入文字來搜尋全站內容,或快速執行各種操作。 最近也多出許多開源套件,能讓我們在網站內實作出 Command Palette,這裡我們使用 [kbar](https://github.com/timc1/kbar) 來實作。 ### 安裝 @heroicons/react 我們會給 Command Palette 裡各個選項指定 icon,這裡我們統一使用 Tailwind CSS 官方出的 Heroicons([官網](https://heroicons.com/)、[Github repo](https://github.com/tailwindlabs/heroicons#react))。 輸入指令安裝: ```shell pnpm add @heroicons/react ``` ### 安裝 @tailwindcss/line-clamp 後續切版 Command Palette 時,我們也希望當選項文字太長時,能截斷文字只顯示一行,避免跑版。 這種效果可以使用 CSS 的 [-webkit-line-clamp](https://developer.mozilla.org/en-US/docs/Web/CSS/-webkit-line-clamp) 來實現。 Tailwind CSS 官方也有推出 `@tailwindcss/line-clamp` plugin 來實現斷行效果([官網](https://tailwindcss.com/docs/plugins#line-clamp)、[Github repo](https://github.com/tailwindlabs/tailwindcss-line-clamp)) 輸入指令安裝: ```shell pnpm add -D @tailwindcss/line-clamp ``` 然後修改 `tailwind.config.js` 來啟用它: ```js // ... /** @type {import('tailwindcss').Config} */ module.exports = { // ... plugins: [ require("@tailwindcss/typography"), // 加入 @tailwindcss/line-clamp require("@tailwindcss/line-clamp"), ], }; ``` ### 安裝 kbar 接著來安裝 Command Palette 主體的 kbar([官網](https://kbar.vercel.app/)、[Github repo](https://github.com/timc1/kbar)): ```shell pnpm add kbar ``` ### 實作 Command Palette 使用 kbar 實作它,切版一樣使用 Tailwind CSS。 這邊切版的程式是修改自這篇文章「[How to implement command palette with Kbar and Tailwind CSS | by Oz Hashimoto | Prototypr](https://blog.prototypr.io/how-to-implement-command-palette-with-kbar-and-tailwind-css-71ea0e3f99c1)」的。 新增 `src/components/CommandPalette/index.ts`: ```ts import CommandPalette from "./CommandPalette"; export default CommandPalette; ``` 新增 `src/components/CommandPalette/CommandPalette.tsx`: (如果你的網站有更多頁面、或想要更多可執行操作,可以擴充裡面的 `actions` array) ```tsx // template come from: // https://blog.prototypr.io/how-to-implement-command-palette-with-kbar-and-tailwind-css-71ea0e3f99c1 import { HomeIcon, LightBulbIcon, MoonIcon, SunIcon, } from "@heroicons/react/24/outline"; import { ActionId, ActionImpl, KBarAnimator, KBarPortal, KBarPositioner, KBarProvider, KBarResults, Priority, useMatches, } from "kbar"; import { useRouter } from "next/router"; import { useTheme } from "next-themes"; import React, { forwardRef, useMemo } from "react"; import { KBarSearch } from "./KBarSearch"; type Props = { children: React.ReactNode; }; export default function CommandPalette({ children }: Props) { const router = useRouter(); const { setTheme } = useTheme(); const actions = [ // Page section { id: "home", name: "首頁", keywords: "home homepage index 首頁", perform: () => router.push("/"), icon: , section: { name: "頁面", priority: Priority.HIGH, }, }, // Operation section // - Theme toggle { id: "theme", name: "切換主題", keywords: "change toggle theme mode color 切換 更換 顏色 主題 模式", icon: , section: "操作", }, { id: "theme-light", name: "明亮模式", keywords: "theme light white mode color 顏色 主題 模式 明亮 白色", perform: () => setTheme("light"), icon: , parent: "theme", section: "操作", }, { id: "theme-dark", name: "暗黑模式", keywords: "theme dark black mode color 顏色 主題 模式 暗黑 黑色 深夜", perform: () => setTheme("dark"), icon: , parent: "theme", section: "操作", }, ]; return ( {children} ); } function CommandBar() { return ( ); } function RenderResults() { const { results, rootActionId } = useMatches(); return ( typeof item === "string" ? (
{item}
) : ( ) } /> ); } interface ResultItemProps { action: ActionImpl; active: boolean; currentRootActionId: ActionId; } type Ref = HTMLDivElement; // eslint-disable-next-line react/display-name const ResultItem = forwardRef( ( { action, active, currentRootActionId, }: { action: ActionImpl; active: boolean; currentRootActionId: ActionId; }, ref: React.Ref ) => { const ancestors = useMemo(() => { if (!currentRootActionId) return action.ancestors; const index = action.ancestors.findIndex( (ancestor) => ancestor.id === currentRootActionId ); // +1 removes the currentRootAction; e.g. // if we are on the "Set theme" parent action, // the UI should not display "Set theme… > Dark" // but rather just "Dark" return action.ancestors.slice(index + 1); }, [action.ancestors, currentRootActionId]); return (
{action.icon && action.icon}
{ancestors.length > 0 && ancestors.map((ancestor) => ( {ancestor.name} ))} {action.name}
{action.subtitle && ( {action.subtitle} )}
{action.shortcut?.length ? (
{action.shortcut.map((sc) => ( {sc} ))}
) : null}
); } ); ``` 接著因為 kbar 目前無法輸入中文字,所以需要客製化自己的 KbarSearch 元件來 workaround。 程式取自 kbar issue 的這篇回覆:[can't input chinese · Issue #237 · timc1/kbar](https://github.com/timc1/kbar/issues/237#issuecomment-1253691644)。 新增 `src/components/CommandPalette/KBarSearch.tsx`: ```tsx // Custom KBarSearch component to fix cannot input Chinese issue // A replacement of KBarSearch component from kbar // import { KBarSearch } from 'kbar'; // Copied from: https://github.com/timc1/kbar/issues/237#issuecomment-1253691644 import { useKBar, VisualState } from "kbar"; import React, { useState } from "react"; export const KBAR_LISTBOX = "kbar-listbox"; export const getListboxItemId = (id: number) => `kbar-listbox-item-${id}`; export function KBarSearch( props: React.InputHTMLAttributes & { defaultPlaceholder?: string; } ) { const { query, searchQuery, actions, currentRootActionId, activeIndex, showing, options, } = useKBar((state) => ({ searchQuery: state.searchQuery, currentRootActionId: state.currentRootActionId, actions: state.actions, activeIndex: state.activeIndex, showing: state.visualState === VisualState.showing, })); const [search, setSearch] = useState(searchQuery); const ownRef = React.useRef(null); const { defaultPlaceholder, ...rest } = props; React.useEffect(() => { query.setSearch(""); ownRef.current!.focus(); return () => query.setSearch(""); }, [currentRootActionId, query]); React.useEffect(() => { query.setSearch(search); }, [query, search]); const placeholder = React.useMemo((): string => { const defaultText = defaultPlaceholder ?? "Type a command or search…"; return currentRootActionId && actions[currentRootActionId] ? actions[currentRootActionId].name : defaultText; }, [actions, currentRootActionId, defaultPlaceholder]); return ( { props.onChange?.(event); setSearch(event.target.value); options?.callbacks?.onQueryChange?.(event.target.value); }} onKeyDown={(event) => { props.onKeyDown?.(event); if (currentRootActionId && !search && event.key === "Backspace") { const parent = actions[currentRootActionId].parent; query.setCurrentRootAction(parent); } }} /> ); } ``` 最後修改 `src/pages/_app.tsx`,用 `` 元件包住整個 App: ```tsx // ... import CommandPalette from '@/components/CommandPalette'; // ... function MyApp({ Component, pageProps }: AppProps) { // ... return ( // 用 包住整個 App // ... ); } export default MyApp; ``` 這樣就成功使用 kbar 加入 Command Palette 了,在網頁內按下 `Cmd + K` 就能開啟了。 ### 在 navigation 加入 Command Palette 按鈕 但正常使用者根本不會發現我們加了 Command Palette,因此我們還需要在 navigation 加入開啟按鈕,讓使用者能手動觸發,進而發現它的存在。 新增 `src/components/CommandPaletteToggle.tsx`: ```tsx import { useKBar } from "kbar"; export default function CommandPaletteToggle() { const { query } = useKBar(); return ( ); } ``` 修改 `src/components/Header.tsx`,加入 ``: ```tsx import CommandPaletteToggle from "@/components/CommandPaletteToggle"; // ... export default function Header() { return (
// ...
// ... // 加入
); } ``` ## 成果 這樣就完成了!使用 `pnpm dev`,進去網站裡按下 `Ctrl + K` (Windows) 或 `Cmd + K` (Mac),或是點右上角的 Command icon,就能開啟 Command Palette 了。 裡面目前能執行的操作有三個:瀏覽首頁、切換深色主題、切換明亮主題。 最終效果如下: ![Command palette toggle button](https://i.imgur.com/km4HREb.jpg) ![Command palette light](https://i.imgur.com/XxGq0nS.jpg) ![Command palette dark](https://i.imgur.com/cw7GbKR.jpg) 這篇修改的程式碼如下: https://github.com/eason-dev/nextjs-tailwind-contentlayer-blog-starter/compare/day25-giscus-comment...day26-command-palette ## References - [timc1/kbar: fast, portable, and extensible cmd+k interface for your site](https://github.com/timc1/kbar) - [How to implement command palette with Kbar and Tailwind CSS | by Oz Hashimoto | Prototypr](https://blog.prototypr.io/how-to-implement-command-palette-with-kbar-and-tailwind-css-71ea0e3f99c1) - [星箭廣播 141 集——神力搜尋框:解決「這個功能在哪裡?」的介面設計(同場加映好用 App 推薦) | Star Rocket Blog](https://blog.starrocket.io/posts/star-rocket-podcast-ep141-what-is-power-bar-and-why-we-like-it/) - [The History of Command Palettes: How Typing Commands Became The Norm Again | Capiche](https://capiche.com/e/consumer-dev-tools-command-palette) - [can't input chinese · Issue #237 · timc1/kbar](https://github.com/timc1/kbar/issues/237#issuecomment-1253691644) ## Troubleshooting 在前面的 `src/components/CommandPalette/KBarSearch.tsx`,裡面有用到 TypeScript 的 [Non-null assertion operator](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-0.html#non-null-assertion-operator)。 如果你發現那邊有 TypeScript Eslint 的 warning 的話,可以修改 `.eslintrc.js`,把這條 rule 關掉: ```js module.exports = { // ... overrides: [ { files: "**/*.{ts,tsx}", // ... rules: { // 加入下面這行關掉 warning "@typescript-eslint/no-non-null-assertion": "off", }, }, ], }; ``` ## 下一篇 恭喜你成功新增了 Command Palette 指令面板,讓網站多了一個炫砲功能,方便讀者快速操作網站。 下一篇我們繼續來擴充它,讓他能搜尋所有文章並跳轉到特定文章內頁! --- # Adding a Comment System to Next.js with giscus - Modern Next.js Blog Series #25 - URL: https://easonchang.com/posts/giscus-comment-system - Date: 2022-10-10T00:00:00.000Z - Language: en - Description: Using @giscus/react to add a comment system within a Next.js blog, binding to Github Discussions as the commenting platform > This article is also published at [it 邦幫忙 2022 iThome Ironman Contest](https://ithelp.ithome.com.tw/articles/10306883) This is the 25th article in the "Modern Blog 30 Days" series. After adding a side table of contents in the previous article, we will now add another dazzling and useful feature: a comment system! The final effect is as follows: ![comment system light](https://i.imgur.com/OVeOIys.jpg) ![comment system dark](https://i.imgur.com/CQFRPwL.jpg) The code changes for this article are as follows: https://github.com/eason-dev/nextjs-tailwind-contentlayer-blog-starter/compare/day24-table-of-contents...day25-giscus-comment --- ## Adding a Comment System to Articles After all the hard work producing numerous articles, our hope is to communicate with the world and our readers, thus we need a place for readers to leave comments and feedback. Let's add a comment system! To implement a comment system, besides laboriously building our own database and API, there are also simpler existing services available for direct use, such as [disqus-react](https://github.com/disqus/disqus-react). Here, we choose [Giscus](https://giscus.app/en) as our commenting service. Giscus is a Github App that binds to Github Discussions of a specified repo to serve as the comment board, supporting multiple languages and customizable styles. Readers simply need to log in with their Github account to leave comments. ### Binding giscus to Your Github Repo First, you need to select a Public Github repo to place comments. If you deployed your Next.js project to Vercel from a Github repo as described in [Day 3 of Deploying Next.js Project to Vercel](/posts/nextjs-to-vercel), you should already have a Github repo, which can be used to place comments. First, you need to go to your repo's settings page and turn on the Discussions feature: ![Github repo settings page](https://i.imgur.com/hIiqcws.jpg) ![Enable Github Discussions](https://i.imgur.com/EOr5NtV.jpg) Then, enable the giscus Github App on your account by clicking the link below to activate it: [https://github.com/apps/giscus](https://github.com/apps/giscus) After activation, you will see the giscus settings page as shown below. Set up Repository access, choose the repo you just activated Discussions for, and allow giscus to operate its Discussions: ![Enable giscus](https://i.imgur.com/Pd9WjVC.jpg) This completes the binding process. ### Verify Binding Status on the giscus Website and Obtain repo ID and category ID Click the link below to go to the giscus website: [https://giscus.app/en](https://giscus.app/en) There you can verify the activation status of giscus for your repo and set up giscus themes, languages, comment box positions, etc., and generate corresponding code. First, copy your repo name (username/repo_name) into it to verify that giscus binding is completed. Then, choose which category of Github Discussions to store comments in. We follow giscus's suggestion and choose "Announcements": ![Configure giscus](https://i.imgur.com/TkkIJZs.jpg) Scroll down, and you will see the code block. Please copy the `data-repo-id` and `data-category-id` from here; we will use them in Next.js: ![Copy repo ID](https://i.imgur.com/PCM4IF2.jpg) ### Install @giscus/react in Next.js and Add Comment Block Install `@giscus/react`: ```shell pnpm add @giscus/react ``` Add `src/configs/giscusConfigs.ts`, filling in the bound repo name, and the copied `repoId`, `categoryId`: ```ts export const giscusConfigs = { repo: "username/repo_name" as `${string}/${string}`, repoId: "R_xxxxxxxxxxx", category: "Announcements", categoryId: "DIC_xxxxxxxxxxxx", }; ``` Add `src/components/Comment.tsx`: ```tsx import Giscus from "@giscus /react"; import { useTheme } from "next-themes"; import { giscusConfigs } from "@/configs/giscusConfigs"; const Comment = () => { const { theme } = useTheme(); return (
); }; export default Comment; ``` Modify `src/components/PostLayout.tsx` to include the `` component: ```tsx import { useRouter } from "next/router"; import Comment from "@/components/Comment"; // ... export default function PostLayout({ post, nextPost, prevPost, children, }: Props) { // ... return (
// ...
// ...
); } ``` ## Results That's it! Use `pnpm dev`, and when you enter an article page, you will see the comment section added at the bottom. Log in with your Github account to leave comments and send emojis. The final effect is as follows: ![comment system light](https://i.imgur.com/OVeOIys.jpg) ![comment system dark](https://i.imgur.com/CQFRPwL.jpg) Going to the Discussions page of your bound Github repo, you will also see that comments are actually stored in Discussions: ![comments in Github Discussions](https://i.imgur.com/QqrH2ad.png) You will receive emails to your Github email inbox for any reader comments, so you won't miss any feedback. The code changes for this article are as follows: https://github.com/eason-dev/nextjs-tailwind-contentlayer-blog-starter/compare/day24-table-of-contents...day25-giscus-comment ## References - [giscus/giscus: A comment system powered by GitHub Discussions.](https://github.com/giscus/giscus) - [giscus homepage](https://giscus.app/en) ## Next Article Congratulations on successfully adding a comment system, giving you the opportunity to interact with your readers! In the next article, we will continue to add more dazzling and useful features: "Command Palette"! --- # 使用 giscus 在 Next.js 加入留言系統 - Modern Next.js Blog 系列 #25 - URL: https://easonchang.com/zh-TW/posts/giscus-comment-system - Date: 2022-10-10T00:00:00.000Z - Language: zh-TW - Description: 使用 @giscus/react 來在 Next.js 部落格內加入留言系統,綁定 Github Discussions 來當作留言板 > 本文同步發佈於 [it 邦幫忙 2022 iThome 鐵人賽](https://ithelp.ithome.com.tw/articles/10306883) 這是「Modern Blog 30 天」系列第 25 篇文章。 上一篇加完了文章側邊目錄,這篇我們再來加入另一個炫砲且實用的功能:留言系統! 最終效果如下: ![comment system light](https://i.imgur.com/OVeOIys.jpg) ![comment system dark](https://i.imgur.com/CQFRPwL.jpg) 這篇修改的程式碼如下: https://github.com/eason-dev/nextjs-tailwind-contentlayer-blog-starter/compare/day24-table-of-contents...day25-giscus-comment --- ## 在文章內加入留言系統 我們辛辛苦苦產出許多文章,希望的就是跟這個世界、跟讀者們交流,因此我們需要一個地方讓讀者能留言、回饋意見。 讓我們來加入留言系統吧! 要實作留言系統,除了辛苦地自建資料庫和 API 來實作,也有其他更簡單的現成服務能直接使用,像是 [disqus-react](https://github.com/disqus/disqus-react)。 這裡我們選用 [Giscus](https://giscus.app/zh-TW) 這套留言服務。 giscus 是一個 Github 的 App,綁定指定 repo 的 Github Discussions 來當作留言板,支援多語系和客製化樣式,讀者只要登入 Github 帳號就能留言。 ### 將 giscus 綁定 Github repo 首先你需要選一個 Public 的 Github repo 來放置留言,如果你在本系列文 [Day3 將 Next.js 專案部署上 Vercel 平台](/posts/nextjs-to-vercel) 裡面是從 Github repo 來部署上 Vercel 平台的話,你應該已經有 Github repo 了,可以使用它來放置留言。 首先你需要進到你 repo 的設定頁,然後將 Discussions 功能打開: ![Github repo settings page](https://i.imgur.com/hIiqcws.jpg) ![Enable Github Discussions](https://i.imgur.com/EOr5NtV.jpg) 接著在你的帳號啟用 giscus Github App,點下面連結進去啟用它: [https://github.com/apps/giscus](https://github.com/apps/giscus) 啟用完會看到下圖的 giscus 設定頁,設定 Repository access,選擇剛剛的 repo,允許 giscus 操作它的 Discussions: ![Enable giscus](https://i.imgur.com/Pd9WjVC.jpg) 這樣就完成綁定了 ### 在 giscus 官網驗證綁定狀態,並取得 repo ID 和 category ID 點下面連結進去 giscus 官網: [https://giscus.app/zh-TW](https://giscus.app/zh-TW) 裡面可以驗證 repo 的 giscus 啟用狀況,並設定 giscus 主題、語言、留言框位置等設定,並產出對應程式碼。 首先複製你的 repo 名稱(username/repo_name)進去,驗證是否已完成 giscus 綁定。 接著選擇要用哪種 Github Discussions 的分類存放留言,這裡我們遵照 giscus 的建議,選擇「Announcements」: ![Configure giscus](https://i.imgur.com/TkkIJZs.jpg) 接著畫面往下滑,就會看到程式碼區塊了。 請你複製這邊的 `data-repo-id` 和 `data-category-id`,等等在 Next.js 裡會用到: ![Cpoy repo ID](https://i.imgur.com/PCM4IF2.jpg) ### 在 Next.js 裡安裝 @giscus/react,加入留言區塊 安裝 `@giscus/react`: ```shell pnpm add @giscus/react ``` 新增 `src/configs/giscusConfigs.ts`,並填入綁定好的 repo 名稱,和剛剛複製的、`repoId`、`categoryId`: ```ts export const giscusConfigs = { repo: "username/repo_name" as `${string}/${string}`, repoId: "R_xxxxxxxxxxx", category: "Announcements", categoryId: "DIC_xxxxxxxxxxxx", }; ``` 新增 `src/components/Comment.tsx`: ```tsx import Giscus from "@giscus/react"; import { useTheme } from "next-themes"; import { giscusConfigs } from "@/configs/giscusConfigs"; const Comment = () => { const { theme } = useTheme(); return (
); }; export default Comment; ``` 修改 `src/components/PostLayout.tsx`,加入上面的 `` 元件: ```tsx import { useRouter } from "next/router"; import Comment from "@/components/Comment"; // ... export default function PostLayout({ post, nextPost, prevPost, children, }: Props) { // ... return (
// ...
{children}
{/* DESKTOP TABLE OF CONTENTS */}
// ...
); } ``` ## 成果 這樣就完成了!使用 `pnpm dev`,進文章內頁就會看到最下面多出留言區塊了。 登入 Github 帳號就能留言和發送表情了。 最終效果如下: ![comment system light](https://i.imgur.com/OVeOIys.jpg) ![comment system dark](https://i.imgur.com/CQFRPwL.jpg) 進去你綁定的 Github repo 的 Discussions 頁面,也會看到留言實際上是存在 Discussions 裡面: ![comments in Github Discussions](https://i.imgur.com/QqrH2ad.png) 有讀者留言的話都會寄信到你的 Github email 信箱,不用擔心漏掉任何留言。 這篇修改的程式碼如下: https://github.com/eason-dev/nextjs-tailwind-contentlayer-blog-starter/compare/day24-table-of-contents...day25-giscus-comment ## References - [giscus/giscus: A comment system powered by GitHub Discussions.](https://github.com/giscus/giscus) - [giscus homepage](https://giscus.app/zh-TW) ## 下一篇 恭喜你成功加完了留言系統,讓你有了跟讀者互動的機會! 下一篇我們繼續加入更多實用炫砲的功能:「Command Palette 指令面板」! --- # Adding a Table of Contents to MDX Articles - Modern Next.js Blog Series #24 - URL: https://easonchang.com/posts/post-side-toc - Date: 2022-10-09T00:00:00.000Z - Language: en - Description: Adding a sidebar table of contents to Contentlayer blog articles to provide readers with a clear view of the article structure, using github-slugger and IntersectionObserver > This article is also published at [it 邦幫忙 2022 iThome Ironman Contest](https://ithelp.ithome.com.tw/articles/10306476) After adding a page progress bar in the previous article, we continue with another dazzling and useful feature: "Article Sidebar Table of Contents," allowing readers to grasp the structure of the article at a glance! The final effect is as follows: ![table of contents dark](https://i.imgur.com/W6Zsfzs.jpg) ![table of contents light](https://i.imgur.com/FLUBZfe.jpg) The code changes for this article are as follows: https://github.com/eason-dev/nextjs-tailwind-contentlayer-blog-starter/compare/day23-nprogress...day24-table-of-contents --- ## Adding a Table of Contents to the Side of Article Pages We will add two types of interactions for the upcoming table of contents: one is highlighting the currently viewed section title in red as you scroll through the article, and the other is auto-scrolling to the corresponding section of the article when you click any title in the table. The latter effect, scrolling on click, utilizes the anchor links added with [rehype-slug](https://github.com/rehypejs/rehype-slug) in [Article 20: Adding Anchor Links to Headings](/posts/post-heading-anchor). ### Installing github-slugger to Convert Title Text to Slugs The table of contents needs to know the titles and their anchors within the article. Here, we need [github-slugger](https://github.com/Flet/github-slugger) to convert title text to slugs. The internal implementation of rehype-slug also uses github-slugger for slug conversion, so there's no need to worry about inconsistency. Let's install github-slugger: ```shell pnpm add github-slugger pnpm add -D @types/github-slugger ``` ### Passing the Original MDX Content of Articles to Article Pages to Capture All Titles The sidebar table of contents needs to know all the titles in the article. We will implement it to only display `h2` and `h3` titles. We can extract all titles from the `##` and `###` in the original MDX content of the article. The Post object passed to us by Contentlayer includes a `body.raw` attribute, where we can get the original MDX content of the article. Modify `src/pages/posts/[slug].tsx` to pass it to the article page: ```tsx // ... export const getStaticProps: GetStaticProps = ({ params }) => { // ... const post: PostForPostPage = { title: postFull.title, date: postFull.date, description: postFull.description, path: postFull.path, socialImage: postFull.socialImage || null, body: { code: postFull.body.code, // Add the following line raw: postFull.body.raw, }, }; // ... ``` ### Adding the `` Component to Display the Table of Contents on the Side of Article Pages Add `src/components/TableOfContents.tsx`, where the logic and styles for the table of contents are defined: ```tsx // ref: https://github.com/ekomenyong/kommy-mdx/blob/main/src/components/TOC.tsx import clsx from "clsx"; import GithubSlugger from "github-slugger"; import { useEffect, useRef, useState } from "react"; // eslint-disable-next-line no-unused-vars type UseIntersectionObserverType = (setActiveId: (id: string) => void) => void; const useIntersectionObserver: UseIntersectionObserverType = (setActiveId) => { const headingElementsRef = useRef<{ [key: string]: IntersectionObserverEntry; }>({}); useEffect(() => { const callback = (headings: IntersectionObserverEntry[]) => { headingElementsRef.current = headings.reduce((map, headingElement) => { map[headingElement.target.id] = headingElement; return map; }, headingElementsRef.current); const visibleHeadings: IntersectionObserverEntry[] = []; Object.keys(headingElementsRef.current).forEach((key) => { const headingElement = headingElementsRef.current[key]; if (headingElement.is Intersecting) visibleHeadings.push(headingElement); }); const getIndexFromId = (id: string) => headingElements.findIndex((heading) => heading.id === id); if (visibleHeadings.length === 1) { setActiveId(visibleHeadings[0].target.id); } else if (visibleHeadings.length > 1) { const sortedVisibleHeadings = visibleHeadings.sort( (a, b) => getIndexFromId(b.target.id) - getIndexFromId(a.target.id) ); setActiveId(sortedVisibleHeadings[0].target.id); } }; const observer = new IntersectionObserver(callback, { rootMargin: "0px 0px -70% 0px", }); const headingElements = Array.from( document.querySelectorAll("article h2,h3") ); headingElements.forEach((element) => observer.observe(element)); return () => observer.disconnect(); }, [setActiveId]); }; type Props = { source: string; }; const TableOfContents = ({ source }: Props) => { const headingLines = source .split("\n") .filter((line) => line.match(/^###?\s/)); const headings = headingLines.map((raw) => { const text = raw.replace(/^###*\s/, ""); const level = raw.slice(0, 3) === "###" ? 3 : 2; const slugger = new GithubSlugger(); return { text, level, id: slugger.slug(text), }; }); const [activeId, setActiveId] = useState(); useIntersectionObserver(setActiveId); return (

Table of Contents

{headings.map((heading, index) => { return ( ); })}
); }; export default TableOfContents; ``` The logic is mostly modified from [EkomEnyong.com](https://ekomenyong.com/posts/react-nextjs-developer-tech-stack-tools) blog's [TOC.tsx file](https://github.com/ekomenyong/kommy-mdx/blob/main/src/components/TOC.tsx), and the styles are inspired by [Tailwind Nextjs Starter Blog](https://tailwind-nextjs-starter-blog.vercel.app/blog/nested-route/introducing-multi-part-posts-with-nested-routing) ([repo](https://github.com/timlrx/tailwind-nextjs-starter-blog)). Finally, modify `src/components/PostLayout.tsx` to use it: ```tsx import { useRouter } from "next/router"; import CustomLink from "@/components/CustomLink"; import PageTitle from "@/components/PageTitle"; import PostBody from "@/components/PostBody"; import TableOfContents from "@/components/TableOfContents"; import formatDate from "@/lib/formatDate"; export interface PostForPostLayout { date: string; title: string; body: { raw: string }; } // ... export default function PostLayout({ post, nextPost, prevPost, children, }: Props) { const { date, title, body: { raw }, } = post; const { locale } = useRouter(); return (
// ...
{children}
{/* DESKTOP TABLE OF CONTENTS */}
// ...
); } ``` ## Results Done! Use `pnpm dev`, and when you view an article page on a desktop browser, you will see the table of contents added to the side! All `h2` and `h3` subheadings will be displayed, with the current reading section highlighted in red as you scroll through the page, and clicking on a title will scroll to the specified section. The final effect is as follows: ![table of contents dark](https://i.imgur.com/W6Zsfzs.jpg) ![table of contents light](https://i.imgur.com/FLUBZfe.jpg) The code changes for this article are as follows: https://github.com/eason-dev/nextjs-tailwind-contentlayer-blog-starter/compare/day23-nprogress...day24-table-of-contents ## References - [kommy-mdx/TOC.tsx at main · ekomenyong/kommy-mdx](https://github.com/ekomenyong/kommy-mdx/blob/main/src/components/TOC.tsx) - [timlrx/tailwind-nextjs-starter-blog: This is a Next.js, Tailwind CSS blogging starter template. Comes out of the box configured with the latest technologies to make technical writing a breeze. Easily configurable and customizable. Perfect as a replacement to existing Jekyll and Hugo individual blogs.](https://github.com/timlrx/tailwind-nextjs-starter-blog) - [Flet/github-slugger: Generate a slug just like GitHub does for markdown headings.](https://github.com/Flet/github-slugger) ## Next Article Congratulations on successfully adding a sidebar table of contents, providing readers with a clear overview of the article structure! In the next article, we will continue to add another dazzling and useful feature: Comment functionality! --- # 在 MDX 文章側邊加入目錄 - Modern Next.js Blog 系列 #24 - URL: https://easonchang.com/zh-TW/posts/post-side-toc - Date: 2022-10-09T00:00:00.000Z - Language: zh-TW - Description: 在 Contentlayer 部落格加入文章側邊目錄,讓讀者一目瞭然文章結構,使用 github-slugger 及 IntersectionObserver > 本文同步發佈於 [it 邦幫忙 2022 iThome 鐵人賽](https://ithelp.ithome.com.tw/articles/10306476) 上一篇加完了換頁進度條,這篇我們繼續加入另一個炫砲且實用的功能:「文章側邊目錄」,讓讀者一目瞭然文章結構! 最終效果如下: ![table of contents dark](https://i.imgur.com/W6Zsfzs.jpg) ![table of contents light](https://i.imgur.com/FLUBZfe.jpg) 這篇修改的程式碼如下: https://github.com/eason-dev/nextjs-tailwind-contentlayer-blog-starter/compare/day23-nprogress...day24-table-of-contents --- ## 在文章內頁側邊加入目錄 我們稍後會加入的目錄有兩種互動效果,一種是捲動文章時,目錄會用紅字標出目前瀏覽的區塊標題,另一種是點目錄裡的任何標題時,就會自動捲動到對應文章區塊。 後面這項點擊後捲動的效果,是搭配先前 [第 20 篇:為內文小標題加入 anchor 錨點連結](/posts/post-heading-anchor) 裡使用 [rehype-slug](https://github.com/rehypejs/rehype-slug) 加完的 anchor 來實現的。 ### 安裝 github-slugger 來將標題文字轉成 slug 目錄需要知道內文有哪些標題,以及他們的 anchor。這裡需要用到 [github-slugger](https://github.com/Flet/github-slugger) 來將標題文字轉成 slug。 rehype-slug 內部實作也是用 github-slugger 來轉換 slug 的,所以不用擔心轉換出來不一致。 讓我們先來安裝 github-slugger: ```shell pnpm add github-slugger pnpm add -D @types/github-slugger ``` ### 將文章原始 MDX 內容傳遞進文章內頁,用來抓取所有標題 側邊目錄需要知道文章所有標題。這裡我們會實作成只顯示 `h2` 和 `h3` 標題。 我們可以從文章 MDX 原始內容裡的 `##`、`###`,來抓出所有標題。 Contentlayer 傳給我們的 Post 物件裡,有包含 `body.raw` 屬性,這裡就能取得文章原始 MDX 內容。 修改 `src/pages/posts/[slug].tsx`,把它傳進文章內頁: ```tsx // ... export const getStaticProps: GetStaticProps = ({ params }) => { // ... const post: PostForPostPage = { title: postFull.title, date: postFull.date, description: postFull.description, path: postFull.path, socialImage: postFull.socialImage || null, body: { code: postFull.body.code, // 加入下面這行 raw raw: postFull.body.raw, }, }; // ... ``` ### 新增 `` 元件,在文章內頁側邊顯示目錄 新增 `src/components/TableOfContents.tsx`,目錄的程式邏輯和樣式都在這裡: ```tsx // ref: https://github.com/ekomenyong/kommy-mdx/blob/main/src/components/TOC.tsx import clsx from "clsx"; import GithubSlugger from "github-slugger"; import { useEffect, useRef, useState } from "react"; // eslint-disable-next-line no-unused-vars type UseIntersectionObserverType = (setActiveId: (id: string) => void) => void; const useIntersectionObserver: UseIntersectionObserverType = (setActiveId) => { const headingElementsRef = useRef<{ [key: string]: IntersectionObserverEntry; }>({}); useEffect(() => { const callback = (headings: IntersectionObserverEntry[]) => { headingElementsRef.current = headings.reduce((map, headingElement) => { map[headingElement.target.id] = headingElement; return map; }, headingElementsRef.current); const visibleHeadings: IntersectionObserverEntry[] = []; Object.keys(headingElementsRef.current).forEach((key) => { const headingElement = headingElementsRef.current[key]; if (headingElement.isIntersecting) visibleHeadings.push(headingElement); }); const getIndexFromId = (id: string) => headingElements.findIndex((heading) => heading.id === id); if (visibleHeadings.length === 1) { setActiveId(visibleHeadings[0].target.id); } else if (visibleHeadings.length > 1) { const sortedVisibleHeadings = visibleHeadings.sort( (a, b) => getIndexFromId(b.target.id) - getIndexFromId(a.target.id) ); setActiveId(sortedVisibleHeadings[0].target.id); } }; const observer = new IntersectionObserver(callback, { rootMargin: "0px 0px -70% 0px", }); const headingElements = Array.from( document.querySelectorAll("article h2,h3") ); headingElements.forEach((element) => observer.observe(element)); return () => observer.disconnect(); }, [setActiveId]); }; type Props = { source: string; }; const TableOfContents = ({ source }: Props) => { const headingLines = source .split("\n") .filter((line) => line.match(/^###?\s/)); const headings = headingLines.map((raw) => { const text = raw.replace(/^###*\s/, ""); const level = raw.slice(0, 3) === "###" ? 3 : 2; const slugger = new GithubSlugger(); return { text, level, id: slugger.slug(text), }; }); const [activeId, setActiveId] = useState(); useIntersectionObserver(setActiveId); return (

目錄

{headings.map((heading, index) => { return ( ); })}
); }; export default TableOfContents; ``` 邏輯大部分是修改自 [EkomEnyong.com](https://ekomenyong.com/posts/react-nextjs-developer-tech-stack-tools) 部落格的 [這隻 TOC.tsx 檔案](https://github.com/ekomenyong/kommy-mdx/blob/main/src/components/TOC.tsx) 的,樣式則參考了 [Tailwind Nextjs Starter Blog](https://tailwind-nextjs-starter-blog.vercel.app/blog/nested-route/introducing-multi-part-posts-with-nested-routing) ([repo](https://github.com/timlrx/tailwind-nextjs-starter-blog))。 最後修改 `src/components/PostLayout.tsx` 來使用它吧: ```tsx import { useRouter } from "next/router"; import CustomLink from "@/components/CustomLink"; import PageTitle from "@/components/PageTitle"; import PostBody from "@/components/PostBody"; import TableOfContents from "@/components/TableOfContents"; import formatDate from "@/lib/formatDate"; export interface PostForPostLayout { date: string; title: string; body: { raw: string }; } // ... export default function PostLayout({ post, nextPost, prevPost, children, }: Props) { const { date, title, body: { raw }, } = post; const { locale } = useRouter(); return (
// ...
{children}
{/* DESKTOP TABLE OF CONTENTS */}
// ...
); } ``` ## 成果 完成了!使用 `pnpm dev`,進文章內頁用電腦版瀏覽,就會看到側邊多出目錄了! 所有 `h2`、`h3` 小標題都會顯示,捲動頁面時會用紅色標出目前讀的段落,點標題則會捲動到指定段落。 最終效果如下: ![table of contents dark](https://i.imgur.com/W6Zsfzs.jpg) ![table of contents light](https://i.imgur.com/FLUBZfe.jpg) 這篇修改的程式碼如下: https://github.com/eason-dev/nextjs-tailwind-contentlayer-blog-starter/compare/day23-nprogress...day24-table-of-contents ## References - [kommy-mdx/TOC.tsx at main · ekomenyong/kommy-mdx](https://github.com/ekomenyong/kommy-mdx/blob/main/src/components/TOC.tsx) - [timlrx/tailwind-nextjs-starter-blog: This is a Next.js, Tailwind CSS blogging starter template. Comes out of the box configured with the latest technologies to make technical writing a breeze. Easily configurable and customizable. Perfect as a replacement to existing Jekyll and Hugo individual blogs.](https://github.com/timlrx/tailwind-nextjs-starter-blog) - [Flet/github-slugger: Generate a slug just like GitHub does for markdown headings.](https://github.com/Flet/github-slugger) ## 下一篇 恭喜你成功加完了文章側邊目錄,讓讀者能一目瞭然文章結構! 下一篇我們繼續加入另一個炫砲且實用的功能:留言功能! --- # Adding a Page Progress Bar with nprogress - Modern Next.js Blog Series #23 - URL: https://easonchang.com/posts/nprogress - Date: 2022-10-08T00:00:00.000Z - Language: en - Description: Improving perceived performance for readers by adding a page transition progress bar with the nprogress package > This article is also published at [it 邦幫忙 2022 iThome Ironman Contest](https://ithelp.ithome.com.tw/articles/10306167) This is the 23rd article in the "Modern Blog 30 Days" series. After optimizing image performance in the previous article, we continue to improve perceived performance for readers by adding a page transition progress bar using [nprogress](https://github.com/rstacruz/nprogress)! The final effect is as follows: ![progress bar animation](https://i.imgur.com/BpakgNT.gif) The code changes for this article are as follows: https://github.com/eason-dev/nextjs-tailwind-contentlayer-blog-starter/compare/day22-custom-image...day23-nprogress --- ## Adding a Page Progress Bar with nprogress ### Installing nprogress in Next.js ```shell pnpm add nprogress pnpm add -D @types/nprogress ``` Modify `src/pages/_app.tsx`: ```tsx // ... // Import nprogress/nprogress.css import "nprogress/nprogress.css"; // ... // Import NProgress, useRouter, useEffect import { useRouter } from "next/router"; import NProgress from "nprogress"; import { useEffect } from "react"; // Call NProgress.configure to initialize NProgress.configure({ showSpinner: false }); function MyApp({ Component, pageProps }: AppProps) { // Add the following useEffect block to start NProgress during Next.js page transitions and stop it when the transition is complete const router = useRouter(); // Integrate nprogress useEffect(() => { router.events.on("routeChangeStart", () => NProgress.start()); router.events.on("routeChangeComplete", () => NProgress.done()); router.events.on("routeChangeError", () => NProgress.done()); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); // ... } ``` ### Adjusting the Progress Bar Color The progress bar is blue by default. To change it, you can write CSS to change the color. Add `src/styles/nprogress-custom.scss`: ```scss #nprogress { .bar { @apply h-1 bg-primary-500; } .peg { @apply shadow-[0_0_10px] shadow-primary-500; } } ``` Modify `src/pages/_app.tsx` to globally import the new scss file: ```tsx import "@/styles/globals.css"; import "@/styles/prism-dracula.css"; import "@/styles/prism-plus.css"; import "nprogress/nprogress.css"; // Add the following line to import nprogress-custom.scss import "@/styles/nprogress-custom.scss"; // ... ``` ## Results Done! Use `pnpm dev`, and after entering the website and clicking links to switch pages, you will see a progress bar at the top during page transitions! It makes visitors more willing to wait for the page to load. The final effect is as follows: ![progress bar animation](https://i.imgur.com/BpakgNT.gif) The code changes for this article are as follows: https://github.com/eason-dev/nextjs-tailwind-contentlayer-blog-starter/compare/day22-custom-image...day23-nprogress ## References - [rstacruz/nprogress: For slim progress bars like on YouTube, Medium, etc](https://github.com/rstacruz/nprogress) - [The comprehensive guide to use NProgress in Next.js [easy guide]](https://learnjsx.com/category/4/posts/nextjs-nprogress) ## Next Article Congratulations on successfully adding a page progress bar with nprogress! In the next article, we will continue to add more dazzling features. Let's add a "Table of Contents" to article pages, providing readers with a clear overview of the article structure. --- # 使用 nprogress 加入換頁進度條 - Modern Next.js Blog 系列 #23 - URL: https://easonchang.com/zh-TW/posts/nprogress - Date: 2022-10-08T00:00:00.000Z - Language: zh-TW - Description: 使用 nprogress 套件加入頁面切換進度條,改善讀者體感效能 > 本文同步發佈於 [it 邦幫忙 2022 iThome 鐵人賽](https://ithelp.ithome.com.tw/articles/10306167) 這是「Modern Blog 30 天」系列第 23 篇文章。 上一篇優化完了內文圖片效能,這篇我們繼續來改善讀者體感效能,使用 [nprogress](https://github.com/rstacruz/nprogress) 來加入換頁進度條! 最終效果如下: ![progress bar animation](https://i.imgur.com/BpakgNT.gif) 這篇修改的程式碼如下: https://github.com/eason-dev/nextjs-tailwind-contentlayer-blog-starter/compare/day22-custom-image...day23-nprogress --- ## 使用 nprogress 加入換頁進度條 ### 在 Next.js 安裝 nprogress ```shell pnpm add nprogress pnpm add -D @types/nprogress ``` 修改 `src/pages/_app.tsx`: ```tsx // ... // 引入 nprogress/nprogress.css import "nprogress/nprogress.css"; // ... // 引入 NProgress、useRouter、useEffect import { useRouter } from "next/router"; import NProgress from "nprogress"; import { useEffect } from "react"; // 呼叫 NProgress.configure 來初始化 NProgress.configure({ showSpinner: false }); function MyApp({ Component, pageProps }: AppProps) { // 新增下面這塊 useEffect,在 Next.js 換頁時開始 Nprogress 讀條,並在換頁完成時停止 const router = useRouter(); // Integrate nprogress useEffect(() => { router.events.on("routeChangeStart", () => NProgress.start()); router.events.on("routeChangeComplete", () => NProgress.done()); router.events.on("routeChangeError", () => NProgress.done()); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); // ... } ``` ### 調整進度條顏色 進度條預設是水藍色的,如果要修改的話能寫 css 改顏色。 新增 `src/styles/nprogress-custom.scss`: ```scss #nprogress { .bar { @apply h-1 bg-primary-500; } .peg { @apply shadow-[0_0_10px] shadow-primary-500; } } ``` 修改 `src/pages/_app.tsx`,在全站引入新的 scss 檔: ```tsx import "@/styles/globals.css"; import "@/styles/prism-dracula.css"; import "@/styles/prism-plus.css"; import "nprogress/nprogress.css"; // 新增下面這行引入 nprogress-custom.scss import "@/styles/nprogress-custom.scss"; // ... ``` ## 成果 完成了!使用 `pnpm dev`,進網站後點連結切換頁面,就會看到最上面多出一條換頁進度條了!讓更願意留在網站等待換頁。 最終效果如下: ![progress bar animation](https://i.imgur.com/BpakgNT.gif) 這篇修改的程式碼如下: https://github.com/eason-dev/nextjs-tailwind-contentlayer-blog-starter/compare/day22-custom-image...day23-nprogress ## References - [rstacruz/nprogress: For slim progress bars like on YouTube, Medium, etc](https://github.com/rstacruz/nprogress) - [The comprehensive guide to use NProgress in Next.js [easy guide]](https://learnjsx.com/category/4/posts/nextjs-nprogress) ## 下一篇 恭喜你成功使用 nprogress 加入了換頁進度條! 下一篇我們繼續加入更多炫砲功能,讓我們在文章內頁加入「目錄」吧,讓讀者一目瞭然文章結構。 --- # Optimizing Image Performance Using Next.js Image, plaiceholder, and Custom MDX Components - Modern Next.js Blog Series #22 - URL: https://easonchang.com/posts/post-custom-image - Date: 2022-10-07T00:00:00.000Z - Language: en - Description: Optimizing in-text image performance using a customized MDX component CustomImage, in conjunction with Next.js and the plaiceholder package > This article is also published at [it 邦幫忙 2022 iThome Ironman Contest](https://ithelp.ithome.com.tw/articles/10305758) Website performance bottlenecks are often due to slow image loading speeds. To enhance reader experience and SEO scores, this article will focus on optimizing in-text image performance! > The code changes for this article are as follows: > https://github.com/eason-dev/nextjs-tailwind-contentlayer-blog-starter/compare/day21-custom-link...day22-custom-image --- ## Adding a Customized `` to Optimize Image Loading Speed ### Installing Related Packages ```shell pnpm add image-size plaiceholder sharp unist-util-visit ``` ### Allowing Next.js Image to Use webp, avif Formats Modify `next.config.mjs` by adding an images section: ```js import { withContentlayer } from "next-contentlayer"; /** @type {import('next').NextConfig} */ const nextConfig = withContentlayer({ // ... // Add images section images: { // Enable modern image formats formats: ["image/avif", "image/webp"], }, }); export default nextConfig; ``` ### Using Custom imageMetadata rehype Plugin to Add Length and Width Attributes and LQIP to Images Add `src/plugins/imageMetadata.ts`: ```ts // Custom rehype plugin to add width and height to local images // To make Next.js work // Ref: https://kylepfromer.com/blog/nextjs-image-component-blog // Similar structure to: // https://github.com/JS-DevTools/rehype-inline-svg/blob/master/src/inline-svg.ts import imageSize from "image-size"; import path from "path"; import { getPlaiceholder } from "plaiceholder"; import { Node, visit } from "unist-util-visit"; import { promisify } from "util"; const sizeOf = promisify(imageSize); /** * An `` HAST node */ interface ImageNode extends Node { type: "element"; tagName: "img"; properties: { src: string; height?: number; width?: number; base64?: string; }; } /** * Determines whether the given HAST node is an `` element. */ function isImageNode(node: Node): node is ImageNode { const img = node as ImageNode; return ( img.type === "element" && img.tagName === "img" && img.properties && typeof img.properties.src === "string" ); } /** * Filters out non absolute paths from the public folder. */ function filterImageNode(node: ImageNode): boolean { return node.properties.src.startsWith("/"); } /** * Adds the image's `height` and `width` to its properties. */ async function addMetadata(node: ImageNode): Promise { const res = await sizeOf( path.join(process.cwd(), "public", node.properties.src) ); if (!res) throw Error(`Invalid image with src "${node.properties.src}"`); const { base64 } = await getPlaiceholder(node.properties.src, { size: 10 }); // 10 is to increase detail (default is 4) node.properties.width = res.width; node.properties.height = res.height; node.properties.base64 = base64; } /** * This is a Rehype plugin that finds image `` elements and adds the height and width to the properties. * Read more about Next.js image: https://nextjs.org/docs/api-reference/next/image#layout */ export default function imageMetadata() { return async function transformer(tree: Node): Promise { const imgNodes: ImageNode[] = []; visit(tree, "element", (node) => { if (isImageNode(node) && filterImageNode(node)) { imgNodes.push(node); } }); for (const node of imgNodes) { await addMetadata(node); } return tree; }; } ``` Modify `contentlayer.config.ts` to apply the above-written imageMetadata rehype plugin: ```ts import imageMetadata from "./src/plugins/imageMetadata"; // ... export default makeSource({ // ... mdx: { rehypePlugins: [ // ... imageMetadata, // For adding image metadata (width, height) ], }, }); ``` Add `src/components/CustomImage.tsx`: ```tsx import Image, { ImageProps } from "next/image"; type Props = ImageProps & { base64?: string }; export default function CustomImage({ src, height, width, base64, alt, ...otherProps }: Props) { if (!src) return null; if (typeof src === "string" && (!height || !width)) { return ( // eslint-disable-next-line @next/next/no-img-element {alt} ); } return ( {alt} ); } ``` Modify `src/lib/mdxComponents.ts` so that all imgs in MDX use CustomImage for rendering: ```ts import CustomImage from "@/components/CustomImage"; // ... // Custom components/renderers to pass to MDX. const mdxComponents = { // ... img: CustomImage, }; export default mdxComponents; ``` ## Results Done! Use `pnpm dev`, and enter any article with images to see the loading speed has improved! A blurred version of the image will also be displayed while loading, informing readers that an image will appear there, also preventing layout shifts. ## References - [next/image | Next.js](https://nextjs.org/docs/api-reference/next/image#acceptable-formats) - [Using the Next.js Image Component with MDX](https://kylepfromer.com/blog/nextjs-image-component-blog/) ## Next Article Congratulations on customizing in-text images and speeding up loading! > The code changes for this article are as follows: > https://github.com/eason-dev/nextjs-tailwind-contentlayer-blog-starter/compare/day21-custom-link...day22-custom-image In the next article, we will use another method to optimize the perceived page transition speed for readers, adding a page transition progress bar with nprogress! --- # 圖片效能最佳化,使用 Next.js Image、plaiceholder、客製 MDX 元件 - Modern Next.js Blog 系列 #22 - URL: https://easonchang.com/zh-TW/posts/post-custom-image - Date: 2022-10-07T00:00:00.000Z - Language: zh-TW - Description: 使用客製化 MDX 元件 CustomImage,搭配 Next.js 及 plaiceholder 套件,最佳化內文圖片效能 > 本文同步發佈於 [it 邦幫忙 2022 iThome 鐵人賽](https://ithelp.ithome.com.tw/articles/10305758) 網站效能瓶頸通常是圖片讀取速度太慢。為了提升讀者體驗、和 SEO 分數,這一篇我們來最佳化內文圖片效能! > 這篇修改的程式碼如下: > https://github.com/eason-dev/nextjs-tailwind-contentlayer-blog-starter/compare/day21-custom-link...day22-custom-image --- ## 加入客製化 ``,最佳化圖片讀取速度 ### 安裝相關套件 ```shell pnpm add image-size plaiceholder sharp unist-util-visit ``` ### 允許 Next.js Image 使用 webp, avif 格式 修改 `next.config.mjs`,加入 images 區塊: ```js import { withContentlayer } from "next-contentlayer"; /** @type {import('next').NextConfig} */ const nextConfig = withContentlayer({ // ... // 加入 images 區塊 images: { // Enable modern image formats formats: ["image/avif", "image/webp"], }, }); export default nextConfig; ``` ### 使用客製 imageMetadata rehype plugin,幫圖片加入長寬屬性和 LQIP 新增 `src/plugins/imageMetadata.ts`: ```ts // Custom rehype plugin to add width and height to local images // To make Next.js works // Ref: https://kylepfromer.com/blog/nextjs-image-component-blog // Similiar structure to: // https://github.com/JS-DevTools/rehype-inline-svg/blob/master/src/inline-svg.ts import imageSize from "image-size"; import path from "path"; import { getPlaiceholder } from "plaiceholder"; import { Node, visit } from "unist-util-visit"; import { promisify } from "util"; const sizeOf = promisify(imageSize); /** * An `` HAST node */ interface ImageNode extends Node { type: "element"; tagName: "img"; properties: { src: string; height?: number; width?: number; base64?: string; }; } /** * Determines whether the given HAST node is an `` element. */ function isImageNode(node: Node): node is ImageNode { const img = node as ImageNode; return ( img.type === "element" && img.tagName === "img" && img.properties && typeof img.properties.src === "string" ); } /** * Filters out non absolute paths from the public folder. */ function filterImageNode(node: ImageNode): boolean { return node.properties.src.startsWith("/"); } /** * Adds the image's `height` and `width` to it's properties. */ async function addMetadata(node: ImageNode): Promise { const res = await sizeOf( path.join(process.cwd(), "public", node.properties.src) ); if (!res) throw Error(`Invalid image with src "${node.properties.src}"`); const { base64 } = await getPlaiceholder(node.properties.src, { size: 10 }); // 10 is to increase detail (default is 4) node.properties.width = res.width; node.properties.height = res.height; node.properties.base64 = base64; } /** * This is a Rehype plugin that finds image `` elements and adds the height and width to the properties. * Read more about Next.js image: https://nextjs.org/docs/api-reference/next/image#layout */ export default function imageMetadata() { return async function transformer(tree: Node): Promise { const imgNodes: ImageNode[] = []; visit(tree, "element", (node) => { if (isImageNode(node) && filterImageNode(node)) { imgNodes.push(node); } }); for (const node of imgNodes) { await addMetadata(node); } return tree; }; } ``` 修改 `contentlayer.config.ts`,套用上面寫的 imageMetadata rehype plugin: ```ts import imageMetadata from "./src/plugins/imageMetadata"; // ... export default makeSource({ // ... mdx: { rehypePlugins: [ // ... imageMetadata, // For adding image metadata (width, height) ], }, }); ``` 新增 `src/components/CustomImage.tsx`: ```tsx import Image, { ImageProps } from "next/image"; type Props = ImageProps & { base64?: string }; export default function CustomImage({ src, height, width, base64, alt, ...otherProps }: Props) { if (!src) return null; if (typeof src === "string" && (!height || !width)) { return ( // eslint-disable-next-line @next/next/no-img-element {alt} ); } return ( {alt} ); } ``` 修改 `src/lib/mdxComponents.ts`,讓 MDX 裡面的 img 都使用 CustomImage 來渲染: ```ts import CustomImage from "@/components/CustomImage"; // ... // Custom components/renderers to pass to MDX. const mdxComponents = { // ... img: CustomImage, }; export default mdxComponents; ``` ## 成果 完成了!使用 `pnpm dev`,進到任何一篇有圖片的文章,就會看到讀取速度變快了! 在讀取時也會先顯示模糊版本的圖片,讓讀者知道那邊將有圖片,也避免版面位移。 ## References - [next/image | Next.js](https://nextjs.org/docs/api-reference/next/image#acceptable-formats) - [Using the Next.js Image Component with MDX](https://kylepfromer.com/blog/nextjs-image-component-blog/) ## 下一篇 恭喜你成功客製化了內文圖片,加快了讀取速度! > 這篇修改的程式碼如下: > https://github.com/eason-dev/nextjs-tailwind-contentlayer-blog-starter/compare/day21-custom-link...day22-custom-image 下一篇我們會來用另一個手段,最佳化讀者的體感換頁速度,使用 nprogress 加入換頁進度條! --- # Enhancing In-text Link Page Speed and Adding External Link Icons - Modern Next.js Blog Series #21 - URL: https://easonchang.com/posts/post-custom-link - Date: 2022-10-06T00:00:00.000Z - Language: en - Description: Modifying the in-text CustomLink, using Next.js's for internal links to accelerate page transitions; adding an external link icon for external links > This article is also published at [it 邦幫忙 2022 iThome Ironman Contest](https://ithelp.ithome.com.tw/articles/10305241) ## TL;DR This is the 21st article in the "Modern Blog 30 Days" series. In the previous article, we added anchor points to subheadings in the text. In this article, we will modify the in-text CustomLink. For internal links, we use Next.js's `` to speed up page transitions; for external links, we add an icon to alert users that it is an external link! Screenshot of the results: ![post links](https://i.imgur.com/BILvjrj.jpg) The code changes for this article are as follows: https://github.com/eason-dev/nextjs-tailwind-contentlayer-blog-starter/compare/day20-custom-heading-anchor...day21-custom-link --- ## Adding a Customized `` to Enhance In-text Link Page Speed and Add External Link Icons We are going to extend the `src/components/CustomLink.tsx` that we previously added during [Day 11 Site-wide Styling](/posts/blog-site-style). Move `src/components/CustomLink.tsx` to `src/components/CustomLink/CustomLink.tsx` and modify its content as follows: ```tsx import Link from "next/link"; import ExternalLinkIcon from "./external-link.svg"; type Props = React.ComponentPropsWithoutRef<"a">; const CustomLink = ({ href, children, ...rest }: Props) => { const isInternalLink = href && href.startsWith("/"); const isAnchorLink = href && href.startsWith("#"); if (isInternalLink) { return ( {children} ); } if (isAnchorLink) { return ( {children} ); } return ( {children} {typeof children === "string" && ( )} ); }; export default CustomLink; ``` The above `` will determine whether a link is an internal link to another page of the blog, an anchor link, or an external link based on the `href`. For internal links, it renders as a [Next.js Link](https://nextjs.org/docs/routing/introduction#linking-between-pages) for faster client-side navigation. For anchor links, it renders a regular a link without any changes. For external links, it adds `target="_blank"` to open the link in a new tab and adds an icon to the right of the link to let readers know it's an external link. Next, add `src/components/CustomLink/external-link.svg`, which is the icon for indicating external links: ```svg ``` Add `src/components/CustomLink/index.ts` to avoid changing the import path in places where CustomLink was previously used: ```ts import CustomLink from "./CustomLink"; export default CustomLink; ``` Modify `src/lib/mdxComponents.ts` to include a custom component for links: ```ts // ... import CustomLink from "@/components/CustomLink"; // Custom components/renderers to pass to MDX. const mdxComponents = { // ... a: CustomLink, }; export default mdxComponents; ``` This completes the setup! ## Results Done! Use `pnpm dev`, and go into any article with links in the text to see that internal link page transitions have become smoother, and external links now have an icon indicating they will open in a new tab. Screenshot of the results: ![post links](https://i.imgur.com/BILvjrj.jpg) The code changes for this article are as follows: https://github.com/eason-dev/nextjs-tailwind-contentlayer-blog-starter/compare/day20-custom-heading-anchor...day21-custom-link ## Next Article Congratulations on successfully customizing in-text links, making them more user-friendly and functional. The next article is the last of the three on customizing in-text components. We will customize "Images" in the text using Next.js's Image component to speed up image loading! --- # 強化內文連結換頁速度、加入外部連結 icon - Modern Next.js Blog 系列 #21 - URL: https://easonchang.com/zh-TW/posts/post-custom-link - Date: 2022-10-06T00:00:00.000Z - Language: zh-TW - Description: 修改內文 CustomLink,針對內部連結使用 Next.js 提供的 加速頁面切換;針對外部連結則加入外聯 icon 提示 > 本文同步發佈於 [it 邦幫忙 2022 iThome 鐵人賽](https://ithelp.ithome.com.tw/articles/10305241) ## TL;DR 這是「Modern Blog 30 天」系列第 21 篇文章,上一篇我們在內文小標題加入 anchor 錨點了,這篇我們會來修改內文 CustomLink。針對內部連結使用 Next.js 提供的 `` 加速頁面切換;針對外部連結則加入 icon 提示使用者這是外部連結! 結果截圖如下: ![post links](https://i.imgur.com/BILvjrj.jpg) 這篇修改的程式碼如下: https://github.com/eason-dev/nextjs-tailwind-contentlayer-blog-starter/compare/day20-custom-heading-anchor...day21-custom-link --- ## 加入客製化 ``,強化內文連結換頁速度、加入外部連結 icon 我們來擴充之前 [Day 11 全站樣式切版](/posts/blog-site-style) 時就已經新增的 `src/components/CustomLink.tsx`。 將 `src/components/CustomLink.tsx` 移動到 `src/components/CustomLink/CustomLink.tsx`,並修改內容如下: ```tsx import Link from "next/link"; import ExternalLinkIcon from "./external-link.svg"; type Props = React.ComponentPropsWithoutRef<"a">; const CustomLink = ({ href, children, ...rest }: Props) => { const isInternalLink = href && href.startsWith("/"); const isAnchorLink = href && href.startsWith("#"); if (isInternalLink) { return ( {children} ); } if (isAnchorLink) { return ( {children} ); } return ( {children} {typeof children === "string" && ( )} ); }; export default CustomLink; ``` 上面的 `` 會判斷 href 來知道連結是連往部落格其他頁面的內部連結、錨點連結、或是外部連結。 針對內部連結會渲染成 [Next.js 的 Link](https://nextjs.org/docs/routing/introduction#linking-between-pages),做 client-side 換頁會快速很多。 針對錨點連結不做任何處理,渲染常規 a link。 針對外部連結,則會加入 `target="_blank"` 以開新分頁方式打開連結,並在連結右邊多加 icon,讓讀者知道這連結會連到外部。 再來加入 `src/components/CustomLink/external-link.svg`,這個是標注外部連結的 icon: ```svg ``` 加入 `src/components/CustomLink/index.ts`,讓之前就有用到 CustomLink 的地方不用改 import 路徑: ```ts import CustomLink from "./CustomLink"; export default CustomLink; ``` 修改 `src/lib/mdxComponents.ts`,加入 a 的客製化元件: ```ts // ... import CustomLink from "@/components/CustomLink"; // Custom components/renderers to pass to MDX. const mdxComponents = { // ... a: CustomLink, }; export default mdxComponents; ``` 這樣就完成了! ## 成果 完成了!使用 `pnpm dev`,進到任何一篇內文有連結的文章內,就會看到內部連結換頁變順暢了,而外部連結多了 icon 提示,點下去時也會用開新分頁的方式開連結。 結果截圖如下: ![post links](https://i.imgur.com/BILvjrj.jpg) 這篇修改的程式碼如下: https://github.com/eason-dev/nextjs-tailwind-contentlayer-blog-starter/compare/day20-custom-heading-anchor...day21-custom-link ## 下一篇 恭喜你成功客製化了內文連結,讓連結更友善、更好用。 下一篇是三篇內文客製化元件的最後一篇,我們來客製化內文「圖片」,使用 Next.js 的 Image 元件,加快圖片讀取速度! --- # Adding Anchor Links to In-text Subheadings - Modern Next.js Blog Series #20 - URL: https://easonchang.com/posts/post-heading-anchor - Date: 2022-10-05T00:00:00.000Z - Language: en - Description: Using the rehype-slug package to add id attributes to in-text subheadings, and adding anchor links with the custom MDX component CustomHeading - Translation: AI-assisted > This article is also published at [it 邦幫忙 2022 iThome Ironman Contest](https://ithelp.ithome.com.tw/articles/10304682) ## TL;DR This is the 20th article in the "Modern Blog 30 Days" series. In the previous article, we finished RSS feed generation, completing all the blog features. From here on, we will keep adding more dazzling details. In this article, we start by adding anchor links to in-text headings! Screenshot of the results: ![title anchor](https://i.imgur.com/paQPqU5.jpg) The code changes for this article are as follows: https://github.com/eason-dev/nextjs-tailwind-contentlayer-blog-starter/compare/day19-rss-feed...day20-custom-heading-anchor --- ## Adding Anchor Links to In-text Subheadings ### Using rehype-slug to Add id Attributes to In-text Subheadings First, we need the final rendered HTML of every in-text subheading to have an `id` attribute. We use [rehype-slug](https://github.com/rehypejs/rehype-slug) to do this for us. It adds `id` attributes to subheadings while Contentlayer converts Markdown content into HTML. For example, it can transform the following HTML: ```html

Lorem ipsum

Dolor sit amet ?

consectetur & adipisicing

elit

elit
``` Into this, with `id` attributes added: ```html

Lorem ipsum

Dolor sit amet ?

consectetur & adipisicing

elit

elit
``` (Example taken from the [rehype-slug README.md](https://github.com/rehypejs/rehype-slug#use)) ### Installing rehype-slug ```shell pnpm add rehype-slug ``` Modify `contentlayer.config.ts` to add rehype-slug to the rehypePlugins list: ```ts // ... import rehypeSlug from "rehype-slug"; // ... export default makeSource({ // ... mdx: { rehypePlugins: [ // 加入 rehypeSlug rehypeSlug, // For generating slugs for headings rehypeCodeTitles, // For adding titles to code blocks [rehypePrism, { ignoreMissing: true }], // For code syntax highlighting ], }, }); ``` ### Displaying an Anchor Button with the CustomHeading Component Next, let's make each subheading show an anchor button on hover. Here we use the same technique as in Day 16 [Adding 'Copy Button' to Code Blocks Using Custom MDX Components - Modern Next.js Blog Series #16](/posts/code-copy-button), implementing it with a custom MDX component. When we implemented the copy button, we created ``. This time, we will implement ``. Add `src/components/CustomHeading.tsx`: ```tsx type CustomHeadingProps = React.ComponentPropsWithRef< "h1" | "h2" | "h3" | "h4" | "h5" | "h6" > & { Component: "h1" | "h2" | "h3" | "h4" | "h5" | "h6" }; function CustomHeading({ Component, id, children, ...otherProps }: CustomHeadingProps) { return ( {children} # ); } export const CustomH1 = (props: React.ComponentPropsWithRef<"h1">) => ( ); export const CustomH2 = (props: React.ComponentPropsWithRef<"h2">) => ( ); export const CustomH3 = (props: React.ComponentPropsWithRef<"h3">) => ( ); export const CustomH4 = (props: React.ComponentPropsWithRef<"h4">) => ( ); export const CustomH5 = (props: React.ComponentPropsWithRef<"h5">) => ( ); export const CustomH6 = (props: React.ComponentPropsWithRef<"h6">) => ( ); ``` Modify `src/lib/mdxComponents.ts` to add the custom components for H1 through H6: ```ts import { CustomH1, CustomH2, CustomH3, CustomH4, CustomH5, CustomH6, } from "@/components/CustomHeading"; import CustomPre from "@/components/CustomPre"; // Custom components/renderers to pass to MDX. const mdxComponents = { h1: CustomH1, h2: CustomH2, h3: CustomH3, h4: CustomH4, h5: CustomH5, h6: CustomH6, pre: CustomPre, }; export default mdxComponents; ``` This completes the setup! ## Results Done! Use `pnpm dev`, go into any article that has subheadings, and hover over a subheading. You will see a hash anchor button appear next to it. Clicking it scrolls the page to that subheading, and the URL in the address bar changes accordingly. Share that URL with someone, and when they open it, they will land directly on the section you want them to see. Screenshot of the results: ![title anchor](https://i.imgur.com/paQPqU5.jpg) The code changes for this article are as follows: https://github.com/eason-dev/nextjs-tailwind-contentlayer-blog-starter/compare/day19-rss-feed...day20-custom-heading-anchor ## References - [rehypejs/rehype-slug: plugin to add \`id\` attributes to headings](https://github.com/rehypejs/rehype-slug) ## Next Article Congratulations on adding anchor links to in-text subheadings, making your articles more useful! In the next article, we will modify the in-text CustomLink. For internal links, we use Next.js's `` to speed up page transitions; for external links, we add an icon to alert users that it is an external link! --- # 為內文小標題加入 anchor 錨點連結 - Modern Next.js Blog 系列 #20 - URL: https://easonchang.com/zh-TW/posts/post-heading-anchor - Date: 2022-10-05T00:00:00.000Z - Language: zh-TW - Description: 使用 rehype-slug 套件在內文小標題補上 id 屬性,並使用客製 MDX 元件 CustomHeading 加入 anchor 連結 > 本文同步發佈於 [it 邦幫忙 2022 iThome 鐵人賽](https://ithelp.ithome.com.tw/articles/10304682) ## TL;DR 這是「Modern Blog 30 天」系列第 20 篇文章,上一篇我們做完 RSS Feed 生成,完成所有部落格功能了。後續讓我們繼續加入更多炫砲細節。這篇先來為內文 heading 加入 anchor 連結! 結果截圖如下: ![title anchor](https://i.imgur.com/paQPqU5.jpg) 這篇修改的程式碼如下: https://github.com/eason-dev/nextjs-tailwind-contentlayer-blog-starter/compare/day19-rss-feed...day20-custom-heading-anchor --- ## 為內文小標題加入 anchor 錨點連結 ### 使用 rehype-slug 套件在內文小標題補上 id 屬性 首先要讓每個內文小標題最後顯示的 HTML 都有 `id` 屬性,我們使用 [rehype-slug](https://github.com/rehypejs/rehype-slug) 幫我們完成這件事。 它能在 Contentlayer 將 Markdown 內容轉成 HTML 時,將小標題補上 `id`。 像是能把下列 HTML: ```html

Lorem ipsum

Dolor sit amet ?

consectetur & adipisicing

elit

elit
``` 補上 id 轉換成這樣: ```html

Lorem ipsum

Dolor sit amet ?

consectetur & adipisicing

elit

elit
``` (範例取自 [rehype-slug README.md](https://github.com/rehypejs/rehype-slug#use)) ### 安裝 rehype-slug ```shell pnpm add rehype-slug ``` 修改 `contentlayer.config.ts`,將 rehype-slug 加入 rehypePlugins 列表: ```ts // ... import rehypeSlug from "rehype-slug"; // ... export default makeSource({ // ... mdx: { rehypePlugins: [ // 加入 rehypeSlug rehypeSlug, // For generating slugs for headings rehypeCodeTitles, // For adding titles to code blocks [rehypePrism, { ignoreMissing: true }], // For code syntax highlighting ], }, }); ``` ### 使用 CustomHeading 元件顯示 anchor 按鈕 接著讓每個小標題在 hover 時,顯示 anchor 按鈕。 這裡使用跟 Day 16 [程式碼區塊樣式優化:複製按鈕 - Modern Blog 30 系列 #16](/posts/code-copy-button) 一樣的手法,用客製化 MDX 元件的方法實作。 當初實作複製按鈕時,我們實作了 ``,這次我們來實作 ``。 新增 `src/components/CustomHeading.tsx`: ```tsx type CustomHeadingProps = React.ComponentPropsWithRef< "h1" | "h2" | "h3" | "h4" | "h5" | "h6" > & { Component: "h1" | "h2" | "h3" | "h4" | "h5" | "h6" }; function CustomHeading({ Component, id, children, ...otherProps }: CustomHeadingProps) { return ( {children} # ); } export const CustomH1 = (props: React.ComponentPropsWithRef<"h1">) => ( ); export const CustomH2 = (props: React.ComponentPropsWithRef<"h2">) => ( ); export const CustomH3 = (props: React.ComponentPropsWithRef<"h3">) => ( ); export const CustomH4 = (props: React.ComponentPropsWithRef<"h4">) => ( ); export const CustomH5 = (props: React.ComponentPropsWithRef<"h5">) => ( ); export const CustomH6 = (props: React.ComponentPropsWithRef<"h6">) => ( ); ``` 修改 `src/lib/mdxComponents.ts`,加入 H1 到 H6 的客製化元件: ```ts import { CustomH1, CustomH2, CustomH3, CustomH4, CustomH5, CustomH6, } from "@/components/CustomHeading"; import CustomPre from "@/components/CustomPre"; // Custom components/renderers to pass to MDX. const mdxComponents = { h1: CustomH1, h2: CustomH2, h3: CustomH3, h4: CustomH4, h5: CustomH5, h6: CustomH6, pre: CustomPre, }; export default mdxComponents; ``` 這樣就完成了! ## 成果 完成了!使用 `pnpm dev`,進到任何一篇內文有小標題的文章內,hover 小標題,就會看到旁邊多出井字號 anchor 按鈕,按下去畫面就會滾動到指定小標題,網址列也會隨之改變。 把這個網址貼給別人,別人進來看時,就會直接看到你希望他看的段落了。 結果截圖如下: ![title anchor](https://i.imgur.com/paQPqU5.jpg) 這篇修改的程式碼如下: https://github.com/eason-dev/nextjs-tailwind-contentlayer-blog-starter/compare/day19-rss-feed...day20-custom-heading-anchor ## References - [rehypejs/rehype-slug: plugin to add \`id\` attributes to headings](https://github.com/rehypejs/rehype-slug) ## 下一篇 恭喜你在內文小標題加入 anchor 錨點,讓內文更實用了! 下一篇我們接著來修改內文 CustomLink,針對內部連結使用 Next.js 提供的 `` 加速頁面切換,針對外部連結則加入 icon 提示使用者這是外部連結! --- # Generating RSS Feed with feed - Modern Next.js Blog Series #19 - URL: https://easonchang.com/posts/rss-feed - Date: 2022-10-04T00:00:00.000Z - Language: en - Description: Add RSS Feed functionality to the blog, allowing readers to subscribe to new articles via RSS > This article is also published at [it 邦幫忙 2022 iThome Ironman Contest](https://ithelp.ithome.com.tw/articles/10304162) ## TL;DR This is the 19th article in the "Modern Blog 30 Days" series. In the previous article, we completed generating a Sitemap. Continuing with a very similar task, this article focuses on generating an RSS Feed, allowing the blog to be subscribed to via RSS. The code changes for this article are as follows: https://github.com/eason-dev/nextjs-tailwind-contentlayer-blog-starter/compare/day18-sitemap...day19-rss-feed --- ## Adding an RSS Feed Install the [feed](https://github.com/jpmonette/feed) package: ```shell pnpm add feed ``` Modify `src/configs/siteConfigs.ts` by adding credit and email: ```ts // ... export const siteConfigs = { // ... credit: "Stark Industries", email: "stark@example.com", }; ``` Add `src/lib/generateRSS.ts`: ```ts import { Feed } from "feed"; import { writeFileSync } from "fs"; import { siteConfigs } from "@/configs/siteConfigs"; import { allPostsNewToOld } from "@/lib/contentLayerAdapter"; import { getPostOGImage } from "@/lib/getPostOGImage"; export default function generateRSS() { const author = { name: siteConfigs.author, email: siteConfigs.email, link: siteConfigs.fqdn, }; const feed = new Feed({ title: siteConfigs.title, description: siteConfigs.description, id: siteConfigs.fqdn, link: siteConfigs.fqdn, image: siteConfigs.logoUrl, favicon: siteConfigs.logoUrl, copyright: `Copyright © 2015 - ${new Date().getFullYear()} ${ siteConfigs.credit }`, feedLinks: { rss2: `${siteConfigs.fqdn}/feed.xml`, json: `${siteConfigs.fqdn}/feed.json`, atom: `${siteConfigs.fqdn}/atom.xml`, }, author: author, }); allPostsNewToOld.forEach((post) => { feed.addItem({ id: siteConfigs.fqdn + post.path, title: post.title, link: siteConfigs.fqdn + post.path, description: post.description, image: getPostOGImage(post.socialImage), author: [author], contributor: [author], date: new Date(post.date), // content: post.body.html, }); }); writeFileSync("./public/feed.xml", feed.rss2()); writeFileSync("./public/atom.xml", feed.atom1()); writeFileSync("./public/feed.json", feed.json1()); } ``` Modify `src/pages/index.tsx` by adding the above `generateRSS()` function into `getStaticProps` so it executes during `pnpm build` to generate RSS files: ```tsx // ... // Add this line to import import generateRSS from "@/lib/generateRSS"; // ... export const getStaticProps: GetStaticProps = () => { // ... // Add the line below generateRSS(); return { props: { posts } }; }; // ... ``` Modify `src/pages/_app.tsx` to add meta data indicating the path of the RSS Feed throughout the site: ```tsx // ... function MyApp({ Component, pageProps }: AppProps) { return ( ); } export default MyApp; ``` Modify `.gitignore`, `.eslintignore`, `.prettierignore` to ignore the generated RSS Feed files: ``` # ... # Add the following 3 rules # RSS related files (generated by generateRSS.js) /public/atom.xml /public /feed.xml /public/feed.json ``` ## Results Done! After running `pnpm build`, you'll see `atom.xml`, `feed.json`, and `feed.xml` added to the /public path, providing three different RSS formats. The generated content includes: `public/atom.xml`: (Example XML content for atom.xml) `public/feed.json`: (Example JSON content for feed.json) `public/feed.xml`: (Example XML content for feed.xml) The code changes for this article are as follows: https://github.com/eason-dev/nextjs-tailwind-contentlayer-blog-starter/compare/day18-sitemap...day19-rss-feed ## References - [jpmonette/feed: A RSS, Atom, and JSON Feed generator for Node.js, making content syndication simple and intuitive! ?](https://github.com/jpmonette/feed) - [Generate RSS feeds for your static Next.js blog | Phiilu | Florian Kapfenberger](https://phiilu.com/generate-rss-feeds-for-your-static-next-js-blog) - [Generate RSS feed for Next.js static site - kpman | code](https://code.kpman.cc/2021/05/25/next-js-rss/) ## Next Article Congratulations on completing the RSS Feed generation! By now, the blog has all the necessary features to go live. However, with 11 days still to go, we're not stopping here. In the next 11 days, we'll continue adding more dazzling features, progressing towards the Modern Blog. In the next article, we'll modify the in-text heading styles and add anchor links to each heading! --- # 使用 feed 生成 RSS Feed - Modern Next.js Blog 系列 #19 - URL: https://easonchang.com/zh-TW/posts/rss-feed - Date: 2022-10-04T00:00:00.000Z - Language: zh-TW - Description: 在部落格加入 RSS Feed 功能,讓讀者能以 RSS 形式訂閱最新文章 > 本文同步發佈於 [it 邦幫忙 2022 iThome 鐵人賽](https://ithelp.ithome.com.tw/articles/10304162) ## TL;DR 這是「Modern Blog 30 天」系列第 19 篇文章,上一篇我們做完 Sitemap 生成了,這篇接著來做非常相似的 RSS Feed 生成,讓部落格能被 RSS 訂閱。 這篇修改的程式碼如下: https://github.com/eason-dev/nextjs-tailwind-contentlayer-blog-starter/compare/day18-sitemap...day19-rss-feed --- ## 加入 RSS Feed 安裝 [feed](https://github.com/jpmonette/feed) 套件 ```shell pnpm add feed ``` 修改 `src/configs/siteConfigs.ts`,新增 credit 和 email: ```ts // ... export const siteConfigs = { // ... credit: "Stark Industries", email: "stark@example.com", }; ``` 新增 `src/lib/generateRSS.ts`: ```ts import { Feed } from "feed"; import { writeFileSync } from "fs"; import { siteConfigs } from "@/configs/siteConfigs"; import { allPostsNewToOld } from "@/lib/contentLayerAdapter"; import { getPostOGImage } from "@/lib/getPostOGImage"; export default function generateRSS() { const author = { name: siteConfigs.author, email: siteConfigs.email, link: siteConfigs.fqdn, }; const feed = new Feed({ title: siteConfigs.title, description: siteConfigs.description, id: siteConfigs.fqdn, link: siteConfigs.fqdn, image: siteConfigs.logoUrl, favicon: siteConfigs.logoUrl, copyright: `Copyright © 2015 - ${new Date().getFullYear()} ${ siteConfigs.credit }`, feedLinks: { rss2: `${siteConfigs.fqdn}/feed.xml`, json: `${siteConfigs.fqdn}/feed.json`, atom: `${siteConfigs.fqdn}/atom.xml`, }, author: author, }); allPostsNewToOld.forEach((post) => { feed.addItem({ id: siteConfigs.fqdn + post.path, title: post.title, link: siteConfigs.fqdn + post.path, description: post.description, image: getPostOGImage(post.socialImage), author: [author], contributor: [author], date: new Date(post.date), // content: post.body.html, }); }); writeFileSync("./public/feed.xml", feed.rss2()); writeFileSync("./public/atom.xml", feed.atom1()); writeFileSync("./public/feed.json", feed.json1()); } ``` 修改 `src/pages/index.tsx`,將上面的 `generateRSS()` function 放進 `getStaticProps` 裡,在 pnpm build 時就會執行它來生成 RSS 檔案: ```tsx // ... // 新增這行 Import import generateRSS from "@/lib/generateRSS"; // ... export const getStaticProps: GetStaticProps = () => { // ... // 新增下面這行 generateRSS(); return { props: { posts } }; }; // ... ``` 修改 `src/pages/_app.tsx`,在全站加入 meta data 標註 RSS Feed 的路徑: ```tsx // ... function MyApp({ Component, pageProps }: AppProps) { return ( ); } export default MyApp; ``` 修改 `.gitignore`、`.eslintignore`、`.prettierignore`,忽略生成的 RSS Feed: ``` # ... # 加入下面這 3 條規則 # RSS related files (generated by generateRSS.js) /public/atom.xml /public/feed.xml /public/feed.json ``` ## 成果 完成了!使用 `pnpm build`,執行完就會看到 /public 路徑裡多了 `atom.xml`、`feed.json` 和 `feed.xml` 了,三種不同的 RSS 格式。 生成的內容如下: `public/atom.xml`: ```xml https://nextjs-tailwind-contentlayer-blog-starter.vercel.app Next.js Tailwind Contentlayer Blog Starter 2022-10-04T15:38:07.971Z https://github.com/jpmonette/feed Tony Stark stark@example.com https://nextjs-tailwind-contentlayer-blog-starter.vercel.app Blog starter template with modern frontend technologies like Next.js, Tailwind CSS, Contentlayer, i18Next https://nextjs-tailwind-contentlayer-blog-starter.vercel.app/logo.png https://nextjs-tailwind-contentlayer-blog-starter.vercel.app/logo.png Copyright © 2015 - 2022 Stark Industries <![CDATA[Post with images]]> https://nextjs-tailwind-contentlayer-blog-starter.vercel.app/posts/post-with-images 2022-09-02T00:00:00.000Z Tony Stark stark@example.com https://nextjs-tailwind-contentlayer-blog-starter.vercel.app Tony Stark stark@example.com https://nextjs-tailwind-contentlayer-blog-starter.vercel.app <![CDATA[Post with code]]> https://nextjs-tailwind-contentlayer-blog-starter.vercel.app/posts/post-with-code 2022-09-01T00:00:00.000Z Tony Stark stark@example.com https://nextjs-tailwind-contentlayer-blog-starter.vercel.app Tony Stark stark@example.com https://nextjs-tailwind-contentlayer-blog-starter.vercel.app <![CDATA[Markdown demo]]> https://nextjs-tailwind-contentlayer-blog-starter.vercel.app/posts/markdown-demo 2022-08-31T00:00:00.000Z Tony Stark stark@example.com https://nextjs-tailwind-contentlayer-blog-starter.vercel.app Tony Stark stark@example.com https://nextjs-tailwind-contentlayer-blog-starter.vercel.app <![CDATA[Sample post]]> https://nextjs-tailwind-contentlayer-blog-starter.vercel.app/posts/sample-post 2022-08-30T00:00:00.000Z Tony Stark stark@example.com https://nextjs-tailwind-contentlayer-blog-starter.vercel.app Tony Stark stark@example.com https://nextjs-tailwind-contentlayer-blog-starter.vercel.app ``` `public/feed.json`: ```json { "version": "https://jsonfeed.org/version/1", "title": "Next.js Tailwind Contentlayer Blog Starter", "home_page_url": "https://nextjs-tailwind-contentlayer-blog-starter.vercel.app", "feed_url": "https://nextjs-tailwind-contentlayer-blog-starter.vercel.app/feed.json", "description": "Blog starter template with modern frontend technologies like Next.js, Tailwind CSS, Contentlayer, i18Next", "icon": "https://nextjs-tailwind-contentlayer-blog-starter.vercel.app/logo.png", "author": { "name": "Tony Stark", "url": "https://nextjs-tailwind-contentlayer-blog-starter.vercel.app" }, "items": [ { "id": "https://nextjs-tailwind-contentlayer-blog-starter.vercel.app/posts/post-with-images", "url": "https://nextjs-tailwind-contentlayer-blog-starter.vercel.app/posts/post-with-images", "title": "Post with images", "summary": "My post with images", "image": "https://nextjs-tailwind-contentlayer-blog-starter.vercel.app/images/anita-chong-unsplash.jpg", "date_modified": "2022-09-02T00:00:00.000Z", "author": { "name": "Tony Stark", "url": "https://nextjs-tailwind-contentlayer-blog-starter.vercel.app" } }, { "id": "https://nextjs-tailwind-contentlayer-blog-starter.vercel.app/posts/post-with-code", "url": "https://nextjs-tailwind-contentlayer-blog-starter.vercel.app/posts/post-with-code", "title": "Post with code", "summary": "My post with code", "image": "https://nextjs-tailwind-contentlayer-blog-starter.vercel.app/og-image.png", "date_modified": "2022-09-01T00:00:00.000Z", "author": { "name": "Tony Stark", "url": "https://nextjs-tailwind-contentlayer-blog-starter.vercel.app" } }, { "id": "https://nextjs-tailwind-contentlayer-blog-starter.vercel.app/posts/markdown-demo", "url": "https://nextjs-tailwind-contentlayer-blog-starter.vercel.app/posts/markdown-demo", "title": "Markdown demo", "summary": "This is a demo of Markdown", "image": "https://nextjs-tailwind-contentlayer-blog-starter.vercel.app/og-image.png", "date_modified": "2022-08-31T00:00:00.000Z", "author": { "name": "Tony Stark", "url": "https://nextjs-tailwind-contentlayer-blog-starter.vercel.app" } }, { "id": "https://nextjs-tailwind-contentlayer-blog-starter.vercel.app/posts/sample-post", "url": "https://nextjs-tailwind-contentlayer-blog-starter.vercel.app/posts/sample-post", "title": "Sample post", "summary": "My first post", "image": "https://nextjs-tailwind-contentlayer-blog-starter.vercel.app/og-image.png", "date_modified": "2022-08-30T00:00:00.000Z", "author": { "name": "Tony Stark", "url": "https://nextjs-tailwind-contentlayer-blog-starter.vercel.app" } } ] } ``` `public/feed.xml`: ```xml Next.js Tailwind Contentlayer Blog Starter https://nextjs-tailwind-contentlayer-blog-starter.vercel.app Blog starter template with modern frontend technologies like Next.js, Tailwind CSS, Contentlayer, i18Next Tue, 04 Oct 2022 15:38:07 GMT https://validator.w3.org/feed/docs/rss2.html https://github.com/jpmonette/feed Next.js Tailwind Contentlayer Blog Starter https://nextjs-tailwind-contentlayer-blog-starter.vercel.app/logo.png https://nextjs-tailwind-contentlayer-blog-starter.vercel.app Copyright © 2015 - 2022 Stark Industries <![CDATA[Post with images]]> https://nextjs-tailwind-contentlayer-blog-starter.vercel.app/posts/post-with-images https://nextjs-tailwind-contentlayer-blog-starter.vercel.app/posts/post-with-images Fri, 02 Sep 2022 00:00:00 GMT stark@example.com (Tony Stark) <![CDATA[Post with code]]> https://nextjs-tailwind-contentlayer-blog-starter.vercel.app/posts/post-with-code https://nextjs-tailwind-contentlayer-blog-starter.vercel.app/posts/post-with-code Thu, 01 Sep 2022 00:00:00 GMT stark@example.com (Tony Stark) <![CDATA[Markdown demo]]> https://nextjs-tailwind-contentlayer-blog-starter.vercel.app/posts/markdown-demo https://nextjs-tailwind-contentlayer-blog-starter.vercel.app/posts/markdown-demo Wed, 31 Aug 2022 00:00:00 GMT stark@example.com (Tony Stark) <![CDATA[Sample post]]> https://nextjs-tailwind-contentlayer-blog-starter.vercel.app/posts/sample-post https://nextjs-tailwind-contentlayer-blog-starter.vercel.app/posts/sample-post Tue, 30 Aug 2022 00:00:00 GMT stark@example.com (Tony Stark) ``` 這篇修改的程式碼如下: https://github.com/eason-dev/nextjs-tailwind-contentlayer-blog-starter/compare/day18-sitemap...day19-rss-feed ## References - [jpmonette/feed: A RSS, Atom and JSON Feed generator for Node.js, making content syndication simple and intuitive! ?](https://github.com/jpmonette/feed) - [Generate RSS feeds for your static Next.js blog | Phiilu | Florian Kapfenberger](https://phiilu.com/generate-rss-feeds-for-your-static-next-js-blog) - [為 Next.js 靜態網站產生 RSS feed - kpman | code](https://code.kpman.cc/2021/05/25/next-js-rss/) ## 下一篇 恭喜你完成了 RSS Feed 生成! 目前部落格該有的功能都具備了,已經是個可以實際上線的專案了。 但後面還有 11 天,我們當然不會止步於此。後面 11 天我們會繼續加入更多炫砲功能,繼續朝向 Modern Blog 前進。 下一篇我們會修改文章內文 heading 小標題樣式,為每個 heading 加入 anchor 連結! --- # Generating Sitemap with next-sitemap - Modern Next.js Blog Series #18 - URL: https://easonchang.com/posts/next-sitemap - Date: 2022-10-03T00:00:00.000Z - Language: en - Description: Generate a Sitemap for the website using next-sitemap, allowing crawlers to understand the site structure and enhance SEO > This article is also published at [it 邦幫忙 2022 iThome Ironman Contest](https://ithelp.ithome.com.tw/articles/10303553) ## TL;DR This is the 18th article in the "Modern Blog 30 Days" series. In the previous article, we configured site-wide meta data. Today, we'll use [next-sitemap](https://github.com/iamvishnusankar/next-sitemap) to generate a Sitemap, helping crawlers understand the website structure. The code changes for this article are as follows: https://github.com/eason-dev/nextjs-tailwind-contentlayer-blog-starter/compare/day17-open-graph-meta...day18-sitemap --- ## Adding a Sitemap Another SEO detail that needs to be addressed is generating a Sitemap. A Sitemap is an XML file that lists all pages of a website, ensuring search engine crawlers don't miss any pages when indexing your site. Sitemaps are typically located at a website’s `/sitemap.xml` path, for example, the Sitemap for this project is available at: [https://nextjs-tailwind-contentlayer-blog-starter.vercel.app/sitemap.xml](https://nextjs-tailwind-contentlayer-blog-starter.vercel.app/sitemap.xml) In Next.js, we can use [next-sitemap](https://github.com/iamvishnusankar/next-sitemap) to generate a Sitemap for us. next-sitemap supports pages of all rendering strategies. And for our blog, where all pages are statically generated, no complex setup is required. next-sitemap can generate a Sitemap for us by analyzing the structure under `/src/pages` when executing `pnpm build`. ### Installing next-sitemap ```shell pnpm add -D next-sitemap ``` Add `next-sitemap.config.js`: ```js /** @type {import('next-sitemap').IConfig} */ module.exports = { siteUrl: "https://nextjs-tailwind-contentlayer-blog-starter.vercel.app", generateRobotsTxt: true, }; ``` Modify the build script in `package.json`: ```json { // ... "scripts": { // ... "build": "next build && next-sitemap --config next-sitemap.config.js" // ... } // ... } ``` Modify `.gitignore`, `.eslintignore`, `.prettierignore` to ignore the generated sitemap files: ``` # ... # Add the following 2 rules # Sitemap related files (generated by next-sitemap) /public/robots.txt /public/sitemap*.xml ``` ## Results Done! After running `pnpm build`, you'll find `sitemap.xml` and `robots.txt` in the /public path. The generated content includes: `public/robots.txt`: ``` # * User-agent: * Allow: / # Host Host: https://nextjs-tailwind-contentlayer-blog-starter.vercel.app # Sitemaps Sitemap: https://nextjs-tailwind-contentlayer-blog-starter.vercel.app/sitemap.xml ``` `public/sitemap.xml`: (Example XML content for sitemap.xml) `public/sitemap-0.xml`: (Example XML content for sitemap-0.xml) After executing `pnpm build`, next-sitemap will also inform you that it has generated the Sitemap related files: ![next-sitemap console output](https://i.imgur.com/kXHcQEa.jpg) The code changes for this article are as follows: https://github.com/eason-dev/nextjs-tailwind-contentlayer-blog-starter/compare/day17-open-graph-meta...day18-sitemap ## References - [iamvishnusankar/next-sitemap: Sitemap generator for next.js. Generate sitemap(s) and robots.txt for all static/pre-rendered/dynamic/server-side pages.](https://github.com/iamvishnusankar/next-sitemap) ## Next Article Having completed the Sitemap generation, the next article will deal with generating a very similar RSS feed! --- # 使用 next-sitemap 生成 Sitemap - Modern Next.js Blog 系列 #18 - URL: https://easonchang.com/zh-TW/posts/next-sitemap - Date: 2022-10-03T00:00:00.000Z - Language: zh-TW - Description: 用 next-sitemap 生成 Sitemap 網站地圖,讓爬蟲了解網站結構,強化 SEO > 本文同步發佈於 [it 邦幫忙 2022 iThome 鐵人賽](https://ithelp.ithome.com.tw/articles/10303553) ## TL;DR 這是「Modern Blog 30 天」系列第 18 篇文章,上一篇我們設定完全站 meta data 了,今天我們來用 [next-sitemap](https://github.com/iamvishnusankar/next-sitemap) 來生成 Sitemap 網站地圖,讓爬蟲了解網站結構。 這篇修改的程式碼如下: https://github.com/eason-dev/nextjs-tailwind-contentlayer-blog-starter/compare/day17-open-graph-meta...day18-sitemap --- ## 加入 Sitemap 網站地圖 另外一個需要處理的 SEO 細節就是生成 Sitemap。Sitemap 是一個 xml 檔案,裡面標示了一個網站的所有頁面,能確保搜尋引擎爬蟲在爬取你的網站時,不會漏掉任何一頁。 Sitemap 通常放在一個網站的 `/sitemap.xml` 路徑,像是我這個專案的 Sitemap 就在這個網址: [https://nextjs-tailwind-contentlayer-blog-starter.vercel.app/sitemap.xml](https://nextjs-tailwind-contentlayer-blog-starter.vercel.app/sitemap.xml) 在 Next.js 我們能使用 [next-sitemap](https://github.com/iamvishnusankar/next-sitemap) 幫我們生成 Sitemap。 next-sitemap 支援所有渲染策略的頁面。而且以我們的部落格來說,頁面全都是 Static 靜態生成的,因此不需要做複雜設定,next-sitemap 就能根據 `/src/pages` 底下的結構,知道我們有哪些頁面,幫我們在執行 `pnpm build` 順便生成 Sitemap。 ### 安裝 next-sitemap ```shell pnpm add -D next-sitemap ``` 新增 `next-sitemap.config.js`: ```js /** @type {import('next-sitemap').IConfig} */ module.exports = { siteUrl: "https://nextjs-tailwind-contentlayer-blog-starter.vercel.app", generateRobotsTxt: true, }; ``` 修改 `package.json` 的 build script: ```json { // ... "scripts": { // ... "build": "next build && next-sitemap --config next-sitemap.config.js" // ... } // ... } ``` 修改 `.gitignore`、`.eslintignore`、`.prettierignore`,忽略生成的 sitemap: ``` # ... # 加入下面這兩條規則 # Sitemap related files (generated by next-sitemap) /public/robots.txt /public/sitemap*.xml ``` ## 成果 完成了!使用 `pnpm build`,執行完就會看到 /public 路徑裡多了 `sitemap.xml` 和 `robots.txt` 了 生成的內容如下: `public/robots.txt`: ``` # * User-agent: * Allow: / # Host Host: https://nextjs-tailwind-contentlayer-blog-starter.vercel.app # Sitemaps Sitemap: https://nextjs-tailwind-contentlayer-blog-starter.vercel.app/sitemap.xml ``` `public/sitemap.xml`: ```xml https://nextjs-tailwind-contentlayer-blog-starter.vercel.app/sitemap-0.xml ``` `public/sitemap-0.xml`: ```xml https://nextjs-tailwind-contentlayer-blog-starter.vercel.app2022-10-03T14:34:13.703Zdaily0.7 https://nextjs-tailwind-contentlayer-blog-starter.vercel.app/posts/post-with-images2022-10-03T14:34:13.703Zdaily0.7 https://nextjs-tailwind-contentlayer-blog-starter.vercel.app/posts/post-with-code2022-10-03T14:34:13.703Zdaily0.7 https://nextjs-tailwind-contentlayer-blog-starter.vercel.app/posts/markdown-demo2022-10-03T14:34:13.703Zdaily0.7 https://nextjs-tailwind-contentlayer-blog-starter.vercel.app/posts/sample-post2022-10-03T14:34:13.703Zdaily0.7 ``` 執行 pnpm build 的最後,next-sitemap 也會跟你說它幫你生成了 Sitemap 相關檔案: ![next-sitemap console output](https://i.imgur.com/kXHcQEa.jpg) 這篇修改的程式碼如下: https://github.com/eason-dev/nextjs-tailwind-contentlayer-blog-starter/compare/day17-open-graph-meta...day18-sitemap ## References - [iamvishnusankar/next-sitemap: Sitemap generator for next.js. Generate sitemap(s) and robots.txt for all static/pre-rendered/dynamic/server-side pages.](https://github.com/iamvishnusankar/next-sitemap) ## 下一篇 這篇我們做完的 Sitemap 生成,下一篇我們來處理非常相似的 RSS feed 生成! --- # Adding Open Graph, LD-JSON, and Other SEO Meta Data - Modern Next.js Blog Series #17 - URL: https://easonchang.com/posts/next-seo-meta-data - Date: 2022-10-02T00:00:00.000Z - Language: en - Description: Using next-seo to configure titles, descriptions, thumbnails, Open Graph, LD-JSON for the entire site, making each page understandable to search engines and enhancing SEO > This article is also published at [it 邦幫忙 2022 iThome Ironman Contest](https://ithelp.ithome.com.tw/articles/10302724) ## TL;DR This is the 17th article in the "Modern Blog 30 Days" series. In the previous article, we completed all style cutovers. In this article, we will use [next-seo](https://github.com/garmeeh/next-seo) to set up titles, descriptions, thumbnails, Open Graph, LD-JSON for the entire site. This allows search engines to understand the meaning of each page and optimize SEO! Screenshot results as follows: ![Home page meta data](https://i.imgur.com/ADTXxJ2.jpg) ![Post page meta data](https://i.imgur.com/6fvyZaR.jpg) The code changes for this article are as follows: https://github.com/eason-dev/nextjs-tailwind-contentlayer-blog-starter/compare/day16-copy-code-button...day17-open-graph-meta --- ## Adding Meta Data for the Entire Site and Individual Pages Running a blog means wanting people to read it and having your articles found in search engines. To optimize for search engines (Search Engine Optimization, SEO), besides having good content and attractive designs, we also need to make some configurations. These configurations let search engine crawlers know what each page is about and allow social platforms to know which thumbnail to use when an article is shared. This is achieved by inserting many `` tags in the `` of each page, marking the page title, summary, and thumbnail. To add meta tags to a Next.js site, you can use the official `next/head` component and insert `` tags directly, as documented here: [next/head | Next.js](https://nextjs.org/docs/api-reference/next/head). Another method is to use the [next-seo](https://github.com/garmeeh/next-seo) package, which provides a more comprehensive component to help render all necessary meta tags, simplifying the code we need to write. ### Installing next-seo ```shell pnpm add next-seo ``` ### Adding a socialImage Field for Posts to Specify Article Thumbnails We wish to specify thumbnails for each article, the images displayed when an article is shared on social platforms. The `socialImage` is a string that can be a path to an image in the /public folder or a URL to an external image. It is not mandatory; if not specified in an article, a shared thumbnail for the entire site will be used. To add a new article field, modify `/contentlayer.config.ts`: ```ts // ... export const Post = defineDocumentType(() => ({ name: "Post", filePathPattern: `content/posts/**/*.mdx`, contentType: "mdx", fields: { // ... date: { type: "date", required: true, }, // Add socialImage socialImage: { type: "string", }, }, // ... })); // ... ``` ### Specifying socialImage for Articles Now you can specify `socialImage` in the front matter of your articles. You can choose an existing article to add to, or like this commit, add a new image to /public and create a new article to use it as a thumbnail: https://github.com/eason-dev/nextjs-tailwind-contentlayer-blog-starter/commit/799d5ec203ac227fe7a8500c2f9ddcb3677a0e1a ### Configuring Meta Data for Each Page with next-seo See this commit for complete changes: https://github.com/eason-dev/nextjs-tailwind-contentlayer-blog-starter/commit/9c105bc2ba84286eac0d26580240a93e104ddee6 Add `/src/configs/siteConfigs.ts` and modify it with your site's desired content: ```ts const fqdn = "https://nextjs-tailwind-contentlayer-blog-starter.vercel.app"; const logoPath = "/logo.png"; const bannerPath = "/og-image.png"; export const siteConfigs = { title: "Next.js Tailwind Contentlayer Blog Starter", titleShort: "Next Blog", description: "Blog starter template with modern frontend technologies like Next.js, Tailwind CSS, Contentlayer, i18Next", author: "Tony Stark", fqdn: fqdn, logoPath: logoPath, logoUrl: fqdn + logoPath, bannerPath: bannerPath, bannerUrl: fqdn + bannerPath, twitterID: "@easondev", datePublished: "2022-09-01", }; ``` Add `/src/lib/getPostOGImage.ts`: ```ts import { siteConfigs } from "@/configs/siteConfigs"; export const getPostOGImage = (socialImage: string | null): string => { if (!socialImage) { return siteConfigs.bannerUrl; } if (socialImage.startsWith("http")) { return socialImage; } return siteConfigs.fqdn + socialImage; }; ``` Modify `/src/pages/_app.tsx`: (Code snippet for setting up default SEO configuration) Modify `/src/pages/index.tsx`: (Code snippet for adding ArticleJsonLd to the homepage) Modify `/src/pages/posts/[slug].tsx`: (Code snippet for setting up SEO and ArticleJsonLd for individual post pages) Add the site logo image to `/public/logo.png`. Add the default socialImage for the site to `/public/og-image.png`. ## Results Done! By running `pnpm dev` and entering the homepage and article pages, then opening the F12 developer tools to check the `` content, you'll see many new meta data tags! You can install this Chrome browser extension for a more convenient way to check meta data on each page: [META SEO inspector - Chrome Web Store](https://chrome.google.com/webstore/detail/meta-seo-inspector/ibkclpciafdglkjkcibmohobjkcfkaef) The code changes for this article are as follows: https://github.com/eason-dev/nextjs-tailwind-contentlayer-blog-starter/compare/day16-copy-code-button...day17-open-graph-meta ## References - [garmeeh/next-seo: Next SEO is a plugin that makes managing your SEO easier in Next.js projects.](https://github.com/garmeeh/next-seo) - [META SEO inspector - Chrome Web Store](https://chrome.google.com/webstore/detail/meta-seo-inspector/ibkclpciafdglkjkcibmohobjkcfkaef) - [First experience with next-seo](https://blog.errorbaker.tw/posts/clay/next-seo/) - [Free Render Image on Unsplash](https://unsplash.com/photos/aSJAalMXCvI) ## Next Article In the next article, we continue to deal with SEO by adding a sitemap! --- # 加入 Open Graph、LD-JSON 等 SEO meta data - Modern Next.js Blog 系列 #17 - URL: https://easonchang.com/zh-TW/posts/next-seo-meta-data - Date: 2022-10-02T00:00:00.000Z - Language: zh-TW - Description: 使用 next-seo 為全站設定標題、描述文字、縮圖、Open Graph、LD-JSON,讓搜尋引擎理解每個頁面意義,強化 SEO > 本文同步發佈於[it 邦幫忙 2022 iThome 鐵人賽](https://ithelp.ithome.com.tw/articles/10302724) ## TL;DR 這是「Modern Blog 30 天」系列第 17 篇文章,上一篇我們完成所有樣式切版了,這篇我們會使用 [next-seo](https://github.com/garmeeh/next-seo),為全站設定標題、描述文字、縮圖、Open Graph、LD-JSON,讓搜尋引擎知道每個頁面意義,做好 SEO! 結果截圖如下: ![Home page meta data](https://i.imgur.com/ADTXxJ2.jpg) ![Post page meta data](https://i.imgur.com/6fvyZaR.jpg) 這篇修改的程式碼如下: https://github.com/eason-dev/nextjs-tailwind-contentlayer-blog-starter/compare/day16-copy-code-button...day17-open-graph-meta --- ## 為全站及各頁面加入 meta data 經營部落格就是希望人們能來閱讀,能在搜尋引擎搜到你寫的文章。 而要做好搜尋引擎優化(Search Engine Optimization、SEO),除了內容好、樣式好看,我們也需要做些設定,讓搜尋引擎的爬蟲知道每個頁面在描述什麼,以及讓文章被貼到社群平台時,社群平台能知道該使用哪張縮圖。 方法是透過在各頁面 `` 裡插入許多 `` tag,標註頁面標題、概述、縮圖。 要在 Next.js 網站加入 meta tags,可以使用官方的 `next/head` 元件,在裡面插入 `` 來實作,可參考此處官方文件:[next/head | Next.js](https://nextjs.org/docs/api-reference/next/head)。 另外一種方法是使用 [next-seo](https://github.com/garmeeh/next-seo) 套件,它提供了包裝更完整的元件幫助渲染出所有必要的 meta tags,能簡化我們需要自己加的程式碼行數。 ### 安裝 next-seo ```shell pnpm add next-seo ``` ### 為文章加入 socialImage 欄位,指定文章縮圖 我們希望能為每篇文章指定縮圖,在文章配貼到社群平台時會顯示的圖片。 圖片 socialImage 是一個 string,可以是 /public 資料夾內的圖片路徑,也可以是遠端圖片的網址。並且它不是必填的,如果寫文章時沒有指定,就會使用另一張全站共用的縮圖。 新增文章欄位需要修改 `/contentlayer.config.ts`: ```ts // ... export const Post = defineDocumentType(() => ({ name: "Post", filePathPattern: `content/posts/**/*.mdx`, contentType: "mdx", fields: { // ... date: { type: "date", required: true, }, // 新增 socialImage socialImage: { type: "string", }, }, // ... })); // ... ``` ### 為文章指定 socialImage 接著就能在文章最前面區塊指定 socialImage 了,你可以挑一篇現成文章來加,或是像我這個 commit 一樣,新增一張圖片在 /public 裡面,並新增一篇文章來使用它當縮圖: https://github.com/eason-dev/nextjs-tailwind-contentlayer-blog-starter/commit/799d5ec203ac227fe7a8500c2f9ddcb3677a0e1a ### 使用 next-seo 設定各頁面 meta data 完整改動可以看這支 commit: https://github.com/eason-dev/nextjs-tailwind-contentlayer-blog-starter/commit/9c105bc2ba84286eac0d26580240a93e104ddee6 新增 `/src/configs/siteConfigs.ts`,並修改成你的網站想要的內容: ```ts const fqdn = "https://nextjs-tailwind-contentlayer-blog-starter.vercel.app"; const logoPath = "/logo.png"; const bannerPath = "/og-image.png"; export const siteConfigs = { title: "Next.js Tailwind Contentlayer Blog Starter", titleShort: "Next Blog", description: "Blog starter template with modern frontend technologies like Next.js, Tailwind CSS, Contentlayer, i18Next", author: "Tony Stark", fqdn: fqdn, logoPath: logoPath, logoUrl: fqdn + logoPath, bannerPath: bannerPath, bannerUrl: fqdn + bannerPath, twitterID: "@easondev", datePublished: "2022-09-01", }; ``` 新增 `/src/lib/getPostOGImage.ts`: ```ts import { siteConfigs } from "@/configs/siteConfigs"; export const getPostOGImage = (socialImage: string | null): string => { if (!socialImage) { return siteConfigs.bannerUrl; } if (socialImage.startsWith("http")) { return socialImage; } return siteConfigs.fqdn + socialImage; }; ``` 修改 `/src/pages/_app.tsx`: ```tsx import "@/styles/globals.css"; import "@/styles/prism-dracula.css"; import "@/styles/prism-plus.css"; import type { AppProps } from "next/app"; import { DefaultSeo } from "next-seo"; import { ThemeProvider } from "next-themes"; import LayoutWrapper from "@/components/LayoutWrapper"; import { siteConfigs } from "@/configs/siteConfigs"; function MyApp({ Component, pageProps }: AppProps) { return ( ); } export default MyApp; ``` 修改 `/src/pages/index.tsx`: ```tsx import type { NextPage } from "next"; import { GetStaticProps } from "next"; import { ArticleJsonLd } from "next-seo"; import PostList, { PostForPostList } from "@/components/PostList"; import { siteConfigs } from "@/configs/siteConfigs"; import { allPostsNewToOld } from "@/lib/contentLayerAdapter"; type PostForIndexPage = PostForPostList; type Props = { posts: PostForIndexPage[]; }; export const getStaticProps: GetStaticProps = () => { const posts = allPostsNewToOld.map((post) => ({ slug: post.slug, date: post.date, title: post.title, description: post.description, path: post.path, })) as PostForIndexPage[]; return { props: { posts } }; }; const Home: NextPage = ({ posts }) => { return ( <>

Hey,I am Iron Man ?

我是 Tony Stark,不是 Stank!

老子很有錢,拯救過很多次世界。

我討厭外星人、紫色的東西、和紫色外星人。

最新文章

); }; export default Home; ``` 修改 `/src/pages/posts/[slug].tsx`: ```tsx import type { GetStaticPaths, GetStaticProps, NextPage } from "next"; import { useMDXComponent } from "next-contentlayer/hooks"; import { ArticleJsonLd, NextSeo } from "next-seo"; import PostLayout, { PostForPostLayout, RelatedPostForPostLayout, } from "@/components/PostLayout"; import { siteConfigs } from "@/configs/siteConfigs"; import { allPosts, allPostsNewToOld } from "@/lib/contentLayerAdapter"; import { getPostOGImage } from "@/lib/getPostOGImage"; import mdxComponents from "@/lib/mdxComponents"; type PostForPostPage = PostForPostLayout & { title: string; description: string; date: string; path: string; socialImage: string | null; body: { code: string; }; }; type Props = { post: PostForPostPage; prevPost: RelatedPostForPostLayout; nextPost: RelatedPostForPostLayout; }; export const getStaticPaths: GetStaticPaths = () => { const paths = allPosts.map((post) => post.path); return { paths, fallback: false, }; }; export const getStaticProps: GetStaticProps = ({ params }) => { const postIndex = allPostsNewToOld.findIndex( (post) => post.slug === params?.slug ); if (postIndex === -1) { return { notFound: true, }; } const prevFull = allPostsNewToOld[postIndex + 1] || null; const prevPost: RelatedPostForPostLayout = prevFull ? { title: prevFull.title, path: prevFull.path } : null; const nextFull = allPostsNewToOld[postIndex - 1] || null; const nextPost: RelatedPostForPostLayout = nextFull ? { title: nextFull.title, path: nextFull.path } : null; const postFull = allPostsNewToOld[postIndex]; const post: PostForPostPage = { title: postFull.title, date: postFull.date, description: postFull.description, path: postFull.path, socialImage: postFull.socialImage || null, body: { code: postFull.body.code, }, }; if (!post) { return { notFound: true, }; } return { props: { post, prevPost, nextPost, }, }; }; const PostPage: NextPage = ({ post, prevPost, nextPost }) => { const { description, title, date, path, socialImage, body: { code }, } = post; const url = siteConfigs.fqdn + path; const ogImage = getPostOGImage(socialImage); const MDXContent = useMDXComponent(code); return ( <> ); }; export default PostPage; ``` 新增網站 Logo 圖片,放在 `/public/logo.png`。 新增網站預設 socialImage,放在 `/public/og-image.png`。 ## 成果 完成了!使用 `pnpm dev` 並進入首頁和文章頁面,打開 F12 查看原始碼 `` 裡面內容,就會看到多出很多 meta data 了! 可以安裝這套 Chrome 瀏覽器 extension 來更方便查看每個頁面的 meta data:[META SEO inspector - Chrome 線上應用程式商店](https://chrome.google.com/webstore/detail/meta-seo-inspector/ibkclpciafdglkjkcibmohobjkcfkaef) http://localhost:3000/ http://localhost:3000/posts/post-with-code 結果截圖如下: ![Home page meta data](https://i.imgur.com/ADTXxJ2.jpg) ![Post page meta data](https://i.imgur.com/6fvyZaR.jpg) 這篇修改的程式碼如下: https://github.com/eason-dev/nextjs-tailwind-contentlayer-blog-starter/compare/day16-copy-code-button...day17-open-graph-meta ## References - [garmeeh/next-seo: Next SEO is a plug in that makes managing your SEO easier in Next.js projects.](https://github.com/garmeeh/next-seo) - [META SEO inspector - Chrome 線上應用程式商店](https://chrome.google.com/webstore/detail/meta-seo-inspector/ibkclpciafdglkjkcibmohobjkcfkaef) - [next-seo 初體驗](https://blog.errorbaker.tw/posts/clay/next-seo/) - [Free Render Image on Unsplash](https://unsplash.com/photos/aSJAalMXCvI) ## 下一篇 下一篇我們繼續處理 SEO,來加入 sitemap! --- # Adding 'Copy Button' to Code Blocks Using Custom MDX Components - Modern Next.js Blog Series #16 - URL: https://easonchang.com/posts/code-copy-button - Date: 2022-10-01T00:00:00.000Z - Language: en - Description: Add a 'Copy Button' to code blocks using the custom MDX component CustomPre > This article is also published at [it 邦幫忙 2022 iThome Ironman Contest](https://ithelp.ithome.com.tw/articles/10302397) In the previous article, we made code blocks display titles. In this article, we continue to enhance their usability by adding a 'Copy Button'! Screenshot results as follows: ![Code block copy button in dark mode](https://i.imgur.com/ByJtP4Z.jpg) ![Code block copy button copied](https://i.imgur.com/eq2Ermv.jpg) > The code changes for this article are as follows: > https://github.com/eason-dev/nextjs-tailwind-contentlayer-blog-starter/compare/day15-code-block-title...day16-copy-code-button --- ## Adding a Copy Button to Code Blocks We will use the method of customizing MDX components to add a copy button. When rendering code blocks, Markdown renders them into `
` elements.

We can create our own `` React component, and with a little configuration, make MDX render code blocks using our ``.

The specific implementation method refers to this article: [How to add a copy code button to your blog posts - Phil Stainer](https://philstainer.io/blog/copy-code-button-markdown).

### Adding ``

Add `/src/components/CustomPre.tsx`:

```tsx
// ref: https://philstainer.io/blog/copy-code-button-markdown

import clsx from "clsx";
import { useEffect, useRef, useState } from "react";

import { copyToClipboard } from "@/lib/copyToClipboard";
import { removeDuplicateNewLine } from "@/lib/removeDuplicateNewLine";

type Props = React.ComponentPropsWithoutRef<"pre">;

function CustomPre({ children, className, ...props }: Props) {
  const preRef = useRef(null);

  const [copied, setCopied] = useState(false);

  useEffect(() => {
    const timer = setTimeout(() => setCopied(false), 2000);

    return () => clearTimeout(timer);
  }, [copied]);

  const onClick = async () => {
    if (preRef.current?.innerText) {
      await copyToClipboard(removeDuplicateNewLine(preRef.current.innerText));
      setCopied(true);
    }
  };

  return (
    
        
Copied!
{children}
); } export default CustomPre; ``` ### Adding Two Functions for Copy Logic Add `/src/lib/copyToClipboard.ts`: ```ts // ref: https://philstainer.io/blog/copy-code-button-markdown export const copyToClipboard = (text: string) => { return new Promise((resolve, reject) => { if (navigator?.clipboard) { const cb = navigator.clipboard; cb.writeText(text).then(resolve).catch(reject); } else { try { const body = document.querySelector("body"); const textarea = document.createElement("textarea"); body?.appendChild(textarea); textarea.value = text; textarea.select(); document.execCommand("copy"); body?.removeChild(textarea); resolve(void 0); } catch (e) { reject(e); } } }); }; ``` Add `/src/lib/removeDuplicateNewLine.ts`: ```ts // Workaround to work with rehype-prism-plus generated Pre block for copy to clipboard feature export const removeDuplicateNewLine = (text: string): string => { if (!text) return text; return text .replace(/(\r\n\r\n)/gm, `\r\n`) .replace(/(\n\n)/gm, `\n`) .replace(/(\r\r)/gm, `\r`); }; ``` ### Replace Article Code Blocks with `` Add `/src/lib/mdxComponents.ts`: ```ts import CustomPre from "@/components/CustomPre"; // Custom components/renderers to pass to MDX. const mdxComponents = { pre: CustomPre, }; export default mdxComponents; ``` Modify `/src/pages/posts/[slug].tsx`, import mdxComponents and pass them to ``: ```tsx import type { GetStaticPaths, GetStaticProps, NextPage } from "next"; // ... // Add the following line to import mdxComponents import mdxComponents from "@/lib/mdxComponents"; // ... const PostPage: NextPage = ({ post, prevPost, nextPost }) => { // ... const MDXContent = useMDXComponent(code); return ( <> {title} // Modify the following line, passing mdxComponents to MDXContent ); }; export default PostPage; ``` ## Results Done! By running `pnpm dev` and entering an article containing code blocks, you'll see the code blocks now have a copy button! http://localhost:3000/posts/post-with-code Screenshot results as follows: ![Code block copy button in dark mode](https://i.imgur.com/ByJtP4Z.jpg) ![Code block copy button copied](https://i.imgur.com/eq2Ermv.jpg) ## References - [How to add a copy code button to your blog posts - Phil Stainer](https://philstainer.io/blog/copy-code-button-markdown) ## Summary & Next Article Congratulations on successfully adding a 'Copy Button' to the code blocks. This was our final article on adjusting the blog style; it's already looking pretty good. > The code changes for this article are as follows: > https://github.com/eason-dev/nextjs-tailwind-contentlayer-blog-starter/compare/day15-code-block-title...day16-copy-code-button In the next article, let's use [next-seo](https://github.com/garmeeh/next-seo) to add Open Graph, meta data, and other SEO optimization techniques to the entire site! --- # 加入程式碼區塊「複製按鈕」,使用客製化 MDX 元件 - Modern Next.js Blog 系列 #16 - URL: https://easonchang.com/zh-TW/posts/code-copy-button - Date: 2022-10-01T00:00:00.000Z - Language: zh-TW - Description: 使用客製化 MDX 元件 CustomPre,在程式碼區塊加入「複製按鈕」 > 本文同步發佈於 [it 邦幫忙 2022 iThome 鐵人賽](https://ithelp.ithome.com.tw/articles/10302397) 上一篇我們讓程式碼區塊顯示了標題,這篇我們繼續讓它更好用,來加入「複製按鈕」! 結果截圖如下: ![Code block copy button in dark mode](https://i.imgur.com/ByJtP4Z.jpg) ![Code block copy button copied](https://i.imgur.com/eq2Ermv.jpg) > 這篇修改的程式碼如下: > https://github.com/eason-dev/nextjs-tailwind-contentlayer-blog-starter/compare/day15-code-block-title...day16-copy-code-button --- ## 在程式碼區塊加入複製按鈕 我們將使用 MDX 客製化元件的方法來加入複製按鈕。 Markdown 在渲染程式碼區塊時,會渲染成 `
` 元素。

我們可以新增自己的 `` React 元件,並稍做設定,來讓 MDX 渲染程式碼區塊時,改用我們提供的 `` 來渲染。

具體實作方法是參考這篇文章:[How to add a copy code button to your blog posts - Phil Stainer](https://philstainer.io/blog/copy-code-button-markdown)。

### 新增 ``

新增 `/src/components/CustomPre.tsx`:

```tsx
// ref: https://philstainer.io/blog/copy-code-button-markdown

import clsx from "clsx";
import { useEffect, useRef, useState } from "react";

import { copyToClipboard } from "@/lib/copyToClipboard";
import { removeDuplicateNewLine } from "@/lib/removeDuplicateNewLine";

type Props = React.ComponentPropsWithoutRef<"pre">;

function CustomPre({ children, className, ...props }: Props) {
  const preRef = useRef(null);

  const [copied, setCopied] = useState(false);

  useEffect(() => {
    const timer = setTimeout(() => setCopied(false), 2000);

    return () => clearTimeout(timer);
  }, [copied]);

  const onClick = async () => {
    if (preRef.current?.innerText) {
      await copyToClipboard(removeDuplicateNewLine(preRef.current.innerText));
      setCopied(true);
    }
  };

  return (
    
        
已複製!
{children}
); } export default CustomPre; ``` ### 新增處理複製邏輯的 2 個 Function 新增 `/src/lib/copyToClipboard.ts`: ```ts // ref: https://philstainer.io/blog/copy-code-button-markdown export const copyToClipboard = (text: string) => { return new Promise((resolve, reject) => { if (navigator?.clipboard) { const cb = navigator.clipboard; cb.writeText(text).then(resolve).catch(reject); } else { try { const body = document.querySelector("body"); const textarea = document.createElement("textarea"); body?.appendChild(textarea); textarea.value = text; textarea.select(); document.execCommand("copy"); body?.removeChild(textarea); resolve(void 0); } catch (e) { reject(e); } } }); }; ``` 新增 `/src/lib/removeDuplicateNewLine.ts`: ```ts // Workaround to work with rehype-prism-plus generated Pre blog for copy to clipboard feature export const removeDuplicateNewLine = (text: string): string => { if (!text) return text; return text .replace(/(\r\n\r\n)/gm, `\r\n`) .replace(/(\n\n)/gm, `\n`) .replace(/(\r\r)/gm, `\r`); }; ``` ### 用 `` 替換掉文章內文程式碼區塊 新增 `/src/lib/mdxComponents.ts`: ```ts import CustomPre from "@/components/CustomPre"; // Custom components/renderers to pass to MDX. const mdxComponents = { pre: CustomPre, }; export default mdxComponents; ``` 修改 `/src/pages/posts/[slug].tsx`,import mdxComponents 傳給 ``: ```tsx import type { GetStaticPaths, GetStaticProps, NextPage } from "next"; // ... // 新增下面這行,import mdxComponents import mdxComponents from "@/lib/mdxComponents"; // ... const PostPage: NextPage = ({ post, prevPost, nextPost }) => { // ... const MDXContent = useMDXComponent(code); return ( <> {title} // 修改下面這行,把 mdxComponents 傳給 MDXContent ); }; export default PostPage; ``` ## 成果 完成了!使用 `pnpm dev` 並進入含有程式碼區塊的文章,就會看到程式碼區塊多出複製按鈕了! http://localhost:3000/posts/post-with-code 結果截圖如下: ![Code block copy button in dark mode](https://i.imgur.com/ByJtP4Z.jpg) ![Code block copy button copied](https://i.imgur.com/eq2Ermv.jpg) ## References - [How to add a copy code button to your blog posts - Phil Stainer](https://philstainer.io/blog/copy-code-button-markdown) ## 小結&下一篇 恭喜你成功加入「複製按鈕」到程式碼區塊了。 這是我們最後一篇部落格樣式調整了,目前它已經相當好看了。 > 這篇修改的程式碼如下: > https://github.com/eason-dev/nextjs-tailwind-contentlayer-blog-starter/compare/day15-code-block-title...day16-copy-code-button 下一篇讓我們來使用 [next-seo](https://github.com/garmeeh/next-seo),為全站加入 Open Graph、meta data 等 SEO 優化手法! --- # Adding Titles to Code Blocks Using rehype-code-titles - Modern Next.js Blog Series #15 - URL: https://easonchang.com/posts/rehype-code-titles - Date: 2022-09-30T00:00:00.000Z - Language: en - Description: Using rehype-code-titles to add titles to each code block > This article is also published at [it 邦幫忙 2022 iThome Ironman Contest](https://ithelp.ithome.com.tw/articles/10301271) In the previous article, we enabled Syntax Highlighting for code blocks. In this article, we'll add titles to them! > The code changes for this article are as follows: > https://github.com/eason-dev/nextjs-tailwind-contentlayer-blog-starter/compare/day14-code-syntax-highlight...day15-code-block-title Screenshot results as follows: ![Post with code](https://i.imgur.com/FCKHNvh.jpg) ![Post with code in dark mode](https://i.imgur.com/jAr8Isc.jpg) --- ## Using rehype-code-titles to Add Titles to Each Code Block Install [rehype-code-titles](https://github.com/rockchalkwushock/rehype-code-titles): ```shell pnpm add rehype-code-titles ``` Activate it by modifying `/contentlayer.config.ts`, adding rehype-code-titles to the `rehypePlugins` list: ```ts import rehypeCodeTitles from "rehype-code-titles"; // Add this line import rehypePrism from "rehype-prism-plus"; import { defineDocumentType, makeSource } from "./src/lib/contentLayerAdapter"; // ... export default makeSource({ contentDirPath: "content", documentTypes: [Post], mdx: { // Add to the rehypePlugins list rehypePlugins: [rehypeCodeTitles, [rehypePrism, { ignoreMissing: true }]], }, }); ``` By now, rehype-code-titles installation is complete, but it won't have any style by default. We need to specify it ourselves. Modify `/src/components/PostBody/PostBody.module.scss`: ```scss .postBody { // Add the following two sections :global(.rehype-code-title) { @apply -mb-3 rounded-tl rounded-tr bg-slate-600 px-4 pt-1 pb-2 font-mono text-sm text-gray-200; } div:global(.rehype-code-title) + pre { @apply rounded-tl-none rounded-tr-none; } # ... } ``` This completes all settings! ## Modify the Article to Add Titles to Code Blocks After the programming language marker in the code block Markdown, add a `:` colon and enter any text. This text will be considered as a title by rehype-code-title. For example, modify `/content/posts/20220901-post-with-code.mdx`: https://github.com/eason-dev/nextjs-tailwind-contentlayer-blog-starter/commit/4f042ea8861adaa2d53fd7fc1a461459862719de ![Post modification for code title](https://i.imgur.com/fWZ72A0.jpg) ## Results Done! By running `pnpm dev` and entering the recently modified article, you'll see the code blocks now have their own titles! http://localhost:3000/posts/post-with-code Screenshot results as follows: ![Post with code](https://i.imgur.com/FCKHNvh.jpg) ![Post with code in dark mode](https://i.imgur.com/jAr8Isc.jpg) ## References - [rockchalkwushock/rehype-code-titles: Rehype plugin for parsing code blocks and adding titles to code blocks](https://github.com/rockchalkwushock/rehype-code-titles) ## Next Article Congratulations on successfully adding titles to the code blocks. > The code changes for this article are as follows: > https://github.com/eason-dev/nextjs-tailwind-contentlayer-blog-starter/compare/day14-code-syntax-highlight...day15-code-block-title In the next article, the last one focusing on adjusting code blocks, we will add a 'Copy Button' for one-click code copying! --- # 加入程式碼區塊標題,使用 rehype-code-titles - Modern Next.js Blog 系列 #15 - URL: https://easonchang.com/zh-TW/posts/rehype-code-titles - Date: 2022-09-30T00:00:00.000Z - Language: zh-TW - Description: 使用 rehype-code-titles,為每個程式碼區塊加入標題 > 本文同步發佈於 [it 邦幫忙 2022 iThome 鐵人賽](https://ithelp.ithome.com.tw/articles/10301271) 上一篇我們讓程式碼區塊支援 Syntax Highlighting 了,這篇我們來讓它多出區塊標題! > 這篇修改的程式碼如下: > https://github.com/eason-dev/nextjs-tailwind-contentlayer-blog-starter/compare/day14-code-syntax-highlight...day15-code-block-title 結果截圖如下: ![Post with code](https://i.imgur.com/FCKHNvh.jpg) ![Post with code in dark mode](https://i.imgur.com/jAr8Isc.jpg) --- ## 使用 rehype-code-titles,為每個程式碼區塊加入標題 安裝 [rehype-code-titles](https://github.com/rockchalkwushock/rehype-code-titles): ```shell pnpm add rehype-code-titles ``` 啟用它,修改 `/contentlayer.config.ts`,將 rehype-code-titles 加入到 `rehypePlugins` 列表: ```ts import rehypeCodeTitles from "rehype-code-titles"; // 新增這行 import rehypePrism from "rehype-prism-plus"; import { defineDocumentType, makeSource } from "./src/lib/contentLayerAdapter"; // ... export default makeSource({ contentDirPath: "content", documentTypes: [Post], mdx: { // 新增到 rehypePlugins 列表裡 rehypePlugins: [rehypeCodeTitles, [rehypePrism, { ignoreMissing: true }]], }, }); ``` 到此為止就完成 rehype-code-titles 的安裝了,但他預設不會有任何樣式,我們需要自己指定。 修改 `/src/components/PostBody/PostBody.module.scss`: ```scss .postBody { # 新增下面這兩塊 :global(.rehype-code-title) { @apply -mb-3 rounded-tl rounded-tr bg-slate-600 px-4 pt-1 pb-2 font-mono text-sm text-gray-200; } div:global(.rehype-code-title) + pre { @apply rounded-tl-none rounded-tr-none; } # ... } ``` 這樣就完成所有設定了! ## 修改文章,在程式碼區塊加入標題 在程式碼區塊 Markdown 的程式語言標示後,加入 `:` 冒號,並輸入任意文字,這些文字就會被 rehype-code-title 當成標題了。 像是這樣,修改 `/content/posts/20220901-post-with-code.mdx`: https://github.com/eason-dev/nextjs-tailwind-contentlayer-blog-starter/commit/4f042ea8861adaa2d53fd7fc1a461459862719de ![Post modification for code title](https://i.imgur.com/fWZ72A0.jpg) ## 成果 完成了!使用 `pnpm dev` 並進入剛剛修改的文章,就會看到程式碼區塊多了各自的標題了! http://localhost:3000/posts/post-with-code 結果截圖如下: ![Post with code](https://i.imgur.com/FCKHNvh.jpg) ![Post with code in dark mode](https://i.imgur.com/jAr8Isc.jpg) ## References - [rockchalkwushock/rehype-code-titles: Rehype plugin for parsing code blocks and adding titles to code blocks](https://github.com/rockchalkwushock/rehype-code-titles) ## 下一篇 恭喜你成功讓程式碼區塊多出標題了。 > 這篇修改的程式碼如下: > https://github.com/eason-dev/nextjs-tailwind-contentlayer-blog-starter/compare/day14-code-syntax-highlight...day15-code-block-title 下一篇是最後一篇針對程式碼區塊調整,我們會加上一鍵複製程式的「複製按鈕」! --- # Adding Syntax Highlighting to Code Blocks Using rehype-prism-plus - Modern Next.js Blog Series #14 - URL: https://easonchang.com/posts/rehype-prism-plus-syntax-highlighting - Date: 2022-09-29T00:00:00.000Z - Language: en - Description: Using the rehype-prism-plus package to add Syntax Highlighting effects to code blocks > This article is also published at [it 邦幫忙 2022 iThome Ironman Contest](https://ithelp.ithome.com.tw/articles/10301243) The soul of a tech blog lies in the code within its articles. Code blocks need to be readable for readers to want to continue reading. At a minimum, code blocks should support "**Syntax Highlighting**," displaying different keywords in different colors. In this article, we'll use [rehype-prism-plus](https://github.com/timlrx/rehype-prism-plus) to add Syntax Highlighting effects to code blocks! > The code changes for this article are as follows: > https://github.com/eason-dev/nextjs-tailwind-contentlayer-blog-starter/compare/day13-basic-post-page-ui...day14-code-syntax-highlight Screenshot results as follows: ![Post with code](https://i.imgur.com/6yFw7B0.jpg) ![Post with code in dark mode](https://i.imgur.com/SyVynzH.jpg) --- ## Syntax Highlighting To add Syntax Highlighting to web code blocks, there are several packages available, including popular ones like [PrismJS](https://prismjs.com/) and [highlight.js](https://highlightjs.org/). Here, we'll use PrismJS. ## Adding Syntax Highlighting with rehype-prism-plus Our blog uses [Contentlayer](https://www.contentlayer.dev/), which processes Markdown and MDX files using [mdx-bundler](https://github.com/kentcdodds/mdx-bundler). mdx-bundler, in turn, uses [mdx-js](https://mdxjs.com/). To customize how mdx-js processes Markdown or MDX, you can install [rehype](https://github.com/rehypejs/rehype) or [remark](https://github.com/remarkjs/remark) plugins. They are responsible for converting Markdown and MDX to HTML, and installing plugins allows us to modify the HTML as desired. We can use the [rehype-prism-plus](https://github.com/timlrx/rehype-prism-plus) rehype plugin to implement PrismJS's Syntax Highlighting. It supports a vast array of programming languages (see [PrismJS language list](https://prismjs.com/#supported-languages)) and dozens of themes (see [PrismJS theme list](https://github.com/PrismJS/prism-themes)), as well as highlighting specific lines, and showing red and green diff files. ## Installing rehype-prism-plus Run the following command to install: ```shell pnpm add rehype-prism-plus ``` Modify `contentlayer.config.ts`, adding rehypePrism to the rehypePlugins for mdx: ```ts // Add the following line import rehypePrism from "rehype-prism-plus"; // ... export default makeSource({ // ... // Add the following line mdx: { rehypePlugins: [[rehypePrism, { ignoreMissing: true }]] }, }); ``` Add `src/styles/prism-plus.css`, styles for line numbers highlighting by rehype-prism-plus: ```css /* https://github.com/timlrx/rehype-prism-plus#styling */ pre { overflow-x: auto; } /** * Inspired by gatsby remark prism - https://www.gatsbyjs.com/plugins/gatsby-remark-prismjs/ * 1. Make the element just wide enough to fit its content. * 2. Always fill the visible space in .code-highlight. */ .code-highlight { float: left; /* 1 */ min-width: 100%; /* 2 */ } .code-line { display: block; padding-left: 16px; padding-right: 16px; margin-left: -16px; margin-right: -16px; border-left-width: 4px; border-left-color: rgba(31, 41, 55, 0); /* Set code block color */ } .code-line.inserted { background-color: rgba(16, 185, 129, 0.2); /* Set inserted line (+) color */ } .code-line.deleted { background-color: rgba(239, 68, 68, 0.2); /* Set deleted line (-) color */ } .highlight-line { margin-left: -16px; margin-right: -16px; background-color: rgba(55, 65, 81, 0.5); /* Set highlight bg color */ border-left-width: 4px; border-left-color: rgb(59, 130, 246); /* Set highlight accent border color */ } .line-number::before { display: inline-block; width: 1rem; text-align: right; margin-right: 16px; margin-left: -8px; color: rgb(156, 163, 175); /* Line number color */ content: attr(line); } ``` Next, you'll also need to add a basic theme for PrismJS. > You can find your preferred theme here: > https://github.com/PrismJS/prism-themes > > I personally use this Dracula theme: > https://github.com/PrismJS/prism-themes/blob/master/themes/prism-darcula.css Add `src/styles/prism-dracula.css`, a chosen theme style for PrismJS: ```css /** * Dracula Theme originally by Zeno Rocha [@zenorocha] * https://draculatheme.com/ * * Ported for PrismJS by Albert Vallverdu [@byverdu] */ /* Style content omitted for brevity */ ``` Modify `src/pages/_app.tsx` to include the new CSS files: ```tsx import "@/styles/prism-dracula.css"; import "@/styles/prism-plus.css"; // ... ``` This completes all settings! ## Adding an Article Containing Code Add `/content/posts/20220901-post-with-code.mdx`: > Due to contest article restrictions, code blocks within code blocks are indented. Remember to remove the indentation when pasting into your own blog. > > Or view the original `20220901-post-with-code.mdx` content here: > > https://github.com/eason-dev/nextjs-tailwind-contentlayer-blog-starter/blob/0f72528c220430e21bee232dd9f81b6e175086d2/content/posts/20220901-post-with-code.mdx?plain=1 ## Results Done! By running `pnpm dev` and entering the newly added article, you'll see the code styling looks much better! http://localhost:3000/posts/post-with-code Screenshot results as follows: ![Post with code](https://i.imgur.com/6yFw7B0.jpg) ![Post with code in dark mode](https://i.imgur.com/SyVynzH.jpg) ## References - [timlrx/tailwind-nextjs-starter-blog: This is a Next.js, Tailwind CSS blogging starter template. Comes out of the box configured with the latest technologies to make technical writing a breeze. Easily configurable and customizable. Perfect as a replacement to existing Jekyll and Hugo individual blogs.](https://github.com/timlrx/tailwind-nextjs-starter-blog) - [timlrx/rehype-prism-plus: rehype plugin to highlight code blocks in HTML with Prism (via refractor) with line highlighting and line numbers](https://github.com/timlrx/rehype-prism-plus) - [PrismJS/prism-themes: A wider selection of Prism themes](https://github.com/PrismJS/prism-themes) - [Processing MDX Files - Contentlayer](https://www.contentlayer.dev/docs/sources/files/mdx) - [kentcdodds/mdx-bundler: ? Give me MDX/TSX strings and I'll give you back a component you can render. Supports imports!](https://github.com/kentcdodds/mdx-bundler) - [Markdown for the component era | MDX](https://mdxjs.com/) - [remarkjs/remark: remark is a popular tool that transforms markdown with plugins. These plugins can inspect and change your markup. You can use remark on the server, the client, CLIs, deno, etc.](https://github.com/remarkjs/remark) - [rehypejs/rehype: HTML processor powered by plugins part of the @unifiedjs collective](https://github.com/rehypejs/rehype) ## Next Article Congratulations on successfully adding Syntax Highlighting to the code blocks! > The code changes for this article are as follows: > > https://github.com/eason-dev/nextjs-tailwind-contentlayer-blog-starter/compare/day13-basic-post-page-ui...day14-code-syntax-highlight In the next article, we'll continue to make the code blocks more readable by allowing you to add titles to each code block! --- # 加入程式碼 Syntax Highlighting,使用 rehype-prism-plus - Modern Next.js Blog 系列 #14 - URL: https://easonchang.com/zh-TW/posts/rehype-prism-plus-syntax-highlighting - Date: 2022-09-29T00:00:00.000Z - Language: zh-TW - Description: 使用 rehype-prism-plus 套件,讓程式碼區塊有 Syntax Highlighting 效果 > 本文同步發佈於 [it 邦幫忙 2022 iThome 鐵人賽](https://ithelp.ithome.com.tw/articles/10301243) 一個技術部落格的靈魂在於文章內的程式碼,程式碼區塊要好讀,讀者才會想看下去。 程式碼區塊最基本至少要支援「**語法凸顯 Syntax Highlighting**」,用不同顏色呈現程式內的不同關鍵字。 這篇我們就來使用 [rehype-prism-plus](https://github.com/timlrx/rehype-prism-plus),讓程式碼區塊有 Syntax Highlighting 效果! > 這篇修改的程式碼如下: > https://github.com/eason-dev/nextjs-tailwind-contentlayer-blog-starter/compare/day13-basic-post-page-ui...day14-code-syntax-highlight 結果截圖如下: ![Post with code](https://i.imgur.com/6yFw7B0.jpg) ![Post with code in dark mode](https://i.imgur.com/SyVynzH.jpg) --- ## 語法凸顯 Syntax Highlighting 要在網頁程式碼區塊加入 Syntax Highlighting,有許多套件能實現,主流的有 [PrismJS](https://prismjs.com/) 和 [highlight.js](https://highlightjs.org/) 等。 這裡我們採用 PrismJS。 ## 使用 rehype-prism-plus 加入 Syntax Highlighting 我們部落格使用的 [Contentlayer](https://www.contentlayer.dev/) 底層是使用 [mdx-bundler](https://github.com/kentcdodds/mdx-bundler) 在處理 Markdown 和 MDX 檔案的。 mdx-bundler 底層則是使用 [mdx-js](https://mdxjs.com/)。 要客製化 mdx-js 處理 Markdown 或 MDX 的邏輯,可以透過安裝 [rehype](https://github.com/rehypejs/rehype) 或 [remark](https://github.com/remarkjs/remark) plugin 來做到。就是他們負責將 Markdown 和 MDX 轉換成 HTML,安裝 plugin 就能讓 HTML 變成我們希望的樣子。 我們可以使用 [rehype-prism-plus](https://github.com/timlrx/rehype-prism-plus) 這個 rehype plugin 來實現 PrismJS 的 Syntax Highlighting。 它支援超多種程式語言(參見 [PrismJS 語言列表](https://prismjs.com/#supported-languages))和數十種主題樣式(參見 [PrismJS 主題列表](https://github.com/PrismJS/prism-themes),以及能夠 highlight 特定行數,和呈現紅色綠色 diff 檔。 ## 安裝 rehype-prism-plus 輸入指令安裝: ```shell pnpm add rehype-prism-plus ``` 修改 `contentlayer.config.ts`,在 mdx 的 rehypePlugins 加入 rehypePrism: ```ts // 加入下面這行 import rehypePrism from "rehype-prism-plus"; // ... export default makeSource({ // ... // 加入下面這行 mdx: { rehypePlugins: [[rehypePrism, { ignoreMissing: true }]] }, }); ``` 新增 `src/styles/prism-plus.css`,rehype-prism-plus 針對行數 highlight 的樣式: ```css /* https://github.com/timlrx/rehype-prism-plus#styling */ pre { overflow-x: auto; } /** * Inspired by gatsby remark prism - https://www.gatsbyjs.com/plugins/gatsby-remark-prismjs/ * 1. Make the element just wide enough to fit its content. * 2. Always fill the visible space in .code-highlight. */ .code-highlight { float: left; /* 1 */ min-width: 100%; /* 2 */ } .code-line { display: block; padding-left: 16px; padding-right: 16px; margin-left: -16px; margin-right: -16px; border-left-width: 4px; border-left-color: rgba(31, 41, 55, 0); /* Set code block color */ } .code-line.inserted { background-color: rgba(16, 185, 129, 0.2); /* Set inserted line (+) color */ } .code-line.deleted { background-color: rgba(239, 68, 68, 0.2); /* Set deleted line (-) color */ } .highlight-line { margin-left: -16px; margin-right: -16px; background-color: rgba(55, 65, 81, 0.5); /* Set highlight bg color */ border-left-width: 4px; border-left-color: rgb(59, 130, 246); /* Set highlight accent border color */ } .line-number::before { display: inline-block; width: 1rem; text-align: right; margin-right: 16px; margin-left: -8px; color: rgb(156, 163, 175); /* Line number color */ content: attr(line); } ``` 接著還需要加入 PrismJS 基本主題。 > 你可以在這裡找你喜歡的主題樣式: > https://github.com/PrismJS/prism-themes > > 我自己使用這個 Dracula 德古拉主題: > https://github.com/PrismJS/prism-themes/blob/master/themes/prism-darcula.css 新增 `src/styles/prism-dracula.css`,PrismJS 的自選主題樣式: ```css /** * Dracula Theme originally by Zeno Rocha [@zenorocha] * https://draculatheme.com/ * * Ported for PrismJS by Albert Vallverdu [@byverdu] */ code[class*="language-"], pre[class*="language-"] { color: #f8f8f2; background: none; text-shadow: 0 1px rgba(0, 0, 0, 0.3); font-family: Consolas, Monaco, "Andale Mono", "Ubuntu Mono", monospace; text-align: left; white-space: pre; word-spacing: normal; word-break: normal; word-wrap: normal; line-height: 1.5; -moz-tab-size: 4; -o-tab-size: 4; tab-size: 4; -webkit-hyphens: none; -moz-hyphens: none; -ms-hyphens: none; hyphens: none; } /* Code blocks */ pre[class*="language-"] { padding: 1em; margin: 0.5em 0; overflow: auto; border-radius: 0.3em; } :not(pre) > code[class*="language-"], pre[class*="language-"] { background: #282a36; } /* Inline code */ :not(pre) > code[class*="language-"] { padding: 0.1em; border-radius: 0.3em; white-space: normal; } .token.comment, .token.prolog, .token.doctype, .token.cdata { color: #6272a4; } .token.punctuation { color: #f8f8f2; } .namespace { opacity: 0.7; } .token.property, .token.tag, .token.constant, .token.symbol, .token.deleted { color: #ff79c6; } .token.boolean, .token.number { color: #bd93f9; } .token.selector, .token.attr-name, .token.string, .token.char, .token.builtin, .token.inserted { color: #50fa7b; } .token.operator, .token.entity, .token.url, .language-css .token.string, .style .token.string, .token.variable { color: #f8f8f2; } .token.atrule, .token.attr-value, .token.function, .token.class-name { color: #f1fa8c; } .token.keyword { color: #8be9fd; } .token.regex, .token.important { color: #ffb86c; } .token.important, .token.bold { font-weight: bold; } .token.italic { font-style: italic; } .token.entity { cursor: help; } ``` 修改 `src/pages/_app.tsx`,引入新的兩個 css 檔: ```tsx import "@/styles/prism-dracula.css"; import "@/styles/prism-plus.css"; // ... ``` 這樣就完成所有設定了! ## 新增包含程式碼的文章 新增 `/content/posts/20220901-post-with-code.mdx`: > 因為鐵人賽文章不能在程式碼區塊內再放程式碼區塊,因此這裡有多加縮排。 > > 貼去你自己部落格內時,記得把縮排刪掉 > > 或是從這邊瀏覽原始 `20220901-post-with-code.mdx` 內容: > > https://github.com/eason-dev/nextjs-tailwind-contentlayer-blog-starter/blob/0f72528c220430e21bee232dd9f81b6e175086d2/content/posts/20220901-post-with-code.mdx?plain=1 ````markdown --- title: Post with code description: My post with code slug: post-with-code date: 2022-09-01 type: Post --- ## Some post with code! Some other posts! with `some inline code`! ```js showLineNumbers const a = 1; a = 2; ``` ```tsx showLineNumbers {5,15-17} import "@/styles/globals.css"; import type { AppProps } from "next/app"; import Head from "next/head"; import { ThemeProvider } from "next-themes"; import LayoutWrapper from "@/components/LayoutWrapper"; function MyApp({ Component, pageProps }: AppProps) { return ( ); } export default MyApp; ``` ```diff-js showLineNumbers {3-4} const a = 1; - a = 2; + a = 2; a = 3; ``` ```shell $ echo "Hello world!" ``` ```python print("Hello world!") ``` ```java System.out.println("Hello world!"); ``` ```csharp Console.WriteLine("Hello world!"); ``` ```c printf("Hello world!"); ``` ```cpp std::cout << "Hello world!"; ``` ```go fmt.Println("Hello world!") ``` ```` ## 成果 完成了!使用 `pnpm dev` 並進入剛剛新增的文章,就會看到程式碼樣式變漂亮了! http://localhost:3000/posts/post-with-code 結果截圖如下: ![Post with code](https://i.imgur.com/6yFw7B0.jpg) ![Post with code in dark mode](https://i.imgur.com/SyVynzH.jpg) ## References - [timlrx/tailwind-nextjs-starter-blog: This is a Next.js, Tailwind CSS blogging starter template. Comes out of the box configured with the latest technologies to make technical writing a breeze. Easily configurable and customizable. Perfect as a replacement to existing Jekyll and Hugo individual blogs.](https://github.com/timlrx/tailwind-nextjs-starter-blog) - [timlrx/rehype-prism-plus: rehype plugin to highlight code blocks in HTML with Prism (via refractor) with line highlighting and line numbers](https://github.com/timlrx/rehype-prism-plus) - [PrismJS/prism-themes: A wider selection of Prism themes](https://github.com/PrismJS/prism-themes) - [Processing MDX Files - Contentlayer](https://www.contentlayer.dev/docs/sources/files/mdx) - [kentcdodds/mdx-bundler: ? Give me MDX/TSX strings and I'll give you back a component you can render. Supports imports!](https://github.com/kentcdodds/mdx-bundler) - [Markdown for the component era | MDX](https://mdxjs.com/) - [remarkjs/remark: remark is a popular tool that transforms markdown with plugins. These plugins can inspect and change your markup. You can use remark on the server, the client, CLIs, deno, etc.](https://github.com/remarkjs/remark) - [rehypejs/rehype: HTML processor powered by plugins part of the @unifiedjs collective](https://github.com/rehypejs/rehype) ## 下一篇 恭喜你成功讓程式碼區塊支援 Syntax Highlighting 了! > 這篇修改的程式碼如下: > > https://github.com/eason-dev/nextjs-tailwind-contentlayer-blog-starter/compare/day13-basic-post-page-ui...day14-code-syntax-highlight 下一篇我們會繼續讓程式碼區塊更加好讀,讓你能為每個程式碼區塊加上標題! --- # Styling the Article Detail Page - Modern Next.js Blog Series #13 - URL: https://easonchang.com/posts/blog-post-page-style - Date: 2022-09-28T00:00:00.000Z - Language: en - Description: Using Tailwind CSS to style the blog post detail page > This article is also published at [it 邦幫忙 2022 iThome Ironman Contest](https://ithelp.ithome.com.tw/articles/10300526) ## TL;DR This is the 13th article in the "Modern Blog 30 Days" series. In the last article, we beautified the homepage style using Tailwind CSS, and in this article, we will continue to beautify the style of the article detail pages! Screenshot results as follows: ![Post page](https://i.imgur.com/AUAUAC5.jpg) ![Post page in dark mode](https://i.imgur.com/6M1zNID.jpg) The code changes for this article are as follows: https://github.com/eason-dev/nextjs-tailwind-contentlayer-blog-starter/compare/day12-basic-index-page-ui...day13-basic-post-page-ui --- ## Installing the sass package to Support SCSS in Next.js In the forthcoming styling, we will add many deep CSS properties for article content. To make CSS more readable, we will switch to using the more convenient and readable [SCSS](https://sass-lang.com/documentation/syntax#scss) syntax. To support SCSS syntax in Next.js, we need to install the [sass](https://github.com/sass/sass) package: ```shell pnpm add -D sass ``` No configuration is needed after installation, and you can now import .scss files in Next.js. For more details, refer to the official Next.js documentation: [Basic Features: Built-in CSS Support | Next.js](https://nextjs.org/docs/basic-features/built-in-css-support#sass-support) ## Styling the Article Detail Page Let's start styling! We will add 5 new files and modify 1 file. The style mainly modifies the project based on [timlrx/tailwind-nextjs-starter-blog](https://github.com/timlrx/tailwind-nextjs-starter-blog). Add `/src/components/PageTitle.tsx`: ```tsx type Props = { children: React.ReactNode; }; export default function PageTitle({ children }: Props) { return (

{children}

); } ``` Add `/src/components/PostBody/PostBody.module.scss`: ```scss .postBody { :global(.rehype-code-title) { @apply -mb-3 rounded-tl rounded-tr bg-slate-600 px-4 pt-1 pb-2 font-mono text-sm text-gray-200; } div:global(.rehype-code-title) + pre { @apply rounded-tl-none rounded-tr-none; } img { @apply ml-auto mr-auto; } blockquote { @apply not-italic; p:first-of-type::before { content: none; } p:last-of-type::after { content: none; } } } ``` Add `/src/components/PostBody/PostBody.tsx`: ```tsx import clsx from "clsx"; import styles from "./PostBody.module.scss"; type Props = { children: React.ReactNode; }; export default function PostBody({ children }: Props) { return (
{children}
); } ``` Add `/src/components/PostBody/index.ts`: ```ts import PostBody from "./PostBody"; export default PostBody; ``` Add `/src/components/PostLayout.tsx`: ```tsx import { useRouter } from "next/router"; import CustomLink from "@/components/CustomLink"; import PageTitle from "@/components/PageTitle"; import PostBody from "@/components/PostBody"; import formatDate from "@/lib/formatDate"; export interface PostForPostLayout { date: string; title: string; } export type RelatedPostForPostLayout = { title: string; path: string; } | null; type Props = { post: PostForPostLayout; nextPost: RelatedPostForPostLayout; prevPost: RelatedPostForPostLayout; children: React.ReactNode; }; export default function PostLayout({ post, nextPost, prevPost, children, }: Props) { const { date, title } = post; const { locale } = useRouter(); return (
{title}
發佈時間
{children}
{prevPost ? (

Previous Post

← {prevPost.title}
) : (
)} {nextPost && (

Next Post

{nextPost.title} →
)}
); } ``` Modify `/src/pages/posts/[slug].tsx`: ```tsx import type { GetStaticPaths, GetStaticProps, NextPage } from "next"; import Head from "next/head"; import { useMDXComponent } from "next-contentlayer/hooks"; import PostLayout, { PostForPostLayout, RelatedPostForPostLayout, } from "@/components/PostLayout"; import { allPosts, allPostsNewToOld } from "@/lib/contentLayerAdapter"; type PostForPostPage = PostForPostLayout & { title: string; description: string; body: { code: string; }; }; type Props = { post: PostForPostPage; prevPost: RelatedPostForPostLayout; nextPost: RelatedPostForPostLayout; }; export const getStaticPaths: GetStaticPaths = () => { const paths = allPosts.map((post) => post.path); return { paths, fallback: false, }; }; export const getStaticProps: GetStaticProps = ({ params }) => { const postIndex = allPostsNewToOld.findIndex( (post) => post.slug === params?.slug ); if (postIndex === -1) { return { notFound: true, }; } const prevFull = allPostsNewToOld[postIndex + 1] || null; const prevPost: RelatedPostForPostLayout = prevFull ? { title: prevFull.title, path: prevFull.path } : null; const nextFull = allPostsNewToOld[postIndex - 1] || null; const nextPost: RelatedPostForPostLayout = nextFull ? { title: nextFull.title, path: nextFull.path } : null; const postFull = allPostsNewToOld[postIndex]; const post: PostForPostPage = { title: postFull.title, date: postFull.date, description: postFull.description, body: { code: postFull.body.code, }, }; if (!post) { return { notFound: true, }; } return { props: { post, prevPost, nextPost, }, }; }; const PostPage: NextPage = ({ post, prevPost, nextPost }) => { const { description, title, body: { code }, } = post; const MDXContent = useMDXComponent(code); return ( <> {title} ); }; export default PostPage; ``` ## Results Done! Use `pnpm dev` and visit any article detail page to see the beautified styles! http://localhost:3000/posts/markdown-demo Screenshot results as follows: ![Post page](https://i.imgur.com/AUAUAC5.jpg) ![Post page in dark mode](https://i.imgur.com/6M1zNID.jpg) The code changes for this article are as follows: https://github.com/eason-dev/nextjs-tailwind-contentlayer-blog-starter/compare/day12-basic-index-page-ui...day13-basic-post-page-ui ## References - [timlrx/tailwind-nextjs-starter-blog: This is a Next.js, Tailwind CSS blogging starter template. Comes out of the box configured with the latest technologies to make technical writing a breeze. Easily configurable and customizable. Perfect as a replacement to existing Jekyll and Hugo individual blogs.](https://github.com/timlrx/tailwind-nextjs-starter-blog) - [Basic Features: Built-in CSS Support | Next.js](https://nextjs.org/docs/basic-features/built-in-css-support#sass-support) ## Next Article Congratulations! We've successfully used Tailwind CSS to style the article detail page in Next.js! However, if you insert code blocks in your article, you'll find that the code does not have Syntax Highlighting, making it very hard to read. In the next article, we will use rehype-prism-plus to add Syntax Highlighting to the articles! --- # 文章內頁樣式切版 - Modern Next.js Blog 系列 #13 - URL: https://easonchang.com/zh-TW/posts/blog-post-page-style - Date: 2022-09-28T00:00:00.000Z - Language: zh-TW - Description: 使用 Tailwind CSS 切版 Blog 內文樣式 > 本文同步發佈於 [it 邦幫忙 2022 iThome 鐵人賽](https://ithelp.ithome.com.tw/articles/10300526) ## TL;DR 這是「Modern Blog 30 天」系列第 13 篇文章,上一篇我們使用 Tailwind CSS 美化了首頁樣式,這篇我們會繼續美化文章內頁樣式! 結果截圖如下: ![Post page](https://i.imgur.com/AUAUAC5.jpg) ![Post page in dark mode](https://i.imgur.com/6M1zNID.jpg) 這篇修改的程式碼如下: https://github.com/eason-dev/nextjs-tailwind-contentlayer-blog-starter/compare/day12-basic-index-page-ui...day13-basic-post-page-ui --- ## 安裝 sass 套件,在 Next.js 支援 SCSS 在稍後的切版,我們會針對文章內文加許多深層 CSS 屬性。 為了讓 CSS 更好讀,我們會改用更方便易讀的 [SCSS](https://sass-lang.com/documentation/syntax#scss) 語法。 而要在 Next.js 支援 SCSS 語法,我們需要安裝 [sass](https://github.com/sass/sass) 套件: ```shell pnpm add -D sass ``` 安裝完不需要任何設定,就能在 Next.js 裡 import .scss 檔案了。 更多細節可參考 Next.js 官方文件:[Basic Features: Built-in CSS Support | Next.js](https://nextjs.org/docs/basic-features/built-in-css-support#sass-support) ## 文章內頁樣式切版 讓我們開始切版吧!會新增 5 個檔案、和修改 1 個檔案。 這邊樣式主要是基於 [timlrx/tailwind-nextjs-starter-blog](https://github.com/timlrx/tailwind-nextjs-starter-blog) 專案修改而成的。 新增 `/src/components/PageTitle.tsx`: ```tsx type Props = { children: React.ReactNode; }; export default function PageTitle({ children }: Props) { return (

{children}

); } ``` 新增 `/src/components/PostBody/PostBody.module.scss`: ```scss .postBody { :global(.rehype-code-title) { @apply -mb-3 rounded-tl rounded-tr bg-slate-600 px-4 pt-1 pb-2 font-mono text-sm text-gray-200; } div:global(.rehype-code-title) + pre { @apply rounded-tl-none rounded-tr-none; } img { @apply ml-auto mr-auto; } blockquote { @apply not-italic; p:first-of-type::before { content: none; } p:last-of-type::after { content: none; } } } ``` 新增 `/src/components/PostBody/PostBody.tsx`: ```tsx import clsx from "clsx"; import styles from "./PostBody.module.scss"; type Props = { children: React.ReactNode; }; export default function PostBody({ children }: Props) { return (
{children}
); } ``` 新增 `/src/components/PostBody/index.ts`: ```ts import PostBody from "./PostBody"; export default PostBody; ``` 新增 `/src/components/PostLayout.tsx`: ```tsx import { useRouter } from "next/router"; import CustomLink from "@/components/CustomLink"; import PageTitle from "@/components/PageTitle"; import PostBody from "@/components/PostBody"; import formatDate from "@/lib/formatDate"; export interface PostForPostLayout { date: string; title: string; } export type RelatedPostForPostLayout = { title: string; path: string; } | null; type Props = { post: PostForPostLayout; nextPost: RelatedPostForPostLayout; prevPost: RelatedPostForPostLayout; children: React.ReactNode; }; export default function PostLayout({ post, nextPost, prevPost, children, }: Props) { const { date, title } = post; const { locale } = useRouter(); return (
{title}
發佈時間
{children}
{prevPost ? (

上一篇

← {prevPost.title}
) : (
)} {nextPost && (

下一篇

{nextPost.title} →
)}
); } ``` 修改 `/src/pages/posts/[slug].tsx`: ```tsx import type { GetStaticPaths, GetStaticProps, NextPage } from "next"; import Head from "next/head"; import { useMDXComponent } from "next-contentlayer/hooks"; import PostLayout, { PostForPostLayout, RelatedPostForPostLayout, } from "@/components/PostLayout"; import { allPosts, allPostsNewToOld } from "@/lib/contentLayerAdapter"; type PostForPostPage = PostForPostLayout & { title: string; description: string; body: { code: string; }; }; type Props = { post: PostForPostPage; prevPost: RelatedPostForPostLayout; nextPost: RelatedPostForPostLayout; }; export const getStaticPaths: GetStaticPaths = () => { const paths = allPosts.map((post) => post.path); return { paths, fallback: false, }; }; export const getStaticProps: GetStaticProps = ({ params }) => { const postIndex = allPostsNewToOld.findIndex( (post) => post.slug === params?.slug ); if (postIndex === -1) { return { notFound: true, }; } const prevFull = allPostsNewToOld[postIndex + 1] || null; const prevPost: RelatedPostForPostLayout = prevFull ? { title: prevFull.title, path: prevFull.path } : null; const nextFull = allPostsNewToOld[postIndex - 1] || null; const nextPost: RelatedPostForPostLayout = nextFull ? { title: nextFull.title, path: nextFull.path } : null; const postFull = allPostsNewToOld[postIndex]; const post: PostForPostPage = { title: postFull.title, date: postFull.date, description: postFull.description, body: { code: postFull.body.code, }, }; if (!post) { return { notFound: true, }; } return { props: { post, prevPost, nextPost, }, }; }; const PostPage: NextPage = ({ post, prevPost, nextPost }) => { const { description, title, body: { code }, } = post; const MDXContent = useMDXComponent(code); return ( <> {title} ); }; export default PostPage; ``` ## 成果 完成了!使用 `pnpm dev` 並進入任何文章內頁,就會看到樣式變漂亮了! http://localhost:3000/posts/markdown-demo 結果截圖如下: ![Post page](https://i.imgur.com/AUAUAC5.jpg) ![Post page in dark mode](https://i.imgur.com/6M1zNID.jpg) 這篇修改的程式碼如下: https://github.com/eason-dev/nextjs-tailwind-contentlayer-blog-starter/compare/day12-basic-index-page-ui...day13-basic-post-page-ui ## References - [timlrx/tailwind-nextjs-starter-blog: This is a Next.js, Tailwind CSS blogging starter template. Comes out of the box configured with the latest technologies to make technical writing a breeze. Easily configurable and customizable. Perfect as a replacement to existing Jekyll and Hugo individual blogs.](https://github.com/timlrx/tailwind-nextjs-starter-blog) - [Basic Features: Built-in CSS Support | Next.js](https://nextjs.org/docs/basic-features/built-in-css-support#sass-support) ## 下一篇 恭喜你!我們成功在 Next.js 裡使用 Tailwind CSS 完成文章內頁樣式切版! 但如果你在文章內插入程式碼,你會發現程式碼並沒有 Syntax Highlighting,非常不好讀。 下一篇我們會使用 rehype-prism-plus,在文章內加入 Syntax Highlighting! --- # Styling the Blog Homepage - Modern Next.js Blog Series #12 - URL: https://easonchang.com/posts/blog-homepage-style - Date: 2022-09-27T00:00:00.000Z - Language: en - Description: Continuing style the blog homepage using Tailwind CSS This is the twelfth article in the "Modern Blog 30 Days" series. In the previous article, we beautified the navbar, footer, and other global styles using Tailwind CSS. This article will continue to beautify the homepage style! Screenshot results are as follows: ![Home page](https://i.imgur.com/gEMLqww.jpg) ![Home page in dark mode](https://i.imgur.com/CRmwFht.jpg) The code changes for this article are as follows: https://github.com/eason-dev/nextjs-tailwind-contentlayer-blog-starter/compare/day11-basic-site-ui...day12-basic-index-page-ui --- ## Homepage Style The styles here are mainly modified based on the [timlrx/tailwind-nextjs-starter-blog](https://github.com/timlrx/tailwind-nextjs-starter-blog) project. Add `/src/lib/formatDate.ts`: ```ts const formatDate = (date: string, locale = "zh-TW") => { const now = new Date(date).toLocaleDateString(locale, { year: "numeric", month: "long", day: "numeric", }); return now; }; export default formatDate; ``` Add `/src/components/PostList.tsx`: ```tsx import { useRouter } from "next/router"; import CustomLink from "@/components/CustomLink"; import formatDate from "@/lib/formatDate"; export interface PostForPostList { slug: string; date: string; title: string; description: string; path: string; } type Props = { posts: PostForPostList[]; }; export default function PostList({ posts = [] }: Props) { const { locale } = useRouter(); return (
    {!posts.length && "No posts found."} {posts.map((post) => { const { slug, date, title, description, path } = post; return (
  • Published on

    {title}

    {description}
  • ); })}
); } ``` Modify `/src/pages/index.tsx`: ```tsx import type { NextPage } from "next"; import { GetStaticProps } from "next"; import Head from "next/head"; import PostList, { PostForPostList } from "@/components/PostList"; import { allPostsNewToOld } from "@/lib/contentLayerAdapter"; type PostForIndexPage = PostForPostList; type Props = { posts: PostForIndexPage[]; }; export const getStaticProps: GetStaticProps = () => { const posts = allPostsNewToOld.map((post) => ({ slug: post.slug, date: post.date, title: post.title, description: post.description, path: post.path, })) as PostForIndexPage[]; return { props: { posts } }; }; const Home: NextPage = ({ posts }) => { return (
Next.js Tailwind Contentlayer Blog Starter

Hey, I am Iron Man ?

I am Tony Stark, not Stank!

I 'm very rich, and I've saved the world many times.

I hate aliens, the color purple, and purple aliens.

Latest Articles

); }; export default Home; ``` ## Results Done! Use `pnpm dev` and visit the homepage to see the beautified homepage style! Screenshot results are as follows: ![Home page](https://i.imgur.com/gEMLqww.jpg) ![Home page in dark mode](https://i.imgur.com/CRmwFht.jpg) The code changes for this article are as follows: https://github.com/eason-dev/nextjs-tailwind-contentlayer-blog-starter/compare/day11-basic-site-ui...day12-basic-index-page-ui ## References - [timlrx/tailwind-nextjs-starter-blog: This is a Next.js, Tailwind CSS blogging starter template. Comes out of the box configured with the latest technologies to make technical writing a breeze. Easily configurable and customizable. Perfect as a replacement to existing Jekyll and Hugo individual blogs.](https://github.com/timlrx/tailwind-nextjs-starter-blog) ## Next Article Congratulations! We've successfully used Tailwind CSS to style the homepage in Next.js! In the next article, we will continue to style the article detail page! --- # 首頁樣式切版 - Modern Next.js Blog 系列 #12 - URL: https://easonchang.com/zh-TW/posts/blog-homepage-style - Date: 2022-09-27T00:00:00.000Z - Language: zh-TW - Description: 繼續使用 Tailwind CSS 切版 Blog 首頁樣式 > 本文同步發佈於 [it 邦幫忙 2022 iThome 鐵人賽](https://ithelp.ithome.com.tw/articles/10299241) ## TL;DR 這是「Modern Blog 30 天」系列第 12 篇文章,上一篇我們使用 Tailwind CSS 美化了 navbar, footer 等全站樣式,這篇我們會繼續美化首頁樣式! 結果截圖如下: ![Home page](https://i.imgur.com/gEMLqww.jpg) ![Home page in dark mode](https://i.imgur.com/CRmwFht.jpg) 這篇修改的程式碼如下: https://github.com/eason-dev/nextjs-tailwind-contentlayer-blog-starter/compare/day11-basic-site-ui...day12-basic-index-page-ui --- ## 首頁樣式切版 這邊樣式主要是基於 [timlrx/tailwind-nextjs-starter-blog](https://github.com/timlrx/tailwind-nextjs-starter-blog) 專案來修改的。 新增 `/src/lib/formatDate.ts`: ```ts const formatDate = (date: string, locale = "zh-TW") => { const now = new Date(date).toLocaleDateString(locale, { year: "numeric", month: "long", day: "numeric", }); return now; }; export default formatDate; ``` 新增 `/src/components/PostList.tsx`: ```tsx import { useRouter } from "next/router"; import CustomLink from "@/components/CustomLink"; import formatDate from "@/lib/formatDate"; export interface PostForPostList { slug: string; date: string; title: string; description: string; path: string; } type Props = { posts: PostForPostList[]; }; export default function PostList({ posts = [] }: Props) { const { locale } = useRouter(); return (
    {!posts.length && "No posts found."} {posts.map((post) => { const { slug, date, title, description, path } = post; return (
  • Published on

    {title}

    {description}
  • ); })}
); } ``` 修改 `/src/pages/index.tsx`: ```tsx import type { NextPage } from "next"; import { GetStaticProps } from "next"; import Head from "next/head"; import PostList, { PostForPostList } from "@/components/PostList"; import { allPostsNewToOld } from "@/lib/contentLayerAdapter"; type PostForIndexPage = PostForPostList; type Props = { posts: PostForIndexPage[]; }; export const getStaticProps: GetStaticProps = () => { const posts = allPostsNewToOld.map((post) => ({ slug: post.slug, date: post.date, title: post.title, description: post.description, path: post.path, })) as PostForIndexPage[]; return { props: { posts } }; }; const Home: NextPage = ({ posts }) => { return (
Next.js Tailwind Contentlayer Blog Starter

Hey,I am Iron Man ?

我是 Tony Stark,不是 Stank!

老子很有錢,拯救過很多次世界。

我討厭外星人、紫色的東西、和紫色外星人。

最新文章

); }; export default Home; ``` ## 成果 完成了!使用 `pnpm dev` 並進入首頁,就會看到首頁樣式變漂亮了! 結果截圖如下: ![Home page](https://i.imgur.com/gEMLqww.jpg) ![Home page in dark mode](https://i.imgur.com/CRmwFht.jpg) 這篇修改的程式碼如下: https://github.com/eason-dev/nextjs-tailwind-contentlayer-blog-starter/compare/day11-basic-site-ui...day12-basic-index-page-ui ## References - [timlrx/tailwind-nextjs-starter-blog: This is a Next.js, Tailwind CSS blogging starter template. Comes out of the box configured with the latest technologies to make technical writing a breeze. Easily configurable and customizable. Perfect as a replacement to existing Jekyll and Hugo individual blogs.](https://github.com/timlrx/tailwind-nextjs-starter-blog) ## 下一篇 恭喜你!我們成功在 Next.js 裡使用 Tailwind CSS 完成首頁樣式切版! 下一篇我們會繼續切版文章內頁的樣式! --- # Full Site Styling - Modern Next.js Blog Series #11 - URL: https://easonchang.com/posts/blog-site-style - Date: 2022-09-26T00:00:00.000Z - Language: en - Description: Start styling the Modern Blog site using Tailwind CSS, including Header, Footer, and more. > This article is also published at [it 邦幫忙 2022 iThome Ironman Contest](https://ithelp.ithome.com.tw/articles/10299066) ## TL;DR This is the eleventh article in the "Modern Blog 30 Days" series. In the previous article, we integrated "Dark Mode" with Tailwind CSS into our Next.js blog. This article will begin styling the entire site using Tailwind CSS, beautifying elements including the Header and Footer! Screenshot results are as follows: ![Home page](https://i.imgur.com/gNT9ZDg.jpg) ![Home page in dark mode](https://i.imgur.com/Gf0hn2v.jpg) ![Post page](https://i.imgur.com/KPgE8x0.jpg) The code changes for this article are as follows: https://github.com/eason-dev/nextjs-tailwind-contentlayer-blog-starter/compare/day10-darkmode...day11-basic-site-ui --- ## Install clsx for Conveniently Combining className Based on Conditions We primarily use various ready-made Tailwind CSS classNames for styling. In many components, we need to provide different classNames combinations based on different conditions (props or state, etc.). Here, we use the [clsx](https://github.com/lukeed/clsx) package to help us write simple, readable, and powerful className combination logic. Use pnpm to install it: ```shell pnpm add clsx ``` ## @tailwindcss/typography for Out-of-the-box Text Styles One advantage of Tailwind CSS is its ease of customization and quick modification. However, when initially setting up good-looking styles for basic titles, text, lists, etc., you need to add a large number of classNames, explicitly specifying font-size, font-weight, letter-spacing, etc., which can be very labor-intensive. Fortunately, Tailwind CSS officially offers several [Plugins](https://tailwindcss.com/docs/plugins), including [@tailwindcss/typography](https://tailwindcss.com/docs/typography-plugin). After installation, it provides `prose` series of classNames, allowing a single className to specify a series of good-looking text styles. ### Install @tailwindcss/typography Use the following command to install the package: ```shell pnpm add -D @tailwindcss/typography ``` And modify `/tailwind.config.js` to add it to the `plugins` array: ```js /** @type {import('tailwindcss').Config} */ module.exports = { content: ["./src/**/*.{js,ts,jsx,tsx}"], darkMode: "class", theme: { extend: {}, }, // Modify the following line plugins: [require("@tailwindcss/typography")], }; ``` This completes the installation. We will actually use it later in the article. ## @svgr/webpack to Support SVG Import in Next.js We will use SVG files in the styling process, but to directly import SVG files in Next.js's JSX/TSX for customization, some settings are needed. We will follow this article to implement it: [Importing SVGs to Next.js - DEV Community](https://dev.to/dolearning/importing-svgs-to-next-js-nna) ### Install @svgr/webpack Use the command to install [@svgr/webpack](https://www.npmjs.com/package/@svgr/webpack): ```shell pnpm add -D @svgr/webpack ``` ### Modify `/next.config.mjs` Modify `/next.config.mjs` by adding custom webpack settings, using @svgr/webpack to support importing SVG files in JS: ```js import { withContentlayer } from "next-contentlayer"; /** @type {import('next').NextConfig} */ const nextConfig = withContentlayer({ reactStrictMode: true, swcMinify: true, // Add the following custom webpack settings // Support svg import // ref: https://dev.to/dolearning/importing-svgs-to-next-js-nna webpack: (config) => { config.module.rules.push({ test: /\.svg$/, use: ["@svgr/webpack"], }); return config; }, // Add the above custom webpack settings eslint: { // Warning: This allows production builds to successfully complete even if // your project has ESLint errors. ignoreDuringBuilds: true, }, typescript: { // Dangerously allow production builds to successfully complete even if // your project has type errors. ignoreBuildErrors: true, }, }); export default nextConfig; ``` ## Site-wide Style Here we will add many components and make numerous changes. Due to space constraints, they are not all listed here. You can see these changes more clearly from this git commit: https://github.com/eason-dev/nextjs-tailwind-contentlayer-blog-starter/commit/61d2999b326d1dcefcc3ec b64108946ca78062b5 Our blog's style mainly modifies the [timlrx/tailwind-nextjs-starter-blog](https://github.com/timlrx/tailwind-nextjs-starter-blog) project, adjusting colors, component structure, and supporting TypeScript beyond the original project. ## Results Done! Use `pnpm dev` and visit the homepage and article pages to see the beautified site-wide style! Added navbar and footer, the layout has also been beautified, and it supports mobile RWD and dark mode. Screenshot results are as follows: ![Home page](https://i.imgur.com/gNT9ZDg.jpg) ![Home page in dark mode](https://i.imgur.com/Gf0hn2v.jpg) ![Post page](https://i.imgur.com/KPgE8x0.jpg) The code changes for this article are as follows: https://github.com/eason-dev/nextjs-tailwind-contentlayer-blog-starter/compare/day10-darkmode...day11-basic-site-ui ## References - [Importing SVGs to Next.js - DEV Community](https://dev.to/dolearning/importing-svgs-to-next-js-nna) - [timlrx/tailwind-nextjs-starter-blog: This is a Next.js, Tailwind CSS blogging starter template. Comes out of the box configured with the latest technologies to make technical writing a breeze. Easily configurable and customizable. Perfect as a replacement to existing Jekyll and Hugo individual blogs.](https://github.com/timlrx/tailwind-nextjs-starter-blog) - [lukeed/clsx: A tiny (228B) utility for constructing `className` strings conditionally.](https://github.com/lukeed/clsx) - [Tailwind CSS - Rapidly build modern websites without ever leaving your HTML.](https://tailwindcss.com/) ## Next Article Congratulations! We've successfully used Tailwind CSS to style the entire site in Next.js! In the next article, we will continue to style the homepage body! --- # 全站樣式切版 - Modern Next.js Blog 系列 #11 - URL: https://easonchang.com/zh-TW/posts/blog-site-style - Date: 2022-09-26T00:00:00.000Z - Language: zh-TW - Description: 使用 Tailwind CSS 開始 Modern Blog 全站樣式切版,包含 Header、Footer 等 > 本文同步發佈於 [it 邦幫忙 2022 iThome 鐵人賽](https://ithelp.ithome.com.tw/articles/10299066) ## TL;DR 這是「Modern Blog 30 天」系列第 11 篇文章,上一篇我們在 Next.js blog 搭配 Tailwind CSS 加入了「暗黑模式 Dark mode」。這篇讓我們使用 Tailwind CSS 開始全站切版,美化樣式! 結果截圖如下: ![Home page](https://i.imgur.com/gNT9ZDg.jpg) ![Home page in dark mode](https://i.imgur.com/Gf0hn2v.jpg) ![Post page](https://i.imgur.com/KPgE8x0.jpg) 這篇修改的程式碼如下: https://github.com/eason-dev/nextjs-tailwind-contentlayer-blog-starter/compare/day10-darkmode...day11-basic-site-ui --- ## 安裝 clsx,方便根據不同條件組合 className 我們主要使用各種現成 [Tailwind CSS](https://tailwindcss.com/) 的 className 來切版。 在許多元件裡,我們都會需要根據不同條件(prop 或 state 等),給定不同 className 組合。 這裡我們使用 [clsx](https://github.com/lukeed/clsx) 這個套件幫助我們寫出簡單易讀又強大的 className 組合邏輯。 使用 pnpm 來安裝它: ```shell pnpm add clsx ``` ## @tailwindcss/typography,開箱即用的文字樣式 Tailwind CSS 優點是方便客製化、修改迅速。但在一開始要針對基本的標題、內文、列表設定好看的樣式時,需要加入大量 className,font-size、font-weight、letter-spacing 等等都要明確指定,非常費工。 好在 Tailwind CSS 官方提供了數個 [Plugins](https://tailwindcss.com/docs/plugins),其中包含了 [@tailwindcss/typography](https://tailwindcss.com/docs/typography-plugin),安裝之後會提供 `prose` 系列的 className,一個 className 就能指定完一系列好看的文字樣式。 ### 安裝 @tailwindcss/typography 輸入以下指令安裝套件: ```shell pnpm add -D @tailwindcss/typography ``` 並修改 `/tailwind.config.js`,將它新增到 `plugins` 陣列中: ```js /** @type {import('tailwindcss').Config} */ module.exports = { content: ["./src/**/*.{js,ts,jsx,tsx}"], darkMode: "class", theme: { extend: {}, }, // 修改下面這行 plugins: [require("@tailwindcss/typography")], }; ``` 這樣就完成安裝了,文章稍後我們會實際使用到它。 ## @svgr/webpack,讓 Next.js 支援 svg 圖檔 import 切版過程我們會用到 svg 圖檔,但要能在 Next.js 的 JSX/TSX 裡直接 import svg 圖檔,方便對圖檔做客製化的話,需要做些設定才能實現。 這裡我們參照這邊文章來實現: [Importing SVGs to Next.js - DEV Community ?‍??‍?](https://dev.to/dolearning/importing-svgs-to-next-js-nna) ### 安裝 @svgr/webpack 輸入指令來安裝 [@svgr/webpack](https://www.npmjs.com/package/@svgr/webpack): ```shell pnpm add -D @svgr/webpack ``` ### 修改 `/next.config.mjs` 修改 `/next.config.mjs`,加入客製 webpack 設定,使用 @svgr/webpack 來支援在 JS 裡 import svg 圖檔: ```js import { withContentlayer } from "next-contentlayer"; /** @type {import('next').NextConfig} */ const nextConfig = withContentlayer({ reactStrictMode: true, swcMinify: true, // 加入以下 custom webpack 設定 // Support svg import // ref: https://dev.to/dolearning/importing-svgs-to-next-js-nna webpack: (config) => { config.module.rules.push({ test: /\.svg$/, use: ["@svgr/webpack"], }); return config; }, // 加入以上 custom webpack 設定 eslint: { // Warning: This allows production builds to successfully complete even if // your project has ESLint errors. ignoreDuringBuilds: true, }, typescript: { // Dangerously allow production builds to successfully complete even if // your project has type errors. ignoreBuildErrors: true, }, }); export default nextConfig; ``` ## 全站樣式切版 這裡我們會新增許多 component 和做不少修改,因為篇幅關係就不全部貼上來了,這些改動從這個 git commit 看會比較清楚: https://github.com/eason-dev/nextjs-tailwind-contentlayer-blog-starter/commit/61d2999b326d1dcefcc3ecb64108946ca78062b5 我們這個部落格的樣式主要是參照 [timlrx/tailwind-nextjs-starter-blog](https://github.com/timlrx/tailwind-nextjs-starter-blog) 專案下去修改的,比起原專案多調整了一些配色、component 結構,和支援 TypeScript。 ## 成果 完成了!使用 `pnpm dev` 並進入首頁和文章內頁,就會看到全站樣式變漂亮了!多出 navbar 和 footer,排版也變漂亮了,並且也支援手機版 RWD 和暗黑模式。 結果截圖如下: ![Home page](https://i.imgur.com/gNT9ZDg.jpg) ![Home page in dark mode](https://i.imgur.com/Gf0hn2v.jpg) ![Post page](https://i.imgur.com/KPgE8x0.jpg) 這篇修改的程式碼如下: https://github.com/eason-dev/nextjs-tailwind-contentlayer-blog-starter/compare/day10-darkmode...day11-basic-site-ui ## References - [Importing SVGs to Next.js - DEV Community](https://dev.to/dolearning/importing-svgs-to-next-js-nna) - [timlrx/tailwind-nextjs-starter-blog: This is a Next.js, Tailwind CSS blogging starter template. Comes out of the box configured with the latest technologies to make technical writing a breeze. Easily configurable and customizable. Perfect as a replacement to existing Jekyll and Hugo individual blogs.](https://github.com/timlrx/tailwind-nextjs-starter-blog) - [lukeed/clsx: A tiny (228B) utility for constructing \`className\` strings conditionally.](https://github.com/lukeed/clsx) - [Tailwind CSS - Rapidly build modern websites without ever leaving your HTML.](https://tailwindcss.com/) ## 下一篇 恭喜你!我們成功在 Next.js 裡使用 Tailwind CSS 完成全站樣式切版! 下一篇我們會繼續切版首頁本體的樣式! --- # Dark Mode Support with Tailwind CSS and next-themes - Modern Next.js Blog Series #10 - URL: https://easonchang.com/posts/darkmode-tailwindcss - Date: 2022-09-25T00:00:00.000Z - Language: en - Description: Supporting dark mode with Tailwind CSS and using the next-themes package to detect reader's preferred mode and implement a toggle button > This article is also published on [iT 邦幫忙 2022 iThome Ironman](https://ithelp.ithome.com.tw/articles/10298268) In this article, we will enable dark mode (also known as dark theme) for Tailwind CSS installed in the previous article. We will use the [next-themes](https://github.com/pacocoursey/next-themes) package to manage theme switching and detect the preferred theme, and we will implement a toggle button. The results are shown in the screenshots below: ![Light mode and toggle](https://i.imgur.com/TFd5fiq.jpg) ![Dark mode](https://i.imgur.com/m0XkqUO.jpg) The code changes for this article are as follows: https://github.com/eason-dev/nextjs-tailwind-contentlayer-blog-starter/compare/day09-install-tailwindcss...day10-darkmode --- ## Installing next-themes Here we use the [next-themes](https://github.com/pacocoursey/next-themes) package to help us manage light and dark mode toggling in a Next.js project, as well as detecting browser settings and providing a default mode. ```shell pnpm add next-themes ``` Next, modify `/src/pages/_app.tsx` by wrapping the entire App with `` provided by next-themes: ```tsx import "@/styles/globals.css"; import type { AppProps } from "next/app"; import { ThemeProvider } from "next-themes"; function MyApp({ Component, pageProps }: AppProps) { return ( ); } export default MyApp; ``` Tweak `/tailwind.config.js` to use class for darkMode determination: ```js /** @type {import('tailwindcss').Config} */ module.exports = { content: ["./src/**/*.{js,ts,jsx,tsx}"], // Add darkMode darkMode: "class", theme: { extend: {}, }, plugins: [], }; ``` > Reference: [Tailwind CSS Official Dark Mode Documentation](https://tailwindcss.com/docs/dark-mode) This completes the installation and configuration! Next, we will add a theme selector. ## Adding a Theme Selector Component Create a new `/src/components/ThemeSwitch.tsx`, mainly using `useTheme()` provided by **next-themes** to get and switch the current theme: ```tsx import { useTheme } from "next-themes"; import { useEffect, useState } from "react"; const ThemeSwitch = () => { const [mounted, setMounted] = useState(false); const { theme, setTheme } = useTheme(); // useEffect only runs on the client, so now we can safely show the UI useEffect(() => { setMounted(true); }, []); if (!mounted) { return null; } return ( ); }; export default ThemeSwitch; ``` ## Placing `` on the Homepage and Setting Dark Mode Styles Next, let's place `` on the homepage. And specify the styles of various UI elements on the homepage in dark mode. In Tailwind CSS, use the `dark:` prefix to explicitly specify. Modified `/src/pages/index.tsx` is as follows: ```tsx import type { NextPage } from "next"; import Head from "next/head"; import ThemeSwitch from "@/components/ThemeSwitch"; import { allPostsNewToOld, Post } from "@/lib/contentLayerAdapter"; export function getStaticProps() { const posts = allPostsNewToOld; return { props: { posts } }; } type Props = { posts: Post[]; }; const Home: NextPage = ({ posts }) => { return (
My blog

Welcome to my blog!

{posts.map((post) => ( ))}
); }; export default Home; ``` ## Results Done! Run `pnpm dev` and enter the homepage, and you will see the homepage now has a selector for light and dark modes. When dark mode is selected, the background becomes black, and the text and lines turn white. Screenshots as follows: ![Light mode and toggle](https://i.imgur.com/TFd5fiq.jpg) ![Dark mode](https://i.imgur.com/m0XkqUO.jpg) The code changes for this article are as follows: https://github.com/eason-dev/nextjs-tailwind-contentlayer-blog-starter/compare/day09-install-tailwindcss...day10-darkmode ## References - https://github.com/pacocoursey/next-themes - https://tailwindcss.com/docs/dark-mode ## Next Article Congratulations! We've successfully integrated dark mode in Next.js using Tailwind CSS and next-themes. In the next article, we will officially start beautifying the homepage style! --- # Dark Mode 深色模式支援,使用 Tailwind CSS、next-themes - Modern Next.js Blog 系列 #10 - URL: https://easonchang.com/zh-TW/posts/darkmode-tailwindcss - Date: 2022-09-25T00:00:00.000Z - Language: zh-TW - Description: 在 Tailwind CSS 支援深色模式,並使用 next-themes 套件來偵測讀者偏好模式,並實作切換按鈕 > 本文同步發佈於 [it 邦幫忙 2022 iThome 鐵人賽](https://ithelp.ithome.com.tw/articles/10298268) 這篇我們來讓上一篇安裝完的 [Tailwind CSS](https://tailwindcss.com/) 支援暗黑模式(深色模式、Dark mode)。 並搭配 [next-themes](https://github.com/pacocoursey/next-themes) 管理主題切換以及偏好主題的偵測,並實作切換按鈕。 結果截圖如下: ![Light mode and toggle](https://i.imgur.com/TFd5fiq.jpg) ![Dark mode](https://i.imgur.com/m0XkqUO.jpg) 這篇修改的程式碼如下: https://github.com/eason-dev/nextjs-tailwind-contentlayer-blog-starter/compare/day09-install-tailwindcss...day10-darkmode --- ## 安裝 next-themes 這裡我們使用 [next-themes](https://github.com/pacocoursey/next-themes) 套件,幫我們在 Next.js 專案裡管理明亮模式、深色模式切換,以及偵測瀏覽器設定並給出預設的模式。 ```shell pnpm add next-themes ``` 接著修改 `/src/pages/_app.tsx`,用 next-themes 提供的 `` 包住整個 App: ```tsx import "@/styles/globals.css"; import type { AppProps } from "next/app"; import { ThemeProvider } from "next-themes"; function MyApp({ Component, pageProps }: AppProps) { return ( ); } export default MyApp; ``` 微調 `/tailwind.config.js`,設定 darkMode 使用 class 判定: ```js /** @type {import('tailwindcss').Config} */ module.exports = { content: ["./src/**/*.{js,ts,jsx,tsx}"], // 加入 darkMode darkMode: "class", theme: { extend: {}, }, plugins: [], }; ``` > 參考:[Tailwind CSS 官方 Dark Mode 說明](https://tailwindcss.com/docs/dark-mode) 這樣就完成安裝和設定了!接著要來加入能讓我們選主題的選擇器。 ## 新增主題選擇器 component 新增 `/src/components/ThemeSwitch.tsx`,裡面主要使用了 **next-themes** 提供的 `useTheme()` 來取得當下主題、和切換主題: ```tsx import { useTheme } from "next-themes"; import { useEffect, useState } from "react"; const ThemeSwitch = () => { const [mounted, setMounted] = useState(false); const { theme, setTheme } = useTheme(); // useEffect only runs on the client, so now we can safely show the UI useEffect(() => { setMounted(true); }, []); if (!mounted) { return null; } return ( ); }; export default ThemeSwitch; ``` ## 在首頁放置 ``,並設定些深色模式樣式 再來我們來把 `` 放進首頁裡。 以及指定首頁各個 UI 在深色模式下的樣式,在 Tailwind CSS 裡面,都要使用 `dark:` 前綴來明確指定。 修改玩的 `/src/pages/index.tsx` 如下: ```tsx import type { NextPage } from "next"; import Head from "next/head"; import ThemeSwitch from "@/components/ThemeSwitch"; import { allPostsNewToOld, Post } from "@/lib/contentLayerAdapter"; export function getStaticProps() { const posts = allPostsNewToOld; return { props: { posts } }; } type Props = { posts: Post[]; }; const Home: NextPage = ({ posts }) => { return (
My blog

Welcome to my blog!

{posts.map((post) => ( ))}
); }; export default Home; ``` ## 成果 完成了!使用 `pnpm dev` 並進入首頁,就會看到首頁多了明亮、暗黑模式的選擇器,選擇暗黑模式時也會看到底色變黑、文字和線條變白色。 截圖如下: ![Light mode and toggle](https://i.imgur.com/TFd5fiq.jpg) ![Dark mode](https://i.imgur.com/m0XkqUO.jpg) 這篇修改的程式碼如下: https://github.com/eason-dev/nextjs-tailwind-contentlayer-blog-starter/compare/day09-install-tailwindcss...day10-darkmode ## References - https://github.com/pacocoursey/next-themes - https://tailwindcss.com/docs/dark-mode ## 下一篇 恭喜你!我們成功在 Next.js 裡搭配使用 Tailwind CSS 和 next-themes 加入了暗黑模式。 下一篇我們會正式開始美化首頁樣式! --- # Installing Tailwind CSS and Related ESLint and Prettier Settings - Modern Next.js Blog Series #09 - URL: https://easonchang.com/posts/tailwindcss-setup - Date: 2022-09-24T00:00:00.000Z - Language: en - Description: Installing Tailwind CSS, a CSS utility tool, and setting up the corresponding ESLint and Prettier rules - Translation: AI-assisted > This article is also published at [it 邦幫忙 2022 iThome Ironman Contest](https://ithelp.ithome.com.tw/articles/10297905) ## TL;DR This is the 9th article in the "Modern Blog 30 Days" series. In the previous article, we enabled MDX support in Contentlayer. In this article, we will install Tailwind CSS, a CSS utility tool, and set up the corresponding ESLint and Prettier rules, getting ready to beautify our site! Screenshot of the results: ![Index Page result of Tailwind CSS](https://i.imgur.com/gJiZP1Q.jpg) The code changes for this article are as follows: https://github.com/eason-dev/nextjs-tailwind-contentlayer-blog-starter/compare/day08-mdx-support...day09-install-tailwindcss --- ## Installing Tailwind CSS ```shell pnpm add -D tailwindcss autoprefixer postcss npx tailwindcss init -p ``` Tweak `/tailwind.config.js`: ```js /** @type {import('tailwindcss').Config} */ module.exports = { content: ["./src/**/*.{js,ts,jsx,tsx}"], theme: { extend: {}, }, plugins: [], }; ``` Tweak `/postcss.config.js`: ```js module.exports = { plugins: { tailwindcss: {}, autoprefixer: {}, }, }; ``` Modify `/src/styles/globals.css` by deleting all its content and replacing it with these three lines: ```css @tailwind base; @tailwind components; @tailwind utilities; ``` ## Applying Basic Styles to the Homepage to Try Out Tailwind CSS Remove `/src/styles/Home.module.css`. Modify `/src/pages/index.tsx` as follows: ```tsx import type { NextPage } from "next"; import Head from "next/head"; import { allPostsNewToOld, Post } from "@/lib/contentLayerAdapter"; export function getStaticProps() { const posts = allPostsNewToOld; return { props: { posts } }; } type Props = { posts: Post[]; }; const Home: NextPage = ({ posts }) => { return (
My blog

Welcome to my blog!

{posts.map((post) => ( ))}
); }; export default Home; ``` Modify `/src/pages/[slug].tsx` as follows: ```tsx import { format, parseISO } from "date-fns"; import type { GetStaticPaths, GetStaticProps, NextPage } from "next"; import Head from "next/head"; import { useMDXComponent } from "next-contentlayer/hooks"; import { allPosts, Post } from "@/lib/contentLayerAdapter"; export const getStaticPaths: GetStaticPaths = () => { const paths = allPosts.map((post) => post.path); return { paths, fallback: false, }; }; export const getStaticProps: GetStaticProps = ({ params }) => { const post = allPosts.find((post) => post.slug === params?.slug); if (!post) { return { notFound: true, }; } return { props: { post, }, }; }; type Props = { post: Post; }; const PostPage: NextPage = ({ post }) => { const MDXContent = useMDXComponent(post.body.code); return (
{post.title}

{post.title}

); }; export default PostPage; ``` ## Installing Tailwind CSS Related ESLint and Prettier Rules ```shell pnpm add -D eslint-plugin-tailwindcss prettier-plugin-tailwindcss ``` Modify `.prettierrc.js` to add the prettier-plugin-tailwindcss plugin. The final result is as follows: ```js module.exports = { trailingComma: "es5", singleQuote: true, printWidth: 80, semi: true, plugins: ["prettier-plugin-tailwindcss"], }; ``` Modify `.eslintrc.js`: ```js module.exports = { extends: [ "eason", "next/core-web-vitals", "plugin:tailwindcss/recommended", "plugin:prettier/recommended", // Make this the last element so prettier config overrides other formatting rules ], plugins: ["tailwindcss"], rules: { "jsx-a11y/anchor-is-valid": [ "error", { components: ["Link"], specialLink: ["hrefLeft", "hrefRight"], aspects: ["invalidHref", "preferButton"], }, ], "tailwindcss/classnames-order": "off", // Respect prettier-plugin-tailwindcss order }, settings: { // Support absolute imports // https://www.npmjs.com/package/eslint-import-resolver-alias "import/resolver": { alias: { map: [["@", "./src"]], extensions: [".js", ".jsx", ".ts", ".tsx"], }, }, "import/ignore": ["contentLayerAdapter.js"], }, overrides: [ { files: "**/*.{ts,tsx}", extends: [ "eason/typescript", "plugin:prettier/recommended", // Make this the last element so prettier config overrides other formatting rules ], }, ], }; ``` ## Results Done! Run `pnpm dev` and enter the homepage, and you will see the homepage style has changed. All of it is achieved with Tailwind CSS utility classes! The results are as follows: ![Index Page result of Tailwind CSS](https://i.imgur.com/gJiZP1Q.jpg) The code changes for this article are as follows: https://github.com/eason-dev/nextjs-tailwind-contentlayer-blog-starter/compare/day08-mdx-support...day09-install-tailwindcss ## References https://tailwindcss.com/docs/guides/nextjs ## Next Article Congratulations! We have successfully installed Tailwind CSS and the corresponding ESLint and Prettier rules in our Next.js project, and given it a quick try. In the next article, we will add an essential element of a modern blog: dark mode! It is also implemented with Tailwind CSS. After that, we will start truly beautifying the styles of the entire site! --- # 安裝 Tailwind CSS 和相關 ESLint、Prettier 設定 - Modern Next.js Blog 系列 #09 - URL: https://easonchang.com/zh-TW/posts/tailwindcss-setup - Date: 2022-09-24T00:00:00.000Z - Language: zh-TW - Description: 安裝 Tailwind CSS 這套 CSS 工具,設定好對應的 ESLint, Prettier 規則 > 本文同步發佈於 [it 邦幫忙 2022 iThome 鐵人賽](https://ithelp.ithome.com.tw/articles/10297905) ## TL;DR 這是「Modern Blog 30 天」系列第 9 篇文章,上一篇我們讓 Contentlayer 也支援了 MDX 格式。這篇讓我們來安裝 Tailwind CSS 這套 CSS 工具,設定好對應的 ESLint, Prettier 規則,準備開始美化畫面! 結果截圖如下: ![Index Page result of Tailwind CSS](https://i.imgur.com/gJiZP1Q.jpg) 這篇修改的程式碼如下: https://github.com/eason-dev/nextjs-tailwind-contentlayer-blog-starter/compare/day08-mdx-support...day09-install-tailwindcss --- ## 安裝 Tailwind CSS ```shell pnpm add -D tailwindcss autoprefixer postcss npx tailwindcss init -p ``` 微調 `/tailwind.config.js`: ```js /** @type {import('tailwindcss').Config} */ module.exports = { content: ["./src/**/*.{js,ts,jsx,tsx}"], theme: { extend: {}, }, plugins: [], }; ``` 微調 `/postcss.config.js`: ```js module.exports = { plugins: { tailwindcss: {}, autoprefixer: {}, }, }; ``` 修改 `/src/styles/globals.css`,刪除所有內容,替換成這三行: ```css @tailwind base; @tailwind components; @tailwind utilities; ``` ## 在首頁套用基本樣式,試用看看 Tailwind CSS 移除 `/src/styles/Home.module.css`。 修改 `/src/pages/index.tsx`,改成下面這樣: ```tsx import type { NextPage } from "next"; import Head from "next/head"; import { allPostsNewToOld, Post } from "@/lib/contentLayerAdapter"; export function getStaticProps() { const posts = allPostsNewToOld; return { props: { posts } }; } type Props = { posts: Post[]; }; const Home: NextPage = ({ posts }) => { return (
My blog

Welcome to my blog!

{posts.map((post) => ( ))}
); }; export default Home; ``` 修改 `/src/pages/[slug].tsx`,改成下面這樣: ```tsx import { format, parseISO } from "date-fns"; import type { GetStaticPaths, GetStaticProps, NextPage } from "next"; import Head from "next/head"; import { useMDXComponent } from "next-contentlayer/hooks"; import { allPosts, Post } from "@/lib/contentLayerAdapter"; export const getStaticPaths: GetStaticPaths = () => { const paths = allPosts.map((post) => post.path); return { paths, fallback: false, }; }; export const getStaticProps: GetStaticProps = ({ params }) => { const post = allPosts.find((post) => post.slug === params?.slug); if (!post) { return { notFound: true, }; } return { props: { post, }, }; }; type Props = { post: Post; }; const PostPage: NextPage = ({ post }) => { const MDXContent = useMDXComponent(post.body.code); return (
{post.title}

{post.title}

); }; export default PostPage; ``` ## 安裝 Tailwind CSS 相關 ESLint, Prettier 規則 ```shell pnpm add -D eslint-plugin-tailwindcss prettier-plugin-tailwindcss ``` 修改 `.prettierrc.js`,加入 prettier-plugin-tailwindcss 這個 plugin,最後結果如下: ```js module.exports = { trailingComma: "es5", singleQuote: true, printWidth: 80, semi: true, plugins: ["prettier-plugin-tailwindcss"], }; ``` 修改 `.eslintrc.js`: ```js module.exports = { extends: [ "eason", "next/core-web-vitals", "plugin:tailwindcss/recommended", "plugin:prettier/recommended", // Make this the last element so prettier config overrides other formatting rules ], plugins: ["tailwindcss"], rules: { "jsx-a11y/anchor-is-valid": [ "error", { components: ["Link"], specialLink: ["hrefLeft", "hrefRight"], aspects: ["invalidHref", "preferButton"], }, ], "tailwindcss/classnames-order": "off", // Respect prettier-plugin-tailwindcss order }, settings: { // Support absolute imports // https://www.npmjs.com/package/eslint-import-resolver-alias "import/resolver": { alias: { map: [["@", "./src"]], extensions: [".js", ".jsx", ".ts", ".tsx"], }, }, "import/ignore": ["contentLayerAdapter.js"], }, overrides: [ { files: "**/*.{ts,tsx}", extends: [ "eason/typescript", "plugin:prettier/recommended", // Make this the last element so prettier config overrides other formatting rules ], }, ], }; ``` ## 成果 完成了!使用 `pnpm dev` 並進入首頁,就會看到首頁樣式改變了,全部都是由 Tailwind CSS 的 Utility classes 實現的樣式! 結果如下: ![Index Page result of Tailwind CSS](https://i.imgur.com/gJiZP1Q.jpg) 這篇修改的程式碼如下: https://github.com/eason-dev/nextjs-tailwind-contentlayer-blog-starter/compare/day08-mdx-support...day09-install-tailwindcss ## References https://tailwindcss.com/docs/guides/nextjs ## 下一篇 恭喜你!我們成功在 Next.js 專案安裝了 Tailwind CSS 和對應的 ESLint, Prettier 規則了,並簡單試用了一下。 下一篇我們會再加入 Modern Blog 必備的要素:暗黑模式!也是使用 Tailwind CSS 實現的。 實作完之後就會開始真正美化全站的樣式了! --- # Enabling MDX Support in Contentlayer Articles - Modern Next.js Blog Series #08 - URL: https://easonchang.com/posts/contentlayer-mdx - Date: 2022-09-23T00:00:00.000Z - Language: en - Description: Supporting MDX format in Contentlayer allows for the insertion of any React component within Markdown, enhancing the flexibility of article content > This article is also published on [it 邦幫忙 2022 iThome Ironman Competition](https://ithelp.ithome.com.tw/articles/10297205) ## TL;DR This is the 8th article in the "Modern Blog 30 Days" series. In the previous article, we implemented the article detail page in Markdown format. In this article, we will extend it to support the MDX format, which allows any React component to be inserted within Markdown, enhancing the flexibility of the article content! The result screenshot is as follows: ![MDX post result](https://i.imgur.com/hTvMx05.jpg) The modified code for this article is as follows: https://github.com/eason-dev/nextjs-tailwind-contentlayer-blog-starter/compare/day07-post-apge-bare-bone...day08-mdx-support --- ## Enabling MDX Format Support in Contentlayer ### Modify the contentlayer.config.ts file Change the filePathPattern filename and add a contentType: ```ts import { defineDocumentType, makeSource } from "./src/lib/contentLayerAdapter"; export const Post = defineDocumentType(() => ({ name: "Post", // Update filePathPattern from *.md to *.mdx, filePathPattern: `content/posts/**/*.mdx`, // and add the following line for contentType contentType: "mdx", fields: { title: { type: "string", required: true, }, description: { type: "string", required: true, }, slug: { type: "string", required: true, }, date: { type: "date", required: true, }, }, computedFields: { path: { type: "string", resolve: (post) => `/posts/${post.slug}`, }, }, })); export default makeSource({ contentDirPath: "content", documentTypes: [Post], }); ``` ### Modify the article detail page's **src/pages/posts/[slug].tsx** file Replace the div that renders HTML with ``: ```tsx import { format, parseISO } from "date-fns"; import type { GetStaticPaths, GetStaticProps, NextPage } from "next"; import Head from "next/head"; // Add the following line import { useMDXComponent } from "next-contentlayer/hooks"; import { allPosts, Post } from "@/lib/contentLayerAdapter"; import styles from "@/styles/Home.module.css"; export const getStaticPaths: GetStaticPaths = () => { const paths = allPosts.map((post) => post.path); return { paths, fallback: false, }; }; export const getStaticProps: GetStaticProps = ({ params }) => { const post = allPosts.find((post) => post.slug === params?.slug); if (!post) { return { notFound: true, }; } return { props: { post, }, }; }; type Props = { post: Post; }; const PostPage: NextPage = ({ post }) => { // Also add the following line const MDXContent = useMDXComponent(post.body.code); return (
{post.title}

{post.title}

// Replace the div that renders HTML with //
); }; export default PostPage; ``` ### Change all article file extensions from .md to .mdx Change all article file extensions under `content/posts/` to .mdx This completes the support for the MDX format! ## Add Any React Component and Insert It Into an MDX Article Next, let's test inserting a React component into the Markdown content of an MDX article. ### Add `src/components/CustomInput.tsx` ```tsx const CustomInput = () => { return (

Hello

); }; export default CustomInput; ``` ### Modify `content/posts/20220831-markdown-demo.mdx` Insert the CustomInput component: ```md --- title: Markdown demo description: This is a demo of Markdown slug: markdown-demo date: 2022-08-31 type: Post --- import CustomInput from "../../src/components/CustomInput"; ## H2 title ### H3 title Some content with [link](https://www.google.com) ``` ## Results Done! Using `pnpm dev` and entering an article with customized MDX components, you will see the MDX component displayed on the screen. The URL will look like this: http://localhost:3000/posts/markdown-demo The screenshot below shows an added text input box made with React: ![MDX post result](https://i.imgur.com/hTvMx05.jpg) The modified code for this article is as follows: https://github.com/eason-dev/nextjs-tailwind-contentlayer-blog-starter/compare/day07-post-apge-bare-bone...day08-mdx-support ## Next Article Congratulations! So far, we can also write articles in MDX format! This allows us to insert any custom component made with React anywhere, making the content of articles more flexible! But the entire blog is still quite unattractive, so let's start beautifying it next! We will use [Tailwind CSS](https://tailwindcss.com/), a popular CSS Framework these days. In the next article, let's install and use it! --- # 讓 Contentlayer 文章支援 MDX - Modern Next.js Blog 系列 #08 - URL: https://easonchang.com/zh-TW/posts/contentlayer-mdx - Date: 2022-09-23T00:00:00.000Z - Language: zh-TW - Description: 在 Contentlayer 支援 MDX 格式,能在 Markdown 內插入任何 React 元件,提升文章內容靈活度 > 本文同步發佈於 [it 邦幫忙 2022 iThome 鐵人賽](https://ithelp.ithome.com.tw/articles/10297205) ## TL;DR 這是「Modern Blog 30 天」系列第 8 篇文章,上一篇我們實作了 Markdown 格式的文章內頁,這篇我們會擴充它,讓他支援 MDX 格式,能在 Markdown 內插入任何 React 元件,提升文章內容靈活度! 結果截圖如下: ![MDX post result](https://i.imgur.com/hTvMx05.jpg) 這篇修改的程式碼如下: https://github.com/eason-dev/nextjs-tailwind-contentlayer-blog-starter/compare/day07-post-apge-bare-bone...day08-mdx-support --- ## 讓 Contentlayer 支援 MDX 格式 ### 修改 contentlayer.config.ts 檔案 修改 filePathPattern 檔名,以及新增 contentType: ```ts import { defineDocumentType, makeSource } from "./src/lib/contentLayerAdapter"; export const Post = defineDocumentType(() => ({ name: "Post", // 更新 filePathPattern,從 *.md 改成 *.mdx, filePathPattern: `content/posts/**/*.mdx`, // 並新增下面這行 contentType contentType: "mdx", fields: { title: { type: "string", required: true, }, description: { type: "string", required: true, }, slug: { type: "string", required: true, }, date: { type: "date", required: true, }, }, computedFields: { path: { type: "string", resolve: (post) => `/posts/${post.slug}`, }, }, })); export default makeSource({ contentDirPath: "content", documentTypes: [Post], }); ``` ### 修改文章內頁的 **src/pages/posts/[slug].tsx** 檔案 替換掉 render html 的 div,改成 render ``: ```tsx import { format, parseISO } from "date-fns"; import type { GetStaticPaths, GetStaticProps, NextPage } from "next"; import Head from "next/head"; // 新增下面這行 import { useMDXComponent } from "next-contentlayer/hooks"; import { allPosts, Post } from "@/lib/contentLayerAdapter"; import styles from "@/styles/Home.module.css"; export const getStaticPaths: GetStaticPaths = () => { const paths = allPosts.map((post) => post.path); return { paths, fallback: false, }; }; export const getStaticProps: GetStaticProps = ({ params }) => { const post = allPosts.find((post) => post.slug === params?.slug); if (!post) { return { notFound: true, }; } return { props: { post, }, }; }; type Props = { post: Post; }; const PostPage: NextPage = ({ post }) => { // 以及新增下面這行 const MDXContent = useMDXComponent(post.body.code); return (
{post.title}

{post.title}

// 替換掉 render html 的 div,改為 //
); }; export default PostPage; ``` ### 將所有文章副檔名由 .md 改成 .mdx 將所有 `content/posts/` 底下的文章副檔名改成 .mdx 如此一來就完成 MDX 格式支援了! ## 新增任意 React 元件,並插入 MDX 文章內 接著讓我們來測試將 React 元件放進 MDX 文章的 Markdown 內容之間。 ### 新增 `src/components/CustomInput.tsx` ```tsx const CustomInput = () => { return (

Hello

); }; export default CustomInput; ``` ### 修改 `content/posts/20220831-markdown-demo.mdx` 插入剛剛的 CustomInput 元件: ```md --- title: Markdown demo description: This is a demo of Markdown slug: markdown-demo date: 2022-08-31 type: Post --- import CustomInput from "../../src/components/CustomInput"; ## H2 title ### H3 title Some content with [link](https://www.google.com) ``` ## 成果 完成了!使用 `pnpm dev` 並進入有客製化 MDX 元件的文章,就會看到 MDX 元件顯示在畫面上了。 網址會長得像這樣: http://localhost:3000/posts/markdown-demo 截圖如下,可以看到多了一個用 React 做成的文字輸入框: ![MDX post result](https://i.imgur.com/hTvMx05.jpg) 這篇修改的程式碼如下: https://github.com/eason-dev/nextjs-tailwind-contentlayer-blog-starter/compare/day07-post-apge-bare-bone...day08-mdx-support ## 下一篇 恭喜你!目前為止我們也能用 MDX 格式撰寫文章了!能在任意地方插入任何 React 做得出來的客製化元件,讓文章內容的靈活度更好! 但目前整個 Blog 還挺醜的,接下來讓我們著手開始美化樣式吧! 我們會使用到 [Tailwind CSS](https://tailwindcss.com/) 這套最近很熱門的 CSS Framework,下一篇讓我們來安裝並使用它! --- # Implementing Post Page Features, Rendering Markdown Post Content - Modern Next.js Blog Series #07 - URL: https://easonchang.com/posts/contentlayer-post-content - Date: 2022-09-22T00:00:00.000Z - Language: en - Description: Implementing Contentlayer post pages, displaying complete article Markdown content > This article is also published at [IThelp 2022 iThome Ironman Contest](https://ithelp.ithome.com.tw/articles/10296357) ## TL;DR This is the 7th article in the "Modern Blog 30 Days" series. In the previous article, we completed the homepage post list feature. This article will continue to implement the post page, presenting the complete Markdown content of the article. The result screenshot is as follows: ![Post Page Result](https://i.imgur.com/w6pTHUJ.jpg) The modified code for this article is as follows: https://github.com/eason-dev/nextjs-tailwind-contentlayer-blog-starter/compare/day06-index-page-bare-bone...day07-post-apge-bare-bone --- ## Adding a Post Page to Display Article Content Add a new file **src/pages/posts/[slug].tsx**: ```tsx import { format, parseISO } from "date-fns"; import type { GetStaticPaths, GetStaticProps, NextPage } from "next"; import Head from "next/head"; import { allPosts, Post } from "@/lib/contentLayerAdapter"; import styles from "@/styles/Home.module.css"; export const getStaticPaths: GetStaticPaths = () => { const paths = allPosts.map((post) => post.path); return { paths, fallback: false, }; }; export const getStaticProps: GetStaticProps = ({ params }) => { const post = allPosts.find((post) => post.slug === params?.slug); if (!post) { return { notFound: true, }; } return { props: { post, }, }; }; type Props = { post: Post; }; const PostPage: NextPage = ({ post }) => { return (
{post.title}

{post.title}

); }; export default PostPage; ``` ## Result Done! Use `pnpm dev` and click on a post on the homepage to go to the post page, where you can see the Markdown content of the article displayed on the screen. The URL will look like this: http://localhost:3000/posts/markdown-demo Screenshot as follows: ![Post Page Result](https://i.imgur.com/w6pTHUJ.jpg) The modified code for this article is as follows: https://github.com/eason-dev/nextjs-tailwind-contentlayer-blog-starter/compare/day06-index-page-bare-bone...day07-post-apge-bare-bone ## Next Article So far, we have been able to write articles in Markdown format! But our goal is to create a "Modern Blog," and the limited Markdown elements cannot meet our needs for writing articles. We hope to insert custom React components into the articles to make them more interactive! Therefore, in the next article, we will expand the settings of Contentlayer to support articles in [MDX](https://mdxjs.com/) format, allowing us to insert custom React components into Markdown articles! --- # 文章內頁功能實作,渲染 Markdown 文章內容 - Modern Next.js Blog 系列 #07 - URL: https://easonchang.com/zh-TW/posts/contentlayer-post-content - Date: 2022-09-22T00:00:00.000Z - Language: zh-TW - Description: 實作 Contentlayer 文章內頁,呈現完整文章 Markdown 內容 > 本文同步發佈於 [it 邦幫忙 2022 iThome 鐵人賽](https://ithelp.ithome.com.tw/articles/10296357) ## TL;DR 這是「Modern Blog 30 天」系列第 7 篇文章,上一篇我們做完了首頁文章列表功能,這篇我們接著實作文章內頁,呈現完整文章 Markdown 內容。 結果截圖如下: ![Post Page Result](https://i.imgur.com/w6pTHUJ.jpg) 這篇修改的程式碼如下: https://github.com/eason-dev/nextjs-tailwind-contentlayer-blog-starter/compare/day06-index-page-bare-bone...day07-post-apge-bare-bone --- ## 新增文章內頁,呈現文章內容 新增 **src/pages/posts/[slug].tsx** 檔案: ```tsx import { format, parseISO } from "date-fns"; import type { GetStaticPaths, GetStaticProps, NextPage } from "next"; import Head from "next/head"; import { allPosts, Post } from "@/lib/contentLayerAdapter"; import styles from "@/styles/Home.module.css"; export const getStaticPaths: GetStaticPaths = () => { const paths = allPosts.map((post) => post.path); return { paths, fallback: false, }; }; export const getStaticProps: GetStaticProps = ({ params }) => { const post = allPosts.find((post) => post.slug === params?.slug); if (!post) { return { notFound: true, }; } return { props: { post, }, }; }; type Props = { post: Post; }; const PostPage: NextPage = ({ post }) => { return (
{post.title}

{post.title}

); }; export default PostPage; ``` ## 成果 完成了!使用 `pnpm dev` 並點擊首頁的文章,就能進到文章內頁,看到 Markdown 文章內容呈現在畫面上。 網址會長得像這樣: http://localhost:3000/posts/markdown-demo 截圖如下: ![Post Page Result](https://i.imgur.com/w6pTHUJ.jpg) 這篇修改的程式碼如下: https://github.com/eason-dev/nextjs-tailwind-contentlayer-blog-starter/compare/day06-index-page-bare-bone...day07-post-apge-bare-bone ## 下一篇 目前為止我們已經能用 Markdown 格式撰寫文章了! 但我們目標是做出「Modern Blog」,有限的 Markdown 元素無法滿足我們寫文章的需求。 我們希望文章內能插入客製化 React 元件,讓文章更有互動性! 因此下一篇,我們會擴充 Contentlayer 的設定,支援 [MDX](https://mdxjs.com/) 格式的文章,讓我們能在 Markdown 文章內插入客製化 React 文件! --- # Implementing Homepage Functionality, Adding Contentlayer Post List - Modern Next.js Blog Series #06 - URL: https://easonchang.com/posts/contentlayer-post-list - Date: 2022-09-21T00:00:00.000Z - Language: en - Description: Implementing homepage post list functionality using Contentlayer, displaying all Markdown articles > This article is also published at [it Help 2022 iThome Ironman Contest](https://ithelp.ithome.com.tw/articles/10295835) Let's use Contentlayer installed in the previous article to implement the homepage post list functionality, displaying all Markdown articles. Result screenshot as follows: ![Index Page Result](https://i.imgur.com/6nqq8VP.jpg) > The code changes for this article are as follows: > https://github.com/eason-dev/nextjs-tailwind-contentlayer-blog-starter/compare/day05-markdown-contentlayer...day06-index-page-bare-bone --- ## Sorting Articles, Using date-fns In the last article, we set the path for articles in `contentlayer.config.ts`. Contentlayer will read `content/posts/**/*.md`, which are all **.md** files in `content/posts/`, becoming the **allPosts** array for our use. However, Contentlayer does not guarantee the order of articles, and I wish the homepage post list to be sorted from newest to oldest. Therefore, we need to sort the article array ourselves. We will use date-fns to sort based on the date attribute of the articles. ### Installing date-fns ```shell pnpm add date-fns ``` ### Modifying contentLayerAdapter.js Import the compareDesc function from date-fns to sort allPosts, and export it as the allPostsNewToOld constant for later use in the homepage post list: ```js import { allPosts, Post } from 'contentlayer/generated'; import { defineDocumentType, defineNestedType, makeSource, } from 'contentlayer/source-files'; import { compareDesc } from 'date-fns'; # Add this export { allPosts, defineDocumentType, defineNestedType, makeSource, Post }; # Add the following export const allPostsNewToOld = allPosts?.sort((a, b) => { return compareDesc(new Date(a.date), new Date(b.date)); }) || []; ``` ## Adding the Post List to the Homepage Modify **/src/pages/index.tsx**: ```tsx import type { NextPage } from "next"; import Head from "next/head"; import Image from "next/image"; import { allPostsNewToOld, Post } from "@/lib/contentLayerAdapter"; import styles from "@/styles/Home.module.css"; export function getStaticProps() { const posts = allPostsNewToOld; return { props: { posts } }; } type Props = { posts: Post[]; }; const Home: NextPage = ({ posts }) => { return (
Create Next App

Welcome to Next.js!

Get started by editing{" "} pages/index.tsx

{posts.map((post) => (

{post.title}

{post.description}

))}
); }; export default Home; ``` ## Result Done! Use `pnpm dev` and browse the homepage, and you should see two articles displayed on the homepage, screenshot as follows: ![Index Page Result](https://i.imgur.com/6nqq8VP.jpg) ## Summary & Next Article Congratulations on successfully importing Markdown articles processed by Contentlayer and displaying them on the homepage post list! > The code changes for this article are as follows: > https://github.com/eason-dev/nextjs-tailwind-contentlayer-blog-starter/compare/day05-markdown-contentlayer...day06-index-page-bare-bone But if you click on these two articles, you will see a 404 error screen, because we have not yet implemented the article detail page. In the next article, let's implement the article detail page and display the full article content! --- # 首頁功能實作,加入 Contentlayer 文章列表 - Modern Next.js Blog 系列 #06 - URL: https://easonchang.com/zh-TW/posts/contentlayer-post-list - Date: 2022-09-21T00:00:00.000Z - Language: zh-TW - Description: 使用 Contentlayer 實作首頁文章列表功能,呈現所有 Markdown 文章 > 本文同步發佈於 [it 邦幫忙 2022 iThome 鐵人賽](https://ithelp.ithome.com.tw/articles/10295835) 讓我們使用上一篇安裝的 Contentlayer,實作首頁文章列表功能,呈現所有 Markdown 文章。 結果截圖如下: ![Index Page Result](https://i.imgur.com/6nqq8VP.jpg) > 這篇修改的程式碼如下: > https://github.com/eason-dev/nextjs-tailwind-contentlayer-blog-starter/compare/day05-markdown-contentlayer...day06-index-page-bare-bone --- ## 排序文章,使用 date-fns 上一篇我們在 `contentlayer.config.ts` 設定了放文章的路徑,Contentlayer 會去讀取 `content/posts/**/*.md`,也就是 `content/posts/` 裡面的所有 **.md** 檔案,變成 **allPosts** 陣列給我們。 但 Contentlayer 不保證文章的排序,而我希望首頁文章列表能從新到舊排列,因此我們需要自己排序文章陣列。 我們將使用 date-fns 來根據文章的 date 屬性排序。 ### 安裝 date-fns ```shell pnpm add date-fns ``` ### 修改 contentLayerAdapter.js 引入 date-fns 的 compareDesc function,來排序 allPosts,並 export 成 allPostsNewToOld 常數,供稍後首頁文章列表使用: ```js import { allPosts, Post } from 'contentlayer/generated'; import { defineDocumentType, defineNestedType, makeSource, } from 'contentlayer/source-files'; import { compareDesc } from 'date-fns'; # Add this export { allPosts, defineDocumentType, defineNestedType, makeSource, Post }; # Add the following export const allPostsNewToOld = allPosts?.sort((a, b) => { return compareDesc(new Date(a.date), new Date(b.date)); }) || []; ``` ## 在首頁加入文章列表 修改 **/src/pages/index.tsx**: ```tsx import type { NextPage } from "next"; import Head from "next/head"; import Image from "next/image"; import { allPostsNewToOld, Post } from "@/lib/contentLayerAdapter"; import styles from "@/styles/Home.module.css"; export function getStaticProps() { const posts = allPostsNewToOld; return { props: { posts } }; } type Props = { posts: Post[]; }; const Home: NextPage = ({ posts }) => { return (
Create Next App

Welcome to Next.js!

Get started by editing{" "} pages/index.tsx

{posts.map((post) => (

{post.title}

{post.description}

))}
); }; export default Home; ``` ## 成果 完成了!使用 `pnpm dev` 並瀏覽首頁,你應該就會看到首頁顯示出了兩篇文章,截圖如下: ![Index Page Result](https://i.imgur.com/6nqq8VP.jpg) ## 小結&下一篇 恭喜你成功引入 Contentlayer 處理完的 Markdown 文章,顯示在首頁文章列表! > 這篇修改的程式碼如下: > https://github.com/eason-dev/nextjs-tailwind-contentlayer-blog-starter/compare/day05-markdown-contentlayer...day06-index-page-bare-bone 但如果你點擊這兩篇文章,會看到 404 錯誤畫面,這是因為我們還沒實作文章內頁。 下一篇文章就讓我們來實作文章內頁,顯示完整文章內容! --- # Introduction to Markdown & Installing ContentLayer - Modern Next.js Blog Series #05 - URL: https://easonchang.com/posts/markdown-contentlayer - Date: 2022-09-20T00:00:00.000Z - Language: en - Description: Writing articles using Markdown syntax and installing Contentlayer to convert Markdown files into articles > This article is also published on [iT 邦幫忙 2022 iThome Ironman](https://ithelp.ithome.com.tw/articles/10295154) In this article, we will introduce how to write articles using Markdown syntax, as well as install Contentlayer to convert Markdown files into articles. > The complete code changes for this article can be seen here: > https://github.com/eason-dev/nextjs-tailwind-contentlayer-blog-starter/compare/day04-prepare...day05-markdown-contentlayer --- ## Writing in Markdown Format [Markdown](https://markdown.tw/) is a typography syntax that is easy to read and write. By adding a few special symbols in a plain text file, you can mark up headings, links, code blocks, bold, italics, bullet points, etc. Examples are as follows: ````markdown # Big Title ## H2 Title ### H3 Title A normal paragraph of text, with **bold** and _italics_. [Hyperlink](https://www.google.com/) ![Image](https://i.imgur.com/oNtgi9e.png) - Bullet point - Bullet point 2 > Callout quote block > Tony Stark: "I am Iron Man" > ``` > Code block > ``` ```` Markdown is very popular among developers, writers, and note-takers. Github's README.md or various comment boxes can use Markdown syntax. The iT 邦幫忙 Ironman articles you see now are also written in Markdown. Modern note-taking software like Notion, [Craft](https://obsidian.md/), [Obsidian](https://obsidian.md/), [HackMD](https://hackmd.io/), etc., also support it. The main advantage of writing in Markdown is that it allows us to focus on the content. It provides just the right amount of formatting syntax, not so little that it can't be beautifully typeset, nor so much that we spend too much time dealing with the formatting. Next, it's easy to write, modifying the format just by adding a symbol, your hands don't have to leave the keyboard, which won't slow down your typing speed. Lastly, because it's plain text and the format is uniform, it's easy to move between platforms. You don't have to worry too much about not being able to retrieve your hard work from a writing platform if it stops being maintained. Therefore, our blog will use Markdown for writing! ## Contentlayer, Handling Markdown Articles in Next.js To write in Markdown or MDX (introduced in [part 8](/posts/contentlayer-mdx)) in Next.js, there are several tools available: - Official MDX support from Next.js ([Advanced Features: Using MDX | Next.js](https://nextjs.org/docs/advanced-features/using-mdx)) - [next-mdx-remote](https://github.com/hashicorp/next-mdx-remote) - [mdx-bundler](https://github.com/kentcdodds/mdx-bundler) - [Contentlayer](https://www.contentlayer.dev/) In this series, we will use [Contentlayer](https://www.contentlayer.dev/), which is the youngest and offers the best development experience among these solutions. ![Contentlayer](https://i.imgur.com/mlDKJBZ.jpg) Contentlayer is a JS package, a content management SDK that can convert various formats of articles into programmatically readable, Type-safe JSON format. Contentlayer is currently in beta, supporting Markdown and MDX format sources, as well as integration with Next.js. Here's how Contentlayer works: First, we need to define the format of the articles, the fields, and types they have. Then, during `pnpm dev` runtime or `pnpm build` packaging, it will convert all specified directory articles into TypeScript-supported JSON format, which can be directly imported in Next.js to render onto the screen. Contentlayer is easy to set up and supports live-reload; when editing articles, the Next.js front-end will also update in real-time, providing a great developer experience. ### Installing Contentlayer Enter the command to install related packages: ```shell pnpm add contentlayer next-contentlayer pnpm add -D esbuild ``` Rename `next.config.js` to `next.config.mjs` and modify its content as follows: ```js import { withContentlayer } from "next-contentlayer"; /** @type {import('next').NextConfig} */ const nextConfig = withContentlayer({ reactStrictMode: true, swcMinify: true, eslint: { // Only run ESLint on the 'pages/' and 'components/' directories during production builds (next build) dirs: ["pages", "components"], }, // Add more Next.js config options here }); export default nextConfig; ``` Next, we create a new file `contentlayer.config.ts` in the root directory, defining the format of the articles: ```ts import { defineDocumentType, makeSource } from "contentlayer/source-files"; const Post = defineDocumentType(() => ({ name: "Post", filePathPattern: `posts/*.md`, fields: { title: { type: "string", required: true }, date: { type: "date", required: true }, slug: { type: "string", required: true }, }, computedFields: { // Define computed fields here }, })); export default makeSource({ contentDirPath: "data", documentTypes: [Post], }); ``` This completes the basic setup for Contentlayer. In the next article, we'll discuss how to display articles on the screen using Contentlayer! --- > This series of articles are written as part of participating in [iT 邦幫忙 2022 iThome Ironman](https://ithelp.ithome.com.tw/articles/10295154), documenting the learning process of Next.js. If you're interested in learning more about web development, feel free to follow this series! --- # Markdown 簡介 & 安裝 ContentLayer - Modern Next.js Blog 系列 #05 - URL: https://easonchang.com/zh-TW/posts/markdown-contentlayer - Date: 2022-09-20T00:00:00.000Z - Language: zh-TW - Description: 使用 Markdown 語法撰寫文章,以及安裝 Contentlayer 將 Markdown 檔案轉換成文章 > 本文同步發佈於 [it 邦幫忙 2022 iThome 鐵人賽](https://ithelp.ithome.com.tw/articles/10295154) 這篇我們來簡介如何使用 Markdown 語法撰寫文章,以及安裝 Contentlayer 將 Markdown 檔案轉換成文章。 > 這篇完整程式碼改動可以在這裡看: > https://github.com/eason-dev/nextjs-tailwind-contentlayer-blog-starter/compare/day04-prepare...day05-markdown-contentlayer --- ## 使用 Markdown 格式寫作 [Markdown](https://markdown.tw/) 是一個好讀又好寫的排版語法,在純文字檔中加入幾個特殊符號,就能標示出標題、連結、程式碼區塊、粗體、斜體、項目符號列點等等。 範例如下: ````markdown # 大標題 ## H2 標題 ### H3 標題 一段正常的文字,和 **粗體** 及 _斜體_。 [超連結](https://www.google.com/) ![圖片](https://i.imgur.com/oNtgi9e.png) - 列點 - 列點 2 > Callout 引用區塊 > Tony Stark:「I am Iron Man」 > ``` > 程式碼區塊 > ``` ```` Markdown 在開發者圈、寫作圈、筆記圈非常盛行,Github 的 README.md 或是各種留言框,都能使用 Markdown 語法。你現在看到的 iT 邦幫忙鐵人賽文章,也是用 Markdown 寫成的。現代筆記軟體如 Notion、[Craft](https://obsidian.md/)、[Obsidian](https://obsidian.md/)、[HackMD](https://hackmd.io/) 等也都支援。 用 Markdown 寫作的主要優點是能讓我們專注於內容。它提供了數量剛好的格式語法,不至於少到無法排版地美觀,也不會多到讓我們花太多時間處理零碎排版。 再來是它好寫,修改格式只要簡單加個符號,手完全不用離開鍵盤,不會拖累你的打字速度。 最後因為他是純文字的,格式統一,很容易在各個平台間互相搬移。不需要過度擔心某個寫作平台停止維護後,自己的文章心血拿不回來的問題。 因此我們的部落格會使用 Markdown 來寫作! ## Contentlayer,在 Next.js 裡處理 Markdown 文章 要在 Next.js 裡使用 Markdown 或 MDX(系列[第 8 篇](/posts/contentlayer-mdx)會介紹)寫作,有好幾個工具能實現: - Next.js 官方的 MDX 支援([Advanced Features: Using MDX | Next.js](https://nextjs.org/docs/advanced-features/using-mdx)) - [next-mdx-remote](https://github.com/hashicorp/next-mdx-remote) - [mdx-bundler](https://github.com/kentcdodds/mdx-bundler) - [Contentlayer](https://www.contentlayer.dev/) 這系列我們會採用 [Contentlayer](https://www.contentlayer.dev/),它是裡面最年輕,開發體驗最好的解決方案。 ![Contentlayer](https://i.imgur.com/mlDKJBZ.jpg) Contentlayer 是一個 JS 套件,功能是內容管理的 SDK,能將各種格式的文章,轉換成程式可讀的、Type-safe 的 JSON 格式。 Contentlayer 目前還在 beta 階段,支援了 Markdown 和 MDX 格式的文章來源,以及 Next.js 的整合。 Contentlayer 的用法是這樣的:我們首先要定義文章的格式、擁有的欄位及型別,接著在 `pnpm dev` 執行期間或 `pnpm build` 打包時,他就會將指定目錄所有文章,轉換成支援 TypeScript 的 JSON 格式,可直接在 Next.js 內 import 來渲染至畫面上。 Contentlayer 設定簡單,而且支援 live-reload,編輯文章時 Next.js 前端畫面也會同步更新,開發者體驗相當好。 ### 安裝 Contentlayer 輸入指令安裝相關套件: ```shell pnpn add contentlayer next-contentlayer pnpm add -D esbuild ``` 將 `next.config.js` 改名為 `next.config.mjs`,並修改內容如下: ```js import { withContentlayer } from "next-contentlayer"; /** @type {import('next').NextConfig} */ const nextConfig = withContentlayer({ reactStrictMode: true, swcMinify: true, eslint: { // Warning: This allows production builds to successfully complete even if // your project has ESLint errors. ignoreDuringBuilds: true, }, typescript: { // Dangerously allow production builds to successfully complete even if // your project has type errors. ignoreBuildErrors: true, }, }); export default nextConfig; ``` 修改 `tsconfig.json`: ```json { "compilerOptions": { // ... "paths": { // ... "contentlayer/generated": ["./.contentlayer/generated"] } }, "include": [ // ... ".contentlayer/generated" ] // ... } ``` 新增 `contentlayer.config.ts`,定義 Post 這個資源以及我們需要的欄位: ```ts import { defineDocumentType, makeSource } from "./src/lib/contentLayerAdapter"; export const Post = defineDocumentType(() => ({ name: "Post", filePathPattern: `content/posts/**/*.md`, fields: { title: { type: "string", required: true, }, description: { type: "string", required: true, }, slug: { type: "string", required: true, }, date: { type: "date", required: true, }, }, computedFields: { path: { type: "string", resolve: (post) => `/posts/${post.slug}`, }, }, })); export default makeSource({ contentDirPath: "content", documentTypes: [Post], }); ``` 新增 `src/lib/contentLayerAdapter.js`: ```js import { allPosts, Post } from "contentlayer/generated"; import { defineDocumentType, defineNestedType, makeSource, } from "contentlayer/source-files"; export { allPosts, defineDocumentType, defineNestedType, makeSource, Post }; ``` ### 修改 ESLint 設定,忽略 Contentlayer 相關警告 Contentlayer 的 import 模式,會被 ESLint 視為錯誤,但不影響功能,需要特別處理。 修改 `.eslintrc.js`,忽略 contentLayerAdapter.js 的 import 警告: ```js module.exports = { // ... settings: { // ... "import/ignore": ["contentLayerAdapter.js"], }, // ... }; ``` 修改 `.gitignore` 和 `.prettierignore`,忽略 Contentlayer 會產生的檔案: ``` # ... # Contentlayer .contentlayer ``` 修改 `.eslintignore`,忽略 Contentlayer 會產生的檔案,以及 contentLayerAdapter.js 的錯誤: ``` # ... # Contentlayer .contentlayer # Ignore contentlayer eslint errors contentLayerAdapter.js ``` 這樣就完成 Contentlayer 設定了! ## 新增測試用 Markdown 文章 未來所有文章都放在 `/content/posts/` 目錄裡,新增幾篇來測試吧! 新增 `content/posts/20220830-sample-post.md`: ```md --- title: Sample post description: My first post slug: sample-post date: 2022-08-30 type: Post --- ## This is my first sample post Congratulations! You have created your first post. ``` 新增 `content/posts/20220831-markdown-demo.md`: ```md --- title: Markdown demo description: This is a demo of Markdown slug: markdown-demo date: 2022-08-31 type: Post --- ## H2 title ### H3 title Some content with [link](https://www.google.com) ``` ## 驗證成果 執行 `pnpm dev` 啟動專案,看到 terminal 出現如下的訊息,就代表 Contentlayer 成功轉換完兩篇文章了: > Generated 2 documents in .contentlayer ![console message](https://i.imgur.com/ebrl5tp.jpg) 目前我們還沒在頁面上顯示文章,所以只能用這種方式驗證。 下一篇就會實際用上文章內容了! ## References - [Markdown 語法說明](https://markdown.tw/) - [Contentlayer makes content easy for developers](https://www.contentlayer.dev/) - [How I Built my Blog using MDX, Next.js, and React](https://www.joshwcomeau.com/blog/how-i-built-my-blog/) ## 小結&下一篇 我們了解了 Markdown 格式的文章寫作,也在 Next.js 安裝了 Contentlayer 處理 Markdown 檔案。 > 這篇完整程式碼改動可以在這裡看: > https://github.com/eason-dev/nextjs-tailwind-contentlayer-blog-starter/compare/day04-prepare...day05-markdown-contentlayer 下一篇我們會開始實作基本首頁功能,顯示文章列表! --- # Basic Setup for Next.js Projects with ESLint, Prettier, TypeScript, etc. - Modern Next.js Blog Series #04 - URL: https://easonchang.com/posts/nextjs-dev-env - Date: 2022-09-19T00:00:00.000Z - Language: en - Description: Basic setup for Next.js projects for a smoother development experience and a more readable project structure. Includes locking the Node.js version with .nvmrc, integrating ESLint and Prettier, using the /src folder, and setting up Absolute Import > This article is also published at [it 邦幫忙 2022 iThome Ironman Contest](https://ithelp.ithome.com.tw/articles/10294414) In this post, we will further make some basic Next.js project setups for a smoother subsequent development experience and a more readable project structure. We will set up the following: - Add a .nvmrc to lock the project's Node.js version - Integrate ESLint and Prettier - Place the main code in the /src folder - Configure Absolute Import We'll start by explaining how to view the Github repo code. --- ## How to view the code from the previous article and the changes in this one I have placed the code for this series in the Github repo [eason-dev/nextjs-tailwind-contentlayer-blog-starter](https://github.com/eason-dev/nextjs-tailwind-contentlayer-blog-starter). Each article that involves code writing will include a link like this: > The complete code changes for this article are as follows: > https://github.com/eason-dev/nextjs-tailwind-contentlayer-blog-starter/compare/day02-init-nextjs...day04-prepare Clicking it will display the screen below, showing all the code changes corresponding to the article: ![source code compare](https://i.imgur.com/laTd6Z0.jpg) The principle is that the commits and the content and order of branches in the repo are carefully designed. Each commit only does one thing, and the commit message is also thoughtfully written, containing many reference links. If you do not understand the reason or source of a particular change, you can click the middle block to see the detailed content of the commit. The sequence of branches follows the 30-day series article order, with each article's implementation corresponding to a branch, marking the final result of that day's article. For instance, the interim result after completing [Day2 Establishing Next.js Project](posts/frontend-intro-create-nextjs) can be seen in the [day02-init-nextjs](https://github.com/eason-dev/nextjs-tailwind-contentlayer-blog-starter/tree/day02-init-nextjs) branch. And today's Day4 is in the [day04-prepare](https://github.com/eason-dev/nextjs-tailwind-contentlayer-blog-starter/tree/day04-prepare) branch. The screen shown above is the difference (compare) of Day4 compared to Day2 (base), which is the part modified in today's article (skipping Day3 because Day3 didn't change anything). Sometimes, due to too many changes, I will not post all the code in the article, and you can go to Github to see the complete changes. If you want to manually implement the changes of this article based on the results of the previous one, you can also clone the base branch (day02-init-nextjs) shown in the picture to modify. --- ## Basic Setup for Next.js Projects Back to the main topic, first, let's do some basic setup for the brand-new Next.js project from the last article, to make the subsequent development experience smoother and the project structure more readable. > The complete code changes for this article are as follows: > https://github.com/eason-dev/nextjs-tailwind-contentlayer-blog-starter/compare/day02-init-nextjs...day04-prepare ## Add .nvmrc to Lock the Project's Node.js Version If you are developing many front-end projects at the same time, each with different Node.js versions, you can use [nvm](https://github.com/nvm-sh/nvm), Node Version Manager, to install multiple versions of Node.js and switch between them as needed. Refer to the [nvm repo's Installing and Updating instructions](https://github.com/nvm-sh/nvm#installing-and-updating) for installation methods, which I won't expand on here. However, after using nvm for a while, you'll find manually switching Node versions to be cumbersome. Fortunately, nvm provides a configuration file for deep shell integration, allowing you to automatically switch versions when you cd into different directories. Please refer to the [nvm repo's Deeper Shell Integration instructions](https://github.com/nvm-sh/nvm#deeper-shell-integration) for setting up each shell. After setting up, you can create a `.nvmrc` file in the root directory of the project and write the desired Node.js version in it, like so: ```plaintext 16 ``` Then, every time you enter the project directory, the shell will automatically switch to the Node.js version specified in `.nvmrc`. ## Integrate ESLint and Prettier Next, let's integrate ESLint and Prettier into the project. ### ESLint [ESLint](https://eslint.org/) is a static code analysis tool for identifying problematic patterns found in JavaScript code. It is widely used in the industry to ensure code quality and consistency. Next.js has built-in ESLint support, which is initialized when creating a new project. You can start using it immediately without additional setup. However, the default configuration might not meet all your needs, so you can customize it by modifying the `.eslintrc` file. For example, to use Airbnb's JavaScript style guide, which is a popular coding style, you can extend the configuration like this: ```json { "extends": ["next", "next/core-web-vitals", "airbnb"] } ``` Remember to install the necessary packages: ```bash npm install eslint-config-airbnb eslint-plugin-import eslint-plugin-jsx-a11y eslint-plugin-react eslint-plugin-react-hooks --save-dev ``` ### Prettier [Prettier](https://prettier.io/) is an opinionated code formatter that supports many languages and integrates with most editors. It removes all original styling and ensures that all outputted code conforms to a consistent style. To integrate Prettier with ESLint, you can use `eslint-config-prettier` and `eslint-plugin-prettier`. These packages disable ESLint rules that might conflict with Prettier, allowing you to use both tools together without issues. First, install the necessary packages: ```bash npm install prettier eslint-config-prettier eslint-plugin-prettier --save-dev ``` Then, update the `.eslintrc` file to include Prettier in the extends section: ```json { "extends": ["next", "next/core-web-vitals", "airbnb", "prettier"], "plugins": ["prettier"], "rules": { "prettier/prettier": "error" } } ``` This configuration makes ESLint run Prettier as an ESLint rule and report formatting differences as ESLint errors. ### Place the Main Code in the /src Folder To make the project structure more readable and maintainable, it's a good practice to place the main code inside a `/src` folder. This includes components, pages, styles, and any other code related to your application. It helps separate the application code from configuration files and other non-code files. Here's an example of how to organize the project structure: ``` my-next-app/ ├── src/ │ ├── components/ │ ├── pages/ │ └── styles/ ├── public/ ├── .eslintrc ├── .prettierrc ├── next.config.js ├── package.json └── tsconfig.json ``` After moving the files, you might need to update import paths in your code to reflect the new structure. However, this setup makes managing and navigating your project much easier in the long run. ## Configure Absolute Import Absolute imports can make your import statements cleaner and more readable, especially in large projects with deep directory structures. Instead of using relative paths like `../../components/Button`, you can use absolute paths based on the root of your project, like `components/Button`. Next.js supports absolute imports out of the box. You just need to configure it in the `jsconfig.json` (for JavaScript projects) or `tsconfig.json` (for TypeScript projects) file. Here's how to set it up in `jsconfig.json`: ```json { "compilerOptions": { "baseUrl": "src" }, "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], "exclude": ["node_modules"] } ``` And in `tsconfig.json` for TypeScript projects: ```json { "compilerOptions": { "baseUrl": ".", "paths": { "@/*": ["src/*"] } } } ``` This configuration tells the TypeScript compiler (and by extension, your editor and Next.js) where to look for modules, allowing you to use absolute paths for imports. Using absolute imports not only makes your import statements cleaner but also reduces the hassle of updating paths when moving files around. ## Conclusion In this article, we covered some basic setups for Next.js projects, including locking the Node.js version with `.nvmrc`, integrating ESLint and Prettier, organizing code in the `/src` folder, and configuring absolute imports. These setups aim to enhance the development experience by ensuring code quality, readability, and maintainability. They serve as a good foundation for building robust Next.js applications. In the next article, we will delve into more advanced topics. Stay tuned! --- # ESLint、Prettier、TypeScript 等 Next.js 專案基礎設定 - Modern Next.js Blog 系列 #04 - URL: https://easonchang.com/zh-TW/posts/nextjs-dev-env - Date: 2022-09-19T00:00:00.000Z - Language: zh-TW - Description: Next.js 專案基本設定,讓後續開發體驗更流暢、專案結構更易讀。包含用 .nvmrc 鎖定 Node.js 版本、導入 ESLint 和 Prettier、使用 /src 資料夾、設定 Absolute Import > 本文同步發佈於 [it 邦幫忙 2022 iThome 鐵人賽](https://ithelp.ithome.com.tw/articles/10294414) 這一篇我們來進一步做些 Next.js 專案基本設定,讓後續開發體驗更流暢、專案結構更易讀。 我們會做這些設定: - 新增 .nvmrc 鎖定專案 Node.js 版本 - 導入 ESLint、Prettier - 將主體 code 放在 /src 資料夾 - 設定 Absolute Import 開頭也會先說明如何查看 Github repo 程式碼。 --- ## 如何查看上一篇的程式碼,和這篇修改的部分 這系列程式碼我都有放在 Github 上的 [eason-dev/nextjs-tailwind-contentlayer-blog-starter](https://github.com/eason-dev/nextjs-tailwind-contentlayer-blog-starter) repo 裡。 每一篇有寫 code 的文章,都會附上下面這種連結: > 這篇改動的完整程式碼如下: > https://github.com/eason-dev/nextjs-tailwind-contentlayer-blog-starter/compare/day02-init-nextjs...day04-prepare 點開會看到下圖畫面,是文章對應的所有程式碼改動: ![source code compare](https://i.imgur.com/laTd6Z0.jpg) 原理是 repo 裡的 commit 及 branch 內容及順序是精心設計過的。 每個 commit 都只做一件事情,commit message 也有用心撰寫,包含不少參考連結。如果你對某個改動的原因或來源不了解,可以點中間區塊進去看 commit 詳細內容。 branch 順序遵照了 30 天系列文章順序,每篇文章實作完,都有一支對應 branch,標出當天文章最終成果。 像是 [Day2 建立 Next.js 專案](posts/frontend-intro-create-nextjs) 做完後的階段性成果,可以看 [day02-init-nextjs](https://github.com/eason-dev/nextjs-tailwind-contentlayer-blog-starter/tree/day02-init-nextjs) branch。 而今天這篇 Day4 則在 [day04-prepare](https://github.com/eason-dev/nextjs-tailwind-contentlayer-blog-starter/tree/day04-prepare) branch。 上圖畫面中看到的是 Day4(compare)相較於 Day2(base)的差異,也就是今天文章有修改的部分(跳過 Day3 是因為 Day3 沒改東西)。 有時候因為改動太多,我不會把所有 code 都貼上文章,你就可以進 Github 看完整的改動。 如果你想基於上一篇的成果,只動手實作這篇的改動的話,也能 clone 圖中的 base branch(day02-init-nextjs)來修改。 --- ## Next.js 專案做基礎設定 回到正題,首先我們來為上一篇的全新 Next.js 專案做基礎設定,讓後續開發體驗更流暢、專案結構更易讀。 > 這篇改動的完整程式碼如下: > https://github.com/eason-dev/nextjs-tailwind-contentlayer-blog-starter/compare/day02-init-nextjs...day04-prepare ## 新增 .nvmrc 鎖定專案 Node.js 版本 如果你同時開發許多前端專案,每個專案 Node.js 版本都不同的話,可以使用 [nvm](https://github.com/nvm-sh/nvm) Node Version Manager 來同時安裝多個 Node.js,依照需求切換不同版本。 安裝方式參考 [nvm repo 的 Installing and Updating 說明](https://github.com/nvm-sh/nvm#installing-and-updating),這邊就不展開了。 但用 nvm 一段時間後,你會發現手動切換 Node 版本很麻煩。 好在 nvm 提供了 shell 深度整合的設定檔,能讓你在終端機 cd 進不同目錄時,自動切換版本。 各 shell 的設定方式也請參考 [nvm repo 的 Deeper Shell Integration 說明](https://github.com/nvm-sh/nvm#deeper-shell-integration) 自行設定。 設定完後還需要在專案內新增 **.nvmrc** 檔案,指定此專案 Node.js 版本號,下次 cd 進來就會自動切換過去。 這裡使用最新的 Node.js LTS 版本 v16.17.0。 在專案根目錄新增 **.nvmrc** 檔案: ``` v16.17.0 ``` ## 導入 ESLint、Prettier [ESLint](https://eslint.org/) 可以幫助我們抓出 JavaScript code 的各種錯誤,而 [Prettier](https://prettier.io/) 能幫我們統一程式碼風格。 需要長期維護或多人協作的專案,強烈建議把他們都裝起來,保持程式碼品質。 他們各自都有設定檔能依照需求調整,這裡我會導入 [eslint-config-eason](https://github.com/eason-dev/eslint-config-eason) 這套我用了許久的 ESLint、Prettier 設定。 輸入指令安裝它,這會一次安裝很多套件: ```shell npx install-peerdeps --pnpm -D eslint-config-eason ``` 接著將 `.eslintrc.json` 改名成 `.eslintrc.js`,並修改內容如下: ```js module.exports = { extends: [ "eason", "next/core-web-vitals", "plugin:prettier/recommended", // Make this the last element so prettier config overrides other formatting rules ], rules: { "jsx-a11y/anchor-is-valid": [ "error", { components: ["Link"], specialLink: ["hrefLeft", "hrefRight"], aspects: ["invalidHref", "preferButton"], }, ], }, overrides: [ { files: "**/*.{ts,tsx}", extends: [ "eason/typescript", "plugin:prettier/recommended", // Make this the last element so prettier config overrides other formatting rules ], }, ], }; ``` 新增 `.prettierrc.js`: ```js module.exports = { trailingComma: "es5", singleQuote: true, printWidth: 80, semi: true, }; ``` 新增 `.eslintignore` 及 `.prettierignore`,內容從 `.gitignore` 複製過來: ``` # ============================= Copied from .gitignore ============================= # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. # dependencies /node_modules /.pnp .pnp.js # testing /coverage # next.js /.next/ /out/ # production /build # misc .DS_Store *.pem # debug npm-debug.log* yarn-debug.log* yarn-error.log* .pnpm-debug.log* # local env files .env*.local # vercel .vercel # typescript *.tsbuildinfo next-env.d.ts # ============================ ./Copied from .gitignore ============================ ``` 修改 `package.json`,加入 lint 和 format 指令: ```json { // ... "scripts": { // ... "lint": "eslint .", "lint:fix": "eslint --fix .", "format:fix": "prettier --write './**/*.{css,scss,md,mdx,json}'" } // ... } ``` 這樣就完成了!你能用下面指令來處理程式碼的各種問題: ```shell pnpm lint # 使用 ESLint 抓出問題 pnpm link:fix # 使用 ESLint 抓出問題,並修理能自動修復的問題 pnpm format:fix # 使用 Prettier 統一程式碼風格 ``` > Note: > 針對 JavaScript 和 TypeScript 的 `.js`、`.ts`、`.jsx`、`.tsx`,請使用 ESLint 來處理,ESLint 抓完錯誤後會自動幫你用 Prettier 統一風格。 > 不要直接使用 Prettier!這樣不會執行到 ESLint 的規則,最終結果會不一樣。 > > 針對其他檔案像是 `.json`、`.css`、`.md` 就用 Prettier 統一格式就好(它們也沒有 ESLint 能用)。 ### 在 VSCode 整合 ESLint 和 Prettier 如果你使用 VSCode 來寫程式,可以安裝 [ESLint extension](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) 和 [Prettier extension](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode),並設定檔案儲存時自動執行 ESLint 和 Prettier 修復錯誤,可以讓開發體驗更順暢。 ## 在 Build 時忽略 ESLint、TypeScript 錯誤 Next.js 在 build 時,如果發現程式碼有任何 ESLint、TypeScript 錯誤,預設行為是會中斷 build,保護我們不要推送有問題的程式碼上線。 但 ESLint、TypeScript 錯誤未必會讓網站無法正常實行,有些只是格式或設定問題而已。 在開發階段時,你可能還是會想先推程式上線,後續再修正這些錯誤。 你可以修改 `next.config.js`,讓 Next.js build 時忽略 ESLint、TypeScript 錯誤: ```js /** @type {import('next').NextConfig} */ const nextConfig = { // ... eslint: { // Warning: This allows production builds to successfully complete even if // your project has ESLint errors. ignoreDuringBuilds: true, }, typescript: { // Dangerously allow production builds to successfully complete even if // your project has type errors. ignoreBuildErrors: true, }, }; module.exports = nextConfig; ``` 詳細說明可參考 Next.js 官方文件:[忽略 TypeScript 錯誤](https://nextjs.org/docs/basic-features/typescript#ignoring-typescript-errors)、[忽略 ESLint 錯誤](https://nextjs.org/docs/api-reference/next.config.js/ignoring-eslint) ## 將部落格本體程式碼移進 /src 資料夾 初始 Next.js 專案的本體程式 `/pages` 和 `/styles` 都在根目錄,但後續我們會在根目錄新增更多設定用的檔案。 建議將本體程式碼移動進 `/src` 資料夾方便區分,詳細可參考 Next.js 官方文件:[Advanced Features: \`src\` Directory](https://nextjs.org/docs/advanced-features/src-directory)。 修改如下: > `/pages/` → `/src/pages/` > `/styles/` → `/src/styles/` 未來新增本體邏輯時,像是新 component 或 utility function,也都會加在 `/src` 裡。 ## 使用絕對路徑來 import JS 程式,Next.js Absolute Imports & Module path aliases 正常在 JS 裡要引用另一個檔案內容,我們會用相對路徑寫法: ```js import "../../../styles/globals.css"; ``` 但像上面這樣路徑差異過大時,就會很難讀。 而且要複製這段 import 給不同層級的 JS 檔時,也要修改前面點點點的數量,多一個煩人步驟要做。 Next.js 提供了 Absolute Imports & Module path aliases 功能,讓我能改用下面絕對路徑方式來 import: ```js import "@/styles/globals.css"; ``` 詳見官方文件:[Advanced Features: Absolute Imports and Module Path Aliases | Next.js](https://nextjs.org/docs/advanced-features/module-path-aliases) ### 設定 Absolute Imports & Module path aliases 我們想用 `@/` 代表 `src/` 資料夾,修改 `tsconfig.json`,加入 baseUrl 和 paths: ```json { "compilerOptions": { // ... "baseUrl": ".", "paths": { "@/*": ["src/*"] } } // ... } ``` 接著要讓 ESLint 也知道新加的規則,否則會有 error。 輸入指令安裝 `eslint-import-resolver-alias`: ```shell pnpm add -D eslint-import-resolver-alias ``` 修改 `.eslintrc.js`,加入 settings 區塊: ```js module.exports = { // ... settings: { // Support absolute imports // https://www.npmjs.com/package/eslint-import-resolver-alias "import/resolver": { alias: { map: [["@", "./src"]], extensions: [".js", ".jsx", ".ts", ".tsx"], }, }, }, // ... }; ``` 這樣就完成設定了! 接著可以把所有檔案的 import 都改用絕對路徑表示。 修改 `src/pages/_app.tsx`: ```tsx import "@/styles/globals.css"; // ... ``` 修改 `src/pages/index.tsx`: ```tsx import type { NextPage } from "next"; import Head from "next/head"; import Image from "next/image"; import styles from "@/styles/Home.module.css"; // ... ``` 這樣就完成所有修改了!用 `pnpm dev` 打開網站瀏覽,如果沒有噴出任何 error 訊息就代表修改成功了! ## 小節&下一篇 恭喜你完成了 Next.js 專案基本設定,讓後續開發體驗更流暢、專案結構更易讀。 我們做完了這些設定: - 新增 .nvmrc 鎖定專案 Node.js 版本 - 導入 ESLint、Prettier - 將主體 code 放在 /src 資料夾 - 設定 Absolute Import > 這篇改動的完整程式碼如下: > https://github.com/eason-dev/nextjs-tailwind-contentlayer-blog-starter/compare/day02-init-nextjs...day04-prepare 下一篇我們將來介紹如何使用 Markdown 寫作,並加入第一個核心功能 Contentlayer,幫我們將 Markdown 轉換成 Next.js 能處理的文章! --- # Deploying a Next.js Project on Vercel Platform - Modern Next.js Blog Series #03 - URL: https://easonchang.com/posts/nextjs-to-vercel - Date: 2022-09-18T00:00:00.000Z - Language: en - Description: An introduction to the Vercel hosting platform, deploying the Next.js empty project from the previous article on Vercel, and integrating GitHub CI/CD > This article is also published on [it Bon Help 2022 iThome Ironman Competition](https://ithelp.ithome.com.tw/articles/10293504) This article will introduce the Vercel hosting platform, how to deploy a Next.js project onto it, and how to bind it with Github to achieve continuous deployment. Contents are as follows: - Introduction to the Vercel hosting platform - Deploying the empty Next.js project from the previous article on Vercel - Continuous deployment with Vercel x Github CI/CD --- ## Although we haven't developed anything yet, let's deploy the website first! Typically, "deployment" is the last step in website development. However, due to different environments, a website that runs well on your computer may not work as smoothly on your chosen hosting platform. If you develop everything at once and then deploy, only to find that it cannot be set up, it could be more difficult to trace the problem since the project is larger. The issue could be with package support, programming language versions, or programming practices, among other things! If your project doesn't need to be very secretive, it's recommended to deploy early and clear the deployment process as soon as possible. Get the blog online early and regularly check it during the development process. If any problems arise, you'll know they can only occur in the most recently modified areas, making it faster to find the root cause. So, let's get the blog deployed online first! ## Vercel: The Best Hosting Platform Experience for Developers [Vercel](https://vercel.com/home) is the company that developed [Next.js](https://nextjs.org/), and they also offer a platform named "Vercel" for deploying websites. (Hereafter, when we mention Vercel, we are referring to their hosting platform.) ![Vercel](https://i.imgur.com/3BIOqos.jpg) The Vercel platform supports deployment for all mainstream frontend frameworks, including Next.js, React.js, Vue.js, Nuxt.js, Anguer, Svelte, Hugo, Gatsby, etc. Vercel also supports various types of frontend projects, including pure static websites after packaging, Client-side Rendering websites, Server-side Rendering websites, Serverless functions. Lastly, Vercel also has a built-in CDN for faster webpage loading and Github CI/CD integration. Excluding the lack of a Database feature, Vercel provides everything a modern frontend project needs. We just need to focus on writing code to implement logic, and Vercel will handle the rest, such as architecture and scalability issues. Vercel's pricing ([Pricing](https://vercel.com/pricing)) is also very generous. Personal non-commercial projects with monthly traffic not exceeding 100GB are completely free under normal use, which is more than enough for a personal blog. In summary, Vercel is an excellent hosting platform for blogs and Side Projects, offering an exceptional developer experience with minimal setup required. Even if you need to migrate to another platform in the future, there's no transfer cost; you can just redeploy the same code onto the new platform. ## Deploying a Next.js Project on Vercel ### Code In the previous article, we created a basic Next.js project on our computer. The code is in the Github repo eason-dev/nextjs-tailwind-contentlayer-blog-starter in the day02-init-nextjs branch, link as follows: https://github.com/eason-dev/nextjs-tailwind-contentlayer-blog-starter/tree/day02-init-nextjs ### Vercel x Github CI/CD Vercel offers Github CI/CD functionality. CI/CD stands for Continuous Integration and Continuous Deployment, which means automatically deploying your code into a website without pressing any buttons or issuing any commands, super convenient! We will use this method for automatic website deployment. ### Putting Next.js on Github First, you need to push your Next.js project code to your Github account. For example, I created the [eason-dev/nextjs-tailwind-contentlayer-blog-starter](https://github.com/eason-dev/nextjs-tailwind-contentlayer-blog-starter) repo. ### Register a Vercel Account Next, go to the [Vercel registration page](https://vercel.com/signup) and sign up and log in using your Github account, or you can register and log in using another method, but you'll need to bind your Github account later. ### Create a New Project on Vercel Dashboard from Github Repo Then, go to the [Vercel Dashboard ](https://vercel.com/dashboard) page, click on the button to create a new project (Add new... -> Project): ![New Project](https://i.imgur.com/ge96JN0.jpg) In the next screen, if your account is already bound with Github, you'll see all your Github projects listed here. Select the project you want to deploy and click the "Import" button: ![Import](https://i.imgur.com/gTd0eiq.jpg) Next, do some basic setup. You don't need to change anything here, as Vercel will automatically detect the project's content (frameworks used and packaging commands, etc.). Just click the "Deploy" button to start the deployment: ![Settings](https://i.imgur.com/ep7al5e.jpg) Vercel will start deploying your project, which takes about a minute: ![Deploying](https://i.imgur.com/SAxAIqQ.jpg) When you see the confetti effect, it means the deployment was successful! ![Success](https://i.imgur.com/Uc1vb2n.jpg) Vercel will automatically assign an XXX.vercel.app domain to your project. Clicking on the large box in the middle allows you to browse your webpage, congratulations! For example, my project would automatically be deployed at this URL: https://nextjs-tailwind-contentlayer-blog-starter.vercel.app/ ### Github CI/CD Remember CI/CD mentioned earlier? Once we complete the process just now, we also finish setting up CI/CD! In the future, when you push code to the project repo, Vercel will automatically detect it and initiate a new deployment round, deploying the latest code. When you push to the main branch, new changes will be deployed to Production for all readers to use. When you push to any other branch, Vercel will deploy it as a Preview site. It will automatically assign a random URL to this change and email notify you, allowing you to test whether the changes meet expectations. So, in the future, we almost don't need to go back to the Vercel Dashboard to operate or issue any commands. We just need to finish developing new features, push them onto Github, and a few minutes later, we can see the latest changes on the website, very convenient! ## Conclusion & Next Article Now we understand the Vercel hosting platform and have set up our Next.js project on it, and it will continue to deploy based on the latest changes in Github. In the next article, we will continue to modify the Next.js project's code, doing some basic settings for ESLint, Prettier, TypeScript, etc.! --- # 將 Next.js 專案部署上 Vercel 平台 - Modern Next.js Blog 系列 #03 - URL: https://easonchang.com/zh-TW/posts/nextjs-to-vercel - Date: 2022-09-18T00:00:00.000Z - Language: zh-TW - Description: Vercel 架站平台介紹、將上一篇的 Next.js 空專案部署上 Vercel,並串接 Github CI/CD > 本文同步發佈於 [it 邦幫忙 2022 iThome 鐵人賽](https://ithelp.ithome.com.tw/articles/10293504) 這篇文章會介紹 Vercel 架站平台、如何把 Next.js 專案部署上去、以及綁定 Github 實現持續部署。 內容如下: - Vercel 架站平台介紹 - 將上一篇做完的 Next.js 空專案部署上 Vercel - Vercel x Github CI/CD 持續部署 --- ## 雖然我們啥都還沒開發,但先來部署網站吧! 通常來說「部署」是網站開發的最後一步。 但因為環境不同,在你電腦上跑得好好的網站,未必能在你選擇的架站平台上運作順利。 如果一次全部開發完再部署上去,最後發現架不起來,因為專案大了,要追查問題會比較困難,問題可能出在套件支援度、程式語言版本、或程式寫法等地方,任何地方! 如果你將開發的專案不需要非常保密的話,建議越早部署、打通部署流程越好。提早把部落格上線,並在開發過程中定期上來玩玩看,如果發現問題的話,就知道問題只會出現在最近幾個有修改的地方,能更快找到問題根源。 因此讓我們先把部落格部署上線吧! ## Vercel:開發者體驗最棒的架站平台 [Vercel](https://vercel.com/home) 是開發 [Next.js](https://nextjs.org/) 的公司,他們也提供了同名的「Vercel」平台,能讓我們部署網站上去。 (文章後續講到 Vercel,都是指他們的架站平台。) ![Vercel](https://i.imgur.com/3BIOqos.jpg) Vercel 平台支援所有主流前端框架部署,包含 Next.js、React.js、Vue.js、Nuxt.js、Anguer、Svelte、Hugo、Gatsby 等。 Vercel 也支援各種渲染類型的前端專案,包含打包完的純靜態網站、Client-side Rendering 網站、Server-side Rendering 網站、Serverless function。 最後 Vercel 也內建讓網頁讀取速度更快的 CDN,和串接 Github CI/CD 功能。 除開沒有資料庫(Database)功能,一個現代前端專案該有的功能,Vercel 都提供了。並且我們只需要專心寫 code 實現邏輯,剩下的架構、擴展性問題 Vercel 都會幫我們處理。 Vercel 費用([Pricing](https://vercel.com/pricing))也很佛心,個人非商業性專案,每月流量不超過 100GB 的話,正常使用是完全免費的,以個人部落格來說綽綽有餘。 總結來說 Vercel 對於部落格、Side Project 來說,是非常好的架站平台,開發者體驗極佳,不需要太多設定,未來即使要搬遷到其他平台也沒啥轉移成本,同一份 Code 重新部署上其他平台即可。 ## 將 Next.js 專案部署上 Vercel ### Code 上一篇文章我們在自己電腦上建立的基本 Next.js 專案,程式碼在 Github 上 eason-dev/nextjs-tailwind-contentlayer-blog-starter repo 的 day02-init-nextjs branch,連結如下: https://github.com/eason-dev/nextjs-tailwind-contentlayer-blog-starter/tree/day02-init-nextjs ### Vercel x Github CI/CD Vercel 提供了 Github CI/CD 功能。CI/CD 指的是 Continuous Integration 持續整合、Continuous Deployment 持續部署,講人話就是自動把 code 部署成網站,不用按任何按鈕或下任何指令,超方便! 我們會使用這方式來自動部署網站。 ### 將 Next.js 放上 Github 首先你需要將 Next.js 專案程式碼推上自己的 Github 帳號上,以我為例我是建成 [eason-dev/nextjs-tailwind-contentlayer-blog-starter](https://github.com/eason-dev/nextjs-tailwind-contentlayer-blog-starter) repo。 ### 註冊 Vercel 帳號 接著進到 [Vercel 註冊頁面](https://vercel.com/signup),使用 Github 帳號來註冊並登入,或是用其他方式註冊登入也行,但後續需要另外綁定 Github 帳號。 ### 在 Vercel Dashboard 建立新專案,從 Github repo 建立 接著進到 [Vercel Dashboard](https://vercel.com/dashboard) 頁面,按下右上角建立新專案按鈕(Add new... -> Project): ![New Project](https://i.imgur.com/ge96JN0.jpg) 下個畫面如果你的帳號跟 Github 綁定完了,就會看到所有 Github 專案列在這裡,選取你要部署的專案,按下「Import」按鈕: ![Import](https://i.imgur.com/gTd0eiq.jpg) 再來做一些基礎設定,這邊不需要修改什麼,Vercel 都會自動偵測專案內容(使用的框架和打包指令等),直接按「Deploy」按鈕開始部署: ![Settings](https://i.imgur.com/ep7al5e.jpg) Vercel 就會開始部署你的專案了,過程約一分鐘: ![Deploying](https://i.imgur.com/SAxAIqQ.jpg) 等他跑完後,看到禮砲特效就代表部署成功了! ![Success](https://i.imgur.com/Uc1vb2n.jpg) Vercel 會自動分配一個 XXX.vercel.app 的網域給你的專案,點選中間的大框框就可以瀏覽你的網頁了,恭喜你! 以我的專案為例,我的專案會自動被部署在這個網址上: https://nextjs-tailwind-contentlayer-blog-starter.vercel.app/ ### Github CI/CD 還記得前面提過 CI/CD 嗎?當我們做完剛剛那套流程後,我們也完成 CI/CD 的設定了!未來當你 push code 到專案 repo 上時,Vercel 都會自動偵測到,並執行新一輪部署,將最新的 code 部署上去。 推到 main branch 時,新改動會被部署成 Production,給所有讀者使用。 而推到其他任何 branch 時,Vercel 則會部署成 Preview site,他會自動分配一個隨機網址給這個改動,並 Email 通知你,讓你能在上面測試改動是否符合預期。 所以未來我們幾乎不需要回到 Vercel Dashboard 做操作,或下任何指令。我們只需要開發完新功能,Push 上 Github,幾分鐘後就能在網站看到最新的改動了,非常方便! ## 小結&下一篇 現在我們瞭解了 Vercel 架站平台,和把 Next.js 專案架設上去了,並且會根據 Github 最新的改動持續部署。 下一篇,我們會繼續改 Next.js 專案的 code,做一些 ESLint、Prettier、TypeScript 等基礎設定! --- # Introduction to Front-End Development Ecosystem & Setting Up a Next.js Project - Modern Next.js Blog Series #02 - URL: https://easonchang.com/posts/frontend-intro-create-nextjs - Date: 2022-09-17T00:00:00.000Z - Language: en - Description: Before starting the blog, let's understand the front-end development ecosystem, install the JS package managers npm and pnpm, and set up a basic Next.js front-end project > This article is also published on [iT 邦幫忙 2022 iThome Ironman Contest](https://ithelp.ithome.com.tw/articles/10292743) ### Introduction to Front-End Development Ecosystem & Setting Up a Next.js Project - Modern Next.js Blog Series #02 Before we start building our blog, let's first understand the front-end development ecosystem, install the JavaScript package managers npm and pnpm, and then create a basic Next.js front-end project and open it on our computer! In this article, we'll cover: - The building-blocks style of web development and the JS package ecosystem - Introduction to the npm JavaScript package repository - Installing pnpm - Creating a Next.js project --- ## Building-Blocks Style of Front-End Web Development The web development ecosystem is very mature. There are endless web development frameworks and various functional packages available for us to use, with new ones emerging every day. It's impossible to keep up with everything. Therefore, in modern web development, a significant amount of time is spent finding packages that meet our needs, learning how to use them, integrating them into our projects, and then customizing them. It's like building with blocks, combining various packages until we have a complete website. ## npm: A Treasure Trove of JavaScript Packages JavaScript is the primary language of web development. JS packages developed by developers worldwide are hosted on [npm (Node Package Manager)](https://www.npmjs.com/). Once npm is installed on our computer, we can install any package from the platform with a single command. For example, if we want to add QR code generation functionality to our website and use the [qrcode](https://www.npmjs.com/package/qrcode) package, we simply follow the instructions on the package's page and enter: ```shell npm install qrcode ``` This command installs the package into our website! Then, by following the instructions further, we can modify our web page to complete the setup! ![node-qrcode](https://i.imgur.com/Qfwew7U.jpg) ## pnpm: A More Efficient Package Manager, Used in This Series npm is a platform for JS packages, and there are many package managers that can install packages from npm. Currently, there are three mainstream package managers: npm, [yarn](https://yarnpkg.com/), and [pnpm](https://pnpm.io/zh-TW/). We will use pnpm in this series because it is the newest, offers faster package installation, and uses less disk space. ![pnpm](https://i.imgur.com/Ff418og.jpg) ### Installing pnpm The pnpm official website provides various installation methods: https://pnpm.io/zh-TW/installation I recommend [installing using npm](https://pnpm.io/zh-TW/installation#%E4%BD%BF%E7%94%A8-npm-%E5%AE%89%E8%A3%9D) by entering the following command: ``` npm install -g pnpm ``` After installation, you can check the pnpm version to verify the installation was successful: ```bash pnpm -v # 7.11.0 ``` ## Creating a Next.js Project The Getting Started page on the Next.js official website explains the installation methods and basic usage of Next.js: https://nextjs.org/docs/getting-started The most convenient way is to use the official create-next-app tool to generate the entire project structure. Our series will be written in TypeScript, so the command to create a project is as follows: ```shell pnpm create next-app --typescript ``` After entering the command, it will ask for the project name. Enter a name you like, and press Enter. This will create a new Next.js project in your current directory! ![create-next-app](https://i.imgur.com/6Bfwz84.png) When opened with VSCode or your preferred editor, the project will include these files: ![files](https://i.imgur.com/RFJ1kAk.jpg) Let's try running it! ### Starting the Next.js Project Several executable commands are predefined in package.json, and we can use pnpm to execute them. To run the project on your computer, enter this command: ```shell pnpm dev ``` This will open a web server on your computer, where you can see the current web page of the project. Usually, it runs on http://localhost:3000/. Browsing this page, you should see the following: ![nextjs-project](https://i.imgur.com/vTqTtMx.png) Congratulations on successfully creating a new Next.js project! ## Source Code The source code for this series will be hosted on this Github repo: [eason-dev/nextjs-tailwind-contentlayer-blog-starter](https://github.com/eason-dev/nextjs-tailwind-contentlayer-blog-starter) And every article that includes code will have a corresponding branch, marking the progress of each article. The source code so far is here: [eason-dev/nextjs-tailwind-contentlayer-blog-starter's **day02-init-nextjs** branch](https://github.com/eason-dev/nextjs-tailwind-contentlayer-blog-starter/tree/day02-init-nextjs) ## Conclusion & Next Article We've successfully installed the pnpm package manager and created a new Next.js project. In the next article, although we don't have any functionality yet, we'll introduce the Vercel hosting platform and deploy our Next.js project on it! --- # 簡介前端開發生態&建立 Next.js 專案 - Modern Next.js Blog 系列 #02 - URL: https://easonchang.com/zh-TW/posts/frontend-intro-create-nextjs - Date: 2022-09-17T00:00:00.000Z - Language: zh-TW - Description: 開幹部落格之前,先來了解前端開發生態、安裝 JS 套件管理器 npm 及 pnpm,並建立基礎 Next.js 前端專案 > 本文同步發佈於 [it 邦幫忙 2022 iThome 鐵人賽](https://ithelp.ithome.com.tw/articles/10292743) 開工之前,讓我們先來了解前端開發生態,以及 JS 套件管理器 pnpm,接著建立基礎 Next.js 前端專案,並在電腦上打開它! 這篇文章會講這些: - 堆積木式的網頁開發、JS 套件生態系 - npm JavaScript 套件庫介紹 - pnpm 安裝 - Next.js 專案建立 --- ## 堆積木式的前端網頁開發 網頁開發生態系已經非常成熟了。有無窮無盡的網頁開發框架(Web Framework)和各種功能的套件(Package)供我們使用,每天也不斷有新東西誕生,追也追不完。 所以在現代要開發網站,有一大部分時間是花在尋找符合需求的套件,學習它的用法,並導入自己的專案,最後在做些客製化。就像堆積木一樣,組合各種套件,最後堆成一個完整的網站。 ## npm:JavaScript 套件寶庫 網頁開發的主要語言是 JavaScript。全世界開發者們開發的 JS 套件,都會放上 [npm (Node Package Manager)](https://www.npmjs.com/) 這個平台。 我們只要在電腦上安裝了 npm,輸入一行指令,就能把上面的任何套件安裝到網頁裡使用。 舉個例子,如果我們想在網頁裡加入 QRCode 生成功能,想使用 [qrcode](https://www.npmjs.com/package/qrcode) 這個套件 那我們只要依照套件介紹頁指令,輸入: ```shell npm install qrcode ``` 就能將此套件安裝進我們的網站裡了!接著再繼續根據介紹,修改你的網頁就完成了! ![node-qrcode](https://i.imgur.com/Qfwew7U.jpg) ## pnpm:更高效的套件管理器,這系列會使用它 npm 是一個 JS 套件平台,有許多套件管理器都能從 npm 平台上安裝套件。 目前主流有三個套件管理器:npm、[yarn](https://yarnpkg.com/)、[pnpm](https://pnpm.io/zh-TW/),我們這系列會使用 pnpm,因為 pnpm 是裡面最新的,安裝套件速度更快,硬碟空間佔用更少。 ![pnpm](https://i.imgur.com/Ff418og.jpg) ### 安裝 pnpm pnpm 官網的安裝說明頁,有提供各種安裝方式: https://pnpm.io/zh-TW/installation 我建議[使用 npm 安裝](https://pnpm.io/zh-TW/installation#%E4%BD%BF%E7%94%A8-npm-%E5%AE%89%E8%A3%9D),輸入以下指令來安裝: ``` npm install -g pnpm ``` 安裝完後可以檢查 pnpm 版本來驗證是否安裝成功: ```bash pnpm -v # 7.11.0 ``` ## 建立 Next.js 專案 Next.js 官網的 Getting Started 頁面,有說明 Next.js 安裝方法及基本使用: https://nextjs.org/docs/getting-started 最方便的方式是使用官方提供的 create-next-app 工具,來生成整個專案結構。 並且我們這系列的程式都會以 TypeScript 來撰寫,所以生成專案的指令如下: ```shell pnpm create next-app --typescript ``` 輸入指令後它會問你你希望專案名稱叫什麼,輸入一個喜歡的名稱,以我的例子名稱叫做「nextjs-tailwind-contentlayer-blog-starter」,輸入完後按 Enter,他就會在你當前目錄裡面新增一個全新的 Next.js 專案了! ![create-next-app](https://i.imgur.com/6Bfwz84.png) 用 VSCode 或你喜歡的編輯器打開後,裡面會包含這些檔案: ![files](https://i.imgur.com/RFJ1kAk.jpg) 讓我們嘗試執行它看看! ### 啟動 Next.js 專案 package.json 裡面預先定義了幾個可執行的指令,我們可以使用 pnpm 來執行它們。 在你們電腦上要執行它的話,輸入這個指令: ```shell pnpm dev ``` 就會在電腦上開啟一個網頁伺服器,可以看到專案目前的網頁。通常他會跑在 http://localhost:3000/ 這頁面,用瀏覽器瀏覽他,你應該會看到下圖的畫面: ![nextjs-project](https://i.imgur.com/vTqTtMx.png) 這就是 Next.js 初始專案的樣子!恭喜你成功建立了一個全新的 Next.js 專案 ## Source code 這系列的原始碼都會放在這個 Github repo 上: [eason-dev/nextjs-tailwind-contentlayer-blog-starter](https://github.com/eason-dev/nextjs-tailwind-contentlayer-blog-starter) 並且每一篇有寫到 code 的文章,都會有一個對應的 branch,標出每篇的進度。 目前為止的原始碼在這裡: [eason-dev/nextjs-tailwind-contentlayer-blog-starter 的 **day02-init-nextjs** branch](https://github.com/eason-dev/nextjs-tailwind-contentlayer-blog-starter/tree/day02-init-nextjs) ## 小結&下一篇 我們成功安裝 pnpm 套件管理器,並建立了全新的 Next.js 專案。 下一篇,雖然我們現在啥功能都沒有,但我們會先介紹 Vercel 架站平台,並將我們的 Next.js 專案部署上去! --- # Building a Stunning Personal Blog from Scratch Series Introduction - Modern Next.js Blog Series #01 - URL: https://easonchang.com/posts/modern-nextjs-blog-intro - Date: 2022-09-16T00:00:00.000Z - Language: en - Description: In this series, we will start from scratch and use the most modern front-end technologies to build your stunning personal blog over 30 days! Introducing the target audience, result demo, content planning, and the technologies used > This article is also published on [iT 邦幫忙 2022 iThome Ironman Competition](https://ithelp.ithome.com.tw/articles/10291960) Welcome to the 30-day **"Building a Stunning Personal Blog from Scratch with Modern Technologies like Next.js, ContentLayer, i18next"** series! In this series, we will start from scratch and use the most modern front-end technologies to build your stunning personal blog over 30 days! This article will introduce the series, including the following contents: - Target audience - Result demo - Open source Github repo - Blog feature list - List of technologies used - 30-day content planning - About me --- ## Target Audience This series is practice-oriented, including a large amount of front-end code, from the most basic environment setup, cool feature development, to finally deploying the blog online, step by step building the blog site. The suitable audience includes: - Those who already have some front-end development experience - Those who want to set up their own blog - Those who love tinkering and customization - Those who just finished the Ironman competition and want to establish a personal blog to post articles It would be great if you have the following prior knowledge: - Basic understanding of HTML, CSS - Basic understanding of JavaScript, TypeScript - Familiarity with Git operations - Basic understanding of React.js - Basic understanding of Next.js This series will not go into all the details of the above languages/technologies, but will clearly explain the parts used during the development process and will include various reference materials, so don't worry! If you have never touched front-end development but still want to make a cool personal blog, you are also welcome to read on! Learning by doing is the fastest way to learn. Each article will also include the complete code at the end. Follow along first, then understand it line by line, and then modify it to your liking, and you'll have learned! ## So what does this blog look like? ### Result Demo You can play with the final result here: https://nextjs-tailwind-contentlayer-blog-starter.vercel.app/ ### Demo Screenshots Homepage: ![Homepage](https://i.imgur.com/edfiyeD.jpg) Article page and dark mode: ![Article page and dark mode](https://i.imgur.com/WboZrmY.jpg) Command palette: ![Command palette](https://i.imgur.com/60aFPZH.jpg) ### Source Code Github Repo The final result is open-sourced as the "[Next.js Tailwind Contentlayer Blog Starter](https://github.com/eason-dev/nextjs-tailwind-contentlayer-blog-starter)" template project, you can see all the code from the 30 days here: https://github.com/eason-dev/nextjs-tailwind-contentlayer-blog-starter ![Github repo](https://i.imgur.com/vYRUaL3.jpg) Feel free to use it to build your own technical blog by clicking the green "Use this template" or "Fork" button! If this series has helped you, I also hope you can give it a "Star" to support ~ If you find any bugs or want to add more features, feel free to open issues and PRs to collaborate! ### Blog Feature List In addition to the basic blog functions, we will also implement the following cool features: - Writing with local Markdown / MDX files - Excellent web performance - SEO friendly - Dark mode - Multilingual in Chinese and English - Command Palette - Comment system - Page progress bar - RSS Feed - Sitemap - Readable code blocks ### Technologies Used The main technologies used throughout the series are as follows: - [Next.js](https://nextjs.org/): A modern full-stack framework - [Vercel](https://vercel.com/home): Website hosting - [Contentlayer](https://github.com/contentlayerdev/contentlayer): Markdown/MDX article processing - [Tailwind CSS](https://tailwindcss.com/): UI styles - [next-i18next](https://github.com/i18next/next-i18next): Multilingual support - [next-seo](https://github.com/garmeeh/next-seo): SEO meta tags - [kbar](https://github.com/timc1/kbar): Command palette - And more... ### 30-Day Content Planning Here is a preliminary plan for the 30-day series: 1. Series Introduction (This article) 2. Environment Setup 3. Basic Page Layout 4. Adding Styling with Tailwind CSS 5. Routing and Navigation 6. Setting Up Contentlayer for Blog Articles 7. Listing Articles 8. Article Page 9. Dark Mode and Theme Switching 10. Multilingual Support 11. SEO Optimization 12. Command Palette 13. Comment System 14. Performance Optimization 15. Deploying Your Blog 16. ... Some topics may be combined or split into multiple articles depending on the content. I will adjust the plan according to the actual situation and feedback. ### About Me Finally, let me introduce myself briefly. I am a front-end developer who loves to share knowledge through writing. I started my blog on various platforms a few years ago and have always wanted to build my own blog site. This series is also a record of my learning process. I hope it can help more people who have the same aspiration. If you have any questions or suggestions during the series, feel free to leave a comment below or contact me on social media. Let's embark on this exciting journey together! --- # 「從零開始打造炫砲個人部落格」系列簡介 - Modern Next.js Blog 系列 #01 - URL: https://easonchang.com/zh-TW/posts/modern-nextjs-blog-intro - Date: 2022-09-16T00:00:00.000Z - Language: zh-TW - Description: 這系列我們會在 30 天內從零開始,用最現代的前端技術,做出你的炫砲個人部落格!簡介本系列的目標讀者、成果 Demo、內容規劃、及用到的技術 > 本文同步發佈於 [it 邦幫忙 2022 iThome 鐵人賽](https://ithelp.ithome.com.tw/articles/10291960) 歡迎來到 30 天 **「從零開始打造炫砲個人部落格,使用 Next.js、ContentLayer、i18next 等現代技術」** 系列! 這系列我們會在 30 天內從零開始,用最現代的前端技術,做出你的炫砲個人部落格! 這篇文章會向你簡介本系列,包含以下內容: - 目標讀者 - 成果 Demo - 開源 Github repo - 部落格功能列表 - 使用的技術列表 - 30 天內容規劃 - 關於我 --- ## 目標讀者 這系列是實作導向,會包含大量前端程式碼,從最基礎的環境配置、酷炫功能開發、到最後將部落格部署上線,一步步架出部落格網站。 適合的讀者群如下: - 已經有些前端開發經驗 - 想架設自己的部落格 - 熱愛折騰與客製化 - 剛挑戰完鐵人賽,想順便建立個人部落格放文章 如果有以下先備知識會很棒: - HTML、CSS 略懂 - JavaScript、TypeScript 略懂 - Git 操作 - React.js 略懂 - Next.js 略懂 這系列不會細講上面這些語言、技術的全部細節,但開發過程中有用到的部分都會清楚說明,也會附上各種參考資料,所以不用擔心! 如果你完全沒接觸過前端開發,但還是想做一個酷酷個人部落格的話,也歡迎往下讀!從做中學才能學得更快。 每一篇文章最後也都會附上完整程式碼,先照著做、再一行行理解,接著修改成你喜歡的樣子,這樣就學會了! ## 所以這個部落格長怎樣? ### 成果 Demo 你可以在這裡玩玩最終成果: https://nextjs-tailwind-contentlayer-blog-starter.vercel.app/ ### 成果截圖 首頁: ![首頁](https://i.imgur.com/edfiyeD.jpg) 文章頁與深色模式: ![文章頁與深色模式](https://i.imgur.com/WboZrmY.jpg) 指令面板: ![指令面板](https://i.imgur.com/60aFPZH.jpg) ### 原始碼 Github repo 最終成果有開源成「[Next.js Tailwind Contentlayer Blog Starter](https://github.com/eason-dev/nextjs-tailwind-contentlayer-blog-starter)」template 專案了,你可以在這兒看到 30 天所有程式碼: https://github.com/eason-dev/nextjs-tailwind-contentlayer-blog-starter ![Github repo](https://i.imgur.com/vYRUaL3.jpg) 歡迎使用它來建立你自己的技術部落格,點綠色「Use this template」或「Fork」按鈕就行了! 如果這系列有幫助到你的話,也希望你能點個「Star」支持一下~ 發現任何 bug 或想加入更多 feature 的話,也歡迎開 issue 和 PR 一起合作! ### 部落格功能列表 除了做出基本部落格功能,我們還會實作下列這些酷炫 feature: - 編輯 local Markdown / MDX 檔案來寫作 - 網頁效能極佳 - SEO 友善 - 深色模式 - 中文、英文多語系 - 指令面板 Command Palette - 留言系統 - 換頁進度條 - RSS Feed - Sitemap - 好讀的程式碼區塊 ### 使用到的技術 全系列主要會使用到的技術如下: - [Next.js](https://nextjs.org/):現代全端框架 - [Vercel](https://vercel.com/home):網站託管 - [Contentlayer](https://github.com/contentlayerdev/contentlayer):Markdown/MDX 文章處理 - [Tailwind CSS](https://tailwindcss.com/):UI 樣式 - [next-i18next](https://github.com/i18next/next-i18next):多語系 - [next-seo](https://github.com/garmeeh/next-seo):SEO meta tags - [kbar](https://github.com/timc1/kbar):指令面板 - [giscus](https://github.com/giscus/giscus):留言系統 - [nprogress](https://github.com/rstacruz/nprogress):換頁進度條 - [TypeScript](https://www.typescriptlang.org/):更扎實的 JavaScript - [ESLint](https://eslint.org/), [Prettier](https://prettier.io/):統一程式碼格式 ### 部落格架站費用 這系列全篇用到的開發與在架站資源都是**「免費」**的,只有 2 處可能會需要付費: 1. [第 3 篇](/posts/nextjs-to-vercel) 介紹的 [Vercel](https://vercel.com/home) 架站平台,網站流量很大時(每月超過 100 GB)需要升級[付費方案](https://vercel.com/pricing),每月 20 美金。 但部落格程式碼都是你自己的,需要時可以輕易搬移全站到其他伺服器。 2. 網域名稱 Domain name。價格依照域名稀有程度有所不同。 我自己在 [GoDaddy](https://tw.godaddy.com/) 購買的 [easonchang.com](https://easonchang.com/) 價格是每年 800 元台幣左右。 不想買域名也行,Vercel 會配給你 `XXX.vercel.app` 的域名,`XXX` 可以自訂,只要沒被別人使用過都行,像是我的 blog 也在 [easonchang.vercel.app](https://easonchang.vercel.app/) 上。 ## 30 天詳細內容規劃 這系列 30 天大致上分成 3 個階段: 第 1 ~ 13 天,我們會先做出了一個有基本功能、且不錯看的部落格。 第 14 ~ 19 天,做完程式碼區塊、SEO 處理、sitemap、RSS 後,就完成一個技術部落格該有的一切條件了。 而最後第 20 ~ 30 天,則會繼續加入更多炫砲且實用的功能,讓你的部落格脫穎而出! 這 30 天目錄如下: 1. 「從零開始打造炫砲個人部落格」系列簡介(You are here) 2. [簡介前端開發生態&建立 Next.js 專案](/posts/frontend-intro-create-nextjs) 3. [將 Next.js 專案部署上 Vercel 平台](/posts/nextjs-to-vercel) 4. [ESLint、Prettier、TypeScript 等 Next.js 專案基礎設定](/posts/nextjs-dev-env) 5. [Markdown 簡介 & 安裝 ContentLayer](/posts/markdown-contentlayer) 6. [首頁功能實作,加入 Contentlayer 文章列表](/posts/contentlayer-post-list) 7. [文章內頁功能實作,渲染 Markdown 文章內容](/posts/contentlayer-post-content) 8. [讓 Contentlayer 文章支援 MDX](/posts/contentlayer-mdx) 9. [安裝 Tailwind CSS 和相關 ESLint、Prettier 設定](/posts/tailwindcss-setup) 10. [Dark Mode 深色模式支援,使用 Tailwind CSS、next-themes](/posts/darkmode-tailwindcss) 11. [全站樣式切版](/posts/blog-site-style) 12. [首頁樣式切版](/posts/blog-homepage-style) 13. [文章內頁樣式切版](/posts/blog-post-page-style) 14. [加入程式碼 Syntax Highlighting,使用 rehype-prism-plus](/posts/rehype-prism-plus-syntax-highlighting) 15. [加入程式碼區塊標題,使用 rehype-code-titles](/posts/rehype-code-titles) 16. [加入程式碼區塊「複製按鈕」,使用客製化 MDX 元件](/posts/code-copy-button) 17. [加入 Open Graph、LD-JSON 等 SEO meta data](/posts/next-seo-meta-data) 18. [使用 next-sitemap 生成 Sitemap](/posts/next-sitemap) 19. [使用 feed 生成 RSS Feed](/posts/rss-feed) 20. [為內文小標題加入 anchor 錨點連結](/posts/post-heading-anchor) 21. [強化內文連結換頁速度、加入外部連結 icon](/posts/post-custom-link) 22. [圖片效能最佳化,使用 Next.js Image、plaiceholder、客製 MDX 元件](/posts/post-custom-image) 23. [使用 nprogress 加入換頁進度條](/posts/nprogress) 24. [在 MDX 文章側邊加入目錄](/posts/post-side-toc) 25. [使用 giscus 在 Next.js 加入留言系統](/posts/giscus-comment-system) 26. [使用 kbar 加入 Command Palette 指令面板](/posts/kbar-command-palette) 27. [在 kbar Command Palette 實作文章搜尋](/posts/kbar-post-search) 28. [使用 next-i18next 實作中英文多語系](/posts/next-i18next-multilingual) 29. [在 Next.js Contentlayer blog 實作舊路徑轉址](/posts/contentlayer-redirection) 30. [總結與回顧:一個炫砲技術部落格的誕生](/posts/modern-nextjs-blog-summary) --- ## 關於我 Eason Chang 我是 [Eason Chang](https://easonchang.com/about),最愛挖坑給自己跳的全端工程師。 我從 7 年前開始接觸前端開發,最近正在往後端邁進。 如果這系列文章對你有幫助,或是想看看我的其他文章,歡迎透過各種方式追蹤我,一起交流切磋! > 個人部落格:https://easonchang.com/ > > Facebook:https://www.facebook.com/easondev > > Twitter (X):https://x.com/easondev > > Github:https://github.com/eason-dev > > LinkedIn:https://www.linkedin.com/in/easonchang101/ 我自己的個人部落格也是用這 30 天開發的「[Next.js Tailwind Contentlayer Blog Starter](https://github.com/eason-dev/nextjs-tailwind-contentlayer-blog-starter)」模板架起來的。 Repo 在這兒:[https://github.com/eason-dev/easonchang.com-next](https://github.com/eason-dev/easonchang.com-next) ,也歡迎來逛逛按個 Star ~ ## 小結&下一篇 現在我們暸解了 Modern Next.js Blog 系列 30 天會經歷的事。 準備好一起打造酷酷部落格了嗎?下一篇,讓我們了解什麼是 Next.js,以及建立 Next.js 專案! Let's gogo! --- # Solving CORS Cross-Origin Issues: Bypass Them with the Built-in Proxy in Create React App - URL: https://easonchang.com/posts/create-react-app-proxy - Date: 2022-08-12T20:58:00.000Z - Language: en - Description: How to set up the built-in Proxy server in Create React App to bypass Cross-Origin Resource Sharing (CORS) issues when calling APIs during development - Translation: AI-assisted When working on a [Create React App](https://create-react-app.dev/) project, have you ever happily finished building the UI, gone to hook up the backend API, and run into an error message like this: > Fetch API cannot load https://example.com/api/todos. No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://localhost:3000' is therefore not allowed access. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled. Congratulations, you've hit a CORS problem! ## A quick intro to CORS [Cross-Origin Resource Sharing (CORS)](https://developer.mozilla.org/zh-TW/docs/Web/HTTP/CORS) is a browser security mechanism that decides whether site A can use site B's resources. For example, whether site A can call site B's API, or embed site B's images. By default, no site can share resources with another — which is why you see a CORS error message when you call an external API from your React project. ## The correct way to handle CORS: set CORS Headers on the backend If the situation is your site A calling site B's API, you **cannot** solve CORS by changing site A alone. You have to change site B's configuration, adding an **Access-Control-Allow-Origin** header to the API Response that allows calls to this API from site A. How to set it differs from backend to backend — that's not the focus of this article, so I won't expand on it here. --- ## The way to bypass CORS during development: use the Create React App Proxy! Until the backend has set up the CORS Header, you can use the [Create React App Proxy](https://create-react-app.dev/docs/proxying-api-requests-in-development/) server feature to bypass the CORS restriction! (From here on, I'll abbreviate **Create React App** as **CRA**.) This only works during local development with `npm start` — it can't be used after the site is deployed, so in the end you still need to ask the backend to set the Header. ## How the CRA Proxy works Say you're developing site A at `http://localhost:3000` and need to call `https://example.com/api`. You'd probably write: ```js fetch('https://example.com/api') ``` Calling it directly hits the CORS problem — so let's just not call it directly! This is where a Proxy server can take a turn in the middle. Once we've set up the CRA Proxy (I'll cover how below), CRA also runs a **Proxy server** at `http://localhost:3000`. Now, if you rewrite the fetch API path to call localhost:3000 instead: ```js fetch('http://localhost:3000/api') // 或更簡單 fetch('/api') ``` When the CRA Proxy server receives the API request, it takes the request untouched and calls the real API `https://example.com/api`, gets the result, and returns it to you. With the Proxy in the middle, the CORS problem is gone. Because the API you're calling is no longer `example.com` but `localhost:3000` — and your page is also on `localhost:3000` — to the browser, calling yourself is perfectly safe. And the leg where the CRA Proxy calls `example.com` happens server-side, where there's no CORS problem. --- ## How to set up the CRA Proxy There are two ways to enable the CRA Proxy: one simple, one more complex but more flexible. ### Simple method 1: add "proxy" to package.json Same scenario — our goal is to proxy `http://localhost:3000/api` to `https://example.com/api` All you need is to **add a `proxy` property to the project's package.json**, with the target API domain as its value, like this: ```json:package.json "proxy": "https://example.com/api" ``` Rerun `npm start` and it takes effect — any API request to `localhost:3000` gets proxied to `example.com`. So this now successfully calls `https://example.com/api`: ```js fetch('/api') ``` ### Complex but flexible method 2: add src/setupProxy.js for custom logic If you have multiple APIs to proxy at once, need to put API paths in environment variables, or have more complex needs, then you want method 2. Method 2 **doesn't change package.json**. Instead, first install the **http-proxy-middleware** package: ```bash $ npm install http-proxy-middleware --save ``` Then add a file **src/setupProxy.js** with this content: ```js:src/setupProxy.js showLineNumbers const { createProxyMiddleware } = require('http-proxy-middleware') module.exports = function (app) { app.use( '/api', createProxyMiddleware({ target: 'https://example.com', changeOrigin: true, }) ) } ``` Rerun `npm start` and you're done! The effect is the same: `http://localhost:3000/api` gets proxied to `https://example.com/api` Under the hood, npm start uses Express to run the local server, and setupProxy.js lets us **slip a small piece of custom logic (middleware) into it to achieve the proxy effect**. #### Method 2 extended: proxying multiple APIs If you're calling multiple APIs at once and they all have CORS problems, method 2 can configure them all together. The extended version looks like this — edit **setupProxy.js**: ```js:src/setupProxy.js showLineNumbers {12-18} const { createProxyMiddleware } = require('http-proxy-middleware') module.exports = function (app) { app.use( '/api', createProxyMiddleware({ target: 'https://example.com', changeOrigin: true, }) ) app.use( '/my-api-2', createProxyMiddleware({ target: 'https://another-remote-api.com', changeOrigin: true, }) ) } ``` Now, besides the original `http://localhost:3000/api` proxying to `https://example.com/api`, `http://localhost:3000/my-api-2` also proxies to `https://another-remote-api.com/my-api-2`! #### Method 2 extended: putting API paths in environment variables You may already keep your API paths in environment variables (see the [CRA environment variables docs](https://create-react-app.dev/docs/adding-custom-environment-variables/#expanding-environment-variables-in-env)), like this: **.env** ```bash:.env REACT_APP_API_DOMAIN = 'https://example.com' REACT_APP_API_PATH = '/api' ``` Then in **setupProxy.js** you can use them like this: ```js:src/setupProxy.js showLineNumbers {5,7} const { createProxyMiddleware } = require('http-proxy-middleware') module.exports = function (app) { app.use( process.env.REACT_APP_API_PATH, createProxyMiddleware({ target: process.env.REACT_APP_API_DOMAIN, changeOrigin: true, }) ) } ``` That's all! Now you know how to use the Create React App Proxy to bypass CORS problems during development! --- ## Trouble Shooting: making setupProxy.js support TypeScript? I tried renaming setupProxy.js to setupProxy.ts and converting the code inside to TypeScript, but could never get it to run. From what I've researched so far, the CRA Proxy doesn't support TypeScript — the bottom of the [official CRA Proxy docs](https://create-react-app.dev/docs/proxying-api-requests-in-development/#configuring-the-proxy-manually) says setupProxy.js only supports Node.js JavaScript with pre-ES5 syntax, and I found a related [issue](https://github.com/facebook/create-react-app/issues/6794) asking whether it supports TypeScript, with no resolution. If you know a way to make TypeScript work, let me know! ## References - [Proxying API Requests in Development | Create React App](https://create-react-app.dev/docs/proxying-api-requests-in-development/) - [Cross-Origin Resource Sharing (CORS) - HTTP | MDN](https://developer.mozilla.org/zh-TW/docs/Web/HTTP/CORS) - [CORS 完全手冊(一):為什麼會發生 CORS 錯誤? - Huli](https://blog.huli.tw/2021/02/19/cors-guide-1/) --- # 解決 CORS 跨域請求問題,使用 Create React App 內建 Proxy 繞過它 - URL: https://easonchang.com/zh-TW/posts/create-react-app-proxy - Date: 2022-08-12T20:58:00.000Z - Language: zh-TW - Description: 如何設定 Create React App 內建的 Proxy server,來在開發階段繞過呼叫 API 遇到的 Cross-Origin Resource Sharing (CORS) 問題 開發 [Create React App](https://create-react-app.dev/) 專案時,開開心心刻完畫面後,要串接後端 API 時,你是否常看到這樣的錯誤訊息: > Fetch API cannot load https://example.com/api/todos. No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://localhost:3000' is therefore not allowed access. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled. 恭喜你碰上 CORS 問題了! ## 簡介 CORS 跨站資源共享 [跨來源資源共用(Cross-Origin Resource Sharing (CORS))](https://developer.mozilla.org/zh-TW/docs/Web/HTTP/CORS),是瀏覽器的安全機制,用來決定 A 網站能否使用 B 網站的資源。 例如能否從 A 網站呼叫 B 網站的 API,或嵌入 B 網站的圖片。 預設狀態下,各個網站之間都不能共享資源,所以從你的 React 專案呼叫外部 API 時,才會看到 CORS 錯誤訊息。 ## 處理 CORS 正確方法:由後端設定 CORS Headers 如果情境是你的 A 網站要呼叫 B 網站 API,你**沒辦法**只改 A 網站就解決 CORS。 你必須修改 B 網站的設定,在 API Response 新增 **Access-Control-Allow-Origin** header,在裡面允許從 A 網站呼叫此 API,設定方法不同後端有不同方法,本文重點不在這兒,就不展開了。 --- ## 開發時繞過 CORS 的方法:使用 Create React App 的 Proxy! 在後端還沒設定 CORS Header 前,你可以使用 [Create React App 的 Proxy](https://create-react-app.dev/docs/proxying-api-requests-in-development/) 代理伺服器功能,繞過 CORS 限制! (以下簡稱 **Create React App** 為 **CRA**) 這個方法只在使用 `npm start` 在 local 開發時有效,不能用在網站部署後,所以最終還是必須請後端設定 Header。 ## CRA 的 Proxy 原理 假設你在 `http://localhost:3000` 開發 A 網站,要呼叫 `https://example.com/api`。 你應該會這樣寫: ```js fetch('https://example.com/api') ``` 直接呼叫會遇到 CORS 問題,那我們不要直接呼叫就好了啊!此時就可以透過 Proxy server 來過一手。 當我們設定完 CRA 的 Proxy 後(後面會講怎麼設定),CRA 會在 `http://localhost:3000` 同時開啟一個 **Proxy server**,此時當你改寫 fetct 的 API 路徑,改成向 localhost:3000 呼叫時: ```js fetch('http://localhost:3000/api') // 或更簡單 fetch('/api') ``` CRA Proxy 伺服器收到 API 請求後,會把 request 原封不動拿去呼叫真正的 API `https://example.com/api`,得到結果後再回傳給你。 經過 Proxy 過一手後,就不會有 CORS 問題了。 因為你呼叫的 API 不再是 `example.com` 了,而是 `localhost:3000`,而你的網頁也在 `localhost:3000`,對瀏覽器來說呼叫自己是很安全的。 而從 CRA Proxy 呼叫 `example.com` 這段是從伺服器端呼叫的,伺服器端不會有 CORS 問題。 --- ## 設定 CRA Proxy 的方法 有兩種方法能開啟 CRA 的 Proxy,一個簡單,一個複雜但更有彈性。 ### 簡單方法 1:在 package.json 加入 "proxy" 來設定 一樣的情境,我們目標是把 `http://localhost:3000/api` proxy 到 `https://example.com/api` 只需要在專案的 **packaga.json 新增 `proxy` 屬性**就好,內容填上目標 API 網域,像這樣: ```json:package.json "proxy": "https://example.com/api" ``` 接著重新執行 `npm start` 後就生效了,對 `localhost:3000` 的任何 API request 都會被 proxy 到 `example.com`。 所以這樣寫就能成功呼叫 `https://example.com/api`: ```js fetch('/api') ``` ### 複雜但彈性的方法 2:新增 src/setupProxy.js 來客製化邏輯 如果你同時有多個 API 需要 proxy、需要把 API 路徑放到環境變數,或有更複雜的需求,那你需要方法 2。 方法 2 **不用改 package.json**,而是需要先安裝 **http-proxy-middleware** package: ```bash $ npm install http-proxy-middleware --save ``` 接著新增一個檔案 **src/setupProxy.js**,內容如下: ```js:src/setupProxy.js showLineNumbers const { createProxyMiddleware } = require('http-proxy-middleware') module.exports = function (app) { app.use( '/api', createProxyMiddleware({ target: 'https://example.com', changeOrigin: true, }) ) } ``` 接著重新 `npm start` 就完成了!效果一樣是把 `http://localhost:3000/api` proxy 到 `https://example.com/api` 原理是 npm start 是用 Express 開啟 local server,setupProxy.js 讓我們能**安插一小段自訂邏輯(middleware)進去,達成 proxy 效果**。 #### 方法 2 延伸:Proxy 多個 API 如果你同時要呼叫多個 API,都有 CORS 問題的話,用方法 2 就能同時做設定,擴充的寫法如下,修改 **setupProxy.js**: ```js:src/setupProxy.js showLineNumbers {12-18} const { createProxyMiddleware } = require('http-proxy-middleware') module.exports = function (app) { app.use( '/api', createProxyMiddleware({ target: 'https://example.com', changeOrigin: true, }) ) app.use( '/my-api-2', createProxyMiddleware({ target: 'https://another-remote-api.com', changeOrigin: true, }) ) } ``` 這樣除了原先 `http://localhost:3000/api` 會 proxy 到 `https://example.com/api`, `http://localhost:3000/my-api-2` 也會 proxy 到 `https://another-remote-api.com/my-api-2`! #### 方法 2 延伸:將 API 路徑放到環境變數 你可能已經將 API 路徑寫在環境變數了(參照 [CRA 環境變數文件](https://create-react-app.dev/docs/adding-custom-environment-variables/#expanding-environment-variables-in-env)),像下面這樣: **.env** ```bash:.env REACT_APP_API_DOMAIN = 'https://example.com' REACT_APP_API_PATH = '/api' ``` 在 **setupProxy.js** 裡就能這樣使用: ```js:src/setupProxy.js showLineNumbers {5,7} const { createProxyMiddleware } = require('http-proxy-middleware') module.exports = function (app) { app.use( process.env.REACT_APP_API_PATH, createProxyMiddleware({ target: process.env.REACT_APP_API_DOMAIN, changeOrigin: true, }) ) } ``` That's all!現在你知道怎麼使用 Create React App 的 Proxy 來在開發時繞過 CORS 問題了! --- ## Trouble Shooting:讓 setupProxy.js 支援 TypeScript? 我嘗試把 setupProxy.js 改成 setupProxy.ts,並把裡面語法改成 TypeScript,但一直無法成功跑起來,目前研究下來應該是 CRA Proxy 不支援 TypeScript,[CRA 官方 Proxy 文件](https://create-react-app.dev/docs/proxying-api-requests-in-development/#configuring-the-proxy-manually) 最下面說了 setupProxy.js 只支援 Node.js 的 JavaScript ES5 前語法,也有找到相關 [issue](https://github.com/facebook/create-react-app/issues/6794) 在詢問它是否支援 TypeScript,但沒有結果。如果你知道有方法能支援 TypeScript 的話,歡迎跟我說! ## 參考資源 - [Proxying API Requests in Development | Create React App](https://create-react-app.dev/docs/proxying-api-requests-in-development/) - [跨來源資源共用(CORS) - HTTP | MDN](https://developer.mozilla.org/zh-TW/docs/Web/HTTP/CORS) - [CORS 完全手冊(一):為什麼會發生 CORS 錯誤? - Huli](https://blog.huli.tw/2021/02/19/cors-guide-1/) --- # What Is an Algorithm Technical Interview? Reading "Cracking the Coding Interview" - URL: https://easonchang.com/posts/cracking-the-coding-interview - Date: 2022-07-24T22:16:00.000Z - Language: en - Description: When software engineers interview at big FANNG companies, the algorithm interview is unavoidable. What does it actually test? How should you prepare? Join me in reading the interview bible "Cracking the Coding Interview" - Translation: AI-assisted For software engineers who want to level up their careers and jump to top global companies like FANNG (Facebook, Amazon, Apple, Netflix, Google), beyond having English good enough for workplace communication, the other big boss to beat is the **algorithm technical interview**. The must-read bible while preparing is this book: "Cracking the Coding Interview: 189 Programming Questions and Solutions". Here, let's read chapters one through three together: what is a technical interview? What does the process look like? And what mindset should you bring to it? > Note: I read the revised 6th edition. ![Cracking the Coding Interview the book](/images/2022-07-24-cracking-the-coding-interview/cracking-the-coding-interview.jpg) ## The structure of "Cracking the Coding Interview" The book has two main parts. The first 100 pages walk through the entire interview process — from preparing your resume early on, to problem-solving techniques during the interview, to negotiating salary at the end — going from soft career planning all the way to hard technical interviewing. Concretely, it covers: - The interview process - How interviews differ at big companies like Microsoft, Amazon, and Google - Preparation for different interview scenarios: candidates with work experience, test engineer interviews, PMs, management roles, startup interviews, acquisition interviews, or when you are the interviewer yourself - Interview preparation: building experience, resume writing, planning your timeline - The Behavioral Interview: how to highlight your strengths and introduce yourself - The Technical Interview: preparation and problem-solving approaches - Offer-related topics: negotiating salary, evaluating a job - Big O notation: describing algorithm efficiency The remaining 800 pages present 189 technical interview questions, categorized by question type, data structure, and difficulty, with problem-solving approaches and detailed solutions, mostly in Java and C++. The chapters break down like this: - Data structures - Array & String - Linked List - Stack & Queue - Tree & Graph - Concepts and algorithms - Bit Manipulation - Math & Logic - Object-oriented design - Recursion, Dynamic Programming - System design - Sorting and searching - Testing - Knowledge based - C/C++ and Java fundamentals - Database design and SQL - Thread & Lock - More problems - Moderate problems - Hard problems - Solutions - Advanced topics (topological sort, Dijkstra, AVL Tree, etc.) - Common libraries This article covers the first half — the interview process. ## About the author, Gayle Laakmann McDowell The author worked as a software engineer at Microsoft, Apple, and Google, and served on Google's hiring committee, interviewing hundreds of candidates and reviewing thousands of interview packets and resumes. She also founded CareerCup, which offers an interview question database to help candidates perform at their best in interviews. The book is written from her experience as both a job seeker and an interviewer. Gayle has written three other books — "Cracking the Tech Career", "Cracking the PM Career", and "Cracking the PM Interview" — also well worth reading if you're building a tech career or working as a PM. ## What is a technical interview The goal of a technical interview is to evaluate a candidate's algorithm and coding skills. Take Google: the first round, the Phone Interview, is a video call with an interviewer where you solve 2 algorithm problems within 45 minutes. The problems might be things like: how do you find the fifth-largest number in an array? Or, given a chart of stock price movements, on which days should you buy and sell to maximize profit? After hearing the problem, you need to proactively ask the interviewer for more details, clarifying the problem's goal, boundaries, and constraints. Then, in an online Google Doc, you come up with test data, draw diagrams, and write pseudocode to sketch out the frame of the algorithm — all while verbally walking the interviewer through your thinking. Once you've confirmed the algorithm's direction is right, you write it as actual code in a programming language, and finally hand-write test data and verify the program's output line by line. Based on your solving speed, algorithm efficiency, communication skills, and code correctness, the interviewer decides whether you move on to the next stage. The content and difficulty vary a bit from company to company, but that's roughly the flow. Solving the problems within the time limit is very challenging, and a normal software engineer doesn't run into algorithms this heavily in everyday work — start a technical interview with zero preparation and you're guaranteed to hit a wall. But big tech companies like FAANG all have one or more rounds of technical interviews, so if you want to take your career to the next level, technical interviews are something to practice hard! "Cracking the Coding Interview" is the secret manual for conquering technical interviews. I recommend picking up a copy and pairing it with hands-on grinding through LeetCode problems — let's push our careers one step further together! --- # 何謂演算法技術面試?讀《Cracking the Coding Interview(提升程式設計師的面試力)》 - URL: https://easonchang.com/zh-TW/posts/cracking-the-coding-interview - Date: 2022-07-24T22:16:00.000Z - Language: zh-TW - Description: 軟體工程師面試 FANNG 大公司時,必不可少的演算法面試,到底在考些什麼?該如何準備?一起來讀面試聖經《Cracking the Coding Interview(提升程式設計師的面試力)》 軟體工程師想要精進職涯,跳槽去頂尖外商公司 FANNG(Facebook、Amazon、Apple、Netflix、Google)的話,除了英文能力要能應付工作上溝通需求外,另一個大魔王就是**演算法技術面試**了。 在準備過程中必讀的聖經就是這本:《Cracking the Coding Interview(提升程式設計師的面試力):189 道面試題目與解答》。 這裡讓我們一起來讀它的第一到第三章,介紹何謂技術面試?會有哪些流程?要以何種心態面對? > 備註:我讀的是第六版之修訂版。 ![Cracking the Coding Interview the book](/images/2022-07-24-cracking-the-coding-interview/cracking-the-coding-interview.jpg) ## 《Cracking the Coding Interview》書籍架構 書中分為兩大部分。 前面 100 頁的篇幅在介紹整套面試流程,從前期履歷準備、面試中解題技巧、到面試尾聲談薪水,從軟性職涯規劃講到硬性技術面試,具體涉及內容如下: - 面試流程 - Microsoft、Amazon、Google 等各大公司面試差異 - 各類面試情境準備要點:已有工作經驗的求職者、面試測試工程師、PM、管理職位、新創公司面試、收購面試、或你自己就是面試官 - 面試準備:累積經歷、履歷撰寫、時程規劃 - 行為面試 Behavioral Interview:如何突出個人優勢、自我介紹 - 技術面試 Technical Interview:準備、解題思路 - 聘用相關問題:談薪水、評估工作 - Big O 表示法:描述演算法效率 後面 800 頁則列出了 189 道技術面試題目,依照題型、資料結構、難度分類,介紹解題思路和詳細解答,主要用 Java 和 C++ 來解題,章節分類如下: - 資料結構 - Array & String - Linked List - Stack & Queue - Tree & Graph - 概念和演算法 - Bit Manipulation - Math & Logic - 物件導向 - 遞迴、Dynamic Programming - 系統設計 - 排序和搜尋 - 測試 - 知識基礎 - C/C++、Java 知識基礎 - 資料庫設計和 SQL - Thread & Lock - 更多問題 - 中等難度問題 - 困難問題 - 解答 - 進階主題(拓墣、DIjkstra、AVL Tree 等) - 常用函式庫 這篇文章會講到前半部分面試流程的內容。 ## 關於作者 Gayle Laakmann McDowell 本書作者在 Microsoft、Apple、Google 擔任過軟體工程師,並在 Google 招募委員會任職,面試過數百名面試者、及評估過數千份面試資料及履歷。 她也創立了 CareerCup,提供面試資料庫,幫助面試者在面試中得到最好的表現。 本書就是以她身為求職者及面試官的經驗寫成的。 Gayle 還著有另外三本書《Cracking the Tech Career》、《PM 職涯發展成功手冊 (Cracking the PM Career)》、《Cracking the PM Interview》,對經營科技職涯,或 PM 從業人員也是非常值得一看的書籍。 ## 什麼是技術面試 技術面試目的是評估求職者的演算法和寫程式能力。 以 Google 為例,第一關 Phone Interview 的內容就是線上跟面試官視訊面試,並在 45 分鐘內解出 2 道演算法題目。 題目可能是如何在一個陣列裡找出第五大的數字?或給定一個股市漲跌圖,在哪幾天買進賣出才能收益最大?諸如此類的題目。 你需要在聽完題目後,主動詢問面試官更多細節,把問題的目的、邊界和限制條件釐清。 接著在線上 Google 文件裡發想測試資料、畫圖、和寫虛擬碼,勾勒出演算法的框架,並且同時口頭跟面試官講解你的思路。 確認演算法方向正確後,在用程式語言寫成實際的程式碼,最後手動編寫測試資料,一行一行驗證程式執行結果是否正確。 面試官會根據你的解題速度、演算法效率、溝通能力、程式正確性,決定你能否進入後續流程。 各個公司內容和難度有些許差異,但大致流程就是如此。 要在時限內解完題目是很有挑戰性的,並且正常軟體工程師在平常工作中,也不會如此大量碰到演算法,如果毫無準備就開始技術面試的話,一定會碰壁。但大型科技公司如 FAANG 等,都會有一到多輪技術面試,因此若想讓職涯更上一層樓的話,技術面試是需要重點練習的! 《Cracking the Coding Interview(提升程式設計師的面試力)》就是攻克技術面試的武功秘笈,建議可以入手一本,搭配實際動手刷 LeetCode 上的題目,一起讓自己的職涯更近一步! --- # Stop Copy-Pasting from Old Files! Quickly Create New Blog Post Markdown Files with Hygen - URL: https://easonchang.com/posts/hygen-generate-new-post - Date: 2022-04-11T20:44:00.000Z - Language: en - Description: I added the Hygen code generator to my personal blog, so I can quickly create new post draft .mdx files with the hygen post new command, cutting out the tedious steps. - Translation: AI-assisted ## TL;DR I added the [Hygen](https://www.hygen.io/) code generator to my personal blog, so I can quickly create new post draft **.md** files with the `hygen post new` command. ## The pain point of blogging: creating a new post With my blog's current setup, starting a new post takes a lot of steps. Take the very post you're reading as an example. Creating it required: 1. In the **/content/posts/en/** folder, pick any post source file (say, **2022-04-07-product-hunt-today.mdx**) and make a copy of it. 2. Check today's date (2022/04/11) and rename the copy to **2022-04-11-hygen-generate-new-post.mdx**. 3. Delete everything in the new file except the post meta data at the top (see the example below). 4. Change the new post's date to the current time. 5. Repeat steps 1–4 in the **/content/posts/zh-TW/** folder 6. In the **/public/images/** folder, create a **2022-04-11-hygen-generate-new-post/** folder to hold the images this post uses. The basic skeleton of a new **.mdx** post file: ```js --- title: '' author: Eason Chang description: '' category: '' tags: - '' socialImage: '' date: 2022-04-11 20:44 template: 'post' meta: title: '' --- ``` That's tedious and takes real mental effort. As an engineer, I decided to automate these steps! ## Adding Hygen: generate the new post template files with one command In the JavaScript world there's a tool called [Hygen](https://www.hygen.io/), born exactly for this purpose. Hygen is a simple, easy-to-use code generator you can install via homebrew or npm. After installing, you create a few **.ejs** template files in your project, and then a single CLI command generates all the template files you want in one go. ![Hygen homepage](/images/2022-04-11-hygen-generate-new-post/hygen-homepage.jpg) > Hygen official site: https://www.hygen.io/ I decided to use Hygen to generate the files for me! ### Installing Hygen I installed hygen into my blog project using yarn: ```shell yarn add --dev hygen ``` You can also install it with homebrew or npm — or skip installing entirely and run it on demand with npx. See the official [Quick Start](https://www.hygen.io/docs/quick-start) page. ### My Hygen template structure I want Hygen to generate 2 .mdx files and 1 folder for me: 1. /content/posts/zh-TW/[date]-[slug].mdx 2. /content/posts/zh-TW/[date]-[slug].mdx 3. /public/images/[date]-[slug]/ As a tree, it looks like this: ```js ├── content/ │ └── posts/ │ ├── en/ │ │ └── [date]-[slug].mdx │ └── zh-TW/ │ └── [date]-[slug].mdx └── public/ └── images/ └── [date]-[slug]/ ``` The way Hygen works, each file you want to create maps to one .ejs.t template file. Templates live under the \_templates/ folder by default, and each level of subfolder below it determines how the CLI command will be used later. I want my command for creating a new post to be `hygen post new`, so the template files look like this as a tree: ```js └── _templates └── post └── new ├── images-folder.ejs.t ├── post-en.ejs.t ├── post-zh.ejs.t └── prompt.js ``` That's 3 .ejs.t template files, plus one prompt.js file. ### Supporting dynamic parameters in the Hygen CLI with prompt.js By creating a prompt.js file, we can make Hygen accept dynamic parameters when generating templates. When I run `hygen post new`, I want to also enter the post's slug — the unique URL name of the post — which decides the names of the 3 generated files. So I need to create prompt.js with this content: **prompt.js** ```js module.exports = [ { type: 'input', name: 'slug', message: 'Enter slug (ex: "my-post"): ', }, ] ``` This defines that the command needs to receive a slug parameter. After I run `hygen post new`, Hygen asks me to enter the slug: ![Hygen terminal](/images/2022-04-11-hygen-generate-new-post/hygen-terminal.jpg) Or I can specify the slug directly in the command, like `hygen post new --slug a-brand-new-post`. For more on Hygen prompts, see the official document: https://www.hygen.io/docs/generators#interactive-prompt ### Adding a helper function `getDateTime(formatStr)` to get the current time Hygen ships with some built-in string-formatting helper functions for simple string transformations. For example, to turn the incoming slug parameter into all caps, you can use `<%= h.capitalize(slug) %>` inside an ejs template file. [Official helper function document](https://www.hygen.io/docs/templates/#helpers-and-inflections) But I need to get the current time and format it in different ways, and Hygen has no built-in function for that. Luckily, Hygen supports customization, so we can extend it with our own helper functions. The way to do it is to add a **.hygen.js** file at the project root: ```js const { format } = require('date-fns') module.exports = { helpers: { getDateTime: function (formatStr) { return format(new Date(), formatStr) }, }, } ``` What this does: I've added a `getDateTime(formatStr)` helper function that gets the current time and converts it into whatever format we want. [Official document on extending helper functions](https://www.hygen.io/docs/extensibility#helpers) ### The .ejs.t template file contents Finally, the real main event: the **ejs.t** template files. ejs stands for Embedded JavaScript — plain-text templates where you can embed simple JS syntax to inject content dynamically. Every target file you want to create needs its own corresponding .ejs.t template file. The .ejs.t filename itself can be anything — it doesn't affect the generated file. First, the 2 target .mdx files, which have identical content: ```js --- title: '' author: Eason Chang description: '' category: '' tags: - '' socialImage: '' date: 2022-04-11 20:44 template: 'post' meta: title: '' --- ``` So I created two template files, **post-zh.ejs.t** and **post-en.ejs.t**: **post-zh.ejs.t** ```js --- to: content/posts/zh-TW/<%= h.getDateTime('yyyy-MM-dd') %>-<%= slug %>.mdx --- --- title: '' author: Eason Chang description: '' category: 'Project' tags: - Project socialImage: '' date: <%= h.getDateTime('yyyy-MM-dd HH:mm') %> template: 'post' meta: title: '' --- ``` **post-en.ejs.t** ```js --- to: content/posts/en/<%= h.getDateTime('yyyy-MM-dd') %>-<%= slug %>.mdx --- --- title: '' author: Eason Chang description: '' category: 'Project' tags: - Project socialImage: '' date: <%= h.getDateTime('yyyy-MM-dd HH:mm') %> template: 'post' meta: title: '' --- ``` Two things to notice here. The first is the dashed block in the file's first three lines: ```js --- to: content/posts/en/<%= h.getDateTime('yyyy-MM-dd') %>-<%= slug %>.mdx --- ``` This block delimited by triple dashes is a format called Frontmatter, used to give Hygen the template's meta data. Here I use `to:` to tell Hygen where the target file should be created. The second is the `<%= %>` syntax — the standard way to embed JS inside ejs. I use it to dynamically inject the slug parameter, and to call the `h.getDateTime()` we just defined to get the formatted time. For more, see the [Templates doc page on the Hygen site](https://www.hygen.io/docs/templates) Last, I still need to create the folder for images. I want to see this folder in git right away after it's created, so it needs an empty .keep file inside. So, at the very end, I created the **images-folder.ejs.t** template to generate the folder and its .keep file: ```js --- to: public/images/<%= h.getDateTime('yyyy-MM-dd') %>-<%= slug %>/.keep --- ``` ## The result That's it! No more spending ages creating new posts! Now I just run `hygen post new` and Hygen does the work for me, so I can put my time where it matters: producing content! ![Hygen terminal](/images/2022-04-11-hygen-generate-new-post/hygen-terminal.jpg) --- # 不要再從舊檔案複製貼上了!使用 Hygen 快速建立 Blog 新文章 Markdown 檔案 - URL: https://easonchang.com/zh-TW/posts/hygen-generate-new-post - Date: 2022-04-11T20:44:00.000Z - Language: zh-TW - Description: 我在個人部落格導入了 Hygen 程式碼產生器,讓我能使用 hygen post new 指令,快速建立新文章草稿 .mdx 檔案,簡化繁瑣步驟。 ## TL;DR 我在個人部落格導入了 [Hygen](https://www.hygen.io/) 程式碼產生器,讓我能使用 `hygen post new` 指令,快速建立新文章草稿 **.md** 檔案。 ## 寫部落格的痛點:建立新文章 以我目前個人部落格的架構,要開始寫一篇新文章時,需要很多步驟。 拿建立你現在看到的這篇文章為例,需要以下步驟: 1. 在 **/content/posts/en/** 資料夾內,隨便挑選一篇文章原始檔(例如:**2022-04-07-product-hunt-today.mdx**),將他複製一份。 2. 查看今天日期(2022/04/11),將剛複製的新檔案改名成 **2022-04-11-hygen-generate-new-post.mdx**。 3. 移除新檔案內所有內容,只保留最上方文章 meta data 資訊(見下方範例)。 4. 把新文章的 date 改成現在的時間。 5. 在 **/content/posts/zh-TW/** 資料夾內重做一次步驟 1 ~ 4 6. 在 **/public/images/** 資料夾內,建立 **2022-04-11-hygen-generate-new-post/** 資料夾,要用來放這篇文章用到的圖片。 新文章 **.mdx** 檔基本架構: ```js --- title: '' author: Eason Chang description: '' category: '' tags: - '' socialImage: '' date: 2022-04-11 20:44 template: 'post' meta: title: '' --- ``` 步驟挺繁瑣且耗費心力,身為一位工程師,我決定自動化這些步驟! ## 導入 Hygen,用一條指令產生新文章模板檔案 在 JavaScript 世界,有一個工具叫做 [Hygen](https://www.hygen.io/),它就是專為這個目的而生的。 Hygen 是一個簡單好用的程式碼產生器(Code Generator),可以透過 homebrew 或 npm 安裝。安裝後在專案內建立幾個 **.ejs** 模板檔,接著就能用 CLI 終端機下一行指令,一次生成你要的模板檔案。 ![Hygen homepage](/images/2022-04-11-hygen-generate-new-post/hygen-homepage.jpg) > Hygen 官網:https://www.hygen.io/ 我決定使用 Hygen 來幫我產生檔案! ### 安裝 Hygen 我使用 yarn 安裝 hygen 到我的部落格專案,安裝指令如下: ```shell yarn add --dev hygen ``` 你也可以使用 homebrew、npm、安裝,或不想安裝的話,使用 npx 在用到時一次性下載來執行也行,可以參考官方 [Quick Start](https://www.hygen.io/docs/quick-start) 頁面。 ### 我的 Hygen 模板架構 我希望 Hygen 幫我產生 2 個 .mdx 檔案,和 1 個資料夾: 1. /content/posts/zh-TW/[date]-[slug].mdx 2. /content/posts/zh-TW/[date]-[slug].mdx 3. /public/images/[date]-[slug]/ 以樹狀圖表示的話如下: ```js ├── content/ │ └── posts/ │ ├── en/ │ │ └── [date]-[slug].mdx │ └── zh-TW/ │ └── [date]-[slug].mdx └── public/ └── images/ └── [date]-[slug]/ ``` Hygen 的邏輯是每個要建立的檔案,對應到一個 .ejs.t 模板檔,模板檔預設是在 \_templates/ 資料夾底下,而底下每層子資料夾會決定將來 CLI 指令時要如何使用。 我希望我用來建新文章的指令是 `hygen post new`,因此我們 template 模板檔案,樹狀圖如下: ```js └── _templates └── post └── new ├── images-folder.ejs.t ├── post-en.ejs.t ├── post-zh.ejs.t └── prompt.js ``` 其中包含 3 個 .ejs.t 模板檔案,以及一個 prompt.js 檔。 ### 用 prompt.js 讓 Hygen CLI 支援動態參數 我們可以透過建立 prompt.js 檔,讓用 Hygen 生成模板時支援動態參數。 我希望我在下 `hygen post new` 指令時,能再輸入文章 slug,代表這篇文章獨一無二的網址名稱,用來決定 3 個生成出來的檔案名稱。 因此我需要建立 prompt.js,內容如下: **prompt.js** ```js module.exports = [ { type: 'input', name: 'slug', message: 'Enter slug (ex: "my-post"): ', }, ] ``` 這定義了我這個指令需接收一個 slug 參數,下了 `hygen post new` 指令後,Hygen 就會請我再輸入 slug: ![Hygen terminal](/images/2022-04-11-hygen-generate-new-post/hygen-terminal.jpg) 或是我也可以直接在指令後面指定 slug,例如:`hygen post new --slug a-brand-new-post`。 更多 Hygen prompt 用法,可以參考官方 document: https://www.hygen.io/docs/generators#interactive-prompt ### 新增 helper 函式 `getDateTime(formatStr)` 用來取得現在時間 Hygen 內建提供了一些字串格式化的工具函式 (helper),幫我們做簡單字串轉換。 例如要將吃進來的 slug 參數轉成全大寫的話,能透過在 ejs 模板檔內,使用 `<%= h.capitalize(slug) %>` 達成。 [官網 helper function document](https://www.hygen.io/docs/templates/#helpers-and-inflections) 但我有個需求是取得現在時間,並且要能轉成不同格式,Hygen 內建沒有這個函式可用,好在 Hygen 支援了客製化功能,我們能擴充自己的 helper function。 方法是在專案根目錄,新增 **.hygen.js** 檔案: ```js const { format } = require('date-fns') module.exports = { helpers: { getDateTime: function (formatStr) { return format(new Date(), formatStr) }, }, } ``` 上面設定的功用是,我新增了 `getDateTime(formatStr)` 這個 helper 函式,讓我們取得現在時間,並轉成想要的格式。 [官網擴充 helper function document](https://www.hygen.io/docs/extensibility#helpers) ### .ejs.t 模板檔案內容 最後進入真正的重點, **ejs.t** 模板檔案。 ejs 是 Embedded JavaScript 的意思,在純文字模板裡,能嵌入簡單 JS 語法,動態注入內容。 每個想要建立的目標檔案,都需要一個對應的 .ejs.t 模板檔案。.ejs.t 檔案檔名可以隨意取,不影響目標檔案的生成。 首先是 2 個目標 .mdx 檔案,內容都一樣,如下: ```js --- title: '' author: Eason Chang description: '' category: '' tags: - '' socialImage: '' date: 2022-04-11 20:44 template: 'post' meta: title: '' --- ``` 所以我建立了兩個模板檔案,**post-zh.ejs.t** 和 **post-en.ejs.t**,內容如下: **post-zh.ejs.t** ```js --- to: content/posts/zh-TW/<%= h.getDateTime('yyyy-MM-dd') %>-<%= slug %>.mdx --- --- title: '' author: Eason Chang description: '' category: 'Project' tags: - Project socialImage: '' date: <%= h.getDateTime('yyyy-MM-dd HH:mm') %> template: 'post' meta: title: '' --- ``` **post-en.ejs.t** ```js --- to: content/posts/en/<%= h.getDateTime('yyyy-MM-dd') %>-<%= slug %>.mdx --- --- title: '' author: Eason Chang description: '' category: 'Project' tags: - Project socialImage: '' date: <%= h.getDateTime('yyyy-MM-dd HH:mm') %> template: 'post' meta: title: '' --- ``` 裡面有兩點值得注意。第一個是檔案前三行橫線區塊: ```js --- to: content/posts/en/<%= h.getDateTime('yyyy-MM-dd') %>-<%= slug %>.mdx --- ``` 三條槓分割的區塊,這種格式叫做 Frontmatter,用來告訴 Hygen 這個模板的 meta data,在這邊我用 `to:` 跟 Hygen 説目標檔案要建立在哪裡。 第二點是 `<%= %>` 這類語法,這是 ejs 裡面塞入 JS 語法的固定格式,我用來動態塞入 slug 參數內容,以及呼叫剛剛定義的 `h.getDateTime()`,取得格式化後的時間。 更多教學請參閱 [Hygen 官網 Templates doc 頁面](https://www.hygen.io/docs/templates) 最後我還需要建立放圖片用的資料夾。 我希望建立後我能在 git 裡馬上看到這個資料夾,因此需要在裡面放入一個 .keep 空檔案。 所以最後的最後,我建立了 **images-folder.ejs.t** 模板,來生成資料夾及其 .keep 檔: ```js --- to: public/images/<%= h.getDateTime('yyyy-MM-dd') %>-<%= slug %>/.keep --- ``` ## 成果 That's it! 我不用再花許多時間建立新文章了!現在我只要下 `hygen post new` 指令,就能讓 Hygen 替我代勞,我能把重要的時間放在內容產出! ![Hygen terminal](/images/2022-04-11-hygen-generate-new-post/hygen-terminal.jpg) --- # Product Hunt Today - An Automatic Twitter Bot for Generating Short Video Introductions to Popular Product Hunt Projects - URL: https://easonchang.com/posts/product-hunt-today - Date: 2022-04-07T20:00:00.000Z - Language: en - Description: A fully automated Twitter bot. Crawls popular projects daily through the Product Hunt API, generates short videos with React.js Remotion, and publishes tweets via the Twitter API. ## TL;DR I made a side project: [Product Hunt Today](https://twitter.com/ProductHunToday), a fully automated Twitter bot. It crawls popular projects daily on [Product Hunt](https://www.producthunt.com/) through the [Product Hunt API](https://api.producthunt.com/v2/docs), then generates short videos using [Remotion](https://www.remotion.dev/) (with React!), writes some introductions, and publishes tweets via the [Twitter API](https://developer.twitter.com/en/docs/twitter-api). > Github repo: [eason-dev/product-hunt-today](https://github.com/eason-dev/product-hunt-today) > > Follow Product Hunt Today: [@ProductHunToday](https://twitter.com/ProductHunToday) ![Tweet of Product Hunt Today](/images/product-hunt-today/product-hunt-today-tweet.jpg) --- ## The Origin of the Product Hunt Today Side Project In early March 2022, I joined the [100DaysOfCode Challenge](https://www.100daysofcode.com/), committing to code every day for 100 days. ### Discovering Remotion, Making Videos with React As I was thinking about what to do first, I saw a [Youtube video](https://youtu.be/deg8bOoziaE) by Fireship introducing [Remotion](https://www.remotion.dev/), this cool tool. Remotion allows us to use React.js code to produce mp4 videos, where every frame's layout and animation are realized using web CSS animations. I found it very interesting, so I decided to create something cool with Remotion. ### Finding the Crypto Prices Model Project I began exploring what kind of videos Remotion could produce and stumbled upon the Showcase page on their official website, where I saw the [Crypto Prices](https://www.remotion.dev/showcase#fAhMmqHE5fQg9V7H2CwNj4buFC6JhLDfgaMI9OBpYhw) project by [@tokenviz](https://twitter.com/tokenviz). This project is also a Twitter bot that publishes several short videos daily, summarizing the recent few hours' trends in cryptocurrency rises and falls. This is one of their tweets, listing the five cryptocurrencies with the largest drops within a day: ![Tokenviz Tweet](/images/product-hunt-today/tokenviz-tweet.jpg) Source: [Original Tweet](https://twitter.com/tokenviz/status/1512021029220384770) I found this application form very interesting, fully leveraging Remotion's advantages, automatically generating a large number of videos by integrating external API data. I could learn several skills and tools, including: video production with Remotion, external data API integration, Twitter posting API, and cronjob for scheduling scripts. The content of the videos was simple, without overly complex animations, and the duration was short, preventing the project scope from becoming too large and reducing the risk of abandonment. The difficulty was just right. So, I started thinking about how to adapt this project's concept into my version. ### The Concept of Product Hunt Today Was Born Usually, when I brainstorm project ideas, I open the Product Hunt website to browse recent cool projects to inspire creativity. [Product Hunt](https://www.producthunt.com/) is a project launch media platform where people upload various projects (mostly software) daily, ranging from independent developers' new side projects to new products from software giants, all seeking exposure to attract users. Users can vote (Upvote) on projects they are interested in to support them. Projects with more Upvotes get better exposure positions, usually indicating the best creativity or completeness. Product Hunt also awards badges to the top five upvoted projects of the day, a significant achievement for developers. While browsing, I thought, since Product Hunt also changes rapidly daily with a ranking concept, why not use Product Hunt's daily popular projects to make Remotion short videos! I also needed this project to keep track of daily popular projects while scrolling through Twitter without constantly opening the Product Hunt webpage. So, I decided to start making the Product Hunt Today Twitter bot! ## What Does Product Hunt Today Do Product Hunt Today [@ProductHunToday](https://twitter.com/ProductHunToday) is a Twitter bot. ![Product Hunt Today Twitter account](/images/product-hunt-today/product-hunt-today-twitter-account.jpg) Currently, it posts an article daily summarizing the previous day's top five products on Product Hunt: Source: [Original Tweet](https://twitter.com/ProductHunToday/status/1506186218714849287) ![Tweet of Product Hunt Today](/images/product-hunt-today/product-hunt-today-tweet.jpg) The video looks like this:
## System Architecture System architecture diagram: ![upload successful](/images/winster/pasted-8.png) ### Hardware STM32F469NIH6 code: [ericz7000nolan/Arm-contest](https://github.com/ericz7000nolan/Arm-contest) 1. Custom voltage detection module: measures resistance inversely proportional to brightness (directly proportional to the reciprocal of resistance). To determine spectral values, temperature, and weight, the resistance is measured first, followed by measuring the voltage across an appropriate resistor to deduce the condition of the wine, hence determining taste and origin. 2. ADC1, ADC2, ADC3: After measuring the test voltage signal, convert the results into actual voltage values for circuit use and server calculations. 3. Activating the LCD screen and touchscreen enhances user interaction with the project, allowing mode selection through the LCD interface. 4. On the STM32F469NIH6 Discovery Board, we implemented functionalities such as USART, ADC, delay, key interrupt, LED control, LCD, touchscreen, FATFS. 5. In this project, the STM32F469NIH6 Discovery board serves for data detection and preliminary processing (ADC data performs a Moving Average of 100 points), and the results are transmitted to the Bluetooth module via USART. Data is sent to the HC05 Bluetooth module via USART6 on the board, which then transmits the data to the host for real-time data analysis. Winster device finished product: ![upload successful](/images/winster/pasted-11.png) ### Data Processing We installed RGB three-color LEDs on the device, allowing it to be attached to both sides of a wine bottle. We utilized changes in LED light, illuminating the bottle with red, yellow, blue, green, cyan, purple, and white light, respectively. A photoresistor on the other end received the light values, analyzing the variations in light values across different types of wine for the purpose of identifying and judging wine types. We analyzed the light values of 50 bottles of wine, including 8 bottles of whiskey, 2 bottles of blended whiskey, 4 bottles of vodka, 1 bottle of rum, 3 bottles of tequila, 3 bottles of gin, 1 bottle of rosé, 1 bottle of red wine, and 27 bottles of white wine. Our findings indicated that after analyzing the spectral values, we could differentiate the wines based on these values. We observed that green light had the most significant impact, and the variations could be linked to the wine's body (such as medium body, full body, etc.), thereby determining the wine's body and possible origin. Furthermore, after detecting the raw data of the test subject, we used the optical density (OD) values of various colors absorbed by the wine types to map regression analysis with the database, selecting the closest wine type. We then rated the differences in scores (analyzed on the server) and returned the results to the board. ![upload successful](/images/winster/pasted-12.png) ### Software The host uses Processing to receive data (light transmission rates of various colors, temperature, weight) sent via Bluetooth from the Winster device, comparing this data with all the wine data in the SQLite3 database through regression analysis to identify the closest wine type. This result is then sent back to the device for display, and the new data is added to the database to expand its volume, making future comparison results more accurate. ![upload successful](/images/winster/pasted-13.png) The web frontend is implemented using HTML, CSS, Javascript, and JqueryUI for an interactive interface. The web backend uses Ruby on Rails as the development framework, with SQLite3 as the database. When users connect to the host, Rails directly retrieves the results that have been analyzed and compared from the database, displaying them on the web page. If the user is a professional winemaker and believes there is a discrepancy in the results displayed on the web page (such as year or body of the wine), they can manually adjust the results of the regression analysis. This modification is immediately written into the database, improving the overall accuracy of the comparison results. Web code: [eason-dev/winster-web](https://github.com/eason-dev/winster-web) Web Interface (Homepage): ![upload successful](/images/winster/pasted-9.png) Web Interface (Wine Cellar Details): ![upload successful](/images/winster/pasted-10.png) ## Related Media Reports - [Cultivating IoT Startups towards the International Stage, FarEasTone Plays a Supporting Role - Yahoo News](https://tw.news.yahoo.com/%E5%9F%B9%E8%82%B2iot%E6%96%B0%E5%89%B5%E5%9C%98%E9%9A%8A%E9%82%81%E5%90%91%E5%9C%8B%E9%9A%9B%E8%88%9E%E5%8F%B0-%E9%81%A0%E5%82%B3%E6%89%AE%E6%8E%A8%E6%89%8B-084206839.html) ## Project Members Eason Chang, Eric Chang, Michael Chen --- # 【專案】智慧釀藏酒大師 Winster - URL: https://easonchang.com/zh-TW/posts/winster - Date: 2018-04-02T16:56:00.000Z - Language: zh-TW - Description: 「智慧釀藏酒大師 Winster」,是一套幫助您釀酒和控管酒況的釀藏酒平台。主要能夠提供專業釀酒玩家、小型釀酒企業,酒的品種以及產地的分析和釀酒過程的錯誤偵測。 ## 簡介 **「智慧釀藏酒大師 Winster」**,是一套幫助您釀酒和控管酒況的釀藏酒平台。 主要能夠提供專業釀酒玩家、小型釀酒企業,酒的品種以及產地的分析和釀酒過程的錯誤偵測,同時在網頁與行動裝置上指示用戶,註冊、偵測並顯示相關資訊,並紀錄之,亦可對於單純收藏酒的一般用戶提供酒況偵測與建議。 技術上透過光學、溫度、濕度和重量感測,以及背後的數據收集,利用 RNN 的機器學習架構,採用 LSTM 來分析原始資料。 我們特地實地收集了五十幾種的酒種,並且研究諸多相關論文,利用偵測值(RGB 的 OD 值、溫度、濕度、重量實時變化)與口感的關係,再利用自製的偵測電路和 3D 列印外殼,實現了 Winster 的硬體裝置。 這項作品拿到了 **[2017 ARM Design Contest](http://www.armdesigncontest.com/customers/login/) 決賽入圍**,以及 **[第 2 屆遠傳物聯網應用開發大賽](http://promotion.fetnet.net/ebu/2017IoTHackathon/index.html)的 Ericsson 企業獎**。 ## Demo 影片 ## 系統架構 系統架構圖: ![upload successful](/images/winster/pasted-8.png) ### 硬體 STM32F469NIH6 程式碼:[ericz7000nolan/Arm-contest](https://github.com/ericz7000nolan/Arm-contest) 一、自製電壓偵測模組:以電阻值反比於亮度(正比於電阻倒數)的形式進行測量。欲量得光譜值、溫度、重量的值,須先測得其電阻,另外串連一個適當電阻,測其跨壓,來得出酒況,進而得出口感和相關產地。 二、ADC1 ADC2、ADC3 :測量待測電壓訊號經過後,將轉換結果換算成實際的電壓值,供電路使用,並且供 server 做計算。 三、將 LCD screen 和 touchscreen 做啟用,提升使用者與作品的親切度,並能透過 LCD 介面選擇模式。 四、STM32F469NIH6 Discovery Board 我們主要實現了以下:功能 USART、ADC、delay、按鍵中斷、LED 控制、LCD、touchscreen、FATFS。 五、在此作品中,STM32F469NIH6 Discovery 開發板作為數據偵測與初階處理的工作(ADC 數據作 Moving Average 100 點),並將結果透過 USART 傳給藍芽模組,USART 與 HC05 藍芽模組透過板上的 USART6,傳送資料給藍芽模組 HC05,藍芽模組再將資料傳送給主機方, 主機方接收後會開始進行即時數據分析。 Winster 裝置成品: ![upload successful](/images/winster/pasted-11.png) ### 資料處理 我們利用 RGB 三色 LED 裝設於裝置上,並使其能貼於酒瓶兩側,利用 LED 燈光之變化:分別以紅、黃、藍、綠、青、紫、白光去照射酒瓶,並於另一端的光敏電阻接收光值,並去分析不同酒種對於光值的變化情形來做酒種的識別與判斷。 我們共分析了 50 支酒的光值,分別為 8 支威士忌、2 支調和威士忌、4 支伏特加、1 支萊姆酒、3 支龍舌蘭酒、3 支琴酒、1 支粉紅酒、1 支紅酒與 27 支白酒。 而我們發現光譜值透過分析後,可將其與酒之間的差異分離出來,並且觀察到綠光的部分造成的影響最大,且差異性可和酒種的 body(口感,像是 medium body、full body 等等)做連結,進而判斷出酒種的 body 和可能產地,再者,偵測到待測物的原始資料後,利用酒類的各色吸光 OD 值來對酒類的酒種、酒體、釀造氣候等等,與資料庫的數據進行迴歸分析的 mapping,再取出最相近的酒種,然後對差異性高低做分數評比(在 server 上做分析),並回傳結果給板子。 ![upload successful](/images/winster/pasted-12.png) ### 軟體 主機上使用 Processing 接收 Winster 裝置藍芽傳進來的資料(各色光穿透率、溫度、重量),將此筆資料與 SQLite3 資料庫中所有酒類 data 進行回歸分析比對,找出最相近的酒種,然後回傳回 device 上顯示,同時將此筆新資料加入資料庫中,擴展資料量,讓下次比對結果更準確。 ![upload successful](/images/winster/pasted-13.png) 網頁前端使用 HTML, CSS, Javascript, JqueryUI 來實現互動式介面。 網頁後端使用 Ruby on Rails 做為開發框架,並用 SQLite3 作為資料庫。 使用者連線上主機後,Rails 會直接抓取資料庫中已經分析比對好的結果,呈現在網頁上。 而若使用者是專業釀酒師,在實際品酒後認為網頁呈現的結果有偏差(年份、酒體有誤差等等),也可以手動更改回歸分析的結果,而這筆更動也會即時寫進資料庫裡,提升整體比對結果準確率。 網頁程式碼:[eason-dev/winster-web](https://github.com/eason-dev/winster-web) 網頁介面(首頁): ![upload successful](/images/winster/pasted-9.png) 網頁介面(酒藏詳細資訊): ![upload successful](/images/winster/pasted-10.png) ## 相關媒體報導 - [培育 IoT 新創團隊邁向國際舞台 遠傳扮推手 - Yahoo 奇摩新聞](https://tw.news.yahoo.com/%E5%9F%B9%E8%82%B2iot%E6%96%B0%E5%89%B5%E5%9C%98%E9%9A%8A%E9%82%81%E5%90%91%E5%9C%8B%E9%9A%9B%E8%88%9E%E5%8F%B0-%E9%81%A0%E5%82%B3%E6%89%AE%E6%8E%A8%E6%89%8B-084206839.html) ## 專案成員 Eason Chang、Eric Chang、Michael Chen --- # [Project] Smart Gloves - URL: https://easonchang.com/posts/smart-gloves - Date: 2018-04-01T22:27:00.000Z - Language: en - Description: Smart Gloves are designed to replace keyboards and mice, becoming the next generation of human-computer interface devices. They allow users to control all aspects of life, such as smart home appliances, as game controllers, playing virtual instruments, etc. Related links: - Code (Github): [ChiShengChen/2018makeNTU_meowmeow](https://github.com/ChiShengChen/2018makeNTU_meowmeow) ## Introduction **"Smart Gloves"** was our project for the **[NTU Electrical Engineering Makerthon MakeNTU 2018](https://make.ntuee.org/)** at the end of March 2018. Smart Gloves are designed to replace keyboards and mice, becoming the next generation of human-computer interface devices. They allow users to control all aspects of life, such as smart home appliances, as game controllers, playing virtual instruments, etc. Technically, the gloves are equipped with 2 **MPU6050 6-axis accelerometers and gyroscopes** to measure hand posture, and 4 **capacitive touch switches** for customized input. They use **Linkit7697** to collect sensor data and send it back to the cloud. On the **Microsoft Azure cloud**, gesture commands are computed (currently using Python to run the **SVM (Support Vector Machine) algorithm** to match the current gesture commands with training data), then sent back to the controlled device (like a smart bulb). This project was a finalist for the **Best Tech Award at NTU Electrical Engineering Makerthon**. Team members: Qi-Sheng Chen, Shu-Ming Zhang, Ying-Xiang Zhang, Yu-Kai Wang ## Demo Video ## DEMO Presentation ## Smart Gloves Results Smart Gloves photo (palm side), fingertips equipped with 4 capacitive touch switches: ![upload successful](/images/smart-gloves/pasted-5.png) Smart Gloves photo (back of the hand), the index fingertip and arm equipped with 2 MPU6050 6-axis accelerometers and gyroscopes, using Linkit7697 as the control board: ![upload successful](/images/smart-gloves/pasted-4.png) Sensor data visualization (created with Processing), real-time display of MPU6050 raw data, calculated posture, and touch switch status: ![upload successful](/images/smart-gloves/pasted-6.png) ## Detailed Explanation ### Application Scenarios - Occupational therapy aids - Smart home control - Game controllers - Playing air instruments - Human-computer interface operation - ... ### System Architecture Smart Gloves system architecture diagram: ![upload successful](/images/smart-gloves/pasted-7.png) ### Code Code (Github): [ChiShengChen/2018makeNTU_meowmeow](https://github.com/ChiShengChen/2018makeNTU_meowmeow) Includes: - Arduino code for testing various sensors - Arduino code for the evolving smart gloves - Processing integration, data visualization code - Python Sci-kit Learn SVM code - Development code: mouse control, cloud data collection, gesture command computation ## Conclusion We're proud to have made this smart gloves, and we're excited to see how it can be used in the future! We will continue to improve the smart gloves, and our goal is to use it to play the game of eating chicken 🐔 If you have any thoughts on this project, or would like a more detailed tutorial, feel free to leave a comment below~ --- # 【專案】智慧手套 Smart Gloves - URL: https://easonchang.com/zh-TW/posts/smart-gloves - Date: 2018-04-01T22:27:00.000Z - Language: zh-TW - Description: 智慧手套旨在取代鍵盤及滑鼠,成為下個世代的人機介面裝置,希望使用者能透過智慧手套,操控生活中的所有事物,例如:控制智慧家電、作為遊戲控制器、演奏虛擬樂器等。 相關連結: - 程式碼(Github):[ChiShengChen/2018makeNTU_meowmeow](https://github.com/ChiShengChen/2018makeNTU_meowmeow) ## 簡介 **「智慧手套 Smart Gloves」**,是我們在 2018 年 3 月底參加 **[台大電機創客松 MakeNTU 2018](https://make.ntuee.org/)** 時的專案作品。 智慧手套旨在取代鍵盤及滑鼠,成為下個世代的人機介面裝置,希望使用者能透過智慧手套,操控生活中的所有事物,例如:控制智慧家電、作為遊戲控制器、演奏虛擬樂器等。 技術上是在手套上配置了 2 個 **MPU6050 6 軸加速規陀螺儀**來量測手部姿態、及 4 個**電容式觸碰開關**作為客製化輸入,並用 **Linkit7697** 來蒐集 sensor 資料傳回雲端,在 **Microsoft Azure 雲端**上運算出手勢命令(目前使用 Python 跑 **SVM(支持向量機)演算法**,可匹配當下手勢命令和 training data),再回傳至欲控制的終端裝置(如智慧燈泡)。 這個專案拿到了 **台大電機創客松 大會獎 Best Tech 入圍**。 團隊成員:陳麒升、張書鳴、張楹翔、王毓愷 ## Demo 影片 ## DEMO 簡報 ## 智慧手套成果 智慧手套照片(手心),指尖裝有 4 個電容觸碰開關: ![upload successful](/images/smart-gloves/pasted-5.png) 智慧手套照片(手背),食指指尖、手臂共裝 2 片 MPU6050 6 軸加速規陀螺儀,控制板使用 Linkit7697: ![upload successful](/images/smart-gloves/pasted-4.png) Sensor 資料視覺化(使用 Processing 寫成),即時呈現 MPU6050 raw data、換算後的姿態、觸碰開關狀態: ![upload successful](/images/smart-gloves/pasted-6.png) ## 細節解釋 ### 應用情境 - 職能治療輔具 - 智慧家庭控制 - 遊戲控制器 - 空氣樂器演奏 - 人機介面操作 - ... ### 系統架構 智慧手套系統架構圖: ![upload successful](/images/smart-gloves/pasted-7.png) ### 程式碼 程式碼(Github):[ChiShengChen/2018makeNTU_meowmeow](https://github.com/ChiShengChen/2018makeNTU_meowmeow) 內含: - 各種 sensor 測試用 Arduino code - 逐步演進的智慧手套 Arduino code - Processing 串接、資料視覺化 code - Python Sci-kit Learn SVM code - 開發中的 code:螢幕滑鼠控制、樹莓派串接等 ## 結語 我們這次創客松做智慧手套並不是為了得獎(當然能得最好啦 XD),而是我們覺得這東西好玩、而且我們會想用,所以我們做這個!這才是創客精神! 我們會繼續把這智慧手套做下去,近期目標是拿這手套來玩吃雞 🐔 如果你對這專案有什麼想法,或是想要更詳細的製作教學,歡迎在底下留言~ --- # 【Project】SigmaGO Smart Guide Dog - URL: https://easonchang.com/posts/sigmago - Date: 2018-03-30T16:40:00.000Z - Language: en - Description: The project aims to develop a low-cost smart guide dog to replace the traditional guide dogs that require extensive training, benefiting the lives of the visually impaired. Related Links: - Project Introduction (HackMD version): [SigmaGO Smart Guide Dog - HackMD](https://hackmd.io/s/Skcy2I1L-) - Code (Github): [eason-dev/SigmaGo](https://github.com/eason-dev/SigmaGo) ## Introduction **"SigmaGO Smart Guide Dog"** is a software-hardware integrated project we developed during the **[2017 National Taiwan University Hackathon HackNTU](https://hackntu.tumblr.com/tagged/intro)** in July 2017. The project's aim is to develop a low-cost smart guide dog to replace traditional guide dogs that require significant training costs, benefiting the lives of the visually impaired. Technologically, it uses a **Raspberry Pi 3** for processing, connected to the **[Olami Chinese Language Semantic Recognition Platform](https://tw.olami.ai/open/website/home/home_show)** provided by Via Technologies, to instantly recognize user's voice commands. The **Arduino** car (prototype of the mechanical guide dog) will then lead the user to their destination. This project won the **Via Technologies Super Hacker Award** and the **Technical Popularity Award** at the event. Team members: **[Eugene](https://github.com/eugenejahn), Shang, Jamie, Eason, Howard** ## SigmaGO Final Product ![](https://i.imgur.com/8E5onDg.jpg) ## Live Operation Demo Video ## Presentation at the Event ## Detailed Introduction ### Origin: Why the World Needs SigmaGO From selection, training to matching and service, guide dogs dedicate their lives to humanity, but it doesn't have to be this hard. We utilize smart technology to not only give the visually impaired an extra pair of eyes but also allow dogs to live more freely. > SigmaGO, Go For It! ### How to Use SigmaGO 1. Turn on the power switch! 2. Give commands to SigmaGo! 3. It will take you where you want to go 4. It's that simple! ### System Architecture Using the [OLAMI Chinese Semantic Analysis Platform](https://tw.olami.ai/open/website/home/home_show) provided by Via Technologies and the Raspberry Pi 3 hardware development kit provided on-site, connected to our custom Arduino mechanical dog, allowing visually impaired individuals to naturally command the mechanical dog in Chinese. The mechanical dog will then guide them to their desired location, improving the lives of the visually impaired! ![](https://i.imgur.com/7thls3j.jpg) ### Code SigmaGo code (Github): [eason-dev/SigmaGo](https://github.com/eason-dev/SigmaGo) Includes: Arduino mechanical dog firmware code, Raspberry Pi connection program, Olami rule file ### Awards [Via Technologies Super Hacker Award](https://www.facebook.com/hackNTU/photos/a.1442005269218025.1073741850.457953580956537/1442006189217933/?type=3&theater): ![upload successful](/images/sigmago/pasted-0.png) [Technical Popularity Award](https://www.facebook.com/hackNTU/photos/a.1442005269218025.1073741850.457953580956537/1442005299218022/?type=3&theater): ![upload successful](/images/sigmago/pasted-2.png) ## Follow-up Development My partner Eugene has continued to develop **SigmaGO Smart Guide Dog 2.0**, and his development records are here. Feel free to check it out: [SigmaGo Smart Guide Dog 2.0 Strikes Strong](https://tw.olami.ai/blog/article/34) --- # 【專案】SigmaGO 智慧導盲犬 - URL: https://easonchang.com/zh-TW/posts/sigmago - Date: 2018-03-30T16:40:00.000Z - Language: zh-TW - Description: 專案目的是開發低成本的智慧導盲犬,取代需要龐大成本訓練的傳統導盲犬,造福盲人朋友的生活。 相關連結: - 專案介紹(HackMD 版本):[SigmaGO 智慧導盲犬 - HackMD](https://hackmd.io/s/Skcy2I1L-) - 程式碼(Github):[eason-dev/SigmaGo](https://github.com/eason-dev/SigmaGo) ## 簡介 **「SigmaGO 智慧導盲犬」**,是我們在 2017 年 7 月時參加 **[2017 臺大黑客松 HackNTU](https://hackntu.tumblr.com/tagged/intro)** 時,開發的軟硬整合專案。 專案目的是開發低成本的智慧導盲犬,取代需要龐大成本訓練的傳統導盲犬,造福盲人朋友的生活。 技術上使用了 **Raspberry Pi 3** 做運算,串接威盛電子提供的 **[Olami 中文語意辨識平台](https://tw.olami.ai/open/website/home/home_show)**,即時辨識使用者的語音命令,**Arduino** 小車車(機械導盲犬的 Prototype)就會帶使用者前往目標地點。 這個專案最後拿下了**威盛電子超級黑客獎**和**大會技術人氣獎**。 團隊成員:**[Eugene](https://github.com/eugenejahn)、Shang、Jamie、Eason、Howard** ## SigmaGO 成品 ![](https://i.imgur.com/8E5onDg.jpg) ## 實際操作 Demo 影片 ## 大會 DEMO 簡報 ## 細節介紹 ### 緣起:為什麼世界需要 SigmaGO 從挑選、訓練到配對、服役,導盲犬為人類奉獻牠的一生,但其實可以不必這樣辛苦。 我們運用智慧科技,讓盲人在擁有另一雙眼睛的同時,也讓狗兒可以擁有更自由的生活。 > SigmaGO, Go For It! ### SigmaGO 使用方法 1. 打開電源開關! 2. 向 SigmaGo 下指令! 3. 牠將帶你到你要去的地方 4. 94 這麼簡單! ### 系統架構 使用威盛電子提供的 [OLAMI 中文語意分析平台](https://tw.olami.ai/open/website/home/home_show)及現場提供的 Raspberry Pi 3 硬體開發套件,連結自製的 Arduino 機械犬,讓盲胞可以自然地使用中文向機械犬下指令,機械犬就會引導盲胞前往想去的地點,改善盲人的生活! ![](https://i.imgur.com/7thls3j.jpg) ### 程式碼 SigmaGo 程式碼(Github):[eason-dev/SigmaGo](https://github.com/eason-dev/SigmaGo) 內含:Arduino 機械犬韌體程式碼、樹莓派串接程式、Olami 規則檔 ### 獎項 [威盛電子超級黑客獎](https://www.facebook.com/hackNTU/photos/a.1442005269218025.1073741850.457953580956537/1442006189217933/?type=3&theater): ![upload successful](/images/sigmago/pasted-0.png) [大會技術人氣獎](https://www.facebook.com/hackNTU/photos/a.1442005269218025.1073741850.457953580956537/1442005299218022/?type=3&theater): ![upload successful](/images/sigmago/pasted-2.png) ## 後續開發 我的夥伴有 Eugene 有繼續開發 **SigmaGO 智慧導盲犬 2.0**,他的開發記錄在這裡,歡迎移駕前往看看: [SigmaGo 智慧導盲犬 2.0 強勢出擊](https://tw.olami.ai/blog/article/34) --- # Trying Out Botkit, Let's Write a Slack Chatbot! - URL: https://easonchang.com/posts/slack-bot-with-botkit - Date: 2017-03-10T02:57:00.000Z - Language: en - Translation: AI-assisted # Trying Out Botkit, Let's Write a Slack Chatbot! Goal: build a working Slack chatbot What you need: - A computer with internet access that can run Node.js (I'm using Arch Linux) - A Slack team for testing (you can create your own or use an existing one) # What Is Botkit [Botkit](https://www.botkit.ai/) is an open-source chatbot development toolkit. It currently supports platforms like [Slack](https://slack.com/), [Facebook Messenger](https://www.messenger.com/), [Cisco Spark](https://www.ciscospark.com/), [Twilio IP Messaging](https://www.twilio.com/chat), and [Microsoft Bot Framework](https://dev.botframework.com/). Botkit's Github link: [https://github.com/howdyai/botkit](https://github.com/howdyai/botkit) # Downloading Botkit and Node.js ## Downloading Node.js (only needed if you haven't installed it before) Node.js is an open-source, cross-platform, lightweight JavaScript runtime, commonly used to build all kinds of web services. Botkit runs on Node.js, so you have to install Node.js before you can use Botkit. You can find installation instructions for each platform on the Node.js official website: https://nodejs.org/en/ On Arch Linux, just enter the following command to easily install Node.js with the system's built-in pacman package manager: ```bash sudo pacman -S nodejs npm ``` ## Downloading Botkit Botkit officially offers two ways to download it: via Git and via npm. Here we'll use Git, so the example code gets downloaded along with it. Enter the following command in your terminal: ```bash git clone https://github.com/howdyai/botkit ``` This downloads botkit into your current directory, including the botkit library and example programs. Then download the Node.js modules that Botkit needs: ```bash cd botkit/ npm install ``` # Setting Up Your First Slack Chatbot Once the download is done, it's time to put a chatbot into your Slack team! ## Slack-side Setup Open https://my.slack.com/services/new/bot in your browser (replace **my** with your **Slack team's own URL**). This link adds a new chatbot to your Slack team. Enter the name you want to give your bot in the **@username** field, then click **Add bot integration** to create it. I named my bot **mmbot**. ![](https://imgur.com/erLvNAU.png) Chatbot created! Next, you'll see an **API Token** on its settings page. **Copy it** — you'll need it in the next step. The chatbot backend program you're about to run needs this token to connect to the Slack bot you just set up. ![](https://imgur.com/W6FAR8S.png) ## Running the Botkit Backend Finally, let's test with the slack-bot.js example that botkit provides! Run it with the command below, remembering to replace `token=your-token` with the API Token you just copied: ```bash token=your-token node slack-bot.js ``` # Done! Say Hi to Your New Bot! Your new chatbot is all set up! Open Slack and go talk to it!! The slack-bot.js example supports a few very simple conversations. Try saying these to it: - Hello - What is your name? - Who am I? - Call me Eason! ![](https://imgur.com/6VI5HdN.png) --- # Botkit初體驗,寫一隻Slack聊天機器人吧! - URL: https://easonchang.com/zh-TW/posts/slack-bot-with-botkit - Date: 2017-03-10T02:57:00.000Z - Language: zh-TW # Botkit 初體驗,寫一隻 Slack 聊天機器人吧! 目標:寫出一隻能運作的 Slack 聊天機器人(Chatbot) 需要的資源: - 連網的可執行 Node.js 的電腦(我使用 Arch Linux) - 可供測試的 Slack team(可以自己建立或使用現有的 team) # 什麼是 Botkit [Botkit](https://www.botkit.ai/)是一套開源的聊天機器人開發工具,目前已支援[Slack](https://slack.com/)、[Facebook Messenger](https://www.messenger.com/)、[Cisco Spark](https://www.ciscospark.com/)、[Twilio IP Messaging](https://www.twilio.com/chat)、[Microsoft Bot Framework](https://dev.botframework.com/)等平台。 Botkit 的 Github 連結:[https://github.com/howdyai/botkit](https://github.com/howdyai/botkit) # 下載 Botkit 與 Node.js ## 下載 Node.js (如果之前沒裝過才需要裝) Node.js 是一套開源、跨平台輕量化的 JavaScript 執行環境,常用來開發各式網路服務。 Botkit 使用 Node.js 來運行,所以要使用 Botkit 你必須先安裝 Node.js 你可以在 Node.js 的官網上找到個平台的安裝方法: https://nodejs.org/en/ 而在 Arch Linux 上,只要輸入以下指令,就可使用系統內建的 pacman 套件管理軟體輕鬆安裝 Node.js: ```bash sudo pacman -S nodejs npm ``` ## 下載 Botkit Botkit 官方提供了兩種下載方式:透過 Git 與透過 npm。 這裡我們使用 Git 來下載,這樣可以連範例程式碼也一起載下來。 在你的電腦終端機中輸入以下指令: ```bash git clone https://github.com/howdyai/botkit ``` 這會將 botkit 下載至你的當前目錄裡,裡頭包含了 botkit 函式庫與範例程式。 接著下載 Botkit 所需的 Node.js 模組檔案: ```bash cd botkit/ npm install ``` # 設置你的第一隻 Slack 聊天機器人 下載完成後,我們就要在你的 Slack team 裡頭放入聊天機器人 Chatbot 了! ## Slack 端設定 用瀏覽器開啟 https://my.slack.com/services/new/bot (請將**my**置換成你的**Slack team 專屬的 URL 連結**)。 這連結用來在你的 Slack team 裡頭新增 Chatbot,在**@username**的欄位輸入你想為 bot 取的名字,然後按下**Add bot integration**來新增,這裡我將我的機器人取名為**mmbot** ![](https://imgur.com/erLvNAU.png) Chatbot 新增成功!接著你會在它的設定頁面看到一串**API Token 金鑰**,把它**複製起來**,下一步會用到。 待會你將執行的 Chatbot 後端程式,必須有這串 Token,才能連結上你剛剛設定的 Slack bot。 ![](https://imgur.com/W6FAR8S.png) ## Botkit 程式後端運行 最後我們使用 botkit 提供的 slack-bot.js 範例來測試!輸入指令來執行它,記得將`token=your-token`換成你剛剛複製起來的 API Token ```bash token=your-token node slack-bot.js ``` # 完成!跟你的新 Bot 打聲招呼吧! 至此你的新聊天機器人就設置完成了!趕快打開 Slack 跟他講講話吧!! slack-bot.js 範例支援幾句非常簡單的對話,試著跟他說以下的話: - Hello - What is your name? - Who am I? - Call me Eason! ![](https://imgur.com/6VI5HdN.png) --- # Build a Bluetooth Piano App! Using AppInventor with Arduino, HC05, and a Buzzer - URL: https://easonchang.com/posts/bluetooth-keyboard - Date: 2017-03-05T23:00:00.000Z - Language: en - Translation: AI-assisted Today we're building a simple Bluetooth piano app! - An Android app built with AppInventor 2 as the control interface - An Arduino UNO for hardware control - An HC-05 Bluetooth module to receive commands from the phone - A passive buzzer as the sound device DEMO video: [![](https://img.youtube.com/vi/hr8DXRMLY9s/0.jpg)](https://www.youtube.com/watch?v=hr8DXRMLY9s) ### Prerequisites - Basic AppInventor usage - Basic Arduino programming - Using the HC-05 Bluetooth module - Using a buzzer --- # Writing the Android App (with AppInventor) The app acts as the control interface of the Bluetooth piano. It tells the Arduino which note to play, over Bluetooth. Download the .aia file: [keyboard.aia](https://drive.google.com/file/d/0B89iCvlgxOnydm9oaW9mOVdNTzg/view?usp=sharing) ### Layout - At the top, **a ListPicker** for connecting to Bluetooth - In the middle, **eight buttons** as piano keys, from DO, RE, ... up to DO5 - At the bottom, **a button** for disconnecting Bluetooth - Plus an invisible **BluetoothClient** component ![](https://imgur.com/jv9FN5e.png) ### Programming - When the **Connect Bluetooth** ListPicker is tapped, list all paired Bluetooth devices - After the **Connect Bluetooth** ListPicker has a selection, connect to Bluetooth - When the **Disconnect** button is pressed, disconnect - When a **DO~DO5** key button is pressed, send **'1'~'8'** over Bluetooth to tell the Arduino to play the note - When a **DO~DO5** key button is released, send **'S'** over Bluetooth to tell the Arduino to stop the sound ![](https://imgur.com/ouu8fbu.png) --- # Hardware Wiring ### Materials: - Arduino UNO x1 (any UNO-like board works) - Passive buzzer x1 - HC-05 Bluetooth module x1 (HC-06 works too) ### Wiring: - HC-05 RX to pin 11, TX to pin 10 - Buzzer positive pin to pin 6 (must support PWM) ![](https://imgur.com/SFuIdNX.png) --- # Arduino Code - Keep reading data from Bluetooth - On receiving '1'~'8', play the corresponding note - On receiving 'S', stop the sound ```c #include SoftwareSerial BTSerial(10, 11); // 藍芽Serial使用的pin腳(Arduino的RX,TX,對應到HC05的TX,RX) #define DO 262 // 音階頻率table #define RE 294 #define MI 330 #define FA 349 #define SOL 392 #define LA 440 #define SI 494 #define DO5 523 #define BUZZER 6 // 蜂鳴器pin腳 void setup() { BTSerial.begin(9600); } void loop() { if (BTSerial.available()){ char note = BTSerial.read(); switch(note){ case '1': // 若接收到字元'1'(從手機App傳來的) tone(BUZZER, DO); // 就開始發出DO的音 break; case '2': // 以此類推 tone(BUZZER, RE); break; case '3': tone(BUZZER, MI); break; case '4': tone(BUZZER, FA); break; case '5': tone(BUZZER, SOL); break; case '6': tone(BUZZER, LA); break; case '7': tone(BUZZER, SI); break; case '8': tone(BUZZER, DO5); break; case 'S': // 若接收到'S' noTone(BUZZER); // 停止發音 break; } } } ``` Done! Now you can play beautiful music with your phone! --- # 開發藍芽鋼琴APP!使用AppInventor結合Arduino、HC05、與蜂鳴器 - URL: https://easonchang.com/zh-TW/posts/bluetooth-keyboard - Date: 2017-03-05T23:00:00.000Z - Language: zh-TW 今天我們要實作簡易藍芽鋼琴 APP! - 用 AppInventor 2 開發 Android APP 作為控制介面 - Arduino UNO 作硬體控制 - HC-05 藍芽模組接收手機指令 - 一個無源蜂鳴器作為發音設備 DEMO 影片: [![](https://img.youtube.com/vi/hr8DXRMLY9s/0.jpg)](https://www.youtube.com/watch?v=hr8DXRMLY9s) ### 先備知識 - 基礎 AppInventor 操作 - 基礎 Arduino 程式撰寫 - HC-05 藍芽模組使用 - 蜂鳴器使用 --- # Android App 撰寫(使用 AppInventor) APP 是當作藍芽鋼琴的控制介面,會透過藍芽告訴 Arduino 應該發出哪個聲音。 下載.aia 檔:[keyboard.aia](https://drive.google.com/file/d/0B89iCvlgxOnydm9oaW9mOVdNTzg/view?usp=sharing) ### 外觀編排 - 最上方有**一個清單選擇器**,用來連接藍芽 - 中央**八個按鈕**是琴鍵,從 DO,RE,...到 DO5 - 下方是**一個按鈕**,用來取消藍芽連接 - 還有一個不可見的**藍芽客戶端**元件 ![](https://imgur.com/jv9FN5e.png) ### 程式設計 - **連接藍芽**清單選擇器被按下時,列出所有已配對藍芽 - **連接藍芽**清單選擇器已選擇後,連接藍芽, - **取消連接按鈕**被按下時,取消連線 - **DO~DO5**琴鍵按鈕被按下時,藍芽送出訊號**'1'~'8'**,告訴 Arduino 該發聲了 - **DO~DO5**琴鍵按鈕被鬆開時,藍芽送出訊號**'S'**,告訴 Arduino 停止發聲 ![](https://imgur.com/ouu8fbu.png) --- # 硬體接線 ### 需求材料: - Arduino UNO x1(或其他類 UNO 開發版都可) - 無源蜂鳴器 x1 - HC-05 藍牙模組 x1(HC-06 也可) ### 接線: - HC-05 的 RX 接 pin11、TX 接 pin10 - 蜂鳴器正極端接 pin6(須具有 PWM 功能) ![](https://imgur.com/SFuIdNX.png) --- # Arduino 程式 - 不斷從藍芽收資料 - 收到'1'~'8'的話,發出對應的音 - 收到'S'的話,停止發音 ```c #include SoftwareSerial BTSerial(10, 11); // 藍芽Serial使用的pin腳(Arduino的RX,TX,對應到HC05的TX,RX) #define DO 262 // 音階頻率table #define RE 294 #define MI 330 #define FA 349 #define SOL 392 #define LA 440 #define SI 494 #define DO5 523 #define BUZZER 6 // 蜂鳴器pin腳 void setup() { BTSerial.begin(9600); } void loop() { if (BTSerial.available()){ char note = BTSerial.read(); switch(note){ case '1': // 若接收到字元'1'(從手機App傳來的) tone(BUZZER, DO); // 就開始發出DO的音 break; case '2': // 以此類推 tone(BUZZER, RE); break; case '3': tone(BUZZER, MI); break; case '4': tone(BUZZER, FA); break; case '5': tone(BUZZER, SOL); break; case '6': tone(BUZZER, LA); break; case '7': tone(BUZZER, SI); break; case '8': tone(BUZZER, DO5); break; case 'S': // 若接收到'S' noTone(BUZZER); // 停止發音 break; } } } ``` 完成!現在你可以用手機彈奏美妙的音樂了! --- # 《Why Do Smart People Use Grid Notebooks?》, Level Up Through Note-Taking - URL: https://easonchang.com/posts/grid-note - Date: 2016-12-13T05:44:00.000Z - Language: en - Translation: AI-assisted > Book: Why Do Smart People Use Grid Notebooks? > Author: Masafumi Takahashi > Who should read this book: anyone who wants to process information more efficiently > What this book gives you: the point of "taking notes", and the techniques for it This book isn't really about the benefits of grid notebooks — it's about "how to take notes effectively". # The point of taking notes Taking notes is about processing information, and there are three situations where you need to process information: - When learning something new - When thinking and making decisions - When communicating and persuading others ## When learning something new Whatever you're doing, learning always comes first. Here the focus of note-taking is "storing". A common trap is students copying the board word for word during class. Storing like that has zero effect. Changing just one habit dramatically improves how well things stick: "look up and read the board first, hold it in your head for a moment, then look down and **restate it in your notes in your own words**". Because the notes are now written in your own words, they'll actually mean something when you read them later — that's when storing works. ## When thinking and making decisions The goal of thinking is to reach a conclusion. Here the focus of note-taking is "discarding". Concretely, you keep **condensing** what you stored in the previous step, and use **arrows and connecting lines** to find the relationships between pieces of information, until a conclusion emerges. ## When communicating and persuading others Reaching a conclusion isn't enough. To communicate a message effectively, it's best to expand the conclusion into **concrete, actionable** steps. # Note-taking techniques ## Why a grid notebook? The advantage of a grid notebook is that it's **easy to draw and lay things out on**. That's it. The point isn't the notebook's format, but how you use it. ## How the grid note-taking method works 1. One topic per page, with the title written at the top of the page 2. Split the page into three blocks, from left to right: facts → interpretation → action 3. The facts block records the board, matching the "storing" function — separate objective facts from subjective opinions, for example with different pen colors 4. The interpretation block condenses the facts, matching the "thinking" function 5. The action block sums up the actions you'll take, matching the "communicating" function The author also brings up two interesting ideas that I find valuable too: - The size of your notebook determines the size of your thinking. Don't buy a notebook that's too small — A4 is the industry standard, and you should buy an A4 grid notebook - Don't be stingy about investing a little money in pens and notebooks that write well; enjoying the writing is what keeps you writing # My take I believe reading has only one purpose: changing your own behavior. The fastest way to do that is to practice what the book says while you're reading it. So halfway through this book, I went to a bookstore, bought a grid notebook, and took notes as I read. Below are my notes. ![image](https://imgur.com/eiwtub7.jpg) I've never doubted the importance of taking notes, and I've always used note-taking apps, because I figured digital notes are more efficient to record than paper ones. But after more than a year of doing this, I hit a big obstacle: I had recorded a lot of content, but my thinking stayed shallow. I also had no habit of condensing and organizing my earlier notes, so when I needed them, my notes were too messy to pull the key points from. Because I noticed this problem, I decided to start over with paper notes, which is why I borrowed 《Why Do Smart People Use Grid Notebooks?》 to read. Back to the point: writing paper notes feels completely different from writing digital ones. On paper, your writing speed is limited, so you filter for the key points much more seriously. And with the three-block structure of "facts, interpretation, action", it forces you to keep condensing the facts and adding your own viewpoint, deepening your thinking. --- # 《為什麼聰明人都用方格筆記本》,用筆記術變身強者吧 - URL: https://easonchang.com/zh-TW/posts/grid-note - Date: 2016-12-13T05:44:00.000Z - Language: zh-TW > 書名:為什麼聰明人都用方格筆記本 > 作者:高橋政史 > 誰適合讀這本書:想提升資訊處理效率的人 > 這本書能給你什麼:告訴你「做筆記」的意義及技巧 這本書並不是在談方格筆記本的好處,而是在談「如何有效做筆記」。 # 做筆記的意義 做筆記是為了處理資訊,而在三種情境下,你會需要處理訊息: - 學習新知識時 - 思考、做決策時 - 傳達、說服別人時 ## 學習新知識時 不論做什麼事,學習總是第一步,此時做筆記的重心在「儲存」。 而常見的誤區是學生在聽老師上課時,一字不漏的抄寫板書,這樣的儲存是完全沒有效果的。 只要改變一個習慣,就可以大幅改善儲存效果,那就是「抬頭看完板書後,先將板書內容暫時塞進腦袋裡,然後低頭**用自己的話轉述進筆記裡**」。如此一來,因為記下來的筆記是用你自己的話寫的,你事後再來讀才有感覺,儲存才有效。 ## 思考、做決策時 思考的目的在於導出結論,此時做筆記的重心在「捨去」。 具體做法是不斷**濃縮**前一步驟儲存下來的內容,並善用**箭頭及連線**找出各資訊的關聯性,以導出結論。 ## 傳達、說服別人時 導出結論還不夠,要有效傳達訊息時最好能將結論再拓展成**具體可執行**的步驟。 # 做筆記的技巧 ## 為什麼是方格筆記本? 方格筆記本的優點是**方便繪圖、排版**,就這樣,重點不在筆記本的形式,而是你如何使用筆記本。 ## 方格筆記術操作方法 1. 一頁只寫一個主題,頁面最上方寫上標題 2. 將畫面分割成三個區塊,由左至右是:事實 → 解釋 → 行動 3. 事實區用來記板書,對應到「儲存」功能,要分清楚客觀事實及主觀看法,可用筆的顏色做區隔 4. 解釋區用來濃縮事實,對應到「思考」功能 5. 行動區用來總結將執行的行動,對應到「傳達」功能 另外作者還講到了兩個有趣的概念,我認為也很有價值: - 筆記本的大小決定你思考的大小,不要買太小的筆記本,A4 是業界標準,你應該買 A4 的方格筆記本 - 不要吝嗇於投資一點錢在好書寫的筆和筆記本上,寫得開心才寫得長久 # 我的看法 我認為閱讀只有一個目的:改變自己的行為。 要做到這點,最快的方法就是邊讀邊實踐書中的內容,所以這本書我讀到一半時,就跑去書局買了本方格筆記本回來,邊讀邊做筆記,下面是我的筆記。 ![image](https://imgur.com/eiwtub7.jpg) 我從沒懷疑過做筆記的重要性,而我一直是用筆記軟體來做筆記,因為我認為電子式的筆記記錄效率比紙本好。但目前實行一年多了,遇到一個很大的障礙是記了很多內容,但思考深度有限,且平時也沒有習慣去濃縮、整理先前的筆記,所以在需要使用時,也因為筆記太過凌亂,而沒辦法有效提取到重點。 因為發現了這個問題,所以我打算重新開始使用紙本筆記,因此才借了《為什麼聰明人都用方格筆記本》這本書來讀。 回到重點,寫紙本筆記給我的感覺跟寫電子筆記完全不同。紙本筆記因為寫字速率被限制,你會更認真去篩選重點。同時,因為有了「事實、解釋、行動」三層架構,它會迫使你不斷地去濃縮事實,以及加入自己的觀點,提升你的思考深度。 --- # [Book Notes] 《The Charisma Myth》, Your First Book for Building Charisma - URL: https://easonchang.com/posts/myth-of-charisma - Date: 2016-11-18T02:37:00.000Z - Language: en - Translation: AI-assisted > Book: The Charisma Myth: How Anyone Can Master the Art and Science of Personal Magnetism > Author: Olivia Fox Cabane > One-line summary: If you have no idea how to build charisma, this is a great introductory book; if you've already read plenty on the subject, close the book and go live your life. # A systematic analysis of "charisma" The book's purpose is to "analyze what creates charisma, and provide techniques for it". The first half of the book dissects charisma systematically. Charisma has three core elements. Within seconds of meeting someone, people automatically judge them on: - Presence — "Are you truly living in the moment?" - Power — "Are you able to change the world?" - Warmth — "Are you willing to do it for me?" Different combinations of these three elements produce four charisma styles: - Authority — Churchill - Visionary — Steve Jobs - Focus — Elon Musk - Kindness — the Dalai Lama Everyone has charisma styles they're naturally good at, and different social situations call for different styles. The book offers a pretty solid theoretical framework for strengthening your charisma strengths and shoring up your weaknesses. # All kinds of charisma exercises The second half of the book provides plenty of applied examples of charisma, along with practical techniques. The techniques here are many and varied, and I personally prefer simple concepts over elaborate techniques, so I won't go through them — if you're interested, go read the book yourself. # My verdict I believe the point of reading is to "change your own behavior", and every book should be written with that goal in mind. There are two concrete ways to get there: 1. Present novel ideas that break through the reader's understanding, achieving a "change in knowing" 2. Serve up chicken soup for the soul that fires the reader up, achieving a "change in doing" For me, 《The Charisma Myth》 achieved neither. I think that's because I'm not the book's target audience. I regularly read articles about charisma, so the book didn't give me many new ideas. I also don't feel that I lack charisma, so however much the author encouraged me that "charisma can be learned", it couldn't move me. That said, it's still an excellent theory book, well suited as the first book for readers who have no clue how to start building charisma. If I had to name one takeaway, the biggest thing I got from 《The Charisma Myth》 is a renewed appreciation of how important "living in the moment (building presence)" is — the basic requirement for showing charisma. Below is my reading summary, presented as slides, for reference~ --- # 【閱讀筆記】《魅力學》,培養魅力的第一本書 - URL: https://easonchang.com/zh-TW/posts/myth-of-charisma - Date: 2016-11-18T02:37:00.000Z - Language: zh-TW > 書名:魅力學:無往不利的自我經營術 > 作者:Olivia Fox Cabane > 一句話書摘:若你對如何培養魅力沒有概念,這會是本很棒的入門書;而若你已接觸過不少相關概念,闔上書專心過生活吧。 # 對「魅力」進行系統化分析 本書宗旨在「分析魅力的成因,並提供相關技巧」。 書籍前半部,有系統地剖析了魅力。 魅力有三大要素,人們在見到一個人的幾秒鐘內,會自動對他進行以下判斷: - 臨在感——「你是否認真地活在當下?」 - 影響力——「你是否有能力改變世界?」 - 親和力——「你願意為我這麼做嗎?」 而此三要素的排列組合,造就了四種魅力類型: - 權威型——邱吉爾 - 遠見型——賈伯斯 - 專注型——馬斯克 - 仁慈型——達賴喇嘛 每個人都有自己擅長的魅力類型,而每種社交場合適用的魅力類型也不同,書中提供了相當紮實的理論框架,讓讀者能強化自己的魅力強項、補正魅力弱項。 # 各式魅力練習技巧 本書後半部,提供了大量的魅力應用範例、及可操作的技巧。 由於此處技巧種類繁雜,而且我個人偏好簡單的概念,而非繁複技巧,所以在此不想贅述,有興趣的讀者請自行閱讀。 # 我的評價 我認為閱讀的意義在於「改變自身的行為」,任何書都應該以此為目的進行書寫。 具體方向有二: 1. 提出突破讀者認知的新穎觀點,達成「知的改變」 2. 端出心靈雞湯讓讀者爽,達成「行的改變」 對我而言,《魅力學》兩點都沒有達成。 我認為是因為我並非此書的目標客群。我平時就常閱讀有關魅力的文章,所以《魅力學》並沒有提供我太多新觀點,同時我也不覺得我自身缺乏魅力,所以作者再怎麼鼓勵我「魅力是可以培養的」,也無法打動我。 但本書依舊是本相當出色的理論書籍,適合完全不知道該如何培養魅力的讀者,作為入門的第一本書。 如果真要說,讀完《魅力學》,我最大的收穫就是重新認知到「活在當下(培養臨在感)」的重要性,此為展現魅力的基本條件。 以下是我做的閱讀摘要,以簡報形式做呈現,供參~ --- # Using SSH with a Putty Key on MacOS - URL: https://easonchang.com/posts/using-ssh-with-putty-key-on-macos - Date: 2016-08-18T11:00:00.000Z - Language: en - Translation: AI-assisted # Quick Reference: ```bash $ brew install putty #安裝puttygen套件 $ puttygen -O private-openssh -o #將putty key轉換成ssh key $ chmod 400 #調整.pem權限 $ ssh -i #使用.pem進行ssh連線 ``` --- I suddenly found a small bug on my academic program's website, so I needed to connect to the AWS (Amazon Web Service) server hosting the site to fix the code. But the senior who built the site only left behind a .ppk Putty-only SSH key file, and my poor Mac really doesn't have the disk space to install a copy of Windows just to run Putty. So I started digging into how to make MacOS's built-in ssh command accept this .ppk putty key file. Here's the solution I found: # 1. Use puttygen to Convert the Putty Key into an SSH-usable Key The ssh command can't accept a .ppk putty key — you have to convert it into a key ssh can use first. The puttygen program can do the conversion for you. You can install it with [Homebrew](http://brew.sh/index_zh-tw.html): ```bash $ brew install putty ``` Then enter the following command to convert the putty key into an ssh key: ```bash $ puttygen -O private-openssh -o ``` # 2. Set Proper Access Permissions on the SSH Key If the permissions on your ssh key file are too open, the server will block you from connecting with this key, so you have to set proper permissions on it (sometimes it's already correct without setting anything): ```bash $ chmod 400 ``` # 3. Connect Now you can use it to connect! ```bash $ ssh -i ``` --- Reference: [https://stackoverflow.com/questions/3475069/use-ppk-file-in-mac-terminal-to-connect-to-remote-connection-over-ssh](https://stackoverflow.com/questions/3475069/use-ppk-file-in-mac-terminal-to-connect-to-remote-connection-over-ssh) --- # 在MacOS使用putty key進行ssh連線 - URL: https://easonchang.com/zh-TW/posts/using-ssh-with-putty-key-on-macos - Date: 2016-08-18T11:00:00.000Z - Language: zh-TW # Quick Reference: ```bash $ brew install putty #安裝puttygen套件 $ puttygen -O private-openssh -o #將putty key轉換成ssh key $ chmod 400 #調整.pem權限 $ ssh -i #使用.pem進行ssh連線 ``` --- 突然發現學程的網站有小 bug,所以需要連上網站所在的 AWS(Amazon Web Service)伺服器裡修改程式碼。但建置網站的學長只留下了一個.ppk 的 putty 專用 SSH 認證檔,而我這台破 Mac 實在沒容量給我再灌一台 Windows 然後裝上 Putty 來連線,所以我就開始研究如何讓 MacOS 內建的 ssh 指令也能吃進這支.ppk 的 putty key 認證檔。 以下是我找到的解法: # 1.使用 puttygen 將 putty key 轉換成 ssh 可用的 key ssh 指令無法接受.ppk 的 putty key,必須先轉換成 ssh 可以使用的 key puttygen 這支程式可以幫你轉換,你可以使用[Homebrew](http://brew.sh/index_zh-tw.html)來安裝它: ```bash $ brew install putty ``` 接著可以輸入下面這條指令,將 putty key 轉換成 ssh key: ```bash $ puttygen -O private-openssh -o ``` # 2.為 ssh key 設定適當的存取權限 如果你的 ssh key 檔的權限太過開放,伺服器會阻擋你使用這支 key 來連線,所以你必須為它設定適當的權限(有時候不需要設定權限就是對的了): ```bash $ chmod 400 ``` # 3.連線 接著你可以用它來進行連線啦! ```bash $ ssh -i ``` --- Reference: [https://stackoverflow.com/questions/3475069/use-ppk-file-in-mac-terminal-to-connect-to-remote-connection-over-ssh](https://stackoverflow.com/questions/3475069/use-ppk-file-in-mac-terminal-to-connect-to-remote-connection-over-ssh) --- # Start Your Rails Project in Production Mode - URL: https://easonchang.com/posts/start-your-rails-project-with-production-mode - Date: 2016-08-16T02:34:00.000Z - Language: en - Translation: AI-assisted # Quick Reference: ```bash $ RAILS_ENV=production rake db:migrate $ rake secret //產生secret key $ export SECRET_KEY_BASE=[貼入上個指令產生出來的密鑰] $ vim config/initializers/assets.rb //加入用到的assets路徑 確認各處引用assets的方法正確 $ RAILS_ENV=production rake assets:precompile $ vim config/environments/production.rb //將這行改成true: config.serve_static_assets = true $ rails s -e production ``` --- # What Is Production Mode? Why Use It? Normally, when you run `$ rails s` in your project, you're starting it in development mode. In development mode, most changes show up on the page right away, which is convenient for development. But when the system actually goes live, we don't use development mode — we use **production mode**. In production mode, rails pre-compiles the assets together and puts them under the public/ folder for access, which **improves site performance**. # Starting a Rails Project in Production Mode ## 0. Migrate Your Database Production mode and development mode use separate databases, so you need to migrate again: ```bash $ RAILS_ENV=production rake db:migrate ``` ## 1. Edit config/secret.yml to Set the secret_key_base for Production Mode To run a rails app properly, you need to set something called the **secret_key_base**, a secret key rails uses to verify browser cookies. rails has already set it up for development mode and test mode, but not for production mode — you have to do it yourself. The setting lives in the **config/secret.yml** file. Let's take a look at it: ```c secret.yml // development mode的設定 // 它使用 b1f4eb...3a9 這串密鑰(隨機生成的) development: secret_key_base: b1f4eb6c1d97f627fbc0a3919fc27ab5f831200cdd0ea72317f404a2b9d878192bca27d92380f56051c60d0a364c86fe53d9b9866d487a17cf06582a783723a9 // test mode使用 fe9d...c3b 這串密鑰 test: secret_key_base: fe9d49b7edc8e3a4dfa47c0682974aca4572388e929ea3e4f037b3e8841747fc3029e030bbd102a62d372596b4d753dd0c419b7425a8dcceca3817cf7d5dbc3b // production mode從執行環境裡抓 $SECRET_KEY_BASE 這個環境變數當作密鑰,但正常情況下該變數並不存在,所以出錯 production: secret_key_base: <%= ENV["SECRET_KEY_BASE"] %> ``` You have two ways to set the secret_key_base for production mode. ### (1) The simpler way is to hardcode a fixed key: ```c secret.yml // 前略... production: // 刪掉原本的 <%= ... %> ,替換成隨便一串長度30以上的英數組合字串給他當密鑰 secret_key_base: iameasonchangiamhandsomeiamthekingoftheworldnoonecandefeatmeiwantagirlfriendiamaloserqq ``` ### (2) The safer way is to set the key in your environment: ```bash $ rake secret // 生成一串密鑰,顯示在終端機上 $ export SECRET_KEY_BASE=[此處貼上你剛剛產生的密鑰] // 把密鑰設定至環境變數 $SECRET_KEY_BASE ``` But if you stop there, the environment variable disappears when you restart your terminal, so you can write the setting into your shell config file. For me, using zsh, the config file is **./.zshrc** For Mac's default terminal shell bash, it's **./.bash_profile** ```bash .zshrc // 在.zshrc中插入下面這行 export SECRET_KEY_BASE=[此處貼上你剛剛產生的密鑰] ``` At this point you should be able to start your rails project in production mode without errors. Run `$ rails s -e production` to start the server, then browse to `localhost:3000` to check the current result. Your screen probably looks a lot like mine. Why is it so ugly? Where's the beautiful page I designed without sleeping for days? ![Imgur](https://i.imgur.com/oVAdVyK.png?2) That's because you haven't told rails which design assets you're using yet. The next steps show you how: ## 2. Tell rails Which Assets Need to Be Pre-compiled As mentioned earlier, the reason to use production mode is to **improve page load performance**, and the way it does that is by **pre-compiling all kinds of assets**. You need to manually tell rails which assets need pre-compiling. Apart from image files, every asset you use needs to be added, such as js and css files. To do this, update the **config/initializers/assets.rb** file and add the paths of the assets you use: ```c++ assets.rb // 前略... // 後面加入這一行,裡頭放入你要precompile的檔案 Rails.application.config.assets.precompile += [ 'bootstrap.js', // 請幫我compile vendor/assets/javascripts/bootstrap.js 這支檔案 'courses/content.js', // 還有 app/assets/javascripts/courses/content.js 'courses/chart.js', // app/assets/javascripts/courses/chart.js 也要 'new-index.css', // 還有這個css,它在app/assets/stylesheets/new-index.css.scss 'login.css', // app/assets/stylesheets/login.css.erb也要 'newcomer/*.css', // app/assets/stylesheets/newcomer/裡面的css檔都要! 'NotoSans-Regular-ttf' // 這個是字形檔唷!它在app/assets/fonts/NotoSans-Regular-ttf ] ``` rails looks for your asset files in the **app/assets/** and **vendor/assets/** folders, and it looks in the corresponding path based on the file extension: - js files are looked up under **javascripts/** - css files are looked up under **stylesheets/** - font files are looked up under **fonts/** - image files are looked up under **images/** But rails compiles all image files for you by default, so **you don't need to add .png, .jpg, and other image files**. ## 3. Make Sure You're Referencing Assets Correctly After compiling, asset filenames change, so the traditional way of referencing assets stops working. You have to use the methods rails provides instead: - To include a js file, use `<%= javascript_include_tag 'your-js-path' %>` - To include a css file, use `<%= stylesheet_link_tag 'your-css-path' %>` - To include an image, use `<%= image_tag 'your-image-path' %>` - To reference an asset inside a js file, use `<%= asset_path('your-img-path') %>` ## 4. Pre-compile it! Run this command in your project directory: ```bash $ RAILS_ENV=production rake assets:precompile ``` This pre-compiles the assets into /public/assets. ## 5. Update the Production Mode Settings The assets are compiled, but by default production mode won't let users access the files in /public/assets. You have to edit the **config/environments/production.rb** file. Around line 24, change ```ruby production.rb config.serve_static_assets = false ``` to **true**: ```ruby production.rb config.serve_static_assets = true ``` ## 6. Start It in Production Mode!! Run this command in your project directory: ```bash $ rails s -e production ``` And your rails project starts in production mode! Browse to `localhost:3000` and you'll see your beautiful page, now loading much faster! ![imgur](https://imgur.com/IGxS4QM.jpg) --- Test environment: - OS: Mac OS X - Rails version: 4.2.6 - Ruby version: 2.0.0p481 - Shell: zsh --- # 使用production mode啟動你的rails專案 - URL: https://easonchang.com/zh-TW/posts/start-your-rails-project-with-production-mode - Date: 2016-08-16T02:34:00.000Z - Language: zh-TW # Quick Reference: ```bash $ RAILS_ENV=production rake db:migrate $ rake secret //產生secret key $ export SECRET_KEY_BASE=[貼入上個指令產生出來的密鑰] $ vim config/initializers/assets.rb //加入用到的assets路徑 確認各處引用assets的方法正確 $ RAILS_ENV=production rake assets:precompile $ vim config/environments/production.rb //將這行改成true: config.serve_static_assets = true $ rails s -e production ``` --- # 什麼是 production 模式?為什麼要用它? 在通常情況下,直接在你的專案裡下`$ rails s`這條指令,我們是使用 development mode 來啟動你的專案 在 development 模式下,大部分的修改都會直接顯現在網頁上,方便開發 但實際讓系統上線,我們不會使用 development 模式,而是**production 模式** 在 production 模式裡,rails 會將 assets 預編譯在一起(pre-compile),並放在 public/資料夾底下以供存取,這能**增進網站效能** # 使用 production mode 啟動 rails 專案 ## 0.migrate 你的資料庫 production mode 和 development mode 的資料庫是分開的,所以你需要再 migrate 一次 ```bash $ RAILS_ENV=production rake db:migrate ``` ## 1.修改 config/secret.yml,設定 production mode 的 secret_key_base 要順利運行 rails 程式,你需要設定一個叫**secret_key_base**的東西,它是 rails 用來認證瀏覽器 cookie 的一串密鑰。rails 已經幫你在 development mode 和 test mode 設定好了,但 production mode 沒有,你得自己來,它的設定在**config/secret.yml**這隻檔案,讓我們來看看它: ```c secret.yml // development mode的設定 // 它使用 b1f4eb...3a9 這串密鑰(隨機生成的) development: secret_key_base: b1f4eb6c1d97f627fbc0a3919fc27ab5f831200cdd0ea72317f404a2b9d878192bca27d92380f56051c60d0a364c86fe53d9b9866d487a17cf06582a783723a9 // test mode使用 fe9d...c3b 這串密鑰 test: secret_key_base: fe9d49b7edc8e3a4dfa47c0682974aca4572388e929ea3e4f037b3e8841747fc3029e030bbd102a62d372596b4d753dd0c419b7425a8dcceca3817cf7d5dbc3b // production mode從執行環境裡抓 $SECRET_KEY_BASE 這個環境變數當作密鑰,但正常情況下該變數並不存在,所以出錯 production: secret_key_base: <%= ENV["SECRET_KEY_BASE"] %> ``` 你有兩種方法能設定 production mode 的 secret_key_base ### (1)比較簡易的做法是直接寫死一串固定的密鑰給它: ```c secret.yml // 前略... production: // 刪掉原本的 <%= ... %> ,替換成隨便一串長度30以上的英數組合字串給他當密鑰 secret_key_base: iameasonchangiamhandsomeiamthekingoftheworldnoonecandefeatmeiwantagirlfriendiamaloserqq ``` ### (2)更安全的做法是把密鑰設在執行環境中: ```bash $ rake secret // 生成一串密鑰,顯示在終端機上 $ export SECRET_KEY_BASE=[此處貼上你剛剛產生的密鑰] // 把密鑰設定至環境變數 $SECRET_KEY_BASE ``` 但只做到這樣的話,重開終端機的話環境變數設定就消失了,所以可以把設定寫進環境設定檔裡頭 像我使用 zsh 的話,環境設定檔是**./.zshrc** 而 Mac 的 terminal 預設的 bash,則是**./.bash_profile** ```bash .zshrc // 在.zshrc中插入下面這行 export SECRET_KEY_BASE=[此處貼上你剛剛產生的密鑰] ``` 到此你應該可以無錯誤地使用 production mode 開啟你的 rails 專案了,你可以下`$ rails s -e production`來開啟伺服器,並且用瀏覽器連至`localhost:3000`查看目前的結果 你的畫面應該跟我很像,怎麼醜不拉幾的?我不眠不休設計出來的精美網頁呢? ![Imgur](https://i.imgur.com/oVAdVyK.png?2) 那是因為你還沒有告訴 rails 你用到了哪些設計元素,接下來的步驟教你怎麼做: ## 2.告訴 rails 你有哪些 assets 需要被 pre-compile 前面我們提過,使用 production mode 的理由是**增進網頁讀取效能**,方法是**預編譯(pre-compile)各類 assets**,而你需要手動告訴 rails 你有哪些 assets 需要預編譯,除了圖檔,所有使用到的 assets 都需要加入裡頭,如 js、css 檔 方法是更新**config/initializers/assets.rb**這隻檔案,加入你有使用到的 assets 的路徑: ```c++ assets.rb // 前略... // 後面加入這一行,裡頭放入你要precompile的檔案 Rails.application.config.assets.precompile += [ 'bootstrap.js', // 請幫我compile vendor/assets/javascripts/bootstrap.js 這支檔案 'courses/content.js', // 還有 app/assets/javascripts/courses/content.js 'courses/chart.js', // app/assets/javascripts/courses/chart.js 也要 'new-index.css', // 還有這個css,它在app/assets/stylesheets/new-index.css.scss 'login.css', // app/assets/stylesheets/login.css.erb也要 'newcomer/*.css', // app/assets/stylesheets/newcomer/裡面的css檔都要! 'NotoSans-Regular-ttf' // 這個是字形檔唷!它在app/assets/fonts/NotoSans-Regular-ttf ] ``` rails 會去 **app/assets/** 和 **vendor/assets/** 這兩個資料夾去找你的 assets 檔案,並且會根據副檔名尋找對應的路徑: - js 檔會去找底下的 **javascripts/** - css 檔會去找底下的 **stylesheets/** - font 檔會去找底下的 **fonts/** - image 檔會去找底下的 **images/** 但 rails 預設會幫你 compile 所有的圖檔,所以**.png、.jpg 等圖檔就不用加進去**了 ## 3.確認你引入 assets 的方法有沒有誤 compile 後的 assets,檔名會改變,所以傳統引入 assets 的方法會失效,必須改用 rails 提供的方法 - 引入 js 檔使用 `<%= javascript_include_tag 'your-js-path' %>` - 引入 css 檔使用 `<%= stylesheet_link_tag 'your-css-path' %>` - 引入圖檔使用 `<%= image_tag 'your-image-path' %>` - js 檔裡要再引入 asset 請使用 `<%= asset_path('your-img-path') %>` ## 4.Pre-compile it! 在你的專案目錄裡下指令: ```bash $ RAILS_ENV=production rake assets:precompile ``` 就會將 assets pre-compile 進/public/assets 裡頭 ## 5.修改 production 的模式設定 雖然 compile 了,但 production 模式預設是不會讓使用者讀取/public/assets 的檔案 必須修改 **config/environments/production.rb** 這個檔案 將位在第 24 行左右的 ```ruby production.rb config.serve_static_assets = false ``` 改為 **true**: ```ruby production.rb config.serve_static_assets = true ``` ## 6.使用 production mode 啟動吧!! 在專案的目錄裡下指令: ```bash $ rails s -e production ``` 就可以使用 production mode 開啟你的 rails 專案啦! 使用瀏覽器連入`localhost:3000`,就可以看到你速度大幅提升的美麗網頁啦! ![imgur](https://imgur.com/IGxS4QM.jpg) --- 測試環境: - OS: Mac OS X - Rails version: 4.2.6 - Ruby version: 2.0.0p481 - Shell: zsh --- # 【DailyUI #003】Landing Page - URL: https://easonchang.com/posts/dailyui-003-landing-page - Date: 2016-05-10T19:43:00.000Z - Language: en - Translation: AI-assisted Here's today's DailyUI result: a landing page for 極限圈, an extreme sports community. I really don't want to ruin my health by staying up too late (it's 3 AM right now QAQ), so I didn't implement it as a web page — today it's just an image :). ![極限圈landing-page.jpg](https://i.imgur.com/5nDhx57.jpg) [HD image](https://imgur.com/SXfzDlx) This time I used Keynote as my image-compositing tool (a real pain, but I have no money for PS and AI — I'll probably look for open-source alternatives later) Background image source: [Librestock](librestock.com) (highly recommended) Vector image source: [Freepik](freepik.com) (also highly recommended) The inspiration for putting text on the mountain peaks came from this video (at 3:28): https://www.youtube.com/watch?v=1Huz2P7tdYk Turns out all the random stuff I usually watch does come in handy one day :) For the design details of single-page websites, I referenced [Pinterest](https://www.pinterest.com/search/pins/?q=landing%20page&rs=typed&0=landing%7Ctyped&1=page%7Ctyped) (can't recommend it enough) There's still so much to learn. I want to learn how to sketch drafts, produce vector graphics and background images, lay out pages by dragging and dropping, and properly turn them into code...... The road ahead is still long That's it — this is today's DailyUI #003 Landing Page! --- # 【DailyUI #003】Landing Page - URL: https://easonchang.com/zh-TW/posts/dailyui-003-landing-page - Date: 2016-05-10T19:43:00.000Z - Language: zh-TW 這是今天 DailyUI 的成果,極限圈的 Landing Page。 我實在不希望太晚睡搞壞身體(現在是凌晨 3 點 QAQ),所以就不實作成網頁了,今天只有圖檔:)。 ![極限圈landing-page.jpg](https://i.imgur.com/5nDhx57.jpg) [HD 圖檔](https://imgur.com/SXfzDlx) 這次我使用 Keynote 作為拼圖軟體(非常坑爹,可是沒錢買 PS 和 AI 啊,之後應該會去找開源的替代方案) 背景圖片來源:[Librestock](librestock.com)(超推) 向量圖片來源:[Freepik](freepik.com)(也是超推) 在山峰上放字的靈感來源是這支影片(3:28 處):https://www.youtube.com/watch?v=1Huz2P7tdYk 可見平常多看一些有的沒的也是有派上用場的一天:) 單頁式網頁的設計細節則是參考[Pinterest](https://www.pinterest.com/search/pins/?q=landing%20page&rs=typed&0=landing%7Ctyped&1=page%7Ctyped)(推上天啦) 要學得真的還很多,我想學習如何畫草稿、產出向量圖和背景圖、用拖拉的方式規劃頁面、並妥善轉換成 code......。 前方的道路還很長呀 以上,這是今天的 DailyUI #003 Landing Page! --- # 【DailyUI #002】Credit Card Checkout - URL: https://easonchang.com/posts/dailyui-002-credit-card-checkout - Date: 2016-05-09T21:03:00.000Z - Language: en - Translation: AI-assisted [Click here to try the Live Demo](http://kamigami55.github.io/DailyUI/002_creditCardCheckout/) [Click here for the Github source code (but it's a total unorganized mess, so maybe you'd better not look)](https://github.com/eason-dev/DailyUI/tree/master/002_creditCardCheckout) It's DailyUI day #2! This is getting addictive, so of course I have to keep the momentum going and continue creating really, really awesome UIs! ![dailyui_002_credit.png](https://i.imgur.com/hEbjURo.jpg) I've discovered there are still many CSS features I don't understand yet: pseudo-elements, animation, @keyframes, transform, ...... My grasp of front-end design is still very low — I can't freely create the effects I want. But through the DailyUI challenge, I can clearly feel myself improving. I hope that after completing this 100-day challenge, I'll become a front-end developer who's actually presentable! --- # 【DailyUI #002】Credit Card Checkout - URL: https://easonchang.com/zh-TW/posts/dailyui-002-credit-card-checkout - Date: 2016-05-09T21:03:00.000Z - Language: zh-TW [點我試試 Live Demo](http://kamigami55.github.io/DailyUI/002_creditCardCheckout/) [點我看看 Github 原始碼(可是完全沒有整理超級亂我覺得你還是不要看好了)](https://github.com/eason-dev/DailyUI/tree/master/002_creditCardCheckout) It's DailyUI day #2! 做一做就上癮了,所以當然要趁勝追擊繼續創造很厲害很厲害的 UI! ![dailyui_002_credit.png](https://i.imgur.com/hEbjURo.jpg) 我發現 CSS 還有很多特性是我還不了解的、pseudo-element(偽元素)、animetion、@keyframes、transform、...... 我對於前端設計的掌握度還是非常低,無法隨心所欲地作出想要的效果,不過在挑戰 DailyUI 的過程中,確實可以明顯感覺到自己正在進步,希望完成這 100 天的挑戰後,自己能成為一位上的了檯面的前端工程師! --- # 【DailyUI #001】Sign Up - URL: https://easonchang.com/posts/dailyui-001-sign-up - Date: 2016-05-09T07:28:00.000Z - Language: en - Translation: AI-assisted # What I Did Today New project launched! This is DailyUI! Today's prompt is **"Sign Up"** — design a sign-up interface. ![Daily-UI-001.png](https://i.imgur.com/rWNKTqv.jpg) I wanted "signing up" to break free from the "filling out a form" framework. Signing up can be like a tour guide, leading visitors into your carefully designed website to explore. So the process should feel like a conversation that guides you along. That's why I borrowed the design of messaging apps and wrapped the sign-up form inside a conversation with a tour guide. [Click here to play with the live demo](http://kamigami55.github.io/DailyUI/001_signup/) [Click here to view the source code on Github](https://github.com/eason-dev/DailyUI/tree/master/001_signup) --- # 【DailyUI #001】Sign Up - URL: https://easonchang.com/zh-TW/posts/dailyui-001-sign-up - Date: 2016-05-09T07:28:00.000Z - Language: zh-TW # 我今天做了什麼 新專案啟動!這是 DailyUI! 今天的題目是**"Sign Up"**,設計一個註冊介面。 ![Daily-UI-001.png](https://i.imgur.com/rWNKTqv.jpg) 而我希望「註冊」這個行為可以脫離「填表單」這個框架。註冊也可以像是一位導遊,引領遊客們進入你精心設計的網站中探索,因此這個過程應該是以「對話、引導」的方式呈現,於是乎我就借用了通訊軟體的設計,將註冊表單包裹在與導遊的對話之中。 [點我玩線上 Demo](http://kamigami55.github.io/DailyUI/001_signup/) [點我查看 Github 原始碼](https://github.com/eason-dev/DailyUI/tree/master/001_signup) --- # 【DailyUI #000】Let's Become an Awesome Front-End Designer - URL: https://easonchang.com/posts/dailyui-000-become-awesome-front-end-designer - Date: 2016-05-08T14:50:00.000Z - Language: en - Translation: AI-assisted # What is DailyUI While surfing the web, I accidentally stumbled upon another amazing website: DailyUI.co It's a service that helps you become a more professional UI Designer by motivating you to do UI design for 100 days in a row. Here's how it works: - Every day it sends you an email with today's UI design prompt (except weekends — weekends are for going out and having fun) - The prompt only gives a general direction without many restrictions, so unleash your creativity and build the most awesome UI! It's show time! - Post your design on Twitter or Dribble to share your creativity with the whole world! - Occasionally there are surprise rewards — don't be too shocked :) This kind of interaction is just amazing! And I also want to find out what the surprise rewards are~ So, "**Challenge Accepted!!**" # My Challenge Log [【DailyUI #001】Sign Up](https://easonchang.com/2016/05/09/dailyui-001-sign-up/) [【DailyUI #002】Credit Card Checkout](https://easonchang.com/2016/05/09/dailyui-002-credit-card-checkout/) [【DailyUI #003】Landing Page](https://easonchang.com/2016/05/10/dailyui-003-landing-page/) --- # 【DailyUI #000】成為最強前端設計師吧 - URL: https://easonchang.com/zh-TW/posts/dailyui-000-become-awesome-front-end-designer - Date: 2016-05-08T14:50:00.000Z - Language: zh-TW # 什麼是 DailyUI 在網上衝浪的時候,不小心又看到了一個驚人的網站:DailyUI.co 這是一個協助你成為更專業的 UI Designer 的服務,透過激勵你連續 100 天進行 UI 設計,他的運作方式是這樣的: - 每天都會寄一封 email 給你,告訴你今天的 UI 設計題目(假日沒有,假日是用來遊山玩水的) - 題目只會指出大方向,沒有太多限制,發揮創意打造出最強 UI!It's show time! - 將你的設計發上 Twitter 或是 Dribble,向全世界分享你的創意! - 偶爾會有驚喜獎勵,不要被嚇到了:) 這種互動模式實在是太驚人了!而且我也想知道有什麼驚喜獎勵~ 於是乎「**Challenge Accepted!!**」 # 我的挑戰紀錄 [【DailyUI #001】Sign Up](https://easonchang.com/2016/05/09/dailyui-001-sign-up/) [【DailyUI #002】Credit Card Checkout](https://easonchang.com/2016/05/09/dailyui-002-credit-card-checkout/) [【DailyUI #003】Landing Page](https://easonchang.com/2016/05/10/dailyui-003-landing-page/) --- # [Book Notes] Learning the Key Abilities of Iron Man Elon Musk from 《He's Playing for Real!》 - URL: https://easonchang.com/posts/10-keys-of-elon-musk - Date: 2016-04-16T18:30:00.000Z - Language: en - Translation: AI-assisted 《He's Playing for Real!: The 10 Abilities "Iron Man" Elon Musk Uses to Change the Future》 Authors: Mario Yang, G Wu, Vista Cheng > One-line summary: Think! Debate! Make changes like Iron Man! Build insight, execution, and leadership! Bet on your vision! # Who is Musk? Elon Musk, often called the most innovative entrepreneur since Steve Jobs, has founded one successful company after another, and every move he makes brings astonishing changes to the world. Musk was born in South Africa, and he figured out very early what he wanted to work toward. In college he asked himself: "What will most affect the future of humanity?" His answer was five fields: the internet, space exploration, clean energy, artificial intelligence, and biotechnology. Every action he has taken since then has been about realizing these visions. The first three are currently his main focus — he founded X.com (the predecessor of PayPal), SpaceX, Tesla, and SolarCity, and has achieved major things in these fields. # Why can Musk pull it off? 《He's Playing for Real!》 sums up 10 key abilities Musk uses to create the future. Here are the three I think matter most: 1. Insight: foresee the future, and think deeply enough to grasp the essence of things 2. Execution: actively pull reality closer to the future, define your own rules, and never let go until you get it 3. Leadership: pool everyone's strengths, and roll up your sleeves to solve problems alongside your team OK, that was the book's point of view. Here's what I want to say: # Why do you like reading about successful people? Bookstores and the internet are full of books and articles telling you how to become a successful person, and the ideas are basically all the same: > Integrity, persistence, pursuit of perfection, focus on goals, attention to detail, vision, great public speaking, skill at motivating teams, connections, drive to act, ...... They tell you these people succeeded because they had these traits. Sure, we can't deny that. But the point isn't how they succeeded. Think about why you're interested in these books and articles in the first place: because "you want to become a successful person like them". So your focus should be on how to make their traits become "your" traits. # How do you make the change? So how do you do that? I can't tell you, because I'm still figuring it out myself — if anyone knows the way, please do tell me! The only thing I'm sure of is that only "you" can make this change happen. Only when you're willing, and hungry, to become someone like them do you have a real shot at changing. The advice I can give you (and myself) is this: > Every time you make a decision, think: what would Musk (or anyone you want to become) do? Why would he/she make that choice? What is your choice? What's the difference between you and him/her? Keeping this thought in mind at all times is, I think, the most promising way to actually change. --- # 【閱讀筆記】從《他是玩真的!》汲取鋼鐵人Elon Musk創造未來的關鍵能力 - URL: https://easonchang.com/zh-TW/posts/10-keys-of-elon-musk - Date: 2016-04-16T18:30:00.000Z - Language: zh-TW 《他是玩真的!:「鋼鐵人」伊隆·馬斯克改變未來的 10 種能力》 作者:楊士範(Mario)、吳居叡(G Wu)、鄭緯筌(Vista) > 一句話書摘:去思考!去辯論!像個鋼鐵人般做出改變吧!培養洞察力、執行力、領導力!賭上你的願景! # 誰是馬斯克? 伊隆·馬斯克(Elon Musk),被稱為繼賈伯斯後最具創新能力的企業家,先後創立了多家成功企業,每次出擊都為世界帶來了驚人的改變。 馬斯克生於南非,他很早就確定了他想努力的方向。大學時期他問了自己這個問題:「什麼是未來最能影響人類的事物?」,他的答案是這五大領域:網路、太空探索、潔淨能源、人工智慧、和生物科技。他之後的每一個行動都是為了實現這些願景,目前前三項領域是他的主力目標,他先後創立了 X.com(PayPal 的前身)、SpaceX、Tesla、SolarCity,在這些領域取得了重大成就。 # 為什麼馬斯克做得到? 《他是玩真的!》歸納出了 10 項馬斯克創造未來的關鍵能力,這邊整理為我認為比較重要的三點: 1. 洞察力:預見未來,並深入思考掌握本質 2. 執行力:主動拉近現實與未來的距離,自己定義規則,不到手絕不放棄 3. 領導力:群策群力,挽起袖子和部屬一起解決問題 好了,以上為本書的觀點,以下是我想說的: # 你為什麼喜歡閱讀成功人士? 書店裡及網路上不乏這類告訴你如何鍊成一位成功人士的書與文章,基本上觀念都大同小異: > 誠信、堅持不輟、追求完美、專注目標、雕琢細節、有遠見、擅長演講、熟於驅動團隊、有人脈、行動力、...... 它會告訴你這些成功人士因為具備了上述特質而成功,沒錯,我們無法否認這點,但重點不在於他們如何成功,想想你為什麼會對這些書籍或文章有興趣,因為「你想成為像他們一樣的成功人士」,所以你應該把重點放在如何讓這些人的特點變成「你」的特點。 # 如何做出改變? 所以要如何做到呢?我沒辦法告訴你,因為我也還在嘗試中,如果有誰知道方法的話還請務必告訴我!唯一可以確定的是,只有「你」才可以促使這個改變發生,只有當你願意並渴望成為像他們一樣的人時,你才有可能真正達到改變。 我能給你(和我自己)的建議就是: > 當你在做每一個決定的時候,想想看馬斯克(或任何你想成為的人)會怎麼做?為什麼他/她會做出這樣的選擇?而你的選擇又是什麼?你和他/她有什麼差異? 時時把這樣的念頭掛在心上,我覺得是比較有可能達到改變的方法了。 --- # 【100sites #011】LifeGame, a simulation of life's infinite possibilities - URL: https://easonchang.com/posts/100sites-011-lifegame - Date: 2016-03-28T03:54:00.000Z - Language: en - Translation: AI-assisted # LifeGame, a simulation of life's infinite possibilities [Play the Game of Life live demo](http://kamigami55.github.io/100sites/011_LifeGame/) [View the source code on Github](https://github.com/eason-dev/100sites/tree/gh-pages/011_LifeGame) - ENTER: pause - Left click: place or remove a cell ![Screenshot 2016-03-27 12.35.39 AM.png](https://i.imgur.com/pZmuBeF.jpg) ## What is the Game of Life? The [Game of Life](https://zh.wikipedia.org/wiki/%E5%BA%B7%E5%A8%81%E7%94%9F%E5%91%BD%E6%B8%B8%E6%88%8F) is a [cellular automaton](https://zh.wikipedia.org/wiki/%E7%B4%B0%E8%83%9E%E8%87%AA%E5%8B%95%E6%A9%9F) model created by the British mathematician [John Horton Conway](https://zh.wikipedia.org/wiki/%E7%B4%84%E7%BF%B0%C2%B7%E4%BD%95%E9%A0%93%C2%B7%E5%BA%B7%E5%A8%81). Every cell on the map decides whether it lives or dies based on the state of the 8 cells around it. The detailed rules are as follows (excerpted from [Wikipedia](https://zh.wikipedia.org/wiki/%E5%BA%B7%E5%A8%81%E7%94%9F%E5%91%BD%E6%B8%B8%E6%88%8F)): 1. A live cell with fewer than 2 live neighbors dies. (simulating underpopulation) 2. A live cell with 2 or 3 live neighbors stays as it is. 3. A live cell with more than 3 live neighbors dies. (simulating overpopulation) 4. A dead cell with exactly 3 live neighbors becomes alive. (simulating reproduction) With just these 4 rules, you can build a complex and beautiful world of life. You can find some units with special shapes that show interesting behavior patterns. Three classic special units: ![Screenshot 2016-03-28 11.36.59 AM.png](https://i.imgur.com/tCVC0LI.jpg) - The Blinker on the left changes in a fixed cycle - The Spaceship in the middle moves toward the bottom right (you can also design ships that move in other directions) - The Beehive on the right stays still ## Today's LifeGame In today's LifeGame, I've already placed a Blinker in the top left corner for you. The rest of the space is yours to create infinite possibilities! Today's code: ```html index.html LifeGame ``` ```css style.css * { margin: 0; padding: 0; } body { overflow: hidden; } ``` ```javascript lifegame.js var SIZE = 20; var oldMap = [], newMap = [], num_x = 0, num_y = 0, paused = false, frameCount = 0; var pauseButton; function setup() { createCanvas(windowWidth, windowHeight); frameRate(30); // init map num_x = windowWidth / SIZE; num_y = windowHeight / SIZE; for (var i = 0; i < num_x; ++i) { oldMap.push([]); newMap.push([]); for (var j = 0; j < num_y; ++j) { oldMap[i].push(false); newMap[i].push(false); } } loadDefaultMap(); // init pause button pauseButton = createButton("Pause"); pauseButton.size(80, 30); pauseButton.position(windowWidth / 2 - 40, windowHeight - 40); pauseButton.mousePressed(togglePauseSimulate); } function draw() { // fresh map every 5 frames if (!paused) { if (frameCount % 5 == 0) { freshMap(); } } // increase frame count ++frameCount; if (frameCount >= 30) { frameCount = 0; } drawMap(); } // default map: a blink unit at top left corner function loadDefaultMap() { oldMap[1][1] = true; oldMap[1][2] = true; oldMap[1][3] = true; } function drawMap() { background(0); // draw grid stroke(30); for (var i = 0; i < num_x; ++i) { line(i * SIZE, 0, i * SIZE, windowHeight); } for (var i = 0; i < num_y; ++i) { line(0, i * SIZE, windowWidth, i * SIZE); } fill(255); // draw cells for (var i = 0; i < num_x; ++i) { for (var j = 0; j < num_y; ++j) { if (oldMap[i][j]) { rect(i * SIZE, j * SIZE, SIZE, SIZE); } } } // draw mouse cell fill(color("rgba(100,100,100,0.5)")); var mouseCellX = int(mouseX / SIZE); var mouseCellY = int(mouseY / SIZE); rect(mouseCellX * SIZE, mouseCellY * SIZE, SIZE, SIZE); } // press ENTER to pause simulate function keyPressed() { if (keyCode == ENTER) { togglePauseSimulate(); } } // press mouse to add or remove cell function mousePressed() { var mouseCellX = int(mouseX / SIZE); var mouseCellY = int(mouseY / SIZE); oldMap[mouseCellX][mouseCellY] = !oldMap[mouseCellX][mouseCellY]; } function togglePauseSimulate() { if (paused) { paused = false; } else { paused = true; } } // count how many neighbors there are of a cell function neighbors(xpos, ypos) { var total = 0; // four corners if (xpos != 0 && ypos != 0) { if (oldMap[xpos - 1][ypos - 1]) { ++total; } } if (xpos != 0 && ypos != num_y - 1) { if (oldMap[xpos - 1][ypos + 1]) { ++total; } } if (xpos != num_x - 1 && ypos != 0) { if (oldMap[xpos + 1][ypos - 1]) { ++total; } } if (xpos != num_x - 1 && ypos != num_y - 1) { if (oldMap[xpos + 1][ypos + 1]) { ++total; } } // left and right if (xpos != 0) { if (oldMap[xpos - 1][ypos]) { ++total; } } if (xpos != num_x - 1) { if (oldMap[xpos + 1][ypos]) { ++total; } } if (ypos != 0) { if (oldMap[xpos][ypos - 1]) { ++total; } } if (ypos != num_y - 1) { if (oldMap[xpos][ypos + 1]) { ++total; } } return total; } // calculate the next map according to the Life Game rule function freshMap() { for (var i = 0; i < num_x; ++i) { for (var j = 0; j < num_y; ++j) { var neighbor_count = neighbors(i, j); if (neighbor_count <= 1 || neighbor_count >= 4) { newMap[i][j] = false; } else if (neighbor_count === 2) { newMap[i][j] = oldMap[i][j]; } else { // neighbor_count === 3 newMap[i][j] = true; } } } for (var i = 0; i < num_x; ++i) { for (var j = 0; j < num_y; ++j) { oldMap[i][j] = newMap[i][j]; } } } ``` --- # 【100sites #011】LifeGame,生命遊戲模擬無限的可能性 - URL: https://easonchang.com/zh-TW/posts/100sites-011-lifegame - Date: 2016-03-28T03:54:00.000Z - Language: zh-TW # LifeGame,生命遊戲模擬無限的可能性 [點我玩生命遊戲線上 demo](http://kamigami55.github.io/100sites/011_LifeGame/) [點我查看 Github 原始碼](https://github.com/eason-dev/100sites/tree/gh-pages/011_LifeGame) - ENTER:暫停 - 滑鼠左鍵:放置或刪除細胞 ![螢幕快照 2016-03-27 上午12.35.39.png](https://i.imgur.com/pZmuBeF.jpg) ## 什麼是生命遊戲? [生命遊戲](https://zh.wikipedia.org/wiki/%E5%BA%B7%E5%A8%81%E7%94%9F%E5%91%BD%E6%B8%B8%E6%88%8F),是由英國數學家[John Horton Conway](https://zh.wikipedia.org/wiki/%E7%B4%84%E7%BF%B0%C2%B7%E4%BD%95%E9%A0%93%C2%B7%E5%BA%B7%E5%A8%81)研發的一套[細胞自動機](https://zh.wikipedia.org/wiki/%E7%B4%B0%E8%83%9E%E8%87%AA%E5%8B%95%E6%A9%9F)模型,地圖中每個細胞會根據周圍 8 格細胞的狀態來決定自身的存亡,詳細規則如下:(摘錄自[維基百科](https://zh.wikipedia.org/wiki/%E5%BA%B7%E5%A8%81%E7%94%9F%E5%91%BD%E6%B8%B8%E6%88%8F)) 1. 當前細胞為存活狀態時,當周圍低於 2 個(不包含 2 個)存活細胞時,該細胞變成死亡狀態。(模擬生命數量稀少) 2. 當前細胞為存活狀態時,當周圍有 2 個或 3 個存活細胞時,該細胞保持原樣。 3. 當前細胞為存活狀態時,當周圍有 3 個以上(不包含 3 個)的存活細胞時,該細胞變成死亡狀態。(模擬生命數量過多) 4. 當前細胞為死亡狀態時,當周圍有 3 個存活細胞時,該細胞變成存活狀態。 (模擬繁殖) 僅僅這 4 條規則,就可以建構出一個複雜而美妙的生物遊戲世界。我們可以找到一些特殊形狀的生物單位,他們會表現出一些有趣的行為模式。 三種經典的特殊生物單位: ![螢幕快照 2016-03-28 上午11.36.59.png](https://i.imgur.com/tCVC0LI.jpg) - 左邊的 Blinker(信號燈)會以特定週期變化 - 中間的 Spaceship(太空船)會往右下角移動(你也可以設計出往其他方向移動的船) - 右邊的 Beehive(蜂窩)則會保持靜止 ## 今天的 LifeGame 而在今天的 LifeGame 裡頭,我已經為你在左上角放置一個 Blinker 了,剩下的空間,就交給你來創造無限的可能性了! 今天的程式碼: ```html index.html LifeGame ``` ```css style.css * { margin: 0; padding: 0; } body { overflow: hidden; } ``` ```javascript lifegame.js var SIZE = 20; var oldMap = [], newMap = [], num_x = 0, num_y = 0, paused = false, frameCount = 0; var pauseButton; function setup() { createCanvas(windowWidth, windowHeight); frameRate(30); // init map num_x = windowWidth / SIZE; num_y = windowHeight / SIZE; for (var i = 0; i < num_x; ++i) { oldMap.push([]); newMap.push([]); for (var j = 0; j < num_y; ++j) { oldMap[i].push(false); newMap[i].push(false); } } loadDefaultMap(); // init pause button pauseButton = createButton("Pause"); pauseButton.size(80, 30); pauseButton.position(windowWidth / 2 - 40, windowHeight - 40); pauseButton.mousePressed(togglePauseSimulate); } function draw() { // fresh map every 5 frames if (!paused) { if (frameCount % 5 == 0) { freshMap(); } } // increase frame count ++frameCount; if (frameCount >= 30) { frameCount = 0; } drawMap(); } // default map: a blink unit at top left corner function loadDefaultMap() { oldMap[1][1] = true; oldMap[1][2] = true; oldMap[1][3] = true; } function drawMap() { background(0); // draw grid stroke(30); for (var i = 0; i < num_x; ++i) { line(i * SIZE, 0, i * SIZE, windowHeight); } for (var i = 0; i < num_y; ++i) { line(0, i * SIZE, windowWidth, i * SIZE); } fill(255); // draw cells for (var i = 0; i < num_x; ++i) { for (var j = 0; j < num_y; ++j) { if (oldMap[i][j]) { rect(i * SIZE, j * SIZE, SIZE, SIZE); } } } // draw mouse cell fill(color("rgba(100,100,100,0.5)")); var mouseCellX = int(mouseX / SIZE); var mouseCellY = int(mouseY / SIZE); rect(mouseCellX * SIZE, mouseCellY * SIZE, SIZE, SIZE); } // press ENTER to pause simulate function keyPressed() { if (keyCode == ENTER) { togglePauseSimulate(); } } // press mouse to add or remove cell function mousePressed() { var mouseCellX = int(mouseX / SIZE); var mouseCellY = int(mouseY / SIZE); oldMap[mouseCellX][mouseCellY] = !oldMap[mouseCellX][mouseCellY]; } function togglePauseSimulate() { if (paused) { paused = false; } else { paused = true; } } // count how many neighbors there are of a cell function neighbors(xpos, ypos) { var total = 0; // four corners if (xpos != 0 && ypos != 0) { if (oldMap[xpos - 1][ypos - 1]) { ++total; } } if (xpos != 0 && ypos != num_y - 1) { if (oldMap[xpos - 1][ypos + 1]) { ++total; } } if (xpos != num_x - 1 && ypos != 0) { if (oldMap[xpos + 1][ypos - 1]) { ++total; } } if (xpos != num_x - 1 && ypos != num_y - 1) { if (oldMap[xpos + 1][ypos + 1]) { ++total; } } // left and right if (xpos != 0) { if (oldMap[xpos - 1][ypos]) { ++total; } } if (xpos != num_x - 1) { if (oldMap[xpos + 1][ypos]) { ++total; } } if (ypos != 0) { if (oldMap[xpos][ypos - 1]) { ++total; } } if (ypos != num_y - 1) { if (oldMap[xpos][ypos + 1]) { ++total; } } return total; } // calculate the next map according to the Life Game rule function freshMap() { for (var i = 0; i < num_x; ++i) { for (var j = 0; j < num_y; ++j) { var neighbor_count = neighbors(i, j); if (neighbor_count <= 1 || neighbor_count >= 4) { newMap[i][j] = false; } else if (neighbor_count === 2) { newMap[i][j] = oldMap[i][j]; } else { // neighbor_count === 3 newMap[i][j] = true; } } } for (var i = 0; i < num_x; ++i) { for (var j = 0; j < num_y; ++j) { oldMap[i][j] = newMap[i][j]; } } } ``` --- # 【100sites #010】Piano, a virtual piano implemented with P5js's sound library - URL: https://easonchang.com/posts/100sites-010-piano - Date: 2016-03-25T17:57:00.000Z - Language: en - Translation: AI-assisted # Piano, a virtual piano implemented with P5js's sound library [Play the live demo](http://kamigami55.github.io/100sites/010_Piano/) [Check out the code](https://github.com/eason-dev/100sites/tree/gh-pages/010_Piano) ![Screenshot 2016-03-26 1.18.01 AM.png](https://i.imgur.com/8aBbIhk.jpg) To set a good mood, you need the right music. So today I wanted to play with music, and after digging into [P5.js](http://p5js.org/)'s [sound](http://p5js.org/reference/#/libraries/p5.sound) library, I made today's Piano, a virtual piano. The library has quite a few features to work with — playback, recording, and waveform transforms are all possible. Today's Piano uses a sine-wave oscillator to generate the sound, then uses FFT.analyze() to display the waveform. Here's the code: ```html index.html Piano ``` ```css style.css * { margin: 0; padding: 0; } body { overflow: hidden; } ``` ```javascript piano.js var attackLevel = 2.0; var releaseLevel = 0; var attackTime = 0.001; var decayTime = 0.2; var susPercent = 0.2; var releaseTime = 0.5; var env, triOsc; var notePressed = [ false, false, false, false, false, false, false, false, false, false, false, false, ]; var fft; function setup() { createCanvas(windowWidth, windowHeight); textAlign(CENTER); textSize(40); env = new p5.Env(); env.setADSR(attackTime, decayTime, susPercent, releaseTime); env.setRange(attackLevel, releaseLevel); triOsc = new p5.Oscillator("sine"); triOsc.amp(env); triOsc.start(); ellipseMode(CENTER); strokeWeight(2); rectMode(CENTER); fft = new p5.FFT(); } function draw() { background(200); // title fill(255); strokeWeight(5); textSize(120); textFont("Georgia"); text("PIANO", width / 2, 150); textFont("Normal"); // spectrum var spectrum = fft.analyze(); noStroke(); fill(230); for (var i = 0; i < spectrum.length; i++) { var x = map(i, 0, spectrum.length, 0, width / 2); var h = -height / 2 + map(spectrum[i], 0, 255, height / 2, 0); rect(x, height / 2, width / spectrum.length, h); rect(width - x, height / 2, width / spectrum.length, h); } // black note drawNote( windowWidth / 2 - 150, windowHeight / 2 - 60, "E", "b", notePressed[0] ); drawNote( windowWidth / 2 - 90, windowHeight / 2 - 60, "R", "b", notePressed[1] ); drawNote( windowWidth / 2 + 30, windowHeight / 2 - 60, "Y", "b", notePressed[2] ); drawNote( windowWidth / 2 + 90, windowHeight / 2 - 60, "U", "b", notePressed[3] ); drawNote( windowWidth / 2 + 150, windowHeight / 2 - 60, "I", "b", notePressed[4] ); // white note drawNote(windowWidth / 2 - 180, windowHeight / 2, "S", "w", notePressed[5]); drawNote(windowWidth / 2 - 120, windowHeight / 2, "D", "w", notePressed[6]); drawNote(windowWidth / 2 - 60, windowHeight / 2, "F", "w", notePressed[7]); drawNote(windowWidth / 2, windowHeight / 2, "G", "w", notePressed[8]); drawNote(windowWidth / 2 + 60, windowHeight / 2, "H", "w", notePressed[9]); drawNote(windowWidth / 2 + 120, windowHeight / 2, "J", "w", notePressed[10]); drawNote(windowWidth / 2 + 180, windowHeight / 2, "K", "w", notePressed[11]); } function playEnv(f) { triOsc.freq(f); env.play(); } function drawNote(xpos, ypos, key, color, pressed) { // black note var big = 0; if (pressed) { big = 8; } // shadow fill(100); strokeWeight(0); ellipse(xpos + 3, ypos + 3, 50 + big, 50 + big); // note if (color == "b") { fill(0); stroke(255); } else { fill(255); stroke(0); } strokeWeight(2); ellipse(xpos, ypos, 50 + big, 50 + big); // text if (color == "b") { fill(255); } else { fill(0); } textSize(40); if (pressed) { textSize(44); } text(key, xpos, ypos + 17); } function keyPressed() { switch (keyCode) { case 83: // S case 115: // s playEnv(261); // C note notePressed[5] = true; break; case 69: // E case 101: // e playEnv(277); // C# note notePressed[0] = true; break; case 68: // D case 110: // d playEnv(293); // D note notePressed[6] = true; break; case 82: // R case 114: // r playEnv(311); // D# note notePressed[1] = true; break; case 70: // F case 102: // f playEnv(329); // E note notePressed[7] = true; break; case 71: // G case 103: // g playEnv(349); // F note notePressed[8] = true; break; case 89: // Y case 121: // y playEnv(369); // F# note notePressed[2] = true; break; case 72: // H case 104: // h playEnv(392); // G note notePressed[9] = true; break; case 85: // U case 117: // u playEnv(415); // G# note notePressed[3] = true; break; case 74: // J case 106: // j playEnv(440); // A note notePressed[10] = true; break; case 73: // U case 105: // u playEnv(466); // A# note notePressed[4] = true; break; case 75: // K case 107: // k playEnv(493); // B note notePressed[11] = true; break; default: break; } } function keyReleased() { switch (keyCode) { case 83: // S case 115: // s // C note notePressed[5] = false; break; case 69: // E case 101: // e // C# note notePressed[0] = false; break; case 68: // D case 110: // d // D note notePressed[6] = false; break; case 82: // R case 114: // r // D# note notePressed[1] = false; break; case 70: // F case 102: // f // E note notePressed[7] = false; break; case 71: // G case 103: // g // F note notePressed[8] = false; break; case 89: // Y case 121: // y // F# note notePressed[2] = false; break; case 72: // H case 104: // h // G note notePressed[9] = false; break; case 85: // U case 117: // u // G# note notePressed[3] = false; break; case 74: // J case 106: // j // A note notePressed[10] = false; break; case 73: // U case 105: // u // A# note notePressed[4] = false; break; case 75: // K case 107: // k // B note notePressed[11] = false; break; default: break; } } ``` --- # 【100sites #010】Piano,用P5js的sound函式庫實作虛擬鋼琴 - URL: https://easonchang.com/zh-TW/posts/100sites-010-piano - Date: 2016-03-25T17:57:00.000Z - Language: zh-TW # Piano,用 P5js 的 sound 函式庫實作虛擬鋼琴 [點我玩線上 Demo](http://kamigami55.github.io/100sites/010_Piano/) [點我查看程式碼](https://github.com/eason-dev/100sites/tree/gh-pages/010_Piano) ![螢幕快照 2016-03-26 上午1.18.01.png](https://i.imgur.com/8aBbIhk.jpg) 要營造出好氣氛,一定要有適合的音樂,於是今天我想要玩音樂,所以就研究了一下[P5.js](http://p5js.org/)的[sound](http://p5js.org/reference/#/libraries/p5.sound)函式庫,做出了今天的 Piano 虛擬鋼琴。 這個函式庫有不少功能可以使用,播音錄音波型轉換都可以做到,今天的 Piano 使用了 sine 函數的 oscillator 製造聲音,再用 FFT.analyze()來顯示波形。 本次的程式碼: ```html index.html Piano ``` ```css style.css * { margin: 0; padding: 0; } body { overflow: hidden; } ``` ```javascript piano.js var attackLevel = 2.0; var releaseLevel = 0; var attackTime = 0.001; var decayTime = 0.2; var susPercent = 0.2; var releaseTime = 0.5; var env, triOsc; var notePressed = [ false, false, false, false, false, false, false, false, false, false, false, false, ]; var fft; function setup() { createCanvas(windowWidth, windowHeight); textAlign(CENTER); textSize(40); env = new p5.Env(); env.setADSR(attackTime, decayTime, susPercent, releaseTime); env.setRange(attackLevel, releaseLevel); triOsc = new p5.Oscillator("sine"); triOsc.amp(env); triOsc.start(); ellipseMode(CENTER); strokeWeight(2); rectMode(CENTER); fft = new p5.FFT(); } function draw() { background(200); // title fill(255); strokeWeight(5); textSize(120); textFont("Georgia"); text("PIANO", width / 2, 150); textFont("Normal"); // spectrum var spectrum = fft.analyze(); noStroke(); fill(230); for (var i = 0; i < spectrum.length; i++) { var x = map(i, 0, spectrum.length, 0, width / 2); var h = -height / 2 + map(spectrum[i], 0, 255, height / 2, 0); rect(x, height / 2, width / spectrum.length, h); rect(width - x, height / 2, width / spectrum.length, h); } // black note drawNote( windowWidth / 2 - 150, windowHeight / 2 - 60, "E", "b", notePressed[0] ); drawNote( windowWidth / 2 - 90, windowHeight / 2 - 60, "R", "b", notePressed[1] ); drawNote( windowWidth / 2 + 30, windowHeight / 2 - 60, "Y", "b", notePressed[2] ); drawNote( windowWidth / 2 + 90, windowHeight / 2 - 60, "U", "b", notePressed[3] ); drawNote( windowWidth / 2 + 150, windowHeight / 2 - 60, "I", "b", notePressed[4] ); // white note drawNote(windowWidth / 2 - 180, windowHeight / 2, "S", "w", notePressed[5]); drawNote(windowWidth / 2 - 120, windowHeight / 2, "D", "w", notePressed[6]); drawNote(windowWidth / 2 - 60, windowHeight / 2, "F", "w", notePressed[7]); drawNote(windowWidth / 2, windowHeight / 2, "G", "w", notePressed[8]); drawNote(windowWidth / 2 + 60, windowHeight / 2, "H", "w", notePressed[9]); drawNote(windowWidth / 2 + 120, windowHeight / 2, "J", "w", notePressed[10]); drawNote(windowWidth / 2 + 180, windowHeight / 2, "K", "w", notePressed[11]); } function playEnv(f) { triOsc.freq(f); env.play(); } function drawNote(xpos, ypos, key, color, pressed) { // black note var big = 0; if (pressed) { big = 8; } // shadow fill(100); strokeWeight(0); ellipse(xpos + 3, ypos + 3, 50 + big, 50 + big); // note if (color == "b") { fill(0); stroke(255); } else { fill(255); stroke(0); } strokeWeight(2); ellipse(xpos, ypos, 50 + big, 50 + big); // text if (color == "b") { fill(255); } else { fill(0); } textSize(40); if (pressed) { textSize(44); } text(key, xpos, ypos + 17); } function keyPressed() { switch (keyCode) { case 83: // S case 115: // s playEnv(261); // C note notePressed[5] = true; break; case 69: // E case 101: // e playEnv(277); // C# note notePressed[0] = true; break; case 68: // D case 110: // d playEnv(293); // D note notePressed[6] = true; break; case 82: // R case 114: // r playEnv(311); // D# note notePressed[1] = true; break; case 70: // F case 102: // f playEnv(329); // E note notePressed[7] = true; break; case 71: // G case 103: // g playEnv(349); // F note notePressed[8] = true; break; case 89: // Y case 121: // y playEnv(369); // F# note notePressed[2] = true; break; case 72: // H case 104: // h playEnv(392); // G note notePressed[9] = true; break; case 85: // U case 117: // u playEnv(415); // G# note notePressed[3] = true; break; case 74: // J case 106: // j playEnv(440); // A note notePressed[10] = true; break; case 73: // U case 105: // u playEnv(466); // A# note notePressed[4] = true; break; case 75: // K case 107: // k playEnv(493); // B note notePressed[11] = true; break; default: break; } } function keyReleased() { switch (keyCode) { case 83: // S case 115: // s // C note notePressed[5] = false; break; case 69: // E case 101: // e // C# note notePressed[0] = false; break; case 68: // D case 110: // d // D note notePressed[6] = false; break; case 82: // R case 114: // r // D# note notePressed[1] = false; break; case 70: // F case 102: // f // E note notePressed[7] = false; break; case 71: // G case 103: // g // F note notePressed[8] = false; break; case 89: // Y case 121: // y // F# note notePressed[2] = false; break; case 72: // H case 104: // h // G note notePressed[9] = false; break; case 85: // U case 117: // u // G# note notePressed[3] = false; break; case 74: // J case 106: // j // A note notePressed[10] = false; break; case 73: // U case 105: // u // A# note notePressed[4] = false; break; case 75: // K case 107: // k // B note notePressed[11] = false; break; default: break; } } ``` --- # 【100sites #009】Flame, cool little animations with P5js - URL: https://easonchang.com/posts/100sites-009-flame - Date: 2016-03-23T07:05:00.000Z - Language: en - Translation: AI-assisted > One-sentence summary: A flame-like interactive effect made with P5.js [Play the live demo](http://kamigami55.github.io/100sites/009_Flame/) [View the source code on Github](https://github.com/eason-dev/100sites/tree/gh-pages/009_Flame) ![Screenshot 2016-03-23 4.08.53 AM.png](https://i.imgur.com/rqkEbP4.jpg) I first learned about [P5.js](http://p5js.org) from [Chiun Hau You](https://www.behance.net/chiunhauyou)'s talk at this year's SITCON conference. Recently his P5.js visual identity project, [Hello World - Programmable Visual Identity](https://www.behance.net/gallery/34490551/Hello-World-Programmable-Visual-Identity), made it into [CodePen](http://codepen.io)'s Picked Pens and got a great response. That made me want to explore more of what P5.js can do, so today I made this little flame interaction as practice. There aren't many details to explain today. Just move your mouse and enjoy the little flames! Left-click to switch styles — there are 4 in total. Here's another style: ![Screenshot 2016-03-23 4.09.52 AM.png](https://i.imgur.com/2hriTFz.jpg) Source code: ```html index.html Flame ``` ```css style.css * { margin: 0; padding: 0; } body { overflow: hidden; } ``` ```javascript flame.js var rows = 0, // how many shapes there are vertically cols = 0, // how many shapes there are horizontally diameter = [], // diameter of every single shape displayMode = 0, // four display mode: 0: red rect, 1: red ellipse, 2: blue rect, 3: blue ellipse MARGIN = 30, // distance between two shapes MAX_SIZE = 100, // max shape size when mouse approach SENSE_RANGE = 200; // what distance shapes will react function setup() { createCanvas(windowWidth, windowHeight); strokeWeight(0); ellipseMode(CENTER); rectMode(CENTER); frameRate(30); rows = windowHeight / MARGIN; cols = windowWidth / MARGIN; // initialize diameter 2-dimensional array for (var y = 0; y < rows; ++y) { diameter.push([]); for (var x = 0; x < cols; ++x) { diameter[y].push(0); } } } function draw() { background(20); for (var y = 0; y < rows; y += 1) { for (var x = 0; x < cols; x += 1) { var thisDiameter = diameter[y][x]; // decrease diameter by time until diameter is 0 if (thisDiameter < 2) { diameter[y][x] = 0; } else { // thisDiameter >= 2 diameter[y][x] -= 2; thisDiameter = diameter[y][x]; // And we can now display it // Determine which color should we use if (displayMode <= 1) { // 0, 1 fill(255, thisDiameter * 2.5, 0); // red color mode } else { // 2, 3 fill(0, thisDiameter * 2.5, 255 - thisDiameter * 1.5); // blue color mode } // Determine which shape should we use if (displayMode == 0 || displayMode == 2) { // 0, 2 rect(x * MARGIN, y * MARGIN, thisDiameter, thisDiameter); // rect mode } else { // 1, 3 ellipse(x * MARGIN, y * MARGIN, thisDiameter, thisDiameter); // ellipse mode } } // .if-else } // .for } // .for } // when mouse moved, adjust diameters of every shapes function mouseMoved() { for (var y = 0; y < rows; y += 1) { for (var x = 0; x < cols; x += 1) { // calculus the distance between this shape and user mouse var dist = Math.sqrt( (x * MARGIN - mouseX) * (x * MARGIN - mouseX) + (y * MARGIN - mouseY) * (y * MARGIN - mouseY) ); if (dist <= SENSE_RANGE) { // calculus the diameter based on the distance var theDiameter = ((SENSE_RANGE - dist) * MAX_SIZE) / SENSE_RANGE; // if current diameter is smaller than the calc diameter if (diameter[y][x] < theDiameter) { diameter[y][x] = theDiameter; // increase the current diameter to it should be } } } } } // switch display mode function mousePressed() { ++displayMode; if (displayMode > 3) { displayMode = 0; } } ``` --- # 【100sites #009】Flame,P5js酷炫小動畫 - URL: https://easonchang.com/zh-TW/posts/100sites-009-flame - Date: 2016-03-23T07:05:00.000Z - Language: zh-TW > 一句話摘要:使用 P5.js 製作的類火焰互動效果 [玩線上 Demo](http://kamigami55.github.io/100sites/009_Flame/) [查看 Github 原始碼](https://github.com/eason-dev/100sites/tree/gh-pages/009_Flame) ![螢幕快照 2016-03-23 上午4.08.53.png](https://i.imgur.com/rqkEbP4.jpg) 我會知道[P5.js](http://p5js.org)的存在,是因為在今年 SITCON 年會上聽了[游宭鎬(Chiun Hau You)](https://www.behance.net/chiunhauyou)先生的分享,而最近看他用 P5.js 做的視覺辨識專案: [Hello World - Programmable Visual Identity](https://www.behance.net/gallery/34490551/Hello-World-Programmable-Visual-Identity)上了[CodePen](http://codepen.io)的 Picked Pens,迴響熱烈,讓我也開始想探索 P5.js 更多的可能性,於是做了今天的小火焰互動特效練習。 今天沒什麼細節要講,移動你的滑鼠,享受這些小火焰吧! 點滑鼠左鍵可以切換樣式,共有 4 種樣式。 另外一種樣式: ![螢幕快照 2016-03-23 上午4.09.52.png](https://i.imgur.com/2hriTFz.jpg) 原始碼: ```html index.html Flame ``` ```css style.css * { margin: 0; padding: 0; } body { overflow: hidden; } ``` ```javascript flame.js var rows = 0, // how many shapes there are vertically cols = 0, // how many shapes there are horizontally diameter = [], // diameter of every single shape displayMode = 0, // four display mode: 0: red rect, 1: red ellipse, 2: blue rect, 3: blue ellipse MARGIN = 30, // distance between two shapes MAX_SIZE = 100, // max shape size when mouse approach SENSE_RANGE = 200; // what distance shapes will react function setup() { createCanvas(windowWidth, windowHeight); strokeWeight(0); ellipseMode(CENTER); rectMode(CENTER); frameRate(30); rows = windowHeight / MARGIN; cols = windowWidth / MARGIN; // initialize diameter 2-dimensional array for (var y = 0; y < rows; ++y) { diameter.push([]); for (var x = 0; x < cols; ++x) { diameter[y].push(0); } } } function draw() { background(20); for (var y = 0; y < rows; y += 1) { for (var x = 0; x < cols; x += 1) { var thisDiameter = diameter[y][x]; // decrease diameter by time until diameter is 0 if (thisDiameter < 2) { diameter[y][x] = 0; } else { // thisDiameter >= 2 diameter[y][x] -= 2; thisDiameter = diameter[y][x]; // And we can now display it // Determine which color should we use if (displayMode <= 1) { // 0, 1 fill(255, thisDiameter * 2.5, 0); // red color mode } else { // 2, 3 fill(0, thisDiameter * 2.5, 255 - thisDiameter * 1.5); // blue color mode } // Determine which shape should we use if (displayMode == 0 || displayMode == 2) { // 0, 2 rect(x * MARGIN, y * MARGIN, thisDiameter, thisDiameter); // rect mode } else { // 1, 3 ellipse(x * MARGIN, y * MARGIN, thisDiameter, thisDiameter); // ellipse mode } } // .if-else } // .for } // .for } // when mouse moved, adjust diameters of every shapes function mouseMoved() { for (var y = 0; y < rows; y += 1) { for (var x = 0; x < cols; x += 1) { // calculus the distance between this shape and user mouse var dist = Math.sqrt( (x * MARGIN - mouseX) * (x * MARGIN - mouseX) + (y * MARGIN - mouseY) * (y * MARGIN - mouseY) ); if (dist <= SENSE_RANGE) { // calculus the diameter based on the distance var theDiameter = ((SENSE_RANGE - dist) * MAX_SIZE) / SENSE_RANGE; // if current diameter is smaller than the calc diameter if (diameter[y][x] < theDiameter) { diameter[y][x] = theDiameter; // increase the current diameter to it should be } } } } } // switch display mode function mousePressed() { ++displayMode; if (displayMode > 3) { displayMode = 0; } } ``` --- # 【100sites #008】Showcase, a single-page website design to showcase my portfolio - URL: https://easonchang.com/posts/100sites-008-showcase - Date: 2016-03-20T18:42:00.000Z - Language: en - Translation: AI-assisted # Showcase, a single-page website design to show off my practice portfolio > One-sentence summary: My first single-page website [Try the Showcase live demo](http://kamigami55.github.io/100sites/008_Showcase/) [Check out the Showcase code on Github](https://github.com/eason-dev/100sites/tree/gh-pages/008_Showcase) ![Screenshot 2016-03-21 2.07.29 AM.png](https://i.imgur.com/sZzCbXP.jpg) I've always wanted to learn how to design single-page websites, so that's today's 100sites topic! I found a pretty good tutorial, [PJCHENder's jQuery study notes, lesson 8 part 1](http://pjchender.blogspot.tw/2015/04/jquery_19.html) and [part 2](http://pjchender.blogspot.tw/2015/04/jquery-2mousewheel.html), which walks you through building a simple single-page website step by step. So I followed the tutorial one step at a time. Once it was done, I swapped in my own images and added some text, links, and an overlay — and that became today's result, Showcase! Besides the single-page design itself, the special parts of Showcase are the frosted glass and dark overlay effects, both done with CSS. The frosted glass effect (adjust the px value to get however much blur you want): ```css -moz-filter: blur(4px); -webkit-filter: blur(4px); -o-filter: blur(4px); -ms-filter: blur(4px); filter: blur(4px); ``` The dark overlay is a div with the class cover, using background-color and opacity to get the effect: ```css .cover { height: 100vh; width: 100vw; position: fixed; top: 0px; background-color: #333; opacity: 0.6; z-index: 1; } ``` The full code is below: ```html index.html Showcase

#001

Timer

Demo

#002

Pomodoro

Demo

#003

Todolist

/* Demo Preparing */

#004

Fireworks

Demo

#005

Drawer

Demo

#006

Snack

Demo

#007

Pong

Demo
``` ```css showcase.css * { padding: 0; margin: 0; } body { overflow: hidden; } .cover { height: 100vh; width: 100vw; position: fixed; top: 0px; background-color: #333; opacity: 0.6; z-index: 1; } .picture { background-size: cover; height: 100vh; -moz-filter: blur(4px); -webkit-filter: blur(4px); -o-filter: blur(4px); -ms-filter: blur(4px); filter: blur(4px); } h1 { font-size: 20vh; color: #fff; font-family: Sans-serif; text-shadow: 4px 4px 30px #222; position: relative; top: 50vh; z-index: 3; } h2 { font-size: 8vh; color: #fff; font-family: Sans-serif; text-shadow: 3px 3px 20px #222; position: relative; top: 50vh; z-index: 3; } .info { text-align: center; } a { font-size: 10vh; color: #fff; font-family: Sans-serif; text-shadow: 3px 3px 20px #222; position: relative; top: 60vh; margin: 0px auto; z-index: 3; text-decoration: none; } a:hover { color: #4f4; } .p1 { background-image: url(images/p1.png); } .p2 { background-image: url(images/p2.png); } .p3 { background-image: url(images/p3.png); } .p4 { background-image: url(images/p4.png); } .p5 { background-image: url(images/p5.png); } .p6 { background-image: url(images/p6.png); } .p7 { background-image: url(images/p7.png); } /* 插入導覽列 */ .nav { position: fixed; top: 50%; right: 0px; z-index: 2; } li { width: 10px; height: 10px; margin: 10px; background-color: white; border-radius: 5px; box-shadow: 1px 1px 1px rgba(0, 0, 0, 0, 5) inset, -1px -1px 1px rgba( 0, 0, 0, 0, 5 ) inset; list-style-type: none; } ``` ```javascript showcase.js $(document).ready(function () { var num_li = $("li").length; n = 1; moving = 0; for (i = 0; i <= num_li; ++i) { $("html,body").stop(); $(".nav li:eq(" + i + ")").click({ id: i }, function (e) { $(".nav li").css("background-color", "white"); page = e.data.id + 1; $("html,body").animate({ scrollTop: $(".p" + page).offset().top }); $(this).css("background-color", "#46dd46"); n = e.data.id + 1; }); } $(window).scroll(function () { if ( $(window).scrollTop() >= $(".p1").offset().top && $(window).scrollTop() < $(".p2").offset().top ) { $(".nav li").css("background-color", "white"); $(".nav li:eq(0)").css("background-color", "#46dd46"); } else if ( $(window).scrollTop() >= $(".p2").offset().top && $(window).scrollTop() < $(".p3").offset().top ) { $(".nav li").css("background-color", "white"); $(".nav li:eq(1)").css("background-color", "#46dd46"); } else if ( $(window).scrollTop() >= $(".p3").offset().top && $(window).scrollTop() < $(".p4").offset().top ) { $(".nav li").css("background-color", "white"); $(".nav li:eq(2)").css("background-color", "#46dd46"); } else if ( $(window).scrollTop() >= $(".p4").offset().top && $(window).scrollTop() < $(".p5").offset().top ) { $(".nav li").css("background-color", "white"); $(".nav li:eq(3)").css("background-color", "#46dd46"); } else if ( $(window).scrollTop() >= $(".p5").offset().top && $(window).scrollTop() < $(".p6").offset().top ) { $(".nav li").css("background-color", "white"); $(".nav li:eq(4)").css("background-color", "#46dd46"); } else if ( $(window).scrollTop() >= $(".p6").offset().top && $(window).scrollTop() < $(".p7").offset().top ) { $(".nav li").css("background-color", "white"); $(".nav li:eq(5)").css("background-color", "#46dd46"); } else if ($(window).scrollTop() >= $(".p7").offset().top) { $(".nav li").css("background-color", "white"); $(".nav li:eq(6)").css("background-color", "#46dd46"); } }); $(window).mousewheel(function (e) { // $("html,body").stop() if (moving == 0) { moving = 1; if (e.deltaY < 0) { if (n < num_li) { ++n; } } else { if (n > 1) { --n; } } $("html,body").animate( { scrollTop: $(".p" + n).offset().top }, 800, function () { moving = 0; } ); } console.log(moving + " " + e.deltaY + " " + n); }); // 一進入網頁時,將導覽列垂直置中 center(); // 縮放網頁時,將導覽列垂直置中 $(window).resize(function () { center(); }); // 計算導覽列置中的位置 function center() { pos = $(window).height() / 2 - $(".nav").height() / 2; $(".nav").css("top", pos); } }); ``` --- # 【100sites #008】Showcase,單頁式網站設計展示我的作品集 - URL: https://easonchang.com/zh-TW/posts/100sites-008-showcase - Date: 2016-03-20T18:42:00.000Z - Language: zh-TW # Showcase,單頁式網站設計,展示我的網站練習作品集 > 一句話摘要:我的第一份單頁式網站 [點我玩玩 Showcase 線上 Demo](http://kamigami55.github.io/100sites/008_Showcase/) [點我查看 Showcase 的 Github 程式碼](https://github.com/eason-dev/100sites/tree/gh-pages/008_Showcase) ![螢幕快照 2016-03-21 上午2.07.29.png](https://i.imgur.com/sZzCbXP.jpg) 我一直很想要學習如何設計單頁式網站,於是今天的 100sites 的題目就是單頁式網站設計啦! 我找到了一份不錯的教學,[PJCHENder 愛分享 - jQuery 學習筆記 第八堂-1](http://pjchender.blogspot.tw/2015/04/jquery_19.html)及[第八堂-2](http://pjchender.blogspot.tw/2015/04/jquery-2mousewheel.html),手把手教你如何做出一個簡單的單頁式網站。 於是我照著他的教學一步一腳印地學習,完成以後改改圖、加些文字、連結、與遮罩,就成為了今天的成品 Showcase 啦! Showcase 比較特別的地方除了單頁式網站設計以外,就是毛玻璃和黑色遮色片的效果了,這兩個效果都是用 CSS 做的。 毛玻璃效果(px 數值大小可以隨你想要的模糊程度做調整): ```css -moz-filter: blur(4px); -webkit-filter: blur(4px); -o-filter: blur(4px); -ms-filter: blur(4px); filter: blur(4px); ``` 黑色遮色片效果則是加了一個 class 為 cover 的 div,設定 background-color 以及 opacity 來達成效果: ```css .cover { height: 100vh; width: 100vw; position: fixed; top: 0px; background-color: #333; opacity: 0.6; z-index: 1; } ``` 下面附上完整程式碼: ```html index.html Showcase

#001

Timer

Demo

#002

Pomodoro

Demo

#003

Todolist

/* Demo Preparing */

#004

Fireworks

Demo

#005

Drawer

Demo

#006

Snack

Demo

#007

Pong

Demo
``` ```css showcase.css * { padding: 0; margin: 0; } body { overflow: hidden; } .cover { height: 100vh; width: 100vw; position: fixed; top: 0px; background-color: #333; opacity: 0.6; z-index: 1; } .picture { background-size: cover; height: 100vh; -moz-filter: blur(4px); -webkit-filter: blur(4px); -o-filter: blur(4px); -ms-filter: blur(4px); filter: blur(4px); } h1 { font-size: 20vh; color: #fff; font-family: Sans-serif; text-shadow: 4px 4px 30px #222; position: relative; top: 50vh; z-index: 3; } h2 { font-size: 8vh; color: #fff; font-family: Sans-serif; text-shadow: 3px 3px 20px #222; position: relative; top: 50vh; z-index: 3; } .info { text-align: center; } a { font-size: 10vh; color: #fff; font-family: Sans-serif; text-shadow: 3px 3px 20px #222; position: relative; top: 60vh; margin: 0px auto; z-index: 3; text-decoration: none; } a:hover { color: #4f4; } .p1 { background-image: url(images/p1.png); } .p2 { background-image: url(images/p2.png); } .p3 { background-image: url(images/p3.png); } .p4 { background-image: url(images/p4.png); } .p5 { background-image: url(images/p5.png); } .p6 { background-image: url(images/p6.png); } .p7 { background-image: url(images/p7.png); } /* 插入導覽列 */ .nav { position: fixed; top: 50%; right: 0px; z-index: 2; } li { width: 10px; height: 10px; margin: 10px; background-color: white; border-radius: 5px; box-shadow: 1px 1px 1px rgba(0, 0, 0, 0, 5) inset, -1px -1px 1px rgba( 0, 0, 0, 0, 5 ) inset; list-style-type: none; } ``` ```javascript showcase.js $(document).ready(function () { var num_li = $("li").length; n = 1; moving = 0; for (i = 0; i <= num_li; ++i) { $("html,body").stop(); $(".nav li:eq(" + i + ")").click({ id: i }, function (e) { $(".nav li").css("background-color", "white"); page = e.data.id + 1; $("html,body").animate({ scrollTop: $(".p" + page).offset().top }); $(this).css("background-color", "#46dd46"); n = e.data.id + 1; }); } $(window).scroll(function () { if ( $(window).scrollTop() >= $(".p1").offset().top && $(window).scrollTop() < $(".p2").offset().top ) { $(".nav li").css("background-color", "white"); $(".nav li:eq(0)").css("background-color", "#46dd46"); } else if ( $(window).scrollTop() >= $(".p2").offset().top && $(window).scrollTop() < $(".p3").offset().top ) { $(".nav li").css("background-color", "white"); $(".nav li:eq(1)").css("background-color", "#46dd46"); } else if ( $(window).scrollTop() >= $(".p3").offset().top && $(window).scrollTop() < $(".p4").offset().top ) { $(".nav li").css("background-color", "white"); $(".nav li:eq(2)").css("background-color", "#46dd46"); } else if ( $(window).scrollTop() >= $(".p4").offset().top && $(window).scrollTop() < $(".p5").offset().top ) { $(".nav li").css("background-color", "white"); $(".nav li:eq(3)").css("background-color", "#46dd46"); } else if ( $(window).scrollTop() >= $(".p5").offset().top && $(window).scrollTop() < $(".p6").offset().top ) { $(".nav li").css("background-color", "white"); $(".nav li:eq(4)").css("background-color", "#46dd46"); } else if ( $(window).scrollTop() >= $(".p6").offset().top && $(window).scrollTop() < $(".p7").offset().top ) { $(".nav li").css("background-color", "white"); $(".nav li:eq(5)").css("background-color", "#46dd46"); } else if ($(window).scrollTop() >= $(".p7").offset().top) { $(".nav li").css("background-color", "white"); $(".nav li:eq(6)").css("background-color", "#46dd46"); } }); $(window).mousewheel(function (e) { // $("html,body").stop() if (moving == 0) { moving = 1; if (e.deltaY < 0) { if (n < num_li) { ++n; } } else { if (n > 1) { --n; } } $("html,body").animate( { scrollTop: $(".p" + n).offset().top }, 800, function () { moving = 0; } ); } console.log(moving + " " + e.deltaY + " " + n); }); // 一進入網頁時,將導覽列垂直置中 center(); // 縮放網頁時,將導覽列垂直置中 $(window).resize(function () { center(); }); // 計算導覽列置中的位置 function center() { pos = $(window).height() / 2 - $(".nav").height() / 2; $(".nav").css("top", pos); } }); ``` --- # 【100sites #007】Pong, a retro game built with P5.js - URL: https://easonchang.com/posts/100sites-007-pong - Date: 2016-03-19T17:55:00.000Z - Language: en - Translation: AI-assisted # Pong, a retro game built with P5.js [Click here to play](http://kamigami55.github.io/100sites/007_Pong/) [Show me the code](https://github.com/eason-dev/100sites/tree/gh-pages/007_Pong) ![Screenshot 2016-03-20 1.23.09 AM.png](https://i.imgur.com/wznoZbr.jpg) > One-sentence summary: Rebuilding the retro game Pong with P5.js, a JavaScript drawing framework, and getting my 100sites project moving again Between being busy, lazy, and procrastinating, it's been almost a month since I last worked on my 100sites project. But now I'm finally done with everything, so it's time to pack up and keep walking this path toward becoming a professional full stack developer! This time, #007 is Pong, a true classic: two players, left and right, each control a paddle and bounce the ball back to the other side. Whoever misses the most balls loses. A while ago at SITCON, a speaker shared a framework called [P5.js](http://p5js.org). It's kind of a sibling of [Processing.js](http://processingjs.org), which I used in #006 Snack. Processing.js uses the Processing language, while P5.js does the same things with JavaScript. With JS as hot as it is right now, P5.js is definitely one of the frameworks worth learning! Here's the code: ```html index.html Pong! ``` ```javascript pong.js var BAR_MOVE_SPEED = 15; var BALL_SIZE = 20; var BALL_SPEED = 10; var MAP_WIDTH = 800; var MAP_HEIGHT = 500; var BAR_WIDTH = 10; var BAR_HEIGHT = 120; var p1_y; var p2_y; var p1_score; var p2_score; var ball_x; var ball_y; var ball_velocity_x; var ball_velocity_y; var gamePaused = false; var gameStarted = false; function setup() { MAP_WIDTH = windowWidth; MAP_HEIGHT = windowHeight; createCanvas(MAP_WIDTH, MAP_HEIGHT); ellipseMode(CENTER); rectMode(CENTER); p1_y = MAP_HEIGHT / 2; p2_y = MAP_HEIGHT / 2; p1_score = 0; p2_score = 0; ball_x = MAP_WIDTH / 2; ball_y = MAP_HEIGHT / 2; ball_velocity_x = BALL_SPEED; ball_velocity_y = BALL_SPEED; } function draw() { if (!gamePaused && gameStarted) { pleyerControl(); ballMove(); ballCollide(); checkBallOutside(); } displayGame(); displayGUI(); } function keyPressed() { if (keyCode === ENTER) { if (!gameStarted) { gameStarted = true; } else { gamePaused = !gamePaused; } } } function pleyerControl() { if (keyIsDown(87) || keyIsDown(119)) { // W if (p1_y - BAR_HEIGHT / 2 > 0) { p1_y -= BAR_MOVE_SPEED; } } else if (keyIsDown(83) || keyIsDown(115)) { // S if (p1_y + BAR_HEIGHT / 2 < MAP_HEIGHT) { p1_y += BAR_MOVE_SPEED; } } if (keyIsDown(UP_ARROW)) { // UP if (p2_y - BAR_HEIGHT / 2 > 0) { p2_y -= BAR_MOVE_SPEED; } } else if (keyIsDown(DOWN_ARROW)) { // DOWN if (p2_y + BAR_HEIGHT / 2 < MAP_HEIGHT) { p2_y += BAR_MOVE_SPEED; } } } function ballMove() { ball_x += ball_velocity_x; ball_y += ball_velocity_y; } function ballCollide() { if (ball_y + BALL_SIZE / 2 > MAP_HEIGHT || ball_y - BALL_SIZE / 2 < 0) { // collide with upper and lower walls ball_velocity_y = -ball_velocity_y; } if ( ball_x <= 30 + BAR_WIDTH / 2 && // collide with player1 ball_x >= 30 - BAR_WIDTH / 2 && ball_y >= p1_y - BAR_HEIGHT / 2 && ball_y <= p1_y + BAR_HEIGHT / 2 ) { ball_velocity_x = -ball_velocity_x; // slightly change ball y speed according to the position ball hit on the bar ball_velocity_y += ((ball_y - p1_y) * 10) / (BAR_HEIGHT / 2); if (ball_velocity_y > BALL_SPEED) // strict the ball speed not exceed max speed ball_velocity_y = BALL_SPEED; else if (ball_velocity_y < -BALL_SPEED) ball_velocity_y = -BALL_SPEED; } if ( ball_x >= MAP_WIDTH - 30 - BAR_WIDTH / 2 && // collide with player2 ball_x <= MAP_WIDTH - 30 + BAR_WIDTH / 2 && ball_y >= p2_y - BAR_HEIGHT / 2 && ball_y <= p2_y + BAR_HEIGHT / 2 ) { ball_velocity_x = -ball_velocity_x; // slightly change ball y speed according to the position ball hit on the bar ball_velocity_y += ((ball_y - p2_y) * 10) / (BAR_HEIGHT / 2); if (ball_velocity_y > BALL_SPEED) // strict the ball speed not exceed max speed ball_velocity_y = BALL_SPEED; else if (ball_velocity_y < -BALL_SPEED) ball_velocity_y = -BALL_SPEED; } } function checkBallOutside() { if (ball_x > MAP_WIDTH) { ball_x = MAP_WIDTH / 2; ball_y = MAP_HEIGHT / 2; ++p1_score; } else if (ball_x < 0) { ball_x = MAP_WIDTH / 2; ball_y = MAP_HEIGHT / 2; ++p2_score; } } function displayGame() { background(0); fill(255); // white rect(30, p1_y, BAR_WIDTH, BAR_HEIGHT); // player1 bar rect(MAP_WIDTH - 30, p2_y, BAR_WIDTH, BAR_HEIGHT); // player2 bar fill(0, 255, 255); ellipse(ball_x, ball_y, BALL_SIZE, BALL_SIZE); // ball } function displayGUI() { if (gamePaused) { textSize(100); fill(255); textAlign(CENTER); text("PAUSED", MAP_WIDTH / 2, MAP_HEIGHT / 2); } if (!gameStarted) { textAlign(CENTER); textSize(100); fill(0, 255, 255); text("Pong!", MAP_WIDTH / 2, 150); fill(255); textSize(80); text("Press ENTER to Start", MAP_WIDTH / 2, MAP_HEIGHT / 2); textSize(40); text( "Left player: W, S to move\nRight player: UP, DOWN to move\nPress ENTER to pause", MAP_WIDTH / 2, MAP_HEIGHT / 2 + 150 ); } textSize(30); textAlign(LEFT); text(p1_score, 30, 30); textAlign(RIGHT); text(p2_score, MAP_WIDTH - 30, 30); } ``` --- # 【100sites #007】Pong,使用P5.js建構的復古遊戲 - URL: https://easonchang.com/zh-TW/posts/100sites-007-pong - Date: 2016-03-19T17:55:00.000Z - Language: zh-TW # Pong,使用 P5.js 建構的復古遊戲 [點我遊玩](http://kamigami55.github.io/100sites/007_Pong/) [我想看看程式碼](https://github.com/eason-dev/100sites/tree/gh-pages/007_Pong) ![螢幕快照 2016-03-20 上午1.23.09.png](https://i.imgur.com/wznoZbr.jpg) > 一句話摘要:使用 P5.js 這個 JavaScript 繪圖框架,重建 Pong 這個復古遊戲,並重新開動 100sites 專案 因為一直很忙加上懶惰加上拖延,已經快一個月沒有持續我的 100sites 專案了,然現在我終於忙完了,是時候該重拾行囊,繼續走完這段成為專業全端工程師的修行之路了! 這次的#007 成品是 Pong,一個很經典遊戲,左右兩位玩家操縱著各自的棒棒將球反彈給對方,漏球最多的一方輸。 前些日子在 SITCON 年有講者分享[P5.js](http://p5js.org)這個框架,他算是我在#006 Snack 裡面使用的[Processing.js](http://processingjs.org)的兄弟,Processing.js 使用 Processing 語言,但 P5.js 使用 JavaScript 來達到相同的功能,對於現在 JS 大熱門的趨勢來看,P5.js 也是一個很值得學起來的框架之一呀! 以下附上本次的程式碼: ```html index.html Pong! ``` ```javascript pong.js var BAR_MOVE_SPEED = 15; var BALL_SIZE = 20; var BALL_SPEED = 10; var MAP_WIDTH = 800; var MAP_HEIGHT = 500; var BAR_WIDTH = 10; var BAR_HEIGHT = 120; var p1_y; var p2_y; var p1_score; var p2_score; var ball_x; var ball_y; var ball_velocity_x; var ball_velocity_y; var gamePaused = false; var gameStarted = false; function setup() { MAP_WIDTH = windowWidth; MAP_HEIGHT = windowHeight; createCanvas(MAP_WIDTH, MAP_HEIGHT); ellipseMode(CENTER); rectMode(CENTER); p1_y = MAP_HEIGHT / 2; p2_y = MAP_HEIGHT / 2; p1_score = 0; p2_score = 0; ball_x = MAP_WIDTH / 2; ball_y = MAP_HEIGHT / 2; ball_velocity_x = BALL_SPEED; ball_velocity_y = BALL_SPEED; } function draw() { if (!gamePaused && gameStarted) { pleyerControl(); ballMove(); ballCollide(); checkBallOutside(); } displayGame(); displayGUI(); } function keyPressed() { if (keyCode === ENTER) { if (!gameStarted) { gameStarted = true; } else { gamePaused = !gamePaused; } } } function pleyerControl() { if (keyIsDown(87) || keyIsDown(119)) { // W if (p1_y - BAR_HEIGHT / 2 > 0) { p1_y -= BAR_MOVE_SPEED; } } else if (keyIsDown(83) || keyIsDown(115)) { // S if (p1_y + BAR_HEIGHT / 2 < MAP_HEIGHT) { p1_y += BAR_MOVE_SPEED; } } if (keyIsDown(UP_ARROW)) { // UP if (p2_y - BAR_HEIGHT / 2 > 0) { p2_y -= BAR_MOVE_SPEED; } } else if (keyIsDown(DOWN_ARROW)) { // DOWN if (p2_y + BAR_HEIGHT / 2 < MAP_HEIGHT) { p2_y += BAR_MOVE_SPEED; } } } function ballMove() { ball_x += ball_velocity_x; ball_y += ball_velocity_y; } function ballCollide() { if (ball_y + BALL_SIZE / 2 > MAP_HEIGHT || ball_y - BALL_SIZE / 2 < 0) { // collide with upper and lower walls ball_velocity_y = -ball_velocity_y; } if ( ball_x <= 30 + BAR_WIDTH / 2 && // collide with player1 ball_x >= 30 - BAR_WIDTH / 2 && ball_y >= p1_y - BAR_HEIGHT / 2 && ball_y <= p1_y + BAR_HEIGHT / 2 ) { ball_velocity_x = -ball_velocity_x; // slightly change ball y speed according to the position ball hit on the bar ball_velocity_y += ((ball_y - p1_y) * 10) / (BAR_HEIGHT / 2); if (ball_velocity_y > BALL_SPEED) // strict the ball speed not exceed max speed ball_velocity_y = BALL_SPEED; else if (ball_velocity_y < -BALL_SPEED) ball_velocity_y = -BALL_SPEED; } if ( ball_x >= MAP_WIDTH - 30 - BAR_WIDTH / 2 && // collide with player2 ball_x <= MAP_WIDTH - 30 + BAR_WIDTH / 2 && ball_y >= p2_y - BAR_HEIGHT / 2 && ball_y <= p2_y + BAR_HEIGHT / 2 ) { ball_velocity_x = -ball_velocity_x; // slightly change ball y speed according to the position ball hit on the bar ball_velocity_y += ((ball_y - p2_y) * 10) / (BAR_HEIGHT / 2); if (ball_velocity_y > BALL_SPEED) // strict the ball speed not exceed max speed ball_velocity_y = BALL_SPEED; else if (ball_velocity_y < -BALL_SPEED) ball_velocity_y = -BALL_SPEED; } } function checkBallOutside() { if (ball_x > MAP_WIDTH) { ball_x = MAP_WIDTH / 2; ball_y = MAP_HEIGHT / 2; ++p1_score; } else if (ball_x < 0) { ball_x = MAP_WIDTH / 2; ball_y = MAP_HEIGHT / 2; ++p2_score; } } function displayGame() { background(0); fill(255); // white rect(30, p1_y, BAR_WIDTH, BAR_HEIGHT); // player1 bar rect(MAP_WIDTH - 30, p2_y, BAR_WIDTH, BAR_HEIGHT); // player2 bar fill(0, 255, 255); ellipse(ball_x, ball_y, BALL_SIZE, BALL_SIZE); // ball } function displayGUI() { if (gamePaused) { textSize(100); fill(255); textAlign(CENTER); text("PAUSED", MAP_WIDTH / 2, MAP_HEIGHT / 2); } if (!gameStarted) { textAlign(CENTER); textSize(100); fill(0, 255, 255); text("Pong!", MAP_WIDTH / 2, 150); fill(255); textSize(80); text("Press ENTER to Start", MAP_WIDTH / 2, MAP_HEIGHT / 2); textSize(40); text( "Left player: W, S to move\nRight player: UP, DOWN to move\nPress ENTER to pause", MAP_WIDTH / 2, MAP_HEIGHT / 2 + 150 ); } textSize(30); textAlign(LEFT); text(p1_score, 30, 30); textAlign(RIGHT); text(p2_score, MAP_WIDTH - 30, 30); } ``` --- # How to Install a Verilog Environment on Mac OS X - URL: https://easonchang.com/posts/verilog-on-macosx - Date: 2016-03-18T17:40:00.000Z - Language: en - Translation: AI-assisted > One-sentence summary: use Icarus Verilog to compile Verilog, and GTKWave to display waveforms For my CS department's digital circuit design course, we need an environment that can compile Verilog, a hardware description language (HDL), and also simulate how a circuit runs and display the waveforms. The environment our teacher recommended is [ModelSim](https://www.mentor.com/products/fpga/model/), but it only supports Windows. I haven't found a Mac OS X or Linux version yet, so I had to look for other tools that can compile Verilog. I found [Icarus Verilog](http://iverilog.icarus.com/home), a Verilog compiler for Linux. On Mac OS X, we can use it through the command line. # Installing Icarus Verilog Here is the [Icarus Verilog installation guide](http://iverilog.wikia.com/wiki/Installation_Guide). It explains very thoroughly how to install it on all kinds of operating systems, though it's a bit long. On Mac OS X you have two ways to install it: compile it from source, or install it through a package manager. For a lazy person like me, the package manager it is! I use [Homebrew](http://brew.sh/index_zh-tw.html) to install Icarus Verilog. If you haven't installed Homebrew yet, go get it! As a Mac user and CS student, Homebrew is a must-have! You can install Homebrew by entering this in your command line (ignore the $ on the far left — that's the command line prompt): ``` $ /usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)" ``` Then use this Homebrew command to install Icarus Verilog: ``` $ brew install icarus-verilog ``` OK! You've now successfully installed Icarus Verilog! Homebrew really is that magical! # Testing Icarus Verilog After installing a new program, of course we have to check that it works properly. Time for the classic Hello World test program! First, create a new file named hello.v with the following content (Verilog source files use the .v extension): ```Verilog hello.v module main; initial begin $display("Hello, World"); $finish ; end endmodule ``` Then use the iverilog command to compile hello.v, with -o hello to name the generated executable hello: ``` $ iverilog -o hello hello.v ``` Finally, use the vvp command to run the hello executable. If you see the output Hello, World, your Icarus Verilog installation succeeded: ``` $ vvp hello Hello, World ``` # Installing GTKWave Next, to check how your circuit runs more conveniently, you need waveform viewer software. I use [GTKWave](http://gtkwave.sourceforge.net), a waveform viewer that runs on Mac OS X, Linux, and Windows. Here's what it looks like (screenshot from the official site): ![13010278_1324168817596781_1227376771_o.png](https://i.imgur.com/qRG7zAU.jpg) Installing it on Mac OS X is easy, just like installing any other app. The hardest part is probably finding the correct download link on the official site... I've marked it out for you — find the download link in the red box in the image above and click it! Then unzip the downloaded .zip file, and the .app file you get is your GTKWave! No installation needed, just click and use. How cool is that! # Using GTKWave to Display Waveforms ## Creating a New Test Program Don't rush to open your GTKWave yet — you still need a new test program. That hello world program from before hardly counts as a circuit! First, create two new files named Simple_Circuit.v and t_Simple_Circuit.v (modified from our teacher's Lab0 example): ```Verilog Simple_Circuit.v module Simple_Circuit(A, B, C, D, E); output D, E; input A, B, C; wire w1; and G1(w1, A, B); not G2(E, C); or G3(D, w1, E); endmodule ``` ```Verilog t_Simple_Circuit.v module t_Simple_Circuit; wire D, E; reg A, B, C; //instantiate device under test Simple_Circuit M1(A, B, C, D, E); //apply inputs one at a time initial begin $dumpfile("mytest.vcd"); $dumpvars; A=1'b0; B=1'b0; C=1'b0; #100 A=1'b1; B=1'b1; C=1'b1; end initial #200 $finish; endmodule ``` Compile these two files into an executable called mycircuit, and run it: ``` $ iverilog -o mycircuit t_Simple_Circuit.v Simple_Circuit.v $ vvp mycircuit ``` Because we added dumpfile and dumpvars to the t_Simple_Circuit.v circuit test file, running mycircuit produces a file named mytest.vcd, which contains the waveform records we want. Now we can use GTKWave to view the waveform! ## Opening GTKWave to View the Waveform Open your GTKWave and click File > Open New Tab in the top left. Select the mytest.vcd file you just generated. You'll probably be disappointed to find that the waveform you want isn't showing up, like in the image below: ![Screenshot 2016-03-19 1.23.20 AM.png](https://i.imgur.com/RJIQuvS.jpg) That's because you haven't told GTKWave what you want to see yet. So let's tell it! In GTKWave's left panel, you'll see the text t_Simple_Circuit — that's your test file. Click the + sign to expand it, click the M1 circuit, and all the wires inside the M1 circuit will be listed below. Drag the wires whose waveforms you want to see into the area on the right. Ta-da! There's your waveform! ![Screenshot 2016-03-19 1.30.18 AM.png](https://i.imgur.com/86eXv9b.jpg) Congratulations on successfully setting up your Verilog environment. Now you can show off doing your digital circuit design homework on your Macbook! # Command Cheat Sheet ``` $ iverilog -o ... // 編譯 $ vvp // 執行 ``` --- # 如何在Mac OS X上安裝Verilog環境 - URL: https://easonchang.com/zh-TW/posts/verilog-on-macosx - Date: 2016-03-18T17:40:00.000Z - Language: zh-TW > 一句話摘要:使用 Icarus Verilog 來編譯 Verilog、使用 GTKWave 來顯示波形 為了資工系的數位電路設計課,我們需要一個能夠編譯 Verilog 這個硬體描述語言(HDL)的環境,並且要能模擬電路運作狀況加上顯示波形。 老師建議的編譯環境是[ModelSim](https://www.mentor.com/products/fpga/model/),但是它只支援 Windows,我目前還沒找到 Mac OS X 或是 Linux 的版本,所以我只好另尋他法,找看看有沒有其他能夠編譯 Verilog 的工具。 我找到了[Icarus Verilog](http://iverilog.icarus.com/home),它是針對 Linux 的 Verilog 編譯器,而在 Mac OS X 底下,我們可以藉由 Command Line 來使用它。 # 安裝 Icarus Verilog 這是[Icarus Verilog 的安裝指南](http://iverilog.wikia.com/wiki/Installation_Guide),很完整的說明了怎麼在各種作業系統上安裝它,不過是英文的,而且字有點多。 對於 Mac OS X 你有兩種安裝手段,編譯程式原始碼來安裝,或是透過套件管理系統安裝,對於我這種懶人來說,當然是選擇使用套件管理系統囉! 我使用[Homebrew](http://brew.sh/index_zh-tw.html)來安裝 Icarus Verilog,如果你還沒有安裝 Homebrew,去把它裝起來吧!身為一位 Mac 用戶兼資工系學生,Homebrew 必備呀! 你可以在你的 Command Line 裡輸入這段文字來安裝 Homebrew(請忽略最左方的$符號,這是 Command Line 的提示字元): ``` $ /usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)" ``` 接著使用這段 homebrew 的指令來安裝 Icarus Verilog: ``` $ brew install icarus-verilog ``` OK!現在你已經成功安裝了 Icarus Verilog 了!Homebrew 就是這麼的神奇! # 測試 Icarus Verilog 安裝完了一個新程式,當然是要測試一下它的功能正不正常呀,這時就輪到經典 Hello World 測試程式登場了! 首先建立一個新文件命名為 hello.v,內含以下內容(Verilog 的源碼檔副檔名為.v): ```Verilog hello.v module main; initial begin $display("Hello, World"); $finish ; end endmodule ``` 接著使用 iverilog 指令來編譯 hello.v,使用-o hello 來指定以 hello 來作為產生的可執行檔檔名: ``` $ iverilog -o hello hello.v ``` 最後,使用 vvp 指令來執行 hello 這個可執行檔,只要你看到 Hello, World 這個輸出,就代表你的 Icarus Verilog 安裝成功了: ``` $ vvp hello Hello, World ``` # 安裝 GTKWave 接著,為了能夠更方便地查看電路運作的狀況,你需要一個波型顯示軟體,我使用[GTKWave](http://gtkwave.sourceforge.net),這是一個可以跑在 Mac OS X、Linux、和 Windows 上的波型顯示軟體,使用效果如下圖(截圖自官網): ![13010278_1324168817596781_1227376771_o.png](https://i.imgur.com/qRG7zAU.jpg) 要在 Mac OS X 上安裝它很容易,就跟你安裝其他應用程式一樣,最大的困難可能是在官網上找到正確的下載連結......,連結已經幫你標出來了,請在官網上找到上圖紅框處 download 連結把它點下去! 接著把載下來了.zip 檔解壓縮,完畢後得到的.app 檔就是你要的 GTKWave 了!隨點隨用免安裝,那~麼~厲害! # 使用 GTKWave 來顯示波形 ## 建立新的測試程式 先別急著打開你的 GTKWave,你還需要一個新的測試程式,之前的 hello world 程式可根本不算是一個電路呢! 首先建立兩個新檔案,命名為 Simple_Circuit.v 和 t_Simple_Circuit.v(修改自我們老師的 Lab0 範例): ```Verilog Simple_Circuit.v module Simple_Circuit(A, B, C, D, E); output D, E; input A, B, C; wire w1; and G1(w1, A, B); not G2(E, C); or G3(D, w1, E); endmodule ``` ```Verilog t_Simple_Circuit.v module t_Simple_Circuit; wire D, E; reg A, B, C; //instantiate device under test Simple_Circuit M1(A, B, C, D, E); //apply inputs one at a time initial begin $dumpfile("mytest.vcd"); $dumpvars; A=1'b0; B=1'b0; C=1'b0; #100 A=1'b1; B=1'b1; C=1'b1; end initial #200 $finish; endmodule ``` 將這兩個檔案編譯為 mycircuit 這個執行檔,並且執行它: ``` $ iverilog -o mycircuit t_Simple_Circuit.v Simple_Circuit.v $ vvp mycircuit ``` 因為我們在 t_Simple_Circuit.v 這個電路測試檔案裡頭加入了 dumpfile 和 dumpvars,所以執行完 mycircuit 後,會產生一個名為 mytest.vcd 的檔案,裡頭包含了我們所要的波型紀錄。 現在我們可以使用 GTKWave 來檢視波型了! ## 正式開啟 GTKWave 來檢視波型 開啟你的 GTKWave,點擊左上角的 File > Open New Tab 選擇剛剛產生的 mytest.vcd 檔,你應該會很失望地發現為什麼沒有出現你要的波型,如下圖: ![螢幕快照 2016-03-19 上午1.23.20.png](https://i.imgur.com/RJIQuvS.jpg) 那是因為你還沒有告訴 GTKWave 你想要看什麼,那我們就來告訴它吧! 在 GTKWave 左側面板,你會看到一串文字寫著 t_Simple_Circuit,那就是你的測試檔案,點開那個+號,點擊 M1 電路,下面就會列出 M1 電路裡所有的線路,把你想要看它波形的線路拖曳到右方的區塊吧!噠答!你要的波型顯示出來了! ![螢幕快照 2016-03-19 上午1.30.18.png](https://i.imgur.com/86eXv9b.jpg) 恭喜你成功建置你的 Verilog 環境,現在你可以炫砲地使用你的 Macbook 來做數電設計作業了! # 指令小筆記 ``` $ iverilog -o ... // 編譯 $ vvp // 執行 ``` --- # [Reading Notes] Essentialism — Live Essentially and Take Back Control of Your Life - URL: https://easonchang.com/posts/less-but-better - Date: 2016-03-17T08:20:00.000Z - Language: en - Translation: AI-assisted *Essentialism* Author: Greg McKeown Translator: Cai-Ni Zhan > One-sentence summary: Say no to anything you don't absolutely love, apply the 80/20 rule, live essentially, and take back control of your life ![IMG_2342.JPG](https://i.imgur.com/P0nkaLj.jpg) I recently finished running a startup forum event and felt my life needed to be healthier and more regular. I didn't want to keep being busy without getting anywhere. That's when I came across *Essentialism*, and I immediately knew it was the book I needed most right now. So I decided to give it a chance — and it turned my life around. The core concept of **Essentialism** is actually very simple: **living essentially**. And how do you do that? **Say no to anything you don't absolutely love.** There you go — you've finished the book. The core concept of *Essentialism* really is that simple, "living essentially", and the remaining two hundred-plus pages exist to support this concept and show you how to put a simple life into practice. A summary this lean is itself a demonstration of the **80/20 rule (the Pareto principle)**. Yes — even though every paragraph in the book stays on topic, I think it has way too many words. If you want to read it, my personal advice is to just read the highlights. Where are they? **The images, the inverted black pages, the bold text, and the "Essentialist vs. Non-Essentialist" table found in every chapter.** If you're short on time, these parts are enough. Here's a quick look at the book's structure. *Essentialism* is divided into four parts: - Essence: What is the core mindset of an Essentialist? Knowing how to make trade-offs - Explore: What are the criteria for those trade-offs? What is the 20% that benefits you the most? - Eliminate: Once you've found your 20%, how do you cut out the less important 80%? - Execute: Finally, how do you effectively execute on that most important 20%? After finishing the book, I immediately started living essentially. I turned down some gatherings I wasn't that into, and stopped eagerly filling my calendar with new activities — stepping away from a life that looks full but actually gets you nothing. I've only just started, but I can already feel a clear improvement in my quality of life. I'm starting to take back **control of my life** — and I'm really not joking about this. > "If you don't prioritize your life, someone else will." If you want a **better quality of life**, want to **stop being busy without getting anywhere**, and want to live a **more productive life**, I think *Essentialism* is a great place to start. --- # 【閱讀筆記】少,但是更好——精準生活,拿回人生主控權 - URL: https://easonchang.com/zh-TW/posts/less-but-better - Date: 2016-03-17T08:20:00.000Z - Language: zh-TW 《少,但是更好(Essentialism)》 作者:葛瑞格・麥基昂 (Greg McKeown) 譯者:詹采妮 > 一句話書摘:對你不是完全喜歡的事情說不,運用 80/20 法則,過上精準生活,拿回人生主控權 ![IMG_2342.JPG](https://i.imgur.com/P0nkaLj.jpg) 我最近剛忙完創業論壇的活動,覺得生活需要更健康規律,我不想再繼續過著窮忙生活了,而此時剛好看到《少,但是更好》,我立刻就知道它是我現在最需要的一本書,於是我決定給它一個機會,而它扭轉了我的生活。 **《少,但是更好》**的核心概念其實很簡單,就是**「精準生活」**,而怎麼做到呢?**「對你不是完全喜歡的事情說不」** 好了,你已經看完這本書了,《少,但是更好》的核心概念就這麼簡單,「精準生活」,剩下兩百多頁的文字都是用來支持這個概念,以及告訴你如何實踐簡單生活。 如此精簡的書摘,其實也是**80/20 法則(帕雷托法則)**的一種展現。 是的,雖然書中的每段文字都緊扣著主題,但我覺得這本書的字數太多了,如果你想要讀這本書,個人建議挑重點讀就好了。重點在哪呢?**「書中的圖片、反黑頁面、粗體字、及書中的每一章都會有的『專準主義者 v.s.非專準主義者』表格」**,如果你趕時間的話,看這些部分就足夠了。 簡單介紹一下本書的架構,《少,但是更好》分為四大部分: - 追求本質:專準主義者的核心理念是什麼?懂得取捨 - 精挑:取捨的標準是什麼?什麼才是對你最有效益的 20%? - 簡化:當你找到了你的 20%,如何排除那些沒那麼重要的 80%? - 準確執行:最後,你要如何有效地執行那最重要的 20%? 讀完了這本書,我立刻就開始實踐「精準生活」,我推掉了一些我沒那麼喜歡的聚會,並且不再一頭熱地用新活動填滿行事曆,遠離看似過得很充實但其實什麼都沒得到的生活。 雖然才剛開始實踐,但我可以感覺得出我的生活品質有明顯提升,我開始拿回我**人生的主控權**了,這真的不是開玩笑的 > 「如果你不主動決定你的生活,別人就會替你代勞」。 我認為如果你想追求**更好的生活品質**、**擺脫窮忙生活**、過上**更有產值的人生**,《少,但是更好》很適合作為你的敲門磚。 --- # 【100sites #006】Snack - URL: https://easonchang.com/posts/100sites-006-snack - Date: 2016-02-16T12:41:00.000Z - Language: en - Translation: AI-assisted # Snack - Snake Game, Eat Those Dots! [Github](https://github.com/eason-dev/100sites/tree/gh-pages/006_Snack) [Live Demo](http://kamigami55.github.io/100sites/006_Snack/) ![Screenshot 2016-02-16 8.39.27 PM.png](https://i.imgur.com/BfDXbAU.jpg) I had some free time today, so I finally built the snake game I'd been wanting to make, using Processing.js. The rules are the same as classic snake: keep eating dots to grow longer and longer, but you lose if you run into yourself. Use the arrow keys to move, and Enter to start/pause. I'm working hard on learning how to organize my code and write proper comments, so feedback is very welcome. Today's code: ```html index.html Snack

Be a Snack!

Move - Arrow key.

Start/Pause - Enter(Return)

``` ```javascript snack.pde /* @pjs globalKeyEvents="true"; */ int NUM_OF_PILES = 26; // how many piles per dimension int PILE_SIZE = 18; // unit: px int SPEED = 12; // frame rate int DEFAULT_SNACK_LENGTH = 4; int DIRECTION_UP = 1; // direction of the snack (1:up 2:down 3:left 4:right) int DIRECTION_DOWN = 2; int DIRECTION_LEFT = 3; int DIRECTION_RIGHT = 4; int DEFAULT_SNACK_DIRECTION = DIRECTION_UP; int direction = DEFAULT_SNACK_DIRECTION; // direction of snack ArrayList snack = new ArrayList(); Food food = new Food(0, 0); // the only food on the board boolean isPausing = true; // whether the game is pausing void setup() { size(NUM_OF_PILES * PILE_SIZE, NUM_OF_PILES * PILE_SIZE); ellipseMode(CORNER); frameRate(SPEED); resetSnack(); food.resetPosition(); noLoop(); // the default state of the game is pausing isPausing = true; } void resetSnack() { // clear snack arrarList for(int i = 0; i < snack.size(); ++i) { snack.remove(0); } direction = DEFAULT_SNACK_DIRECTION; // set direction to default (up) // set the snack at the center of the board with default snack length // +i is to set the tail of snack to the south(+Y) for(int i = 0; i < DEFAULT_SNACK_LENGTH; ++i) { snack.add(new SnackUnit(NUM_OF_PILES/2, NUM_OF_PILES/2+i)); } } void draw() { background(50); for(int i = 0; i < snack.size(); ++i) { snack.get(i).display(); } food.display(); snackMove(); checkFood(); checkLose(); } void snackMove() { int vx = 0; // velocity of the snack at x axis (-1, 0, 1) int vy = 0; // velocity of the snack at x axis (-1, 0, 1) // set velocities according to the direction switch(direction) { case DIRECTION_UP: vy = -1; break; case DIRECTION_DOWN: vy = 1; break; case DIRECTION_LEFT: vx = -1; break; case DIRECTION_RIGHT: vx = 1; break; default: break; } // move snack body forward for(int i = snack.size()-1; i > 0; --i) { snack.get(i).xpos = snack.get(i-1).xpos; snack.get(i).ypos = snack.get(i-1).ypos; } // move snack head forward according to the velocities snack.get(0).xpos += vx; snack.get(0).ypos += vy; // if the snack is in the wall, make it appear on the other side if(snack.get(0).xpos < 0) snack.get(0).xpos = NUM_OF_PILES-1; else if(snack.get(0).xpos > NUM_OF_PILES-1) snack.get(0).xpos = 0; else if(snack.get(0).ypos < 0) snack.get(0).ypos = NUM_OF_PILES-1; else if(snack.get(0).ypos > NUM_OF_PILES-1) snack.get(0).ypos = 0; } void checkFood() { if(snack.get(0).isOn(food.xpos, food.ypos)) { // append a new snack body at the tail snack.add(new SnackUnit(snack.get(snack.size()-1).xpos, snack.get(snack.size()-1).ypos)); food.resetPosition(); } } void checkLose() { // if the snack hit itself for(int i = 1; i < snack.size(); ++i) { if(snack.get(0).isOn(snack.get(i).xpos, snack.get(i).ypos)) { // the player lose, pause the game isPausing = true; noLoop(); } } } void keyPressed() { switch(keyCode) { // pressing arrow key to change the direction case UP: direction = DIRECTION_UP; break; case DOWN: direction = DIRECTION_DOWN; break; case LEFT: direction = DIRECTION_LEFT; break; case RIGHT: direction = DIRECTION_RIGHT; break; // pressing ENTER and RETURN to toggle Pausing state case ENTER: case RETURN: if(isPausing) { isPausing = false; loop(); } else { isPausing = true; noLoop(); } break; default: break; } } class SnackUnit { int xpos, ypos; SnackUnit(int x, int y) { xpos = x; ypos = y; } void display() { fill(255); rect(xpos*PILE_SIZE, ypos*PILE_SIZE, PILE_SIZE, PILE_SIZE); } boolean isOn(int x, int y) { return xpos == x && ypos == y; } } class Food { int xpos, ypos; Food(int x, int y) { xpos = x; ypos = y; } void display() { fill(255); // the size of the food is 4px smaller than a pile ellipse(xpos*PILE_SIZE+2, ypos*PILE_SIZE+2, PILE_SIZE-4, PILE_SIZE-4); } void resetPosition() { xpos = int(random(0, NUM_OF_PILES)); ypos = int(random(0, NUM_OF_PILES)); for(int i = 0; i < snack.size(); ++i) { if(snack.get(i).isOn(xpos, ypos)) { // if the snack is already on the position of the new food resetPosition(); // reset food position to another place } } } } ``` --- # 【100sites #006】Snack - URL: https://easonchang.com/zh-TW/posts/100sites-006-snack - Date: 2016-02-16T12:41:00.000Z - Language: zh-TW # Snack - 貪食蛇,吃掉那些點點吧! [Github](https://github.com/eason-dev/100sites/tree/gh-pages/006_Snack) [Live Demo](http://kamigami55.github.io/100sites/006_Snack/) ![螢幕快照 2016-02-16 下午8.39.27.png](https://i.imgur.com/BfDXbAU.jpg) 今天比較有閒有空,把之前一直想做的貪食蛇遊戲做出來了,使用 Processing.js。 規則如同經典的貪食蛇,目標是不斷的吃點點,讓自己越長越長,但如果撞到自己就輸了。 使用方向鍵來移動,Enter 鍵來開始/暫停。 我正在努力學習如何組織自己的程式碼和適當地下註解,非常歡迎指教。 今天的 code: ```html index.html Snack

Be a Snack!

Move - Arrow key.

Start/Pause - Enter(Return)

``` ```javascript snack.pde /* @pjs globalKeyEvents="true"; */ int NUM_OF_PILES = 26; // how many piles per dimension int PILE_SIZE = 18; // unit: px int SPEED = 12; // frame rate int DEFAULT_SNACK_LENGTH = 4; int DIRECTION_UP = 1; // direction of the snack (1:up 2:down 3:left 4:right) int DIRECTION_DOWN = 2; int DIRECTION_LEFT = 3; int DIRECTION_RIGHT = 4; int DEFAULT_SNACK_DIRECTION = DIRECTION_UP; int direction = DEFAULT_SNACK_DIRECTION; // direction of snack ArrayList snack = new ArrayList(); Food food = new Food(0, 0); // the only food on the board boolean isPausing = true; // whether the game is pausing void setup() { size(NUM_OF_PILES * PILE_SIZE, NUM_OF_PILES * PILE_SIZE); ellipseMode(CORNER); frameRate(SPEED); resetSnack(); food.resetPosition(); noLoop(); // the default state of the game is pausing isPausing = true; } void resetSnack() { // clear snack arrarList for(int i = 0; i < snack.size(); ++i) { snack.remove(0); } direction = DEFAULT_SNACK_DIRECTION; // set direction to default (up) // set the snack at the center of the board with default snack length // +i is to set the tail of snack to the south(+Y) for(int i = 0; i < DEFAULT_SNACK_LENGTH; ++i) { snack.add(new SnackUnit(NUM_OF_PILES/2, NUM_OF_PILES/2+i)); } } void draw() { background(50); for(int i = 0; i < snack.size(); ++i) { snack.get(i).display(); } food.display(); snackMove(); checkFood(); checkLose(); } void snackMove() { int vx = 0; // velocity of the snack at x axis (-1, 0, 1) int vy = 0; // velocity of the snack at x axis (-1, 0, 1) // set velocities according to the direction switch(direction) { case DIRECTION_UP: vy = -1; break; case DIRECTION_DOWN: vy = 1; break; case DIRECTION_LEFT: vx = -1; break; case DIRECTION_RIGHT: vx = 1; break; default: break; } // move snack body forward for(int i = snack.size()-1; i > 0; --i) { snack.get(i).xpos = snack.get(i-1).xpos; snack.get(i).ypos = snack.get(i-1).ypos; } // move snack head forward according to the velocities snack.get(0).xpos += vx; snack.get(0).ypos += vy; // if the snack is in the wall, make it appear on the other side if(snack.get(0).xpos < 0) snack.get(0).xpos = NUM_OF_PILES-1; else if(snack.get(0).xpos > NUM_OF_PILES-1) snack.get(0).xpos = 0; else if(snack.get(0).ypos < 0) snack.get(0).ypos = NUM_OF_PILES-1; else if(snack.get(0).ypos > NUM_OF_PILES-1) snack.get(0).ypos = 0; } void checkFood() { if(snack.get(0).isOn(food.xpos, food.ypos)) { // append a new snack body at the tail snack.add(new SnackUnit(snack.get(snack.size()-1).xpos, snack.get(snack.size()-1).ypos)); food.resetPosition(); } } void checkLose() { // if the snack hit itself for(int i = 1; i < snack.size(); ++i) { if(snack.get(0).isOn(snack.get(i).xpos, snack.get(i).ypos)) { // the player lose, pause the game isPausing = true; noLoop(); } } } void keyPressed() { switch(keyCode) { // pressing arrow key to change the direction case UP: direction = DIRECTION_UP; break; case DOWN: direction = DIRECTION_DOWN; break; case LEFT: direction = DIRECTION_LEFT; break; case RIGHT: direction = DIRECTION_RIGHT; break; // pressing ENTER and RETURN to toggle Pausing state case ENTER: case RETURN: if(isPausing) { isPausing = false; loop(); } else { isPausing = true; noLoop(); } break; default: break; } } class SnackUnit { int xpos, ypos; SnackUnit(int x, int y) { xpos = x; ypos = y; } void display() { fill(255); rect(xpos*PILE_SIZE, ypos*PILE_SIZE, PILE_SIZE, PILE_SIZE); } boolean isOn(int x, int y) { return xpos == x && ypos == y; } } class Food { int xpos, ypos; Food(int x, int y) { xpos = x; ypos = y; } void display() { fill(255); // the size of the food is 4px smaller than a pile ellipse(xpos*PILE_SIZE+2, ypos*PILE_SIZE+2, PILE_SIZE-4, PILE_SIZE-4); } void resetPosition() { xpos = int(random(0, NUM_OF_PILES)); ypos = int(random(0, NUM_OF_PILES)); for(int i = 0; i < snack.size(); ++i) { if(snack.get(i).isOn(xpos, ypos)) { // if the snack is already on the position of the new food resetPosition(); // reset food position to another place } } } } ``` --- # 【100sites #005】Drawer - URL: https://easonchang.com/posts/100sites-005-drawer - Date: 2016-02-15T17:49:00.000Z - Language: en - Translation: AI-assisted # Drawer - Simple Number Drawing [Github](https://github.com/eason-dev/100sites/tree/gh-pages/005_Drawer) [Live Demo](http://kamigami55.github.io/100sites/005_Drawer/) ![Screenshot 2016-02-16 1.47.27 AM.png](https://i.imgur.com/k5wMk9z.jpg) I moved back to Hsinchu today and really didn't have much time to code, so I wrote a simple but practical little number-drawing program. The feature so far is very basic: the user enters a minimum and a maximum, clicks the button, and gets a random number. For frameworks I only used JQuery and Bootstrap, my two favorites. I'll add more features when I have time, like different drawing modes and a spinning-wheel animation. Here's the code: ```html index.html Drawer

Drawer!

Pick One:

From To
Choose your Max and Min, I will give you a number!
``` ```javascript drawer.js $(document).ready(function () { pickone_btn = $("#pickone"); result_panel = $("#result-panel"); // style of result panel, used to change result panel style result_body = $("#result-body"); // body of result panel, used to display result max_fld = $("#maximum"); // maximum input field min_fld = $("#minimum"); // minimum input field pickone_btn.click(function () { var min = parseInt(min_fld.val()); var max = parseInt(max_fld.val()); // if the input is invalid (empty or not number) if (isNaN(min) || isNaN(max)) { result_body.html("I need both MIN and MAX!"); // set style to danger(red) result_panel.addClass("panel-danger"); result_panel.removeClass("panel-info"); result_panel.removeClass("panel-success"); return; } if (min > max) { // interchange min & max var tmp = min; min = max; max = tmp; // interchange value of input fields max_fld.val(max); min_fld.val(min); } var result = Math.floor(Math.random() * (max - min + 1) + min); result_body.html("You got " + result + " !"); // set style to success(green) result_panel.addClass("panel-success"); result_panel.removeClass("panel-info"); result_panel.removeClass("panel-danger"); }); }); ``` --- # 【100sites #005】Drawer - URL: https://easonchang.com/zh-TW/posts/100sites-005-drawer - Date: 2016-02-15T17:49:00.000Z - Language: zh-TW # Drawer - 簡單抽數字 [Github](https://github.com/eason-dev/100sites/tree/gh-pages/005_Drawer) [Live Demo](http://kamigami55.github.io/100sites/005_Drawer/) ![螢幕快照 2016-02-16 上午1.47.27.png](https://i.imgur.com/k5wMk9z.jpg) 今天搬回新竹,實在沒多少時間寫程式,於是就寫了一個簡單卻實用的小抽籤程式。 目前寫出來的功能很簡單,使用者輸入最小值和最大值,點擊按鈕後就能抽到一個隨機數字。 框架上只使用到了 JQuery 和 Bootstrap,我最愛的兩個框架。 之後有時間會加上更多功能,如:不同的抽籤模式、轉盤動畫等。 以下是程式碼: ```html index.html Drawer

Drawer!

Pick One:

From To
Choose your Max and Min, I will give you a number!
``` ```javascript drawer.js $(document).ready(function () { pickone_btn = $("#pickone"); result_panel = $("#result-panel"); // style of result panel, used to change result panel style result_body = $("#result-body"); // body of result panel, used to display result max_fld = $("#maximum"); // maximum input field min_fld = $("#minimum"); // minimum input field pickone_btn.click(function () { var min = parseInt(min_fld.val()); var max = parseInt(max_fld.val()); // if the input is invalid (empty or not number) if (isNaN(min) || isNaN(max)) { result_body.html("I need both MIN and MAX!"); // set style to danger(red) result_panel.addClass("panel-danger"); result_panel.removeClass("panel-info"); result_panel.removeClass("panel-success"); return; } if (min > max) { // interchange min & max var tmp = min; min = max; max = tmp; // interchange value of input fields max_fld.val(max); min_fld.val(min); } var result = Math.floor(Math.random() * (max - min + 1) + min); result_body.html("You got " + result + " !"); // set style to success(green) result_panel.addClass("panel-success"); result_panel.removeClass("panel-info"); result_panel.removeClass("panel-danger"); }); }); ``` --- # 【100sites #004】Firework - URL: https://easonchang.com/posts/100sites-004-firework - Date: 2016-02-14T16:57:00.000Z - Language: en - Translation: AI-assisted # Firework - Enjoy the Taipei 101 New Year's Fireworks [Github](https://github.com/eason-dev/100sites/tree/gh-pages/004_Firework) [Live Demo](http://kamigami55.github.io/100sites/004_Firework/) ![Screenshot 2016-02-15 12.08.44 AM.png](https://i.imgur.com/wHyk3xI.jpg) I've been looking for a framework for building front-end animations, and today I finally found one: [Processing.js](http://processingjs.org). It's the js version of [Processing](https://processing.org), and it lets you build all kinds of visual arts projects. So I used it to make today's 100sites project: Firework, a simple fireworks animation. Here's today's code: ```html index.html Fireworks 101

Enjoy the fireworks!

``` ```javascript firework.pde int time = 0; ArrayList fireworks = new ArrayList(); void setup() { size(600, 600); frameRate(30); smooth(); noStroke(); loop(); } void draw() { background(0); if(int(random(4)) == 0){ add_firework(50+int(random(500)), 50+int(random(350))); } display_firework(); draw_taipei101(); removeOldFirework(); } void mousePressed() { add_firework(mouseX, mouseY); } void display_firework() { for(int i = 0; i < fireworks.size(); ++i){ fireworks.get(i).display(); } } void removeOldFirework() { for(int i = 0; i < fireworks.size(); ++i) { if(fireworks.get(i).passingTime > 20) { fireworks.remove(i); } } } void add_firework(int x, int y) { fireworks.add(new Firework(x, y)); } void draw_taipei101() { fill(100); quad(270, 560, 330, 560, 320, 600, 280, 600); quad(270, 520, 330, 520, 320, 560, 280, 560); quad(270, 480, 330, 480, 320, 520, 280, 520); quad(270, 440, 330, 440, 320, 480, 280, 480); rect(295, 410, 10, 30); rect(299, 390, 2, 20); } class Firework { int x; int y; int directions; int passingTime; int colorR; int colorG; int colorB; Firework(int myX, int myY) { x = myX; y = myY; directions = 6+int(random(16)); colorR = 100+int(random(155)); colorG = 100+int(random(155)); colorB = 100+int(random(155)); passingTime = 0; } void display() { fill(colorR, colorG, colorB); for(int i = 0; i < directions; ++i) { ellipse(x+4*passingTime*sin(TWO_PI*i/directions), y+4*passingTime*cos(TWO_PI*i/directions), 7, 7); } ++passingTime; } } ``` --- # 【100sites #004】Firework - URL: https://easonchang.com/zh-TW/posts/100sites-004-firework - Date: 2016-02-14T16:57:00.000Z - Language: zh-TW # Firework - 享受臺北 101 的跨年煙火吧 [Github](https://github.com/eason-dev/100sites/tree/gh-pages/004_Firework) [Live Demo](http://kamigami55.github.io/100sites/004_Firework/) ![螢幕快照 2016-02-15 上午12.08.44.png](https://i.imgur.com/wHyk3xI.jpg) 我一直在尋找製作前端動畫的框架,今天我終於找到了:[Processing.js](http://processingjs.org)。它是[Processing](https://processing.org)的 js 版本,可以讓你建構各種視覺藝術(visual arts)專案。 於是我用它來完成了今天的 100sites 專案:Firework,簡單的煙火動畫。 下面是今天的 code: ```html index.html Fireworks 101

Enjoy the fireworks!

``` ```javascript firework.pde int time = 0; ArrayList fireworks = new ArrayList(); void setup() { size(600, 600); frameRate(30); smooth(); noStroke(); loop(); } void draw() { background(0); if(int(random(4)) == 0){ add_firework(50+int(random(500)), 50+int(random(350))); } display_firework(); draw_taipei101(); removeOldFirework(); } void mousePressed() { add_firework(mouseX, mouseY); } void display_firework() { for(int i = 0; i < fireworks.size(); ++i){ fireworks.get(i).display(); } } void removeOldFirework() { for(int i = 0; i < fireworks.size(); ++i) { if(fireworks.get(i).passingTime > 20) { fireworks.remove(i); } } } void add_firework(int x, int y) { fireworks.add(new Firework(x, y)); } void draw_taipei101() { fill(100); quad(270, 560, 330, 560, 320, 600, 280, 600); quad(270, 520, 330, 520, 320, 560, 280, 560); quad(270, 480, 330, 480, 320, 520, 280, 520); quad(270, 440, 330, 440, 320, 480, 280, 480); rect(295, 410, 10, 30); rect(299, 390, 2, 20); } class Firework { int x; int y; int directions; int passingTime; int colorR; int colorG; int colorB; Firework(int myX, int myY) { x = myX; y = myY; directions = 6+int(random(16)); colorR = 100+int(random(155)); colorG = 100+int(random(155)); colorB = 100+int(random(155)); passingTime = 0; } void display() { fill(colorR, colorG, colorB); for(int i = 0; i < directions; ++i) { ellipse(x+4*passingTime*sin(TWO_PI*i/directions), y+4*passingTime*cos(TWO_PI*i/directions), 7, 7); } ++passingTime; } } ``` --- # 【100sites #003】TodoList - URL: https://easonchang.com/posts/100sites-003-todolist - Date: 2016-02-13T19:12:00.000Z - Language: en - Translation: AI-assisted

Github

TodoList, an extremely simple to-do list.

![Screenshot 2016-02-14 2.17.42 AM.png](https://i.imgur.com/GVtN27w.jpg)

I wanted to start learning some backend stuff early, so I can build a wider variety of websites. That's how I found this: Node.js Learning Notes . It's a simple beginner's guide to Node.js with over thirty articles, each one short and to the point, walking you through building a simple Node.js web app step by step. It also covers Express.js and MongoDB.

After spending some time reading through the notes, this TodoList is my result for today. Right now all it can do is keep adding Todo items — there's no done button or delete button to click. I'll fill in the missing features when I have time.

It uses Node.js as the backend, Express.js as the framework, MongoDB as the database (connected via Monk), and Hogan.js as the template engine.

--- # 【100sites #003】TodoList - URL: https://easonchang.com/zh-TW/posts/100sites-003-todolist - Date: 2016-02-13T19:12:00.000Z - Language: zh-TW

Github

TodoList,一個極為簡單的待辦清單。

![螢幕快照 2016-02-14 上午2.17.42.png](https://i.imgur.com/GVtN27w.jpg)

我想儘早把後端的東西也學一學,這麼一來能做的網站就更多樣了,於是我找到了這個: Node.js學習筆記 。這是一個簡單的Node.js入門指南,包含了三十多篇文章,每一篇的份量都很精簡,手把手教你做出一個簡單的Node.js網頁應用。內容也同時涵括了Express.js和MongoDB。

花了點時間讀完筆記後,這個TodoList就是我今天的成果了,它目前只能不斷新增Todo項目,沒有完成按鈕或刪除按鈕可以點選,之後有時間會補齊它的功能吧。

使用Node.js作為後端、Express.js當框架、MongoDB當資料庫並用Monk來連結、Hogan.js當模板引擎。

--- # 【100sites #002】Pomodoro - URL: https://easonchang.com/posts/100sites-002-pomodoro - Date: 2016-02-12T16:07:00.000Z - Language: en - Description: A simple Pomodoro Technique timer. - Translation: AI-assisted [Github](https://github.com/eason-dev/100sites/tree/master/002_Pomodoro) [Live Demo](http://kamigami55.github.io/100sites/002_Pomodoro/pomodoro.html) ![Screenshot 2016-02-13 12.00.31 AM.png](https://i.imgur.com/9qcTEZY.jpg) A simple [Pomodoro Technique](https://zh.wikipedia.org/zh-tw/番茄工作法) timer. I reused the code from [#001Timer](http://easonchang.com/2016/02/12/100sites-001-timer/) with some tweaks and improvements. I Googled an interval function that automatically corrects for drift, so the time-drift problem is now solved. But when Pomodoro runs in the background, the time-freeze problem remains — the time in the title still drifts badly. --- # 【100sites #002】Pomodoro - URL: https://easonchang.com/zh-TW/posts/100sites-002-pomodoro - Date: 2016-02-12T16:07:00.000Z - Language: zh-TW - Description: 一個簡單的番茄鐘工作法計時器。 [Github](https://github.com/eason-dev/100sites/tree/master/002_Pomodoro) [Live Demo](http://kamigami55.github.io/100sites/002_Pomodoro/pomodoro.html) ![螢幕快照 2016-02-13 上午12.00.31.png](https://i.imgur.com/9qcTEZY.jpg) 一個簡單的[番茄鐘工作法](https://zh.wikipedia.org/zh-tw/番茄工作法)計時器。沿用了[#001Timer](http://easonchang.com/2016/02/12/100sites-001-timer/)的程式碼做了一些修改與優化。 我 Google 到了一個 interval 函數可以自動修正誤差,如此一來時間誤差問題就解決了。 但將 Pomodoro 放到背景執行後,時間凍結問題依舊還沒辦法解決,title 的時間依然會有極大的誤差。 --- # 【100sites #001】Timer - URL: https://easonchang.com/posts/100sites-001-timer - Date: 2016-02-12T15:56:00.000Z - Language: en - Description: A very simple countdown timer. - Translation: AI-assisted

Github

Live Demo

![Screenshot 2016-02-12 1.01.54 AM.png](https://i.imgur.com/7fMXTrA.jpg)

A very simple countdown timer.

I built it with the classic front-end trio — HTML, CSS, and JavaScript — one file each.

The core timing feature is done with window.setInterval(function, delay), which works fine in normal use.

The current problem is that the time drifts once it runs for a while. And when the user switches to another tab, with the Timer running in the background, the browser gives setInterval fewer resources, so the countdown slows way down or even stops completely. I haven't found a solution yet.

--- # 【100sites #001】Timer - URL: https://easonchang.com/zh-TW/posts/100sites-001-timer - Date: 2016-02-12T15:56:00.000Z - Language: zh-TW - Description: 一個很簡單的倒數計時器。

Github

Live Demo

![螢幕快照 2016-02-12 上午1.01.54.png](https://i.imgur.com/7fMXTrA.jpg)

一個很簡單的倒數計時器。

使用了很基本的前端老三套:HTML、CSS、JavaScript,各一個檔案來完成這個計時器。

關鍵的計時功能由window.setInterval(function, delay)來完成,正常使用沒問題。

目前的問題是時間拉長以後就會產生時間誤差,並且當使用者切換到別的頁面後,此時Timer在背景執行,瀏覽器分配給setInterval的資源就會變少,倒數速度就會變非常慢,甚至完全停止,我目前還找不到解決方法。

--- # 【100sites #000】My Journey to Becoming a Full Stack Developer - URL: https://easonchang.com/posts/100sites - Date: 2016-02-12T15:28:00.000Z - Language: en - Description: To become a full stack developer, I've decided to start a large number of side-projects to learn by doing and efficiently improve the skills I need. # 【100sites #000】My Journey to Becoming a Full Stack Developer To become a full stack developer, I've decided to start a large number of side-projects to learn by doing and efficiently improve the skills I need. This is 100sites. Starting now, I will create and upload 100 web pages, explaining as much as possible about the technologies and tools I used. ![100Sites.png](https://imgur.com/i5gPgdO.png) [【100sites #001】Timer](https://easonchang.com/2016/02/12/100sites-001-timer/) [【100sites #002】Pomodoro](https://easonchang.com/2016/02/12/100sites-002-pomodoro/) [【100sites #003】TodoList](https://easonchang.com/2016/02/13/100sites-003-todolist/) [【100sites #004】Firework](https://easonchang.com/2016/02/14/100sites-004-firework/) [【100sites #005】Drawer](https://easonchang.com/2016/02/15/100sites-005-drawer/) [【100sites #006】Snack](https://easonchang.com/2016/02/16/100sites-006-snack/) [【100sites #007】Pong, a retro game built with P5.js](https://easonchang.com/2016/03/19/100sites-007-pong/) [【100sites #008】Showcase, a single-page website design to showcase my portfolio](https://easonchang.com/2016/03/20/100sites-008-showcase/) [【100sites #009】Flame, cool little animations with P5js](https://easonchang.com/2016/03/23/100sites-009-flame/) [【100sites #010】Piano, a virtual piano implemented with P5js's sound library](https://easonchang.com/2016/03/25/100sites-010-piano/) [【100sites #011】LifeGame, a simulation of life's infinite possibilities](https://easonchang.com/2016/03/28/100sites-011-lifegame/) (...more projects continue to update!) Feel free to check out my [GitHub](https://github.com/eason-dev) to see what projects I'm currently working on, or take a look at more of my [project works](https://easonchang.com/projects/). --- # 【100sites #000】我的全端工程師之路 - URL: https://easonchang.com/zh-TW/posts/100sites - Date: 2016-02-12T15:28:00.000Z - Language: zh-TW - Description: 為了成為全端工程師,我決定開始做大量的side-project,從做中學,高效地提升我所需要的能力。 # 【100sites #000】我的全端工程師之路 為了成為全端工程師,我決定開始做大量的 side-project,從做中學,高效地提升我所需要的能力。 這就是 100sites,從現在起我會寫出 100 個網頁並將其上傳,然後盡可能地說明我使用了哪些技術和工具。 ![100Sites.png](https://imgur.com/i5gPgdO.png) [【100sites #001】Timer](https://easonchang.com/2016/02/12/100sites-001-timer/) [【100sites #002】Pomodoro](https://easonchang.com/2016/02/12/100sites-002-pomodoro/) [【100sites #003】TodoList](https://easonchang.com/2016/02/13/100sites-003-todolist/) [【100sites #004】Firework](https://easonchang.com/2016/02/14/100sites-004-firework/) [【100sites #005】Drawer](https://easonchang.com/2016/02/15/100sites-005-drawer/) [【100sites #006】Snack](https://easonchang.com/2016/02/16/100sites-006-snack/) [【100sites #007】Pong,使用 P5.js 建構的復古遊戲](https://easonchang.com/2016/03/19/100sites-007-pong/) [【100sites #008】Showcase,單頁式網站設計展示我的作品集](https://easonchang.com/2016/03/20/100sites-008-showcase/) [【100sites #009】Flame,P5js 酷炫小動畫](https://easonchang.com/2016/03/23/100sites-009-flame/) [【100sites #010】Piano,用 P5js 的 sound 函式庫實作虛擬鋼琴](https://easonchang.com/2016/03/25/100sites-010-piano/) [【100sites #011】LifeGame,生命遊戲模擬無限的可能性](https://easonchang.com/2016/03/28/100sites-011-lifegame/) (...更多專案持續更新中!...) 也歡迎逛逛我的 [GitHub](https://github.com/eason-dev) 看我最近在忙什麼專案,或是看看我的更多 [專案作品](https://easonchang.com/projects/)。 --- # [Reading Notes] Leverage Reading by Honda Naoyuki - URL: https://easonchang.com/posts/lever-reading - Date: 2015-10-27T18:07:00.000Z - Language: en - Description: Boost the "reading ROI" of business books by approaching reading as an "investment". - Translation: AI-assisted *Leverage Reading* Author: Honda Naoyuki Translator: Bing-Ting Ye Publisher: Azoth Books ![lever-reading](https://i.imgur.com/9V5G9NC.jpg) --- ## Purpose Boost the "reading ROI" of business books by approaching reading as an "investment". ## Philosophy - Based on the author's experience, the return on investment of reading is 10000%. - Merely doing the "input" is just self-satisfaction. Being able to "output" and use what you read is what really decides the game. - Leverage reading means using a book up completely, until it falls apart. - If you carry your reading notes around and reread them a few times, you'll gradually internalize the content — like a well-worn tool that becomes one with your hand. ## Method - Read with a clear "purpose" in mind, skip the unimportant parts, and "skim". - To build skills you can use right away, pick "experience-based" books. - Read books in the same category thoroughly — whatever every book mentions is the key point of that field. - Discover new books in physical bookstores, and actively buy them in online bookstores. - Underline, jot down thoughts, and dog-ear pages to squeeze the maximum value out of a book. - Make "**reading notes**" (I think this is the most valuable part of the book), then reread your notes over and over. Unless the book is amazing, don't go back and read it again. --- Image from Anobii: [http://www.anobii.com/books/槓桿閱讀術/9789866858192/01fdecafeff15f5c07](http://www.anobii.com/books/槓桿閱讀術/9789866858192/01fdecafeff15f5c07) --- # 「閱讀筆記」槓桿閱讀術——本田直之 - URL: https://easonchang.com/zh-TW/posts/lever-reading - Date: 2015-10-27T18:07:00.000Z - Language: zh-TW - Description: 提升商業書「閱讀效益」,以「投資」的角度進行閱讀。 《槓桿閱讀術》 作者:本田直之(Honda Naoyuki) 譯者:葉冰婷 出版社:漫遊者文化 ![lever-reading](https://i.imgur.com/9V5G9NC.jpg) --- ## 目的: 提升商業書「閱讀效益」,以「投資」的角度進行閱讀。 ## 哲學: - 根據作者經驗,讀書的投資報酬率為 10000%。 - 僅僅做「輸入」的動作,只不過是自我滿足而已,如果能「輸出」使用,才是真正勝負的關鍵。 - 槓桿效益閱讀術是徹底把書用盡,直到一本書支離破碎。 - 如果經常帶著閱讀筆記,並反覆閱讀幾次的話,逐漸會對該內容融會貫通。猶如用慣的工具和手掌結為一體一般。 ## 方法: - 秉持「目的」意識,捨棄不重要的部分,進行「掃讀」。 - 培養即戰力,挑選「經驗類型」的書籍。 - 徹底閱讀同類別書籍,每一本書都提到的內容就是該領域的重點。 - 在實體書店發現新書,在網路書店主動購買。 - 在書上劃線、紀錄想法、摺角,徹底發揮書籍的最大效益。 - 製作「**閱讀筆記**」(我認為是本書最有價值的部分),之後就反覆閱讀你的筆記,除非書籍超好,否則不要再去讀那本書了。 --- 圖片引用自 Anobii:[http://www.anobii.com/books/槓桿閱讀術/9789866858192/01fdecafeff15f5c07](http://www.anobii.com/books/槓桿閱讀術/9789866858192/01fdecafeff15f5c07)