diff --git a/content/about.md b/content/about.md index 719bdd0..82df7ce 100644 --- a/content/about.md +++ b/content/about.md @@ -2,7 +2,7 @@ templateKey: AboutPage title: about jumbotron: - image: /img/about-jumbotron.jpg + image: /img/about-jumbotron.webp headline: |- Programming = Problem Solving @@ -12,22 +12,22 @@ facts: description: | My favorite drink is coke. However it is not good for my health 😂️, so I drink malt beverage or diet coke now. - image: /img/drink.jpg + image: /img/drink.webp - title: Fruit description: | My favorite fruit is guava and durian. Yes, I like tropical fruits. - image: /img/guava.jpg + image: /img/guava.webp - title: Old games description: | Some old games are quite impressive to me including 'Baldur's gate' and 'Nox'. - image: /img/nox.jpg + image: /img/nox.webp - title: Substitution description: | I am quite good at substitution or empathizing. The most frequent question I was thinking about as a student is that if I was the professor, what questions would I give at next exam. - image: /img/substitution.png + image: /img/substitution.webp - title: Gaming description: | I enjoy gaming very much. My favourite game catogory is strategy or card @@ -38,16 +38,16 @@ facts: My another hobby is reading. I train myself to read multiple lines at once because I am so thirsty for the new line when I read, espically if I found a good story, like the story of Sherlock Holmes. I spent most of my teenage hanging around book mall. Of course, for the free air conditioning. - image: /img/bookmall.png + image: /img/bookmall.webp - title: Anime description: | I am also a big fan of anime. My favourite one is called 'Code Geass'. - image: /img/code-geass.jpg + image: /img/code-geass.webp - title: Highschool description: | My graduated highschool is Rockridge Secondary School in west Vancouver. - image: /img/highschool.jpg + image: /img/highschool.webp - title: University description: | I graduated from University of Toronto in 2018 with a degree in computer engineering. @@ -61,7 +61,7 @@ facts: description: | Free Loop by Daniel Powter is one of my favourite english songs. Flower Dance is the melody I frequently listen. - image: /img/freeloop.jpg + image: /img/freeloop.webp - title: Creation description: > Sometimes I enjoy drawing, just for the feeling of creation. @@ -70,17 +70,17 @@ facts: Sometimes I do my haircut myself, and I like to challenge myself. However I am not able to handle my backside well, I need a photo to assist me. If you ever saw myself wearing a hat, that is probably another failure on my way becoming a legendary barber. - image: /img/hair-cutting.jpg + image: /img/hair-cutting.webp - title: Alias description: | My alias, especially when in game, is 'ewgdg'. I simply created this word by face-roll. If it has to have some meanings, it means 'creation'. - image: /img/ewgdg.png + image: /img/ewgdg.webp - title: Color description: | I like red. It stands for life, or HP(Hit Point) in games. - image: /img/hp.jpg + image: /img/hp.webp - title: Color Scheme description: | My current favorite color scheme is gruvbox-dark. @@ -92,5 +92,5 @@ facts: - title: Number description: | Number 9 is my lucky number, because it takes 9 strokes to write my name. - image: /img/name.png + image: /img/name.webp --- diff --git a/content/aboutTemplate.md b/content/aboutTemplate.md index e949ef9..a793ce0 100644 --- a/content/aboutTemplate.md +++ b/content/aboutTemplate.md @@ -6,6 +6,6 @@ isTemplate: true facts: - title: template description: template fact - image: /img/hp.jpg + image: /img/hp.webp links: [about:blank] --- \ No newline at end of file diff --git a/content/blog.md b/content/blog.md index b02031d..02b39bd 100644 --- a/content/blog.md +++ b/content/blog.md @@ -2,7 +2,7 @@ templateKey: BlogPage title: blog jumbotron: - image: /img/blog-jumbotron.jpg + image: /img/blog-jumbotron.webp headline: |- Thoughts & Ideas diff --git a/content/blog/hearthstone-conditional-probability-question.md b/content/blog/hearthstone-conditional-probability-question.md index af5a0c0..0538d40 100644 --- a/content/blog/hearthstone-conditional-probability-question.md +++ b/content/blog/hearthstone-conditional-probability-question.md @@ -2,7 +2,7 @@ templateKey: BlogPost title: An Interesting Hearthstone Conditional Probability Question date: 2020-04-17T13:00:00.000Z -lastModified: 2025-07-27T08:54:45.000Z +lastModified: 2026-05-23T08:25:05.380Z description: Reasonable assumptions make differences. featuredPost: false tags: @@ -10,7 +10,7 @@ tags: - HearthStone - Probability --- -![](/img/An-Interesting-Hearthstone-Conditional-Probability-Question.png) +![](/img/An-Interesting-Hearthstone-Conditional-Probability-Question.webp) There was a very interesting conditional probability problem last month as shown in the picture and it caused a debate over it. Some claimed diff --git a/content/blog/hearthstone-legend-rank-achievement.md b/content/blog/hearthstone-legend-rank-achievement.md index 1b76365..5cf4dce 100644 --- a/content/blog/hearthstone-legend-rank-achievement.md +++ b/content/blog/hearthstone-legend-rank-achievement.md @@ -2,7 +2,7 @@ templateKey: BlogPost title: 'My HearthStone Gameplay Milestone: Legend Rank Achieved' date: 2019-12-26T03:48:19.785Z -lastModified: 2025-07-27T08:54:45.000Z +lastModified: 2026-05-23T08:25:05.381Z description: Data and gaming. featuredPost: false tags: @@ -11,7 +11,7 @@ tags: --- Gaming is always the best reward for me. After my completion of my personal page site project, I decided to take a short break and spend my time into playing HearthStone. Within 2 weeks in the new expansion of HS, I achieved my first legend rank for the game. I was very satisfied about it at the moment so I wrote this post to share my joy. -![](/img/my-hearthstone-gameplay-milestone-legend-rank-achieved-1.jpg) +![](/img/my-hearthstone-gameplay-milestone-legend-rank-achieved-1.webp) The key to this achievement is on data analysis. To be specific, there is a website collecting match-ups data from HS players named hsreplay.net. By analysing these data, I soon targeting a deck called face hunter to level up my rank tier from 25 to 1. Now rank tier 1 is one level away from the highest rank tier --- legend tier. However, the environment changed very quickly. Face hunter is less competitive in rank tier 1. As the hsreplay.net showed, the win ratio of face hunter in rank tier 1 is barely above 50%, 50.38% to be specific. Players need 5 net wins to achieve legend rank from rank tier 1. With 50.38% win ratio, I need to play n game plays in total in expectation, where 50.38%\*n-(100%-50.38%)\*n = 5 and n ~= 658. It was obviously not a smart choice to continue to play face hunter. Fortunately, there was a new patch arriving to nerf Shaman so that I knew that the environment would be changing soon. If I can find out a deck with high win ratio that is rare enough that nobody is trying to counter against it in the new environment, then I can quickly arrive to the legend rank. diff --git a/content/blog/how-to-pronounce-my-name.md b/content/blog/how-to-pronounce-my-name.md index e16d14c..fcd2aba 100644 --- a/content/blog/how-to-pronounce-my-name.md +++ b/content/blog/how-to-pronounce-my-name.md @@ -2,7 +2,7 @@ templateKey: BlogPost title: How to Pronounce My Name date: 2019-10-23T02:12:00.771Z -lastModified: 2026-05-08T07:05:41.350Z +lastModified: 2026-05-23T08:25:05.381Z description: '' featuredPost: true tags: @@ -23,7 +23,7 @@ You might think this is the end of the story, but it is not. Since learning the The first option is to read it the English way, i.e. she-yen/see-yen/shen, whatever you like. Let us represent the action of calling a person’s name as a part of a two-way communication model. -![](/img/how-to-pronounce-my-name-image0.png) +![](/img/how-to-pronounce-my-name-image0.webp) In this model, a sender broadcasts a message, the name, to the receiver, and the receiver then deciphers the message and responds to it by paying attention to the sender. The key is for both the sender and the receiver to have an agreement on the message. Pronouncing the exact sound is hard but recognizing the similar sound is relatively easy. As a result, all of those English version pronunciation like she-yen/see-yen/shen can be captured and recognized correctly by me as a receiver. The problem is solved. @@ -47,7 +47,7 @@ Johnny is not my real name, but it is a name I would use when ordering a coffee Johnny, like many other Chinese international students, was too hurried on having his first English name to really be able to figure the meaning out first. Or it could be the reason that his home stay family was so arrogant to give him this sarcastic English name. Anyway, the source of this name remains mysterious. On the other hand, Johnny is very satisfied with his English name. He likes it so much that he rarely tells his friends his original Chinese name. Obviously his intention failed and people came up with a nickname for him, ‘jie ni gui’(a Chinese name for Squirtle from Pokemon) for how ‘jie ni’ is similar to ‘Johnny’ in terms of pronunciation and how Johnny behaved like a turtle. -
+
Although many Chinese parents hold unrealistic expectations on foreign education to turn their naughty kids into elites magically, it turns out that it does not work, at least the public school is not that magical. Being an international student is very stressful and isolated. Gaming was the most relaxing moment for me. I met Johnny through gaming. Johnny is very impressive to me on his gaming skills. He seemed to be born for gaming, and he was very good at predicting opponent players’ move. There is a special term called game sense/awareness to describe this talent. I played games with Johnny and many other friends among which Bill and Tony are another ones. Johnny, Bill, and Tony were the three giants on ‘Dota’ among the group, but Johnny was more capable of that. Johnny was a master on ‘Dota’, ‘League of Legends’, ‘World of Warcraft’, ‘Counter Strike: Global Offensive’, ‘Starcraft’, you name it. diff --git a/content/blog/my-first-blog-post.md b/content/blog/my-first-blog-post.md index a97c99c..6a258e2 100644 --- a/content/blog/my-first-blog-post.md +++ b/content/blog/my-first-blog-post.md @@ -2,10 +2,10 @@ templateKey: BlogPost title: My First Blog Post date: 2019-10-20T18:33:45.012Z -lastModified: 2025-07-27T09:25:05.000Z +lastModified: 2026-05-23T08:25:05.381Z description: For testing purpose. featuredPost: false -featuredImage: /img/hp.jpg +featuredImage: /img/hp.webp tags: - testing - boring diff --git a/content/blog/my-first-vue-web-app-part-1.md b/content/blog/my-first-vue-web-app-part-1.md index 45aa9c2..b3eaf9e 100644 --- a/content/blog/my-first-vue-web-app-part-1.md +++ b/content/blog/my-first-vue-web-app-part-1.md @@ -2,11 +2,11 @@ templateKey: BlogPost title: 'My First Vue Web App: Difficulties and Things I Wish I Knew, Part 1' date: 2019-11-06T21:59:10.643Z -lastModified: 2026-05-08T07:05:41.351Z +lastModified: 2026-05-23T08:25:05.381Z description: My decision branch tree on dilemmas when developing my first Vue app. featuredPost: false featuredImage: >- - /img/my-first-vue-web-app-difficulties-and-things-i-wish-i-knew-part-1-image1.png + /img/my-first-vue-web-app-difficulties-and-things-i-wish-i-knew-part-1-image1.webp tags: - Vue - JavaScript @@ -151,7 +151,7 @@ documentations. large communities. Vue is becoming trendy but the community is smaller. -![](/img/my-first-vue-web-app-difficulties-and-things-i-wish-i-knew-part-1-image1.png) +![](/img/my-first-vue-web-app-difficulties-and-things-i-wish-i-knew-part-1-image1.webp) To conclude, Vue was slightly ahead on the evaluation and I eventually decided to use Nuxt.js which is framework of Vue.js for diff --git a/content/blog/my-first-vue-web-app-part-2.md b/content/blog/my-first-vue-web-app-part-2.md index 3824aeb..2a564d3 100644 --- a/content/blog/my-first-vue-web-app-part-2.md +++ b/content/blog/my-first-vue-web-app-part-2.md @@ -2,7 +2,7 @@ templateKey: BlogPost title: 'My First Vue Web App: Difficulties and Things I Wish I Knew, Part 2' date: 2019-11-08T02:06:51.406Z -lastModified: 2025-07-31T05:33:41.000Z +lastModified: 2026-05-23T08:25:05.381Z description: Mistakes I made. featuredPost: false tags: @@ -17,13 +17,13 @@ In this section, I want to talk about some mistakes I made for my first Vue app. Linter is actually the best tool I have ever used for JavaScript developers. A linter is a static code analysis tool that can help me to avoid style and syntax errors. It was especially useful for me when I was not familiar with JS syntax during my first Vue project. As a result, I strictly followed the linting tool I was using and became over reliant on it. The direct result was that my development speed was restricted. The linter is not as smart as a real person, and some rules are just of personal preference of the author. Some styles that can be useful are considered as unsafe such as the reuse of variable name in different scopes. But the reuse of name it is not necessarily dangerous because the constraint of scopes/namespaces. Strictly following the rules of linter, I wasted quite a lot of time on things that are less important, like thinking of unique name for the same kind of variables in different scopes. By reading the official documentation of Eslint, I realized that this was not the right way to use linter. The correct way is to configure and customize the linting rules for your project and preference. And indeed, there is actually a configuration file for the linter and you can even disable rules using inline comments. Linter is a tool to facilitate development, not to restrict. -![](/img/my-first-vue-web-app-difficulties-and-things-i-wish-i-knew-part-2-image2.png) +![](/img/my-first-vue-web-app-difficulties-and-things-i-wish-i-knew-part-2-image2.webp) **Mistake 2: Abuse of Vuex** My first vue app requires some heavy computations, and the whole app contains many states, some are internal states that are for calculations and some are external states used for UI rendering. To manage such a large number states and data flows, I used the centralized data management store called Vuex. All of the internal and external states were stored in Vuex store. Same for the computations. Vuex are very useful on debugging and tracking down state mutations. However, tracking large amounts of data made the debugging tool laggy and the frequent freezes gave me instincts that something went wrong. When I read the documentation of Vue, I eventually caught the mistake. It is all about the reactivity system of Vue. Vue implements an observer pattern that appends a watcher for every component instance including the store of Vuex. -![](/img/my-first-vue-web-app-difficulties-and-things-i-wish-i-knew-part-2-image1.png) +![](/img/my-first-vue-web-app-difficulties-and-things-i-wish-i-knew-part-2-image1.webp) All of the states/properties in the Vuex store will be modified by Vue to be able to notify the watcher because all of the states in the store will be ‘touched’ during the renderings and computations. If a large amount of states send the change-notifications, the app will be very slow. It is important to minify the number of notifications. That requires the separation of internal states and external states. To reach the goal, I extracted the computation logic into an OOP class, and read only external states that are necessary for rendering from the object instance instantiated by the class into Vuex store. It turned out that only a small fraction of states were related UI display so they were read into Vuex store. The problem is solved. This mistake reminds me that Vue is a rendering framework and heavy data processing should be separated from it. diff --git a/content/blog/my-first-vue-web-app-part-3.md b/content/blog/my-first-vue-web-app-part-3.md index a77ee9e..a9797e8 100644 --- a/content/blog/my-first-vue-web-app-part-3.md +++ b/content/blog/my-first-vue-web-app-part-3.md @@ -2,7 +2,7 @@ templateKey: BlogPost title: 'My First Vue Web App: Difficulties and Things I Wish I Knew, Part 3' date: 2019-11-08T18:48:19.565Z -lastModified: 2025-07-27T08:54:45.000Z +lastModified: 2026-05-23T08:25:05.381Z description: Tackling difficulties. featuredPost: false tags: @@ -58,7 +58,7 @@ I have to admit it is quite hacky here. But the intuition behind it is that the However, this is not the happy ending of the story. You might have guessed the problem as an experienced developer. The profiling tool from google chrome browser illustrates the problem. The below graph is a timeline recording result of the whole loading process. 7430 ms out of 8563 ms is used for idling. -![](/img/my-first-vue-web-app-difficulties-and-things-i-wish-i-knew-part-3-image1.png) +![](/img/my-first-vue-web-app-difficulties-and-things-i-wish-i-knew-part-3-image1.webp) Initially, I thought I misunderstood the word ‘idle’ as a non-native English speaker. After I checked the dictionary and confirmed the meaning of ‘idle’, I realized that something went wrong. Remember that I broke my loading process into \~200 steps and if each step takes \~30 ms break, then the idling time is roughly 6000 ms. That means ~90% of the loading time is wasted. I could compromise for better performance or for better animation, but there would be a trade-off anyway. To really overcome the issue, I have to use true parallelism, the Web Worker. diff --git a/content/blog/nodejs-certification-challenge.md b/content/blog/nodejs-certification-challenge.md index 073cca9..e8fdba4 100644 --- a/content/blog/nodejs-certification-challenge.md +++ b/content/blog/nodejs-certification-challenge.md @@ -2,10 +2,10 @@ templateKey: BlogPost title: A Node.js certification challenge date: 2020-11-11T22:59:39.935Z -lastModified: 2025-07-27T08:54:45.000Z +lastModified: 2026-05-23T08:25:05.385Z description: To challenge myself. featuredPost: false -featuredImage: /img/nodejs-challenge.png +featuredImage: /img/nodejs-challenge.webp tags: - challenge - node.js @@ -72,7 +72,7 @@ What is next? I plan to practice with my node.js knowledge to deliver a simple m The exam is a little bit different from my imagination but I passed the exam anyway. My certificate ID number is LF-mt7qrutu8t. My original expected time to complete this challenge is 21 days and I spent 18 days on this challenge. Achievement reached: node.js learner; look at my badge earned below, what a wonderful reward for this gameplay! -![](/img/nodejs-challenge.png) +![](/img/nodejs-challenge.webp) At this point, I decide to continue my node.js study together with mongo DB and express.js on coursera.com. @@ -82,4 +82,4 @@ At this point, I decide to continue my node.js study together with mongo DB and Completed my Coursera express.js backend course. -![](/img/express-certificate.png) \ No newline at end of file +![](/img/express-certificate.webp) \ No newline at end of file diff --git a/content/blog/note-taking-app-2.md b/content/blog/note-taking-app-2.md index 6841a7c..b656829 100644 --- a/content/blog/note-taking-app-2.md +++ b/content/blog/note-taking-app-2.md @@ -2,10 +2,10 @@ templateKey: BlogPost title: Note Taking App 2 date: 2021-11-13T20:04:38.538Z -lastModified: 2025-07-27T08:54:45.000Z +lastModified: 2026-05-23T08:25:05.385Z description: It matters. featuredPost: false -featuredImage: /img/note-taking-app-2-20211113135526.png +featuredImage: /img/note-taking-app-2-20211113135526.webp tags: [] --- ## Background @@ -18,7 +18,7 @@ The most touching point of this app is that your note is future proof. Even if o Besides, Obsidian is more open to the world, it has a large community that can help and contribute to various community plugins. I even created my own plugin to support my style of zettelkasten system. -![](/img/note-taking-app-2-20211113135526.png) +![](/img/note-taking-app-2-20211113135526.webp) ## Notion @@ -30,7 +30,7 @@ Another disadvantage is that Notion rarely listen to the community or they are n As a result, I mainly use Notion for task management, I have a task database where I can insert all todos into it and track their states. -![](/img/note-taking-app-2-20211113145514.png) +![](/img/note-taking-app-2-20211113145514.webp) ## Why it matters @@ -38,6 +38,6 @@ In short, for the dopamine rewards. A right note-taking app can help to reward you when doing note taking with dopamine factor. This reinforce learning model can then help me to solidify this note-taking behavior into a habit. -![](/img/dopamine-factor.png) +![](/img/dopamine-factor.webp) Another example is that with Notion's database, I am more motivated to take actions when I visualize how many todos and ideas I have. \ No newline at end of file diff --git a/content/blog/question-answering-chatbot.md b/content/blog/question-answering-chatbot.md index 210fdf6..c215f79 100644 --- a/content/blog/question-answering-chatbot.md +++ b/content/blog/question-answering-chatbot.md @@ -2,10 +2,10 @@ templateKey: BlogPost title: Question Answering Chatbot date: 2020-11-24T20:41:08.570Z -lastModified: 2025-07-27T08:54:45.000Z +lastModified: 2026-05-23T08:25:05.385Z description: Screening interview? featuredPost: false -featuredImage: /img/question-answering-chatbot-0.png +featuredImage: /img/question-answering-chatbot-0.webp --- Recently I got a phone interview about my experience and background kind stuff. What is interesting is that this interview is a non-technical one but the position I applied to is a technical position. This makes me wonder, is it the necessity for non-tech interviews? If so, how would I pass it? Today I am going to solve the problem. @@ -62,7 +62,7 @@ It is built on top of the Hugging Face transformer and most importantly it comes The result is as demonstrated: -![](/img/question-answering-chatbot-0.png) +![](/img/question-answering-chatbot-0.webp) You might notice that it does not perform well on some cryptic questions; this is because the QA bot is based on my currently existing blog posts, and if there is no relevant context in my blog to the question, then the bot cannot answer it. To solve this issue, I created a new Q&A section below such that the bot can read the context and extract useful answers from it. @@ -70,7 +70,7 @@ Unfortunately, the result with extractive QA method is still poor, I might need After adding another assisting sentence embedding document retriever for FAQ section, the result is more reliable now. -![](/img/question-answering-chatbot-1.png) +![](/img/question-answering-chatbot-1.webp) ## Q&A section: diff --git a/content/blog/react-trick-initializer-block-function-component.md b/content/blog/react-trick-initializer-block-function-component.md index 10efb12..e483286 100644 --- a/content/blog/react-trick-initializer-block-function-component.md +++ b/content/blog/react-trick-initializer-block-function-component.md @@ -2,10 +2,10 @@ templateKey: BlogPost title: 'React Trick: How to Create an Initializer Block for a Function Component' date: 2019-11-04T23:01:58.242Z -lastModified: 2026-02-24T00:31:27.882Z +lastModified: 2026-05-23T08:25:05.385Z featuredPost: false featuredImage: >- - /img/react-trick-how-to-create-an-initializer-block-for-a-function-component-image1.png + /img/react-trick-how-to-create-an-initializer-block-for-a-function-component-image1.webp tags: - react - function component @@ -28,7 +28,7 @@ Before diving into the details, I want to mention that there are actually two wa There are actually many ways to initialize the states in function components, the challenging part here is what is the most elegant way to do so. Let us start from the very basic method from the React official documentation. -![](/img/react-trick-how-to-create-an-initializer-block-for-a-function-component-image1.png) +![](/img/react-trick-how-to-create-an-initializer-block-for-a-function-component-image1.webp) The official way is to directly pass initial value to `useRef`. Simple enough. However, it is not very useful if your initial state is generated by a very complex and costly computation, for example a randomly generated map data for your game. The naive way of doing this is as follows: diff --git a/content/blog/review-anime-astra-lost-in-space-ep1.md b/content/blog/review-anime-astra-lost-in-space-ep1.md index eccd45c..274d0e2 100644 --- a/content/blog/review-anime-astra-lost-in-space-ep1.md +++ b/content/blog/review-anime-astra-lost-in-space-ep1.md @@ -2,10 +2,10 @@ templateKey: BlogPost title: 'Review on Anime, "Astra Lost in Space", Episode 1' date: 2019-10-24T21:18:44.563Z -lastModified: 2025-07-27T09:25:18.000Z +lastModified: 2026-05-23T08:25:05.385Z description: 'Lesson learnt: an obvious solution might not be an optimal solution.' featuredPost: false -featuredImage: /img/review-on-astra-lost-in-space-image4.png +featuredImage: /img/review-on-astra-lost-in-space-image4.webp --- *Astra Lost in Space (彼方のアストラ Kanata no Asutora)* is an anime TV series released September 2019. I am very impressed by this @@ -55,13 +55,13 @@ but luckily they all had their space suit equipped and thus they could move in the space with their propulsion system. -![](/img/review-on-astra-lost-in-space-image2.png) +![](/img/review-on-astra-lost-in-space-image2.webp) More luckily, they found a spaceship! They regrouped themselves at the spaceship and it was at that moment they realized that Aries was still alone waiting for help with her thruster malfunctioning. -![](/img/review-on-astra-lost-in-space-image3.png) +![](/img/review-on-astra-lost-in-space-image3.webp) Kanata, the captain, decided to get out of the spaceship to save the @@ -70,7 +70,7 @@ thing to notice is that he got a safety cable. Everything seemed to be fine except the sudden when the cable was not long enough. -![](/img/review-on-astra-lost-in-space-image1.png) +![](/img/review-on-astra-lost-in-space-image1.webp) Kanata unhooked his cable and he went forward without it. The problem came when Kanata and Aries on their way back. They ran out of fuel and @@ -81,7 +81,7 @@ this reminds me of "The Human Centipede"). -![](/img/review-on-astra-lost-in-space-image4.png) +![](/img/review-on-astra-lost-in-space-image4.webp) I mean, this is shocking not because it is heroic, but because it doesn't make sense. Why would people not use the cable they found? You @@ -136,7 +136,7 @@ Each of the joints will give you some extent of freedom for moving you arm. The more joints you have the more control you gain. That is why there are many joints on the robotic arm. -![](/img/robotic-arm.png) +![](/img/robotic-arm.webp) The human chain is an analogy to the arm structure. Each person on the chain can exert force to the next person he connects to through their diff --git a/content/blog/template.md b/content/blog/template.md index f613533..f9eb1df 100644 --- a/content/blog/template.md +++ b/content/blog/template.md @@ -3,7 +3,7 @@ templateKey: BlogPost title: Template date: 2000-00-00T00:00:00.000Z featuredPost: false -featuredImage: /img/hp.jpg +featuredImage: /img/hp.webp description: template for gatsby to build schema. isPortfolio: false isTemplate: true diff --git a/content/blog/vue-trick-v-for-optimization.md b/content/blog/vue-trick-v-for-optimization.md index 084b0d1..2d921fd 100644 --- a/content/blog/vue-trick-v-for-optimization.md +++ b/content/blog/vue-trick-v-for-optimization.md @@ -2,12 +2,12 @@ templateKey: BlogPost title: 'Vue Trick: Optimization for the v-for Directive' date: 2019-11-04T21:21:47.632Z -lastModified: 2025-07-27T08:54:45.000Z +lastModified: 2026-05-23T08:25:05.385Z description: |- Lesson learnt: an optimized solution might not be the best solution. featuredPost: false -featuredImage: /img/vue-trick-optimization-for-v-for-image1.png +featuredImage: /img/vue-trick-optimization-for-v-for-image1.webp tags: - vue - optimization diff --git a/content/blog/why-i-created-this-page.md b/content/blog/why-i-created-this-page.md index f6a004e..5bc398b 100644 --- a/content/blog/why-i-created-this-page.md +++ b/content/blog/why-i-created-this-page.md @@ -2,10 +2,10 @@ templateKey: BlogPost title: Why I Created This Page date: 2019-10-30T19:29:43.803Z -lastModified: 2026-03-06T09:09:35.623Z +lastModified: 2026-05-23T08:25:05.385Z description: 'In short, to get a job.' featuredPost: true -featuredImage: /img/steve-elizabeth-x.jpg +featuredImage: /img/steve-elizabeth-x.webp tags: - jobs - why @@ -206,9 +206,9 @@ is the proof. So this is why I created this page, to have an impact.
-
-
-
+
+
+
diff --git a/content/blogTemplate.md b/content/blogTemplate.md index 3407f8a..a385189 100644 --- a/content/blogTemplate.md +++ b/content/blogTemplate.md @@ -3,7 +3,7 @@ templateKey: BlogPage title: Template isTemplate: true jumbotron: - image: /img/hp.jpg + image: /img/hp.webp headline: template subtitle: template --- \ No newline at end of file diff --git a/content/index.md b/content/index.md index 0d0911d..b418510 100644 --- a/content/index.md +++ b/content/index.md @@ -2,7 +2,7 @@ templateKey: IndexPage title: home jumbotron: - image: /img/home-jumbotron.jpg + image: /img/home-jumbotron.webp headline: "\nXian" subtitle: 显 --- diff --git a/content/indexTemplate.md b/content/indexTemplate.md index b2a25ab..6486545 100644 --- a/content/indexTemplate.md +++ b/content/indexTemplate.md @@ -3,7 +3,7 @@ templateKey: IndexPage title: Template isTemplate: true jumbotron: - image: /img/hp.jpg + image: /img/hp.webp headline: template subtitle: template --- \ No newline at end of file diff --git a/content/portfolio/gacha-simulator.md b/content/portfolio/gacha-simulator.md index 45b07d7..975774b 100644 --- a/content/portfolio/gacha-simulator.md +++ b/content/portfolio/gacha-simulator.md @@ -6,5 +6,5 @@ description: >- Use strategy to collect more cards. externalLink: https://gacha.xianzzz.com/ isPortfolio: true -featuredImage: /img/gacha-splash.png +featuredImage: /img/gacha-splash.webp --- \ No newline at end of file diff --git a/content/portfolio/maze-generator.md b/content/portfolio/maze-generator.md index 2563d4d..f1676fb 100644 --- a/content/portfolio/maze-generator.md +++ b/content/portfolio/maze-generator.md @@ -5,7 +5,7 @@ title: Maze Generator date: 2019-10-14T14:54:00.000Z description: Survive inside a randomly generated maze. featuredPost: true -featuredImage: /img/maze-splash.png +featuredImage: /img/maze-splash.webp --- The inspiration for this game came from 3+ years ago with the algorithm Depth First Search and was developed back then. Being lost in a @@ -28,18 +28,18 @@ only one can survive by reaching the exit first. Sam is one of the few who got summoned. He is smart and goes for the tutorial before the game starts. -![](/img/maze-generator-image15.png) +![](/img/maze-generator-image15.webp) He gets his first instruction. -![](/img/maze-generator-image1.png) +![](/img/maze-generator-image1.webp) He found his exp bar and he is gaining exp whenever he is moving. -![](/img/maze-generator-image7.png) +![](/img/maze-generator-image7.webp) @@ -48,63 +48,63 @@ is moving. There is a hidden explosive trap that will be revealed when close enough. Be careful. -![](/img/maze-generator-image16.png) +![](/img/maze-generator-image16.webp) -![](/img/maze-generator-image9.png) +![](/img/maze-generator-image9.webp) He can heal himself by standing beside the red cross. -![](/img/maze-generator-image6.png) +![](/img/maze-generator-image6.webp) He is level one now and he can jump across the explosive trap to avoid the damage. -![](/img/maze-generator-image8.png) +![](/img/maze-generator-image8.webp) He keeps gaining his exp and he soon becomes level 2 and learns to sprint. -![](/img/maze-generator-image2.png) +![](/img/maze-generator-image2.webp) He found a car and is able to drive it for faster straight-line movement. -![](/img/maze-generator-image3.png) +![](/img/maze-generator-image3.webp) He quickly learned how to paint on the wall but he has a limited amount of ink so he must use it wisely. -![](/img/maze-generator-image4.png) +![](/img/maze-generator-image4.webp) Say hi. -![](/img/maze-generator-image10.png) +![](/img/maze-generator-image10.webp) He is level 4 now and he is even more powerful to be able to fly, consuming energy which will restore -slowly.![](/img/maze-generator-image11.png) +slowly.![](/img/maze-generator-image11.webp) He found a weapon. It is very chilling to think about what people can do with it. -![](/img/maze-generator-image12.png) +![](/img/maze-generator-image12.webp) It is time to start the game. -![](/img/maze-generator-image13.png) +![](/img/maze-generator-image13.webp) Oh my god. He meets his opponent player. -![](/img/maze-generator-image5.png) +![](/img/maze-generator-image5.webp) “Where is my gun?”, he wonders. He looks around and realizes that weapon is banned because this is just a demo game. @@ -116,7 +116,7 @@ like those in movies, not because of the ban. He found it, the exit. He won, although his opponent is so close to the final win. -![](/img/maze-generator-image14.png) +![](/img/maze-generator-image14.webp) diff --git a/package-lock.json b/package-lock.json index 0a77637..0487272 100644 --- a/package-lock.json +++ b/package-lock.json @@ -59,6 +59,7 @@ "jest-environment-jsdom": "^30.0.5", "raw-loader": "^4.0.2", "serve": "^14.2.6", + "sharp": "^0.34.5", "typescript": "^5.8.3" } }, @@ -1784,8 +1785,8 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.0.0.tgz", "integrity": "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==", + "devOptional": true, "license": "MIT", - "optional": true, "engines": { "node": ">=18" } @@ -5808,9 +5809,9 @@ } }, "node_modules/body-parser": { - "version": "1.20.4", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", - "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", + "version": "1.20.5", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.5.tgz", + "integrity": "sha512-3grm+/2tUOvu2cjJkvsIxrv/wVpfXQW4PsQHYm7yk4vfpu7Ekl6nEsYBoJUL6qDwZUx8wUhQ8tR2qz+ad9c9OA==", "dev": true, "license": "MIT", "dependencies": { @@ -5822,7 +5823,7 @@ "http-errors": "~2.0.1", "iconv-lite": "~0.4.24", "on-finished": "~2.4.1", - "qs": "~6.14.0", + "qs": "~6.15.1", "raw-body": "~2.5.3", "type-is": "~1.6.18", "unpipe": "~1.0.0" @@ -8647,8 +8648,8 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "devOptional": true, "license": "Apache-2.0", - "optional": true, "engines": { "node": ">=8" } @@ -9772,15 +9773,15 @@ } }, "node_modules/express": { - "version": "4.22.1", - "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", - "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", + "version": "4.22.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.2.tgz", + "integrity": "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==", "dev": true, "license": "MIT", "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "~1.20.3", + "body-parser": "~1.20.5", "content-disposition": "~0.5.4", "content-type": "~1.0.4", "cookie": "~0.7.1", @@ -9799,7 +9800,7 @@ "parseurl": "~1.3.3", "path-to-regexp": "~0.1.12", "proxy-addr": "~2.0.7", - "qs": "~6.14.0", + "qs": "~6.15.1", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", "send": "~0.19.0", @@ -16546,9 +16547,9 @@ "license": "MIT" }, "node_modules/qs": { - "version": "6.14.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", - "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==", + "version": "6.15.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz", + "integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==", "license": "BSD-3-Clause", "dependencies": { "side-channel": "^1.1.0" @@ -18952,9 +18953,9 @@ "version": "0.34.5", "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", + "devOptional": true, "hasInstallScript": true, "license": "Apache-2.0", - "optional": true, "dependencies": { "@img/colour": "^1.0.0", "detect-libc": "^2.1.2", diff --git a/package.json b/package.json index 3ee8af4..f9055c6 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,7 @@ "private": true, "scripts": { "dev": "next dev -p 8080 -H 0.0.0.0", + "prebuild": "npm run images:check", "build": "next build", "clean:admin": "rm -rf out/admin", "start": "next start", @@ -21,6 +22,10 @@ "predeploy": "echo 'Use GitHub Actions workflow for deployment instead.' && exit 1 && npm run build", "deploy": "gh-pages --no-history --dist out --nojekyll --cname xianzzz.com", "update:last-modified": "node scripts/update-last-modified.mjs --all --mode git --path-prefix content/blog --template-key BlogPost", + "images:generate": "node scripts/generate-responsive-images.mjs", + "images:check": "node scripts/check-responsive-images.mjs", + "images:convert-webp": "node scripts/convert-images-to-webp.mjs", + "images:optimize": "npm run images:convert-webp && npm run images:generate", "install:git-hooks": "node scripts/install-git-hooks.mjs" }, "dependencies": { @@ -74,6 +79,7 @@ "jest-environment-jsdom": "^30.0.5", "raw-loader": "^4.0.2", "serve": "^14.2.6", + "sharp": "^0.34.5", "typescript": "^5.8.3" }, "overrides": { diff --git a/public/img/An-Interesting-Hearthstone-Conditional-Probability-Question.png b/public/img/An-Interesting-Hearthstone-Conditional-Probability-Question.png deleted file mode 100644 index 0e6333a..0000000 Binary files a/public/img/An-Interesting-Hearthstone-Conditional-Probability-Question.png and /dev/null differ diff --git a/public/img/An-Interesting-Hearthstone-Conditional-Probability-Question.webp b/public/img/An-Interesting-Hearthstone-Conditional-Probability-Question.webp new file mode 100644 index 0000000..408c2f2 Binary files /dev/null and b/public/img/An-Interesting-Hearthstone-Conditional-Probability-Question.webp differ diff --git a/public/img/about-jumbotron.jpg b/public/img/about-jumbotron.jpg deleted file mode 100644 index 17343e6..0000000 Binary files a/public/img/about-jumbotron.jpg and /dev/null differ diff --git a/public/img/about-jumbotron.webp b/public/img/about-jumbotron.webp new file mode 100644 index 0000000..ebf3185 Binary files /dev/null and b/public/img/about-jumbotron.webp differ diff --git a/public/img/blog-jumbotron.jpg b/public/img/blog-jumbotron.jpg deleted file mode 100644 index 45f863b..0000000 Binary files a/public/img/blog-jumbotron.jpg and /dev/null differ diff --git a/public/img/blog-jumbotron.webp b/public/img/blog-jumbotron.webp new file mode 100644 index 0000000..bec0777 Binary files /dev/null and b/public/img/blog-jumbotron.webp differ diff --git a/public/img/bookmall.png b/public/img/bookmall.png deleted file mode 100644 index 5ba26ab..0000000 Binary files a/public/img/bookmall.png and /dev/null differ diff --git a/public/img/bookmall.webp b/public/img/bookmall.webp new file mode 100644 index 0000000..1c62eb9 Binary files /dev/null and b/public/img/bookmall.webp differ diff --git a/public/img/bulin.png b/public/img/bulin.png deleted file mode 100644 index fe542d3..0000000 Binary files a/public/img/bulin.png and /dev/null differ diff --git a/public/img/bulin.webp b/public/img/bulin.webp new file mode 100644 index 0000000..430785b Binary files /dev/null and b/public/img/bulin.webp differ diff --git a/public/img/casual-drawing.png b/public/img/casual-drawing.png deleted file mode 100644 index b40a9ab..0000000 Binary files a/public/img/casual-drawing.png and /dev/null differ diff --git a/public/img/casual-drawing.webp b/public/img/casual-drawing.webp new file mode 100644 index 0000000..929c93d Binary files /dev/null and b/public/img/casual-drawing.webp differ diff --git a/public/img/chemex.jpg b/public/img/chemex.jpg deleted file mode 100644 index 903f68a..0000000 Binary files a/public/img/chemex.jpg and /dev/null differ diff --git a/public/img/chemex.webp b/public/img/chemex.webp new file mode 100644 index 0000000..25731fc Binary files /dev/null and b/public/img/chemex.webp differ diff --git a/public/img/code-geass.jpg b/public/img/code-geass.jpg deleted file mode 100644 index 1254644..0000000 Binary files a/public/img/code-geass.jpg and /dev/null differ diff --git a/public/img/code-geass.webp b/public/img/code-geass.webp new file mode 100644 index 0000000..b8ce7bb Binary files /dev/null and b/public/img/code-geass.webp differ diff --git a/public/img/dopamine-factor.png b/public/img/dopamine-factor.png deleted file mode 100644 index 5798f52..0000000 Binary files a/public/img/dopamine-factor.png and /dev/null differ diff --git a/public/img/dopamine-factor.webp b/public/img/dopamine-factor.webp new file mode 100644 index 0000000..593a25a Binary files /dev/null and b/public/img/dopamine-factor.webp differ diff --git a/public/img/drink.jpg b/public/img/drink.jpg deleted file mode 100644 index 9464592..0000000 Binary files a/public/img/drink.jpg and /dev/null differ diff --git a/public/img/drink.webp b/public/img/drink.webp new file mode 100644 index 0000000..e365fcf Binary files /dev/null and b/public/img/drink.webp differ diff --git a/public/img/ewgdg.png b/public/img/ewgdg.png deleted file mode 100644 index 65bd802..0000000 Binary files a/public/img/ewgdg.png and /dev/null differ diff --git a/public/img/ewgdg.webp b/public/img/ewgdg.webp new file mode 100644 index 0000000..3759f6f Binary files /dev/null and b/public/img/ewgdg.webp differ diff --git a/public/img/express-certificate.png b/public/img/express-certificate.png deleted file mode 100644 index f4264e8..0000000 Binary files a/public/img/express-certificate.png and /dev/null differ diff --git a/public/img/express-certificate.webp b/public/img/express-certificate.webp new file mode 100644 index 0000000..24d9767 Binary files /dev/null and b/public/img/express-certificate.webp differ diff --git a/public/img/freeloop.jpg b/public/img/freeloop.jpg deleted file mode 100644 index db37ac9..0000000 Binary files a/public/img/freeloop.jpg and /dev/null differ diff --git a/public/img/freeloop.webp b/public/img/freeloop.webp new file mode 100644 index 0000000..e19d57a Binary files /dev/null and b/public/img/freeloop.webp differ diff --git a/public/img/gacha-splash.png b/public/img/gacha-splash.png deleted file mode 100644 index 93cc8f4..0000000 Binary files a/public/img/gacha-splash.png and /dev/null differ diff --git a/public/img/gacha-splash.webp b/public/img/gacha-splash.webp new file mode 100644 index 0000000..560a745 Binary files /dev/null and b/public/img/gacha-splash.webp differ diff --git a/public/img/generated/about-jumbotron-1280.avif b/public/img/generated/about-jumbotron-1280.avif new file mode 100644 index 0000000..d8a47ec Binary files /dev/null and b/public/img/generated/about-jumbotron-1280.avif differ diff --git a/public/img/generated/about-jumbotron-1280.webp b/public/img/generated/about-jumbotron-1280.webp new file mode 100644 index 0000000..e64496e Binary files /dev/null and b/public/img/generated/about-jumbotron-1280.webp differ diff --git a/public/img/generated/about-jumbotron-1600.avif b/public/img/generated/about-jumbotron-1600.avif new file mode 100644 index 0000000..92c3c66 Binary files /dev/null and b/public/img/generated/about-jumbotron-1600.avif differ diff --git a/public/img/generated/about-jumbotron-1600.webp b/public/img/generated/about-jumbotron-1600.webp new file mode 100644 index 0000000..926ff21 Binary files /dev/null and b/public/img/generated/about-jumbotron-1600.webp differ diff --git a/public/img/generated/about-jumbotron-1920.avif b/public/img/generated/about-jumbotron-1920.avif new file mode 100644 index 0000000..6f82889 Binary files /dev/null and b/public/img/generated/about-jumbotron-1920.avif differ diff --git a/public/img/generated/about-jumbotron-1920.webp b/public/img/generated/about-jumbotron-1920.webp new file mode 100644 index 0000000..fa4abb9 Binary files /dev/null and b/public/img/generated/about-jumbotron-1920.webp differ diff --git a/public/img/generated/about-jumbotron-2560.avif b/public/img/generated/about-jumbotron-2560.avif new file mode 100644 index 0000000..a787706 Binary files /dev/null and b/public/img/generated/about-jumbotron-2560.avif differ diff --git a/public/img/generated/about-jumbotron-2560.webp b/public/img/generated/about-jumbotron-2560.webp new file mode 100644 index 0000000..2c1d7b8 Binary files /dev/null and b/public/img/generated/about-jumbotron-2560.webp differ diff --git a/public/img/generated/about-jumbotron-640.avif b/public/img/generated/about-jumbotron-640.avif new file mode 100644 index 0000000..822e490 Binary files /dev/null and b/public/img/generated/about-jumbotron-640.avif differ diff --git a/public/img/generated/about-jumbotron-640.webp b/public/img/generated/about-jumbotron-640.webp new file mode 100644 index 0000000..affe3ca Binary files /dev/null and b/public/img/generated/about-jumbotron-640.webp differ diff --git a/public/img/generated/about-jumbotron-960.avif b/public/img/generated/about-jumbotron-960.avif new file mode 100644 index 0000000..0a28fa2 Binary files /dev/null and b/public/img/generated/about-jumbotron-960.avif differ diff --git a/public/img/generated/about-jumbotron-960.webp b/public/img/generated/about-jumbotron-960.webp new file mode 100644 index 0000000..11e401a Binary files /dev/null and b/public/img/generated/about-jumbotron-960.webp differ diff --git a/public/img/generated/blog-jumbotron-1280.avif b/public/img/generated/blog-jumbotron-1280.avif new file mode 100644 index 0000000..10008b1 Binary files /dev/null and b/public/img/generated/blog-jumbotron-1280.avif differ diff --git a/public/img/generated/blog-jumbotron-1280.webp b/public/img/generated/blog-jumbotron-1280.webp new file mode 100644 index 0000000..9bdbf4e Binary files /dev/null and b/public/img/generated/blog-jumbotron-1280.webp differ diff --git a/public/img/generated/blog-jumbotron-640.avif b/public/img/generated/blog-jumbotron-640.avif new file mode 100644 index 0000000..2dc169e Binary files /dev/null and b/public/img/generated/blog-jumbotron-640.avif differ diff --git a/public/img/generated/blog-jumbotron-640.webp b/public/img/generated/blog-jumbotron-640.webp new file mode 100644 index 0000000..72b89b9 Binary files /dev/null and b/public/img/generated/blog-jumbotron-640.webp differ diff --git a/public/img/generated/blog-jumbotron-960.avif b/public/img/generated/blog-jumbotron-960.avif new file mode 100644 index 0000000..8bc2703 Binary files /dev/null and b/public/img/generated/blog-jumbotron-960.avif differ diff --git a/public/img/generated/blog-jumbotron-960.webp b/public/img/generated/blog-jumbotron-960.webp new file mode 100644 index 0000000..5e3c8bf Binary files /dev/null and b/public/img/generated/blog-jumbotron-960.webp differ diff --git a/public/img/generated/home-jumbotron-1280.avif b/public/img/generated/home-jumbotron-1280.avif new file mode 100644 index 0000000..aa87fd2 Binary files /dev/null and b/public/img/generated/home-jumbotron-1280.avif differ diff --git a/public/img/generated/home-jumbotron-1280.webp b/public/img/generated/home-jumbotron-1280.webp new file mode 100644 index 0000000..5c36eb3 Binary files /dev/null and b/public/img/generated/home-jumbotron-1280.webp differ diff --git a/public/img/generated/home-jumbotron-1600.avif b/public/img/generated/home-jumbotron-1600.avif new file mode 100644 index 0000000..1531b66 Binary files /dev/null and b/public/img/generated/home-jumbotron-1600.avif differ diff --git a/public/img/generated/home-jumbotron-1600.webp b/public/img/generated/home-jumbotron-1600.webp new file mode 100644 index 0000000..788564b Binary files /dev/null and b/public/img/generated/home-jumbotron-1600.webp differ diff --git a/public/img/generated/home-jumbotron-1920.avif b/public/img/generated/home-jumbotron-1920.avif new file mode 100644 index 0000000..2b8b5a5 Binary files /dev/null and b/public/img/generated/home-jumbotron-1920.avif differ diff --git a/public/img/generated/home-jumbotron-1920.webp b/public/img/generated/home-jumbotron-1920.webp new file mode 100644 index 0000000..ace134f Binary files /dev/null and b/public/img/generated/home-jumbotron-1920.webp differ diff --git a/public/img/generated/home-jumbotron-640.avif b/public/img/generated/home-jumbotron-640.avif new file mode 100644 index 0000000..e771a4a Binary files /dev/null and b/public/img/generated/home-jumbotron-640.avif differ diff --git a/public/img/generated/home-jumbotron-640.webp b/public/img/generated/home-jumbotron-640.webp new file mode 100644 index 0000000..cb89f7a Binary files /dev/null and b/public/img/generated/home-jumbotron-640.webp differ diff --git a/public/img/generated/home-jumbotron-960.avif b/public/img/generated/home-jumbotron-960.avif new file mode 100644 index 0000000..1b3368a Binary files /dev/null and b/public/img/generated/home-jumbotron-960.avif differ diff --git a/public/img/generated/home-jumbotron-960.webp b/public/img/generated/home-jumbotron-960.webp new file mode 100644 index 0000000..f7685e5 Binary files /dev/null and b/public/img/generated/home-jumbotron-960.webp differ diff --git a/public/img/guava.jpg b/public/img/guava.jpg deleted file mode 100644 index f58e5c1..0000000 Binary files a/public/img/guava.jpg and /dev/null differ diff --git a/public/img/guava.webp b/public/img/guava.webp new file mode 100644 index 0000000..2c5381b Binary files /dev/null and b/public/img/guava.webp differ diff --git a/public/img/hair-cutting.jpg b/public/img/hair-cutting.jpg deleted file mode 100644 index 357dac3..0000000 Binary files a/public/img/hair-cutting.jpg and /dev/null differ diff --git a/public/img/hair-cutting.webp b/public/img/hair-cutting.webp new file mode 100644 index 0000000..aee423f Binary files /dev/null and b/public/img/hair-cutting.webp differ diff --git a/public/img/highschool.jpg b/public/img/highschool.jpg deleted file mode 100644 index b6ba2e0..0000000 Binary files a/public/img/highschool.jpg and /dev/null differ diff --git a/public/img/highschool.webp b/public/img/highschool.webp new file mode 100644 index 0000000..14c5db4 Binary files /dev/null and b/public/img/highschool.webp differ diff --git a/public/img/home-jumbotron.jpg b/public/img/home-jumbotron.jpg deleted file mode 100644 index 68a46a6..0000000 Binary files a/public/img/home-jumbotron.jpg and /dev/null differ diff --git a/public/img/home-jumbotron.webp b/public/img/home-jumbotron.webp new file mode 100644 index 0000000..c13c452 Binary files /dev/null and b/public/img/home-jumbotron.webp differ diff --git a/public/img/how-to-pronounce-my-name-image0.png b/public/img/how-to-pronounce-my-name-image0.png deleted file mode 100644 index 1cfbc84..0000000 Binary files a/public/img/how-to-pronounce-my-name-image0.png and /dev/null differ diff --git a/public/img/how-to-pronounce-my-name-image0.webp b/public/img/how-to-pronounce-my-name-image0.webp new file mode 100644 index 0000000..0f35976 Binary files /dev/null and b/public/img/how-to-pronounce-my-name-image0.webp differ diff --git a/public/img/hp.jpg b/public/img/hp.jpg deleted file mode 100644 index 0e8d101..0000000 Binary files a/public/img/hp.jpg and /dev/null differ diff --git a/public/img/hp.webp b/public/img/hp.webp new file mode 100644 index 0000000..60ad9ea Binary files /dev/null and b/public/img/hp.webp differ diff --git a/public/img/maze-generator-image1.png b/public/img/maze-generator-image1.png deleted file mode 100644 index c81f7af..0000000 Binary files a/public/img/maze-generator-image1.png and /dev/null differ diff --git a/public/img/maze-generator-image1.webp b/public/img/maze-generator-image1.webp new file mode 100644 index 0000000..c44a4fa Binary files /dev/null and b/public/img/maze-generator-image1.webp differ diff --git a/public/img/maze-generator-image10.png b/public/img/maze-generator-image10.png deleted file mode 100644 index da59070..0000000 Binary files a/public/img/maze-generator-image10.png and /dev/null differ diff --git a/public/img/maze-generator-image10.webp b/public/img/maze-generator-image10.webp new file mode 100644 index 0000000..8bcc051 Binary files /dev/null and b/public/img/maze-generator-image10.webp differ diff --git a/public/img/maze-generator-image11.png b/public/img/maze-generator-image11.png deleted file mode 100644 index 5a05401..0000000 Binary files a/public/img/maze-generator-image11.png and /dev/null differ diff --git a/public/img/maze-generator-image11.webp b/public/img/maze-generator-image11.webp new file mode 100644 index 0000000..9fb9772 Binary files /dev/null and b/public/img/maze-generator-image11.webp differ diff --git a/public/img/maze-generator-image12.png b/public/img/maze-generator-image12.png deleted file mode 100644 index 3472735..0000000 Binary files a/public/img/maze-generator-image12.png and /dev/null differ diff --git a/public/img/maze-generator-image12.webp b/public/img/maze-generator-image12.webp new file mode 100644 index 0000000..92f0f72 Binary files /dev/null and b/public/img/maze-generator-image12.webp differ diff --git a/public/img/maze-generator-image13.png b/public/img/maze-generator-image13.png deleted file mode 100644 index 4997472..0000000 Binary files a/public/img/maze-generator-image13.png and /dev/null differ diff --git a/public/img/maze-generator-image13.webp b/public/img/maze-generator-image13.webp new file mode 100644 index 0000000..8a2110d Binary files /dev/null and b/public/img/maze-generator-image13.webp differ diff --git a/public/img/maze-generator-image14.png b/public/img/maze-generator-image14.png deleted file mode 100644 index d4ed7d9..0000000 Binary files a/public/img/maze-generator-image14.png and /dev/null differ diff --git a/public/img/maze-generator-image14.webp b/public/img/maze-generator-image14.webp new file mode 100644 index 0000000..1bdc653 Binary files /dev/null and b/public/img/maze-generator-image14.webp differ diff --git a/public/img/maze-generator-image15.png b/public/img/maze-generator-image15.png deleted file mode 100644 index 6120eea..0000000 Binary files a/public/img/maze-generator-image15.png and /dev/null differ diff --git a/public/img/maze-generator-image15.webp b/public/img/maze-generator-image15.webp new file mode 100644 index 0000000..c4d5f8f Binary files /dev/null and b/public/img/maze-generator-image15.webp differ diff --git a/public/img/maze-generator-image16.png b/public/img/maze-generator-image16.png deleted file mode 100644 index 914356a..0000000 Binary files a/public/img/maze-generator-image16.png and /dev/null differ diff --git a/public/img/maze-generator-image16.webp b/public/img/maze-generator-image16.webp new file mode 100644 index 0000000..c742231 Binary files /dev/null and b/public/img/maze-generator-image16.webp differ diff --git a/public/img/maze-generator-image2.png b/public/img/maze-generator-image2.png deleted file mode 100644 index 95d29ba..0000000 Binary files a/public/img/maze-generator-image2.png and /dev/null differ diff --git a/public/img/maze-generator-image2.webp b/public/img/maze-generator-image2.webp new file mode 100644 index 0000000..750c280 Binary files /dev/null and b/public/img/maze-generator-image2.webp differ diff --git a/public/img/maze-generator-image3.png b/public/img/maze-generator-image3.png deleted file mode 100644 index d44d66c..0000000 Binary files a/public/img/maze-generator-image3.png and /dev/null differ diff --git a/public/img/maze-generator-image3.webp b/public/img/maze-generator-image3.webp new file mode 100644 index 0000000..97c90cd Binary files /dev/null and b/public/img/maze-generator-image3.webp differ diff --git a/public/img/maze-generator-image4.png b/public/img/maze-generator-image4.png deleted file mode 100644 index 7abe463..0000000 Binary files a/public/img/maze-generator-image4.png and /dev/null differ diff --git a/public/img/maze-generator-image4.webp b/public/img/maze-generator-image4.webp new file mode 100644 index 0000000..9061ce3 Binary files /dev/null and b/public/img/maze-generator-image4.webp differ diff --git a/public/img/maze-generator-image5.png b/public/img/maze-generator-image5.png deleted file mode 100644 index ecc2e83..0000000 Binary files a/public/img/maze-generator-image5.png and /dev/null differ diff --git a/public/img/maze-generator-image5.webp b/public/img/maze-generator-image5.webp new file mode 100644 index 0000000..76e0501 Binary files /dev/null and b/public/img/maze-generator-image5.webp differ diff --git a/public/img/maze-generator-image6.png b/public/img/maze-generator-image6.png deleted file mode 100644 index 414ef65..0000000 Binary files a/public/img/maze-generator-image6.png and /dev/null differ diff --git a/public/img/maze-generator-image6.webp b/public/img/maze-generator-image6.webp new file mode 100644 index 0000000..2be530d Binary files /dev/null and b/public/img/maze-generator-image6.webp differ diff --git a/public/img/maze-generator-image7.png b/public/img/maze-generator-image7.png deleted file mode 100644 index 79a68b8..0000000 Binary files a/public/img/maze-generator-image7.png and /dev/null differ diff --git a/public/img/maze-generator-image7.webp b/public/img/maze-generator-image7.webp new file mode 100644 index 0000000..980b6a6 Binary files /dev/null and b/public/img/maze-generator-image7.webp differ diff --git a/public/img/maze-generator-image8.png b/public/img/maze-generator-image8.png deleted file mode 100644 index a67581d..0000000 Binary files a/public/img/maze-generator-image8.png and /dev/null differ diff --git a/public/img/maze-generator-image8.webp b/public/img/maze-generator-image8.webp new file mode 100644 index 0000000..56ebd9f Binary files /dev/null and b/public/img/maze-generator-image8.webp differ diff --git a/public/img/maze-generator-image9.png b/public/img/maze-generator-image9.png deleted file mode 100644 index 7209afb..0000000 Binary files a/public/img/maze-generator-image9.png and /dev/null differ diff --git a/public/img/maze-generator-image9.webp b/public/img/maze-generator-image9.webp new file mode 100644 index 0000000..51a856a Binary files /dev/null and b/public/img/maze-generator-image9.webp differ diff --git a/public/img/maze-splash.png b/public/img/maze-splash.png deleted file mode 100644 index db60ee6..0000000 Binary files a/public/img/maze-splash.png and /dev/null differ diff --git a/public/img/maze-splash.webp b/public/img/maze-splash.webp new file mode 100644 index 0000000..0f1b976 Binary files /dev/null and b/public/img/maze-splash.webp differ diff --git a/public/img/my-first-vue-web-app-difficulties-and-things-i-wish-i-knew-part-1-image1.png b/public/img/my-first-vue-web-app-difficulties-and-things-i-wish-i-knew-part-1-image1.png deleted file mode 100644 index 182ab04..0000000 Binary files a/public/img/my-first-vue-web-app-difficulties-and-things-i-wish-i-knew-part-1-image1.png and /dev/null differ diff --git a/public/img/my-first-vue-web-app-difficulties-and-things-i-wish-i-knew-part-1-image1.webp b/public/img/my-first-vue-web-app-difficulties-and-things-i-wish-i-knew-part-1-image1.webp new file mode 100644 index 0000000..d3c0c42 Binary files /dev/null and b/public/img/my-first-vue-web-app-difficulties-and-things-i-wish-i-knew-part-1-image1.webp differ diff --git a/public/img/my-first-vue-web-app-difficulties-and-things-i-wish-i-knew-part-2-image1.png b/public/img/my-first-vue-web-app-difficulties-and-things-i-wish-i-knew-part-2-image1.png deleted file mode 100644 index 9ec4ed7..0000000 Binary files a/public/img/my-first-vue-web-app-difficulties-and-things-i-wish-i-knew-part-2-image1.png and /dev/null differ diff --git a/public/img/my-first-vue-web-app-difficulties-and-things-i-wish-i-knew-part-2-image1.webp b/public/img/my-first-vue-web-app-difficulties-and-things-i-wish-i-knew-part-2-image1.webp new file mode 100644 index 0000000..9c1618c Binary files /dev/null and b/public/img/my-first-vue-web-app-difficulties-and-things-i-wish-i-knew-part-2-image1.webp differ diff --git a/public/img/my-first-vue-web-app-difficulties-and-things-i-wish-i-knew-part-2-image2.png b/public/img/my-first-vue-web-app-difficulties-and-things-i-wish-i-knew-part-2-image2.png deleted file mode 100644 index 8981f03..0000000 Binary files a/public/img/my-first-vue-web-app-difficulties-and-things-i-wish-i-knew-part-2-image2.png and /dev/null differ diff --git a/public/img/my-first-vue-web-app-difficulties-and-things-i-wish-i-knew-part-2-image2.webp b/public/img/my-first-vue-web-app-difficulties-and-things-i-wish-i-knew-part-2-image2.webp new file mode 100644 index 0000000..b770589 Binary files /dev/null and b/public/img/my-first-vue-web-app-difficulties-and-things-i-wish-i-knew-part-2-image2.webp differ diff --git a/public/img/my-first-vue-web-app-difficulties-and-things-i-wish-i-knew-part-3-image1.png b/public/img/my-first-vue-web-app-difficulties-and-things-i-wish-i-knew-part-3-image1.png deleted file mode 100644 index 88624cd..0000000 Binary files a/public/img/my-first-vue-web-app-difficulties-and-things-i-wish-i-knew-part-3-image1.png and /dev/null differ diff --git a/public/img/my-first-vue-web-app-difficulties-and-things-i-wish-i-knew-part-3-image1.webp b/public/img/my-first-vue-web-app-difficulties-and-things-i-wish-i-knew-part-3-image1.webp new file mode 100644 index 0000000..f28c3bd Binary files /dev/null and b/public/img/my-first-vue-web-app-difficulties-and-things-i-wish-i-knew-part-3-image1.webp differ diff --git a/public/img/my-hearthstone-gameplay-milestone-legend-rank-achieved-1.jpg b/public/img/my-hearthstone-gameplay-milestone-legend-rank-achieved-1.jpg deleted file mode 100644 index 57ff53e..0000000 Binary files a/public/img/my-hearthstone-gameplay-milestone-legend-rank-achieved-1.jpg and /dev/null differ diff --git a/public/img/my-hearthstone-gameplay-milestone-legend-rank-achieved-1.webp b/public/img/my-hearthstone-gameplay-milestone-legend-rank-achieved-1.webp new file mode 100644 index 0000000..7445bc2 Binary files /dev/null and b/public/img/my-hearthstone-gameplay-milestone-legend-rank-achieved-1.webp differ diff --git a/public/img/name.png b/public/img/name.png deleted file mode 100644 index 01b03db..0000000 Binary files a/public/img/name.png and /dev/null differ diff --git a/public/img/name.webp b/public/img/name.webp new file mode 100644 index 0000000..8c32543 Binary files /dev/null and b/public/img/name.webp differ diff --git a/public/img/nodejs-challenge.png b/public/img/nodejs-challenge.png deleted file mode 100644 index a1f28ea..0000000 Binary files a/public/img/nodejs-challenge.png and /dev/null differ diff --git a/public/img/nodejs-challenge.webp b/public/img/nodejs-challenge.webp new file mode 100644 index 0000000..17afa7b Binary files /dev/null and b/public/img/nodejs-challenge.webp differ diff --git a/public/img/note-taking-app-2-20211113135526.png b/public/img/note-taking-app-2-20211113135526.png deleted file mode 100644 index 847ef1c..0000000 Binary files a/public/img/note-taking-app-2-20211113135526.png and /dev/null differ diff --git a/public/img/note-taking-app-2-20211113135526.webp b/public/img/note-taking-app-2-20211113135526.webp new file mode 100644 index 0000000..6253ac1 Binary files /dev/null and b/public/img/note-taking-app-2-20211113135526.webp differ diff --git a/public/img/note-taking-app-2-20211113145514.png b/public/img/note-taking-app-2-20211113145514.png deleted file mode 100644 index 7081eb6..0000000 Binary files a/public/img/note-taking-app-2-20211113145514.png and /dev/null differ diff --git a/public/img/note-taking-app-2-20211113145514.webp b/public/img/note-taking-app-2-20211113145514.webp new file mode 100644 index 0000000..bfaa77d Binary files /dev/null and b/public/img/note-taking-app-2-20211113145514.webp differ diff --git a/public/img/nox.jpg b/public/img/nox.jpg deleted file mode 100644 index 90a06e9..0000000 Binary files a/public/img/nox.jpg and /dev/null differ diff --git a/public/img/nox.webp b/public/img/nox.webp new file mode 100644 index 0000000..bb9a21d Binary files /dev/null and b/public/img/nox.webp differ diff --git a/public/img/question-answering-chatbot-0.png b/public/img/question-answering-chatbot-0.png deleted file mode 100644 index 7298055..0000000 Binary files a/public/img/question-answering-chatbot-0.png and /dev/null differ diff --git a/public/img/question-answering-chatbot-0.webp b/public/img/question-answering-chatbot-0.webp new file mode 100644 index 0000000..5b1bb64 Binary files /dev/null and b/public/img/question-answering-chatbot-0.webp differ diff --git a/public/img/question-answering-chatbot-1.png b/public/img/question-answering-chatbot-1.png deleted file mode 100644 index f6f897f..0000000 Binary files a/public/img/question-answering-chatbot-1.png and /dev/null differ diff --git a/public/img/question-answering-chatbot-1.webp b/public/img/question-answering-chatbot-1.webp new file mode 100644 index 0000000..39017c2 Binary files /dev/null and b/public/img/question-answering-chatbot-1.webp differ diff --git a/public/img/react-trick-how-to-create-an-initializer-block-for-a-function-component-image1.png b/public/img/react-trick-how-to-create-an-initializer-block-for-a-function-component-image1.png deleted file mode 100644 index 265d972..0000000 Binary files a/public/img/react-trick-how-to-create-an-initializer-block-for-a-function-component-image1.png and /dev/null differ diff --git a/public/img/react-trick-how-to-create-an-initializer-block-for-a-function-component-image1.webp b/public/img/react-trick-how-to-create-an-initializer-block-for-a-function-component-image1.webp new file mode 100644 index 0000000..157eacb Binary files /dev/null and b/public/img/react-trick-how-to-create-an-initializer-block-for-a-function-component-image1.webp differ diff --git a/public/img/review-on-astra-lost-in-space-image1.png b/public/img/review-on-astra-lost-in-space-image1.png deleted file mode 100644 index 50c8bc9..0000000 Binary files a/public/img/review-on-astra-lost-in-space-image1.png and /dev/null differ diff --git a/public/img/review-on-astra-lost-in-space-image1.webp b/public/img/review-on-astra-lost-in-space-image1.webp new file mode 100644 index 0000000..1a22f7d Binary files /dev/null and b/public/img/review-on-astra-lost-in-space-image1.webp differ diff --git a/public/img/review-on-astra-lost-in-space-image2.png b/public/img/review-on-astra-lost-in-space-image2.png deleted file mode 100644 index 04d84bb..0000000 Binary files a/public/img/review-on-astra-lost-in-space-image2.png and /dev/null differ diff --git a/public/img/review-on-astra-lost-in-space-image2.webp b/public/img/review-on-astra-lost-in-space-image2.webp new file mode 100644 index 0000000..f6fbdbe Binary files /dev/null and b/public/img/review-on-astra-lost-in-space-image2.webp differ diff --git a/public/img/review-on-astra-lost-in-space-image3.png b/public/img/review-on-astra-lost-in-space-image3.png deleted file mode 100644 index 810363e..0000000 Binary files a/public/img/review-on-astra-lost-in-space-image3.png and /dev/null differ diff --git a/public/img/review-on-astra-lost-in-space-image3.webp b/public/img/review-on-astra-lost-in-space-image3.webp new file mode 100644 index 0000000..9f2ce4c Binary files /dev/null and b/public/img/review-on-astra-lost-in-space-image3.webp differ diff --git a/public/img/review-on-astra-lost-in-space-image4.png b/public/img/review-on-astra-lost-in-space-image4.png deleted file mode 100644 index dc0eb17..0000000 Binary files a/public/img/review-on-astra-lost-in-space-image4.png and /dev/null differ diff --git a/public/img/review-on-astra-lost-in-space-image4.webp b/public/img/review-on-astra-lost-in-space-image4.webp new file mode 100644 index 0000000..a0cb836 Binary files /dev/null and b/public/img/review-on-astra-lost-in-space-image4.webp differ diff --git a/public/img/robotic-arm.png b/public/img/robotic-arm.png deleted file mode 100644 index 993864c..0000000 Binary files a/public/img/robotic-arm.png and /dev/null differ diff --git a/public/img/robotic-arm.webp b/public/img/robotic-arm.webp new file mode 100644 index 0000000..d8d1785 Binary files /dev/null and b/public/img/robotic-arm.webp differ diff --git a/public/img/steve-elizabeth-x.jpg b/public/img/steve-elizabeth-x.jpg deleted file mode 100644 index cad6961..0000000 Binary files a/public/img/steve-elizabeth-x.jpg and /dev/null differ diff --git a/public/img/steve-elizabeth-x.webp b/public/img/steve-elizabeth-x.webp new file mode 100644 index 0000000..d8a2540 Binary files /dev/null and b/public/img/steve-elizabeth-x.webp differ diff --git a/public/img/substitution.png b/public/img/substitution.png deleted file mode 100644 index fa1f1a3..0000000 Binary files a/public/img/substitution.png and /dev/null differ diff --git a/public/img/substitution.webp b/public/img/substitution.webp new file mode 100644 index 0000000..29d44ca Binary files /dev/null and b/public/img/substitution.webp differ diff --git a/public/img/vue-trick-optimization-for-v-for-image1.png b/public/img/vue-trick-optimization-for-v-for-image1.png deleted file mode 100644 index f706607..0000000 Binary files a/public/img/vue-trick-optimization-for-v-for-image1.png and /dev/null differ diff --git a/public/img/vue-trick-optimization-for-v-for-image1.webp b/public/img/vue-trick-optimization-for-v-for-image1.webp new file mode 100644 index 0000000..819f7af Binary files /dev/null and b/public/img/vue-trick-optimization-for-v-for-image1.webp differ diff --git a/public/img/why-i-created-this-page-image1.png b/public/img/why-i-created-this-page-image1.png deleted file mode 100644 index d2acf61..0000000 Binary files a/public/img/why-i-created-this-page-image1.png and /dev/null differ diff --git a/public/img/why-i-created-this-page-image1.webp b/public/img/why-i-created-this-page-image1.webp new file mode 100644 index 0000000..7502a81 Binary files /dev/null and b/public/img/why-i-created-this-page-image1.webp differ diff --git a/public/img/why-i-created-this-page-image2.png b/public/img/why-i-created-this-page-image2.png deleted file mode 100644 index 66cac90..0000000 Binary files a/public/img/why-i-created-this-page-image2.png and /dev/null differ diff --git a/public/img/why-i-created-this-page-image2.webp b/public/img/why-i-created-this-page-image2.webp new file mode 100644 index 0000000..768df69 Binary files /dev/null and b/public/img/why-i-created-this-page-image2.webp differ diff --git a/scripts/check-responsive-images.mjs b/scripts/check-responsive-images.mjs new file mode 100644 index 0000000..18a217b --- /dev/null +++ b/scripts/check-responsive-images.mjs @@ -0,0 +1,110 @@ +import fs from 'node:fs/promises' +import path from 'node:path' +import sharp from 'sharp' + +const projectRoot = process.cwd() +const publicDir = path.join(projectRoot, 'public') +const outputDir = path.join(publicDir, 'img', 'generated') +const variantsConfigPath = path.join(projectRoot, 'src', 'lib', 'images', 'jumbotron-image-variants.json') +const manifestPath = path.join(projectRoot, 'src', 'lib', 'images', 'jumbotron-generated-images.json') +const jumbotronImageConfig = JSON.parse(await fs.readFile(variantsConfigPath, 'utf8')) +const generatedJumbotronImages = JSON.parse(await fs.readFile(manifestPath, 'utf8')) +const REQUIRED_FORMATS = ['webp', 'avif'] + +function publicPathToFilePath(publicPath) { + return path.join(publicDir, publicPath.replace(/^\//, '')) +} + +function outputPathFor(publicImagePath, width, format) { + const filename = path.basename(publicImagePath) + const basename = filename.replace(/\.[^.]+$/, '') + return path.join(outputDir, `${basename}-${width}.${format}`) +} + +async function exists(filePath) { + try { + await fs.access(filePath) + return true + } catch { + return false + } +} + +function sameArray(left, right) { + return left.length === right.length && left.every((item, index) => item === right[index]) +} + +async function checkResponsiveImages() { + const missingFiles = [] + const manifestErrors = [] + + for (const [publicImagePath, imageConfig] of Object.entries(jumbotronImageConfig.images)) { + const inputPath = publicPathToFilePath(publicImagePath) + if (!(await exists(inputPath))) { + missingFiles.push(path.relative(projectRoot, inputPath)) + continue + } + + const metadata = await sharp(inputPath).metadata() + const originalWidth = metadata.width ?? 0 + const widths = imageConfig.widths || jumbotronImageConfig.widths + const targetWidths = widths.filter(width => width <= originalWidth) + const manifestImage = generatedJumbotronImages[publicImagePath] + + if (targetWidths.length === 0) { + throw new Error(`${publicImagePath} is too small for configured responsive widths`) + } + + if (!manifestImage) { + manifestErrors.push(`${publicImagePath} missing from generated manifest`) + } else { + if (!sameArray(manifestImage.widths, targetWidths)) { + manifestErrors.push( + `${publicImagePath} manifest widths ${JSON.stringify(manifestImage.widths)} do not match expected ${JSON.stringify(targetWidths)}` + ) + } + + if (!manifestImage.placeholderDataUrl?.startsWith('data:image/webp;base64,')) { + manifestErrors.push(`${publicImagePath} missing WebP placeholder data URL`) + } + } + + for (const width of targetWidths) { + for (const format of REQUIRED_FORMATS) { + const outputPath = outputPathFor(publicImagePath, width, format) + if (!(await exists(outputPath))) { + missingFiles.push(path.relative(projectRoot, outputPath)) + } + } + } + } + + for (const publicImagePath of Object.keys(generatedJumbotronImages)) { + if (!jumbotronImageConfig.images[publicImagePath]) { + manifestErrors.push(`${publicImagePath} is stale in generated manifest`) + } + } + + if (manifestErrors.length > 0) { + console.error('Responsive image manifest is stale:') + manifestErrors.forEach(error => console.error(`- ${error}`)) + console.error('\nRun `npm run images:generate` and commit the generated files.') + process.exitCode = 1 + return + } + + if (missingFiles.length > 0) { + console.error('Missing generated responsive image files:') + missingFiles.forEach(file => console.error(`- ${file}`)) + console.error('\nRun `npm run images:generate` and commit the generated files.') + process.exitCode = 1 + return + } + + console.log('Responsive image files are present.') +} + +checkResponsiveImages().catch(error => { + console.error(error) + process.exitCode = 1 +}) diff --git a/scripts/convert-images-to-webp.mjs b/scripts/convert-images-to-webp.mjs new file mode 100644 index 0000000..1b8171c --- /dev/null +++ b/scripts/convert-images-to-webp.mjs @@ -0,0 +1,187 @@ +import fs from 'node:fs/promises' +import path from 'node:path' +import sharp from 'sharp' + +const projectRoot = process.cwd() +const imageRoot = path.join(projectRoot, 'public', 'img') +const rewriteRoots = [ + path.join(projectRoot, 'content'), + path.join(projectRoot, 'src'), + path.join(projectRoot, 'public'), +] +const imageExtensions = new Set(['.jpg', '.jpeg', '.png']) +const textExtensions = new Set([ + '.cjs', + '.css', + '.html', + '.js', + '.json', + '.jsx', + '.md', + '.mjs', + '.ts', + '.tsx', + '.txt', + '.yml', + '.yaml', +]) +const ignoredDirectoryNames = new Set([ + '.git', + '.next', + 'node_modules', + 'out', + 'generated', +]) +const ignoredImageFilenames = new Set([ + // Keep PNG for favicon/PWA compatibility; WebP icon support is less universal. + 'icon.png', +]) + +function toPosixPath(filePath) { + return filePath.split(path.sep).join('/') +} + +async function pathExists(filePath) { + try { + await fs.access(filePath) + return true + } catch { + return false + } +} + +async function walkFiles(root, predicate) { + if (!(await pathExists(root))) return [] + + const result = [] + const entries = await fs.readdir(root, { withFileTypes: true }) + + for (const entry of entries) { + if (entry.name.startsWith('.') && ignoredDirectoryNames.has(entry.name)) continue + + const fullPath = path.join(root, entry.name) + if (entry.isDirectory()) { + if (ignoredDirectoryNames.has(entry.name)) continue + result.push(...await walkFiles(fullPath, predicate)) + } else if (!predicate || predicate(fullPath)) { + result.push(fullPath) + } + } + + return result +} + +function isConvertibleImage(filePath) { + const ext = path.extname(filePath).toLowerCase() + return imageExtensions.has(ext) && !ignoredImageFilenames.has(path.basename(filePath)) +} + +function isTextFile(filePath) { + const ext = path.extname(filePath).toLowerCase() + return textExtensions.has(ext) +} + +function getReplacementPairs(imagePath) { + const relativeToPublic = toPosixPath(path.relative(path.join(projectRoot, 'public'), imagePath)) + const publicUrl = `/${relativeToPublic}` + const webpPublicUrl = publicUrl.replace(/\.[^.]+$/, '.webp') + const publicRelativePath = relativeToPublic + const webpPublicRelativePath = publicRelativePath.replace(/\.[^.]+$/, '.webp') + + return [ + [publicUrl, webpPublicUrl], + [publicRelativePath, webpPublicRelativePath], + ] +} + +async function convertImage(imagePath) { + const outputPath = imagePath.replace(/\.[^.]+$/, '.webp') + await sharp(imagePath) + .rotate() + .webp({ quality: 82, effort: 6 }) + .toFile(outputPath) + return outputPath +} + +async function rewriteReferences(replacements) { + const textFilesNested = await Promise.all( + rewriteRoots.map(root => walkFiles(root, isTextFile)) + ) + const textFiles = [...new Set(textFilesNested.flat())] + const changedFiles = [] + + for (const filePath of textFiles) { + let content = await fs.readFile(filePath, 'utf8') + let nextContent = content + + for (const [oldText, newText] of replacements) { + nextContent = nextContent.split(oldText).join(newText) + } + + if (nextContent !== content) { + await fs.writeFile(filePath, nextContent) + changedFiles.push(filePath) + } + } + + return changedFiles +} + +async function findRemainingReferences(oldReferences) { + const textFilesNested = await Promise.all( + rewriteRoots.map(root => walkFiles(root, isTextFile)) + ) + const textFiles = [...new Set(textFilesNested.flat())] + const remaining = [] + + for (const filePath of textFiles) { + const content = await fs.readFile(filePath, 'utf8') + for (const reference of oldReferences) { + if (content.includes(reference)) { + remaining.push({ filePath, reference }) + } + } + } + + return remaining +} + +async function convertImagesToWebp() { + const imageFiles = await walkFiles(imageRoot, isConvertibleImage) + const replacements = [] + const converted = [] + + for (const imagePath of imageFiles) { + const outputPath = await convertImage(imagePath) + converted.push({ imagePath, outputPath }) + replacements.push(...getReplacementPairs(imagePath)) + } + + const changedFiles = await rewriteReferences(replacements) + const oldReferences = replacements.map(([oldText]) => oldText) + const remainingReferences = await findRemainingReferences(oldReferences) + + if (remainingReferences.length > 0) { + console.error('Some old image references remain; originals were kept:') + remainingReferences.forEach(({ filePath, reference }) => { + console.error(`- ${path.relative(projectRoot, filePath)}: ${reference}`) + }) + process.exitCode = 1 + return + } + + for (const { imagePath } of converted) { + await fs.unlink(imagePath) + } + + console.log(`Converted ${converted.length} images to WebP.`) + console.log(`Updated ${changedFiles.length} text files.`) + converted.forEach(({ imagePath, outputPath }) => { + console.log(`${path.relative(projectRoot, imagePath)} -> ${path.relative(projectRoot, outputPath)}`) + }) +} + +convertImagesToWebp().catch(error => { + console.error(error) + process.exitCode = 1 +}) diff --git a/scripts/generate-responsive-images.mjs b/scripts/generate-responsive-images.mjs new file mode 100644 index 0000000..eeb6c4d --- /dev/null +++ b/scripts/generate-responsive-images.mjs @@ -0,0 +1,153 @@ +import fs from 'node:fs/promises' +import path from 'node:path' +import sharp from 'sharp' + +const projectRoot = process.cwd() +const publicDir = path.join(projectRoot, 'public') +const outputDir = path.join(publicDir, 'img', 'generated') +const variantsConfigPath = path.join(projectRoot, 'src', 'lib', 'images', 'jumbotron-image-variants.json') +const manifestPath = path.join(projectRoot, 'src', 'lib', 'images', 'jumbotron-generated-images.json') +const jumbotronImageConfig = JSON.parse(await fs.readFile(variantsConfigPath, 'utf8')) + +const FORMAT_OPTIONS = { + webp: { quality: 82, effort: 5 }, + avif: { quality: 55, effort: 4 }, +} +const PLACEHOLDER_OPTIONS = { + width: 32, + blurSigma: 4, + quality: 35, + effort: 4, +} + +function publicPathToFilePath(publicPath) { + return path.join(publicDir, publicPath.replace(/^\//, '')) +} + +function outputPathFor(publicImagePath, width, format) { + const filename = path.basename(publicImagePath) + const basename = filename.replace(/\.[^.]+$/, '') + return path.join(outputDir, `${basename}-${width}.${format}`) +} + +async function fileSize(filePath) { + const stat = await fs.stat(filePath) + return stat.size +} + +async function generateVariant(inputPath, outputPath, width, format) { + let pipeline = sharp(inputPath) + .rotate() + .resize({ width, withoutEnlargement: true }) + + if (format === 'webp') { + pipeline = pipeline.webp(FORMAT_OPTIONS.webp) + } else if (format === 'avif') { + pipeline = pipeline.avif(FORMAT_OPTIONS.avif) + } else { + throw new Error(`Unsupported image format: ${format}`) + } + + await pipeline.toFile(outputPath) +} + +async function generatePlaceholderDataUrl(inputPath) { + const buffer = await sharp(inputPath) + .rotate() + .resize({ width: PLACEHOLDER_OPTIONS.width, withoutEnlargement: true }) + .blur(PLACEHOLDER_OPTIONS.blurSigma) + .webp({ quality: PLACEHOLDER_OPTIONS.quality, effort: PLACEHOLDER_OPTIONS.effort }) + .toBuffer() + + return `data:image/webp;base64,${buffer.toString('base64')}` +} + +async function listGeneratedFiles() { + try { + const entries = await fs.readdir(outputDir) + return entries.map(entry => path.join(outputDir, entry)) + } catch (error) { + if (error.code === 'ENOENT') return [] + throw error + } +} + +async function removeUnusedGeneratedFiles(expectedOutputPaths) { + const expected = new Set(expectedOutputPaths) + const existingFiles = await listGeneratedFiles() + const unusedFiles = existingFiles.filter(filePath => !expected.has(filePath)) + + for (const filePath of unusedFiles) { + await fs.unlink(filePath) + console.log(`removed stale ${path.relative(projectRoot, filePath)}`) + } +} + +async function getTargetImages() { + const images = {} + + for (const [publicImagePath, imageConfig] of Object.entries(jumbotronImageConfig.images)) { + const inputPath = publicPathToFilePath(publicImagePath) + const metadata = await sharp(inputPath).metadata() + const originalWidth = metadata.width ?? 0 + const widths = imageConfig.widths || jumbotronImageConfig.widths + const targetWidths = widths.filter(width => width <= originalWidth) + + if (targetWidths.length === 0) { + throw new Error(`${publicImagePath} is too small for configured responsive widths`) + } + + images[publicImagePath] = { + alt: imageConfig.alt || '', + inputPath, + widths: targetWidths, + variants: targetWidths.flatMap(width => Object.keys(FORMAT_OPTIONS).map(format => ({ + inputPath, + width, + format, + outputPath: outputPathFor(publicImagePath, width, format), + }))), + } + } + + return images +} + +async function writeManifest(targetImages) { + const manifestEntries = await Promise.all( + Object.entries(targetImages).map(async ([publicImagePath, image]) => [ + publicImagePath, + { + alt: image.alt, + widths: image.widths, + placeholderDataUrl: await generatePlaceholderDataUrl(image.inputPath), + }, + ]) + ) + const manifest = Object.fromEntries(manifestEntries) + + await fs.writeFile(`${manifestPath}.tmp`, `${JSON.stringify(manifest, null, 2)}\n`) + await fs.rename(`${manifestPath}.tmp`, manifestPath) +} + +async function generateResponsiveImages() { + await fs.mkdir(outputDir, { recursive: true }) + + const targetImages = await getTargetImages() + const variants = Object.values(targetImages).flatMap(image => image.variants) + await removeUnusedGeneratedFiles(variants.map(variant => variant.outputPath)) + + for (const variant of variants) { + await generateVariant(variant.inputPath, variant.outputPath, variant.width, variant.format) + const size = await fileSize(variant.outputPath) + console.log(`${path.relative(projectRoot, variant.outputPath)} ${(size / 1024).toFixed(1)} KB`) + } + + await writeManifest(targetImages) + console.log(`${path.relative(projectRoot, manifestPath)} updated`) +} + +generateResponsiveImages().catch(error => { + console.error(error) + process.exitCode = 1 +}) diff --git a/src/app/about/page.tsx b/src/app/about/page.tsx index 37baaee..3d57e10 100644 --- a/src/app/about/page.tsx +++ b/src/app/about/page.tsx @@ -10,7 +10,7 @@ async function getAboutPageData() { jumbotronProps: (aboutData?.frontmatter as any)?.jumbotron || { headline: "About", subtitle: "Get to know me", - image: "/img/about-jumbotron.jpg" + image: "/img/about-jumbotron.webp" }, facts: (aboutData?.frontmatter as any)?.facts || [] } diff --git a/src/app/blog/loading.tsx b/src/app/blog/loading.tsx deleted file mode 100644 index 37033ed..0000000 --- a/src/app/blog/loading.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { calcViewportHeight } from '../../lib/dom/viewport' - -export default function Loading() { - return ( -
- Loading blog posts... -
- ) -} \ No newline at end of file diff --git a/src/app/blog/page.tsx b/src/app/blog/page.tsx index b99e1da..d819bc1 100644 --- a/src/app/blog/page.tsx +++ b/src/app/blog/page.tsx @@ -1,32 +1,44 @@ +import { Suspense } from 'react' import { getAllBlogPosts, getMarkdownData } from '@/lib/content/content' -import BlogPage from '@/templates/blog-page' +import SEO from '@/components/header/seo' +import { BlogCardsLoadingSection, BlogPageFrame, BlogPostsSection } from '@/templates/blog-page-template' -async function getBlogPageData() { - const blogData = await getMarkdownData('blog.md') - const blogPosts = await getAllBlogPosts() +const BLOG_URI = '/blog' +const BLOG_TABLE_NAME = 'blogTable' - // Simplified data structure without GraphQL nesting - const data = { - frontmatter: { - jumbotronProps: (blogData?.frontmatter as any)?.jumbotron || { - headline: "Blog", - subtitle: "Thoughts and ideas", - image: "/img/blog-jumbotron.jpg" - } - }, - posts: blogPosts +async function getBlogJumbotronProps() { + const blogData = await getMarkdownData('blog.md', { includeContent: false }) + + return (blogData?.frontmatter as any)?.jumbotron || { + headline: 'Blog', + subtitle: 'Thoughts and ideas', + image: '/img/blog-jumbotron.webp', } +} - return data +async function BlogPostsSectionLoader() { + const posts = await getAllBlogPosts() + + return ( + + ) } export default async function BlogPageWrapper() { - const data = await getBlogPageData() + const jumbotronProps = await getBlogJumbotronProps() return ( - + <> + + + }> + + + + ) } diff --git a/src/app/page.jsx b/src/app/page.jsx index 0fc1359..4acffe7 100644 --- a/src/app/page.jsx +++ b/src/app/page.jsx @@ -20,7 +20,7 @@ async function getHomePageData() { jumbotronProps: indexData?.frontmatter?.jumbotron || { headline: "Xian", subtitle: "昞", - image: "/img/home-jumbotron.jpg" + image: "/img/home-jumbotron.webp" }, blogPosts: featuredBlogPosts, portfolioItems: featuredPortfolioItems diff --git a/src/components/header/jumbotron.tsx b/src/components/header/jumbotron.tsx index 73de51e..1106109 100644 --- a/src/components/header/jumbotron.tsx +++ b/src/components/header/jumbotron.tsx @@ -3,6 +3,7 @@ import React from "react" import { makeStyles } from "@mui/styles" import ParallaxSection from "../sections/parallax-section" +import { getJumbotronResponsiveImage } from "../../lib/images/jumbotron-images" import { calcViewportHeight, calcViewportWidth } from "../../lib/dom/viewport" const useStyles = makeStyles({ @@ -32,8 +33,22 @@ const useStyles = makeStyles({ // this is commented out due to limited browser support on mobile devices, use ParallaxSection instead // backgroundAttachment: "fixed", margin: 0, + position: "relative", backgroundSize: "cover", backgroundPosition: "center center", + backgroundRepeat: "no-repeat", + }, + backgroundPicture: { + display: "block", + width: "100%", + height: "100%", + }, + backgroundImage: { + display: "block", + width: "100%", + height: "100%", + objectFit: "cover", + objectPosition: "center center", }, headlineContainer: { position: "absolute", @@ -67,6 +82,7 @@ interface JumbotronProps { const Jumbotron: React.FC = ({ image, headline = "", subtitle = "", darkFilter = 0.3 }) => { const classes = useStyles() const backgroundImage = image || '' + const responsiveImage = getJumbotronResponsiveImage(backgroundImage) const lines = headline.split("\n") const [firstLine, ...restLines] = lines @@ -80,10 +96,28 @@ const Jumbotron: React.FC = ({ image, headline = "", subtitle =
+ style={responsiveImage?.placeholderDataUrl + ? { backgroundImage: `url(${responsiveImage.placeholderDataUrl})` } + : undefined} + > + + {responsiveImage && ( + <> + + + + )} + {responsiveImage?.alt + +
diff --git a/src/components/sprite/flying-sprite.jsx b/src/components/sprite/flying-sprite.jsx index 291a1f3..e369f1f 100644 --- a/src/components/sprite/flying-sprite.jsx +++ b/src/components/sprite/flying-sprite.jsx @@ -223,7 +223,7 @@ function startAnimationFlyingSprite( const SPRITE_DIMENSION = { x: 130, y: 134 } function FlyingSprite({ style }) { - const imgSrc = "/img/bulin.png" // Static asset path + const imgSrc = "/img/bulin.webp" // Static asset path const spriteRef = useRef(null) // const context = useLayoutContext(); diff --git a/src/lib/images/jumbotron-generated-images.json b/src/lib/images/jumbotron-generated-images.json new file mode 100644 index 0000000..9db4b7d --- /dev/null +++ b/src/lib/images/jumbotron-generated-images.json @@ -0,0 +1,34 @@ +{ + "/img/home-jumbotron.webp": { + "alt": "", + "widths": [ + 640, + 960, + 1280, + 1600, + 1920 + ], + "placeholderDataUrl": "data:image/webp;base64,UklGRlIAAABXRUJQVlA4IEYAAABQAwCdASogABIAPuFapE6sJSOiN+gBgBwJZwDQVD9oFGc+AAD+7zZ8676wY170ohxEAT6TUs463Bkq6bYobgMFRrC2iRwA" + }, + "/img/blog-jumbotron.webp": { + "alt": "", + "widths": [ + 640, + 960, + 1280 + ], + "placeholderDataUrl": "data:image/webp;base64,UklGRlgAAABXRUJQVlA4IEwAAACwBACdASogABIAPwFqrE6rJi2mMBgMAbAgCWcAzjgQlyTqdOT4rKBpynpcDnbQAPCmMyMkY4f9F6dr1JHS61Na1iE3Ydl9Hpv4IAAA" + }, + "/img/about-jumbotron.webp": { + "alt": "", + "widths": [ + 640, + 960, + 1280, + 1600, + 1920, + 2560 + ], + "placeholderDataUrl": "data:image/webp;base64,UklGRmYAAABXRUJQVlA4IFoAAACwBACdASogABoAPvVuqU6qpyQiMAwBUB6JaQAA78/RH4XgSVupUjXXdV0DTTTAAP76GrOvHD1RV27K3WXTyFgec1eJSBSejMTRfNPDLO9ivL3vcUYYDjAAAAA=" + } +} diff --git a/src/lib/images/jumbotron-image-variants.json b/src/lib/images/jumbotron-image-variants.json new file mode 100644 index 0000000..bd18d62 --- /dev/null +++ b/src/lib/images/jumbotron-image-variants.json @@ -0,0 +1,14 @@ +{ + "widths": [640, 960, 1280, 1600, 1920, 2560], + "images": { + "/img/home-jumbotron.webp": { + "alt": "" + }, + "/img/blog-jumbotron.webp": { + "alt": "" + }, + "/img/about-jumbotron.webp": { + "alt": "" + } + } +} diff --git a/src/lib/images/jumbotron-images.js b/src/lib/images/jumbotron-images.js new file mode 100644 index 0000000..b05ccd3 --- /dev/null +++ b/src/lib/images/jumbotron-images.js @@ -0,0 +1,29 @@ +import generatedJumbotronImages from "./jumbotron-generated-images.json" + +function buildGeneratedImagePath(imagePath, width, format) { + const filename = imagePath.split("/").pop() + const basename = filename.replace(/\.[^.]+$/, "") + return `/img/generated/${basename}-${width}.${format}` +} + +function buildSrcSet(imagePath, widths, format) { + return widths + .map(width => `${buildGeneratedImagePath(imagePath, width, format)} ${width}w`) + .join(", ") +} + +export function getJumbotronResponsiveImage(imagePath) { + const imageConfig = generatedJumbotronImages[imagePath] + if (!imageConfig) return null + + return { + alt: imageConfig.alt || "", + sizes: "100vw", + avifSrcSet: buildSrcSet(imagePath, imageConfig.widths, "avif"), + webpSrcSet: buildSrcSet(imagePath, imageConfig.widths, "webp"), + fallbackSrc: imagePath, + placeholderDataUrl: imageConfig.placeholderDataUrl, + } +} + +export { generatedJumbotronImages } diff --git a/src/templates/blog-page-template.jsx b/src/templates/blog-page-template.jsx index c42d5dd..0e8ef5b 100644 --- a/src/templates/blog-page-template.jsx +++ b/src/templates/blog-page-template.jsx @@ -1,13 +1,17 @@ /* eslint-disable react/prop-types */ 'use client' import React, { useEffect, useState } from "react" +import { useSearchParams } from 'next/navigation' import { debounce } from "../lib/performance/throttle" import CardTable from "../components/thumbnail/card-table" +import useBlogPostCards from "../components/others/use-blog-post-cards" import PageContainer from "../components/page-scroll/container" import Section from "../components/page-scroll/section" import HeaderContainer from "../components/header/header-container" import Footer from "../components/footer/footer" import ParallaxSection from "../components/sections/parallax-section" +import { setComponentState } from "../lib/contexts/use-restore-component-state" +import useLayoutContext from "../lib/contexts/use-layout-context" import { calcViewportHeight } from "../lib/dom/viewport" function BlogPagePreview({ jumbotronProps }) { @@ -19,8 +23,46 @@ function BlogPagePreview({ jumbotronProps }) { ) } -function BlogPageTemplate({ - jumbotronProps, +function BlogPageFrame({ jumbotronProps, children }) { + return ( +
+ +
+ +
+ {children} +
+
+
+
+
+ ) +} + +function BlogCardsLoadingSection() { + return ( + + ) +} + +function BlogCardsSection({ uri, blogRollData, tableName = "blogTable", @@ -46,31 +88,55 @@ function BlogPageTemplate({ }, []) return ( -
- -
- -
- -
-
-
-
-
+ + ) +} + +function BlogPostsSection({ posts, uri, tableName = "blogTable" }) { + const searchParams = useSearchParams() + const tags = searchParams.get('tags') + const context = useLayoutContext() + + if (tags) { + setComponentState([uri, tableName, "keywords"], tags, context) + } + + const blogRollData = useBlogPostCards(posts) + + return ( + + ) +} + +function BlogPageTemplate({ + jumbotronProps, + uri, + blogRollData, + tableName = "blogTable", +}) { + return ( + + + ) } -export { BlogPageTemplate, BlogPagePreview } +export { BlogCardsLoadingSection, BlogCardsSection, BlogPageFrame, BlogPageTemplate, BlogPagePreview, BlogPostsSection } export default BlogPageTemplate