feat(projects): split projects page into starred, linked and contributed lists

Resolve owned namespaces from org memberships with the admin role instead of
assuming a fixed owner list, so repos under orgs I own are included.

- starred: owned public non-fork repos with at least one star, by stars then
  most recent push
- linked: owned public non-fork repos exposing a homepage or Pages URL, by most
  recent push
- contributed: public repos owned by others, by my merged PR count then most
  recent push

Private repos are excluded everywhere; forks are excluded from the owned lists
because their homepage belongs to upstream. Split the generator into API access
and rendering modules.
This commit is contained in:
tiennm99 committed 2026-09-18 12:24:44 +07:00
1 parent aad39d9369
commit a9bb2f0621
4 files changed
+515 -250

No files matched your search

+235
View File
@@ -0,0 +1,235 @@
// GitHub API access for the projects list: owned namespaces, their repos, and
// the repos the user contributed merged pull requests to.
const API = "https://api.github.com";
const GRAPHQL = `${API}/graphql`;
/**
* @typedef {object} Repo
* @property {string} nameWithOwner
* @property {string} name
* @property {string} owner
* @property {string} htmlUrl
* @property {string} description
* @property {string} homepage
* @property {boolean} hasPages
* @property {number} stars
* @property {boolean} isFork
* @property {boolean} isPrivate
* @property {boolean} isArchived
* @property {string} pushedAt Last push timestamp, ISO 8601.
*/
/**
* @typedef {object} Contribution
* @property {Repo} repo
* @property {number} mergedPrs
*/
/** Minimal GitHub client bound to one token and one user. */
export class GitHubClient {
/**
* @param {string} token Personal access token with `repo` and `read:org`.
* @param {string} user Login whose projects are listed.
*/
constructor(token, user) {
this.token = token;
this.user = user;
}
/** @returns {Record<string, string>} */
get restHeaders() {
return {
Authorization: `Bearer ${this.token}`,
Accept: "application/vnd.github+json",
"X-GitHub-Api-Version": "2022-11-28",
"User-Agent": `${this.user}-projects-updater`,
};
}
/**
* Fetch every page of a REST collection endpoint.
* @param {string} pathAndQuery Path starting with `/`, query without `page`.
* @returns {Promise<object[]>}
*/
async restPaged(pathAndQuery) {
const all = [];
const sep = pathAndQuery.includes("?") ? "&" : "?";
for (let page = 1; page <= 20; page += 1) {
const res = await fetch(`${API}${pathAndQuery}${sep}page=${page}`, {
headers: this.restHeaders,
});
if (!res.ok) {
throw new Error(`REST ${pathAndQuery} ${res.status}: ${await res.text()}`);
}
const batch = await res.json();
if (!Array.isArray(batch) || batch.length === 0) break;
all.push(...batch);
if (batch.length < 100) break;
}
return all;
}
/**
* @param {string} query
* @param {Record<string, unknown>} variables
* @returns {Promise<object>}
*/
async graphql(query, variables) {
const res = await fetch(GRAPHQL, {
method: "POST",
headers: {
Authorization: `Bearer ${this.token}`,
"Content-Type": "application/json",
"User-Agent": `${this.user}-projects-updater`,
},
body: JSON.stringify({ query, variables }),
});
if (!res.ok) throw new Error(`GraphQL ${res.status}: ${await res.text()}`);
const body = await res.json();
if (body.errors) throw new Error(`GraphQL errors: ${JSON.stringify(body.errors)}`);
return body.data;
}
/**
* Logins the user controls: their own account plus every organization where
* their membership role is `admin`.
* @returns {Promise<Set<string>>}
*/
async fetchOwnedLogins() {
let memberships;
try {
memberships = await this.restPaged("/user/memberships/orgs?state=active&per_page=100");
} catch (err) {
throw new Error(
`Cannot read organization memberships (the token needs the read:org scope): ${err.message}`,
);
}
const logins = new Set([this.user.toLowerCase()]);
for (const m of memberships) {
if (m.role === "admin" && m.organization?.login) {
logins.add(m.organization.login.toLowerCase());
}
}
return logins;
}
/**
* Public repos across every owned login, deduplicated by full name.
* @param {Set<string>} ownedLogins
* @returns {Promise<Repo[]>}
*/
async fetchOwnedRepos(ownedLogins) {
const byName = new Map();
for (const login of ownedLogins) {
const endpoint =
login === this.user.toLowerCase()
? `/users/${login}/repos?type=owner&sort=updated&per_page=100`
: `/orgs/${login}/repos?type=all&sort=updated&per_page=100`;
for (const r of await this.restPaged(endpoint)) {
if (r.private) continue;
if (!byName.has(r.full_name)) byName.set(r.full_name, fromRest(r));
}
}
return [...byName.values()];
}
/**
* Repos the user has merged pull requests in, with the per-repo count.
* Uses the search API, which caps at 1000 results.
* @returns {Promise<Contribution[]>}
*/
async fetchMergedPrContributions() {
const counts = new Map();
const repos = new Map();
let cursor = null;
for (let i = 0; i < 10; i += 1) {
const data = await this.graphql(MERGED_PR_QUERY, {
q: `is:pr is:merged is:public author:${this.user}`,
cursor,
});
const search = data?.search;
if (!search) break;
for (const node of search.nodes) {
const repo = node?.repository;
if (!repo || repo.isPrivate) continue;
const key = repo.nameWithOwner;
counts.set(key, (counts.get(key) || 0) + 1);
if (!repos.has(key)) repos.set(key, fromGraphql(repo));
}
if (!search.pageInfo.hasNextPage) break;
cursor = search.pageInfo.endCursor;
}
return [...counts.entries()].map(([key, mergedPrs]) => ({
repo: repos.get(key),
mergedPrs,
}));
}
}
const MERGED_PR_QUERY = `
query($q: String!, $cursor: String) {
search(query: $q, type: ISSUE, first: 100, after: $cursor) {
pageInfo { hasNextPage endCursor }
nodes {
... on PullRequest {
repository {
nameWithOwner
name
url
description
homepageUrl
stargazerCount
pushedAt
isPrivate
isFork
isArchived
owner { login }
}
}
}
}
}`;
/**
* @param {object} r REST repository payload.
* @returns {Repo}
*/
function fromRest(r) {
return {
nameWithOwner: r.full_name,
name: r.name,
owner: r.owner?.login || "",
htmlUrl: r.html_url,
description: r.description || "",
homepage: r.homepage || "",
hasPages: !!r.has_pages,
stars: r.stargazers_count || 0,
isFork: !!r.fork,
isPrivate: !!r.private,
isArchived: !!r.archived,
pushedAt: r.pushed_at || "",
};
}
/**
* @param {object} r GraphQL repository node.
* @returns {Repo}
*/
function fromGraphql(r) {
return {
nameWithOwner: r.nameWithOwner,
name: r.name,
owner: r.owner?.login || "",
htmlUrl: r.url,
description: r.description || "",
homepage: r.homepageUrl || "",
// has_pages is not exposed by GraphQL; contributed repos rely on homepage.
hasPages: false,
stars: r.stargazerCount || 0,
isFork: !!r.isFork,
isPrivate: !!r.isPrivate,
isArchived: !!r.isArchived,
pushedAt: r.pushedAt || "",
};
}
+109
View File
@@ -0,0 +1,109 @@
// Markdown rendering and marker injection for the projects page.
/**
* Public URL of a repo: its declared homepage, else its GitHub Pages URL when
* Pages is enabled. Returns an empty string when the repo has neither.
* @param {import("./github-api.mjs").Repo} repo
* @returns {string}
*/
export function resolveUrl(repo) {
const home = (repo.homepage || "").trim();
if (home) return home;
if (repo.hasPages) return `https://${repo.owner}.github.io/${repo.name}/`;
return "";
}
/**
* @param {string} text
* @returns {string} Cell-safe single-line text.
*/
function cell(text) {
return (text || "")
.replace(/\|/g, "\\|")
.replace(/\r?\n/g, " ")
.trim();
}
/**
* @param {string} url
* @returns {string}
*/
function linkOrDash(url) {
return url ? `[${url}](${url})` : "—";
}
/**
* @param {string[]} headers
* @param {string[][]} rows
* @returns {string[]} Markdown table lines, or an empty-state line.
*/
function table(headers, rows) {
if (rows.length === 0) return ["_Chưa có project nào._"];
return [
`| ${headers.join(" | ")} |`,
`| ${headers.map(() => "---").join(" | ")} |`,
...rows.map((r) => `| ${r.join(" | ")} |`),
];
}
/**
* Repos with at least one star, most-starred first.
* @param {import("./github-api.mjs").Repo[]} repos
* @returns {string[]}
*/
export function renderStarred(repos) {
const rows = repos.map((r) => [
`[${r.nameWithOwner}](${r.htmlUrl})`,
cell(r.description),
String(r.stars),
linkOrDash(resolveUrl(r)),
]);
return table(["Name", "Description", "Stars", "URL"], rows);
}
/**
* Repos that have a clickable URL, by name.
* @param {import("./github-api.mjs").Repo[]} repos
* @returns {string[]}
*/
export function renderLinked(repos) {
const rows = repos.map((r) => [
`[${r.nameWithOwner}](${r.htmlUrl})`,
cell(r.description),
linkOrDash(resolveUrl(r)),
]);
return table(["Name", "Description", "URL"], rows);
}
/**
* Repos owned by others, by merged pull request count.
* @param {import("./github-api.mjs").Contribution[]} contributions
* @returns {string[]}
*/
export function renderContributed(contributions) {
const rows = contributions.map((c) => [
`[${c.repo.nameWithOwner}](${c.repo.htmlUrl})`,
cell(c.repo.description),
String(c.mergedPrs),
String(c.repo.stars),
]);
return table(["Name", "Description", "Merged PRs", "Stars"], rows);
}
/**
* Replace the content between a marker pair. Throws when the pair is missing,
* so a renamed marker fails loudly instead of silently dropping a list.
* @param {string} source
* @param {string} marker Marker base name, e.g. `PROJECTS_STARRED`.
* @param {string[]} lines
* @returns {string}
*/
export function injectBlock(source, marker, lines) {
const start = `<!-- ${marker}_START -->`;
const end = `<!-- ${marker}_END -->`;
const pattern = new RegExp(`${start}[\\s\\S]*?${end}`);
if (!pattern.test(source)) {
throw new Error(`Marker pair ${marker} not found in projects page`);
}
return source.replace(pattern, [start, "", ...lines, "", end].join("\n"));
}
+56 -166
View File
@@ -1,189 +1,79 @@
#!/usr/bin/env node
// Build a projects list (owned + contributed) and inject it into
// content/page/projects/index.md between the PROJECTS_LIST markers.
// Build the three project lists and inject them into the projects page:
//
// A project is included if it has a URL, meaning either:
// - repo.homepage is set, OR
// - repo.has_pages is true (owned repos only — GraphQL does not expose
// has_pages for contributed repos, so those must have homepage set).
// 1. Starred — public repos I own (personal account or an org I administer)
// with at least one star, by stars then most recent push.
// 2. Linked — public repos I own that have a clickable URL (homepage or
// GitHub Pages), no star requirement, by most recent push.
// May overlap list 1.
// 3. Contributed — public repos owned by someone else, by how many of my pull
// requests were merged there, then most recent push.
//
// Forks are excluded from lists 1 and 2: their homepage belongs to upstream,
// not to me. Private repos are excluded everywhere — the page is public.
import fs from "node:fs/promises";
import path from "node:path";
const token = process.env.GH_TOKEN;
const user = process.env.GH_USER;
import { GitHubClient } from "./lib/github-api.mjs";
import {
injectBlock,
renderContributed,
renderLinked,
renderStarred,
resolveUrl,
} from "./lib/render-projects.mjs";
if (!token || !user) {
console.error("GH_TOKEN and GH_USER env vars are required");
process.exit(1);
}
const PAGE = "content/page/projects/index.md";
const restHeaders = {
Authorization: `Bearer ${token}`,
Accept: "application/vnd.github+json",
"X-GitHub-Api-Version": "2022-11-28",
"User-Agent": `${user}-projects-updater`,
};
async function fetchOwnedRepos() {
const all = [];
for (let page = 1; page <= 20; page += 1) {
const url = `https://api.github.com/users/${user}/repos?type=all&sort=updated&per_page=100&page=${page}`;
const res = await fetch(url, { headers: restHeaders });
if (!res.ok) {
throw new Error(`REST /users/${user}/repos ${res.status}: ${await res.text()}`);
}
const batch = await res.json();
if (!Array.isArray(batch) || batch.length === 0) break;
all.push(...batch);
if (batch.length < 100) break;
}
return all.map((r) => ({
nameWithOwner: r.full_name,
name: r.name,
htmlUrl: r.html_url,
description: r.description,
homepage: r.homepage,
hasPages: !!r.has_pages,
owner: r.owner?.login,
isOwned: true,
}));
}
const CONTRIB_QUERY = `
query($login: String!, $cursor: String) {
user(login: $login) {
repositoriesContributedTo(
first: 100
after: $cursor
includeUserRepositories: false
privacy: PUBLIC
contributionTypes: [COMMIT, PULL_REQUEST, PULL_REQUEST_REVIEW]
orderBy: { field: UPDATED_AT, direction: DESC }
) {
pageInfo { hasNextPage endCursor }
nodes {
nameWithOwner
name
url
description
homepageUrl
owner { login }
}
}
}
}`;
async function fetchContributedRepos() {
const all = [];
let cursor = null;
for (let i = 0; i < 20; i += 1) {
const res = await fetch("https://api.github.com/graphql", {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
"User-Agent": `${user}-projects-updater`,
},
body: JSON.stringify({
query: CONTRIB_QUERY,
variables: { login: user, cursor },
}),
});
if (!res.ok) {
throw new Error(`GraphQL ${res.status}: ${await res.text()}`);
}
const body = await res.json();
if (body.errors) {
throw new Error(`GraphQL errors: ${JSON.stringify(body.errors)}`);
}
const node = body.data?.user?.repositoriesContributedTo;
if (!node) break;
for (const r of node.nodes) {
all.push({
nameWithOwner: r.nameWithOwner,
name: r.name,
htmlUrl: r.url,
description: r.description,
homepage: r.homepageUrl,
hasPages: false, // not exposed via GraphQL; rely on homepage
owner: r.owner.login,
isOwned: false,
});
}
if (!node.pageInfo.hasNextPage) break;
cursor = node.pageInfo.endCursor;
}
return all;
}
function resolveUrl(repo) {
const home = (repo.homepage || "").trim();
if (home) return home;
if (repo.hasPages && repo.isOwned) {
return `https://${repo.owner}.github.io/${repo.name}`;
}
return null;
}
function toRow(repo, url) {
const desc = (repo.description || "")
.replace(/\|/g, "\\|")
.replace(/\r?\n/g, " ")
.trim();
return `| [${repo.nameWithOwner}](${repo.htmlUrl}) | ${desc} | [${url}](${url}) |`;
}
const START = "<!-- PROJECTS_LIST_START -->";
const END = "<!-- PROJECTS_LIST_END -->";
function buildBlock(rows) {
return [
START,
"",
"| Name | Description | URL |",
"| --- | --- | --- |",
...rows,
"",
END,
].join("\n");
}
/**
* Most recently pushed first; a repo with no timestamp sorts last.
* @param {import("./lib/github-api.mjs").Repo} a
* @param {import("./lib/github-api.mjs").Repo} b
* @returns {number}
*/
const byRecentPush = (a, b) => (b.pushedAt || "").localeCompare(a.pushedAt || "");
async function main() {
const [owned, contributed] = await Promise.all([
fetchOwnedRepos(),
fetchContributedRepos(),
const token = process.env.GH_TOKEN;
const user = process.env.GH_USER;
if (!token || !user) {
console.error("GH_TOKEN and GH_USER env vars are required");
process.exit(1);
}
const gh = new GitHubClient(token, user);
const ownedLogins = await gh.fetchOwnedLogins();
const [ownedRepos, contributions] = await Promise.all([
gh.fetchOwnedRepos(ownedLogins),
gh.fetchMergedPrContributions(),
]);
const merged = new Map();
for (const r of [...owned, ...contributed]) {
if (!merged.has(r.nameWithOwner)) merged.set(r.nameWithOwner, r);
}
const mine = ownedRepos.filter((r) => !r.isFork);
const rows = [];
const sorted = [...merged.values()].sort((a, b) =>
a.nameWithOwner.localeCompare(b.nameWithOwner),
);
for (const r of sorted) {
const url = resolveUrl(r);
if (!url) continue;
rows.push(toRow(r, url));
}
const starred = mine
.filter((r) => r.stars > 0)
.sort((a, b) => b.stars - a.stars || byRecentPush(a, b));
const block = buildBlock(rows);
const file = path.resolve("content/page/projects/index.md");
const linked = mine.filter((r) => resolveUrl(r) !== "").sort(byRecentPush);
const contributed = contributions
.filter((c) => !ownedLogins.has(c.repo.owner.toLowerCase()))
.sort((a, b) => b.mergedPrs - a.mergedPrs || byRecentPush(a.repo, b.repo));
const file = path.resolve(PAGE);
const current = await fs.readFile(file, "utf8");
const marker = /<!-- PROJECTS_LIST_START -->[\s\S]*?<!-- PROJECTS_LIST_END -->/;
const next = marker.test(current)
? current.replace(marker, block)
: `${current.trimEnd()}\n\n${block}\n`;
let next = injectBlock(current, "PROJECTS_STARRED", renderStarred(starred));
next = injectBlock(next, "PROJECTS_LINKED", renderLinked(linked));
next = injectBlock(next, "PROJECTS_CONTRIBUTED", renderContributed(contributed));
const summary = `${starred.length} starred, ${linked.length} linked, ${contributed.length} contributed`;
if (next === current) {
console.log(`No changes (${rows.length} projects).`);
console.log(`No changes (${summary}).`);
return;
}
await fs.writeFile(file, next);
console.log(`Updated ${file} with ${rows.length} projects.`);
console.log(`Updated ${PAGE} (${summary}).`);
}
main().catch((err) => {
+115 -84
View File
@@ -10,105 +10,136 @@ menu:
icon: list
---
Danh sách những projects mình đã làm hoặc có đóng góp, có URL demo / homepage. URL có thể là:
Projects nổi bật của mình, chia thành ba danh sách. Tất cả đều là repo public, và
đều được **tạo tự động bằng script** (`.github/scripts/update-projects-list.mjs`,
chạy mỗi ngày qua GitHub Actions) — không sửa tay phần trong bảng.
## 1. Projects có star
Repo mình sở hữu — trên tài khoản cá nhân hoặc trong org do mình làm owner — có ít
nhất 1 star. Sắp xếp theo số star giảm dần, cùng số star thì repo cập nhật gần
đây hơn lên trước. Fork không được tính.
<!-- PROJECTS_STARRED_START -->
| Name | Description | Stars | URL |
| --- | --- | --- | --- |
| [tiennm99/awesome-ai-dev-tools](https://github.com/tiennm99/awesome-ai-dev-tools) | Daily-refreshed ranking of open-source AI developer tools — coding agents, ADEs, AI editors and code-review tools — by GitHub stars, with 7-day deltas, star-history charts and a tag-filtered dashboard. | 3 | [https://awesome-ai-dev-tools.pages.dev/](https://awesome-ai-dev-tools.pages.dev/) |
| [tiennm99/penny-pincher-provider](https://github.com/tiennm99/penny-pincher-provider) | Curated list of affordable + free LLM API providers — Claude Code passes, coding-plan subs, free-tier APIs. | 1 | [https://tiennm99.github.io/penny-pincher-provider/](https://tiennm99.github.io/penny-pincher-provider/) |
| [tiennm99/ai-labs](https://github.com/tiennm99/ai-labs) | Experiments with AI-assisted coding workflows — ClaudeKit, Superpowers, BMAD, oh-my-claudecode, gstack, and course work, merged with full history | 1 | [https://tiennm99.github.io/ai-labs/](https://tiennm99.github.io/ai-labs/) |
| [tiennm99/export-telegram-group-members](https://github.com/tiennm99/export-telegram-group-members) | Export Telegram group members to CSV using Telethon — admin auth required for full member visibility. | 1 | — |
| [tiennm99/time-mocker](https://github.com/tiennm99/time-mocker) | Windows tool that injects fake time into running processes by hooking Win32 time APIs. Rust. | 1 | — |
| [tiennm99/lottery-generator](https://github.com/tiennm99/lottery-generator) | Go port of my lottery number generator — useful for Vietlott and similar lotteries. | 1 | — |
| [tiennm99/arduino](https://github.com/tiennm99/arduino) | Collection of small Arduino projects and sketches. | 1 | — |
| [tiennm99/adventofcode](https://github.com/tiennm99/adventofcode) | My Advent of Code 2025 solutions | 1 | — |
<!-- PROJECTS_STARRED_END -->
## 2. Projects có link
Repo mình sở hữu và có URL để bấm vào xem trực tiếp, không cần star. Danh sách này
có thể trùng với danh sách trên. URL có thể là:
- Website tĩnh / app host trên [GitHub Pages](https://pages.github.com/), [Cloudflare Pages](https://pages.cloudflare.com/), [Vercel](https://vercel.com/), [Netlify](https://www.netlify.com/)...
- [Cloudflare Workers](https://workers.cloudflare.com/) hoặc serverless endpoint khác
- Telegram bot ([t.me/...](https://t.me/))
- Custom domain, hoặc bất kỳ hình thức deploy nào khác
_(Danh sách này được tạo tự động, sắp xếp theo tên repo)_
Sắp xếp theo lần cập nhật gần nhất.
<!-- PROJECTS_LIST_START -->
<!-- PROJECTS_LINKED_START -->
| Name | Description | URL |
| --- | --- | --- |
| [abhisheknaiidu/awesome-github-profile-readme](https://github.com/abhisheknaiidu/awesome-github-profile-readme) | 😎 A curated list of awesome GitHub Profile which updates in real time | [https://bit.ly/awesome-gh](https://bit.ly/awesome-gh) |
| [BerriAI/litellm](https://github.com/BerriAI/litellm) | The fastest, litest AI Gateway. Rust core with Python SDK. Call 100+ LLM APIs in OpenAI (or native) format with cost tracking, guardrails, load balancing, and logging [Bedrock, Azure, OpenAI, Anthropic, OpenAI, VertexAI, vLLM, Nvidia NIM] | [https://docs.litellm.ai/docs/](https://docs.litellm.ai/docs/) |
| [bkfcduchoa/bkfcduchoa.github.io](https://github.com/bkfcduchoa/bkfcduchoa.github.io) | | [https://bkfcduchoa.github.io/](https://bkfcduchoa.github.io/) |
| [coollabsio/coolify](https://github.com/coollabsio/coolify) | An open-source, self-hostable PaaS alternative to Vercel, Heroku & Netlify that lets you easily deploy static sites, databases, full-stack applications and 280+ one-click services on your own servers. | [https://coolify.io](https://coolify.io) |
| [gohugoio/hugoThemesSiteBuilder](https://github.com/gohugoio/hugoThemesSiteBuilder) | The source for https://themes.gohugo.io | [https://themes.gohugo.io](https://themes.gohugo.io) |
| [kaitranntt/ccs](https://github.com/kaitranntt/ccs) | Switch between Claude accounts, Gemini, Copilot, OpenRouter (300+ models) via CLIProxyAPI OAuth proxy. Visual dashboard, remote proxy support, WebSearch fallback. Zero-config to production-ready. | [https://ccs.kaitran.ca/](https://ccs.kaitran.ca/) |
| [ngamtheproject/ngamtheproject.github.io](https://github.com/ngamtheproject/ngamtheproject.github.io) | Website for Ngăm - a charity organization founded by high school students. | [https://ngamtheproject.github.io/](https://ngamtheproject.github.io/) |
| [ripienaar/free-for-dev](https://github.com/ripienaar/free-for-dev) | A list of SaaS, PaaS and IaaS offerings that have free tiers of interest to devops and infradev | [https://free-for.dev/](https://free-for.dev/) |
| [survival2d/survival2d.github.io](https://github.com/survival2d/survival2d.github.io) | Repo to deploy client to GitHub Pages | [https://survival2d.github.io/](https://survival2d.github.io/) |
| [tiennm99/20200411](https://github.com/tiennm99/20200411) | An old gift website. Archived 2021-02-06. | [https://tiennm99.github.io/20200411/](https://tiennm99.github.io/20200411/) |
| [tiennm99/20221225](https://github.com/tiennm99/20221225) | A small just-for-fun website built on Christmas Day 2022. | [https://tiennm99.github.io/20221225/](https://tiennm99.github.io/20221225/) |
| [tiennm99/ai-labs](https://github.com/tiennm99/ai-labs) | Experiments with AI-assisted coding workflows — ClaudeKit, Superpowers, BMAD, oh-my-claudecode, gstack, and course work, merged with full history | [https://tiennm99.github.io/ai-labs/](https://tiennm99.github.io/ai-labs/) |
| [tiennm99/apart](https://github.com/tiennm99/apart) | Small personal website with photos and music for someone far away. | [https://tiennm99.github.io/apart/](https://tiennm99.github.io/apart/) |
| [tiennm99/awesome-ai-dev-tools](https://github.com/tiennm99/awesome-ai-dev-tools) | Daily-refreshed ranking of open-source AI developer tools — coding agents, ADEs, AI editors and code-review tools — by GitHub stars, with 7-day deltas, star-history charts and a tag-filtered dashboard. | [https://awesome-ai-dev-tools.pages.dev/](https://awesome-ai-dev-tools.pages.dev/) |
| [tiennm99/awesome-github-profile-readme](https://github.com/tiennm99/awesome-github-profile-readme) | 😎 A curated list of awesome GitHub Profile which updates in real time | [https://bit.ly/awesome-gh](https://bit.ly/awesome-gh) |
| [tiennm99/beentogether](https://github.com/tiennm99/beentogether) | Been Together — a small website celebrating time spent together with music and photos. | [https://tiennm99.github.io/beentogether/](https://tiennm99.github.io/beentogether/) |
| [tiennm99/blog](https://github.com/tiennm99/blog) | Source of miti99.com — personal blog (VN/EN) built with Hugo + Stack theme. | [https://miti99.com](https://miti99.com) |
| [tiennm99/ccs](https://github.com/tiennm99/ccs) | Switch between Claude accounts, Gemini, Copilot, OpenRouter (300+ models) via CLIProxyAPI OAuth proxy. Visual dashboard, remote proxy support, WebSearch fallback. Zero-config to production-ready. | [https://ccs.kaitran.ca/](https://ccs.kaitran.ca/) |
| [tiennm99/cf-worker-telegram](https://github.com/tiennm99/cf-worker-telegram) | A Cloudflare Worker that acts as a transparent proxy for the Telegram Bot API. Useful for bypassing network restrictions or as a middleware for your Telegram bot. | [https://telegram.miti99.workers.dev](https://telegram.miti99.workers.dev) |
| [tiennm99/claude-status-webhook](https://github.com/tiennm99/claude-status-webhook) | Telegram bot forwarding Claude Status updates — alerts for incidents and resolutions. Serverless on Cloudflare Workers. | [https://t.me/MiTiClaudeStatusBot](https://t.me/MiTiClaudeStatusBot) |
| [tiennm99/cloudflare-proxy](https://github.com/tiennm99/cloudflare-proxy) | Inspired by tuanpb99/cf-worker-telegram, this Cloudflare Worker script has been adapted to act as a transparent proxy for any HTTP or HTTPS URL, powered by Cloudflare’s global edge network. | [https://cloudflare-proxy.miti99.workers.dev/](https://cloudflare-proxy.miti99.workers.dev/) |
| [tiennm99/coolify](https://github.com/tiennm99/coolify) | An open-source, self-hostable PaaS alternative to Vercel, Heroku & Netlify that lets you easily deploy static sites, databases, full-stack applications and 280+ one-click services on your own servers. | [https://coolify.io](https://coolify.io) |
| [tiennm99/cv](https://github.com/tiennm99/cv) | My personal CV written in Typst with RenderCV. | [https://tiennm99.github.io/cv/](https://tiennm99.github.io/cv/) |
| [tiennm99/demngayxaem](https://github.com/tiennm99/demngayxaem) | Đếm ngày xa em — a small relationship 'days-apart' counter website. | [https://tiennm99.github.io/demngayxaem/](https://tiennm99.github.io/demngayxaem/) |
| [tiennm99/dental-visit](https://github.com/tiennm99/dental-visit) | Small website created for a dental visit appointment. | [https://tiennm99.github.io/dental-visit/](https://tiennm99.github.io/dental-visit/) |
| [tiennm99/DocsGPT](https://github.com/tiennm99/DocsGPT) | Private AI platform for agents, assistants and enterprise search. Built-in Agent Builder, Deep research, Document analysis, Multi-model support, and API connectivity for agents. | [https://app.docsgpt.cloud/](https://app.docsgpt.cloud/) |
| [tiennm99/double-click-test](https://github.com/tiennm99/double-click-test) | Website to quickly check a mouse for double-click issues. | [https://tiennm99.github.io/double-click-test/](https://tiennm99.github.io/double-click-test/) |
| [tiennm99/exchange-rate-export](https://github.com/tiennm99/exchange-rate-export) | Export BIDV and Techcombank FX rates to Excel — Next.js web app with date range selection | [https://exchange-rate-export.vercel.app/](https://exchange-rate-export.vercel.app/) |
| [tiennm99/free-domains](https://github.com/tiennm99/free-domains) | Free subdomains for personal sites, open-source projects, and more. | [https://freesubdomains.org](https://freesubdomains.org) |
| [tiennm99/free-for-dev](https://github.com/tiennm99/free-for-dev) | A list of SaaS, PaaS and IaaS offerings that have free tiers of interest to devops and infradev | [https://free-for.dev/](https://free-for.dev/) |
| [tiennm99/goclaw](https://github.com/tiennm99/goclaw) | GoClaw - GoClaw is OpenClaw rebuilt in Go — with multi-tenant isolation, 5-layer security, and native concurrency. Deploy AI agent teams at scale without compromising on safety. | [https://goclaw.sh](https://goclaw.sh) |
| [tiennm99/godot](https://github.com/tiennm99/godot) | Godot Engine – Multi-platform 2D and 3D game engine | [https://godotengine.org](https://godotengine.org) |
| [tiennm99/hugo-blox-builder](https://github.com/tiennm99/hugo-blox-builder) | 🧱 Describe your site, AI builds it, you own it as Markdown. Snap together Tailwind blocks like Lego — landing pages, blogs, portfolios, docs & more. No AI slop. Free to deploy anywhere 👇 | [https://wowchemy.com/templates/](https://wowchemy.com/templates/) |
| [tiennm99/hugoThemesSiteBuilder](https://github.com/tiennm99/hugoThemesSiteBuilder) | The source for https://themes.gohugo.io | [https://themes.gohugo.io](https://themes.gohugo.io) |
| [tiennm99/hurt](https://github.com/tiennm99/hurt) | Small website inspired by the film 'Thắp sáng anh, sưởi ấm em' — a little unusual, just for fun. | [https://tiennm99.github.io/hurt/](https://tiennm99.github.io/hurt/) |
| [tiennm99/is-a-dev](https://github.com/tiennm99/is-a-dev) | Grab your own sweet-looking '.is-a.dev' subdomain. | [https://is-a.dev](https://is-a.dev) |
| [tiennm99/is-a-good-dev](https://github.com/tiennm99/is-a-good-dev) | Register for your is-a-good.dev domain! | [https://is-a-good.dev](https://is-a-good.dev) |
| [tiennm99/java-design-patterns](https://github.com/tiennm99/java-design-patterns) | Design patterns implemented in Java | [https://java-design-patterns.com](https://java-design-patterns.com) |
| [tiennm99/litellm](https://github.com/tiennm99/litellm) | Python SDK, Proxy Server (AI Gateway) to call 100+ LLM APIs in OpenAI (or native) format, with cost tracking, guardrails, loadbalancing and logging. [Bedrock, Azure, OpenAI, VertexAI, Cohere, Anthropic, Sagemaker, HuggingFace, VLLM, NVIDIA NIM] | [https://docs.litellm.ai/docs/](https://docs.litellm.ai/docs/) |
| [tiennm99/llmapikey](https://github.com/tiennm99/llmapikey) | Pending/archived: free capped OpenRouter API key giveaway paused until a suitable LLM gateway/provider is found. | [https://llmapikey.vercel.app](https://llmapikey.vercel.app) |
| [tiennm99/loldle](https://github.com/tiennm99/loldle) | LoLdle-style League of Legends daily champion guessing game — SvelteKit web app, Go data scraper, and Android stub in one repo; champion data auto-updates weekly | [https://tiennm99.github.io/loldle/](https://tiennm99.github.io/loldle/) |
| [tiennm99/lombok](https://github.com/tiennm99/lombok) | Very spicy additions to the Java programming language. | [https://projectlombok.org/](https://projectlombok.org/) |
| [tiennm99/loto](https://github.com/tiennm99/loto) | Lô Tô (Vietnamese lottery bingo) number board — Next.js web app | [https://tiennm99.github.io/loto/](https://tiennm99.github.io/loto/) |
| [tiennm99/mai-birthday-2021](https://github.com/tiennm99/mai-birthday-2021) | Birthday page for Trúc Mai (2021) | [https://tiennm99.github.io/mai-birthday-2021/](https://tiennm99.github.io/mai-birthday-2021/) |
| [tiennm99/miti-loki](https://github.com/tiennm99/miti-loki) | Cloudflare Worker that forwards log messages to Grafana Cloud Loki. | [https://miti-loki.miti99.workers.dev/](https://miti-loki.miti99.workers.dev/) |
| [tiennm99/miti-telegram](https://github.com/tiennm99/miti-telegram) | Cloudflare Worker that forwards incoming messages to my Telegram account | [https://miti-telegram.miti99.workers.dev](https://miti-telegram.miti99.workers.dev) |
| [tiennm99/miti99bot](https://github.com/tiennm99/miti99bot) | Plug-n-play Telegram bot framework in Go, self-hosted on Coolify + MongoDB Atlas with cron, games, schedules, and paper trading modules. | [https://t.me/miti99bot](https://t.me/miti99bot) |
| [tiennm99/nntv](https://github.com/tiennm99/nntv) | Night Ninja: Twilight Voyage — turn-based stealth puzzle. Ninja rabbit rescues carrot princess from veggie guards. | [https://tiennm99.github.io/nntv/](https://tiennm99.github.io/nntv/) |
| [tiennm99/ocrot](https://github.com/tiennm99/ocrot) | Website mừng bạn Thân trở thành ông chủ quán ăn Ốc Rốt | [https://tiennm99.github.io/ocrot/](https://tiennm99.github.io/ocrot/) |
| [tiennm99/open-domains](https://github.com/tiennm99/open-domains) | Free subdomains for personal sites, open-source projects, and more. | [https://open-domains.net](https://open-domains.net) |
| [tiennm99/penny-pincher-provider](https://github.com/tiennm99/penny-pincher-provider) | Curated list of affordable + free LLM API providers — Claude Code passes, coding-plan subs, free-tier APIs. | [https://tiennm99.github.io/penny-pincher-provider/](https://tiennm99.github.io/penny-pincher-provider/) |
| [tiennm99/rplace](https://github.com/tiennm99/rplace) | Collaborative pixel-art canvas (r/place clone). Svelte 5 + Hono on Cloudflare Workers + Durable Object SQLite. | [https://rplace.miti99.workers.dev/](https://rplace.miti99.workers.dev/) |
| [tiennm99/sokoban](https://github.com/tiennm99/sokoban) | From-scratch Sokoban puzzle game — Svelte 5 + Vite, all 155 Microban levels, undo, progress tracking | [https://tiennm99.github.io/sokoban/](https://tiennm99.github.io/sokoban/) |
| [tiennm99/store-scraper-api](https://github.com/tiennm99/store-scraper-api) | HTTP wrapper hosting google-play-scraper and app-store-scraper — POST to /google/:method or /apple/:method | [https://store-scraper-api.vercel.app](https://store-scraper-api.vercel.app) |
| [tiennm99/store-scraper-bot](https://github.com/tiennm99/store-scraper-bot) | Telegram bot tracking Apple App Store + Google Play app updates. JS port of store-scraper-bot-java. | [https://store-scraper-bot.vercel.app](https://store-scraper-bot.vercel.app) |
| [tiennm99/styleguide](https://github.com/tiennm99/styleguide) | Style guides for Google-originated open-source projects | [https://google.github.io/styleguide/](https://google.github.io/styleguide/) |
| [tiennm99/sudoku-solver](https://github.com/tiennm99/sudoku-solver) | 9×9 Sudoku solver in the browser using recursive backtracking — plain JavaScript, Svelte 5 + Vite, on GitHub Pages. | [https://tiennm99.github.io/sudoku-solver/](https://tiennm99.github.io/sudoku-solver/) |
| [tiennm99/test-deeplink-sms](https://github.com/tiennm99/test-deeplink-sms) | Small test page for verifying SMS deeplink behavior on mobile devices. | [https://tiennm99.github.io/test-deeplink-sms/](https://tiennm99.github.io/test-deeplink-sms/) |
| [tiennm99/thedev.id](https://github.com/tiennm99/thedev.id) | Identity for developers on the web. | [https://thedev.id](https://thedev.id) |
| [tiennm99/thptqg](https://github.com/tiennm99/thptqg) | Tra cứu điểm thi THPT Quốc gia 2016–2017 — 1,7 triệu thí sinh · Truy vấn SQL client-side với sql.js | [https://tiennm99.github.io/thptqg/](https://tiennm99.github.io/thptqg/) |
| [tiennm99/tiennm99](https://github.com/tiennm99/tiennm99) | Config files for my GitHub profile. | [https://github.com/tiennm99](https://github.com/tiennm99) |
| [tiennm99/tiennm99.github.io](https://github.com/tiennm99/tiennm99.github.io) | Redirect page for my personal website | [https://tiennm99.github.io/](https://tiennm99.github.io/) |
| [tiennm99/tn1](https://github.com/tiennm99/tn1) | Hugo photo gallery site built on the Gallery Deluxe module | [https://tiennm99.github.io/tn1/](https://tiennm99.github.io/tn1/) |
| [tiennm99/Valentine-Vue-Tailwind](https://github.com/tiennm99/Valentine-Vue-Tailwind) | Support @lds217 for Valentine 2025 | [https://tiennm99.github.io/Valentine-Vue-Tailwind/](https://tiennm99.github.io/Valentine-Vue-Tailwind/) |
| [tiennm99/valentine2025](https://github.com/tiennm99/valentine2025) | A fun Valentine's Day 2025 website. | [https://tiennm99.github.io/valentine2025/](https://tiennm99.github.io/valentine2025/) |
| [tiennm99/vigeoguessr](https://github.com/tiennm99/vigeoguessr) | Guess place on map from street view image | [https://vigeoguessr.netlify.app/](https://vigeoguessr.netlify.app/) |
| [tiennm99/vngeoguessr](https://github.com/tiennm99/vngeoguessr) | GeoGuessr clone for Vietnamese locations — Mapillary panoramas, Turf.js boundaries, Upstash REST leaderboards | [https://vngeoguessr.vercel.app](https://vngeoguessr.vercel.app) |
| [tiennm99/webgl-heart](https://github.com/tiennm99/webgl-heart) | | [https://tiennm99.github.io/webgl-heart/](https://tiennm99.github.io/webgl-heart/) |
| [tiennm99dev/artemis-demo-agent](https://github.com/tiennm99dev/artemis-demo-agent) | Vietnamese VNG hackathon demo agent for lost-and-found matching and an internal marketplace | [https://artemis-lime.vercel.app](https://artemis-lime.vercel.app) |
| [tiennm99dev/bonsai](https://github.com/tiennm99dev/bonsai) | Minimalist Hugo theme for link-in-bio pages — Linktree alternative with Japanese bonsai aesthetics. | [https://tiennm99dev.github.io/bonsai/](https://tiennm99dev.github.io/bonsai/) |
| [tiennm99dev/chudaibi](https://github.com/tiennm99dev/chudaibi) | Chú đại bi, từ bộ Kinh Nhật Tụng của chùa Hoằng Pháp | [https://thuvienhoangphap.github.io/chudaibi/](https://thuvienhoangphap.github.io/chudaibi/) |
| [tiennm99dev/colordle](https://github.com/tiennm99dev/colordle) | | [https://tiennm99dev.github.io/colordle/](https://tiennm99dev.github.io/colordle/) |
| [tiennm99dev/dating](https://github.com/tiennm99dev/dating) | Warm-witty dating JD and CV site built with SvelteKit | [https://tiennm99dev.github.io/dating/](https://tiennm99dev.github.io/dating/) |
| [tiennm99dev/github-readme-stats](https://github.com/tiennm99dev/github-readme-stats) | :zap: Dynamically generated stats for your github readmes | [https://github-readme-stats.vercel.app](https://github-readme-stats.vercel.app) |
| [tiennm99dev/iammiti99](https://github.com/tiennm99dev/iammiti99) | Personal link-in-bio page — a self-hosted Linktree alternative for my own use. | [https://tiennm99dev.github.io/iammiti99/](https://tiennm99dev.github.io/iammiti99/) |
| [tiennm99/blog](https://github.com/tiennm99/blog) | Source of miti99.com — personal blog (VN/EN) built with Hugo + Stack theme. | [https://miti99.com](https://miti99.com) |
| [tiennm99/miti99bot](https://github.com/tiennm99/miti99bot) | Plug-n-play Telegram bot framework in Go, self-hosted on Coolify + MongoDB Atlas with cron, games, schedules, and paper trading modules. | [https://t.me/miti99bot](https://t.me/miti99bot) |
| [tiennm99dev/mathmax](https://github.com/tiennm99dev/mathmax) | MathMax — Toán tương tác cho học sinh THCS Việt Nam (lớp 6-9). Static site. | [https://tiennm99dev.github.io/mathmax/](https://tiennm99dev.github.io/mathmax/) |
| [tiennm99/penny-pincher-provider](https://github.com/tiennm99/penny-pincher-provider) | Curated list of affordable + free LLM API providers — Claude Code passes, coding-plan subs, free-tier APIs. | [https://tiennm99.github.io/penny-pincher-provider/](https://tiennm99.github.io/penny-pincher-provider/) |
| [tiennm99/loldle](https://github.com/tiennm99/loldle) | LoLdle-style League of Legends daily champion guessing game — SvelteKit web app, Go data scraper, and Android stub in one repo; champion data auto-updates weekly | [https://tiennm99.github.io/loldle/](https://tiennm99.github.io/loldle/) |
| [tiennm99/vngeoguessr](https://github.com/tiennm99/vngeoguessr) | GeoGuessr clone for Vietnamese locations — Mapillary panoramas, Turf.js boundaries, Upstash REST leaderboards | [https://vngeoguessr.vercel.app](https://vngeoguessr.vercel.app) |
| [bkfcduchoa/bkfcduchoa.github.io](https://github.com/bkfcduchoa/bkfcduchoa.github.io) | | [https://bkfcduchoa.github.io/](https://bkfcduchoa.github.io/) |
| [tiennm99/cv](https://github.com/tiennm99/cv) | My personal CV written in Typst with RenderCV. | [https://tiennm99.github.io/cv/](https://tiennm99.github.io/cv/) |
| [tiennm99/ai-labs](https://github.com/tiennm99/ai-labs) | Experiments with AI-assisted coding workflows — ClaudeKit, Superpowers, BMAD, oh-my-claudecode, gstack, and course work, merged with full history | [https://tiennm99.github.io/ai-labs/](https://tiennm99.github.io/ai-labs/) |
| [tiennm99/sokoban](https://github.com/tiennm99/sokoban) | From-scratch Sokoban puzzle game — Svelte 5 + Vite, all 155 Microban levels, undo, progress tracking | [https://tiennm99.github.io/sokoban/](https://tiennm99.github.io/sokoban/) |
| [tiennm99/loto](https://github.com/tiennm99/loto) | Lô Tô (Vietnamese lottery bingo) number board — Next.js web app | [https://tiennm99.github.io/loto/](https://tiennm99.github.io/loto/) |
| [tiennm99/sudoku-solver](https://github.com/tiennm99/sudoku-solver) | 9×9 Sudoku solver in the browser using recursive backtracking — plain JavaScript, Svelte 5 + Vite, on GitHub Pages. | [https://tiennm99.github.io/sudoku-solver/](https://tiennm99.github.io/sudoku-solver/) |
| [tiennm99dev/timshow](https://github.com/tiennm99dev/timshow) | Tìm show — find live shows and events in Vietnam. | [https://tiennm99dev.github.io/timshow/](https://tiennm99dev.github.io/timshow/) |
| [tiennm99dev/pikachu](https://github.com/tiennm99dev/pikachu) | Pikachu tile-matching puzzle game — Phaser 3 + Next.js + TypeScript | [https://tiennm99dev.github.io/pikachu/](https://tiennm99dev.github.io/pikachu/) |
| [tiennm99dev/programming-fengshui](https://github.com/tiennm99dev/programming-fengshui) | Map programming languages to Ngũ Hành (Five Elements) by Linguist colors. Classic and HSL-classifier modes. | [https://tiennm99dev.github.io/programming-fengshui/](https://tiennm99dev.github.io/programming-fengshui/) |
| [tiennm99dev/quycongdongvi](https://github.com/tiennm99dev/quycongdongvi) | Website của Quỹ cộng đồng Vị | [https://tiennm99dev.github.io/quycongdongvi/](https://tiennm99dev.github.io/quycongdongvi/) |
| [tiennm99dev/reigen](https://github.com/tiennm99dev/reigen) | reigen | [https://tiennm99dev.github.io/reigen/](https://tiennm99dev.github.io/reigen/) |
| [tiennm99dev/rubik](https://github.com/tiennm99dev/rubik) | Interactive 3x3 Rubik's cube in the browser — drag faces, scramble, timer, undo. Three.js + Svelte 5 + Vite. | [https://tiennm99dev.github.io/rubik/](https://tiennm99dev.github.io/rubik/) |
| [tiennm99dev/thuvienhoangphap](https://github.com/tiennm99dev/thuvienhoangphap) | Thư Viện Hoằng Pháp — Vietnamese Buddhist dharma library website. | [https://thuvienhoangphap.netlify.app/](https://thuvienhoangphap.netlify.app/) |
| [tiennm99dev/timshow](https://github.com/tiennm99dev/timshow) | Tìm show — find live shows and events in Vietnam. | [https://tiennm99dev.github.io/timshow](https://tiennm99dev.github.io/timshow) |
| [tiennm99dev/tsuki](https://github.com/tiennm99dev/tsuki) | 月 — Hugo blog & portfolio theme. Vietnamese-first typography, View Transitions, Pagefind search, Giscus comments. | [https://tiennm99dev.github.io/tsuki/](https://tiennm99dev.github.io/tsuki/) |
| [tiennm99/miti-telegram](https://github.com/tiennm99/miti-telegram) | Cloudflare Worker that forwards incoming messages to my Telegram account | [https://miti-telegram.miti99.workers.dev](https://miti-telegram.miti99.workers.dev) |
| [tiennm99/claude-status-webhook](https://github.com/tiennm99/claude-status-webhook) | Telegram bot forwarding Claude Status updates — alerts for incidents and resolutions. Serverless on Cloudflare Workers. | [https://t.me/MiTiClaudeStatusBot](https://t.me/MiTiClaudeStatusBot) |
| [tiennm99/hurt](https://github.com/tiennm99/hurt) | Small website inspired by the film 'Thắp sáng anh, sưởi ấm em' — a little unusual, just for fun. | [https://tiennm99.github.io/hurt/](https://tiennm99.github.io/hurt/) |
| [tiennm99/rplace](https://github.com/tiennm99/rplace) | Collaborative pixel-art canvas (r/place clone). Svelte 5 + Hono on Cloudflare Workers + Durable Object SQLite. | [https://rplace.miti99.workers.dev/](https://rplace.miti99.workers.dev/) |
| [tiennm99/store-scraper-bot](https://github.com/tiennm99/store-scraper-bot) | Telegram bot tracking Apple App Store + Google Play app updates. JS port of store-scraper-bot-java. | [https://store-scraper-bot.vercel.app](https://store-scraper-bot.vercel.app) |
| [tiennm99/nntv](https://github.com/tiennm99/nntv) | Night Ninja: Twilight Voyage — turn-based stealth puzzle. Ninja rabbit rescues carrot princess from veggie guards. | [https://tiennm99.github.io/nntv/](https://tiennm99.github.io/nntv/) |
| [tiennm99/miti-loki](https://github.com/tiennm99/miti-loki) | Cloudflare Worker that forwards log messages to Grafana Cloud Loki. | [https://miti-loki.miti99.workers.dev/](https://miti-loki.miti99.workers.dev/) |
| [tiennm99/store-scraper-api](https://github.com/tiennm99/store-scraper-api) | HTTP wrapper hosting google-play-scraper and app-store-scraper — POST to /google/:method or /apple/:method | [https://store-scraper-api.vercel.app](https://store-scraper-api.vercel.app) |
| [tiennm99/exchange-rate-export](https://github.com/tiennm99/exchange-rate-export) | Export BIDV and Techcombank FX rates to Excel — Next.js web app with date range selection | [https://exchange-rate-export.vercel.app/](https://exchange-rate-export.vercel.app/) |
| [tiennm99/thptqg](https://github.com/tiennm99/thptqg) | Tra cứu điểm thi THPT Quốc gia 2016–2017 — 1,7 triệu thí sinh · Truy vấn SQL client-side với sql.js | [https://tiennm99.github.io/thptqg/](https://tiennm99.github.io/thptqg/) |
| [tiennm99/ocrot](https://github.com/tiennm99/ocrot) | Website mừng bạn Thân trở thành ông chủ quán ăn Ốc Rốt | [https://tiennm99.github.io/ocrot/](https://tiennm99.github.io/ocrot/) |
| [tiennm99dev/reigen](https://github.com/tiennm99dev/reigen) | reigen | [https://tiennm99dev.github.io/reigen/](https://tiennm99dev.github.io/reigen/) |
| [tiennm99dev/wolzard](https://github.com/tiennm99dev/wolzard) | | [https://tiennm99dev.github.io/wolzard/](https://tiennm99dev.github.io/wolzard/) |
| [tiennm99dev/xr2s25fe](https://github.com/tiennm99dev/xr2s25fe) | Personal device-upgrade datasheet — iPhone XR (2018) to Galaxy S25 FE (2025), rendered six ways: blueprint + five Apple-style formats. | [https://tiennm99dev.github.io/xr2s25fe/](https://tiennm99dev.github.io/xr2s25fe/) |
| [tiennm99dev/dating](https://github.com/tiennm99dev/dating) | Warm-witty dating JD and CV site built with SvelteKit | [https://tiennm99dev.github.io/dating/](https://tiennm99dev.github.io/dating/) |
| [tiennm99dev/tsuki](https://github.com/tiennm99dev/tsuki) | 月 — Hugo blog & portfolio theme. Vietnamese-first typography, View Transitions, Pagefind search, Giscus comments. | [https://tiennm99dev.github.io/tsuki/](https://tiennm99dev.github.io/tsuki/) |
| [tiennm99dev/bonsai](https://github.com/tiennm99dev/bonsai) | Minimalist Hugo theme for link-in-bio pages — Linktree alternative with Japanese bonsai aesthetics. | [https://tiennm99dev.github.io/bonsai/](https://tiennm99dev.github.io/bonsai/) |
| [ngamtheproject/ngamtheproject.github.io](https://github.com/ngamtheproject/ngamtheproject.github.io) | Website for Ngăm - a charity organization founded by high school students. | [https://ngamtheproject.github.io/](https://ngamtheproject.github.io/) |
| [tiennm99/llmapikey](https://github.com/tiennm99/llmapikey) | Pending/archived: free capped OpenRouter API key giveaway paused until a suitable LLM gateway/provider is found. | [https://llmapikey.vercel.app](https://llmapikey.vercel.app) |
| [tienthieusac/tienthieusac.github.io](https://github.com/tienthieusac/tienthieusac.github.io) | Tiên Thiếu Sắc | [https://tienthieusac.github.io/](https://tienthieusac.github.io/) |
| [tiennm99dev/chudaibi](https://github.com/tiennm99dev/chudaibi) | Chú đại bi, từ bộ Kinh Nhật Tụng của chùa Hoằng Pháp | [https://thuvienhoangphap.github.io/chudaibi/](https://thuvienhoangphap.github.io/chudaibi/) |
| [tiennm99/dental-visit](https://github.com/tiennm99/dental-visit) | Small website created for a dental visit appointment. | [https://tiennm99.github.io/dental-visit/](https://tiennm99.github.io/dental-visit/) |
| [tiennm99/mai-birthday-2021](https://github.com/tiennm99/mai-birthday-2021) | Birthday page for Trúc Mai (2021) | [https://tiennm99.github.io/mai-birthday-2021/](https://tiennm99.github.io/mai-birthday-2021/) |
| [tiennm99/apart](https://github.com/tiennm99/apart) | Small personal website with photos and music for someone far away. | [https://tiennm99.github.io/apart/](https://tiennm99.github.io/apart/) |
| [tiennm99dev/xr2s25fe](https://github.com/tiennm99dev/xr2s25fe) | Personal device-upgrade datasheet — iPhone XR (2018) to Galaxy S25 FE (2025), rendered six ways: blueprint + five Apple-style formats. | [https://tiennm99dev.github.io/xr2s25fe/](https://tiennm99dev.github.io/xr2s25fe/) |
| [tiennm99/tiennm99.github.io](https://github.com/tiennm99/tiennm99.github.io) | Redirect page for my personal website | [https://tiennm99.github.io/](https://tiennm99.github.io/) |
| [tiennm99dev/iammiti99](https://github.com/tiennm99dev/iammiti99) | Personal link-in-bio page — a self-hosted Linktree alternative for my own use. | [https://tiennm99dev.github.io/iammiti99/](https://tiennm99dev.github.io/iammiti99/) |
| [tiennm99/demngayxaem](https://github.com/tiennm99/demngayxaem) | Đếm ngày xa em — a small relationship 'days-apart' counter website. | [https://tiennm99.github.io/demngayxaem/](https://tiennm99.github.io/demngayxaem/) |
| [tiennm99/beentogether](https://github.com/tiennm99/beentogether) | Been Together — a small website celebrating time spent together with music and photos. | [https://tiennm99.github.io/beentogether/](https://tiennm99.github.io/beentogether/) |
| [tiennm99/20221225](https://github.com/tiennm99/20221225) | A small just-for-fun website built on Christmas Day 2022. | [https://tiennm99.github.io/20221225/](https://tiennm99.github.io/20221225/) |
| [tiennm99dev/programming-fengshui](https://github.com/tiennm99dev/programming-fengshui) | Map programming languages to Ngũ Hành (Five Elements) by Linguist colors. Classic and HSL-classifier modes. | [https://tiennm99dev.github.io/programming-fengshui/](https://tiennm99dev.github.io/programming-fengshui/) |
| [tiennm99/20200411](https://github.com/tiennm99/20200411) | An old gift website. Archived 2021-02-06. | [https://tiennm99.github.io/20200411/](https://tiennm99.github.io/20200411/) |
| [tiennm99dev/thuvienhoangphap](https://github.com/tiennm99dev/thuvienhoangphap) | Thư Viện Hoằng Pháp — Vietnamese Buddhist dharma library website. | [https://thuvienhoangphap.netlify.app/](https://thuvienhoangphap.netlify.app/) |
| [tiennm99/test-deeplink-sms](https://github.com/tiennm99/test-deeplink-sms) | Small test page for verifying SMS deeplink behavior on mobile devices. | [https://tiennm99.github.io/test-deeplink-sms/](https://tiennm99.github.io/test-deeplink-sms/) |
| [tiennm99dev/quycongdongvi](https://github.com/tiennm99dev/quycongdongvi) | Website của Quỹ cộng đồng Vị | [https://tiennm99dev.github.io/quycongdongvi/](https://tiennm99dev.github.io/quycongdongvi/) |
| [tiennm99/valentine2025](https://github.com/tiennm99/valentine2025) | A fun Valentine's Day 2025 website. | [https://tiennm99.github.io/valentine2025/](https://tiennm99.github.io/valentine2025/) |
| [tiennm99/double-click-test](https://github.com/tiennm99/double-click-test) | Website to quickly check a mouse for double-click issues. | [https://tiennm99.github.io/double-click-test/](https://tiennm99.github.io/double-click-test/) |
| [ngamtheproject/ngamtheproject](https://github.com/ngamtheproject/ngamtheproject) | Redirect to Ngăm's website | [https://ngamtheproject.github.io/ngamtheproject/](https://ngamtheproject.github.io/ngamtheproject/) |
| [tiennm99/tn1](https://github.com/tiennm99/tn1) | Hugo photo gallery site built on the Gallery Deluxe module | [https://tiennm99.github.io/tn1/](https://tiennm99.github.io/tn1/) |
| [survival2d/survival2d.github.io](https://github.com/survival2d/survival2d.github.io) | Repo to deploy client to GitHub Pages | [https://survival2d.github.io/](https://survival2d.github.io/) |
<!-- PROJECTS_LIST_END -->
<!-- PROJECTS_LINKED_END -->
## 3. Projects mình đóng góp
Repo của người khác (không phải của mình, cũng không thuộc org mình làm owner) mà
mình có pull request đã được merge. Sắp xếp theo số PR đã merge giảm dần, cùng số
PR thì repo cập nhật gần đây hơn lên trước.
<!-- PROJECTS_CONTRIBUTED_START -->
| Name | Description | Merged PRs | Stars |
| --- | --- | --- | --- |
| [iluwatar/java-design-patterns](https://github.com/iluwatar/java-design-patterns) | Design patterns implemented in Java | 6 | 94706 |
| [is-a-dev/register](https://github.com/is-a-dev/register) | Grab your own sweet-looking '.is-a.dev' subdomain. | 4 | 11323 |
| [is-a-good-dev/register](https://github.com/is-a-good-dev/register) | Register for your is-a-good.dev domain! | 4 | 620 |
| [open-domains/register](https://github.com/open-domains/register) | Free subdomains for personal sites, open-source projects, and more. | 4 | 2601 |
| [coollabsio/coolify](https://github.com/coollabsio/coolify) | An open-source, self-hostable PaaS alternative to Vercel, Heroku & Netlify that lets you easily deploy static sites, databases, full-stack applications and 280+ one-click services on your own servers. | 3 | 61962 |
| [tarampampam/domains](https://github.com/tarampampam/domains) | 🌐 DNS configuration for some of my domains | 3 | 807 |
| [zfoo-project/zfoo](https://github.com/zfoo-project/zfoo) | 💡Extremely fast enterprise server framework, can be used in RPC, game server, web server. | 3 | 2014 |
| [old-free-domains/register](https://github.com/old-free-domains/register) | Free subdomains for personal sites, open-source projects, and more. | 2 | 598 |
| [onyx-dot-app/documentation](https://github.com/onyx-dot-app/documentation) | Documentation for the Onyx Software | 1 | 33 |
| [oraios/serena](https://github.com/oraios/serena) | A powerful MCP toolkit for coding, providing semantic retrieval and editing capabilities - the IDE for your agent | 1 | 29543 |
| [sirmalloc/ccstatusline](https://github.com/sirmalloc/ccstatusline) | 🚀 Beautiful highly customizable statusline for Claude Code CLI with powerline support, themes, and more. | 1 | 12930 |
| [ripienaar/free-for-dev](https://github.com/ripienaar/free-for-dev) | A list of SaaS, PaaS and IaaS offerings that have free tiers of interest to devops and infradev | 1 | 137712 |
| [Bogdanp/awesome-advent-of-code](https://github.com/Bogdanp/awesome-advent-of-code) | A collection of awesome resources related to the yearly Advent of Code challenge. | 1 | 3225 |
| [kaitranntt/ccs](https://github.com/kaitranntt/ccs) | Switch between Claude accounts, Gemini, Copilot, OpenRouter (300+ models) via CLIProxyAPI OAuth proxy. Visual dashboard, remote proxy support, WebSearch fallback. Zero-config to production-ready. | 1 | 2856 |
| [projectlombok/lombok](https://github.com/projectlombok/lombok) | Very spicy additions to the Java programming language. | 1 | 13484 |
| [HugoBlox/kit](https://github.com/HugoBlox/kit) | 🧱 Describe your site, AI builds it, you own it as Markdown. Snap together Tailwind blocks like Lego — landing pages, blogs, portfolios, docs & more. No AI slop. Free to deploy anywhere 👇 | 1 | 9722 |
| [tkowalcz/tjahzi](https://github.com/tkowalcz/tjahzi) | Java clients, log4j2 and logback appenders for Grafana Loki | 1 | 139 |
| [dhyeythumar/awesome-readme-tools](https://github.com/dhyeythumar/awesome-readme-tools) | A curated list of awesome readme tools. ✌ | 1 | 265 |
| [thedev-id/thedev.id](https://github.com/thedev-id/thedev.id) | Identity for developers on the web. | 1 | 1728 |
| [is-amazing/register](https://github.com/is-amazing/register) | Register your is-amaz.ing project domain here! | 1 | 149 |
| [DavidNiessen/SimpleGuicePlugin](https://github.com/DavidNiessen/SimpleGuicePlugin) | An example for dependency injection (Google Guice) in a Spigot plugin | 1 | 2 |
<!-- PROJECTS_CONTRIBUTED_END -->