Extending docsfy
docsfy has four main extension surfaces:
- Prompt construction (
src/docsfy/prompts.py) - HTML rendering and template selection (
src/docsfy/renderer.py,src/docsfy/templates/) - Frontend behavior and styling (
src/docsfy/static/plus shared template partials) - Generation orchestration and caching (
src/docsfy/main.py,src/docsfy/generator.py,src/docsfy/repository.py,src/docsfy/storage.py)
Note: docsfy uses two template contexts: - Generated docs pages:
index.html,page.html, and static assets copied toassets/- Web app UI (dashboard/admin/status/login): Jinja templates rendered by FastAPI, many with inline CSS/JS
1) Customizing prompts
All planner/page prompts are built in src/docsfy/prompts.py.
PLAN_SCHEMA = """{
"project_name": "string - project name",
"tagline": "string - one-line project description",
"navigation": [
{
"group": "string - section group name",
"pages": [
{
"slug": "string - URL-friendly page identifier",
"title": "string - human-readable page title",
"description": "string - brief description of what this page covers"
}
]
}
]
}"""
def build_planner_prompt(project_name: str) -> str:
return f"""You are a technical documentation planner. Explore this repository thoroughly.
Explore the source code, configuration files, tests, CI/CD pipelines, and project structure.
Do NOT rely on the README — understand the project from its code and configuration.
...
Output format:
{PLAN_SCHEMA}"""
def build_page_prompt(project_name: str, page_title: str, page_description: str) -> str:
return f"""You are a technical documentation writer. Explore this repository to write
the "{page_title}" page for the {project_name} documentation.
...
Use these callout formats for special content:
- Notes: > **Note:** text
- Warnings: > **Warning:** text
- Tips: > **Tip:** text
...
Output ONLY the markdown content for this page. No wrapping, no explanation."""
Prompt contract you must preserve
The generation pipeline expects plan JSON with navigation -> pages -> slug/title/description:
for group in plan.get("navigation", []):
for page in group.get("pages", []):
slug = page.get("slug", "")
title = page.get("title", slug)
Warning: If you change prompt output shape, update all plan consumers (
generator.py,renderer.py, and any tests expectingnavigation/pages).
2) Customizing renderer templates
Renderer wiring lives in src/docsfy/renderer.py:
TEMPLATES_DIR = Path(__file__).parent / "templates"
STATIC_DIR = Path(__file__).parent / "static"
_jinja_env = Environment(
loader=FileSystemLoader(str(TEMPLATES_DIR)),
autoescape=select_autoescape(["html"]),
)
Generated docs pages use index.html and page.html:
def render_page(...):
env = _get_jinja_env()
template = env.get_template("page.html")
content_html, toc_html = _md_to_html(markdown_content)
return template.render(...)
def render_index(...):
env = _get_jinja_env()
template = env.get_template("index.html")
return template.render(...)
Site output assembly (render_site) includes static copy, HTML, markdown, search index, and LLM files:
if output_dir.exists():
shutil.rmtree(output_dir)
output_dir.mkdir(parents=True, exist_ok=True)
assets_dir = output_dir / "assets"
assets_dir.mkdir(exist_ok=True)
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)
(output_dir / "index.html").write_text(index_html, encoding="utf-8")
(output_dir / f"{slug}.html").write_text(page_html, encoding="utf-8")
(output_dir / f"{slug}.md").write_text(md_content, encoding="utf-8")
(output_dir / "search-index.json").write_text(json.dumps(search_index), encoding="utf-8")
(output_dir / "llms.txt").write_text(llms_txt, encoding="utf-8")
(output_dir / "llms-full.txt").write_text(llms_full_txt, encoding="utf-8")
Warning:
render_site()deletesoutput_dirbefore rendering. Do not place manual files there unless your extension re-creates them every run.
Markdown-to-HTML behavior you can extend
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))
The sanitizer strips dangerous tags/attributes and allowlists URL schemes (http://, https://, #, /, mailto:). If you loosen this, update tests/test_renderer.py.
3) Customizing frontend assets
Generated docs pages load assets from assets/ (copied from src/docsfy/static/):
<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>
{% include '_sidebar.html' %}
Callout behavior
src/docsfy/static/callouts.js turns blockquotes into styled callouts based on first bold token:
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';
}
This matches the prompt’s preferred syntax (> **Note:**, > **Warning:**, > **Tip:**) and additional aliases.
Theme, search, and code-label hooks
theme.js: togglesdata-themeand persistslocalStorage["theme"]search.js: loadssearch-index.jsonand provides Cmd/Ctrl+K modal searchcodelabels.js: mapslanguage-*classes to human labelsstyle.css: centralized design tokens (:rootand[data-theme="dark"])
Tip: To add a new docs-page behavior, add a file under
src/docsfy/static/, then include it insrc/docsfy/templates/page.htmlandsrc/docsfy/templates/index.html.
4) Customizing generation logic
High-level flow starts in POST /api/generate (src/docsfy/main.py) and runs _run_generation() / _generate_from_path().
Core orchestration:
plan = await run_planner(
repo_path=repo_dir,
project_name=project_name,
ai_provider=ai_provider,
ai_model=ai_model,
ai_cli_timeout=ai_cli_timeout,
)
plan["repo_url"] = source_url
...
pages = await generate_all_pages(
repo_path=repo_dir,
plan=plan,
cache_dir=cache_dir,
ai_provider=ai_provider,
ai_model=ai_model,
ai_cli_timeout=ai_cli_timeout,
use_cache=use_cache if use_cache else not force,
project_name=project_name,
owner=owner,
)
site_dir = get_project_site_dir(project_name, ai_provider, ai_model, owner)
render_site(plan=plan, pages=pages, output_dir=site_dir)
Page generation parallelism
src/docsfy/generator.py limits concurrent page jobs:
MAX_CONCURRENT_PAGES = 5
...
results = await run_parallel_with_limit(
coroutines, max_concurrency=MAX_CONCURRENT_PAGES
)
Incremental regeneration path
When commits differ, docsfy diffs changed files and asks the incremental planner which page slugs to invalidate:
changed_files = get_changed_files(repo_dir, old_sha, commit_sha)
...
pages_to_regen = await run_incremental_planner(
repo_dir,
project_name,
ai_provider,
ai_model,
changed_files,
existing_plan,
ai_cli_timeout,
)
if pages_to_regen != ["all"]:
for slug in pages_to_regen:
cache_file = cache_dir / f"{slug}.md"
if cache_file.exists():
cache_file.unlink()
Cache/output location model
src/docsfy/storage.py defines project storage layout:
def get_project_dir(name: str, ai_provider: str = "", ai_model: str = "", owner: str = "") -> Path:
...
safe_owner = _validate_owner(owner)
return PROJECTS_DIR / safe_owner / _validate_name(name) / ai_provider / ai_model
def get_project_site_dir(...):
return get_project_dir(...) / "site"
def get_project_cache_dir(...):
return get_project_dir(...) / "cache" / "pages"
Warning: Slug/path safety checks are enforced in both generation and rendering. If you change slug rules, update all validations (
main.py,generator.py,renderer.py) consistently.
Adding a new AI provider (beyond claude/gemini/cursor)
Provider support is explicitly constrained in request validation and API checks:
ai_provider: Literal["claude", "gemini", "cursor"] | None = None
if ai_provider not in ("claude", "gemini", "cursor"):
raise HTTPException(
status_code=400,
detail=f"Invalid AI provider: '{ai_provider}'. Must be claude, gemini, or cursor.",
)
Also update provider dropdowns in templates (dashboard.html, status.html) and relevant tests.
5) Configuration knobs for extension work
Runtime settings come from .env (see .env.example) via src/docsfy/config.py.
ADMIN_KEY=your-secure-admin-key-here-min-16-chars
AI_PROVIDER=claude
AI_MODEL=claude-opus-4-6[1m]
AI_CLI_TIMEOUT=60
LOG_LEVEL=INFO
# SECURE_COOKIES=false
config.py defaults:
admin_key: str = ""
ai_provider: str = "claude"
ai_model: str = "claude-opus-4-6[1m]"
ai_cli_timeout: int = Field(default=60, gt=0)
log_level: str = "INFO"
data_dir: str = "/data"
secure_cookies: bool = True
App run-time host/port/debug toggles:
reload = os.getenv("DEBUG", "").lower() == "true"
host = os.getenv("HOST", "127.0.0.1")
port = int(os.getenv("PORT", "8000"))
uvicorn.run("docsfy.main:app", host=host, port=port, reload=reload)
Container/dev config (docker-compose.yaml):
services:
docsfy:
build: .
ports:
- "8000:8000"
env_file: .env
volumes:
- ./data:/data
Tip: For local HTTP-only development, set
SECURE_COOKIES=falseso session cookies are accepted without TLS.
6) Tests and CI/CD status when extending
The repo has strong unit/integration coverage for prompt building, generation, rendering, auth, and storage. Pytest is configured in pyproject.toml:
[tool.pytest.ini_options]
asyncio_mode = "auto"
testpaths = ["tests"]
pythonpath = ["src"]
Warning: There are currently no CI/CD workflow files in this repository (
.github/workflowsand.gitlab-ci*are absent). Run tests locally after extension changes:
pytest- Focused suites like
pytest tests/test_generator.py tests/test_renderer.py tests/test_main.pyfor generation/rendering changes