mirror of
https://github.com/tiennm99/iammiti99.git
synced 2026-10-11 03:13:37 +00:00
feat: rebuild site on Hugo + bonsai theme
Replace the custom Node.js builder with Hugo and the bonsai theme (added as a submodule at themes/bonsai). Site content moves from config.yaml into [params] in hugo.toml. CI switched to Hugo Extended 0.154.0 build; daily cron dropped (no API decoration to refresh). Avatar imported from miti99 project.
This commit is contained in:
1 parent
191bf38d86
commit
37dbf84c14
16 files changed
+94
-569
No files matched your search
@@ -3,9 +3,6 @@ name: Deploy to Pages
|
|||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [main]
|
branches: [main]
|
||||||
schedule:
|
|
||||||
# Daily 00:00 UTC — refreshes stars + last-commit decoration on repo links.
|
|
||||||
- cron: "0 0 * * *"
|
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
@@ -18,33 +15,38 @@ concurrency:
|
|||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-and-deploy:
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
submodules: recursive
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- id: pages
|
||||||
|
uses: actions/configure-pages@v5
|
||||||
|
with:
|
||||||
|
enablement: true
|
||||||
|
|
||||||
|
- name: Setup Hugo Extended
|
||||||
|
uses: peaceiris/actions-hugo@v3
|
||||||
|
with:
|
||||||
|
hugo-version: '0.154.0'
|
||||||
|
extended: true
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: hugo --gc --minify --baseURL "${{ steps.pages.outputs.base_url }}/"
|
||||||
|
|
||||||
|
- uses: actions/upload-pages-artifact@v3
|
||||||
|
with:
|
||||||
|
path: public
|
||||||
|
|
||||||
|
deploy:
|
||||||
|
needs: build
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
environment:
|
environment:
|
||||||
name: github-pages
|
name: github-pages
|
||||||
url: ${{ steps.deploy.outputs.page_url }}
|
url: ${{ steps.deploy.outputs.page_url }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: 20
|
|
||||||
cache: npm
|
|
||||||
|
|
||||||
- run: npm ci
|
|
||||||
|
|
||||||
- name: Build site
|
|
||||||
run: node build.js
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
- uses: actions/configure-pages@v5
|
|
||||||
with:
|
|
||||||
enablement: true
|
|
||||||
|
|
||||||
- uses: actions/upload-pages-artifact@v3
|
|
||||||
with:
|
|
||||||
path: _site
|
|
||||||
|
|
||||||
- id: deploy
|
- id: deploy
|
||||||
uses: actions/deploy-pages@v4
|
uses: actions/deploy-pages@v4
|
||||||
+4
-24
@@ -1,36 +1,16 @@
|
|||||||
# Dependencies
|
# Hugo build output
|
||||||
node_modules/
|
public/
|
||||||
.pnp
|
resources/_gen/
|
||||||
.pnp.js
|
.hugo_build.lock
|
||||||
|
|
||||||
# Build output
|
|
||||||
dist/
|
|
||||||
build/
|
|
||||||
out/
|
|
||||||
.next/
|
|
||||||
.nuxt/
|
|
||||||
.cache/
|
|
||||||
_site/
|
|
||||||
|
|
||||||
# Env / secrets
|
# Env / secrets
|
||||||
.env
|
.env
|
||||||
.env.*
|
.env.*
|
||||||
!.env.example
|
!.env.example
|
||||||
|
|
||||||
# Logs
|
|
||||||
*.log
|
|
||||||
npm-debug.log*
|
|
||||||
yarn-debug.log*
|
|
||||||
yarn-error.log*
|
|
||||||
pnpm-debug.log*
|
|
||||||
|
|
||||||
# Editor / OS
|
# Editor / OS
|
||||||
.vscode/
|
.vscode/
|
||||||
.idea/
|
.idea/
|
||||||
*.swp
|
*.swp
|
||||||
.DS_Store
|
.DS_Store
|
||||||
Thumbs.db
|
Thumbs.db
|
||||||
|
|
||||||
# Misc
|
|
||||||
coverage/
|
|
||||||
.tmp/
|
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
[submodule "themes/bonsai"]
|
||||||
|
path = themes/bonsai
|
||||||
|
url = https://github.com/tiennm99/bonsai.git
|
||||||
@@ -9,7 +9,5 @@ You may obtain a copy of the License at
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
This project is inspired by linkyee (https://github.com/ZhgChgLi/linkyee, MIT).
|
This site is built with the bonsai Hugo theme
|
||||||
The architectural idea (config-driven personal link page deployed via GitHub
|
(https://github.com/tiennm99/bonsai, Apache-2.0).
|
||||||
Pages, with build-time decoration of GitHub repo links) is borrowed; no source
|
|
||||||
code is copied.
|
|
||||||
@@ -1,51 +1,43 @@
|
|||||||
# iammiti99
|
# iammiti99
|
||||||
|
|
||||||
Personal link-in-bio page — a self-hosted Linktree alternative for my own use.
|
Personal link-in-bio page — built with [Hugo](https://gohugo.io) and the
|
||||||
|
[bonsai](https://github.com/tiennm99/bonsai) theme. Self-hosted Linktree
|
||||||
Inspired by [linkyee](https://github.com/ZhgChgLi/linkyee) (MIT). No source
|
alternative for my own use.
|
||||||
code is copied; see [`NOTICE`](NOTICE).
|
|
||||||
|
|
||||||
## How it works
|
## How it works
|
||||||
|
|
||||||
Edit [`config.yaml`](config.yaml) → push to `main` → GitHub Actions runs
|
Edit [`hugo.toml`](hugo.toml) (`[params]` block) → push to `main` → GitHub
|
||||||
[`build.js`](build.js) (Node 20) → output deploys to GitHub Pages.
|
Actions builds with Hugo + bonsai → output deploys to GitHub Pages.
|
||||||
|
|
||||||
Links flagged with `repo: owner/name` are decorated at build time with
|
|
||||||
GitHub stars and "last updated N days ago", refreshed daily by a cron run.
|
|
||||||
|
|
||||||
## Local development
|
## Local development
|
||||||
|
|
||||||
```bash
|
Requires Hugo Extended ≥ 0.128.
|
||||||
npm install
|
|
||||||
npm run build # writes _site/index.html
|
|
||||||
npm run serve # python http.server on :8080
|
|
||||||
```
|
|
||||||
|
|
||||||
`build.js` reads `GITHUB_TOKEN` if set; without it, GitHub API calls are
|
```bash
|
||||||
unauthenticated (60 req/h, plenty for a few links).
|
git submodule update --init --recursive
|
||||||
|
hugo server # http://localhost:1313
|
||||||
|
hugo --gc --minify # build to ./public
|
||||||
|
```
|
||||||
|
|
||||||
## Deployment
|
## Deployment
|
||||||
|
|
||||||
Every push to `main` (plus the daily 00:00 UTC cron) deploys to GitHub
|
Every push to `main` deploys to GitHub Pages. The workflow self-enables Pages
|
||||||
Pages. The workflow self-enables Pages on first run via
|
on first run via `actions/configure-pages@v5` with `enablement: true`, so no
|
||||||
`actions/configure-pages@v5` with `enablement: true`, so no manual UI
|
manual UI setup is required for fresh forks (provided **Settings → Actions →
|
||||||
setup is required for fresh forks (provided **Settings → Actions →
|
|
||||||
General → Workflow permissions** is set to "Read and write").
|
General → Workflow permissions** is set to "Read and write").
|
||||||
|
|
||||||
(Optional) Custom domain: drop a `src/CNAME` file containing your apex
|
(Optional) Custom domain: drop a `static/CNAME` file containing your apex
|
||||||
domain — `build.js` copies it through to the deployed site.
|
domain — Hugo will copy it through to the deployed site.
|
||||||
|
|
||||||
## Project layout
|
## Project layout
|
||||||
|
|
||||||
```
|
```
|
||||||
config.yaml # site content — edit this
|
hugo.toml # site content — edit [params] here
|
||||||
src/
|
content/_index.md # placeholder so Hugo renders the index
|
||||||
template.html # HTML template with ${var} placeholders
|
static/images/ # avatar etc.
|
||||||
styles.css # mobile-first, light + dark via prefers-color-scheme
|
themes/bonsai/ # bonsai theme (git submodule)
|
||||||
images/ # avatar etc.
|
|
||||||
build.js # ~70 LOC, Node 20, only dep is js-yaml
|
|
||||||
.github/workflows/deploy.yml
|
.github/workflows/deploy.yml
|
||||||
_site/ # build output (gitignored)
|
public/ # build output (gitignored)
|
||||||
```
|
```
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|||||||
@@ -1,118 +0,0 @@
|
|||||||
// Build iammiti99: render src/template.html with values from config.yaml,
|
|
||||||
// decorate `repo:`-flagged links with live GitHub stars + last-commit age.
|
|
||||||
// Output: _site/index.html plus copied static assets from src/.
|
|
||||||
//
|
|
||||||
// Runs locally and in GitHub Actions. Uses Node 20+ built-in fetch.
|
|
||||||
|
|
||||||
import { readFile, writeFile, mkdir, cp } from "node:fs/promises";
|
|
||||||
import { existsSync } from "node:fs";
|
|
||||||
import yaml from "js-yaml";
|
|
||||||
|
|
||||||
const SITE_DIR = "_site";
|
|
||||||
const TEMPLATE_PATH = "src/template.html";
|
|
||||||
const CONFIG_PATH = "config.yaml";
|
|
||||||
const TOKEN = process.env.GITHUB_TOKEN;
|
|
||||||
|
|
||||||
const config = yaml.load(await readFile(CONFIG_PATH, "utf8"));
|
|
||||||
const template = await readFile(TEMPLATE_PATH, "utf8");
|
|
||||||
|
|
||||||
if (!config?.site || !config?.profile) {
|
|
||||||
throw new Error("config.yaml: `site` and `profile` are required.");
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- GitHub data fetcher ---------------------------------------------------
|
|
||||||
|
|
||||||
async function fetchRepo(slug) {
|
|
||||||
const headers = { "User-Agent": "iammiti99-build", Accept: "application/vnd.github+json" };
|
|
||||||
if (TOKEN) headers.Authorization = `Bearer ${TOKEN}`;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const r = await fetch(`https://api.github.com/repos/${slug}`, { headers });
|
|
||||||
if (!r.ok) {
|
|
||||||
console.warn(`[warn] GitHub API ${slug} → ${r.status}`);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const j = await r.json();
|
|
||||||
return { stars: j.stargazers_count, pushed: j.pushed_at };
|
|
||||||
} catch (e) {
|
|
||||||
console.warn(`[warn] GitHub API ${slug} threw: ${e.message}`);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const repoSlugs = (config.links ?? []).filter((l) => l.repo).map((l) => l.repo);
|
|
||||||
const repoData = Object.fromEntries(
|
|
||||||
await Promise.all(repoSlugs.map(async (s) => [s, await fetchRepo(s)])),
|
|
||||||
);
|
|
||||||
|
|
||||||
// --- Helpers ---------------------------------------------------------------
|
|
||||||
|
|
||||||
const daysSince = (iso) => Math.floor((Date.now() - new Date(iso).getTime()) / 86_400_000);
|
|
||||||
|
|
||||||
// Minimal HTML escape for config-derived text. Single-user content but cheap insurance.
|
|
||||||
const esc = (s) =>
|
|
||||||
String(s ?? "")
|
|
||||||
.replace(/&/g, "&")
|
|
||||||
.replace(/</g, "<")
|
|
||||||
.replace(/>/g, ">")
|
|
||||||
.replace(/"/g, """);
|
|
||||||
|
|
||||||
// --- Renderers -------------------------------------------------------------
|
|
||||||
|
|
||||||
function renderLink(l) {
|
|
||||||
const data = l.repo ? repoData[l.repo] : null;
|
|
||||||
const meta = data
|
|
||||||
? `<span class="meta">★ ${data.stars} · ${daysSince(data.pushed)}d ago</span>`
|
|
||||||
: "";
|
|
||||||
const target = esc(l.target ?? "_self");
|
|
||||||
const rel = target === "_blank" ? ' rel="noopener"' : "";
|
|
||||||
return ` <li><a href="${esc(l.url)}" target="${target}"${rel}><i class="${esc(l.icon)}" aria-hidden="true"></i><span>${esc(l.text)}</span>${meta}</a></li>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderSocial(s) {
|
|
||||||
return ` <a href="${esc(s.url)}" title="${esc(s.title)}" rel="noopener" target="_blank" aria-label="${esc(s.title)}"><i class="${esc(s.icon)}" aria-hidden="true"></i></a>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const linksHtml = (config.links ?? []).map(renderLink).join("\n");
|
|
||||||
const socialsHtml = (config.socials ?? []).map(renderSocial).join("\n");
|
|
||||||
|
|
||||||
// --- Template substitution -------------------------------------------------
|
|
||||||
|
|
||||||
const replacements = {
|
|
||||||
"${site.title}": esc(config.site.title),
|
|
||||||
"${site.description}": esc(config.site.description),
|
|
||||||
"${site.url}": esc(config.site.url),
|
|
||||||
"${site.lang}": esc(config.site.lang ?? "en"),
|
|
||||||
"${site.theme_color_light}": esc(config.site.theme_color_light ?? "#ffffff"),
|
|
||||||
"${site.theme_color_dark}": esc(config.site.theme_color_dark ?? "#0d1117"),
|
|
||||||
"${profile.name}": esc(config.profile.name),
|
|
||||||
"${profile.tagline}": esc(config.profile.tagline),
|
|
||||||
"${profile.avatar}": esc(config.profile.avatar),
|
|
||||||
"${links_html}": linksHtml,
|
|
||||||
"${socials_html}": socialsHtml,
|
|
||||||
"${footer.text}": esc(config.footer?.text ?? ""),
|
|
||||||
"${footer.copyright}": esc(config.footer?.copyright ?? ""),
|
|
||||||
"${last_modified}": new Date().toISOString(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let output = template;
|
|
||||||
for (const [key, value] of Object.entries(replacements)) {
|
|
||||||
output = output.replaceAll(key, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Write output ----------------------------------------------------------
|
|
||||||
|
|
||||||
if (!existsSync(SITE_DIR)) await mkdir(SITE_DIR, { recursive: true });
|
|
||||||
await writeFile(`${SITE_DIR}/index.html`, output);
|
|
||||||
|
|
||||||
// Copy static assets (skip template.html and the gitkeep).
|
|
||||||
for (const name of ["styles.css", "scripts.js", "images", "favicons", "CNAME"]) {
|
|
||||||
const src = `src/${name}`;
|
|
||||||
if (existsSync(src)) {
|
|
||||||
await cp(src, `${SITE_DIR}/${name}`, { recursive: true });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
`Built ${SITE_DIR}/index.html (${repoSlugs.length} repos, ${Object.values(repoData).filter(Boolean).length} resolved)`,
|
|
||||||
);
|
|
||||||
-52
@@ -1,52 +0,0 @@
|
|||||||
# iammiti99 — site configuration
|
|
||||||
# Single source of truth. build.js reads this and renders src/template.html
|
|
||||||
# into _site/index.html. Links flagged with `repo:` get live star + last-commit
|
|
||||||
# decorations from the GitHub REST API at build time.
|
|
||||||
|
|
||||||
site:
|
|
||||||
title: "Tien — links"
|
|
||||||
description: "Tien Nguyen Minh — engineer, builder, writer."
|
|
||||||
url: "https://tiennm99.github.io/iammiti99"
|
|
||||||
lang: "en"
|
|
||||||
theme_color_light: "#f7f7f7"
|
|
||||||
theme_color_dark: "#1b1b1e"
|
|
||||||
|
|
||||||
profile:
|
|
||||||
name: "@miti99"
|
|
||||||
tagline: "Engineer building things on the web."
|
|
||||||
avatar: "./images/avatar.jpg"
|
|
||||||
|
|
||||||
# Optional. Leave empty string to disable.
|
|
||||||
analytics:
|
|
||||||
google_analytics_id: ""
|
|
||||||
|
|
||||||
# Primary list — rendered as full-width buttons in order.
|
|
||||||
# Fields: text (required), url (required), icon (FA class), target, repo (owner/name)
|
|
||||||
# `repo:` triggers a GitHub API fetch and adds "★ N · Md ago" to the button.
|
|
||||||
links:
|
|
||||||
- text: "GitHub"
|
|
||||||
url: "https://github.com/tiennm99"
|
|
||||||
icon: "fa-brands fa-github"
|
|
||||||
target: "_blank"
|
|
||||||
- text: "iammiti99"
|
|
||||||
url: "https://github.com/tiennm99/iammiti99"
|
|
||||||
icon: "fa-solid fa-link"
|
|
||||||
target: "_blank"
|
|
||||||
repo: "tiennm99/iammiti99"
|
|
||||||
- text: "Email"
|
|
||||||
url: "mailto:minhtienit99@gmail.com"
|
|
||||||
icon: "fa-solid fa-envelope"
|
|
||||||
target: "_self"
|
|
||||||
|
|
||||||
# Small icon row below the link buttons.
|
|
||||||
socials:
|
|
||||||
- icon: "fa-brands fa-github"
|
|
||||||
url: "https://github.com/tiennm99"
|
|
||||||
title: "GitHub"
|
|
||||||
- icon: "fa-solid fa-envelope"
|
|
||||||
url: "mailto:minhtienit99@gmail.com"
|
|
||||||
title: "Email"
|
|
||||||
|
|
||||||
footer:
|
|
||||||
text: "Thanks for stopping by."
|
|
||||||
copyright: "© 2026 Tien Nguyen Minh"
|
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
title: "Home"
|
||||||
|
---
|
||||||
|
|
||||||
|
This file exists so Hugo renders the index page. All bio content comes from `[params]` in `hugo.toml`.
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
baseURL = "https://tiennm99.github.io/iammiti99/"
|
||||||
|
languageCode = "en"
|
||||||
|
title = "Tien — links"
|
||||||
|
theme = "bonsai"
|
||||||
|
|
||||||
|
# Single-page bio — disable everything Hugo doesn't need.
|
||||||
|
disableKinds = ["taxonomy", "term", "RSS", "sitemap", "404"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
name = "@miti99"
|
||||||
|
tagline = "Engineer building things on the web."
|
||||||
|
bio = "Tien Nguyen Minh — engineer, builder, writer."
|
||||||
|
avatar = "/images/avatar.jpg"
|
||||||
|
themeToggle = false
|
||||||
|
footerText = "Thanks for stopping by. © 2026 Tien Nguyen Minh"
|
||||||
|
|
||||||
|
[[params.links]]
|
||||||
|
title = "GitHub"
|
||||||
|
url = "https://github.com/tiennm99"
|
||||||
|
icon = "github"
|
||||||
|
|
||||||
|
[[params.links]]
|
||||||
|
title = "iammiti99"
|
||||||
|
url = "https://github.com/tiennm99/iammiti99"
|
||||||
|
icon = "link"
|
||||||
|
|
||||||
|
[[params.links]]
|
||||||
|
title = "Email"
|
||||||
|
url = "mailto:minhtienit99@gmail.com"
|
||||||
|
icon = "mail"
|
||||||
Generated
-37
@@ -1,37 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "iammiti99",
|
|
||||||
"version": "0.1.0",
|
|
||||||
"lockfileVersion": 3,
|
|
||||||
"requires": true,
|
|
||||||
"packages": {
|
|
||||||
"": {
|
|
||||||
"name": "iammiti99",
|
|
||||||
"version": "0.1.0",
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"dependencies": {
|
|
||||||
"js-yaml": "^4.1.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=20"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/argparse": {
|
|
||||||
"version": "2.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
|
||||||
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
|
|
||||||
"license": "Python-2.0"
|
|
||||||
},
|
|
||||||
"node_modules/js-yaml": {
|
|
||||||
"version": "4.1.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
|
|
||||||
"integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"argparse": "^2.0.1"
|
|
||||||
},
|
|
||||||
"bin": {
|
|
||||||
"js-yaml": "bin/js-yaml.js"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "iammiti99",
|
|
||||||
"version": "0.1.0",
|
|
||||||
"description": "Personal link page for tiennm99 — config-driven, deployed to GitHub Pages.",
|
|
||||||
"type": "module",
|
|
||||||
"private": true,
|
|
||||||
"engines": {
|
|
||||||
"node": ">=20"
|
|
||||||
},
|
|
||||||
"scripts": {
|
|
||||||
"build": "node build.js",
|
|
||||||
"serve": "python3 -m http.server -d _site 8080"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"js-yaml": "^4.1.0"
|
|
||||||
},
|
|
||||||
"license": "Apache-2.0"
|
|
||||||
}
|
|
||||||
Whitespace-only changes.
-175
@@ -1,175 +0,0 @@
|
|||||||
/* iammiti99 — minimalist link page
|
|
||||||
Light + dark via prefers-color-scheme. No JS toggle. */
|
|
||||||
|
|
||||||
:root {
|
|
||||||
--bg: #f7f7f7;
|
|
||||||
--fg: #1b1b1e;
|
|
||||||
--muted: #5b6068;
|
|
||||||
--card: #ffffff;
|
|
||||||
--button-bg: #ffffff;
|
|
||||||
--button-bg-hover: #efefef;
|
|
||||||
--border: #e3e5e8;
|
|
||||||
--accent: #1b1b1e;
|
|
||||||
--shadow: 0 1px 2px rgba(0, 0, 0, 0.04), 0 4px 16px rgba(0, 0, 0, 0.06);
|
|
||||||
--radius: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
|
||||||
:root {
|
|
||||||
--bg: #0d1117;
|
|
||||||
--fg: #e6edf3;
|
|
||||||
--muted: #8b949e;
|
|
||||||
--card: #161b22;
|
|
||||||
--button-bg: #21262d;
|
|
||||||
--button-bg-hover: #30363d;
|
|
||||||
--border: #30363d;
|
|
||||||
--accent: #e6edf3;
|
|
||||||
--shadow: 0 1px 2px rgba(0, 0, 0, 0.4), 0 4px 16px rgba(0, 0, 0, 0.3);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
*,
|
|
||||||
*::before,
|
|
||||||
*::after {
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
html {
|
|
||||||
-webkit-text-size-adjust: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
margin: 0;
|
|
||||||
min-height: 100vh;
|
|
||||||
background: var(--bg);
|
|
||||||
color: var(--fg);
|
|
||||||
font: 16px/1.5 system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
padding: 24px 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card {
|
|
||||||
width: 100%;
|
|
||||||
max-width: 480px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.avatar {
|
|
||||||
width: 120px;
|
|
||||||
height: 120px;
|
|
||||||
border-radius: 50%;
|
|
||||||
object-fit: cover;
|
|
||||||
border: 2px solid var(--border);
|
|
||||||
margin: 0 auto 12px;
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.name {
|
|
||||||
font-size: 1.5rem;
|
|
||||||
font-weight: 700;
|
|
||||||
margin: 8px 0 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tagline {
|
|
||||||
color: var(--muted);
|
|
||||||
margin: 0 0 28px;
|
|
||||||
line-height: 1.6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.links {
|
|
||||||
list-style: none;
|
|
||||||
padding: 0;
|
|
||||||
margin: 0 0 24px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.links a {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 10px;
|
|
||||||
background: var(--button-bg);
|
|
||||||
color: var(--fg);
|
|
||||||
text-decoration: none;
|
|
||||||
padding: 14px 18px;
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
border-radius: var(--radius);
|
|
||||||
box-shadow: var(--shadow);
|
|
||||||
font-weight: 500;
|
|
||||||
transition: background 0.15s ease, transform 0.15s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.links a:hover {
|
|
||||||
background: var(--button-bg-hover);
|
|
||||||
transform: translateY(-1px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.links a:active {
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
.links a i {
|
|
||||||
font-size: 18px;
|
|
||||||
width: 22px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.links a .meta {
|
|
||||||
margin-left: auto;
|
|
||||||
font-size: 0.8rem;
|
|
||||||
color: var(--muted);
|
|
||||||
font-weight: 400;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.socials {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 18px;
|
|
||||||
margin-bottom: 32px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.socials a {
|
|
||||||
color: var(--fg);
|
|
||||||
font-size: 22px;
|
|
||||||
width: 44px;
|
|
||||||
height: 44px;
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
border-radius: 50%;
|
|
||||||
transition: color 0.15s ease, transform 0.15s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.socials a:hover {
|
|
||||||
color: var(--muted);
|
|
||||||
transform: scale(1.08);
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer {
|
|
||||||
color: var(--muted);
|
|
||||||
font-size: 0.875rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer p {
|
|
||||||
margin: 0 0 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.copyright {
|
|
||||||
display: block;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 360px) {
|
|
||||||
.links a {
|
|
||||||
padding: 12px 14px;
|
|
||||||
font-size: 0.95rem;
|
|
||||||
}
|
|
||||||
.links a .meta {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
<html lang="${site.lang}">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8" />
|
|
||||||
<meta
|
|
||||||
name="viewport"
|
|
||||||
content="width=device-width, initial-scale=1, viewport-fit=cover"
|
|
||||||
/>
|
|
||||||
<title>${site.title}</title>
|
|
||||||
<meta name="description" content="${site.description}" />
|
|
||||||
<meta name="author" content="${profile.name}" />
|
|
||||||
<meta name="last-modified" content="${last_modified}" />
|
|
||||||
|
|
||||||
<!-- OpenGraph -->
|
|
||||||
<meta property="og:type" content="website" />
|
|
||||||
<meta property="og:title" content="${site.title}" />
|
|
||||||
<meta property="og:description" content="${site.description}" />
|
|
||||||
<meta property="og:url" content="${site.url}" />
|
|
||||||
<meta property="og:image" content="${profile.avatar}" />
|
|
||||||
|
|
||||||
<!-- Twitter -->
|
|
||||||
<meta name="twitter:card" content="summary" />
|
|
||||||
<meta name="twitter:title" content="${site.title}" />
|
|
||||||
<meta name="twitter:description" content="${site.description}" />
|
|
||||||
<meta name="twitter:image" content="${profile.avatar}" />
|
|
||||||
|
|
||||||
<!-- Theme color (light + dark) -->
|
|
||||||
<meta
|
|
||||||
name="theme-color"
|
|
||||||
media="(prefers-color-scheme: light)"
|
|
||||||
content="${site.theme_color_light}"
|
|
||||||
/>
|
|
||||||
<meta
|
|
||||||
name="theme-color"
|
|
||||||
media="(prefers-color-scheme: dark)"
|
|
||||||
content="${site.theme_color_dark}"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- Apple PWA -->
|
|
||||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
|
||||||
<meta
|
|
||||||
name="apple-mobile-web-app-status-bar-style"
|
|
||||||
content="black-translucent"
|
|
||||||
/>
|
|
||||||
<meta name="apple-mobile-web-app-title" content="${site.title}" />
|
|
||||||
|
|
||||||
<!-- Font Awesome 6 (CDN). TODO: add SRI integrity hash. -->
|
|
||||||
<link
|
|
||||||
rel="stylesheet"
|
|
||||||
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css"
|
|
||||||
crossorigin="anonymous"
|
|
||||||
referrerpolicy="no-referrer"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<link rel="stylesheet" href="./styles.css" />
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<main class="card">
|
|
||||||
<img
|
|
||||||
class="avatar"
|
|
||||||
src="${profile.avatar}"
|
|
||||||
alt="${profile.name} avatar"
|
|
||||||
width="120"
|
|
||||||
height="120"
|
|
||||||
loading="eager"
|
|
||||||
decoding="async"
|
|
||||||
/>
|
|
||||||
<h1 class="name">${profile.name}</h1>
|
|
||||||
<p class="tagline">${profile.tagline}</p>
|
|
||||||
|
|
||||||
<ul class="links">
|
|
||||||
${links_html}
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<nav class="socials" aria-label="Social profiles">
|
|
||||||
${socials_html}
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
<footer class="footer">
|
|
||||||
<p>${footer.text}</p>
|
|
||||||
<small class="copyright">${footer.copyright}</small>
|
|
||||||
</footer>
|
|
||||||
</main>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 635 KiB |
Submodule
+1
Submodule themes/bonsai added at 9be87cd573.
Reference in new issue
Block a user