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.js applies active, while the stylesheet defines .toc-container a.toc-active; align class names if you want a styled active-state indicator.


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_url is empty, the GitHub link and star counter are not rendered.

Tip: The regex supports both https://github.com/org/repo(.git) and git@github.com:org/repo.git style 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”).