diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index e4e65d7..b60a350 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -25,6 +25,9 @@ jobs:
- name: Build example - multi-page
run: go run ./cmd/inkssg build ./examples/multi-page
+ - name: Build example - bio
+ run: go run ./cmd/inkssg build ./examples/bio
+
- name: Build example - library
working-directory: examples/library
run: |
diff --git a/README.md b/README.md
index d09f61e..a4dc1e8 100644
--- a/README.md
+++ b/README.md
@@ -79,7 +79,7 @@ Build output goes to `public/`.
## Themes
-inkssg ships with two themes: `minimal` and `devtool`. Pick one in `ink.yaml`:
+inkssg ships with three themes: `minimal`, `devtool`, and `bio`. Pick one in `ink.yaml`:
```yaml
default_theme: devtool
diff --git a/examples/bio/ink.yaml b/examples/bio/ink.yaml
new file mode 100644
index 0000000..a761ecb
--- /dev/null
+++ b/examples/bio/ink.yaml
@@ -0,0 +1,31 @@
+name: snowz.ai
+avatar: /assets/img/avatar.png
+bio: Open Source Engineer · AI workflows and tools
+status: currently building vikusha
+default_theme: bio
+output_dir: public
+
+meta:
+ lang: en
+ title: snowz.ai
+ description: AI workflows and developer tools.
+ siteUrl: https://snowz.ai
+
+projects:
+ - name: vikusha
+ description: framework for AI assistants
+ url: https://github.com/snowztech/vikusha
+ badge: go · wip
+ - name: nevinho
+ description: my personal AI assistant
+ url: https://github.com/lucasnevespereira/nevinho
+ badge: go
+
+contact:
+ socials:
+ - name: github
+ url: https://github.com/snowztech
+ - name: youtube
+ url: https://www.youtube.com/c/lucaasnp
+ - name: tiktok
+ url: https://www.tiktok.com/@snowz.ai
diff --git a/examples/bio/pages/index/content.md b/examples/bio/pages/index/content.md
new file mode 100644
index 0000000..9c3fcd8
--- /dev/null
+++ b/examples/bio/pages/index/content.md
@@ -0,0 +1,4 @@
+---
+title: snowz.ai
+description: AI workflows and developer tools
+---
diff --git a/inkssg.go b/inkssg.go
index b2e6061..a5afaf7 100644
--- a/inkssg.go
+++ b/inkssg.go
@@ -44,15 +44,25 @@ type Page struct {
}
type SiteConfig struct {
- Name string `yaml:"name"`
- Avatar string `yaml:"avatar"`
- Bio string `yaml:"bio"`
- Install string `yaml:"install"`
- DefaultTheme string `yaml:"default_theme"`
- OutputDir string `yaml:"output_dir"`
- Links []Link `yaml:"links"`
- Meta Meta `yaml:"meta"`
- Contact Contact `yaml:"contact"`
+ Name string `yaml:"name"`
+ Avatar string `yaml:"avatar"`
+ Bio string `yaml:"bio"`
+ Status string `yaml:"status"`
+ Install string `yaml:"install"`
+ DefaultTheme string `yaml:"default_theme"`
+ OutputDir string `yaml:"output_dir"`
+ Links []Link `yaml:"links"`
+ Projects []Project `yaml:"projects"`
+ Meta Meta `yaml:"meta"`
+ Contact Contact `yaml:"contact"`
+}
+
+type Project struct {
+ Name string `yaml:"name"`
+ Description string `yaml:"description"`
+ URL string `yaml:"url"`
+ Image string `yaml:"image"`
+ Badge string `yaml:"badge"`
}
type Link struct {
diff --git a/themes/bio/layout.html b/themes/bio/layout.html
new file mode 100644
index 0000000..b2d6c46
--- /dev/null
+++ b/themes/bio/layout.html
@@ -0,0 +1,88 @@
+
+
+
+
+
+
+ {{if .Site.Meta.Title}}{{.Site.Meta.Title}}{{else}}{{.Page.Title}}{{end}}
+ {{if .Site.Meta.Author}}{{end}}
+ {{if .Site.Meta.SiteUrl}}{{end}}
+
+
+
+
+
+
+
+
+
+ {{if .Site.Meta.Title}}
+
+
+ {{end}}
+ {{if .Site.Meta.Description}}{{end}}
+ {{if .Site.Meta.Lang}}{{end}}
+ {{if .Site.Avatar}}
+
+
+
+ {{end}}
+
+
+
+
+ {{if .Site.Avatar}}
+
+ {{end}}
+
+
+
+
+ {{if .Site.Name}}
{{.Site.Name}}
{{end}}
+ {{if .Site.Bio}}
{{.Site.Bio}}
{{end}}
+ {{if .Site.Status}}
+
+
+ {{.Site.Status}}
+
+ {{end}}
+
+
+ {{if .Content}}{{.Content}}
{{end}}
+
+ {{if .Site.Projects}}
+
+
// projects
+
+ {{range .Site.Projects}}
+ -
+ {{if .Image}}
{{end}}
+
+
{{if .URL}}{{.Name}}{{else}}{{.Name}}{{end}}
+ {{if .Description}}
{{.Description}}{{end}}
+
+ {{if .Badge}}{{.Badge}}{{end}}
+
+ {{end}}
+
+
+ {{end}}
+
+
+
+
+
+
diff --git a/themes/bio/script.js b/themes/bio/script.js
new file mode 100644
index 0000000..133c1a6
--- /dev/null
+++ b/themes/bio/script.js
@@ -0,0 +1,46 @@
+(function () {
+ const toggle = document.getElementById("theme-toggle");
+ const icon = toggle?.querySelector(".theme-icon");
+ const body = document.body;
+
+ let savedTheme;
+ try {
+ savedTheme = localStorage.getItem("theme");
+ } catch (err) {}
+
+ const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
+ const currentTheme = savedTheme || (prefersDark ? "dark" : "light");
+
+ if (currentTheme === "dark") {
+ body.classList.add("dark");
+ document.documentElement.classList.remove("light");
+ if (icon) icon.textContent = "●";
+ } else {
+ body.classList.remove("dark");
+ document.documentElement.classList.add("light");
+ if (icon) icon.textContent = "○";
+ }
+
+ if (toggle) {
+ const handleToggle = (e) => {
+ e.preventDefault();
+ e.stopPropagation();
+ const isDark = body.classList.toggle("dark");
+ if (isDark) {
+ document.documentElement.classList.remove("light");
+ } else {
+ document.documentElement.classList.add("light");
+ }
+ try {
+ localStorage.setItem("theme", isDark ? "dark" : "light");
+ } catch (err) {}
+ if (icon) icon.textContent = isDark ? "●" : "○";
+ };
+
+ toggle.addEventListener("click", handleToggle);
+ toggle.addEventListener("touchend", handleToggle, { passive: false });
+ }
+
+ const yearEl = document.querySelector(".year");
+ if (yearEl) yearEl.textContent = new Date().getFullYear();
+})();
diff --git a/themes/bio/styles.css b/themes/bio/styles.css
new file mode 100644
index 0000000..706ac6e
--- /dev/null
+++ b/themes/bio/styles.css
@@ -0,0 +1,326 @@
+/* Bio Theme — centered profile card with project list */
+
+* {
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+}
+
+:root {
+ --font-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
+ --font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
+
+ --bg-light: #ffffff;
+ --text-light: #1a1a1a;
+ --text-muted-light: #666666;
+ --link-light: #0066cc;
+ --border-light: #e0e0e0;
+
+ --bg-dark: #0d0d0d;
+ --text-dark: #e6e6e6;
+ --text-muted-dark: #888888;
+ --link-dark: #66b3ff;
+ --border-dark: #333333;
+
+ --bg: var(--bg-light);
+ --text: var(--text-light);
+ --text-muted: var(--text-muted-light);
+ --link: var(--link-light);
+ --border: var(--border-light);
+}
+
+body.dark {
+ --bg: var(--bg-dark);
+ --text: var(--text-dark);
+ --text-muted: var(--text-muted-dark);
+ --link: var(--link-dark);
+ --border: var(--border-dark);
+}
+
+:root.light {
+ --bg: var(--bg-light);
+ --text: var(--text-light);
+ --text-muted: var(--text-muted-light);
+ --link: var(--link-light);
+ --border: var(--border-light);
+}
+
+@media (prefers-color-scheme: dark) {
+ :root:not(.light) {
+ --bg: var(--bg-dark);
+ --text: var(--text-dark);
+ --text-muted: var(--text-muted-dark);
+ --link: var(--link-dark);
+ --border: var(--border-dark);
+ }
+}
+
+html {
+ font-family: var(--font-mono);
+ font-size: 15px;
+ line-height: 1.6;
+}
+
+body {
+ background-color: var(--bg);
+ color: var(--text);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ min-height: 100vh;
+ padding: 2rem 1.5rem;
+}
+
+main {
+ width: 100%;
+ max-width: 440px;
+ position: relative;
+}
+
+header {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ margin-bottom: 1.25rem;
+ gap: 1rem;
+}
+
+.avatar {
+ width: 88px;
+ height: 88px;
+ border-radius: 50%;
+ object-fit: cover;
+}
+
+.header-actions {
+ position: absolute;
+ top: 0;
+ right: 0;
+}
+
+.theme-toggle {
+ background: none;
+ border: none;
+ cursor: pointer;
+ font-family: inherit;
+ font-size: 18px;
+ color: var(--text-muted);
+ padding: 0.75rem;
+ margin: -0.5rem;
+ transition: color 0.2s ease;
+ outline: none;
+}
+
+.theme-toggle:hover {
+ color: var(--text);
+}
+
+.hero {
+ text-align: center;
+ margin-bottom: 1.75rem;
+}
+
+.hero-name {
+ font-weight: 600;
+ font-size: 17px;
+ margin-bottom: 0.35rem;
+}
+
+.hero-tagline {
+ color: var(--text-muted);
+ font-size: 13px;
+}
+
+.status {
+ display: inline-flex;
+ align-items: center;
+ gap: 0.5rem;
+ font-size: 12px;
+ color: var(--text-muted);
+ margin-top: 0.75rem;
+}
+
+.status-dot {
+ width: 6px;
+ height: 6px;
+ border-radius: 50%;
+ background: #4ade80;
+ box-shadow: 0 0 0 0 rgba(74, 222, 128, 0.7);
+ animation: pulse 2s infinite;
+}
+
+@keyframes pulse {
+ 0% { box-shadow: 0 0 0 0 rgba(74, 222, 128, 0.5); }
+ 70% { box-shadow: 0 0 0 6px rgba(74, 222, 128, 0); }
+ 100% { box-shadow: 0 0 0 0 rgba(74, 222, 128, 0); }
+}
+
+.content {
+ text-align: center;
+ margin-bottom: 1.75rem;
+ font-size: 14px;
+ color: var(--text-muted);
+}
+
+.content a {
+ color: var(--link);
+ text-decoration: none;
+ border-bottom: 1px solid transparent;
+ transition: border-color 0.2s ease;
+}
+
+.content a:hover {
+ border-bottom-color: var(--link);
+}
+
+.projects {
+ margin-bottom: 1.75rem;
+}
+
+.section-label {
+ font-size: 11px;
+ color: var(--text-muted);
+ text-transform: lowercase;
+ letter-spacing: 0.08em;
+ margin: 0 0 0.75rem;
+ opacity: 0.6;
+}
+
+.projects ul {
+ list-style: none;
+ display: flex;
+ flex-direction: column;
+ gap: 0.5rem;
+}
+
+.project {
+ position: relative;
+ padding: 0.85rem 1rem;
+ display: flex;
+ align-items: center;
+ gap: 0.85rem;
+ border: 1px solid var(--border);
+ border-radius: 10px;
+ background: rgba(255, 255, 255, 0.02);
+ transition: border-color 0.2s ease, background 0.2s ease, transform 0.15s ease;
+}
+
+.project:hover {
+ border-color: var(--text-muted);
+ background: rgba(255, 255, 255, 0.04);
+ transform: translateY(-1px);
+}
+
+body:not(.dark) .project {
+ background: rgba(0, 0, 0, 0.02);
+}
+
+body:not(.dark) .project:hover {
+ background: rgba(0, 0, 0, 0.04);
+}
+
+.project a {
+ text-decoration: none;
+ color: var(--text);
+}
+
+.project a::after {
+ content: "";
+ position: absolute;
+ inset: 0;
+}
+
+.project-icon {
+ width: 38px;
+ height: 38px;
+ border-radius: 50%;
+ object-fit: cover;
+ flex-shrink: 0;
+}
+
+.project-body {
+ display: flex;
+ flex-direction: column;
+ gap: 0.15rem;
+ min-width: 0;
+}
+
+.project-name {
+ font-weight: 600;
+ font-size: 14px;
+}
+
+.project-name a {
+ color: var(--link);
+}
+
+.project-desc {
+ font-size: 12px;
+ color: var(--text-muted);
+}
+
+.project-meta {
+ margin-left: auto;
+ font-size: 10px;
+ color: var(--text-muted);
+ opacity: 0.7;
+ letter-spacing: 0.05em;
+ padding-left: 0.5rem;
+ flex-shrink: 0;
+}
+
+footer {
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+ align-items: center;
+ border-top: 1px solid var(--border);
+ padding-top: 1.25rem;
+ font-size: 14px;
+ color: var(--text-muted);
+}
+
+.links {
+ display: flex;
+ gap: 1.25rem;
+ justify-content: center;
+ flex-wrap: wrap;
+}
+
+.links a {
+ color: var(--text-muted);
+ text-decoration: none;
+ transition: color 0.2s ease;
+}
+
+.links a:hover {
+ color: var(--text);
+}
+
+.copyright {
+ font-size: 11px;
+ opacity: 0.6;
+}
+
+@media (max-width: 480px) {
+ body {
+ padding: 1.5rem 1rem;
+ align-items: flex-start;
+ }
+ .avatar {
+ width: 72px;
+ height: 72px;
+ }
+ .project {
+ padding: 0.75rem 0.85rem;
+ }
+ .project-icon {
+ width: 34px;
+ height: 34px;
+ }
+}
+
+::selection {
+ background-color: var(--link);
+ color: var(--bg);
+}