Generated Site Features
docsfy-generated sites ship with a built-in front-end feature bundle from src/docsfy/static/, copied into each output site's assets/ directory during rendering.
# src/docsfy/renderer.py
if STATIC_DIR.exists():
for static_file in STATIC_DIR.iterdir():
if static_file.is_file():
shutil.copy2(static_file, assets_dir / static_file.name)
search_index = _build_search_index(valid_pages, plan)
(output_dir / "search-index.json").write_text(
json.dumps(search_index), encoding="utf-8"
)
<!-- src/docsfy/templates/page.html -->
<script src="assets/theme.js"></script>
<script src="assets/search.js"></script>
<script src="assets/copy.js"></script>
<script src="assets/callouts.js"></script>
<script src="assets/scrollspy.js"></script>
<script src="assets/codelabels.js"></script>
<script src="assets/github.js"></script>
Search Modal
The site uses a client-side modal search (Cmd/Ctrl+K) backed by search-index.json.
- Opens via keyboard shortcut, top-bar Search button, or sidebar search input focus.
- Matches against page title and markdown content.
- Limits results to 10 entries.
- Supports arrow navigation and Enter to open the selected result.
// src/docsfy/static/search.js
fetch('search-index.json').then(function(r) { return r.json(); })
.then(function(data) { index = data; }).catch(function() {});
document.addEventListener('keydown', function(e) {
if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
e.preventDefault();
openModal();
}
if (e.key === 'Escape') closeModal();
});
var matches = index.filter(function(item) {
return item.title.toLowerCase().includes(q) || item.content.toLowerCase().includes(q);
}).slice(0, 10);
# src/docsfy/renderer.py
index.append(
{
"slug": slug,
"title": title_map.get(slug, slug),
"content": content[:2000],
}
)
Tip: Search content is truncated to the first 2000 characters per page, so placing key terms early in each page improves discoverability.
Theme Toggle (Dark/Light)
Theme state is controlled through the data-theme attribute on <html> and persisted in localStorage under theme.
// src/docsfy/static/theme.js
var stored = getTheme();
if (stored) {
document.documentElement.setAttribute('data-theme', stored);
} else {
document.documentElement.setAttribute('data-theme', 'dark');
}
if (toggle) toggle.addEventListener('click', function() {
var current = document.documentElement.getAttribute('data-theme');
var next = current === 'dark' ? 'light' : 'dark';
document.documentElement.setAttribute('data-theme', next);
setTheme(next);
});
/* src/docsfy/static/style.css */
[data-theme="dark"] .icon-sun { display: block; }
[data-theme="dark"] .icon-moon { display: none; }
Note: Generated pages default to dark mode (
<html ... data-theme="dark">) and switch to the saved preference when available.
Callouts
Callouts are authored as markdown blockquotes with a bold first label (Note, Warning, Tip, etc.). A post-render script maps those labels to callout classes.
// src/docsfy/static/callouts.js
var text = firstStrong.textContent.toLowerCase().replace(':', '').trim();
if (text === 'note' || text === 'info') {
type = 'note';
} else if (text === 'warning' || text === 'caution') {
type = 'warning';
} else if (text === 'tip' || text === 'hint') {
type = 'tip';
} else if (text === 'danger' || text === 'error') {
type = 'danger';
} else if (text === 'important') {
type = 'important';
}
if (type) {
bq.classList.add('callout', 'callout-' + type);
}
/* src/docsfy/static/style.css */
blockquote.callout-note { border-left: 4px solid #3b82f6; background: rgba(59, 130, 246, 0.08); }
blockquote.callout-warning { border-left: 4px solid #f59e0b; background: rgba(245, 158, 11, 0.08); }
blockquote.callout-tip { border-left: 4px solid #10b981; background: rgba(16, 185, 129, 0.08); }
Use the same authoring format enforced in prompt generation:
# src/docsfy/prompts.py
- Notes: > **Note:** text
- Warnings: > **Warning:** text
- Tips: > **Tip:** text
Code Copy Buttons
Every <pre> block gets a Copy button automatically at runtime.
- Uses Clipboard API when available.
- Falls back to
document.execCommand('copy')for compatibility. - Shows temporary feedback (
Copied!/Failed).
// src/docsfy/static/copy.js
document.querySelectorAll('pre').forEach(function(pre) {
var btn = document.createElement('button');
btn.className = 'copy-btn';
btn.textContent = 'Copy';
btn.addEventListener('click', function() {
var code = pre.querySelector('code');
var text = code ? code.textContent : pre.textContent;
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(text).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 2000);
}).catch(function() {
fallbackCopy(text, btn);
});
} else {
fallbackCopy(text, btn);
}
});
pre.style.position = 'relative';
pre.appendChild(btn);
});
/* src/docsfy/static/style.css */
.copy-btn { opacity: 0; }
pre:hover .copy-btn { opacity: 1; }
@media (hover: none) {
.copy-btn { opacity: 0.7; }
}
Table of Contents (TOC)
TOC generation is handled during markdown conversion and rendered only when headings are present.
# src/docsfy/renderer.py
md = markdown.Markdown(
extensions=["fenced_code", "codehilite", "tables", "toc"],
extension_configs={
"codehilite": {"css_class": "highlight", "guess_lang": False},
"toc": {"toc_depth": "2-3"},
},
)
content_html = _sanitize_html(md.convert(md_text))
toc_html = getattr(md, "toc", "")
<!-- src/docsfy/templates/page.html -->
{% if toc %}
<aside class="toc-sidebar">
<div class="toc-container">
<h3>On this page</h3>
{{ toc | safe }}
</div>
</aside>
{% endif %}
// src/docsfy/static/scrollspy.js
var tocLinks = document.querySelectorAll('.toc-container a');
...
current.link.classList.add('active');
/* src/docsfy/static/style.css */
@media (min-width: 1280px) {
.toc-sidebar { display: block; }
.content { margin-right: 220px; }
}
.toc-container ul ul { display: none; }
Warning:
scrollspy.jsappliesactive, while the stylesheet defines.toc-container a.toc-active; align class names if you want a styled active-state indicator.
GitHub Metadata (Repo Link + Stars)
When repo_url is available in the generated plan, pages render a GitHub button and lazily fetch star count from GitHub API.
# src/docsfy/main.py
plan["repo_url"] = source_url
# src/docsfy/renderer.py
repo_url: str = plan.get("repo_url", "")
...
page_html = render_page(..., repo_url=repo_url)
<!-- src/docsfy/templates/page.html -->
{% if repo_url %}
<a href="{{ repo_url }}" ... id="github-link" data-repo-url="{{ repo_url }}">
...
<span class="github-stars" id="github-stars"></span>
</a>
{% endif %}
// src/docsfy/static/github.js
var match = repoUrl.match(/github\.com[/:]([^/]+)\/([^/.]+)/);
...
fetch('https://api.github.com/repos/' + owner + '/' + repo)
.then(function(response) {
if (!response.ok) return null;
return response.json();
})
.then(function(data) {
if (!data || typeof data.stargazers_count === 'undefined') return;
var count = data.stargazers_count;
var display;
if (count >= 1000) {
display = (count / 1000).toFixed(1).replace(/\.0$/, '') + 'k';
} else {
display = count.toString();
}
starsEl.textContent = display;
starsEl.title = count.toLocaleString() + ' stars';
})
.catch(function() {
// Silently fail - star count is a nice-to-have
});
Note: If
repo_urlis empty, the GitHub link and star counter are not rendered.Tip: The regex supports both
https://github.com/org/repo(.git)andgit@github.com:org/repo.gitstyle URLs.
Verification Coverage (Tests + Pipeline)
The rendering pipeline has unit coverage for generated artifacts and a defined test command in tox.
# tests/test_renderer.py
render_site(plan=plan, pages=pages, output_dir=output_dir)
assert (output_dir / "search-index.json").exists()
index = json.loads((output_dir / "search-index.json").read_text())
assert index[0]["slug"] == "intro"
assert index[0]["title"] == "Intro"
# tox.toml
[env.unittests]
deps = ["uv"]
commands = [["uv", "run", "--extra", "dev", "pytest", "-n", "auto", "tests"]]
Manual UI checks for these generated-site features are also documented in test-plans/e2e-ui-test-plan.md (see “Test 8: Generated Docs Quality”).