{"id":15118499,"url":"https://github.com/kachkaev/website","last_synced_at":"2025-10-17T00:10:13.634Z","repository":{"id":142144214,"uuid":"603448532","full_name":"kachkaev/website","owner":"kachkaev","description":"Personal mini-website built with Next.js, React, TypeScript, TailwindCSS and Playwright","archived":false,"fork":false,"pushed_at":"2025-10-02T23:37:25.000Z","size":3081,"stargazers_count":12,"open_issues_count":4,"forks_count":0,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-10-03T01:24:30.733Z","etag":null,"topics":["next-intl","nextjs","nextjs-app","nextjs-appdir","playwright","react-app","tailwind","tailwind-applications","tailwindcss","typescript-app"],"latest_commit_sha":null,"homepage":"https://kachkaev.uk","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"bsd-3-clause","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/kachkaev.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE.md","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2023-02-18T14:48:42.000Z","updated_at":"2025-10-02T23:36:10.000Z","dependencies_parsed_at":"2023-09-22T09:43:30.460Z","dependency_job_id":"51888d07-619b-4ce3-849e-6930bb4615e2","html_url":"https://github.com/kachkaev/website","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/kachkaev/website","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kachkaev%2Fwebsite","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kachkaev%2Fwebsite/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kachkaev%2Fwebsite/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kachkaev%2Fwebsite/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/kachkaev","download_url":"https://codeload.github.com/kachkaev/website/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kachkaev%2Fwebsite/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":279258279,"owners_count":26135664,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-10-16T02:00:06.019Z","response_time":53,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["next-intl","nextjs","nextjs-app","nextjs-appdir","playwright","react-app","tailwind","tailwind-applications","tailwindcss","typescript-app"],"created_at":"2024-09-26T01:46:18.089Z","updated_at":"2025-10-17T00:10:13.627Z","avatar_url":"https://github.com/kachkaev.png","language":"TypeScript","funding_links":[],"categories":["TypeScript"],"sub_categories":[],"readme":"# [kachkaev.uk](https://kachkaev.uk) / [.ru](https://kachkaev.ru)\n\nThis repository contains the source code of my personal mini-website.\nIt represents a simple instance of a\u0026nbsp;Next.js app with server components, API routes and domain-based internationalization.\nFeel free to explore this repository to learn something new or even to reuse its code in your own projects.\n\n## Key ingredients\n\n- **[Next.js](https://nextjs.org)** (with `/app` directory) as the architecture framework\n- **[React](https://reactjs.org)** to define the UI (server and client components)\n- **[Tailwind CSS](https://tailwindcss.com)** to style the UI (including dark/light themes)\n- **[FormatJS](https://formatjs.io)** to handle internationalization ([ICU](https://formatjs.io/docs/core-concepts/icu-syntax/) plurals etc.)\n- **[Google Analytics](https://analytics.google.com)** to track website usage\n- **[Zod](https://zod.dev)** and **[Playwright](https://playwright.dev)** to update profile infos\n- **[ESLint](https://eslint.org)**, **[Markdownlint](https://github.com/DavidAnson/markdownlint)**, **[Prettier](https://prettier.io)** and **[TypeScript](https://www.typescriptlang.org)** to statically check and autocorrect source files\n- **[pnpm](https://pnpm.io)** to manage dependencies\n- **[Docker](https://www.docker.com)** to generate a deployable production artifact\n- **[Kubernetes](https://kubernetes.io)** to run the app in production\n- **[GitHub Actions](https://github.com/features/actions)** to run [CI/CD](https://en.wikipedia.org/wiki/CI/CD) pipelines\n\n## Project structure\n\nThe codebase is inspired by these Next.js examples:\n\n- [app-dir-i18n-routing](https://github.com/vercel/next.js/tree/canary/examples/app-dir-i18n-routing)\n- [with-tailwindcss](https://github.com/vercel/next.js/tree/canary/examples/with-tailwindcss)\n- [with-typescript](https://github.com/vercel/next.js/tree/canary/examples/with-typescript)\n\nThe main web page renders a list of profiles and shows relevant stats.\nThe numbers are taken from a few [YAML](https://en.wikipedia.org/wiki/YAML) files that are stored locally.\nThanks to React server components, these files can be read directly by the Node.js process and there is no need to introduce any React state to re-hydrate profile infos on the client.\nThis simplifies the architecture and reduces the amount of the JavaScript to for browsers to download.\n\nProfile infos are updated inside Next.js API routes.\nA GET request to `/update-profiles/[profile-name]?[security-token]` scrapes a third-party service (e.g. GitHub, OpenStreetMap, etc.) and updates a\u0026nbsp;corresponding YAML file.\nSome profile infos are generated by making lightweight HTTP requests to JSON endpoints.\nHowever, the most common approach involves using [Playwright](https://playwright.dev) which is a browser automation library.\nThis approach is used when it is easier to scrape a web page than to interact with an official third-party\u0026nbsp;API.\n\nProfiles are updated on a schedule (see [Deployment](#deployment) section).\nBecause `/update-profiles/*` endpoints are public, a security token is introduced to prevent unauthorised requests.\n\n## Known issues\n\n- **Internationalized (i18n) is hacky**  \n  My go-to solution for internationalising Next.js pages is [`next-i18next`](https://www.npmjs.com/package/next-i18next).\n  Because this package is incompatible with the `/app` directory (at least as of early March 2023), I have used a rather bare-bones approach inspired by the [app-dir-i18n-routing](https://github.com/vercel/next.js/tree/canary/examples/app-dir-i18n-routing) example.\n  The current solution is not as polished as `next-i18next` when it comes to propagating translations to components, but it works well enough for my needs.\n  I might consider following [i18next/next-13-app-dir-i18next-example](https://github.com/i18next/next-13-app-dir-i18next-example) in the future but I am generally waiting for this space to mature.\n\n  In the meantime, I use [`@formatjs/intl`](https://www.npmjs.com/package/@formatjs/intl) to handle plurals, which is somewhat low-level and should not be done without a wrapper in a Next.js app.\n  Ideally, I would like to have i18n resources available as React context and use components inside i18n strings (e.g. `Hello \u003ca\u003eworld\u003c/a\u003e!`).\n  The latter is possible with [`\u003cTrans /\u003e` component](https://react.i18next.com/latest/trans-component) in `react-i18next`, which I hope to use at some point.\n\n- **Custom 404 page is implemented via `middleware.ts`**  \n  As of early March 2023, Next.js [does not support](https://beta.nextjs.org/docs/api-reference/file-conventions/not-found) custom 404 pages inside the `/app` directory.\n  Until a\u0026nbsp;permanent solution is available, incoming requests are checked against `existingPathnamePatterns` in\u0026nbsp;[`middleware.ts`](./middleware.ts).\n  This enables custom 404 pages which are i18n-aware, but requires manual updates to `existingPathnamePatterns` each time a new app route is added.\n  Thus, the current workaround is error-prone, especially for apps that have a lot of routes.\n\n- **Progress bar for page navigation may need improvement**  \n  I like using [`nprogress`](https://www.npmjs.com/package/nprogress) in apps with client-side navigation between pages.\n  A progress bar improves the perceived performance of the app and makes it feel more responsive.\n  Unfortunately, established approaches to integrating `nprogress` with Next.js do not work with server components.\n\n  I hope to find a good solution to this problem in the future, but in the meantime, I have implemented a custom solution in [`app/[locale]/layout/next-app-nprogress.tsx`](app/[locale]/layout/next-app-nprogress.tsx).\n\n  Related issues:\n  - https://github.com/vercel/next.js/issues/45499\n  - https://github.com/apal21/nextjs-progressbar/issues/86\n\n## Local development\n\n### Getting started\n\n1.  Open a command line and ensure you have [git](https://git-scm.com) and [Node.js](https://nodejs.org) installed:\n\n    ```sh\n    git --version\n    ## ≥ 2.30.0\n    \n    node --version\n    ## ≥ 18.12.0\n    \n    corepack --version\n    ## ≥ 0.14.0, comes with Node.js\n    ```\n\n1.  Clone the repo from GitHub:\n\n    ```sh\n    cd PATH/TO/MISC/PROJECTS ## replace example path with a directory of your choice\n    git clone https://github.com/kachkaev/website.git\n    cd website\n    ```\n\n1.  Prepare [pnpm](https://pnpm.io) for dependency management:\n\n    ```sh\n    corepack enable \u0026\u0026 corepack prepare --activate\n    \n    pnpm --version\n    ## same as in package.json → packageManager\n    ```\n\n1.  Install dependencies:\n\n    ```sh\n    pnpm install\n    ```\n\n1.  Copy `.env` to `.env.local`:\n\n    ```sh\n    cp .env .env.local\n    ```\n\n    Unlike `.env`, `.env.local` is not tracked by git and can therefore be used to store sensitive environment variables (security tokens, etc.).\n    We will need this file later.\n\n1.  Start Next.js in development mode:\n\n    ```sh\n    pnpm dev\n    ```\n\n    If you see a home page with empty profiles at [localhost:3000](http://localhost:3000), congratulations!\n    Modifying source files will automatically refresh the app.\n    To stop running the dev server, press `ctrl+c`.\n\n1.  If you want to try a copy of the app that is optimized for production, you can build and start it like this:\n\n    ```sh\n    pnpm build\n    pnpm start\n    ```\n\n### Updating profile infos\n\nTo be able to update profile infos locally, you will need to define an environment variable named `UPDATE_PROFILE_SECURITY_TOKEN` .\nTo set it to `123`, add the following line to `.env.local`:\n\n```sh\nUPDATE_PROFILE_SECURITY_TOKEN=123\n```\n\nOnce you have saved `.env.local` and have restarted the dev server (`pnpm dev`), you can update profile infos by making GET requests to `/update-profiles/[profile-name]?123`.\nIf a request is successful, the app will create or update a corresponding file named `data/profile-infos/[profile-name].yaml`.\nThe contents of this file are then used to render profile info on the home page.\n\nThe list of available profiles can be found in [`app/[locale]/update-profiles/`](app/[locale]/update-profiles/).\nNote that updating Flickr profile requires API authentication, so requests to `/update-profiles/flickr?123` will fail without valid values for `FLICKR_USER_ID` and `FLICKR_API_KEY` inside `.env.local`.\n\n### Playing with i18n\n\nInternationalization (i18n) is setup in [`i18n-config.ts`](i18n-config.ts), [`i18n-server.ts`](i18n-server.ts) and [`middleware.ts`](middleware.ts).\n\nBy default, requests to [localhost:3000](http://localhost:3000) map to the `en` locale and requests to [ru.localhost:3000](http://ru.localhost:3000) map to the `ru` locale.\nYou can change this by setting `BASE_URL_RU` and `BASE_URL_EN` in `.env.local`.\nFor example, if you add `BASE_URL_RU=http://localhost:3000`, requests to [localhost:3000](http://localhost:3000) will be mapped to the `ru` locale.\nJust like with any other changes in `.env.local`, you will need to restart the dev server (`pnpm dev`) for the new values to be read.\n\nNote that `localhost` subdomains [need to be configured on your machine](https://stackoverflow.com/q/19016553/1818285) to become resolvable.\n\n## Quality checks\n\n### Linting\n\nCodebase integrity is continuously checked with several [linting](\u003chttps://en.wikipedia.org/wiki/Lint_(software)\u003e) tools.\nYou can find them in [`package.json`](`package.json`) under `scripts` → `lint:*`.\nTo run a specific linter, use `pnpm lint:\u003clinter-name\u003e` (e.g. `pnpm lint:eslint`).\nTo run all linters, use `pnpm lint`.\n\nThe linters examine the codebase from different angles and help with early detection of potential issues.\nThey are also used to maintain a consistent code style.\nSome linters provide autofixes, which can be applied with `pnpm fix:\u003clinter-name\u003e` (e.g. `pnpm fix:eslint`).\n\nAll linters are executed as part of the CI pipeline ([`.github/workflows/ci.yaml`](.github/workflows/ci.yaml)).\nThey run for pull requests as well as pushes to the `main` branch.\n\n### Testing\n\nTODO implement\n\n## Deployment\n\nNext.js apps are often deployed to cloud-native environments such as [Vercel](https://vercel.com), [Netlify](https://netlify.com), [AWS Amplify](https://aws.amazon.com/amplify/), etc.\nThis makes them highly scalable and resilient to failures.\nOne limitation that cloud-native deployments impose on Next.js apps is to do with the the size of the [Lambda functions](https://en.wikipedia.org/wiki/AWS_Lambda).\nThese functions handle API requests and render React pages on the server side.\n\n\u003cimg src=\"https://gitlab.com/kachkaev/website/uploads/a416ccf87b7a1cd2e2bb386f8109f936/thinking.png\" alt=\"thinking meme face\" width=\"120\" /\u003e\u003cbr\u003e\n\nWith Playwright browser used in `/update-profiles/[profile-name]?[security-token]`, the size of some Lambda functions would exceed the [limit of 50 MB](https://docs.aws.amazon.com/lambda/latest/dg/gettingstarted-limits.html).\nBesides, deploying this mini-website to a cloud-native environment would make it harder for me to co-host it with other projects on the same domains.\n\nTo overcome these two limitations, I have decided to deploy the app to a [Kubernetes](https://kubernetes.io) cluster, in which I run most of my side projects.\n\n### Docker\n\nI use [Docker](https://www.docker.com) to make the Next.js app deployable to Kubernetes.\nYou can dockerize the app locally with this command:\n\n```sh\ndocker build --tag website .\n```\n\nOnce the container image is created, you can test it at [localhost:3000](http://localhost:3000) like this:\n\n```sh\ndocker run \\\n  --env-file=.env.local \\\n  --publish 3000:3000 \\\n  --rm \\\n  --volume $(pwd)/data:/data \\\n  website\n```\n\nThe ‘official’ website image is created from GitHub Actions ([`.github/workflows/generate-docker-image.yaml`](.github/workflows/generate-docker-image.yaml)) and is hosted on GitHub ([github.com/kachkaev/website/pkgs/container/website](https://github.com/kachkaev/website/pkgs/container/website)).\n\n### Kubernetes (K8s)\n\n\u003cimg src=\"https://gitlab.com/kachkaev/website/uploads/c0799225fbfc40e2c493ed290bc345d1/doing.png\" alt=\"concentrated meme face\" width=\"120\" /\u003e\n\n[Kubernetes](https://kubernetes.io) is an open platform for running custom cloud-native workloads.\nJust as any K8s deployment, this mini-website is described in yaml files which are located in [`k8s`](k8s) directory of this repo.\nThese yamls can serve as examples for deploying similar Next.js apps with ‘heavy’ API handlers.\nSuch handlers would be [slow inside Lambda functions](https://github.com/orgs/vercel/discussions/496) or even exceed their size limit.\n\nThe commands in this section assume that a Kubernetes cluster is already setup, `kubectl` client is configured against it and the current Kubernetes user is able to create resources in the `website` namespace.\nIt is also assumed that the cluster’s [ingress controller](https://kubernetes.io/docs/concepts/services-networking/ingress/) is in place, so the creation of `Ingress` objects leads to exposing Kubernetes services to the outer world via HTTP and HTTPS.\nIf you are not using [Traefik](https://traefik.io) as the ingress controller, you might need to replace a couple of annotations in the yamls (e.g. `traefik.ingress.kubernetes.io/router.tls`).\nYou may also want to modify some other bits such as those containing host names.\n\n#### Namespace\n\nFirst, let’s ensure that a namespace called `website` exists in your cluster:\n\n```sh\nkubectl apply -f k8s/namespace.yaml\n```\n\n#### Persistent volume claim for data\n\nWe don’t want profile infos to be erased every time the app deployment is updated.\nTo achieve this, we will use a persistent volume claim (PVC):\n\n```sh\nkubectl apply -f k8s/pvc.yaml\n```\n\n#### Secrets\n\nWe can pass environment variables to the app deployment directly inside the yaml.\nHowever, because some of the values are sensitive, it is better to maintain them as Kubernetes secrets:\n\n```sh\nFLICKR_USER_ID=??\nFLICKR_API_KEY=??\nkubectl create secret generic flickr-api \\\n  --from-literal=user_id=${FLICKR_USER_ID} \\\n  --from-literal=api_key=${FLICKR_API_KEY} \\\n  --namespace=website\n\nUPDATE_PROFILE_SECURITY_TOKEN=??\nUPDATE_PROFILE_PROXY_SERVER_URL=??\nkubectl create secret generic update-profile \\\n  --namespace=website \\\n  --from-literal=security-token=${UPDATE_PROFILE_SECURITY_TOKEN} \\\n  --from-literal=proxy-server-url=${UPDATE_PROFILE_PROXY_SERVER_URL}\n```\n\n#### The app (deployment, service, ingress)\n\nNow that we have a namespace, a PVC and the secrets, we can deploy the app itself:\n\n```sh\nkubectl apply -f k8s/app.yaml\n```\n\nTo manually update the app deployment, this commands can be used:\n\n```sh\nNEW_IMAGE_TAG=$(git rev-parse --short HEAD)\nkubectl set image --namespace=website deployment/website-app main=ghcr.io/kachkaev/website:${NEW_IMAGE_TAG}\n```\n\nAlternatively, it is possible to modify Docker image urls directly in yaml files and then run `kubectl apply ...` again.\nIn any case, the updates will run with zero downtime because of their rolling nature.\n\n#### Initial profile infos\n\nBecause profile infos have not been collected yet, the app will ‘gracefully degrade’ by showing blank space instead of statistics.\nWe can instantiate profile infos by manually requesting corresponding URLs: `/update-profiles/[profile-name]?[security-token]`.\n\n#### Cron jobs (recurring updates to profile infos)\n\nWe can always update profile infos by opening `/update-profiles/[profile-name]?[security-token]`.\nTo automate this process, we can use Kubernetes cron jobs which will make GET requests on a schedule:\n\n```sh\nkubectl apply -f k8s/cron-jobs.yaml\n```\n\nAll done!\n\n\u003cimg src=\"https://gitlab.com/kachkaev/website/uploads/83aa65c795d488f754a34a4e61d57cfd/done.png\" alt=\"happy meme face\" width=\"120\" /\u003e\u003cbr\u003e\u003cbr\u003e\n\nThe real production cluster contains other Kubernetes payload including redirects from www domains to non-www ones.\nThese yamls are omitted from the repository to keep it focused.\n\n## Project history\n\nMy mini-website has been live since 2009.\nYou can find its historic snapshots on [📜 web.archive.org](https://web.archive.org):\n\n- Russian version: [📜 kachkaev.ru](https://web.archive.org/web/*/kachkaev.ru) (2009+)\n- English version: [📜 en.kachkaev.ru](https://web.archive.org/web/*/en.kachkaev.ru) (2010–2022), [📜 kachkaev.uk](https://web.archive.org/web/*/kachkaev.uk) (2022+)\n\nI moved the English version to the `.uk` domain zone in 2022 to mitigate a potential loss of my primary hostname.\nWhen Russia started its [‘special military operation’ in Ukraine](https://en.wikipedia.org/wiki/On_conducting_a_special_military_operation), I assumed a non-zero chance of ‘special civil servants’ taking over `kachkaev.ru` because of my ‘special attitude and activities’.\n\nThe first open-source version of this project was crafted in 2017 and it became my first TypeScript-enabled Next.js app.\nThis was before Next.js implemented [API routes](https://nextjs.org/docs/api-routes/introduction), so I had to split the app into two microservices: `frontend` and `graphql-server`.\nI used [GitLab](https://gitlab.com) to host the repositories and to run CI/CD pipelines (GitHub Actions did not exist until 2019). You can still find the original open-source repositories at:\n\n- [gitlab.com/kachkaev/website](https://gitlab.com/kachkaev/website)\n- [gitlab.com/kachkaev/website-graphql-server](https://gitlab.com/kachkaev/website-graphql-server)\n- [gitlab.com/kachkaev/website-frontend](https://gitlab.com/kachkaev/website-frontend)\n\nWhen Next.js [announced](https://nextjs.org/blog/next-13) the new `/app` directory in late 2022, I saw this as an opportunity to refactor my mini-website and to experiment with [React server components](https://nextjs.org/docs/advanced-features/react-18/server-components).\nSo when I got a couple of spare weekends in early 2023, I replaced three GitLab repos with [github.com/kachkaev/website](https://github.com/kachkaev/website).\nDoing so simplified things a lot!\n\n## Getting involved\n\nThis is a pretty small personal project, so frankly speaking, there is not much to collaborate on.\nNevertheless, you might want to learn something new by playing with this repo or even decide to make your own (much better) website based on my code.\nIf you have questions, feel free to ask me anything by creating a new [GitHub issue](https://github.com/kachkaev/website/issues) or by [sending an email](mailto:alexander@kachkaev.uk)!\n\nThe repository is available under [BSD-3-Clause license](./LICENSE.md), so you are free to do whatever you want with it!\n\n\u003cimg src=\"https://gitlab.com/kachkaev/website/uploads/749d5f4679392be346e7e986f2e5e5e1/thumbs-up.png\" alt=\"thumbs-up meme face\" width=\"160\" /\u003e\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkachkaev%2Fwebsite","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fkachkaev%2Fwebsite","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkachkaev%2Fwebsite/lists"}