# Какво е компетентност? Компетентността показва, колко трябва да сте на ти с технологиите в статията. - [**Няма**](https://thoughts.bizarre.how/bg/competence/none): Не се изисква технически опит; - [**Базова**](https://thoughts.bizarre.how/bg/competence/elementary): `Getting Started` го знаете наизуст; - [**Про**](https://thoughts.bizarre.how/bg/competence/pro): Ползвате технологията и целия й инструментариум; - [**Маниак**](https://thoughts.bizarre.how/bg/competence/geek): Имате повече отговори, от колкото въпроси; Може да си мислите за нея, като за категория на статията. Една статия има само една компетентност/категория. ## Какво са таговете? Таговете, или както още етикети, са лесен начин да филтрирате статиите, които виждате, в дадена област, технология, идея и т.н. Най-често пиша за дадена технология или съвкупност от технологии. Тогава таговете оказват, кои са тези технологии. Понякога няма да не са свързани с технологии, а с моите мисли или интересни неща от мрежата или заобикалящата ме среда, които отбелязвам. ## Кои са технологиите? Основните технологии, за които пиша са насочени повече към front-end, но щe има и за back-end, и за инструментите, и за облаците, и за моделите, и за добрите практики и още други. [VueJS](https://vuejs.org/){rel="nofollow"}, [Nuxt](https://nuxt.com){rel="nofollow"}, [TailwindCSS](https://tailwindcss.com/){rel="nofollow"}, [TypeScript](https://www.typescriptlang.org/){rel="nofollow"}, [NodeJS](https://nodejs.org/){rel="nofollow"}, [NPM](https://www.npmjs.com/){rel="nofollow"}, [Vite](https://vitejs.dev/){rel="nofollow"}, [Parcel](https://parceljs.org/){rel="nofollow"}, [ExpressJS](https://expressjs.com/){rel="nofollow"}, [MongoDB](https://www.mongodb.com/){rel="nofollow"}, [GraphQL](https://graphql.org/){rel="nofollow"}, [Firebase](https://firebase.google.com/){rel="nofollow"}, [PM2](https://pm2.keymetrics.io/){rel="nofollow"}, [NginX](https://www.nginx.com/){rel="nofollow"} и др... ## Кой съм аз ли? > I am a Front-end Senpai, who strictly follows the W3Code of Bushido!. Обичам да използвам тази фраза. А иначе, аз съм **Коста**, още познат като **HowBizarre**, и заедно със семейството си работя и живея в **София**, **България**. Може да разгледате профилите ми в [GitHub](https://github.com/howbizarre){rel="nofollow"}, [X](https://x.com/howbizarre){rel="nofollow"} и [LinkedIn](https://www.linkedin.com/in/howbizarre){rel="nofollow"}. ## И малко за Изкуствения Интелект [Кратък индекс](https://thoughts.bizarre.how/llms.txt){rel="noopener"} на сайта, за да подпомогне Езиковите модели. :br [Разширен индекс](https://thoughts.bizarre.how/llms-full.txt){rel="noopener"} на сайта, който включва и съдържанието на статиите. # What is competence? Competence shows how familiar you should be with the technologies mentioned in the article. - [**None**](https://thoughts.bizarre.how/en/competence/none): No technical experience required; - [**Elementary**](https://thoughts.bizarre.how/en/competence/elementary): You know `Getting Started` by heart; - [**Pro**](https://thoughts.bizarre.how/en/competence/pro): You use the technology and its toolings; - [**Geek**](https://thoughts.bizarre.how/en/competence/geek): You have more answers than questions; You may think of it as the category of the article. An article has only one competence/category. ## What are tags? Tags, are an easy way to filter the articles you see in a given field, technology, idea, etc. I often write about a given technology or a set of technologies. Then the tags indicate what these technologies are. Sometimes they won't be related to technology, but to my thoughts or interesting things from the network or the life around me that I note. ## What are the technologies? The main technologies I write about are mostly focused on front-end, but there will also be content about back-end, tools, clouds, patterns, best practices, and more. [VueJS](https://vuejs.org/){rel="nofollow"}, [Nuxt](https://nuxt.com){rel="nofollow"}, [TailwindCSS](https://tailwindcss.com/){rel="nofollow"}, [TypeScript](https://www.typescriptlang.org/){rel="nofollow"}, [NodeJS](https://nodejs.org/){rel="nofollow"}, [NPM](https://www.npmjs.com/){rel="nofollow"}, [Vite](https://vitejs.dev/){rel="nofollow"}, [Parcel](https://parceljs.org/){rel="nofollow"}, [ExpressJS](https://expressjs.com/){rel="nofollow"}, [MongoDB](https://www.mongodb.com/){rel="nofollow"}, [GraphQL](https://graphql.org/){rel="nofollow"}, [Firebase](https://firebase.google.com/){rel="nofollow"}, [PM2](https://pm2.keymetrics.io/){rel="nofollow"}, [NginX](https://www.nginx.com/){rel="nofollow"} and more... ## Who am I? > I am a Front-end Senpai, who strictly follows the W3Code of Bushido!. I like to use that phrase. Otherwise, I am **Kosta**, also known as **HowBizarre**, and together with my family, I work and live in **Sofia**, **Bulgaria**. You can check out my profiles on [GitHub](https://github.com/howbizarre){rel="nofollow"}, [X](https://x.com/howbizarre){rel="nofollow"} and [LinkedIn](https://www.linkedin.com/in/howbizarre){rel="nofollow"}. ## FYI For translation from my native language, Bulgarian, to English, I use some AI helpers such as **DeepL**, **CoPilot**, **Gemini**, and **Google Translate** with corrections, and adjustments. I am not a native English speaker, so please let me know if you find any mistakes. Thank you! ## A bit about Artificial Intelligence [Short index](https://thoughts.bizarre.how/llms.txt){rel="noopener"} of the site to assist Language Models. :br [Extended index](https://thoughts.bizarre.how/llms-full.txt){rel="noopener"} of the site, which includes the content of the articles. # Здравей Свят Преди много време спрях да поддържам блог. В момента пиша разни статии към репозиторитата в **GitHub**, но те не целят да достигнат крайния потребител. Повече са за хора, които преминават на бързо за информация и не се старая много, защото са насочени към разбиращите технологиите, за които пиша. Започвам този блог, за да споделя опита ми с един прекрасен стек от технологии, с които може да се постигне всичко, е... почти всичко. [Vue](https://vuejs.org/){rel="nofollow"}, [Nuxt](https://nuxt.com/){rel="nofollow"}, [TailwindCSS](https://tailwindcss.com/){rel="nofollow"} и [TypeScript](https://www.typescriptlang.org/){rel="nofollow"}. Това ще бъдат основните теми на в размислите ми. Силно насочени към front-end и допълнени с beck-end. Струва ми се, че общността около тези технологии в **България** е малка и се надявам с времето да подпомогна нейното развитие. ## Vue Ако изключим **jQuery** - Някъде около 2012 година започнах да използвам "реактивни" JavaScript библиотеки. Една от първите беше [Knockout](https://knockoutjs.com/){rel="nofollow"}. Велика. Всеки стартиращ с Observable модела, и изобщо, всеки стартиращ със "реактивните" JavaScript библиотеки трябва да мине през нея. След това се наредиха още много - в това число и **Angular** и **React**. Дори за кратко пишех и моя собствена, базирана на jQuery и **Mustache**. Накрая попаднах на **Vue**. По онова време пишех много **CSS** и използвах **ASP.NET** и **Razor** за създаване на front-end. Използвах и доста CSS библиотеки, като **960.gs**, **Bootstrap**, **Foundation** и др. и Vue някак си естествено навлезе в ежедневието ми със сепарираното писане в компонентите наподобяващо до някъде модела на организация на файловете с който бях свикнал. Когато добави и Vue Router и Pinia (Vuex преди нея) и картинката става още по-добра. **Vue + Vue Router + Pinia = MVC** in front-end. Ще напиша допълнително как изграждам MVC (Model View Controller) с тях. ## Nuxt **Nuxt** е базиран на Vue. Освен множеството улеснения, които дава, като автоматични импорти, автоматичен рутинг, плъгини, модули и т.н. - добавя и back-end сървър (**Nitro**) и работите като с една система, без да е необходимо да създавате второ приложение за сървър, без да са необходими супер познания по работата на Node & Express. Това е инструмента, който махна .NET & C# от полезрението ми. С Nitro можете да изградите сървърен middleware, API endpoints, връзка към бази с данни - всичко, което Ви е необходимо от back-end. ## TailwindCSS След Just-in-Time Mode - **TailwindCSS** замени изцяло Bootstrap и не само. Вече не ползвам масивни CSS библиотеки с включени UI компоненти. Сепарирам двете отделно. Дори компонентните библиотеки също са изградени с TailwindCSS. Може би в последно време залагам повече на **Nuxt UI** и то, предимно, за да поддържам екосистемата. ## TypeScript **JavaScript** дава огромна свобода на писане, деклариране, извикване, навързване, паралелност, асинхронност и т.н. Има изградени патърни за всякакви аспекти от програмните модели. Ползваш го за beck & front едновременно. Има доста масивни организации и огромна общност, които го развиват. Но тази свобода има и своите недостатъци. Няма компилатор, който да те пази. Няма единен модел за дебъгване. Няма правилен начин за генериране на крайния/проекционния код. **TypeScript** подпомага премахването на някои от проблемите на JavaScript. Не е панацея и понякога не е лесен за конфигуриране, особено когато работите със споделени модели от данни между front & back end, но дава една много по-правилна представа за пътя на разработка, поддръжка и доставяне на кода. ## The others **Vite**, **Node**, **Express**, **MongoDB**, **NPM**, **Firebase** допълват сегашния ми стек от технологии. Vite e личния ми избор за разработка. И не само за Vue проекти. Понякога използвам **Parcel**, но за специфични решения. Работя и с другите от "metro" технологиите, като **Nest**, **ElectonJS** & **React Native**, **Bun** и т.н. но повечето са за малки или лични проекти. --- ::comments :: # Google Fonts в Nuxt с TailwindCSS Услугата на Google за шрифтовете е страшно удобна за използване. Има голям избор от шрифтове и лесен начин да ги филтрираш спрямо нуждите ти. В екосистемата на Nuxt има много добър модул за интегриране на Google шрифтове в приложението Ви, но аз ще Ви покажа малко по-различен подход. В [конфигурацията](https://nuxt.com/docs/api/nuxt-config#head){rel="nofollow"} си Nuxt позволява да обработваме **Head** парчето от HTML-а. Има и други места на които може да го правите, но за целта ще използвам конфигурационния файл `nuxt.config.ts`. Когато изберете шрифт от Google, той Ви предоставя код, който да добавите към приложението си. Кода има следния вид: ```html ``` За да добавите това парче от код в `nuxt.config.ts` трябва да го разделите на части в `link` масива в конфигурацията. ```typescript //nuxt.config.ts export default defineNuxtConfig({ app: { head: { link: [ { rel: "preconnect", href: "https://fonts.googleapis.com" }, { rel: "preconnect", href: "https://fonts.gstatic.com", crossorigin: "" }, { rel: "stylesheet", href: "https://fonts.googleapis.com/css2?family=Roboto&display=swap" } ] } } ``` Изключително просто и елегантно решение. При билдването горния код се инжектира в Head-а на приложението Ви и шрифтовете се зареждат от Google. Това работи прекрасно, но понякога не е достатъчно. Например, ако генерирате приложението си статично с `npx nuxt generate`. Тогава е добре да помислите как да оптимизирате зареждането на шрифтовете, защото може да достигнат доста големи обеми. Става лесно, като първоначално променим стойността на `rel` атрибута и след извикването на `onload` събитието го възстановяваме. ```typescript //nuxt.config.ts export default defineNuxtConfig({ app: { head: { link: [ { rel: "preconnect", href: "https://fonts.googleapis.com" }, { rel: "preconnect", href: "https://fonts.gstatic.com", crossorigin: "" }, { rel: "preload", as: "style", onload: "this.onload = null; this.rel = 'stylesheet';", href: "https://fonts.googleapis.com/css2?family=Roboto&display=swap" } ] } } ``` След като шрифта е зареден трябва да го популяризираме в приложението. В моя случай използвам **TailwindCSS**. TailwindCSS Ви позволява да използвате предварително подготвени [фамилии от шрифтове](https://tailwindcss.com/docs/font-family){rel="nofollow"}, но са предоставили и лесен начин да ги преконфигурирате в конфигурационния файл `tailwind.config.js`. ```javascript // tailwind.config.js /** @type {import('tailwindcss').Config} */ export const theme = { fontFamily: { "sans": ["Inter", "sans-serif"], "serif": ["Playfair Display", "serif"], }, }; ``` От тук на там CSS класът `font-sans` ще рисува текста Ви с **Roboto** шрифта. --- ::comments{discussions="https://github.com/howbizarre/thoughts/discussions/6"} :: # Наследството на Бари Моето семейството имаше куче. Казваше се **Бари Наит**. Викахме му още **Баритош**, **Баритошев**, **Баритошко**, **Торонгаш**, **Пальок**, **Черньо**, **мр. Президент** и др. Черен среден пудел. Около 12 килограма. Живя точно 3 години. Почина на рождения си ден. Нашето прекрасно пале вече го няма, но то ни остави наследство, което ще споделя с всички Вас, попаднали на тази страница. Сутрин, когато се будехме **Бари** идваше при нас. Въртеше опашка, подскачаше по кревата, донасяше играчка и се радваше безкрайно много. Радваше се, че денят започва и той ще е с нас. За него беше без значение, как е минал предния ден, как е минала нощта - радостта му беше безгранична всяка една сутрин. Ако искахме да станем от кревата, трябваше да му върнем порядъчна доза гушкане, игра и радост. Ако някой от нас изчезнеше от полезрението му, дори за 5 мин., когато се върнеше започваше бурно посрещане. Радостно джафкане, подскачане, въртене на опашката и подкана, да му отдадеш внимание, любов и радост. И за да продължим с ежедневните си задължения, трябваше да му отделиш достатъчно внимание, любов и радост. Когато станехме прекалено сериозни или заети със задължения, той идваше при нас. Подбутваше ни с носле или с лапа, за да привлече вниманието ни. И ако успееше, с безкрайна радост носеше, някоя от любимите му играчки и започваше весела игра. Ако пък не успееше, не се тревожеше - лягаше до/върху нас и се сгушваше в очакване. Има още безкрай подобни истории с нашето пале. Можем да разкажем 1001 вероятно, а може и повече. Но това, което се случваше с нас, когато **Бари** е наоколо, са вулканите с радост и любов които изригваха от сърцата ни - и не само към него - към всичко заобикалящо ни. --- Ето защо пиша тази няколко реда, за да Ви споделя, че не **Бари** поставяше в нас радостта и любовта. Той само ни учеше как да я откриваме и да я споделяме. И сега, когато го няма, Ви споделям наследството, което ни остави: > Всяко същество на тази земя се ражда с вулкан от радост и любов в сърцето си. Не е лесно да ги активираме, и на много от нас са ни необходими проводници, като **Бари**, но те са там. Изпитвайте радост, когато видите любими хора - не пропускайте никога. Изпитвайте радост сутрин, все едно ги е нямало цяла вечност. Показвайте им любов и за всичко, което правят. Прегръщайте ги, целувайте ги и им се радвайте. Вулканите в сърцата ни са безкрайни. Те никога няма да свършат. Довиждане наше малко пале. Ще се видим в безкрайни полета - и да знаеш, ще донесем зелената гума. --- ::comments :: # Една входна точка за множество сайтове Имаме приложение, което представлява микросайт и когато го заредите, виждате страница за вход. Нашите клиенти го предоставят на своите потребители и след като потребител влезе, се зареждат данни, свързани, както с клиента, към който принадлежат, така и с правата, зададени му от нашия клиент. С развитието на приложението, клиентите ни започнаха да искат микросайта да заработи на техен домейн. Да носи техния бранд. Да се добави "богато" съдържание към иначе "постния" екран за вход. Да се разширят функционалностите му - а повечето от исканите функционалности бяха силно персонализирани. Пред нас имаше 2 пътя - да разделим приложението за всеки клиент и в него да добавим всичко, от което клиента има нужда, или да добавим допълнителна конфигурация, която да позволи на приложение да се персонализира. И при двата варианта, сървърната back-end част нямаше да се променя. Само front-end частта щеше да се персонализира спрямо клиента. Бързо отхвърлихме първия вариант. Ние сме малък екип и поддръжката на множество инсталации и версии щеше да отнема ресурс, който не искахме да отделяме. Затова се насочихме към една входна точка за всички клиенти и различни конфигурации за всеки. Наричаме го **ПРОФИЛИ**. --- ## Профили > Как да разпознаваме профила? Това беше първия въпрос, който стоеше пред нас. Когато потребител зареди приложението то трябва още на входния екран да започне да показва различни шрифтове, лого, картинки за фон и др. Веднага трябваше да знае, дали клиента поддържа повече от един език, дали се зареждат допълнителни контроли, като регистрация, форма за обратна връзка и т.н. Тогава решихме, че на базата на домейна, който извикваше приложението ще казваме на *beck-end*, кой е профила. Това обаче доведе до малка, но неприятна, флуктуация на приложението, защото зареждаше нещо, общо за всички и след като *beck-end* получи, кой е домейна, извършвахме *postback* пренасочване на приложението към конкретния профил. За да избегнем това премигване - изнесохме логика във *front-end* частта на приложението. В `main.ts` файла се обръщаме към *beck-end*, да ни върне идентификатора на профила. ```typescript // main.ts const profileId: string = await whatMyProfileIs("api/profile/id"); ``` Само това - свръх бърза операция, която връща един стрингов идентификатор. Няма *postback*, няма 301 пренасочване, няма излишни заявки към *beck-end*. Как *beck-end* разбира кой е контекста ще ви разкажа някой друг път. Следващата стъпка е да активираме профила в приложението. Само да добавя - към приложението има административна част, която ни позволява, а донякъде и на клиента, да се конфигурира профила. Така че като получим идентификатора на профила да зареждаме конфигурацията му. Използваме функция в `main.ts` файла. ```typescript // main.ts async function loadClientConfiguration(configStore: string): Promise { try { const response = await fetch(configStore); const config = await response.json(); return config; } catch (error) { Logger.fatal("Failed to load client configuration", error); } } ``` След което зареждане конфигурацията и я подаваме на приложението. ```typescript // main.ts const config = await loadClientConfiguration(`/tote/${profileId}.json`); Object.freeze(config); app.provide("clientConfig", config); app.provide("clientId", profileId); ``` Това е цялата логика по зареждането на профила и популяризирането му в приложението. ## Рутинг Всеки профил, освен стандартния, зарежда и собствен рутинг. С него лесно може да активираме и допълнителни плъгини към *Vue* енджина. В стандартния рутер добавяме добавяме допълнителния рутер и филтрираме спрямо профила. След което го добавяме към основния рутинг. ```typescript // router/index.ts import { customRoutes } from "@/router/custom"; const customRoute: Array = clientId ? customRoutes[clientId] : []; if (customRoute?.length > 0) { routes.push(...customRoute); } const history = createWebHashHistory(); const router = createRouter({ history, routes }); ``` Лесно за добавяне, лесно за поддръжка. Дава свобода за разширяване, за всеки профил, независимо от останалите. Работи бързо и без проблемно. Малко опростих примера, но дава добра представа как може да разширите начина на използване на *Vue Router*. При тази реализация се натъкнахме на малък проблем. При стандартното активиране на *Vue енджина* в `main.ts` добавяме следните редове: ```typescript // main.ts import { createApp } from "vue"; import App from "@/App.vue"; import router from "@/router"; import { whatMyProfileIs } from "@/endpoints/profile"; import Logger from "@/system/fs/workers/logger"; const profileId: string = await whatMyProfileIs("api/profile/id"); const config = await loadClientConfiguration(`/tote/${profileId}.json`); Object.freeze(config); const app = createApp(App); app.provide("clientConfig", config); app.provide("clientId", profileId); app.use(router); app.mount("#app"); async function loadClientConfiguration(configStore: string): Promise { try { const response = await fetch(configStore); const config = await response.json(); return config; } catch (error) { Logger.fatal("Failed to load client configuration", error, { "predef": 500 }); } } ``` По този начин Vue Router & Vue App се зареждат независимо и профила не достига до рутера на време. Затова направихме допълнителна ф-ия и там асинхронно зареждаме рутера. ```typescript // main.ts import { createApp } from "vue"; import App from "@/App.vue"; import { whatMyProfileIs } from "@/endpoints/profile"; import Logger from "@/system/fs/workers/logger"; BigBang(); async function BigBang(): Promise { const profileId: string = await whatMyProfileIs("api/profile/id"); if (profileId) { const app = createApp(App); const config = await loadClientConfiguration(`/tote/${profileId}.json`); Object.freeze(config); const { default: router } = await import("@/router"); app.use(router); app.provide("clientConfig", config); app.provide("clientId", profileId); await router.isReady(); app.mount("#app"); } } async function loadClientConfiguration(configStore: string): Promise { try { const response = await fetch(configStore); const config = await response.json(); return config; } catch (error) { Logger.fatal("Failed to load client configuration", error, { "predef": 500 }); } } ``` Цялото решение за една входна точка и множество сайтове заработи с тези няколко простички изменения. --- ::note Изпускам част от имплементацията на обектите в статията - те не са съществени, а служат само да подскажат каква бизнес логика стои зад тях. :: --- ::note "Използвам думата **back-end** доста примитивно. Чудех се, дали да не е просто сървър, но при нас това, не е съвсем правилно определение. :: --- ::comments{discussions="https://github.com/howbizarre/thoughts/discussions/7"} :: # Parcel & Robots Има различни начини, да окажете на *ParcelJS*, кои файлове са статични и не трябва да преминават през билд трансформацията, но ако са малък брой, можете да активирате `transformers` плъгина и след това да ги окажете директно в билд скрипта в `package.json` файла. ```json // package.json { "scripts": { "build": "parcel build src/index.html src/robots.txt src/favicon.ico" } } ``` По този начин `robots.txt` и `favicon.ico` няма да бъде обработван от [ParcelJS](https://parceljs.org/){rel="nofollow"} и ще бъде директно прехвърлен в билд директории. ## Transformers плъгина За да заработи правилно горния билд скрипт, трябва да добавите в `.parcelrc` файла следния код: ```json // .parcelrc { "extends": "@parcel/config-default", "transformers": { "*.{txt,ico}": ["@parcel/transformer-raw"] } } ``` *ParcelJS* не само, че няма да добави хешове към имената, на файловете, но и линкнантите обектите във файловете, които се процесват няма да бъдат променени. --- ::comments{discussions="https://github.com/howbizarre/thoughts/discussions/8"} :: # Vue emits с параметри Предаването на събитията във **Vue** от компонент, обратно към този, който го извиква става с **emits**. Предаването може да се направи и с **Pinia** или друга библиотека за управление на 'състоянието', но това ще е за някоя друга статия. ## Кое е страница и кое е компонент? Много често ме питат: '*Този код да го изкарам ли в компонентите или да го оставя в страницата?*'. Изградил съм си едно основно правило - ако ми изниква въпроса, дали този код да стане компонент, значи трябва да стане компонент. Самите компоненти също ги групирам по определени х-ки. Много често давам за пример [@layer](https://tailwindcss.com/docs/functions-and-directives#layer){rel="nofollow"} директивата на **TailwindCSS**, когато някой се чуди как да си групира компонентите. Компонентите ги разделям на 2 вида: 1. **Спекуланти**: Такива, които не изпълняват никаква бизнес логика, а само рисуват и/или трансферират данни; 2. **Работници**: Такива, които извършват вторична обработка на входящите параметри и добавят бизнес логика, която връщат към извикващия ги компонент. **Работниците** много често може извикват други **работници** или **спекуланти**, докато **Спекулантите** обикновено работят самостоятелно. ## Как се предават параметри? Нека си направим един пример. Имаме компонент (**Спекулант**), който показва на екрана, колко още страници с продукти остават, преди да се достигне последната страница. Кръщаваме го `LoadMore.vue`. Входните параметри на компонента са 2: '**кой номер е текущата страница**' и '**колко е общия брой страници**'. Компонента не извършва никаква бизнес логика. Пресмята колко още страници остават и ги рисува. ```vue // components/utilities/LoadMore.vue ``` ### Директно предаване на събитие Нека да преработим малко нашия компонент, така че като се натисне да предава събитието към извикващия компонент, който да пали функция. ```vue // components/utilities/LoadMore.vue ``` Това е директен начин за предаване на събитието. Когато се натисне бутона, се предава събитието `loadMore` и параметъра `page` към извикващия компонент. ```vue // pages/Products.vue ``` Винаги ползвам **kebab-case** за емитнатите събитията. ### Дефиниране на emit Когато искате да направите някакво изменение на параметрите, които предавате, преди да емитнете събитието, ще трябва да дефинираите emit-а. ```vue // components/utilities/LoadMore.vue ``` Така вече имаме много голям контрол върху изходните параметри. ```vue // pages/Products.vue ``` Този пример е само показателен. Ако се прави подобно изменението на номера на страницата, то трябва да е логика в извикващия компонент. Но за нашия пример е прекрасен начин да видите, как може да поемете контрола на връщаните данни. --- ::comments{discussions="https://github.com/howbizarre/thoughts/discussions/9"} :: # Nitro, i18n and Dev Proxy Когато ползвате **Nuxt**, нормално е да използвате и **Nitro**, но понякога това не влиза в сценария, който са Ви подготвили. За API заявките отговаря друг beck-end сървър и за да може да си разработвате локално приложението, трябва да се настрои прокси. Nitro имат описание в документацията, как да настроите **devProxy**, и всичко работи добре, докато не се сблъсках с Nuxt приложение с активен **i18n** модул с няколко езикови локализации и настроен в режим (стратегия) **prefix**. При префиксната стратегия, URL адреса се подменя автоматично и devProxy спира да работи. ```typescript // nuxt.config.ts export default defineNuxtConfig({ nitro: { devProxy: { "/~": { target: "http://127.0.0.1:3243/", }, }, }, }); ``` В нашия случай, всички заявки, които започват с `/~`, например: `/~/api/request/method`, Nitro ги пренасочва към другия beck-end сървър. Но когато в адреса ненадейно се добави и езиковата култура `/en/~/api/request/method` и Nitro спира да комуникира с др. сървър. Затова набързо обогатихме конфигурацията на devProxy. ```typescript // nuxt.config.ts export default defineNuxtConfig({ nitro: { devProxy: { "/bg/~": { target: "http://127.0.0.1:1818/", }, "/en/~": { target: "http://127.0.0.1:1818/", }, "/~": { target: "http://127.0.0.1:1818/", }, }, }, }); ``` Идеята беше да потвърдим, че това така ще работи, но и до момента не съм открил друг начин. Проблема идва в добавянето на нова езикова локализация. Всяка една трябва да се добавя в конфигурацията на devProxy. Драснете ако знаете по-културен начин. --- ::comments{discussions="https://github.com/howbizarre/thoughts/discussions/10"} :: # Динамичен manifest.json Ако желаете потребителите на вашето уеб приложение да го „инсталират“ на своите устройства, достатъчно е да попълните `manifest.json` файла. Има широка поддръжка от всякакви браузъри и операционни системи и е елементарно да се направи, за да се пропуска. Поддържам едно **Vue** приложение, което комуникира с **SaaS** система. Приложението се генерира почти статично, без да има beck-end зад себе си и има една входна точка, но в зависимост от сайта, които я зарежда, в **LocaleStorage**-а записвам променлива, която съдържа идентификатор на този сайт. Приложението си има изграден `manifest.json`, но с времето се наложи да го персонализирам за всеки един сайт. Динамичното зареждане на `manifest.json` файла трябва да стане при клиента (в браузъра). **Vue & Vue Router** ги изключихме от сметките, защото манифеста трябва да е наличен, дори и техните 'машинки' да не заработят. Така че, прехвърлихме всичко в `index.html` файла на приложението. Написах малка **Node** конзола, която преминава през активните бази и генерира статично `manifest.siteId.json` файл за всеки един сайт в определена папка. След това добавих един малък скрипт в `index.html` файла, който зарежда този файл и го добавя към `document.head`. ```html ``` ## Защо fetch? Това е един много добър въпрос. При генерирането на `manifest.siteId.json` файловете, може да възникне ситуация, при която няма да съществува такъв файл. Понеже **JavaScript**-а, не може да прави проверка за файл на сървъра - правя **fetch** заявка за него. Ако заявката не сработи, тогава зареждам `manifest.json` файл с базовите описания. --- ::comments{discussions="https://github.com/howbizarre/thoughts/discussions/11"} :: # Премахване на наклонената черта от адресната линия в Nuxt Всяка страница във Вашия уеб сайт трябва да е с уникално съдържание. Ако имате втора страница с същото съдържание, то тежестта на информацията за търсачките се разделя между двете и това силно намалява шансовете им за по-ранно показване в резултатите от търсенето. Ако имате страница, която се зарежда на адрес `https://example.com/page` е много възможно същата страница да се зарежда и на адрес `https://example.com/page/`. От наша гледна точка, това е една и съща страница, но за търсещите машини, това са два различни адреса. Съответно очакват да има различно съдържание на тях. Едни от начините да окажем на търсачките, кое съдържание трябва да се отчита, са каноничните адреси на страниците. Аз ще обърна внимание на друг - да кажем на търсачките, че 'грешния' адрес е преместен. Както разбирате, това трябва да стене, още преди обхождащата машина да зареди страницата ни. В **Nuxt** това може да стане с един малък **middleware**. ```typescript // middleware/remove-trailing-slash.global.ts export default defineNuxtRouteMiddleware((to) => { if (to.path === "/" || !to.path.endsWith("/")) return; // --> const removedSlash = to.path.replace(/\/+$/, "") || "/"; const seoRoute = { path: removedSlash }; return navigateTo(seoRoute, { redirectCode: 301 }); }); ``` На middleware му добавяме **global** в името, за да се изпълнява преди зареждането на всяка една страница, без да е необходимо страниците да го викат изрично. --- ::comments{discussions="https://github.com/howbizarre/thoughts/discussions/12"} :: # Логване на информация с Web Workers от Vue 3 към сървъра **Web Workers** са малки, но мощни скриптове, които работят на 'заден план' в браузъра. И понеже не пречат на рендирането на приложението Ви, може да ги товарите с разни задачи, които да изпълняват. Ползвам Web Workers по два начина - В първия не връщат информация обратно към приложението и след свършване на работа се самоунищожават. При втория връщат резултат и тогава приложението се грижи, дали и кога Web Worker-а да се унищожи. Тук ще разгледаме само първия начин на работа, а за втория ще напиша отделна статия. ## LOG WORKER Най-често използвам първия начин на работа Web Workers за логване на информацията от приложението към сървъра, но не само. Създавам директория, която се казва `workers` и в нея създавам файл - в нашия случай `LOGS.ts`. Файла винаги е с главни букви, за да го разпознавам лесно при импорта и е с говоримо име. С времето съм възприел, че всеки вид задача, която изпълнява даден Web Worker, трябва да е в отделен файл. ```typescript // workers/LOGS.ts import axios from "axios"; self.onmessage = async (event) => { const { message, code, type } = event.data; await axios.post("/api/log", { type: `${message}`, statusCode: code }, { contentType: "text/plain" }); /** * Log Worker does not return data, * so we close the worker after the POST request, * even if there is an error with writing to the server, * the worker will close. */ self.close(); }; ``` За да използвате Web Workers във Вашето приложение, много трябва да внимавате с импорта му. За **Vue 3** с **Composition API** и ` ``` --- ::comments{discussions="https://github.com/howbizarre/thoughts/discussions/13"} :: # Electron, TypeScript и Parcel Документацията на **Electron** е изцяло за **JavaScript**, но това не пречи да си използвате **TypeScript**, с който да генерира този JavaScript. Трябва да се спазват няколко простички правила, и то основно в пътищата на зареждане на файловете. Подготвил съм и малко допълнение за front-end частта. Вместо стандартната за Electron HTML страница ще направя малка компилация с **Parcel**. ## Проекта Най-напред ще си организираме проекта. В него ще има 2 подпапки - едната за Electron частта, др. за Browser частта. Създаваме папка `electron-typescript-parcel` и я отваряме във [**VSCode**](https://code.visualstudio.com/){rel="nofollow"} - или който редактор ползвате. Отваряме вградения във VSCode терминал (или друг ако не ползвате VSCode) и изпълнявате: ```bash npm init -y ``` Това ще създаде `package.json` файл в папката `electron-typescript-parcel`. Отваряте файла и редактирате полето `author`. Като начало е достатъчно. Следва да добавим Electron модула в проекта. ```bash npm install --save-dev electron ``` Ако ще ползвате **GIT**, сега е добре да изпълните: ```bash git init ``` и да добавите `.gitignore` файл. В него добавете като начало добавяме `node_modules`. След това добавяме 2 папки - `electron` и `browser` в папката на проекта. Както подсказват имената - в първата ще живее Electron, а във втората - front-end частта за браузъра. ## Electron През терминала влизаме в папката `electron` и изпълняваме: ```bash npm init -y ``` и веднага след това добавяме и TypeScript модула: ```bash npm install --save-dev typescript ``` Зареждаме `package.json`. В **script** частта добавяме `"build": "tsc"` и махаме `"main"` атрибута. ```json // electron/package.json { "name": "electron", "version": "1.0.0", "scripts": { "build": "tsc" }, "keywords": [], "author": "", "license": "ISC", "description": "", "devDependencies": { "typescript": "^5.5.4" } } ``` В конзолата изпълняваме: ```bash npx tsc --init ``` Това ще създаде `tsconfig.json` файл. В него ще трябва да намерите `"outDir"`, да премахнете коментираната час на реда и задавате `"outDir": "../dist"`. От тук следваме стандартните за Electron стъпки за създаване на базово приложение, като изпускаме частта за създаване на `index.html` файла и `renderer.js` файла, които ще добавим чрез Parcel. Добавяме `main.ts` файл в папката `electron` и в него пишем: ```typescript // electron/main.ts import { app, BrowserWindow, ipcMain, nativeTheme } from "electron"; import path from "node:path"; /** * Creates a new window and loads an HTML file. */ const createWindow = (): void => { const mainWindow = new BrowserWindow({ width: 800, height: 600, webPreferences: { preload: path.join(__dirname, "./preload.js"), }, }); mainWindow.loadFile("./dist/index.html"); ipcMain.handle("dark-mode:toggle", () => { if (nativeTheme.shouldUseDarkColors) { nativeTheme.themeSource = "light"; } else { nativeTheme.themeSource = "dark"; } return nativeTheme.shouldUseDarkColors; }); }; app.whenReady().then(() => { createWindow(); app.on("activate", () => { if (BrowserWindow.getAllWindows().length === 0) createWindow(); }); }); app.on("window-all-closed", () => { if (process.platform !== "darwin") app.quit(); }); ``` Това е [примера от Electron документацията](https://www.electronjs.org/docs/latest/tutorial/dark-mode){rel="nofollow"}, който сменя тъмна или светла темата на приложението. След това добавяме `preload.ts` файл в папката `electron` и в него пишем: ```typescript // electron/preload.ts import { contextBridge, ipcRenderer } from "electron/renderer"; contextBridge.exposeInMainWorld("electronAPI", { toggle: () => ipcRenderer.invoke("dark-mode:toggle"), }); ``` ## Browser През терминала влизаме в папката `browser` и изпълняваме: ```bash npm init -y ``` след което инсталираме ***Parcel***: ```bash npm install --save-dev parcel ``` Зареждаме `package.json`. В **script** частта добавяме `"build": "parcel build index.html --dist-dir ../dist --no-source-maps --public-url ./ --no-optimize"`и махаме `"main"` атрибута. Създаваме `index.html` файл в папката `browser` и в него пишем: ```html Hello World!

Hello World!

Current theme source: System

``` Създаваме `styles.css` файл в папката `browser` и в него пишем: ```css /* browser/styles.css */ @media (prefers-color-scheme: dark) { body { background: #333; color: white; } } @media (prefers-color-scheme: light) { body { background: #ddd; color: black; } } ``` Добавяме `render.ts` файл в папката `browser` и в него пишем: ```typescript const toggleDarkMode = document.getElementById("toggle-dark-mode"); const themeSource = document.getElementById("theme-source"); if (themeSource && toggleDarkMode) { toggleDarkMode.addEventListener("click", async () => { // @ts-expect-error const isDarkMode = await window.electronAPI.toggle(); themeSource.innerHTML = isDarkMode ? "Dark" : "Light"; toggleDarkMode.innerHTML = `Toggle ${!isDarkMode ? "Dark" : "Light"} Mode`; }); } ``` За да 'компилираме' typescript файла с Parcel ще добавим в папката `.parcelrc` файл и в него ще напишем: ```json // browser/.parcelrc { "extends": "@parcel/config-default", "transformers": { "*.ts": ["@parcel/transformer-typescript-tsc"] } } ``` ## Старт Връщаме се обратно в папката на проекта и редактираме в `package.json` файла полетата **scripts** и **main** : ```json // package.json { "main": "./dist/main.js", "scripts": { "start": "npm run build --prefix ./electron && npm run build --prefix ./browser && electron ." } } ``` В `.gitignore` файла добавяме `dist` и `.parcel-cache` папките и в командния ред изпълняваме: ```bash npm start ``` След старта на приложението, ще се появи папка `dist` в папката на проекта и в нея ще е целия код на Electron приложението. Направил съм репозитори за този проект в [GitHub](https://github.com/howbizarre/electron-typescript-parcel){rel="nofollow"}. --- ::comments{discussions="https://github.com/howbizarre/thoughts/discussions/14"} :: # Vue Router и Main файла При стартиране на нов **Vue** проект, използвам **Quick Start** секцията на сайта на Vue. След това, задължително, правя няколко малки промени, преди да добавя проекта към Source Control банката. От въпросите, които ми задава `npm create vue@latest` почти винаги си избирам: ```shell √ Add TypeScript? ... no / YES √ Add Vue Router for Single Page Application development? ... no / YES √ Add Pinia for state management? ... no / YES √ Add an End-to-End Testing Solution? » Playwright ``` Другите, ако в процеса на разработка се наложат. След изпълнение на всички инструкции, които Ви излизат по екрана, получавате един готов за стартиране проект. Първото нещо, което редактирам е Main файла - `src/main.ts`. В началото той има следния вид: ```typescript import './assets/main.css' import { createApp } from 'vue' import { createPinia } from 'pinia' import App from './App.vue' import router from './router' const app = createApp(App) app.use(createPinia()) app.use(router) app.mount('#app') ``` Тук има една малка скрита тайна, която вероятно не знаете. Понякога може да се случи, Vue приложението да се зареди, преди да се инициализира рутера. За да избегна появата на подобни Vue jokes, редактирам `src/main.ts`: ```typescript import "./assets/main.css"; import { createApp } from "vue"; import { createPinia } from "pinia"; import App from "./App.vue"; initializeApp(); async function initializeApp(): Promise { const app = createApp(App); const pinia = createPinia(); const { default: router } = await import("@/router"); app.use(router); app.use(pinia); await router.isReady(); app.mount("#app"); } ``` По този начин рутера се инициализира преди Vue приложението да бъде заредено. Една мъничка хитрина, която ще Ви спаси от евентуални бъдещи проблеми. --- ::comments{discussions="https://github.com/howbizarre/thoughts/discussions/4"} :: # Простичък Vue плъгин за гео локация Когато пиша плъгин, мисля за него, като самостоятелно парче код, което има собствена логика на работа и е независимо от мястото, на което ще се ползва. Това разбира се не е съвсем вярно, но при проектирането на плъгин, винаги изхождам от тази идея. Все пак всяка система има своя логика и архитектура, която трябва да се спазва - особено за изходящите данни. ## Гео локация В *съвременните браузъри* е много лесно може да достъпите [APIто за гео локация](https://developer.mozilla.org/en-US/docs/Web/API/Geolocation_API){rel="nofollow"}. Извиквате ```javascript navigator.geolocation.getCurrentPosition(call_success_function, call_error_function); ``` и в обекта, който се връща към `call_success_function` ще получите всичко необходимо, в това число и **latitude** & **longitude**. При необходимост можете да проверите, дали браузъра поддържа APIто за гео локация `if (!navigator.geolocation) { ... } else { ... }`. Ще добавя една малка абстракция към стандартната функция, която да я направи да работи в асинхронен среда. ```typescript async function GeoLocation(): Promise { return new Promise((resolve, reject) => { navigator.geolocation.getCurrentPosition( (success) => resolve(location.coords), (error) => reject(error) ); }); } ``` Чуден метод, който ще върне координатите и (както Ви казах в началото) работи независимо от средата в която ще се ползва. ## Vue плъгин В проект, където искаме да използваме, като плъгин, горния метод създавам папка `plugins` и в нея създавам под-папка `geo-location`. В тази папка добавям 2 файла - `index.ts` и `GeoLocation.ts`. Подразбира се, че съдържанието на `GeoLocation.ts` е горния метод. ```typescript // plugins/geo-location/GeoLocation.ts export async function GeoLocation(): Promise { return new Promise((resolve, reject) => { navigator.geolocation.getCurrentPosition( (success) => resolve(location.coords), (error) => reject(error) ); }); } ``` В `index.ts` спазвам стандартната структура на Vue плъгините. ```typescript // plugins/geo-location/index.ts import type { App } from "vue"; import { GeoLocation } from "./GeoLocation"; export default { install(app: App) { app.provide("GeoLocation", GeoLocation); }, }; ``` След което го добавяме в `main.ts`. ```typescript // main.ts import { createApp } from "vue"; import App from "./App.vue"; import GeoLocationPlugin from "./plugins/geo-location"; const app = createApp(App); app.use(GeoLocationPlugin); app.mount("#app"); ``` ## Използване на плъгина Тук е причината да напиша тази статиика. За да използвате плъгина в **TypeScript** среда, ще трябва да внимавате с инициализацията във Vue компонента, в който ще го ползвате. ```typescript const GEOLocation = inject<() => Promise>('GeoLocation'); ``` Ako пропуснете каста `() => Promise` ще получите грешка, че `GeoLocation` не е функция. --- ::note Може да дефинирате и собствен тип: ```typescript type GeoLocationType = () => Promise; const GEOLocation = inject('GeoLocation'); ``` :: --- ::comments{discussions="https://github.com/howbizarre/thoughts/discussions/15"} :: # HB's Thoughts Малка блог система построен с Nuxt 4, фокусиран върху статии за Vue, Nuxt, TailwindCSS, TypeScript и фронт-енд разработка. ## 📖 За Проекта HB's Thoughts е личен блог със статии главно за Vue, Nuxt, TailwindCSS и TypeScript, но не само - повече фронт-енд и по-малко бек-енд. Блогът поддържа множество езици (английски и български) и е оптимизиран за производителност и потребителско изживяване. ## ✨ Функционалности - **Модерен Технологичен Стек**: Построен с Nuxt 4, Vue 3 и TypeScript - **Многоезична Поддръжка**: Достъпен на английски и български с i18n - **Управление на Съдържание**: Задвижван от Nuxt Content за статии в markdown формат - **Модерен UI**: Стилизиран с Nuxt UI и TailwindCSS - **Функционалност за Търсене**: Пълнотекстово търсене с Fuse.js - **Система за Тагове**: Статиите са организирани по тагове и компетенции - **SEO Оптимизация**: Рендериране от страна на сървъра с оптимизирани мета тагове - **Структурирани Данни**: JSON-LD структурирани данни за блог постове, листинги и навигация - **Облачна Инсталация**: Инсталиран на Cloudflare Workers - **Адаптивен Дизайн**: Mobile-first адаптивен лейаут ## 🛠 Технологичен Стек - **Фреймуърк**: [Nuxt 4](https://nuxt.com/){rel="nofollow"} - **Фронт-енд**: [Vue 3](https://vuejs.org/){rel="nofollow"} с TypeScript - **Стилизиране**: [TailwindCSS](https://tailwindcss.com/){rel="nofollow"} + [Nuxt UI](https://ui.nuxt.com/){rel="nofollow"} - **Съдържание**: [Nuxt Content](https://content.nuxt.com/){rel="nofollow"} за markdown статии - **Интернационализация**: [Nuxt i18n](https://i18n.nuxtjs.org/){rel="nofollow"} - **Търсене**: [Fuse.js](https://fusejs.io/){rel="nofollow"} за размито търсене - **База данни**: Better SQLite3 - **Инсталация**: Cloudflare Workers - **Build Tool**: Vite ## 🚀 Започване ### Изисквания - Node.js (v18 или по-нова версия) - npm или yarn - Wrangler CLI (за Cloudflare инсталация) ### Инсталация 1. Клонирайте хранилището: ```bash git clone https://github.com/hristobotev/hbsthoughts.git cd hbsthoughts ``` 2. Инсталирайте зависимостите: ```bash npm install ``` 3. Стартирайте development сървъра: ```bash npm run dev ``` Сайтът ще бъде достъпен на `http://localhost:7410` ## 📝 Налични Скриптове - `npm run dev` - Стартира development сървър на порт 7410 - `npm run build` - Билдва приложението за продукция - `npm run generate` - Генерира статични файлове - `npm run preview` - Билдва и прегледва с Wrangler - `npm run deploy` - Билдва и инсталира на Cloudflare Workers - `npm run cf-typegen` - Генерира Cloudflare типове ## 📁 Структура на Проекта ```bash ├── app/ # Nuxt app директория │ ├── components/ # Vue компоненти │ ├── composables/ # Vue composables (JSON-LD, utilities) │ ├── layouts/ # Layout компоненти │ ├── pages/ # Page компоненти и routing │ └── assets/ # Статични ресурси ├── content/ # Markdown съдържание │ ├── bg/ # Български статии │ ├── en/ # Английски статии │ ├── seo/ # SEO конфигурации ├── i18n/ # Интернационализация ├── public/ # Публични ресурси └── server/ # Server-side код ``` ## 🌍 Управление на Съдържанието Статиите са написани в Markdown и съхранени в `content/` директорията: - `/content/en/articles/` - Английски статии - `/content/bg/articles/` - Български статии - `/content/en/static/` - Английски статични страници (като help страници) - `/content/bg/static/` - Български статични страници (като help страници) ### Формат на Статия Всяка статия следва тази frontmatter структура: ```markdown --- title: "Заглавие на Статията" date: "2024-02-06T12:01:53.293Z" draft: false tags: ["vue", "nuxt"] slug: "slug-na-statiata" navigation: false competence: "frontend" --- Съдържание на статията тук... ``` ## 🔍 SEO & Структурирани Данни Блогът имплементира цялостна SEO оптимизация със JSON-LD структурирани данни: ### JSON-LD Имплементация Приложението включва три типа структурирани данни използвайки Schema.org речника: 1. **Blog Listing** (`useJsonLdBlogListing`): - Генерира `Blog` схема за страници с листинг на статии - Включва всички статии с техните метаданни - Автоматично се актуализира когато статиите се заредят 2. **Blog Posts** (`useJsonLdBlogPost`): - Генерира `BlogPosting` схема за отделни статии - Включва автор, издател, дати и метаданни на статията - Поддържа опционални featured изображения 3. **Breadcrumbs** (`useJsonLdBreadcrumbs`): - Генерира `BreadcrumbList` схема за навигация - Работи с Nuxt UI breadcrumb компонентите - Обработва многоезични маршрути и динамично съдържание ### Използване JSON-LD composables се импортират автоматично и могат да се използват в която и да е страница: ```vue ``` Всички структурирани данни са реактивни и се актуализират автоматично при промяна на съдържанието. ### Карти на Сайта Блогът генерира карта на сайта за SEO *(Search Engine Optimization)* цели, която включва всички статии и статични страници. Картата на сайта се актуализира автоматично, когато се добави ново съдържание, благодарение на Nuxt Sitemap модула. ## 🚀 Инсталация Приложението е конфигурирано за инсталация на Cloudflare Workers: 1. Конфигурирайте Wrangler: ```bash npm run cf-typegen ``` 2. Инсталирайте: ```bash npm run deploy ``` ## 🤝 Сътрудничество 1. Направете fork на хранилището 2. Създайте feature branch: `git checkout -b feature/amazing-feature` 3. Commit-нете промените си: `git commit -m 'Add amazing feature'` 4. Push-нете към branch-а: `git push origin feature/amazing-feature` 5. Отворете Pull Request ## 📄 Лиценз Този проект е лицензиран под MIT лиценза. ## 🙏 Благодарности - Построен с [Nuxt 4](https://nuxt.com/){rel="nofollow"} - UI компоненти от [Nuxt UI](https://ui.nuxt.com/){rel="nofollow"} - Съдържание от [Nuxt Content](https://content.nuxt.com/){rel="nofollow"} - Многоезична поддръжка с [Nuxt i18n](https://i18n.nuxtjs.org/){rel="nofollow"} - Икони от [Heroicons](https://heroicons.com/){rel="nofollow"} - Шрифтове от [Google Fonts](https://fonts.google.com/){rel="nofollow"} - Back-end [Cloudflare Workers](https://workers.cloudflare.com/){rel="nofollow"} --- ::comments --- discussions: https://github.com/howbizarre/hbsthoughts/discussions/5 --- :: # Cloudflare и Email Binding Не е нужно да имате имейл сървър, за да изпращате писма с Cloudflare Worker. Достатъчно е Cloudflare да валидира ваш имейл адрес, който ползвате и той ще изпрати входящите имейли към него. ## Изисквания Има 3 неща, с които трябва да разполагате във вашия Cloudflare акаунт, за да можете да изпращате имейли: 1. **Домейн** - Трябва да имате активен домейн във вашия Cloudflare акаунт. Това не значи, че трябва да е купен от тях - достатъчно е да настроите DNS записите си, да сочат към Cloudflare. 2. **Имейл** - Трябва да имате активен имейл адрес. Не е нужно да е свързан с домейна от предната точка. Спокойно може да е gmail акаунт, или който и да е друг, до който имате пълен достъп. Това може да е й имейла с който се логвате в Cloudflare. Този имейл трябва да е добавен към *'Destination addresses'* в настройките на *'Email Routing'* на домейна от предната точка, което ще го направи и активен. 3. **Routing rules** - Трябва да имате добавен имейл адрес в *'Routing rules'* на *'Email Routing'*. Този адрес е част от домейна от първа точка и ще се ползва, да препраща писмата към имейла от предната точка. Май го описах малко сложно, но като започнете да добавяте настройките една след друга ще го разберете правилно. ## Binding Към **Nuxt** проекта ви, който използва **Cloudflare Worker**, би трябвало да има `wrangler.jsonc` или `wrangler.toml` файл за конфигурация. Към него трябва да добавите следните настройки: ```json // wrangler.jsonc { "send_email": [ { "name": "INFO_EMAIL", // свободен текст - добре е да има смисъл "destination_address": "your@valid.email" // имейла от 2ра точка горе } ] } ``` Това са всички първоначални настройки на средата, за да заработи машината. Не забравяйте да актуализирате типовете на Cloudflare. ```bash npx wrangler types ``` ## Използване Най-лесно е към **Nitro** средата да добавите един *API endpoint*. Например `/api/send-info-email`. Този endpoint трябва да инициализира Cloudflare binding-а, за да го ползваме директно. Това е най-яката част. Става с един ред: ```js // Достъп до Email binding const env = event.context.cloudflare?.env; ``` И както може би се досещате `env` ви дава пълен достъп до Email binding-а и може да пращате на воля писма. ```js // Изпращане на имейл await env.INFO_EMAIL.send({ sender, recipient, content }); ``` Екстремно просто и адски елегантно решение. **DX** на **MAX**. --- ::comments --- discussions: https://github.com/howbizarre/hbsthoughts/discussions/12 --- :: # Cloudflare Tail Worker С **Cloudflare Tail Worker** насочвате логването на едно място - всичко е в екосистемата и в реално време. До скоро ползвах само външни инструменти като **Sentry**, **LogRocket** или **Loki** с **Grafana**, но подкарването им изисква да познаваш процесинга и отнема време. Много често, когато ползвате *[**SaaS**] (Software as a Service)* услуги нямате достъп до логовете - или ако имате, логовете са силно орязани. Тогава си конфигурирате средата за разработка максимално близка до продукционната и се опитвате да симулирате грешките или създавате процес, на който пращате грешките и от там ги засилвате към външен инструмент. Всеки Worker в Cloudflare има собствени логове, но нямате особен контрол върху тях. А, когато се налага да следите повече от един Worker, спрямо време, в което са се случило някакво събитие във всички тези Workers, нещата загрубяват. Затова си създавате, отделно, един нормален Worker, но `async fetch(request, env, ctx)` функцията я кръщавате `async tail(events, env, ctx)` и вече имате дефиниран *Tail Worker*. ```typescript // src/index.ts export default { async tail(events: TraceItem[], _env: unknown, _ctx: unknown) { if (!events || events.length === 0) { return; } console.log(`[TAIL] Получени ${events.length} събития`); for (const trace of events) { try { handleTraceItem(trace); // Вашата функция за обработка на събитията } catch (error) { console.error(`[TAIL_ERROR] Грешка при обработка на събитие: ${error instanceof Error ? error.message : String(error)}`); } } console.log(`[TAIL] Завършена обработка на ${events.length} събития`); } } satisfies ExportedHandler; ``` Кръщавате Вашия Worker - например: `tail-for-me-all-the-app-events`. Добавяте във `wrangler.jsonc` ред ```json { // ... "observability": { "enabled": true } } ``` и го качвате на Cloudflare. Готово - имате активен Tail Worker. От тук нататък, във всеки един ***Producer Worker***, на който искате да следите логовете, добавяте в неговия `wrangler.jsonc`: ```json { // ... "tail_consumers": [ { "service": "tail-for-me-all-the-app-events" } ] } ``` За момента няма ограничение на броя Producer Workers, които могат да пращат логовете си към даден Tail Worker. Трябва да знаете, че над определено време на ползване на CPU, Cloudflare ще Ви таксува допълнително. Основно следвам 2 правила - Ако два или повече Producer Workers споделят поне една база - ползват общ Tail Worker. - Един Producer Worker ползва повече от един Tail Worker, само ако има дефинирани правила за достъп. --- ::note Създадох един **GitHub Gist** с [пример на `handleTraceItem` функцията](https://gist.github.com/howbizarre/2643b54a2af7c9494f8befe1fd1dd8ba){rel="nofollow"}. :: --- ::comments{discussions="https://github.com/howbizarre/hbsthoughts/discussions"} :: # Динамично зареждане на Vue плъгини в main файл Разработвам В2В приложение, което има една входна точка, но се ползва от различни клиенти с различни домейни, различна визии и собствени функционалности. На базата на домейна и още няколко хитрини, разпознавам коя конфигурация да се зареди и така, макар и една, входната точка и всичко след нея е силно персонализирано. Една от особеностите на Vue е, че плъгините се регистрират глобално и ако имате нужда от различни плъгини в различните конфигурации, ще трябва да зареждате само тези, които са нужни. Това може да стане динамично в main файла. ```ts // main.ts import Vue from 'vue'; const loadPlugin = (pluginName: string): Promise => { return import(`@/plugins/${pluginName}`).then((module) => { Vue.use(module.default); }); }; // Example usage if (client === 'MyPressureClient') { loadPlugin('my-pressure-client-plugin'); } else { loadPlugin('my-regular-client-plugin'); } // ... ``` В този пример функцията `loadPlugin` приема име на плъгин като аргумент и динамично импортира съответния модул на плъгина. До тук добре, но когато имате над 50 плъгина и даден клиент използва около 10 от тях, то схемата с започва да тежи и да изчезва паралелизъма на зареждане. За да се справя с това, създавам един обект, който съдържа масив от плъгини за всеки клиент и зареждам всички наведнъж с `Promise.all`. ```ts const plugins = clientConfig.plugins || {}; const pluginLoaders: Promise<{ name: string; plugin: any }>[] = []; if (plugins?.plugin-one) { pluginLoaders.push(import('@/plugins/plugin-one').then((m) => ({ name: 'PluginOne', plugin: m.default }))); } if (plugins?.plugin-two) { pluginLoaders.push(import('@/plugins/plugin-two').then((m) => ({ name: 'PluginTwo', plugin: m.default }))); } if (plugins?.plugin-three) { pluginLoaders.push(import('@/plugins/plugin-three').then((m) => ({ name: 'PluginThree', plugin: m.default }))); } // ... още плъгини, които клиента ползва, ако има такива // Зареждате всички плъгини паралелно (50-70% по-бързо от последователното) const loadedPlugins = await Promise.all(pluginLoaders); // Регистрирайте плъгините по ред loadedPlugins.forEach(({ name, plugin }) => { app.use(plugin); }); ``` Елегантно и ефективно. **DX** на **MAX** :o ) --- ::comments{discussions="https://github.com/howbizarre/hbsthoughts/discussions"} :: # Hello World I stopped blogging a long time ago. I am currently writing various articles in the **GitHub** repositories, but they are not intended to reach the end user. They're more for people who are just passing through for information and I'm not trying too hard because they're aimed at the tech savvy I'm writing about. I'm starting this blog to share my experience with a wonderful technology stack that can do anything, well...almost anything. [Vue](https://vuejs.org/){rel="nofollow"}, [Nuxt](https://nuxt.com/){rel="nofollow"}, [TailwindCSS](https://tailwindcss.com/){rel="nofollow"} and [TypeScript](https://www.typescriptlang.org/){rel="nofollow"}. These will be the main topics of my reflections. Strongly focused on the front-end and complemented by the back-end. It seems to me that the community around these technologies in **Bulgaria** is small and I hope to help its development over time. ## Vue If we exclude **jQuery** - Sometime around 2012 I started using "reactive" JavaScript libraries. One of the first was [Knockout](https://knockoutjs.com/){rel="nofollow"}. Great one. Anyone starting with the Observable model, and generally anyone starting with "reactive" JavaScript libraries, should go through it. Many more followed after that, including **Angular** and **React**. I even briefly wrote my own based on jQuery and **Mustache**. Finally, I came across **Vue**. At the time, I wrote a lot of **CSS** and used **ASP.NET** and **Razor** to create the front-end. I also used quite a few CSS libraries, such as **960.gs**, **Bootstrap**, **Foundation** and more. and Vue somehow naturally entered my daily life with separated writing in components somewhat resembling the file organization model I was used to. When you add Vue Router and Pinia (Vuex before it) and the picture becomes even better. **Vue + Vue Router + Pinia = MVC** in front-end. I will write further how I build MVC (Model View Controller) with them. ## Nuxt **Nuxt** is based on Vue. Apart from the many facilities it gives like automatic imports, automatic routing, plugins, modules, etc. - also adds a back-end server (**Nitro**) and you work as a single system, without the need to create a second server application, without the need for super knowledge of Node & Express operation. This is the tool that took .NET & C# out of my sight. With Nitro you can build server middleware, API endpoints, database connection - everything you need from the back-end. ## TailwindCSS After Just-in-Time Mode - **TailwindCSS** completely replaced Bootstrap and more. I no longer use massive CSS libraries with included UI components. I separate them. Even the component libraries are also built with TailwindCSS. Maybe lately I'm betting more on **Nuxt UI** and that's mostly to support the ecosystem. ## TypeScript **JavaScript** gives enormous freedom in writing, declaring, calling, binding, concurrency, async, etc. There are a lot of patterns for all aspects of programming models. You use it for beck & front at the same time. There are pretty massive organizations and a huge community developing it. But this freedom also has its drawbacks. There is no compiler to protect you. There is no unified debugging model. There is no correct way to generate the final/production code. **TypeScript** helps alleviate some of the problems of JavaScript. It is not a panacea and sometimes it is not easy to configure, especially when working with shared data models between front-end and back-end, but it provides a much more structured approach to the development, maintenance, and delivery of code. ## The others **Vite**, **Node**, **Express**, **MongoDB**, **NPM**, **Firebase** round out my current technology stack. Vite is my personal choice for development. And not just for Vue projects. Sometimes I use **Parcel** but for specific solutions. I also work with other "metro" technologies like **Nest**, **ElectonJS** & **React Native**, **Bun** etc. but most are for small or personal projects. --- ::comments :: # Google Fonts in Nuxt with TailwindCSS The Google Fonts service is very easy to use. There is a large selection of fonts and an easy way to filter them according to your needs. The Nuxt ecosystem has a very good Google Fonts module you can easily integrate into your app, but I will show you a slightly different approach. In the [config](https://nuxt.com/docs/api/nuxt-config#head){rel="nofollow"} Nuxt allows us to handle the **Head** piece of HTML. There are other places you can do this, but for this, I will use the `nuxt.config.ts` configuration file. When you choose a font from Google, it provides you with code to add to your app. The code looks like this: ```html ``` To add this piece of code to `nuxt.config.ts` you need to split it into parts in the `link` array in the configuration. ```typescript // nuxt.config.ts export default defineNuxtConfig({ app: { head: { link: [ { rel: "preconnect", href: "https://fonts.googleapis.com" }, { rel: "preconnect", href: "https://fonts.gstatic.com", crossorigin: "" }, { rel: "stylesheet", href: "https://fonts.googleapis.com/css2?family=Roboto&display=swap" } ] } } ``` An extremely simple and elegant solution. On build, the above code is injected into the Head of your app and the fonts are loaded from Google. This works great, but sometimes it's not enough. For example, if you generate your app statically with `npx nuxt generate`. Then it is good to think about how to optimize the loading of the fonts because they can reach quite large volumes. It is easily done by initially changing the value of the `rel` attribute and after calling the `onload` event we restore it. ```typescript // nuxt.config.ts export default defineNuxtConfig({ app: { head: { link: [ { rel: "preconnect", href: "https://fonts.googleapis.com" }, { rel: "preconnect", href: "https://fonts.gstatic.com", crossorigin: "" }, { rel: "preload", as: "style", onload: "this.onload = null; this.rel = 'stylesheet';", href: "https://fonts.googleapis.com/css2?family=Roboto&display=swap" } ] } } ``` Once the font is loaded we need to promote it to the app. In my case I use **TailwindCSS**. TailwindCSS allows you to use pre-made [font families](https://tailwindcss.com/docs/font-family){rel="nofollow"}, but they have also provided an easy way to reconfigure them in the `tailwind.config.js` configuration file. ```javascript // tailwind.config.js /** @type {import('tailwindcss').Config} */ export const theme = { fontFamily: { "sans": ["Inter", "sans-serif"], "serif": ["Playfair Display", "serif"], }, }; ``` Now the `font-sans` CSS class will draw your text with the **Roboto** font. --- ::comments{discussions="https://github.com/howbizarre/thoughts/discussions/6"} :: # The legacy of Barry My family had a dog. His name was **Barry Night**. We also called him **Baritosh**, **Baritoshev**, **Baritoshko**, **Torongash**, **Paliok**, **Chernio**, **Mr. President**, and others. A black standard poodle. About 12 kilograms. He lived exactly 3 years. He passed away on his birthday. Our beautiful pal is no longer here, but he left us with a legacy that I will share with all of you who have come across this page. In the morning, when we woke up, **Barry** would come to us. He wagged his tail, jumped on the bed, brought a toy, and was extremely happy. He was happy that the day was starting and he would be with us. It didn't matter to him how the previous day went, how the night went - his joy was boundless every morning. If we wanted to get out of bed, we had to give him a good dose of cuddling, play, and joy. If any of us disappeared from his sight, even for 5 minutes, when we returned, he would start a lively welcome. Joyful barking, jumping, wagging his tail, and inviting us to give him attention, love, and joy. In order to continue with our daily duties, we had to give him enough attention, love, and joy. When we became too serious or busy with our obligations, he would come to us. He nudged us with his nose or paw to get our attention. And if he succeeded, he would bring one of his favorite toys with endless joy and start a playful game. If he didn't succeed, he didn't worry - he would lie down next to or on top of us and snuggle in anticipation. There are countless similar stories with our pal. We could probably tell 1001, maybe even more. But what happened to us when **Barry** was around were the volcanoes of joy and love that erupted from our hearts - not just towards him - towards everything around us. --- That's why I'm writing these few lines, to share with you that it wasn't Barry who placed joy and love in us. He only taught us how to discover and share it. And now, when he's gone, I share with you the legacy he left us: > Every creature on this world is born with a volcano of joy and love in its heart. It's not easy to activate them, and many of us need conductors like **Barry**, but they are there. Experience joy when you see loved ones - never miss it. Experience joy in the morning, as if they haven't been there for an eternity. Show them love for everything they do. Embrace them, kiss them, and rejoice in their presence. The volcanoes in our hearts are infinite. They will never run out. Until soon, our little pal. We will meet in endless fields - and know that we will bring the green ball. --- ::comments :: # One entry point for multiple sites We have an application that represents a microsite, and when you load it, you see a login page. Our clients provide it to their users. After a user logs in, data related to the client they belong to and the permissions assigned by our client are loaded. With the development of the application, our clients started requesting the microsite to work on their own domain. To carry their brand. To add "rich" content to the otherwise "plain" login screen. To expand its functionalities - and most of the requested functionalities were highly personalized. We had two options - to separate the application for each client and add everything the client needs, or to add additional configuration that allows the application to be personalized. In both cases, the server-side *back-end* would not change. Only the *front-end* part would be personalized according to the client. We quickly dismissed the first option. We are a small team and maintaining multiple installations and versions would require resources that we didn't want to allocate. That's why we decided to go with a single entry point for all clients and different configurations for each. We call it **PROFILES**. --- ## Profiles > How do we recognize the profile? This was the first question that stood before us. When a user loads the application, it should start showing different fonts, logos, background images, etc. on the login screen. We immediately needed to know if the client supports more than one language, if additional controls are loaded, such as registration, feedback form, etc. Then we decided that based on the domain that calls the application, we will tell the *back-end* which profile it is. However, this led to a small, but unpleasant, fluctuation of the application because it loaded something common to all and after the *back-end* received the domain, we performed a *postback* redirection of the application to the specific profile. To avoid this flicker - we moved the logic to the *front-end* part of the application. In the `main.ts` file, we turn to the *back-end* to return the profile identifier. ```typescript // main.ts const profileId: string = await whatMyProfileIs("api/profile/id"); ``` Just that - a super-fast operation that returns a string identifier. No *postback*, no 301 redirection, no unnecessary requests to the *back-end*. How *back-end* understands what the context is, I will tell you some other time. The next step is to activate the profile in the application. Just to add - the application has an administrative part that allows us and to some extent the client, to configure the profile. So when we get the profile identifier, we load its configuration. We use a function in the `main.ts` file. ```typescript // main.ts async function loadClientConfiguration(configStore: string): Promise { try { const response = await fetch(configStore); const config = await response.json(); return config; } catch (error) { Logger.fatal("Failed to load client configuration", error); } } ``` Then we load the configuration and pass it to the application. ```typescript // main.ts const config = await loadClientConfiguration(`/tote/${profileId}.json`); Object.freeze(config); app.provide("clientConfig", config); app.provide("clientId", profileId); ``` This is the entire logic for loading the profile and propagating it in the application. ## Routing Each profile, except the default one, loads its own routing. With it, we can easily activate additional plugins for the *Vue* engine. We add the additional router to the standard and filter it based on the profile. Then we add it to the main routing. ```typescript // router/index.ts import { customRoutes } from "@/router/custom"; const customRoute: Array = clientId ? customRoutes[clientId] : []; if (customRoute?.length > 0) { routes.push(...customRoute); } const history = createWebHashHistory(); const router = createRouter({ history, routes }); ``` Easy to add, easy to maintain. It provides freedom for expansion, for each profile, regardless of the others. It works fast and without issues. I simplified the example a bit, but it gives a good idea of how you can extend the usage of *Vue Router*. With this implementation, we encountered a small problem. When activating the *Vue* engine in `main.ts`, we add the following lines: ```typescript // main.ts import { createApp } from "vue"; import App from "@/App.vue"; import router from "@/router"; import { whatMyProfileIs } from "@/endpoints/profile"; import Logger from "@/system/fs/workers/logger"; const profileId: string = await whatMyProfileIs("api/profile/id"); const config = await loadClientConfiguration(`/tote/${profileId}.json`); Object.freeze(config); const app = createApp(App); app.provide("clientConfig", config); app.provide("clientId", profileId); app.use(router); app.mount("#app"); async function loadClientConfiguration(configStore: string): Promise { try { const response = await fetch(configStore); const config = await response.json(); return config; } catch (error) { Logger.fatal("Failed to load client configuration", error, { "predef": 500 }); } } ``` This way, *Vue Router* & *Vue App* load independently and the profile doesn't reach the router on time. That's why we created an additional function and asynchronously load the router there. ```typescript // main.ts import { createApp } from "vue"; import App from "@/App.vue"; import { whatMyProfileIs } from "@/endpoints/profile"; import Logger from "@/system/fs/workers/logger"; BigBang(); async function BigBang(): Promise { const profileId: string = await whatMyProfileIs("api/profile/id"); if (profileId) { const app = createApp(App); const config = await loadClientConfiguration(`/tote/${profileId}.json`); Object.freeze(config); const { default: router } = await import("@/router"); app.use(router); app.provide("clientConfig", config); app.provide("clientId", profileId); await router.isReady(); app.mount("#app"); } } async function loadClientConfiguration(configStore: string): Promise { try { const response = await fetch(configStore); const config = await response.json(); return config; } catch (error) { Logger.fatal("Failed to load client configuration", error, { "predef": 500 }); } } ``` The whole solution for one entry point and multiple sites worked with these few simple changes. --- ::note I am omitting some of the object implementations in the article - they are not essential but only serve to suggest what business logic is behind them. :: --- ::note "I use the term **back-end** quite loosely. I wondered if it might just be a server, but in our case, that's not quite the correct definition. :: --- ::comments{discussions="https://github.com/howbizarre/thoughts/discussions/7"} :: # Parcel & Robots There are different ways to tell **ParcelJS** which files are static and should not go through the build transformation, but if there are only a few, you can activate the transformers plugin and then include them directly in the build script in the `package.json` file. ```json // package.json { "scripts": { "build": "parcel build src/index.html src/robots.txt src/favicon.ico" } } ``` This way, `robots.txt` and `favicon.ico` will not be processed by [ParcelJS](https://parceljs.org/){rel="nofollow"} and will be directly transferred to the build directory. ## Transformers plugin To make the above build script work properly, you need to add the following code to the `.parcelrc` file: ```json // .parcelrc { "extends": "@parcel/config-default", "transformers": { "*.{txt,ico}": ["@parcel/transformer-raw"] } } ``` So *ParcelJS* will not add hashes to the names of the files, but also the linked objects in the files being processed will not be changed. --- ::comments{discussions="https://github.com/howbizarre/thoughts/discussions/8"} :: # Vue emits with parameters Passing events in **Vue** from a component back to the one that calls it is done with **emits**. The emits can also be done with **Pinia** or another library for state management, but this will be for another article. ## What is a page and what is a component? I am often asked: '*Should I extract this code into components or leave it on the page?*'. I have built myself a basic rule - if I have the question, whether this code should become a component, it should become a component. I also group the components by certain characteristics. I often give the [@layer](https://tailwindcss.com/docs/functions-and-directives#layer){rel="nofollow"} directive of **TailwindCSS** as an example when someone wonders how to group their components. I divide the components into 2 types: 1. **Speculators**: Those that do not perform any business logic, but only draw and/or transfer data; 2. **Workers**: Those that perform secondary processing of the incoming parameters and add business logic, which they return to the calling component. **Workers** can often call other **workers** or **speculators**, while **Speculators** usually work independently. ## How are parameters passed? Let's make an example. We have a component (**Speculator**) that shows on the screen how many more product pages are left before the last page is reached. We call it `LoadMore.vue`. The input parameters of the component are 2: '**which number is the current page**' and '**how many are the total number of pages**'. The component does not perform any business logic. It calculates how many more pages are left and draws them. ```vue // components/utilities/LoadMore.vue ``` ### Direct event passing Let's rework our component a bit so that when it is clicked, it passes the event to the calling component, which fires a function. ```vue // components/utilities/LoadMore.vue ``` This is a direct way to pass the event. When the button is clicked, the `loadMore` event and the `page` parameter are passed to the calling component. ```vue // pages/Products.vue ``` I always use **kebab-case** for emitted events. ### Define emit When you want to make some changes to the parameters you are passing before emitting the event, you will need to define the emit. ```vue // components/utilities/LoadMore.vue ``` This way we now have a lot of control over the output parameters. ```vue // pages/Products.vue ``` This example is just illustrative. If a similar change to the page number is being made, it should be logic in the calling component. But for our example, it's a great way to see how you can take control of the returned data. --- ::comments{discussions="https://github.com/howbizarre/thoughts/discussions/9"} :: # Nitro, i18n and Dev Proxy When you use **Nuxt**, it is normal to use **Nitro** as well, but sometimes this does not fit into the scenario you have been prepared for. The API requests are handled by another back-end server and in order to develop the application locally, a proxy must be set up. Nitro has a description in the documentation on how to set up the devProxy, and everything works fine until I encountered a Nuxt application with an active **i18n** module with several language localizations and configured in **prefix** strategy. With the prefix strategy, the URL address is automatically replaced and the devProxy stops working. ```javascript // nuxt.config.ts export default defineNuxtConfig({ nitro: { devProxy: { "/~": { target: "http://127.0.0.1:3243/", }, }, }, }); ``` In our case, all requests starting with `/~`, for example: `/~/api/request/method`, Nitro redirects them to another back-end server. But when the language culture `/en/~/api/request/method` is unexpectedly added to the address and Nitro stops communicating with the other server. That's why we quickly enriched the devProxy configuration. ```javascript // nuxt.config.ts export default defineNuxtConfig({ nitro: { devProxy: { "/bg/~": { target: "http://127.0.0.1:1818/", }, "/en/~": { target: "http://127.0.0.1:1818/", }, "/~": { target: "http://127.0.0.1:1818/", }, }, }, }); ``` The idea was to confirm that this will work, but so far I have not found another way. The problem comes when adding a new language localization. Each one must be added to the devProxy configuration. Drop a line if you know a more cultured way. --- ::comments{discussions="https://github.com/howbizarre/thoughts/discussions/10"} :: # Dynamic manifest.json If you want the users of your web application to "install" it on their devices, it is enough to fill in the `manifest.json` file. It is already widely supported by the browsers and the operating systems and it is very easy to do it, so you should not skip it. I maintain a **Vue** application that communicates with a **SaaS** system. The application is generated almost statically, without having a back-end behind it and has one entry point, but depending on the site that loads it, I save a variable in **LocaleStorage** that contains the identifier of this site. The application has a built-in `manifest.json`, but over time I had to personalize it for each site. The dynamic loading of the `manifest.json` file should be done at the client (in the browser). We excluded **Vue & Vue Router** from the calculations, because the manifest must be available, even if their 'engines' do not work. So, we transferred everything to the `index.html` file of the application. I wrote a small **Node** console that goes through the active databases and generates a static `manifest.siteId.json` files for each site in a specific folder. Then I added a small script to the `index.html` file that loads this file and adds it to `document.head`. ```html ``` ## Why fetch? This is a very good question. When generating the `manifest.siteId.json` files, there may be a situation where such a file does not exist. Since **JavaScript** cannot check for a file on the server - I make a **fetch** request for it. If the request does not work, then I load the `manifest.json` file with the basic descriptions. --- ::comments{discussions="https://github.com/howbizarre/thoughts/discussions/11"} :: # Removing the trailing slash from the URLs in Nuxt Every page on your website should have unique content. If you have a second page with the same content, the weight of the information for the search engines is divided between the two, and this greatly reduces their chances of appearing earlier in the search results. If you have a page that loads at `https://example.com/page`, it is very likely that the same page will also load at `https://example.com/page/`. From our point of view, this is the same page, but for search engines, these are two different addresses. They expect to have different content on them. One of the ways to tell the search engines which content should be considered is the canonical addresses of the pages. I will pay attention to another - to tell the search engines that the 'wrong' address has been moved. As you understand, this should happen even before the crawling machine loads our page. In **Nuxt**, this can be done with a small **middleware**. ```typescript // middleware/remove-trailing-slash.global.ts export default defineNuxtRouteMiddleware((to) => { if (to.path === "/" || !to.path.endsWith("/")) return; // --> const removedSlash = to.path.replace(/\/+$/, "") || "/"; const seoRoute = { path: removedSlash }; return navigateTo(seoRoute, { redirectCode: 301 }); }); ``` We add **global** to the middleware's name to run it before loading each page, without the pages needing to call it explicitly. --- ::comments{discussions="https://github.com/howbizarre/thoughts/discussions/12"} :: # Log info with Web Workers from Vue 3 to the server **Web Workers** are small, powerful scripts that run in the browser's background. And because they don't interfere with the rendering of your application, you can load them with various tasks to perform. I use Web Workers in two ways - in the first one, they don't return information to the application and after finishing the work, they self-destruct. In the second one, they return a result and then the application takes care of when and if the Web Worker should be destroyed. Here we will look at only the first way of working. I will write a separate article for the second one. ## LOG WORKER I most often use the first way of working for Web Workers to log information from the application to the server, but not only. I create a directory called `workers` and in it, I create a file - in our case `LOGS.ts`. The file is always in capital letters, so I can easily recognize it when importing it, and has a speaking name. Over time, I have concluded that every type of task that a given Web Worker performs should be in a separate file. ```typescript // workers/LOGS.ts import axios from "axios"; self.onmessage = async (event) => { const { message, code, type } = event.data; await axios.post("/api/log", { type: `${message}`, statusCode: code }, { contentType: "text/plain" }); /** * Log Worker does not return data, * so we close the worker after the POST request, * even if there is an error with writing to the server, * the worker will close. */ self.close(); }; ``` To use Web Workers in your application, you need to be very careful with its import. For **Vue 3** with **Composition API** and ` ``` --- ::comments{discussions="https://github.com/howbizarre/thoughts/discussions/13"} :: # Electron, TypeScript & Parcel Documentation for **Electron** is entirely in **JavaScript**, but that doesn't stop you from using **TypeScript** to generate that JavaScript. A few simple rules must be followed, mainly in the file loading paths. I have also prepared a small addition for the front-end part. Instead of the standard Electron HTML page, I will make a small compilation with **Parcel**. ## The Project First, we will organize our project. It will have 2 subfolders - one for the Electron part, the other for the Browser part. We create a folder `electron-typescript-parcel` and open it in [**VSCode**](https://code.visualstudio.com/){rel="nofollow"} - or whichever editor you use. Open the built-in terminal in VSCode (or another if you don't use VSCode) and execute: ```bash npm init -y ``` This will create a `package.json` file in the `electron-typescript-parcel` folder. Open the file and edit the `author` field. As a start, it is enough. Next, we need to add the Electron module to the project. ```bash npm install --save-dev electron ``` If you are going to use **GIT**, now is a good time to execute: ```bash git init ``` and add a `.gitignore` file. Add `node_modules` as a start. Then add 2 folders - `electron` and `browser` in the project folder. As the names suggest - Electron will live in the first, and the front-end part for the browser will live in the second. ## Electron Through the terminal, enter the `electron` folder and execute: ```bash npm init -y ``` and immediately after that add the TypeScript module: ```bash npm install --save-dev typescript ``` Load `package.json`. In the **script** part, add `"build": "tsc"` and remove the `"main"` attribute. ```json // electron/package.json { "name": "electron", "version": "1.0.0", "scripts": { "build": "tsc" }, "keywords": [], "author": "", "license": "ISC", "description": "", "devDependencies": { "typescript": "^5.5.4" } } ``` In the console execute: ```bash npx tsc --init ``` This will create a `tsconfig.json` file. In it, you will need to find `"outDir"`, uncomment the line, and set `"outDir": "../dist"`. From here, we follow the standard steps for creating a basic Electron application, skipping the part about creating the `index.html` file and `renderer.js` file, which we will add through Parcel. Add a `main.ts` file to the `electron` folder and write the following code: ```typescript // electron/main.ts import { app, BrowserWindow, ipcMain, nativeTheme } from "electron"; import path from "node:path"; /** * Creates a new window and loads an HTML file. */ const createWindow = (): void => { const mainWindow = new BrowserWindow({ width: 800, height: 600, webPreferences: { preload: path.join(__dirname, "./preload.js"), }, }); mainWindow.loadFile("./dist/index.html"); ipcMain.handle("dark-mode:toggle", () => { if (nativeTheme.shouldUseDarkColors) { nativeTheme.themeSource = "light"; } else { nativeTheme.themeSource = "dark"; } return nativeTheme.shouldUseDarkColors; }); }; app.whenReady().then(() => { createWindow(); app.on("activate", () => { if (BrowserWindow.getAllWindows().length === 0) createWindow(); }); }); app.on("window-all-closed", () => { if (process.platform !== "darwin") app.quit(); }); ``` This is an [example from the Electron documentation](https://www.electronjs.org/docs/latest/tutorial/dark-mode){rel="nofollow"} that changes the dark or light theme of the application. Next, we add a `preload.ts` file to the `electron` folder and write the following code: ```typescript // electron/preload.ts import { contextBridge, ipcRenderer } from "electron/renderer"; contextBridge.exposeInMainWorld("electronAPI", { toggle: () => ipcRenderer.invoke("dark-mode:toggle"), }); ``` ## Browser Through the terminal, we navigate to the `browser` folder and execute: ```bash npm init -y ``` After that, we install ***Parcel***: ```bash npm install --save-dev parcel ``` Loading `package.json`. In the **script** section, we add `"build": "parcel build index.html --dist-dir ../dist --no-source-maps --public-url ./ --no-optimize"` and remove the `"main"` attribute. Create an `index.html` file in the `browser` folder and write the following code: ```html Hello World!

Hello World!

Current theme source: System

``` Creating a `styles.css` file in the `browser` folder and writing the following code in it: ```css /* browser/styles.css */ @media (prefers-color-scheme: dark) { body { background: #333; color: white; } } @media (prefers-color-scheme: light) { body { background: #ddd; color: black; } } ``` Adding a `render.ts` file to the `browser` folder and writing the following code in it: ```typescript const toggleDarkMode = document.getElementById("toggle-dark-mode"); const themeSource = document.getElementById("theme-source"); if (themeSource && toggleDarkMode) { toggleDarkMode.addEventListener("click", async () => { // @ts-expect-error const isDarkMode = await window.electronAPI.toggle(); themeSource.innerHTML = isDarkMode ? "Dark" : "Light"; toggleDarkMode.innerHTML = `Toggle ${!isDarkMode ? "Dark" : "Light"} Mode`; }); } ``` To 'compile' the TypeScript file with Parcel, we will add a `.parcelrc` file in the folder and write the following: ```json // browser/.parcelrc { "extends": "@parcel/config-default", "transformers": { "*.ts": ["@parcel/transformer-typescript-tsc"] } } ``` ## Start We go back to the project folder and edit the **scripts** and **main** fields in the `package.json` file: ```json // package.json { "main": "./dist/main.js", "scripts": { "start": "npm run build --prefix ./electron && npm run build --prefix ./browser && electron ." } } ``` In the `.gitignore` file, we add the `dist` and `.parcel-cache` folders, and in the command line, we execute: ```bash npm start ``` After starting the application, a `dist` folder will appear in the project folder, containing all the code of the Electron application. I have created repositories for this project on [GitHub](https://github.com/howbizarre/electron-typescript-parcel){rel="nofollow"}. --- ::comments{discussions="https://github.com/howbizarre/thoughts/discussions/14"} :: # Vue Router & Main file When starting a new **Vue** project, I use the **Quick Start** section of the Vue website. Then, I make a few small changes before adding the project to the Source Control bank. From the questions that `npm create vue@latest` asks me, I almost always choose: ```shell √ Add TypeScript? ... no / YES √ Add Vue Router for Single Page Application development? ... no / YES √ Add Pinia for state management? ... no / YES √ Add an End-to-End Testing Solution? » Playwright ``` The others, if necessary during development. After executing all the instructions that appear on the screen, you get a project ready to start. The first thing I edit is the Main file - `src/main.ts`. At the beginning, it looks like this: ```typescript import './assets/main.css' import { createApp } from 'vue' import { createPinia } from 'pinia' import App from './App.vue' import router from './router' const app = createApp(App) app.use(createPinia()) app.use(router) app.mount('#app') ``` There is a small hidden secret here that you probably don't know. Sometimes, it may happen that the Vue application loads before the router is initialized. To avoid the appearance of such Vue jokes, I edit `src/main.ts`: ```typescript import "./assets/main.css"; import { createApp } from "vue"; import { createPinia } from "pinia"; import App from "./App.vue"; initializeApp(); async function initializeApp(): Promise { const app = createApp(App); const pinia = createPinia(); const { default: router } = await import("@/router"); app.use(router); app.use(pinia); await router.isReady(); app.mount("#app"); } ``` This way, the router is initialized before the Vue application is loaded. A tiny trick that will save you from possible future problems. --- ::comments{discussions="https://github.com/howbizarre/thoughts/discussions/4"} :: # Simple Vue plugin for geo location When I write a plugin, I think of it as a standalone piece of code that has its own logic and is independent of where it will be used. This is not entirely true, of course, but when designing a plugin, I always start from this idea. After all, every system has its own logic and architecture that must be followed - especially for outgoing data. ## Geo location In *modern browsers*, it is very easy to access the [Geolocation API](https://developer.mozilla.org/en-US/docs/Web/API/Geolocation_API){rel="nofollow"}. You call ```javascript navigator.geolocation.getCurrentPosition(call_success_function, call_error_function); ``` and in the object returned to `call_success_function` you will get everything you need, including **latitude** & **longitude**. If necessary, you can check if the browser supports the Geolocation API `if (!navigator.geolocation) { ... } else { ... }`. I will add a small abstraction to the standard function to make it work in an asynchronous environment. ```typescript async function GeoLocation(): Promise { return new Promise((resolve, reject) => { navigator.geolocation.getCurrentPosition( (success) => resolve(location.coords), (error) => reject(error) ); }); } ``` A wonderful method that will return the coordinates and (as I told you at the beginning) works regardless of the environment in which it will be used. ## Vue plugin In a project where we want to use the above method as a plugin, I create a folder `plugins` and in it I create a sub-folder `geo-location`. In this folder I add 2 files - `index.ts` and `GeoLocation.ts`. Of course, the content of `GeoLocation.ts` is the above method. ```typescript // plugins/geo-location/GeoLocation.ts export async function GeoLocation(): Promise { return new Promise((resolve, reject) => { navigator.geolocation.getCurrentPosition( (success) => resolve(location.coords), (error) => reject(error) ); }); } ``` In `index.ts` I follow the standard structure of Vue plugins. ```typescript // plugins/geo-location/index.ts import type { App } from "vue"; import { GeoLocation } from "./GeoLocation"; export default { install(app: App) { app.provide("GeoLocation", GeoLocation); }, }; ``` Then we add it to `main.ts`. ```typescript // main.ts import { createApp } from "vue"; import App from "./App.vue"; import GeoLocationPlugin from "./plugins/geo-location"; const app = createApp(App); app.use(GeoLocationPlugin); app.mount("#app"); ``` ## Using the plugin Here is the reason for writing this article. To use the plugin in a **TypeScript** environment, you need to be careful with the initialization in the Vue component where you will use it. ```typescript const GEOLocation = inject<() => Promise>('GeoLocation'); ``` If you omit the cast `() => Promise`, you will get an error that `GeoLocation` is not a function. --- ::note You can also define your own type: ```typescript type GeoLocationType = () => Promise; const GEOLocation = inject('GeoLocation'); ``` :: --- ::comments{discussions="https://github.com/howbizarre/thoughts/discussions/15"} :: # HB's Thoughts A simple blog system built with Nuxt 4, focused on articles about Vue, Nuxt, TailwindCSS, TypeScript, and front-end development. ## 📖 About HB's Thoughts is a personal blog featuring articles mostly about Vue, Nuxt, TailwindCSS, and TypeScript, but not limited to — more on the front-end and less on the back-end. The blog supports multiple languages (English and Bulgarian) and is optimized for performance and user experience. ## ✨ Features - **Modern Tech Stack**: Built with Nuxt 4, Vue 3, and TypeScript - **Multilingual Support**: Available in English and Bulgarian with i18n - **Content Management**: Powered by Nuxt Content for markdown-based articles - **Modern UI**: Styled with Nuxt UI and TailwindCSS - **Search Functionality**: Full-text search with Fuse.js - **Tag System**: Articles organized by tags and competencies - **SEO Optimized**: Server-side rendering with optimized meta tags - **Structured Data**: JSON-LD structured data for blog posts, listings, and breadcrumbs - **Cloud Deployment**: Deployed on Cloudflare Workers - **Responsive Design**: Mobile-first responsive layout ## 🛠 Tech Stack - **Framework**: [Nuxt 4](https://nuxt.com/){rel="nofollow"} - **Frontend**: [Vue 3](https://vuejs.org/){rel="nofollow"} with TypeScript - **Styling**: [TailwindCSS](https://tailwindcss.com/){rel="nofollow"} + [Nuxt UI](https://ui.nuxt.com/){rel="nofollow"} - **Content**: [Nuxt Content](https://content.nuxt.com/){rel="nofollow"} for markdown articles - **Internationalization**: [Nuxt i18n](https://i18n.nuxtjs.org/){rel="nofollow"} - **Search**: [Fuse.js](https://fusejs.io/){rel="nofollow"} for fuzzy search - **Database**: Better SQLite3 - **Deployment**: Cloudflare Workers - **Build Tool**: Vite ## 🚀 Getting Started ### Prerequisites - Node.js (v18 or higher) - npm or yarn - Wrangler CLI (for Cloudflare deployment) ### Installation 1. Clone the repository: ```bash git clone https://github.com/hristobotev/hbsthoughts.git cd hbsthoughts ``` 2. Install dependencies: ```bash npm install ``` 3. Start the development server: ```bash npm run dev ``` The site will be available at `http://localhost:7410` ## 📝 Available Scripts - `npm run dev` - Start development server on port 7410 - `npm run build` - Build the application for production - `npm run generate` - Generate static files - `npm run preview` - Build and preview with Wrangler - `npm run deploy` - Build and deploy to Cloudflare Workers - `npm run cf-typegen` - Generate Cloudflare types ## 📁 Project Structure ```bash ├── app/ # Nuxt app directory │ ├── components/ # Vue components │ ├── composables/ # Vue composables (JSON-LD, utilities) │ ├── layouts/ # Layout components │ ├── pages/ # Page components and routing │ └── assets/ # Static assets ├── content/ # Markdown content │ ├── bg/ # Bulgarian articles │ ├── en/ # English articles │ ├── seo/ # SEO configurations ├── i18n/ # Internationalization ├── public/ # Public assets └── server/ # Server-side code ``` ## 🌍 Content Management Articles are written in Markdown and stored in the `content/` directory: - `/content/en/articles/` - English articles - `/content/bg/articles/` - Bulgarian articles - `/content/en/static/` - English static pages (like help pages) - `/content/bg/static/` - Bulgarian static pages (like help pages) ### Article Format Each article follows this frontmatter structure: ```markdown --- title: "Article Title" date: "2024-02-06T12:01:53.293Z" draft: false tags: ["vue", "nuxt"] slug: "article-slug" navigation: false competence: "frontend" --- Article content here... ``` ## 🔍 SEO & Structured Data The blog implements comprehensive SEO optimization with JSON-LD structured data: ### JSON-LD Implementation The application includes three types of structured data using Schema.org vocabulary: 1. **Blog Listing** (`useJsonLdBlogListing`): - Generates `Blog` schema for article listing pages - Includes all articles with their metadata - Automatically updates when articles are loaded 2. **Blog Posts** (`useJsonLdBlogPost`): - Generates `BlogPosting` schema for individual articles - Includes author, publisher, dates, and article metadata - Supports optional featured images 3. **Breadcrumbs** (`useJsonLdBreadcrumbs`): - Generates `BreadcrumbList` schema for navigation - Works with Nuxt UI breadcrumb components - Handles multilingual routes and dynamic content ### Usage The JSON-LD composables are automatically imported and can be used in any page: ```vue ``` All structured data is reactive and updates automatically when content changes. ### Site Maps The blog generates a sitemap for SEO *(Search Engine Optimization)* purposes, which includes all articles and static pages. The sitemap is automatically updated when new content is added, thanks to the **Nuxt Sitemap** module. ## 🚀 Deployment The application is configured for deployment on Cloudflare Workers: 1. Configure Wrangler: ```bash npm run cf-typegen ``` 2. Deploy: ```bash npm run deploy ``` ## 🤝 Contributing 1. Fork the repository 2. Create a feature branch: `git checkout -b feature/amazing-feature` 3. Commit your changes: `git commit -m 'Add amazing feature'` 4. Push to the branch: `git push origin feature/amazing-feature` 5. Open a Pull Request ## 📄 License This project is licensed under the MIT License. ## 🙏 Acknowledgments - Built with [Nuxt 4](https://nuxt.com/){rel="nofollow"} - UI components from [Nuxt UI](https://ui.nuxt.com/){rel="nofollow"} - Content from [Nuxt Content](https://content.nuxt.com/){rel="nofollow"} - Multilingual support with [Nuxt i18n](https://i18n.nuxtjs.org/){rel="nofollow"} - Icons from [Heroicons](https://heroicons.com/){rel="nofollow"} - Fonts from [Google Fonts](https://fonts.google.com/){rel="nofollow"} - Back-end [Cloudflare Workers](https://workers.cloudflare.com/){rel="nofollow"} --- ::comments --- discussions: https://github.com/howbizarre/hbsthoughts/discussions/5 --- :: # Cloudflare & Email Binding You don't need to have an email server to send emails with Cloudflare Worker. Just let Cloudflare validate the email address you're using and Cloudflare will forward incoming emails to it. ## Requirements There are 3 things you need to have in your Cloudflare account to be able to send emails: 1. **Domain** - You need to have an active domain in your Cloudflare account. This doesn't mean you have to buy it from them - it's enough to set your DNS records to point to Cloudflare. 2. **Email** - You need to have an active email address. It doesn't have to be associated with the domain from the previous point. It can be a Gmail account or any other account you have full access to. This can also be the email you use to log in to Cloudflare. This email must be added to the *'Destination addresses'* in the *'Email Routing'* settings for the domain from previous point, which will make it active. 3. **Routing rules** - You need to have an email address added to the *'Routing rules'* in the *'Email Routing'*. This address is part of the domain from the first point and will be used to forward emails to the email from the previous point. I may have described it a bit complicated, but once you start adding the settings one after another, you will understand it correctly. ## Binding In your **Nuxt** project that uses **Cloudflare Worker**, there should be a `wrangler.jsonc` or `wrangler.toml` file for configuration. You need to add the following settings to it: ```json // wrangler.jsonc { "send_email": [ { "name": "INFO_EMAIL", // free text - it's good to have meaning "destination_address": "your@valid.email" // email from point 2 above } ] } ``` These are all the initial environment settings for the machine to work. And do not forget to update Cloudflare types. ```bash npx wrangler types ``` ## Usage The easiest way is to add an *API endpoint* to the **Nitro** environment. For example `/api/send-info-email`. This endpoint needs to initialize the Cloudflare binding so we can use it directly. This is the coolest part. It can be done in one line: ```js // Accessing Email binding const env = event.context.cloudflare?.env; ``` And as you might guess, `env` gives you full access to the Email binding and you can send emails at will. ```js // Sending an email await env.INFO_EMAIL.send({ sender, recipient, content }); ``` Extremely simple and incredibly elegant solution. **DX** to the **MAX**. --- ::comments --- discussions: https://github.com/howbizarre/hbsthoughts/discussions/12 --- :: # Cloudflare Tail Worker With **Cloudflare Tail Worker** you direct logging to one place - everything is in the ecosystem and in real time. Until recently I only used external tools like **Sentry**, **LogRocket** or **Loki** with **Grafana**, but setting them up requires knowing the processing and takes time. Very often, when you use *[**SaaS**] (Software as a Service)* services you don't have access to the logs - or if you do, the logs are heavily truncated. Then you configure your development environment as close as possible to production and try to simulate the errors or create a process to which you send the errors and from there amplify them to an external tool. Every Worker in Cloudflare has its own logs, but you don't have much control over them. And when you need to monitor more than one Worker, relative to the time when some event occurred in all these Workers, things get complicated. That's why you create, separately, a normal Worker, but the `async fetch(request, env, ctx)` function you rename to `async tail(events, env, ctx)` and you already have a defined *Tail Worker*. ```typescript // src/index.ts export default { async tail(events: TraceItem[], _env: unknown, _ctx: unknown) { if (!events || events.length === 0) { return; } console.log(`[TAIL] Received ${events.length} events`); for (const trace of events) { try { handleTraceItem(trace); // Your function for processing events } catch (error) { console.error(`[TAIL_ERROR] Error processing event: ${error instanceof Error ? error.message : String(error)}`); } } console.log(`[TAIL] Completed processing ${events.length} events`); } } satisfies ExportedHandler; ``` You name your Worker - for example: `tail-for-me-all-the-app-events`. You add to `wrangler.jsonc` the line ```json { // ... "observability": { "enabled": true } } ``` and upload it to Cloudflare. Done - you have an active Tail Worker. From here on, in each ***Producer Worker*** whose logs you want to monitor, you add to its `wrangler.jsonc`: ```json { // ... "tail_consumers": [ { "service": "tail-for-me-all-the-app-events" } ] } ``` Currently there's no limit on the number of Producer Workers that can send their logs to a given Tail Worker. You should know that above a certain CPU usage time, Cloudflare will charge you additionally. I mainly follow 2 rules - If two or more Producer Workers share at least one database - they use a common Tail Worker. - A Producer Worker uses more than one Tail Worker only if it has defined access rules. --- ::note I created a **GitHub Gist** with an [example of the `handleTraceItem` function](https://gist.github.com/howbizarre/2643b54a2af7c9494f8befe1fd1dd8ba){rel="nofollow"}. :: --- ::comments{discussions="https://github.com/howbizarre/hbsthoughts/discussions"} :: # Dynamically loading of Vue plugins in the main file I am developing a B2B application that has a single entry point but is used by different clients with different domains, appearances, and custom functionalities. Based on the domain and a few other tricks, I identify which configuration to load, and thus, although there is a single entry point, everything after it is highly personalized. One of the features of Vue is that plugins are registered globally, and if you need different plugins in different configurations, you will only need to load the ones you need. This can be done dynamically in the main file. ```ts // main.ts import Vue from 'vue'; const loadPlugin = (pluginName: string): Promise => { return import(`@/plugins/${pluginName}`).then((module) => { Vue.use(module.default); }); }; // Example usage if (client === 'MyPressureClient') { loadPlugin('my-pressure-client-plugin'); } else { loadPlugin('my-regular-client-plugin'); } // ... ``` In this example, the `loadPlugin` function takes a plugin name as an argument and dynamically imports the corresponding plugin module. So far so good, but when you have over 50 plugins and a given client uses around 10 of them, the scheme starts to get heavy and the parallelism of loading disappears. To handle this, I create an object that contains an array of plugins for each client and load them all at once with `Promise.all`. ```ts const plugins = clientConfig.plugins || {}; const pluginLoaders: Promise<{ name: string; plugin: any }>[] = []; if (plugins?.plugin-one) { pluginLoaders.push(import('@/plugins/plugin-one').then((m) => ({ name: 'PluginOne', plugin: m.default }))); } if (plugins?.plugin-two) { pluginLoaders.push(import('@/plugins/plugin-two').then((m) => ({ name: 'PluginTwo', plugin: m.default }))); } if (plugins?.plugin-three) { pluginLoaders.push(import('@/plugins/plugin-three').then((m) => ({ name: 'PluginThree', plugin: m.default }))); } // ... more plugins that the client uses, if any // You load all plugins in parallel (50-70% faster than sequential) const loadedPlugins = await Promise.all(pluginLoaders); // You register the plugins in order loadedPlugins.forEach(({ name, plugin }) => { app.use(plugin); }); ``` Elegant and efficient. **DX** at **MAX** :o ) --- ::comments{discussions="https://github.com/howbizarre/hbsthoughts/discussions"} :: # Здравей Свят Преди много време спрях да поддържам блог. В момента пиша разни статии към репозиторитата в **GitHub**, но те не целят да достигнат крайния потребител. Повече са за хора, които преминават на бързо за информация и не се старая много, защото са насочени към разбиращите технологиите, за които пиша. Започвам този блог, за да споделя опита ми с един прекрасен стек от технологии, с които може да се постигне всичко, е... почти всичко. [Vue](https://vuejs.org/){rel="nofollow"}, [Nuxt](https://nuxt.com/){rel="nofollow"}, [TailwindCSS](https://tailwindcss.com/){rel="nofollow"} и [TypeScript](https://www.typescriptlang.org/){rel="nofollow"}. Това ще бъдат основните теми на в размислите ми. Силно насочени към front-end и допълнени с beck-end. Струва ми се, че общността около тези технологии в **България** е малка и се надявам с времето да подпомогна нейното развитие. ## Vue Ако изключим **jQuery** - Някъде около 2012 година започнах да използвам "реактивни" JavaScript библиотеки. Една от първите беше [Knockout](https://knockoutjs.com/){rel="nofollow"}. Велика. Всеки стартиращ с Observable модела, и изобщо, всеки стартиращ със "реактивните" JavaScript библиотеки трябва да мине през нея. След това се наредиха още много - в това число и **Angular** и **React**. Дори за кратко пишех и моя собствена, базирана на jQuery и **Mustache**. Накрая попаднах на **Vue**. По онова време пишех много **CSS** и използвах **ASP.NET** и **Razor** за създаване на front-end. Използвах и доста CSS библиотеки, като **960.gs**, **Bootstrap**, **Foundation** и др. и Vue някак си естествено навлезе в ежедневието ми със сепарираното писане в компонентите наподобяващо до някъде модела на организация на файловете с който бях свикнал. Когато добави и Vue Router и Pinia (Vuex преди нея) и картинката става още по-добра. **Vue + Vue Router + Pinia = MVC** in front-end. Ще напиша допълнително как изграждам MVC (Model View Controller) с тях. ## Nuxt **Nuxt** е базиран на Vue. Освен множеството улеснения, които дава, като автоматични импорти, автоматичен рутинг, плъгини, модули и т.н. - добавя и back-end сървър (**Nitro**) и работите като с една система, без да е необходимо да създавате второ приложение за сървър, без да са необходими супер познания по работата на Node & Express. Това е инструмента, който махна .NET & C# от полезрението ми. С Nitro можете да изградите сървърен middleware, API endpoints, връзка към бази с данни - всичко, което Ви е необходимо от back-end. ## TailwindCSS След Just-in-Time Mode - **TailwindCSS** замени изцяло Bootstrap и не само. Вече не ползвам масивни CSS библиотеки с включени UI компоненти. Сепарирам двете отделно. Дори компонентните библиотеки също са изградени с TailwindCSS. Може би в последно време залагам повече на **Nuxt UI** и то, предимно, за да поддържам екосистемата. ## TypeScript **JavaScript** дава огромна свобода на писане, деклариране, извикване, навързване, паралелност, асинхронност и т.н. Има изградени патърни за всякакви аспекти от програмните модели. Ползваш го за beck & front едновременно. Има доста масивни организации и огромна общност, които го развиват. Но тази свобода има и своите недостатъци. Няма компилатор, който да те пази. Няма единен модел за дебъгване. Няма правилен начин за генериране на крайния/проекционния код. **TypeScript** подпомага премахването на някои от проблемите на JavaScript. Не е панацея и понякога не е лесен за конфигуриране, особено когато работите със споделени модели от данни между front & back end, но дава една много по-правилна представа за пътя на разработка, поддръжка и доставяне на кода. ## The others **Vite**, **Node**, **Express**, **MongoDB**, **NPM**, **Firebase** допълват сегашния ми стек от технологии. Vite e личния ми избор за разработка. И не само за Vue проекти. Понякога използвам **Parcel**, но за специфични решения. Работя и с другите от "metro" технологиите, като **Nest**, **ElectonJS** & **React Native**, **Bun** и т.н. но повечето са за малки или лични проекти. --- ::comments :: # Google Fonts в Nuxt с TailwindCSS Услугата на Google за шрифтовете е страшно удобна за използване. Има голям избор от шрифтове и лесен начин да ги филтрираш спрямо нуждите ти. В екосистемата на Nuxt има много добър модул за интегриране на Google шрифтове в приложението Ви, но аз ще Ви покажа малко по-различен подход. В [конфигурацията](https://nuxt.com/docs/api/nuxt-config#head){rel="nofollow"} си Nuxt позволява да обработваме **Head** парчето от HTML-а. Има и други места на които може да го правите, но за целта ще използвам конфигурационния файл `nuxt.config.ts`. Когато изберете шрифт от Google, той Ви предоставя код, който да добавите към приложението си. Кода има следния вид: ```html ``` За да добавите това парче от код в `nuxt.config.ts` трябва да го разделите на части в `link` масива в конфигурацията. ```typescript //nuxt.config.ts export default defineNuxtConfig({ app: { head: { link: [ { rel: "preconnect", href: "https://fonts.googleapis.com" }, { rel: "preconnect", href: "https://fonts.gstatic.com", crossorigin: "" }, { rel: "stylesheet", href: "https://fonts.googleapis.com/css2?family=Roboto&display=swap" } ] } } ``` Изключително просто и елегантно решение. При билдването горния код се инжектира в Head-а на приложението Ви и шрифтовете се зареждат от Google. Това работи прекрасно, но понякога не е достатъчно. Например, ако генерирате приложението си статично с `npx nuxt generate`. Тогава е добре да помислите как да оптимизирате зареждането на шрифтовете, защото може да достигнат доста големи обеми. Става лесно, като първоначално променим стойността на `rel` атрибута и след извикването на `onload` събитието го възстановяваме. ```typescript //nuxt.config.ts export default defineNuxtConfig({ app: { head: { link: [ { rel: "preconnect", href: "https://fonts.googleapis.com" }, { rel: "preconnect", href: "https://fonts.gstatic.com", crossorigin: "" }, { rel: "preload", as: "style", onload: "this.onload = null; this.rel = 'stylesheet';", href: "https://fonts.googleapis.com/css2?family=Roboto&display=swap" } ] } } ``` След като шрифта е зареден трябва да го популяризираме в приложението. В моя случай използвам **TailwindCSS**. TailwindCSS Ви позволява да използвате предварително подготвени [фамилии от шрифтове](https://tailwindcss.com/docs/font-family){rel="nofollow"}, но са предоставили и лесен начин да ги преконфигурирате в конфигурационния файл `tailwind.config.js`. ```javascript // tailwind.config.js /** @type {import('tailwindcss').Config} */ export const theme = { fontFamily: { "sans": ["Inter", "sans-serif"], "serif": ["Playfair Display", "serif"], }, }; ``` От тук на там CSS класът `font-sans` ще рисува текста Ви с **Roboto** шрифта. --- ::comments{discussions="https://github.com/howbizarre/thoughts/discussions/6"} :: # Наследството на Бари Моето семейството имаше куче. Казваше се **Бари Наит**. Викахме му още **Баритош**, **Баритошев**, **Баритошко**, **Торонгаш**, **Пальок**, **Черньо**, **мр. Президент** и др. Черен среден пудел. Около 12 килограма. Живя точно 3 години. Почина на рождения си ден. Нашето прекрасно пале вече го няма, но то ни остави наследство, което ще споделя с всички Вас, попаднали на тази страница. Сутрин, когато се будехме **Бари** идваше при нас. Въртеше опашка, подскачаше по кревата, донасяше играчка и се радваше безкрайно много. Радваше се, че денят започва и той ще е с нас. За него беше без значение, как е минал предния ден, как е минала нощта - радостта му беше безгранична всяка една сутрин. Ако искахме да станем от кревата, трябваше да му върнем порядъчна доза гушкане, игра и радост. Ако някой от нас изчезнеше от полезрението му, дори за 5 мин., когато се върнеше започваше бурно посрещане. Радостно джафкане, подскачане, въртене на опашката и подкана, да му отдадеш внимание, любов и радост. И за да продължим с ежедневните си задължения, трябваше да му отделиш достатъчно внимание, любов и радост. Когато станехме прекалено сериозни или заети със задължения, той идваше при нас. Подбутваше ни с носле или с лапа, за да привлече вниманието ни. И ако успееше, с безкрайна радост носеше, някоя от любимите му играчки и започваше весела игра. Ако пък не успееше, не се тревожеше - лягаше до/върху нас и се сгушваше в очакване. Има още безкрай подобни истории с нашето пале. Можем да разкажем 1001 вероятно, а може и повече. Но това, което се случваше с нас, когато **Бари** е наоколо, са вулканите с радост и любов които изригваха от сърцата ни - и не само към него - към всичко заобикалящо ни. --- Ето защо пиша тази няколко реда, за да Ви споделя, че не **Бари** поставяше в нас радостта и любовта. Той само ни учеше как да я откриваме и да я споделяме. И сега, когато го няма, Ви споделям наследството, което ни остави: > Всяко същество на тази земя се ражда с вулкан от радост и любов в сърцето си. Не е лесно да ги активираме, и на много от нас са ни необходими проводници, като **Бари**, но те са там. Изпитвайте радост, когато видите любими хора - не пропускайте никога. Изпитвайте радост сутрин, все едно ги е нямало цяла вечност. Показвайте им любов и за всичко, което правят. Прегръщайте ги, целувайте ги и им се радвайте. Вулканите в сърцата ни са безкрайни. Те никога няма да свършат. Довиждане наше малко пале. Ще се видим в безкрайни полета - и да знаеш, ще донесем зелената гума. --- ::comments :: # Една входна точка за множество сайтове Имаме приложение, което представлява микросайт и когато го заредите, виждате страница за вход. Нашите клиенти го предоставят на своите потребители и след като потребител влезе, се зареждат данни, свързани, както с клиента, към който принадлежат, така и с правата, зададени му от нашия клиент. С развитието на приложението, клиентите ни започнаха да искат микросайта да заработи на техен домейн. Да носи техния бранд. Да се добави "богато" съдържание към иначе "постния" екран за вход. Да се разширят функционалностите му - а повечето от исканите функционалности бяха силно персонализирани. Пред нас имаше 2 пътя - да разделим приложението за всеки клиент и в него да добавим всичко, от което клиента има нужда, или да добавим допълнителна конфигурация, която да позволи на приложение да се персонализира. И при двата варианта, сървърната back-end част нямаше да се променя. Само front-end частта щеше да се персонализира спрямо клиента. Бързо отхвърлихме първия вариант. Ние сме малък екип и поддръжката на множество инсталации и версии щеше да отнема ресурс, който не искахме да отделяме. Затова се насочихме към една входна точка за всички клиенти и различни конфигурации за всеки. Наричаме го **ПРОФИЛИ**. --- ## Профили > Как да разпознаваме профила? Това беше първия въпрос, който стоеше пред нас. Когато потребител зареди приложението то трябва още на входния екран да започне да показва различни шрифтове, лого, картинки за фон и др. Веднага трябваше да знае, дали клиента поддържа повече от един език, дали се зареждат допълнителни контроли, като регистрация, форма за обратна връзка и т.н. Тогава решихме, че на базата на домейна, който извикваше приложението ще казваме на *beck-end*, кой е профила. Това обаче доведе до малка, но неприятна, флуктуация на приложението, защото зареждаше нещо, общо за всички и след като *beck-end* получи, кой е домейна, извършвахме *postback* пренасочване на приложението към конкретния профил. За да избегнем това премигване - изнесохме логика във *front-end* частта на приложението. В `main.ts` файла се обръщаме към *beck-end*, да ни върне идентификатора на профила. ```typescript // main.ts const profileId: string = await whatMyProfileIs("api/profile/id"); ``` Само това - свръх бърза операция, която връща един стрингов идентификатор. Няма *postback*, няма 301 пренасочване, няма излишни заявки към *beck-end*. Как *beck-end* разбира кой е контекста ще ви разкажа някой друг път. Следващата стъпка е да активираме профила в приложението. Само да добавя - към приложението има административна част, която ни позволява, а донякъде и на клиента, да се конфигурира профила. Така че като получим идентификатора на профила да зареждаме конфигурацията му. Използваме функция в `main.ts` файла. ```typescript // main.ts async function loadClientConfiguration(configStore: string): Promise { try { const response = await fetch(configStore); const config = await response.json(); return config; } catch (error) { Logger.fatal("Failed to load client configuration", error); } } ``` След което зареждане конфигурацията и я подаваме на приложението. ```typescript // main.ts const config = await loadClientConfiguration(`/tote/${profileId}.json`); Object.freeze(config); app.provide("clientConfig", config); app.provide("clientId", profileId); ``` Това е цялата логика по зареждането на профила и популяризирането му в приложението. ## Рутинг Всеки профил, освен стандартния, зарежда и собствен рутинг. С него лесно може да активираме и допълнителни плъгини към *Vue* енджина. В стандартния рутер добавяме добавяме допълнителния рутер и филтрираме спрямо профила. След което го добавяме към основния рутинг. ```typescript // router/index.ts import { customRoutes } from "@/router/custom"; const customRoute: Array = clientId ? customRoutes[clientId] : []; if (customRoute?.length > 0) { routes.push(...customRoute); } const history = createWebHashHistory(); const router = createRouter({ history, routes }); ``` Лесно за добавяне, лесно за поддръжка. Дава свобода за разширяване, за всеки профил, независимо от останалите. Работи бързо и без проблемно. Малко опростих примера, но дава добра представа как може да разширите начина на използване на *Vue Router*. При тази реализация се натъкнахме на малък проблем. При стандартното активиране на *Vue енджина* в `main.ts` добавяме следните редове: ```typescript // main.ts import { createApp } from "vue"; import App from "@/App.vue"; import router from "@/router"; import { whatMyProfileIs } from "@/endpoints/profile"; import Logger from "@/system/fs/workers/logger"; const profileId: string = await whatMyProfileIs("api/profile/id"); const config = await loadClientConfiguration(`/tote/${profileId}.json`); Object.freeze(config); const app = createApp(App); app.provide("clientConfig", config); app.provide("clientId", profileId); app.use(router); app.mount("#app"); async function loadClientConfiguration(configStore: string): Promise { try { const response = await fetch(configStore); const config = await response.json(); return config; } catch (error) { Logger.fatal("Failed to load client configuration", error, { "predef": 500 }); } } ``` По този начин Vue Router & Vue App се зареждат независимо и профила не достига до рутера на време. Затова направихме допълнителна ф-ия и там асинхронно зареждаме рутера. ```typescript // main.ts import { createApp } from "vue"; import App from "@/App.vue"; import { whatMyProfileIs } from "@/endpoints/profile"; import Logger from "@/system/fs/workers/logger"; BigBang(); async function BigBang(): Promise { const profileId: string = await whatMyProfileIs("api/profile/id"); if (profileId) { const app = createApp(App); const config = await loadClientConfiguration(`/tote/${profileId}.json`); Object.freeze(config); const { default: router } = await import("@/router"); app.use(router); app.provide("clientConfig", config); app.provide("clientId", profileId); await router.isReady(); app.mount("#app"); } } async function loadClientConfiguration(configStore: string): Promise { try { const response = await fetch(configStore); const config = await response.json(); return config; } catch (error) { Logger.fatal("Failed to load client configuration", error, { "predef": 500 }); } } ``` Цялото решение за една входна точка и множество сайтове заработи с тези няколко простички изменения. --- ::note Изпускам част от имплементацията на обектите в статията - те не са съществени, а служат само да подскажат каква бизнес логика стои зад тях. :: --- ::note "Използвам думата **back-end** доста примитивно. Чудех се, дали да не е просто сървър, но при нас това, не е съвсем правилно определение. :: --- ::comments{discussions="https://github.com/howbizarre/thoughts/discussions/7"} :: # Parcel & Robots Има различни начини, да окажете на *ParcelJS*, кои файлове са статични и не трябва да преминават през билд трансформацията, но ако са малък брой, можете да активирате `transformers` плъгина и след това да ги окажете директно в билд скрипта в `package.json` файла. ```json // package.json { "scripts": { "build": "parcel build src/index.html src/robots.txt src/favicon.ico" } } ``` По този начин `robots.txt` и `favicon.ico` няма да бъде обработван от [ParcelJS](https://parceljs.org/){rel="nofollow"} и ще бъде директно прехвърлен в билд директории. ## Transformers плъгина За да заработи правилно горния билд скрипт, трябва да добавите в `.parcelrc` файла следния код: ```json // .parcelrc { "extends": "@parcel/config-default", "transformers": { "*.{txt,ico}": ["@parcel/transformer-raw"] } } ``` *ParcelJS* не само, че няма да добави хешове към имената, на файловете, но и линкнантите обектите във файловете, които се процесват няма да бъдат променени. --- ::comments{discussions="https://github.com/howbizarre/thoughts/discussions/8"} :: # Vue emits с параметри Предаването на събитията във **Vue** от компонент, обратно към този, който го извиква става с **emits**. Предаването може да се направи и с **Pinia** или друга библиотека за управление на 'състоянието', но това ще е за някоя друга статия. ## Кое е страница и кое е компонент? Много често ме питат: '*Този код да го изкарам ли в компонентите или да го оставя в страницата?*'. Изградил съм си едно основно правило - ако ми изниква въпроса, дали този код да стане компонент, значи трябва да стане компонент. Самите компоненти също ги групирам по определени х-ки. Много често давам за пример [@layer](https://tailwindcss.com/docs/functions-and-directives#layer){rel="nofollow"} директивата на **TailwindCSS**, когато някой се чуди как да си групира компонентите. Компонентите ги разделям на 2 вида: 1. **Спекуланти**: Такива, които не изпълняват никаква бизнес логика, а само рисуват и/или трансферират данни; 2. **Работници**: Такива, които извършват вторична обработка на входящите параметри и добавят бизнес логика, която връщат към извикващия ги компонент. **Работниците** много често може извикват други **работници** или **спекуланти**, докато **Спекулантите** обикновено работят самостоятелно. ## Как се предават параметри? Нека си направим един пример. Имаме компонент (**Спекулант**), който показва на екрана, колко още страници с продукти остават, преди да се достигне последната страница. Кръщаваме го `LoadMore.vue`. Входните параметри на компонента са 2: '**кой номер е текущата страница**' и '**колко е общия брой страници**'. Компонента не извършва никаква бизнес логика. Пресмята колко още страници остават и ги рисува. ```vue // components/utilities/LoadMore.vue ``` ### Директно предаване на събитие Нека да преработим малко нашия компонент, така че като се натисне да предава събитието към извикващия компонент, който да пали функция. ```vue // components/utilities/LoadMore.vue ``` Това е директен начин за предаване на събитието. Когато се натисне бутона, се предава събитието `loadMore` и параметъра `page` към извикващия компонент. ```vue // pages/Products.vue ``` Винаги ползвам **kebab-case** за емитнатите събитията. ### Дефиниране на emit Когато искате да направите някакво изменение на параметрите, които предавате, преди да емитнете събитието, ще трябва да дефинираите emit-а. ```vue // components/utilities/LoadMore.vue ``` Така вече имаме много голям контрол върху изходните параметри. ```vue // pages/Products.vue ``` Този пример е само показателен. Ако се прави подобно изменението на номера на страницата, то трябва да е логика в извикващия компонент. Но за нашия пример е прекрасен начин да видите, как може да поемете контрола на връщаните данни. --- ::comments{discussions="https://github.com/howbizarre/thoughts/discussions/9"} :: # Nitro, i18n and Dev Proxy Когато ползвате **Nuxt**, нормално е да използвате и **Nitro**, но понякога това не влиза в сценария, който са Ви подготвили. За API заявките отговаря друг beck-end сървър и за да може да си разработвате локално приложението, трябва да се настрои прокси. Nitro имат описание в документацията, как да настроите **devProxy**, и всичко работи добре, докато не се сблъсках с Nuxt приложение с активен **i18n** модул с няколко езикови локализации и настроен в режим (стратегия) **prefix**. При префиксната стратегия, URL адреса се подменя автоматично и devProxy спира да работи. ```typescript // nuxt.config.ts export default defineNuxtConfig({ nitro: { devProxy: { "/~": { target: "http://127.0.0.1:3243/", }, }, }, }); ``` В нашия случай, всички заявки, които започват с `/~`, например: `/~/api/request/method`, Nitro ги пренасочва към другия beck-end сървър. Но когато в адреса ненадейно се добави и езиковата култура `/en/~/api/request/method` и Nitro спира да комуникира с др. сървър. Затова набързо обогатихме конфигурацията на devProxy. ```typescript // nuxt.config.ts export default defineNuxtConfig({ nitro: { devProxy: { "/bg/~": { target: "http://127.0.0.1:1818/", }, "/en/~": { target: "http://127.0.0.1:1818/", }, "/~": { target: "http://127.0.0.1:1818/", }, }, }, }); ``` Идеята беше да потвърдим, че това така ще работи, но и до момента не съм открил друг начин. Проблема идва в добавянето на нова езикова локализация. Всяка една трябва да се добавя в конфигурацията на devProxy. Драснете ако знаете по-културен начин. --- ::comments{discussions="https://github.com/howbizarre/thoughts/discussions/10"} :: # Динамичен manifest.json Ако желаете потребителите на вашето уеб приложение да го „инсталират“ на своите устройства, достатъчно е да попълните `manifest.json` файла. Има широка поддръжка от всякакви браузъри и операционни системи и е елементарно да се направи, за да се пропуска. Поддържам едно **Vue** приложение, което комуникира с **SaaS** система. Приложението се генерира почти статично, без да има beck-end зад себе си и има една входна точка, но в зависимост от сайта, които я зарежда, в **LocaleStorage**-а записвам променлива, която съдържа идентификатор на този сайт. Приложението си има изграден `manifest.json`, но с времето се наложи да го персонализирам за всеки един сайт. Динамичното зареждане на `manifest.json` файла трябва да стане при клиента (в браузъра). **Vue & Vue Router** ги изключихме от сметките, защото манифеста трябва да е наличен, дори и техните 'машинки' да не заработят. Така че, прехвърлихме всичко в `index.html` файла на приложението. Написах малка **Node** конзола, която преминава през активните бази и генерира статично `manifest.siteId.json` файл за всеки един сайт в определена папка. След това добавих един малък скрипт в `index.html` файла, който зарежда този файл и го добавя към `document.head`. ```html ``` ## Защо fetch? Това е един много добър въпрос. При генерирането на `manifest.siteId.json` файловете, може да възникне ситуация, при която няма да съществува такъв файл. Понеже **JavaScript**-а, не може да прави проверка за файл на сървъра - правя **fetch** заявка за него. Ако заявката не сработи, тогава зареждам `manifest.json` файл с базовите описания. --- ::comments{discussions="https://github.com/howbizarre/thoughts/discussions/11"} :: # Премахване на наклонената черта от адресната линия в Nuxt Всяка страница във Вашия уеб сайт трябва да е с уникално съдържание. Ако имате втора страница с същото съдържание, то тежестта на информацията за търсачките се разделя между двете и това силно намалява шансовете им за по-ранно показване в резултатите от търсенето. Ако имате страница, която се зарежда на адрес `https://example.com/page` е много възможно същата страница да се зарежда и на адрес `https://example.com/page/`. От наша гледна точка, това е една и съща страница, но за търсещите машини, това са два различни адреса. Съответно очакват да има различно съдържание на тях. Едни от начините да окажем на търсачките, кое съдържание трябва да се отчита, са каноничните адреси на страниците. Аз ще обърна внимание на друг - да кажем на търсачките, че 'грешния' адрес е преместен. Както разбирате, това трябва да стене, още преди обхождащата машина да зареди страницата ни. В **Nuxt** това може да стане с един малък **middleware**. ```typescript // middleware/remove-trailing-slash.global.ts export default defineNuxtRouteMiddleware((to) => { if (to.path === "/" || !to.path.endsWith("/")) return; // --> const removedSlash = to.path.replace(/\/+$/, "") || "/"; const seoRoute = { path: removedSlash }; return navigateTo(seoRoute, { redirectCode: 301 }); }); ``` На middleware му добавяме **global** в името, за да се изпълнява преди зареждането на всяка една страница, без да е необходимо страниците да го викат изрично. --- ::comments{discussions="https://github.com/howbizarre/thoughts/discussions/12"} :: # Логване на информация с Web Workers от Vue 3 към сървъра **Web Workers** са малки, но мощни скриптове, които работят на 'заден план' в браузъра. И понеже не пречат на рендирането на приложението Ви, може да ги товарите с разни задачи, които да изпълняват. Ползвам Web Workers по два начина - В първия не връщат информация обратно към приложението и след свършване на работа се самоунищожават. При втория връщат резултат и тогава приложението се грижи, дали и кога Web Worker-а да се унищожи. Тук ще разгледаме само първия начин на работа, а за втория ще напиша отделна статия. ## LOG WORKER Най-често използвам първия начин на работа Web Workers за логване на информацията от приложението към сървъра, но не само. Създавам директория, която се казва `workers` и в нея създавам файл - в нашия случай `LOGS.ts`. Файла винаги е с главни букви, за да го разпознавам лесно при импорта и е с говоримо име. С времето съм възприел, че всеки вид задача, която изпълнява даден Web Worker, трябва да е в отделен файл. ```typescript // workers/LOGS.ts import axios from "axios"; self.onmessage = async (event) => { const { message, code, type } = event.data; await axios.post("/api/log", { type: `${message}`, statusCode: code }, { contentType: "text/plain" }); /** * Log Worker does not return data, * so we close the worker after the POST request, * even if there is an error with writing to the server, * the worker will close. */ self.close(); }; ``` За да използвате Web Workers във Вашето приложение, много трябва да внимавате с импорта му. За **Vue 3** с **Composition API** и ` ``` --- ::comments{discussions="https://github.com/howbizarre/thoughts/discussions/13"} :: # Electron, TypeScript и Parcel Документацията на **Electron** е изцяло за **JavaScript**, но това не пречи да си използвате **TypeScript**, с който да генерира този JavaScript. Трябва да се спазват няколко простички правила, и то основно в пътищата на зареждане на файловете. Подготвил съм и малко допълнение за front-end частта. Вместо стандартната за Electron HTML страница ще направя малка компилация с **Parcel**. ## Проекта Най-напред ще си организираме проекта. В него ще има 2 подпапки - едната за Electron частта, др. за Browser частта. Създаваме папка `electron-typescript-parcel` и я отваряме във [**VSCode**](https://code.visualstudio.com/){rel="nofollow"} - или който редактор ползвате. Отваряме вградения във VSCode терминал (или друг ако не ползвате VSCode) и изпълнявате: ```bash npm init -y ``` Това ще създаде `package.json` файл в папката `electron-typescript-parcel`. Отваряте файла и редактирате полето `author`. Като начало е достатъчно. Следва да добавим Electron модула в проекта. ```bash npm install --save-dev electron ``` Ако ще ползвате **GIT**, сега е добре да изпълните: ```bash git init ``` и да добавите `.gitignore` файл. В него добавете като начало добавяме `node_modules`. След това добавяме 2 папки - `electron` и `browser` в папката на проекта. Както подсказват имената - в първата ще живее Electron, а във втората - front-end частта за браузъра. ## Electron През терминала влизаме в папката `electron` и изпълняваме: ```bash npm init -y ``` и веднага след това добавяме и TypeScript модула: ```bash npm install --save-dev typescript ``` Зареждаме `package.json`. В **script** частта добавяме `"build": "tsc"` и махаме `"main"` атрибута. ```json // electron/package.json { "name": "electron", "version": "1.0.0", "scripts": { "build": "tsc" }, "keywords": [], "author": "", "license": "ISC", "description": "", "devDependencies": { "typescript": "^5.5.4" } } ``` В конзолата изпълняваме: ```bash npx tsc --init ``` Това ще създаде `tsconfig.json` файл. В него ще трябва да намерите `"outDir"`, да премахнете коментираната час на реда и задавате `"outDir": "../dist"`. От тук следваме стандартните за Electron стъпки за създаване на базово приложение, като изпускаме частта за създаване на `index.html` файла и `renderer.js` файла, които ще добавим чрез Parcel. Добавяме `main.ts` файл в папката `electron` и в него пишем: ```typescript // electron/main.ts import { app, BrowserWindow, ipcMain, nativeTheme } from "electron"; import path from "node:path"; /** * Creates a new window and loads an HTML file. */ const createWindow = (): void => { const mainWindow = new BrowserWindow({ width: 800, height: 600, webPreferences: { preload: path.join(__dirname, "./preload.js"), }, }); mainWindow.loadFile("./dist/index.html"); ipcMain.handle("dark-mode:toggle", () => { if (nativeTheme.shouldUseDarkColors) { nativeTheme.themeSource = "light"; } else { nativeTheme.themeSource = "dark"; } return nativeTheme.shouldUseDarkColors; }); }; app.whenReady().then(() => { createWindow(); app.on("activate", () => { if (BrowserWindow.getAllWindows().length === 0) createWindow(); }); }); app.on("window-all-closed", () => { if (process.platform !== "darwin") app.quit(); }); ``` Това е [примера от Electron документацията](https://www.electronjs.org/docs/latest/tutorial/dark-mode){rel="nofollow"}, който сменя тъмна или светла темата на приложението. След това добавяме `preload.ts` файл в папката `electron` и в него пишем: ```typescript // electron/preload.ts import { contextBridge, ipcRenderer } from "electron/renderer"; contextBridge.exposeInMainWorld("electronAPI", { toggle: () => ipcRenderer.invoke("dark-mode:toggle"), }); ``` ## Browser През терминала влизаме в папката `browser` и изпълняваме: ```bash npm init -y ``` след което инсталираме ***Parcel***: ```bash npm install --save-dev parcel ``` Зареждаме `package.json`. В **script** частта добавяме `"build": "parcel build index.html --dist-dir ../dist --no-source-maps --public-url ./ --no-optimize"`и махаме `"main"` атрибута. Създаваме `index.html` файл в папката `browser` и в него пишем: ```html Hello World!

Hello World!

Current theme source: System

``` Създаваме `styles.css` файл в папката `browser` и в него пишем: ```css /* browser/styles.css */ @media (prefers-color-scheme: dark) { body { background: #333; color: white; } } @media (prefers-color-scheme: light) { body { background: #ddd; color: black; } } ``` Добавяме `render.ts` файл в папката `browser` и в него пишем: ```typescript const toggleDarkMode = document.getElementById("toggle-dark-mode"); const themeSource = document.getElementById("theme-source"); if (themeSource && toggleDarkMode) { toggleDarkMode.addEventListener("click", async () => { // @ts-expect-error const isDarkMode = await window.electronAPI.toggle(); themeSource.innerHTML = isDarkMode ? "Dark" : "Light"; toggleDarkMode.innerHTML = `Toggle ${!isDarkMode ? "Dark" : "Light"} Mode`; }); } ``` За да 'компилираме' typescript файла с Parcel ще добавим в папката `.parcelrc` файл и в него ще напишем: ```json // browser/.parcelrc { "extends": "@parcel/config-default", "transformers": { "*.ts": ["@parcel/transformer-typescript-tsc"] } } ``` ## Старт Връщаме се обратно в папката на проекта и редактираме в `package.json` файла полетата **scripts** и **main** : ```json // package.json { "main": "./dist/main.js", "scripts": { "start": "npm run build --prefix ./electron && npm run build --prefix ./browser && electron ." } } ``` В `.gitignore` файла добавяме `dist` и `.parcel-cache` папките и в командния ред изпълняваме: ```bash npm start ``` След старта на приложението, ще се появи папка `dist` в папката на проекта и в нея ще е целия код на Electron приложението. Направил съм репозитори за този проект в [GitHub](https://github.com/howbizarre/electron-typescript-parcel){rel="nofollow"}. --- ::comments{discussions="https://github.com/howbizarre/thoughts/discussions/14"} :: # Vue Router и Main файла При стартиране на нов **Vue** проект, използвам **Quick Start** секцията на сайта на Vue. След това, задължително, правя няколко малки промени, преди да добавя проекта към Source Control банката. От въпросите, които ми задава `npm create vue@latest` почти винаги си избирам: ```shell √ Add TypeScript? ... no / YES √ Add Vue Router for Single Page Application development? ... no / YES √ Add Pinia for state management? ... no / YES √ Add an End-to-End Testing Solution? » Playwright ``` Другите, ако в процеса на разработка се наложат. След изпълнение на всички инструкции, които Ви излизат по екрана, получавате един готов за стартиране проект. Първото нещо, което редактирам е Main файла - `src/main.ts`. В началото той има следния вид: ```typescript import './assets/main.css' import { createApp } from 'vue' import { createPinia } from 'pinia' import App from './App.vue' import router from './router' const app = createApp(App) app.use(createPinia()) app.use(router) app.mount('#app') ``` Тук има една малка скрита тайна, която вероятно не знаете. Понякога може да се случи, Vue приложението да се зареди, преди да се инициализира рутера. За да избегна появата на подобни Vue jokes, редактирам `src/main.ts`: ```typescript import "./assets/main.css"; import { createApp } from "vue"; import { createPinia } from "pinia"; import App from "./App.vue"; initializeApp(); async function initializeApp(): Promise { const app = createApp(App); const pinia = createPinia(); const { default: router } = await import("@/router"); app.use(router); app.use(pinia); await router.isReady(); app.mount("#app"); } ``` По този начин рутера се инициализира преди Vue приложението да бъде заредено. Една мъничка хитрина, която ще Ви спаси от евентуални бъдещи проблеми. --- ::comments{discussions="https://github.com/howbizarre/thoughts/discussions/4"} :: # Простичък Vue плъгин за гео локация Когато пиша плъгин, мисля за него, като самостоятелно парче код, което има собствена логика на работа и е независимо от мястото, на което ще се ползва. Това разбира се не е съвсем вярно, но при проектирането на плъгин, винаги изхождам от тази идея. Все пак всяка система има своя логика и архитектура, която трябва да се спазва - особено за изходящите данни. ## Гео локация В *съвременните браузъри* е много лесно може да достъпите [APIто за гео локация](https://developer.mozilla.org/en-US/docs/Web/API/Geolocation_API){rel="nofollow"}. Извиквате ```javascript navigator.geolocation.getCurrentPosition(call_success_function, call_error_function); ``` и в обекта, който се връща към `call_success_function` ще получите всичко необходимо, в това число и **latitude** & **longitude**. При необходимост можете да проверите, дали браузъра поддържа APIто за гео локация `if (!navigator.geolocation) { ... } else { ... }`. Ще добавя една малка абстракция към стандартната функция, която да я направи да работи в асинхронен среда. ```typescript async function GeoLocation(): Promise { return new Promise((resolve, reject) => { navigator.geolocation.getCurrentPosition( (success) => resolve(location.coords), (error) => reject(error) ); }); } ``` Чуден метод, който ще върне координатите и (както Ви казах в началото) работи независимо от средата в която ще се ползва. ## Vue плъгин В проект, където искаме да използваме, като плъгин, горния метод създавам папка `plugins` и в нея създавам под-папка `geo-location`. В тази папка добавям 2 файла - `index.ts` и `GeoLocation.ts`. Подразбира се, че съдържанието на `GeoLocation.ts` е горния метод. ```typescript // plugins/geo-location/GeoLocation.ts export async function GeoLocation(): Promise { return new Promise((resolve, reject) => { navigator.geolocation.getCurrentPosition( (success) => resolve(location.coords), (error) => reject(error) ); }); } ``` В `index.ts` спазвам стандартната структура на Vue плъгините. ```typescript // plugins/geo-location/index.ts import type { App } from "vue"; import { GeoLocation } from "./GeoLocation"; export default { install(app: App) { app.provide("GeoLocation", GeoLocation); }, }; ``` След което го добавяме в `main.ts`. ```typescript // main.ts import { createApp } from "vue"; import App from "./App.vue"; import GeoLocationPlugin from "./plugins/geo-location"; const app = createApp(App); app.use(GeoLocationPlugin); app.mount("#app"); ``` ## Използване на плъгина Тук е причината да напиша тази статиика. За да използвате плъгина в **TypeScript** среда, ще трябва да внимавате с инициализацията във Vue компонента, в който ще го ползвате. ```typescript const GEOLocation = inject<() => Promise>('GeoLocation'); ``` Ako пропуснете каста `() => Promise` ще получите грешка, че `GeoLocation` не е функция. --- ::note Може да дефинирате и собствен тип: ```typescript type GeoLocationType = () => Promise; const GEOLocation = inject('GeoLocation'); ``` :: --- ::comments{discussions="https://github.com/howbizarre/thoughts/discussions/15"} :: # HB's Thoughts Малка блог система построен с Nuxt 4, фокусиран върху статии за Vue, Nuxt, TailwindCSS, TypeScript и фронт-енд разработка. ## 📖 За Проекта HB's Thoughts е личен блог със статии главно за Vue, Nuxt, TailwindCSS и TypeScript, но не само - повече фронт-енд и по-малко бек-енд. Блогът поддържа множество езици (английски и български) и е оптимизиран за производителност и потребителско изживяване. ## ✨ Функционалности - **Модерен Технологичен Стек**: Построен с Nuxt 4, Vue 3 и TypeScript - **Многоезична Поддръжка**: Достъпен на английски и български с i18n - **Управление на Съдържание**: Задвижван от Nuxt Content за статии в markdown формат - **Модерен UI**: Стилизиран с Nuxt UI и TailwindCSS - **Функционалност за Търсене**: Пълнотекстово търсене с Fuse.js - **Система за Тагове**: Статиите са организирани по тагове и компетенции - **SEO Оптимизация**: Рендериране от страна на сървъра с оптимизирани мета тагове - **Структурирани Данни**: JSON-LD структурирани данни за блог постове, листинги и навигация - **Облачна Инсталация**: Инсталиран на Cloudflare Workers - **Адаптивен Дизайн**: Mobile-first адаптивен лейаут ## 🛠 Технологичен Стек - **Фреймуърк**: [Nuxt 4](https://nuxt.com/){rel="nofollow"} - **Фронт-енд**: [Vue 3](https://vuejs.org/){rel="nofollow"} с TypeScript - **Стилизиране**: [TailwindCSS](https://tailwindcss.com/){rel="nofollow"} + [Nuxt UI](https://ui.nuxt.com/){rel="nofollow"} - **Съдържание**: [Nuxt Content](https://content.nuxt.com/){rel="nofollow"} за markdown статии - **Интернационализация**: [Nuxt i18n](https://i18n.nuxtjs.org/){rel="nofollow"} - **Търсене**: [Fuse.js](https://fusejs.io/){rel="nofollow"} за размито търсене - **База данни**: Better SQLite3 - **Инсталация**: Cloudflare Workers - **Build Tool**: Vite ## 🚀 Започване ### Изисквания - Node.js (v18 или по-нова версия) - npm или yarn - Wrangler CLI (за Cloudflare инсталация) ### Инсталация 1. Клонирайте хранилището: ```bash git clone https://github.com/hristobotev/hbsthoughts.git cd hbsthoughts ``` 2. Инсталирайте зависимостите: ```bash npm install ``` 3. Стартирайте development сървъра: ```bash npm run dev ``` Сайтът ще бъде достъпен на `http://localhost:7410` ## 📝 Налични Скриптове - `npm run dev` - Стартира development сървър на порт 7410 - `npm run build` - Билдва приложението за продукция - `npm run generate` - Генерира статични файлове - `npm run preview` - Билдва и прегледва с Wrangler - `npm run deploy` - Билдва и инсталира на Cloudflare Workers - `npm run cf-typegen` - Генерира Cloudflare типове ## 📁 Структура на Проекта ```bash ├── app/ # Nuxt app директория │ ├── components/ # Vue компоненти │ ├── composables/ # Vue composables (JSON-LD, utilities) │ ├── layouts/ # Layout компоненти │ ├── pages/ # Page компоненти и routing │ └── assets/ # Статични ресурси ├── content/ # Markdown съдържание │ ├── bg/ # Български статии │ ├── en/ # Английски статии │ ├── seo/ # SEO конфигурации ├── i18n/ # Интернационализация ├── public/ # Публични ресурси └── server/ # Server-side код ``` ## 🌍 Управление на Съдържанието Статиите са написани в Markdown и съхранени в `content/` директорията: - `/content/en/articles/` - Английски статии - `/content/bg/articles/` - Български статии - `/content/en/static/` - Английски статични страници (като help страници) - `/content/bg/static/` - Български статични страници (като help страници) ### Формат на Статия Всяка статия следва тази frontmatter структура: ```markdown --- title: "Заглавие на Статията" date: "2024-02-06T12:01:53.293Z" draft: false tags: ["vue", "nuxt"] slug: "slug-na-statiata" navigation: false competence: "frontend" --- Съдържание на статията тук... ``` ## 🔍 SEO & Структурирани Данни Блогът имплементира цялостна SEO оптимизация със JSON-LD структурирани данни: ### JSON-LD Имплементация Приложението включва три типа структурирани данни използвайки Schema.org речника: 1. **Blog Listing** (`useJsonLdBlogListing`): - Генерира `Blog` схема за страници с листинг на статии - Включва всички статии с техните метаданни - Автоматично се актуализира когато статиите се заредят 2. **Blog Posts** (`useJsonLdBlogPost`): - Генерира `BlogPosting` схема за отделни статии - Включва автор, издател, дати и метаданни на статията - Поддържа опционални featured изображения 3. **Breadcrumbs** (`useJsonLdBreadcrumbs`): - Генерира `BreadcrumbList` схема за навигация - Работи с Nuxt UI breadcrumb компонентите - Обработва многоезични маршрути и динамично съдържание ### Използване JSON-LD composables се импортират автоматично и могат да се използват в която и да е страница: ```vue ``` Всички структурирани данни са реактивни и се актуализират автоматично при промяна на съдържанието. ### Карти на Сайта Блогът генерира карта на сайта за SEO *(Search Engine Optimization)* цели, която включва всички статии и статични страници. Картата на сайта се актуализира автоматично, когато се добави ново съдържание, благодарение на Nuxt Sitemap модула. ## 🚀 Инсталация Приложението е конфигурирано за инсталация на Cloudflare Workers: 1. Конфигурирайте Wrangler: ```bash npm run cf-typegen ``` 2. Инсталирайте: ```bash npm run deploy ``` ## 🤝 Сътрудничество 1. Направете fork на хранилището 2. Създайте feature branch: `git checkout -b feature/amazing-feature` 3. Commit-нете промените си: `git commit -m 'Add amazing feature'` 4. Push-нете към branch-а: `git push origin feature/amazing-feature` 5. Отворете Pull Request ## 📄 Лиценз Този проект е лицензиран под MIT лиценза. ## 🙏 Благодарности - Построен с [Nuxt 4](https://nuxt.com/){rel="nofollow"} - UI компоненти от [Nuxt UI](https://ui.nuxt.com/){rel="nofollow"} - Съдържание от [Nuxt Content](https://content.nuxt.com/){rel="nofollow"} - Многоезична поддръжка с [Nuxt i18n](https://i18n.nuxtjs.org/){rel="nofollow"} - Икони от [Heroicons](https://heroicons.com/){rel="nofollow"} - Шрифтове от [Google Fonts](https://fonts.google.com/){rel="nofollow"} - Back-end [Cloudflare Workers](https://workers.cloudflare.com/){rel="nofollow"} --- ::comments --- discussions: https://github.com/howbizarre/hbsthoughts/discussions/5 --- :: # Cloudflare и Email Binding Не е нужно да имате имейл сървър, за да изпращате писма с Cloudflare Worker. Достатъчно е Cloudflare да валидира ваш имейл адрес, който ползвате и той ще изпрати входящите имейли към него. ## Изисквания Има 3 неща, с които трябва да разполагате във вашия Cloudflare акаунт, за да можете да изпращате имейли: 1. **Домейн** - Трябва да имате активен домейн във вашия Cloudflare акаунт. Това не значи, че трябва да е купен от тях - достатъчно е да настроите DNS записите си, да сочат към Cloudflare. 2. **Имейл** - Трябва да имате активен имейл адрес. Не е нужно да е свързан с домейна от предната точка. Спокойно може да е gmail акаунт, или който и да е друг, до който имате пълен достъп. Това може да е й имейла с който се логвате в Cloudflare. Този имейл трябва да е добавен към *'Destination addresses'* в настройките на *'Email Routing'* на домейна от предната точка, което ще го направи и активен. 3. **Routing rules** - Трябва да имате добавен имейл адрес в *'Routing rules'* на *'Email Routing'*. Този адрес е част от домейна от първа точка и ще се ползва, да препраща писмата към имейла от предната точка. Май го описах малко сложно, но като започнете да добавяте настройките една след друга ще го разберете правилно. ## Binding Към **Nuxt** проекта ви, който използва **Cloudflare Worker**, би трябвало да има `wrangler.jsonc` или `wrangler.toml` файл за конфигурация. Към него трябва да добавите следните настройки: ```json // wrangler.jsonc { "send_email": [ { "name": "INFO_EMAIL", // свободен текст - добре е да има смисъл "destination_address": "your@valid.email" // имейла от 2ра точка горе } ] } ``` Това са всички първоначални настройки на средата, за да заработи машината. Не забравяйте да актуализирате типовете на Cloudflare. ```bash npx wrangler types ``` ## Използване Най-лесно е към **Nitro** средата да добавите един *API endpoint*. Например `/api/send-info-email`. Този endpoint трябва да инициализира Cloudflare binding-а, за да го ползваме директно. Това е най-яката част. Става с един ред: ```js // Достъп до Email binding const env = event.context.cloudflare?.env; ``` И както може би се досещате `env` ви дава пълен достъп до Email binding-а и може да пращате на воля писма. ```js // Изпращане на имейл await env.INFO_EMAIL.send({ sender, recipient, content }); ``` Екстремно просто и адски елегантно решение. **DX** на **MAX**. --- ::comments --- discussions: https://github.com/howbizarre/hbsthoughts/discussions/12 --- :: # Cloudflare Tail Worker С **Cloudflare Tail Worker** насочвате логването на едно място - всичко е в екосистемата и в реално време. До скоро ползвах само външни инструменти като **Sentry**, **LogRocket** или **Loki** с **Grafana**, но подкарването им изисква да познаваш процесинга и отнема време. Много често, когато ползвате *[**SaaS**] (Software as a Service)* услуги нямате достъп до логовете - или ако имате, логовете са силно орязани. Тогава си конфигурирате средата за разработка максимално близка до продукционната и се опитвате да симулирате грешките или създавате процес, на който пращате грешките и от там ги засилвате към външен инструмент. Всеки Worker в Cloudflare има собствени логове, но нямате особен контрол върху тях. А, когато се налага да следите повече от един Worker, спрямо време, в което са се случило някакво събитие във всички тези Workers, нещата загрубяват. Затова си създавате, отделно, един нормален Worker, но `async fetch(request, env, ctx)` функцията я кръщавате `async tail(events, env, ctx)` и вече имате дефиниран *Tail Worker*. ```typescript // src/index.ts export default { async tail(events: TraceItem[], _env: unknown, _ctx: unknown) { if (!events || events.length === 0) { return; } console.log(`[TAIL] Получени ${events.length} събития`); for (const trace of events) { try { handleTraceItem(trace); // Вашата функция за обработка на събитията } catch (error) { console.error(`[TAIL_ERROR] Грешка при обработка на събитие: ${error instanceof Error ? error.message : String(error)}`); } } console.log(`[TAIL] Завършена обработка на ${events.length} събития`); } } satisfies ExportedHandler; ``` Кръщавате Вашия Worker - например: `tail-for-me-all-the-app-events`. Добавяте във `wrangler.jsonc` ред ```json { // ... "observability": { "enabled": true } } ``` и го качвате на Cloudflare. Готово - имате активен Tail Worker. От тук нататък, във всеки един ***Producer Worker***, на който искате да следите логовете, добавяте в неговия `wrangler.jsonc`: ```json { // ... "tail_consumers": [ { "service": "tail-for-me-all-the-app-events" } ] } ``` За момента няма ограничение на броя Producer Workers, които могат да пращат логовете си към даден Tail Worker. Трябва да знаете, че над определено време на ползване на CPU, Cloudflare ще Ви таксува допълнително. Основно следвам 2 правила - Ако два или повече Producer Workers споделят поне една база - ползват общ Tail Worker. - Един Producer Worker ползва повече от един Tail Worker, само ако има дефинирани правила за достъп. --- ::note Създадох един **GitHub Gist** с [пример на `handleTraceItem` функцията](https://gist.github.com/howbizarre/2643b54a2af7c9494f8befe1fd1dd8ba){rel="nofollow"}. :: --- ::comments{discussions="https://github.com/howbizarre/hbsthoughts/discussions"} :: # Динамично зареждане на Vue плъгини в main файл Разработвам В2В приложение, което има една входна точка, но се ползва от различни клиенти с различни домейни, различна визии и собствени функционалности. На базата на домейна и още няколко хитрини, разпознавам коя конфигурация да се зареди и така, макар и една, входната точка и всичко след нея е силно персонализирано. Една от особеностите на Vue е, че плъгините се регистрират глобално и ако имате нужда от различни плъгини в различните конфигурации, ще трябва да зареждате само тези, които са нужни. Това може да стане динамично в main файла. ```ts // main.ts import Vue from 'vue'; const loadPlugin = (pluginName: string): Promise => { return import(`@/plugins/${pluginName}`).then((module) => { Vue.use(module.default); }); }; // Example usage if (client === 'MyPressureClient') { loadPlugin('my-pressure-client-plugin'); } else { loadPlugin('my-regular-client-plugin'); } // ... ``` В този пример функцията `loadPlugin` приема име на плъгин като аргумент и динамично импортира съответния модул на плъгина. До тук добре, но когато имате над 50 плъгина и даден клиент използва около 10 от тях, то схемата с започва да тежи и да изчезва паралелизъма на зареждане. За да се справя с това, създавам един обект, който съдържа масив от плъгини за всеки клиент и зареждам всички наведнъж с `Promise.all`. ```ts const plugins = clientConfig.plugins || {}; const pluginLoaders: Promise<{ name: string; plugin: any }>[] = []; if (plugins?.plugin-one) { pluginLoaders.push(import('@/plugins/plugin-one').then((m) => ({ name: 'PluginOne', plugin: m.default }))); } if (plugins?.plugin-two) { pluginLoaders.push(import('@/plugins/plugin-two').then((m) => ({ name: 'PluginTwo', plugin: m.default }))); } if (plugins?.plugin-three) { pluginLoaders.push(import('@/plugins/plugin-three').then((m) => ({ name: 'PluginThree', plugin: m.default }))); } // ... още плъгини, които клиента ползва, ако има такива // Зареждате всички плъгини паралелно (50-70% по-бързо от последователното) const loadedPlugins = await Promise.all(pluginLoaders); // Регистрирайте плъгините по ред loadedPlugins.forEach(({ name, plugin }) => { app.use(plugin); }); ``` Елегантно и ефективно. **DX** на **MAX** :o ) --- ::comments{discussions="https://github.com/howbizarre/hbsthoughts/discussions"} :: # Какво е компетентност? Компетентността показва, колко трябва да сте на ти с технологиите в статията. - [**Няма**](https://thoughts.bizarre.how/bg/competence/none): Не се изисква технически опит; - [**Базова**](https://thoughts.bizarre.how/bg/competence/elementary): `Getting Started` го знаете наизуст; - [**Про**](https://thoughts.bizarre.how/bg/competence/pro): Ползвате технологията и целия й инструментариум; - [**Маниак**](https://thoughts.bizarre.how/bg/competence/geek): Имате повече отговори, от колкото въпроси; Може да си мислите за нея, като за категория на статията. Една статия има само една компетентност/категория. ## Какво са таговете? Таговете, или както още етикети, са лесен начин да филтрирате статиите, които виждате, в дадена област, технология, идея и т.н. Най-често пиша за дадена технология или съвкупност от технологии. Тогава таговете оказват, кои са тези технологии. Понякога няма да не са свързани с технологии, а с моите мисли или интересни неща от мрежата или заобикалящата ме среда, които отбелязвам. ## Кои са технологиите? Основните технологии, за които пиша са насочени повече към front-end, но щe има и за back-end, и за инструментите, и за облаците, и за моделите, и за добрите практики и още други. [VueJS](https://vuejs.org/){rel="nofollow"}, [Nuxt](https://nuxt.com){rel="nofollow"}, [TailwindCSS](https://tailwindcss.com/){rel="nofollow"}, [TypeScript](https://www.typescriptlang.org/){rel="nofollow"}, [NodeJS](https://nodejs.org/){rel="nofollow"}, [NPM](https://www.npmjs.com/){rel="nofollow"}, [Vite](https://vitejs.dev/){rel="nofollow"}, [Parcel](https://parceljs.org/){rel="nofollow"}, [ExpressJS](https://expressjs.com/){rel="nofollow"}, [MongoDB](https://www.mongodb.com/){rel="nofollow"}, [GraphQL](https://graphql.org/){rel="nofollow"}, [Firebase](https://firebase.google.com/){rel="nofollow"}, [PM2](https://pm2.keymetrics.io/){rel="nofollow"}, [NginX](https://www.nginx.com/){rel="nofollow"} и др... ## Кой съм аз ли? > I am a Front-end Senpai, who strictly follows the W3Code of Bushido!. Обичам да използвам тази фраза. А иначе, аз съм **Коста**, още познат като **HowBizarre**, и заедно със семейството си работя и живея в **София**, **България**. Може да разгледате профилите ми в [GitHub](https://github.com/howbizarre){rel="nofollow"}, [X](https://x.com/howbizarre){rel="nofollow"} и [LinkedIn](https://www.linkedin.com/in/howbizarre){rel="nofollow"}. ## И малко за Изкуствения Интелект [Кратък индекс](https://thoughts.bizarre.how/llms.txt){rel="noopener"} на сайта, за да подпомогне Езиковите модели. :br [Разширен индекс](https://thoughts.bizarre.how/llms-full.txt){rel="noopener"} на сайта, който включва и съдържанието на статиите. # Hello World I stopped blogging a long time ago. I am currently writing various articles in the **GitHub** repositories, but they are not intended to reach the end user. They're more for people who are just passing through for information and I'm not trying too hard because they're aimed at the tech savvy I'm writing about. I'm starting this blog to share my experience with a wonderful technology stack that can do anything, well...almost anything. [Vue](https://vuejs.org/){rel="nofollow"}, [Nuxt](https://nuxt.com/){rel="nofollow"}, [TailwindCSS](https://tailwindcss.com/){rel="nofollow"} and [TypeScript](https://www.typescriptlang.org/){rel="nofollow"}. These will be the main topics of my reflections. Strongly focused on the front-end and complemented by the back-end. It seems to me that the community around these technologies in **Bulgaria** is small and I hope to help its development over time. ## Vue If we exclude **jQuery** - Sometime around 2012 I started using "reactive" JavaScript libraries. One of the first was [Knockout](https://knockoutjs.com/){rel="nofollow"}. Great one. Anyone starting with the Observable model, and generally anyone starting with "reactive" JavaScript libraries, should go through it. Many more followed after that, including **Angular** and **React**. I even briefly wrote my own based on jQuery and **Mustache**. Finally, I came across **Vue**. At the time, I wrote a lot of **CSS** and used **ASP.NET** and **Razor** to create the front-end. I also used quite a few CSS libraries, such as **960.gs**, **Bootstrap**, **Foundation** and more. and Vue somehow naturally entered my daily life with separated writing in components somewhat resembling the file organization model I was used to. When you add Vue Router and Pinia (Vuex before it) and the picture becomes even better. **Vue + Vue Router + Pinia = MVC** in front-end. I will write further how I build MVC (Model View Controller) with them. ## Nuxt **Nuxt** is based on Vue. Apart from the many facilities it gives like automatic imports, automatic routing, plugins, modules, etc. - also adds a back-end server (**Nitro**) and you work as a single system, without the need to create a second server application, without the need for super knowledge of Node & Express operation. This is the tool that took .NET & C# out of my sight. With Nitro you can build server middleware, API endpoints, database connection - everything you need from the back-end. ## TailwindCSS After Just-in-Time Mode - **TailwindCSS** completely replaced Bootstrap and more. I no longer use massive CSS libraries with included UI components. I separate them. Even the component libraries are also built with TailwindCSS. Maybe lately I'm betting more on **Nuxt UI** and that's mostly to support the ecosystem. ## TypeScript **JavaScript** gives enormous freedom in writing, declaring, calling, binding, concurrency, async, etc. There are a lot of patterns for all aspects of programming models. You use it for beck & front at the same time. There are pretty massive organizations and a huge community developing it. But this freedom also has its drawbacks. There is no compiler to protect you. There is no unified debugging model. There is no correct way to generate the final/production code. **TypeScript** helps alleviate some of the problems of JavaScript. It is not a panacea and sometimes it is not easy to configure, especially when working with shared data models between front-end and back-end, but it provides a much more structured approach to the development, maintenance, and delivery of code. ## The others **Vite**, **Node**, **Express**, **MongoDB**, **NPM**, **Firebase** round out my current technology stack. Vite is my personal choice for development. And not just for Vue projects. Sometimes I use **Parcel** but for specific solutions. I also work with other "metro" technologies like **Nest**, **ElectonJS** & **React Native**, **Bun** etc. but most are for small or personal projects. --- ::comments :: # Google Fonts in Nuxt with TailwindCSS The Google Fonts service is very easy to use. There is a large selection of fonts and an easy way to filter them according to your needs. The Nuxt ecosystem has a very good Google Fonts module you can easily integrate into your app, but I will show you a slightly different approach. In the [config](https://nuxt.com/docs/api/nuxt-config#head){rel="nofollow"} Nuxt allows us to handle the **Head** piece of HTML. There are other places you can do this, but for this, I will use the `nuxt.config.ts` configuration file. When you choose a font from Google, it provides you with code to add to your app. The code looks like this: ```html ``` To add this piece of code to `nuxt.config.ts` you need to split it into parts in the `link` array in the configuration. ```typescript // nuxt.config.ts export default defineNuxtConfig({ app: { head: { link: [ { rel: "preconnect", href: "https://fonts.googleapis.com" }, { rel: "preconnect", href: "https://fonts.gstatic.com", crossorigin: "" }, { rel: "stylesheet", href: "https://fonts.googleapis.com/css2?family=Roboto&display=swap" } ] } } ``` An extremely simple and elegant solution. On build, the above code is injected into the Head of your app and the fonts are loaded from Google. This works great, but sometimes it's not enough. For example, if you generate your app statically with `npx nuxt generate`. Then it is good to think about how to optimize the loading of the fonts because they can reach quite large volumes. It is easily done by initially changing the value of the `rel` attribute and after calling the `onload` event we restore it. ```typescript // nuxt.config.ts export default defineNuxtConfig({ app: { head: { link: [ { rel: "preconnect", href: "https://fonts.googleapis.com" }, { rel: "preconnect", href: "https://fonts.gstatic.com", crossorigin: "" }, { rel: "preload", as: "style", onload: "this.onload = null; this.rel = 'stylesheet';", href: "https://fonts.googleapis.com/css2?family=Roboto&display=swap" } ] } } ``` Once the font is loaded we need to promote it to the app. In my case I use **TailwindCSS**. TailwindCSS allows you to use pre-made [font families](https://tailwindcss.com/docs/font-family){rel="nofollow"}, but they have also provided an easy way to reconfigure them in the `tailwind.config.js` configuration file. ```javascript // tailwind.config.js /** @type {import('tailwindcss').Config} */ export const theme = { fontFamily: { "sans": ["Inter", "sans-serif"], "serif": ["Playfair Display", "serif"], }, }; ``` Now the `font-sans` CSS class will draw your text with the **Roboto** font. --- ::comments{discussions="https://github.com/howbizarre/thoughts/discussions/6"} :: # The legacy of Barry My family had a dog. His name was **Barry Night**. We also called him **Baritosh**, **Baritoshev**, **Baritoshko**, **Torongash**, **Paliok**, **Chernio**, **Mr. President**, and others. A black standard poodle. About 12 kilograms. He lived exactly 3 years. He passed away on his birthday. Our beautiful pal is no longer here, but he left us with a legacy that I will share with all of you who have come across this page. In the morning, when we woke up, **Barry** would come to us. He wagged his tail, jumped on the bed, brought a toy, and was extremely happy. He was happy that the day was starting and he would be with us. It didn't matter to him how the previous day went, how the night went - his joy was boundless every morning. If we wanted to get out of bed, we had to give him a good dose of cuddling, play, and joy. If any of us disappeared from his sight, even for 5 minutes, when we returned, he would start a lively welcome. Joyful barking, jumping, wagging his tail, and inviting us to give him attention, love, and joy. In order to continue with our daily duties, we had to give him enough attention, love, and joy. When we became too serious or busy with our obligations, he would come to us. He nudged us with his nose or paw to get our attention. And if he succeeded, he would bring one of his favorite toys with endless joy and start a playful game. If he didn't succeed, he didn't worry - he would lie down next to or on top of us and snuggle in anticipation. There are countless similar stories with our pal. We could probably tell 1001, maybe even more. But what happened to us when **Barry** was around were the volcanoes of joy and love that erupted from our hearts - not just towards him - towards everything around us. --- That's why I'm writing these few lines, to share with you that it wasn't Barry who placed joy and love in us. He only taught us how to discover and share it. And now, when he's gone, I share with you the legacy he left us: > Every creature on this world is born with a volcano of joy and love in its heart. It's not easy to activate them, and many of us need conductors like **Barry**, but they are there. Experience joy when you see loved ones - never miss it. Experience joy in the morning, as if they haven't been there for an eternity. Show them love for everything they do. Embrace them, kiss them, and rejoice in their presence. The volcanoes in our hearts are infinite. They will never run out. Until soon, our little pal. We will meet in endless fields - and know that we will bring the green ball. --- ::comments :: # One entry point for multiple sites We have an application that represents a microsite, and when you load it, you see a login page. Our clients provide it to their users. After a user logs in, data related to the client they belong to and the permissions assigned by our client are loaded. With the development of the application, our clients started requesting the microsite to work on their own domain. To carry their brand. To add "rich" content to the otherwise "plain" login screen. To expand its functionalities - and most of the requested functionalities were highly personalized. We had two options - to separate the application for each client and add everything the client needs, or to add additional configuration that allows the application to be personalized. In both cases, the server-side *back-end* would not change. Only the *front-end* part would be personalized according to the client. We quickly dismissed the first option. We are a small team and maintaining multiple installations and versions would require resources that we didn't want to allocate. That's why we decided to go with a single entry point for all clients and different configurations for each. We call it **PROFILES**. --- ## Profiles > How do we recognize the profile? This was the first question that stood before us. When a user loads the application, it should start showing different fonts, logos, background images, etc. on the login screen. We immediately needed to know if the client supports more than one language, if additional controls are loaded, such as registration, feedback form, etc. Then we decided that based on the domain that calls the application, we will tell the *back-end* which profile it is. However, this led to a small, but unpleasant, fluctuation of the application because it loaded something common to all and after the *back-end* received the domain, we performed a *postback* redirection of the application to the specific profile. To avoid this flicker - we moved the logic to the *front-end* part of the application. In the `main.ts` file, we turn to the *back-end* to return the profile identifier. ```typescript // main.ts const profileId: string = await whatMyProfileIs("api/profile/id"); ``` Just that - a super-fast operation that returns a string identifier. No *postback*, no 301 redirection, no unnecessary requests to the *back-end*. How *back-end* understands what the context is, I will tell you some other time. The next step is to activate the profile in the application. Just to add - the application has an administrative part that allows us and to some extent the client, to configure the profile. So when we get the profile identifier, we load its configuration. We use a function in the `main.ts` file. ```typescript // main.ts async function loadClientConfiguration(configStore: string): Promise { try { const response = await fetch(configStore); const config = await response.json(); return config; } catch (error) { Logger.fatal("Failed to load client configuration", error); } } ``` Then we load the configuration and pass it to the application. ```typescript // main.ts const config = await loadClientConfiguration(`/tote/${profileId}.json`); Object.freeze(config); app.provide("clientConfig", config); app.provide("clientId", profileId); ``` This is the entire logic for loading the profile and propagating it in the application. ## Routing Each profile, except the default one, loads its own routing. With it, we can easily activate additional plugins for the *Vue* engine. We add the additional router to the standard and filter it based on the profile. Then we add it to the main routing. ```typescript // router/index.ts import { customRoutes } from "@/router/custom"; const customRoute: Array = clientId ? customRoutes[clientId] : []; if (customRoute?.length > 0) { routes.push(...customRoute); } const history = createWebHashHistory(); const router = createRouter({ history, routes }); ``` Easy to add, easy to maintain. It provides freedom for expansion, for each profile, regardless of the others. It works fast and without issues. I simplified the example a bit, but it gives a good idea of how you can extend the usage of *Vue Router*. With this implementation, we encountered a small problem. When activating the *Vue* engine in `main.ts`, we add the following lines: ```typescript // main.ts import { createApp } from "vue"; import App from "@/App.vue"; import router from "@/router"; import { whatMyProfileIs } from "@/endpoints/profile"; import Logger from "@/system/fs/workers/logger"; const profileId: string = await whatMyProfileIs("api/profile/id"); const config = await loadClientConfiguration(`/tote/${profileId}.json`); Object.freeze(config); const app = createApp(App); app.provide("clientConfig", config); app.provide("clientId", profileId); app.use(router); app.mount("#app"); async function loadClientConfiguration(configStore: string): Promise { try { const response = await fetch(configStore); const config = await response.json(); return config; } catch (error) { Logger.fatal("Failed to load client configuration", error, { "predef": 500 }); } } ``` This way, *Vue Router* & *Vue App* load independently and the profile doesn't reach the router on time. That's why we created an additional function and asynchronously load the router there. ```typescript // main.ts import { createApp } from "vue"; import App from "@/App.vue"; import { whatMyProfileIs } from "@/endpoints/profile"; import Logger from "@/system/fs/workers/logger"; BigBang(); async function BigBang(): Promise { const profileId: string = await whatMyProfileIs("api/profile/id"); if (profileId) { const app = createApp(App); const config = await loadClientConfiguration(`/tote/${profileId}.json`); Object.freeze(config); const { default: router } = await import("@/router"); app.use(router); app.provide("clientConfig", config); app.provide("clientId", profileId); await router.isReady(); app.mount("#app"); } } async function loadClientConfiguration(configStore: string): Promise { try { const response = await fetch(configStore); const config = await response.json(); return config; } catch (error) { Logger.fatal("Failed to load client configuration", error, { "predef": 500 }); } } ``` The whole solution for one entry point and multiple sites worked with these few simple changes. --- ::note I am omitting some of the object implementations in the article - they are not essential but only serve to suggest what business logic is behind them. :: --- ::note "I use the term **back-end** quite loosely. I wondered if it might just be a server, but in our case, that's not quite the correct definition. :: --- ::comments{discussions="https://github.com/howbizarre/thoughts/discussions/7"} :: # Parcel & Robots There are different ways to tell **ParcelJS** which files are static and should not go through the build transformation, but if there are only a few, you can activate the transformers plugin and then include them directly in the build script in the `package.json` file. ```json // package.json { "scripts": { "build": "parcel build src/index.html src/robots.txt src/favicon.ico" } } ``` This way, `robots.txt` and `favicon.ico` will not be processed by [ParcelJS](https://parceljs.org/){rel="nofollow"} and will be directly transferred to the build directory. ## Transformers plugin To make the above build script work properly, you need to add the following code to the `.parcelrc` file: ```json // .parcelrc { "extends": "@parcel/config-default", "transformers": { "*.{txt,ico}": ["@parcel/transformer-raw"] } } ``` So *ParcelJS* will not add hashes to the names of the files, but also the linked objects in the files being processed will not be changed. --- ::comments{discussions="https://github.com/howbizarre/thoughts/discussions/8"} :: # Vue emits with parameters Passing events in **Vue** from a component back to the one that calls it is done with **emits**. The emits can also be done with **Pinia** or another library for state management, but this will be for another article. ## What is a page and what is a component? I am often asked: '*Should I extract this code into components or leave it on the page?*'. I have built myself a basic rule - if I have the question, whether this code should become a component, it should become a component. I also group the components by certain characteristics. I often give the [@layer](https://tailwindcss.com/docs/functions-and-directives#layer){rel="nofollow"} directive of **TailwindCSS** as an example when someone wonders how to group their components. I divide the components into 2 types: 1. **Speculators**: Those that do not perform any business logic, but only draw and/or transfer data; 2. **Workers**: Those that perform secondary processing of the incoming parameters and add business logic, which they return to the calling component. **Workers** can often call other **workers** or **speculators**, while **Speculators** usually work independently. ## How are parameters passed? Let's make an example. We have a component (**Speculator**) that shows on the screen how many more product pages are left before the last page is reached. We call it `LoadMore.vue`. The input parameters of the component are 2: '**which number is the current page**' and '**how many are the total number of pages**'. The component does not perform any business logic. It calculates how many more pages are left and draws them. ```vue // components/utilities/LoadMore.vue ``` ### Direct event passing Let's rework our component a bit so that when it is clicked, it passes the event to the calling component, which fires a function. ```vue // components/utilities/LoadMore.vue ``` This is a direct way to pass the event. When the button is clicked, the `loadMore` event and the `page` parameter are passed to the calling component. ```vue // pages/Products.vue ``` I always use **kebab-case** for emitted events. ### Define emit When you want to make some changes to the parameters you are passing before emitting the event, you will need to define the emit. ```vue // components/utilities/LoadMore.vue ``` This way we now have a lot of control over the output parameters. ```vue // pages/Products.vue ``` This example is just illustrative. If a similar change to the page number is being made, it should be logic in the calling component. But for our example, it's a great way to see how you can take control of the returned data. --- ::comments{discussions="https://github.com/howbizarre/thoughts/discussions/9"} :: # Nitro, i18n and Dev Proxy When you use **Nuxt**, it is normal to use **Nitro** as well, but sometimes this does not fit into the scenario you have been prepared for. The API requests are handled by another back-end server and in order to develop the application locally, a proxy must be set up. Nitro has a description in the documentation on how to set up the devProxy, and everything works fine until I encountered a Nuxt application with an active **i18n** module with several language localizations and configured in **prefix** strategy. With the prefix strategy, the URL address is automatically replaced and the devProxy stops working. ```javascript // nuxt.config.ts export default defineNuxtConfig({ nitro: { devProxy: { "/~": { target: "http://127.0.0.1:3243/", }, }, }, }); ``` In our case, all requests starting with `/~`, for example: `/~/api/request/method`, Nitro redirects them to another back-end server. But when the language culture `/en/~/api/request/method` is unexpectedly added to the address and Nitro stops communicating with the other server. That's why we quickly enriched the devProxy configuration. ```javascript // nuxt.config.ts export default defineNuxtConfig({ nitro: { devProxy: { "/bg/~": { target: "http://127.0.0.1:1818/", }, "/en/~": { target: "http://127.0.0.1:1818/", }, "/~": { target: "http://127.0.0.1:1818/", }, }, }, }); ``` The idea was to confirm that this will work, but so far I have not found another way. The problem comes when adding a new language localization. Each one must be added to the devProxy configuration. Drop a line if you know a more cultured way. --- ::comments{discussions="https://github.com/howbizarre/thoughts/discussions/10"} :: # Dynamic manifest.json If you want the users of your web application to "install" it on their devices, it is enough to fill in the `manifest.json` file. It is already widely supported by the browsers and the operating systems and it is very easy to do it, so you should not skip it. I maintain a **Vue** application that communicates with a **SaaS** system. The application is generated almost statically, without having a back-end behind it and has one entry point, but depending on the site that loads it, I save a variable in **LocaleStorage** that contains the identifier of this site. The application has a built-in `manifest.json`, but over time I had to personalize it for each site. The dynamic loading of the `manifest.json` file should be done at the client (in the browser). We excluded **Vue & Vue Router** from the calculations, because the manifest must be available, even if their 'engines' do not work. So, we transferred everything to the `index.html` file of the application. I wrote a small **Node** console that goes through the active databases and generates a static `manifest.siteId.json` files for each site in a specific folder. Then I added a small script to the `index.html` file that loads this file and adds it to `document.head`. ```html ``` ## Why fetch? This is a very good question. When generating the `manifest.siteId.json` files, there may be a situation where such a file does not exist. Since **JavaScript** cannot check for a file on the server - I make a **fetch** request for it. If the request does not work, then I load the `manifest.json` file with the basic descriptions. --- ::comments{discussions="https://github.com/howbizarre/thoughts/discussions/11"} :: # Removing the trailing slash from the URLs in Nuxt Every page on your website should have unique content. If you have a second page with the same content, the weight of the information for the search engines is divided between the two, and this greatly reduces their chances of appearing earlier in the search results. If you have a page that loads at `https://example.com/page`, it is very likely that the same page will also load at `https://example.com/page/`. From our point of view, this is the same page, but for search engines, these are two different addresses. They expect to have different content on them. One of the ways to tell the search engines which content should be considered is the canonical addresses of the pages. I will pay attention to another - to tell the search engines that the 'wrong' address has been moved. As you understand, this should happen even before the crawling machine loads our page. In **Nuxt**, this can be done with a small **middleware**. ```typescript // middleware/remove-trailing-slash.global.ts export default defineNuxtRouteMiddleware((to) => { if (to.path === "/" || !to.path.endsWith("/")) return; // --> const removedSlash = to.path.replace(/\/+$/, "") || "/"; const seoRoute = { path: removedSlash }; return navigateTo(seoRoute, { redirectCode: 301 }); }); ``` We add **global** to the middleware's name to run it before loading each page, without the pages needing to call it explicitly. --- ::comments{discussions="https://github.com/howbizarre/thoughts/discussions/12"} :: # Log info with Web Workers from Vue 3 to the server **Web Workers** are small, powerful scripts that run in the browser's background. And because they don't interfere with the rendering of your application, you can load them with various tasks to perform. I use Web Workers in two ways - in the first one, they don't return information to the application and after finishing the work, they self-destruct. In the second one, they return a result and then the application takes care of when and if the Web Worker should be destroyed. Here we will look at only the first way of working. I will write a separate article for the second one. ## LOG WORKER I most often use the first way of working for Web Workers to log information from the application to the server, but not only. I create a directory called `workers` and in it, I create a file - in our case `LOGS.ts`. The file is always in capital letters, so I can easily recognize it when importing it, and has a speaking name. Over time, I have concluded that every type of task that a given Web Worker performs should be in a separate file. ```typescript // workers/LOGS.ts import axios from "axios"; self.onmessage = async (event) => { const { message, code, type } = event.data; await axios.post("/api/log", { type: `${message}`, statusCode: code }, { contentType: "text/plain" }); /** * Log Worker does not return data, * so we close the worker after the POST request, * even if there is an error with writing to the server, * the worker will close. */ self.close(); }; ``` To use Web Workers in your application, you need to be very careful with its import. For **Vue 3** with **Composition API** and ` ``` --- ::comments{discussions="https://github.com/howbizarre/thoughts/discussions/13"} :: # Electron, TypeScript & Parcel Documentation for **Electron** is entirely in **JavaScript**, but that doesn't stop you from using **TypeScript** to generate that JavaScript. A few simple rules must be followed, mainly in the file loading paths. I have also prepared a small addition for the front-end part. Instead of the standard Electron HTML page, I will make a small compilation with **Parcel**. ## The Project First, we will organize our project. It will have 2 subfolders - one for the Electron part, the other for the Browser part. We create a folder `electron-typescript-parcel` and open it in [**VSCode**](https://code.visualstudio.com/){rel="nofollow"} - or whichever editor you use. Open the built-in terminal in VSCode (or another if you don't use VSCode) and execute: ```bash npm init -y ``` This will create a `package.json` file in the `electron-typescript-parcel` folder. Open the file and edit the `author` field. As a start, it is enough. Next, we need to add the Electron module to the project. ```bash npm install --save-dev electron ``` If you are going to use **GIT**, now is a good time to execute: ```bash git init ``` and add a `.gitignore` file. Add `node_modules` as a start. Then add 2 folders - `electron` and `browser` in the project folder. As the names suggest - Electron will live in the first, and the front-end part for the browser will live in the second. ## Electron Through the terminal, enter the `electron` folder and execute: ```bash npm init -y ``` and immediately after that add the TypeScript module: ```bash npm install --save-dev typescript ``` Load `package.json`. In the **script** part, add `"build": "tsc"` and remove the `"main"` attribute. ```json // electron/package.json { "name": "electron", "version": "1.0.0", "scripts": { "build": "tsc" }, "keywords": [], "author": "", "license": "ISC", "description": "", "devDependencies": { "typescript": "^5.5.4" } } ``` In the console execute: ```bash npx tsc --init ``` This will create a `tsconfig.json` file. In it, you will need to find `"outDir"`, uncomment the line, and set `"outDir": "../dist"`. From here, we follow the standard steps for creating a basic Electron application, skipping the part about creating the `index.html` file and `renderer.js` file, which we will add through Parcel. Add a `main.ts` file to the `electron` folder and write the following code: ```typescript // electron/main.ts import { app, BrowserWindow, ipcMain, nativeTheme } from "electron"; import path from "node:path"; /** * Creates a new window and loads an HTML file. */ const createWindow = (): void => { const mainWindow = new BrowserWindow({ width: 800, height: 600, webPreferences: { preload: path.join(__dirname, "./preload.js"), }, }); mainWindow.loadFile("./dist/index.html"); ipcMain.handle("dark-mode:toggle", () => { if (nativeTheme.shouldUseDarkColors) { nativeTheme.themeSource = "light"; } else { nativeTheme.themeSource = "dark"; } return nativeTheme.shouldUseDarkColors; }); }; app.whenReady().then(() => { createWindow(); app.on("activate", () => { if (BrowserWindow.getAllWindows().length === 0) createWindow(); }); }); app.on("window-all-closed", () => { if (process.platform !== "darwin") app.quit(); }); ``` This is an [example from the Electron documentation](https://www.electronjs.org/docs/latest/tutorial/dark-mode){rel="nofollow"} that changes the dark or light theme of the application. Next, we add a `preload.ts` file to the `electron` folder and write the following code: ```typescript // electron/preload.ts import { contextBridge, ipcRenderer } from "electron/renderer"; contextBridge.exposeInMainWorld("electronAPI", { toggle: () => ipcRenderer.invoke("dark-mode:toggle"), }); ``` ## Browser Through the terminal, we navigate to the `browser` folder and execute: ```bash npm init -y ``` After that, we install ***Parcel***: ```bash npm install --save-dev parcel ``` Loading `package.json`. In the **script** section, we add `"build": "parcel build index.html --dist-dir ../dist --no-source-maps --public-url ./ --no-optimize"` and remove the `"main"` attribute. Create an `index.html` file in the `browser` folder and write the following code: ```html Hello World!

Hello World!

Current theme source: System

``` Creating a `styles.css` file in the `browser` folder and writing the following code in it: ```css /* browser/styles.css */ @media (prefers-color-scheme: dark) { body { background: #333; color: white; } } @media (prefers-color-scheme: light) { body { background: #ddd; color: black; } } ``` Adding a `render.ts` file to the `browser` folder and writing the following code in it: ```typescript const toggleDarkMode = document.getElementById("toggle-dark-mode"); const themeSource = document.getElementById("theme-source"); if (themeSource && toggleDarkMode) { toggleDarkMode.addEventListener("click", async () => { // @ts-expect-error const isDarkMode = await window.electronAPI.toggle(); themeSource.innerHTML = isDarkMode ? "Dark" : "Light"; toggleDarkMode.innerHTML = `Toggle ${!isDarkMode ? "Dark" : "Light"} Mode`; }); } ``` To 'compile' the TypeScript file with Parcel, we will add a `.parcelrc` file in the folder and write the following: ```json // browser/.parcelrc { "extends": "@parcel/config-default", "transformers": { "*.ts": ["@parcel/transformer-typescript-tsc"] } } ``` ## Start We go back to the project folder and edit the **scripts** and **main** fields in the `package.json` file: ```json // package.json { "main": "./dist/main.js", "scripts": { "start": "npm run build --prefix ./electron && npm run build --prefix ./browser && electron ." } } ``` In the `.gitignore` file, we add the `dist` and `.parcel-cache` folders, and in the command line, we execute: ```bash npm start ``` After starting the application, a `dist` folder will appear in the project folder, containing all the code of the Electron application. I have created repositories for this project on [GitHub](https://github.com/howbizarre/electron-typescript-parcel){rel="nofollow"}. --- ::comments{discussions="https://github.com/howbizarre/thoughts/discussions/14"} :: # Vue Router & Main file When starting a new **Vue** project, I use the **Quick Start** section of the Vue website. Then, I make a few small changes before adding the project to the Source Control bank. From the questions that `npm create vue@latest` asks me, I almost always choose: ```shell √ Add TypeScript? ... no / YES √ Add Vue Router for Single Page Application development? ... no / YES √ Add Pinia for state management? ... no / YES √ Add an End-to-End Testing Solution? » Playwright ``` The others, if necessary during development. After executing all the instructions that appear on the screen, you get a project ready to start. The first thing I edit is the Main file - `src/main.ts`. At the beginning, it looks like this: ```typescript import './assets/main.css' import { createApp } from 'vue' import { createPinia } from 'pinia' import App from './App.vue' import router from './router' const app = createApp(App) app.use(createPinia()) app.use(router) app.mount('#app') ``` There is a small hidden secret here that you probably don't know. Sometimes, it may happen that the Vue application loads before the router is initialized. To avoid the appearance of such Vue jokes, I edit `src/main.ts`: ```typescript import "./assets/main.css"; import { createApp } from "vue"; import { createPinia } from "pinia"; import App from "./App.vue"; initializeApp(); async function initializeApp(): Promise { const app = createApp(App); const pinia = createPinia(); const { default: router } = await import("@/router"); app.use(router); app.use(pinia); await router.isReady(); app.mount("#app"); } ``` This way, the router is initialized before the Vue application is loaded. A tiny trick that will save you from possible future problems. --- ::comments{discussions="https://github.com/howbizarre/thoughts/discussions/4"} :: # Simple Vue plugin for geo location When I write a plugin, I think of it as a standalone piece of code that has its own logic and is independent of where it will be used. This is not entirely true, of course, but when designing a plugin, I always start from this idea. After all, every system has its own logic and architecture that must be followed - especially for outgoing data. ## Geo location In *modern browsers*, it is very easy to access the [Geolocation API](https://developer.mozilla.org/en-US/docs/Web/API/Geolocation_API){rel="nofollow"}. You call ```javascript navigator.geolocation.getCurrentPosition(call_success_function, call_error_function); ``` and in the object returned to `call_success_function` you will get everything you need, including **latitude** & **longitude**. If necessary, you can check if the browser supports the Geolocation API `if (!navigator.geolocation) { ... } else { ... }`. I will add a small abstraction to the standard function to make it work in an asynchronous environment. ```typescript async function GeoLocation(): Promise { return new Promise((resolve, reject) => { navigator.geolocation.getCurrentPosition( (success) => resolve(location.coords), (error) => reject(error) ); }); } ``` A wonderful method that will return the coordinates and (as I told you at the beginning) works regardless of the environment in which it will be used. ## Vue plugin In a project where we want to use the above method as a plugin, I create a folder `plugins` and in it I create a sub-folder `geo-location`. In this folder I add 2 files - `index.ts` and `GeoLocation.ts`. Of course, the content of `GeoLocation.ts` is the above method. ```typescript // plugins/geo-location/GeoLocation.ts export async function GeoLocation(): Promise { return new Promise((resolve, reject) => { navigator.geolocation.getCurrentPosition( (success) => resolve(location.coords), (error) => reject(error) ); }); } ``` In `index.ts` I follow the standard structure of Vue plugins. ```typescript // plugins/geo-location/index.ts import type { App } from "vue"; import { GeoLocation } from "./GeoLocation"; export default { install(app: App) { app.provide("GeoLocation", GeoLocation); }, }; ``` Then we add it to `main.ts`. ```typescript // main.ts import { createApp } from "vue"; import App from "./App.vue"; import GeoLocationPlugin from "./plugins/geo-location"; const app = createApp(App); app.use(GeoLocationPlugin); app.mount("#app"); ``` ## Using the plugin Here is the reason for writing this article. To use the plugin in a **TypeScript** environment, you need to be careful with the initialization in the Vue component where you will use it. ```typescript const GEOLocation = inject<() => Promise>('GeoLocation'); ``` If you omit the cast `() => Promise`, you will get an error that `GeoLocation` is not a function. --- ::note You can also define your own type: ```typescript type GeoLocationType = () => Promise; const GEOLocation = inject('GeoLocation'); ``` :: --- ::comments{discussions="https://github.com/howbizarre/thoughts/discussions/15"} :: # HB's Thoughts A simple blog system built with Nuxt 4, focused on articles about Vue, Nuxt, TailwindCSS, TypeScript, and front-end development. ## 📖 About HB's Thoughts is a personal blog featuring articles mostly about Vue, Nuxt, TailwindCSS, and TypeScript, but not limited to — more on the front-end and less on the back-end. The blog supports multiple languages (English and Bulgarian) and is optimized for performance and user experience. ## ✨ Features - **Modern Tech Stack**: Built with Nuxt 4, Vue 3, and TypeScript - **Multilingual Support**: Available in English and Bulgarian with i18n - **Content Management**: Powered by Nuxt Content for markdown-based articles - **Modern UI**: Styled with Nuxt UI and TailwindCSS - **Search Functionality**: Full-text search with Fuse.js - **Tag System**: Articles organized by tags and competencies - **SEO Optimized**: Server-side rendering with optimized meta tags - **Structured Data**: JSON-LD structured data for blog posts, listings, and breadcrumbs - **Cloud Deployment**: Deployed on Cloudflare Workers - **Responsive Design**: Mobile-first responsive layout ## 🛠 Tech Stack - **Framework**: [Nuxt 4](https://nuxt.com/){rel="nofollow"} - **Frontend**: [Vue 3](https://vuejs.org/){rel="nofollow"} with TypeScript - **Styling**: [TailwindCSS](https://tailwindcss.com/){rel="nofollow"} + [Nuxt UI](https://ui.nuxt.com/){rel="nofollow"} - **Content**: [Nuxt Content](https://content.nuxt.com/){rel="nofollow"} for markdown articles - **Internationalization**: [Nuxt i18n](https://i18n.nuxtjs.org/){rel="nofollow"} - **Search**: [Fuse.js](https://fusejs.io/){rel="nofollow"} for fuzzy search - **Database**: Better SQLite3 - **Deployment**: Cloudflare Workers - **Build Tool**: Vite ## 🚀 Getting Started ### Prerequisites - Node.js (v18 or higher) - npm or yarn - Wrangler CLI (for Cloudflare deployment) ### Installation 1. Clone the repository: ```bash git clone https://github.com/hristobotev/hbsthoughts.git cd hbsthoughts ``` 2. Install dependencies: ```bash npm install ``` 3. Start the development server: ```bash npm run dev ``` The site will be available at `http://localhost:7410` ## 📝 Available Scripts - `npm run dev` - Start development server on port 7410 - `npm run build` - Build the application for production - `npm run generate` - Generate static files - `npm run preview` - Build and preview with Wrangler - `npm run deploy` - Build and deploy to Cloudflare Workers - `npm run cf-typegen` - Generate Cloudflare types ## 📁 Project Structure ```bash ├── app/ # Nuxt app directory │ ├── components/ # Vue components │ ├── composables/ # Vue composables (JSON-LD, utilities) │ ├── layouts/ # Layout components │ ├── pages/ # Page components and routing │ └── assets/ # Static assets ├── content/ # Markdown content │ ├── bg/ # Bulgarian articles │ ├── en/ # English articles │ ├── seo/ # SEO configurations ├── i18n/ # Internationalization ├── public/ # Public assets └── server/ # Server-side code ``` ## 🌍 Content Management Articles are written in Markdown and stored in the `content/` directory: - `/content/en/articles/` - English articles - `/content/bg/articles/` - Bulgarian articles - `/content/en/static/` - English static pages (like help pages) - `/content/bg/static/` - Bulgarian static pages (like help pages) ### Article Format Each article follows this frontmatter structure: ```markdown --- title: "Article Title" date: "2024-02-06T12:01:53.293Z" draft: false tags: ["vue", "nuxt"] slug: "article-slug" navigation: false competence: "frontend" --- Article content here... ``` ## 🔍 SEO & Structured Data The blog implements comprehensive SEO optimization with JSON-LD structured data: ### JSON-LD Implementation The application includes three types of structured data using Schema.org vocabulary: 1. **Blog Listing** (`useJsonLdBlogListing`): - Generates `Blog` schema for article listing pages - Includes all articles with their metadata - Automatically updates when articles are loaded 2. **Blog Posts** (`useJsonLdBlogPost`): - Generates `BlogPosting` schema for individual articles - Includes author, publisher, dates, and article metadata - Supports optional featured images 3. **Breadcrumbs** (`useJsonLdBreadcrumbs`): - Generates `BreadcrumbList` schema for navigation - Works with Nuxt UI breadcrumb components - Handles multilingual routes and dynamic content ### Usage The JSON-LD composables are automatically imported and can be used in any page: ```vue ``` All structured data is reactive and updates automatically when content changes. ### Site Maps The blog generates a sitemap for SEO *(Search Engine Optimization)* purposes, which includes all articles and static pages. The sitemap is automatically updated when new content is added, thanks to the **Nuxt Sitemap** module. ## 🚀 Deployment The application is configured for deployment on Cloudflare Workers: 1. Configure Wrangler: ```bash npm run cf-typegen ``` 2. Deploy: ```bash npm run deploy ``` ## 🤝 Contributing 1. Fork the repository 2. Create a feature branch: `git checkout -b feature/amazing-feature` 3. Commit your changes: `git commit -m 'Add amazing feature'` 4. Push to the branch: `git push origin feature/amazing-feature` 5. Open a Pull Request ## 📄 License This project is licensed under the MIT License. ## 🙏 Acknowledgments - Built with [Nuxt 4](https://nuxt.com/){rel="nofollow"} - UI components from [Nuxt UI](https://ui.nuxt.com/){rel="nofollow"} - Content from [Nuxt Content](https://content.nuxt.com/){rel="nofollow"} - Multilingual support with [Nuxt i18n](https://i18n.nuxtjs.org/){rel="nofollow"} - Icons from [Heroicons](https://heroicons.com/){rel="nofollow"} - Fonts from [Google Fonts](https://fonts.google.com/){rel="nofollow"} - Back-end [Cloudflare Workers](https://workers.cloudflare.com/){rel="nofollow"} --- ::comments --- discussions: https://github.com/howbizarre/hbsthoughts/discussions/5 --- :: # Cloudflare & Email Binding You don't need to have an email server to send emails with Cloudflare Worker. Just let Cloudflare validate the email address you're using and Cloudflare will forward incoming emails to it. ## Requirements There are 3 things you need to have in your Cloudflare account to be able to send emails: 1. **Domain** - You need to have an active domain in your Cloudflare account. This doesn't mean you have to buy it from them - it's enough to set your DNS records to point to Cloudflare. 2. **Email** - You need to have an active email address. It doesn't have to be associated with the domain from the previous point. It can be a Gmail account or any other account you have full access to. This can also be the email you use to log in to Cloudflare. This email must be added to the *'Destination addresses'* in the *'Email Routing'* settings for the domain from previous point, which will make it active. 3. **Routing rules** - You need to have an email address added to the *'Routing rules'* in the *'Email Routing'*. This address is part of the domain from the first point and will be used to forward emails to the email from the previous point. I may have described it a bit complicated, but once you start adding the settings one after another, you will understand it correctly. ## Binding In your **Nuxt** project that uses **Cloudflare Worker**, there should be a `wrangler.jsonc` or `wrangler.toml` file for configuration. You need to add the following settings to it: ```json // wrangler.jsonc { "send_email": [ { "name": "INFO_EMAIL", // free text - it's good to have meaning "destination_address": "your@valid.email" // email from point 2 above } ] } ``` These are all the initial environment settings for the machine to work. And do not forget to update Cloudflare types. ```bash npx wrangler types ``` ## Usage The easiest way is to add an *API endpoint* to the **Nitro** environment. For example `/api/send-info-email`. This endpoint needs to initialize the Cloudflare binding so we can use it directly. This is the coolest part. It can be done in one line: ```js // Accessing Email binding const env = event.context.cloudflare?.env; ``` And as you might guess, `env` gives you full access to the Email binding and you can send emails at will. ```js // Sending an email await env.INFO_EMAIL.send({ sender, recipient, content }); ``` Extremely simple and incredibly elegant solution. **DX** to the **MAX**. --- ::comments --- discussions: https://github.com/howbizarre/hbsthoughts/discussions/12 --- :: # Cloudflare Tail Worker With **Cloudflare Tail Worker** you direct logging to one place - everything is in the ecosystem and in real time. Until recently I only used external tools like **Sentry**, **LogRocket** or **Loki** with **Grafana**, but setting them up requires knowing the processing and takes time. Very often, when you use *[**SaaS**] (Software as a Service)* services you don't have access to the logs - or if you do, the logs are heavily truncated. Then you configure your development environment as close as possible to production and try to simulate the errors or create a process to which you send the errors and from there amplify them to an external tool. Every Worker in Cloudflare has its own logs, but you don't have much control over them. And when you need to monitor more than one Worker, relative to the time when some event occurred in all these Workers, things get complicated. That's why you create, separately, a normal Worker, but the `async fetch(request, env, ctx)` function you rename to `async tail(events, env, ctx)` and you already have a defined *Tail Worker*. ```typescript // src/index.ts export default { async tail(events: TraceItem[], _env: unknown, _ctx: unknown) { if (!events || events.length === 0) { return; } console.log(`[TAIL] Received ${events.length} events`); for (const trace of events) { try { handleTraceItem(trace); // Your function for processing events } catch (error) { console.error(`[TAIL_ERROR] Error processing event: ${error instanceof Error ? error.message : String(error)}`); } } console.log(`[TAIL] Completed processing ${events.length} events`); } } satisfies ExportedHandler; ``` You name your Worker - for example: `tail-for-me-all-the-app-events`. You add to `wrangler.jsonc` the line ```json { // ... "observability": { "enabled": true } } ``` and upload it to Cloudflare. Done - you have an active Tail Worker. From here on, in each ***Producer Worker*** whose logs you want to monitor, you add to its `wrangler.jsonc`: ```json { // ... "tail_consumers": [ { "service": "tail-for-me-all-the-app-events" } ] } ``` Currently there's no limit on the number of Producer Workers that can send their logs to a given Tail Worker. You should know that above a certain CPU usage time, Cloudflare will charge you additionally. I mainly follow 2 rules - If two or more Producer Workers share at least one database - they use a common Tail Worker. - A Producer Worker uses more than one Tail Worker only if it has defined access rules. --- ::note I created a **GitHub Gist** with an [example of the `handleTraceItem` function](https://gist.github.com/howbizarre/2643b54a2af7c9494f8befe1fd1dd8ba){rel="nofollow"}. :: --- ::comments{discussions="https://github.com/howbizarre/hbsthoughts/discussions"} :: # Dynamically loading of Vue plugins in the main file I am developing a B2B application that has a single entry point but is used by different clients with different domains, appearances, and custom functionalities. Based on the domain and a few other tricks, I identify which configuration to load, and thus, although there is a single entry point, everything after it is highly personalized. One of the features of Vue is that plugins are registered globally, and if you need different plugins in different configurations, you will only need to load the ones you need. This can be done dynamically in the main file. ```ts // main.ts import Vue from 'vue'; const loadPlugin = (pluginName: string): Promise => { return import(`@/plugins/${pluginName}`).then((module) => { Vue.use(module.default); }); }; // Example usage if (client === 'MyPressureClient') { loadPlugin('my-pressure-client-plugin'); } else { loadPlugin('my-regular-client-plugin'); } // ... ``` In this example, the `loadPlugin` function takes a plugin name as an argument and dynamically imports the corresponding plugin module. So far so good, but when you have over 50 plugins and a given client uses around 10 of them, the scheme starts to get heavy and the parallelism of loading disappears. To handle this, I create an object that contains an array of plugins for each client and load them all at once with `Promise.all`. ```ts const plugins = clientConfig.plugins || {}; const pluginLoaders: Promise<{ name: string; plugin: any }>[] = []; if (plugins?.plugin-one) { pluginLoaders.push(import('@/plugins/plugin-one').then((m) => ({ name: 'PluginOne', plugin: m.default }))); } if (plugins?.plugin-two) { pluginLoaders.push(import('@/plugins/plugin-two').then((m) => ({ name: 'PluginTwo', plugin: m.default }))); } if (plugins?.plugin-three) { pluginLoaders.push(import('@/plugins/plugin-three').then((m) => ({ name: 'PluginThree', plugin: m.default }))); } // ... more plugins that the client uses, if any // You load all plugins in parallel (50-70% faster than sequential) const loadedPlugins = await Promise.all(pluginLoaders); // You register the plugins in order loadedPlugins.forEach(({ name, plugin }) => { app.use(plugin); }); ``` Elegant and efficient. **DX** at **MAX** :o ) --- ::comments{discussions="https://github.com/howbizarre/hbsthoughts/discussions"} :: # What is competence? Competence shows how familiar you should be with the technologies mentioned in the article. - [**None**](https://thoughts.bizarre.how/en/competence/none): No technical experience required; - [**Elementary**](https://thoughts.bizarre.how/en/competence/elementary): You know `Getting Started` by heart; - [**Pro**](https://thoughts.bizarre.how/en/competence/pro): You use the technology and its toolings; - [**Geek**](https://thoughts.bizarre.how/en/competence/geek): You have more answers than questions; You may think of it as the category of the article. An article has only one competence/category. ## What are tags? Tags, are an easy way to filter the articles you see in a given field, technology, idea, etc. I often write about a given technology or a set of technologies. Then the tags indicate what these technologies are. Sometimes they won't be related to technology, but to my thoughts or interesting things from the network or the life around me that I note. ## What are the technologies? The main technologies I write about are mostly focused on front-end, but there will also be content about back-end, tools, clouds, patterns, best practices, and more. [VueJS](https://vuejs.org/){rel="nofollow"}, [Nuxt](https://nuxt.com){rel="nofollow"}, [TailwindCSS](https://tailwindcss.com/){rel="nofollow"}, [TypeScript](https://www.typescriptlang.org/){rel="nofollow"}, [NodeJS](https://nodejs.org/){rel="nofollow"}, [NPM](https://www.npmjs.com/){rel="nofollow"}, [Vite](https://vitejs.dev/){rel="nofollow"}, [Parcel](https://parceljs.org/){rel="nofollow"}, [ExpressJS](https://expressjs.com/){rel="nofollow"}, [MongoDB](https://www.mongodb.com/){rel="nofollow"}, [GraphQL](https://graphql.org/){rel="nofollow"}, [Firebase](https://firebase.google.com/){rel="nofollow"}, [PM2](https://pm2.keymetrics.io/){rel="nofollow"}, [NginX](https://www.nginx.com/){rel="nofollow"} and more... ## Who am I? > I am a Front-end Senpai, who strictly follows the W3Code of Bushido!. I like to use that phrase. Otherwise, I am **Kosta**, also known as **HowBizarre**, and together with my family, I work and live in **Sofia**, **Bulgaria**. You can check out my profiles on [GitHub](https://github.com/howbizarre){rel="nofollow"}, [X](https://x.com/howbizarre){rel="nofollow"} and [LinkedIn](https://www.linkedin.com/in/howbizarre){rel="nofollow"}. ## FYI For translation from my native language, Bulgarian, to English, I use some AI helpers such as **DeepL**, **CoPilot**, **Gemini**, and **Google Translate** with corrections, and adjustments. I am not a native English speaker, so please let me know if you find any mistakes. Thank you! ## A bit about Artificial Intelligence [Short index](https://thoughts.bizarre.how/llms.txt){rel="noopener"} of the site to assist Language Models. :br [Extended index](https://thoughts.bizarre.how/llms-full.txt){rel="noopener"} of the site, which includes the content of the articles.