Extending docsfy

docsfy has four main extension surfaces:

  1. Prompt construction (src/docsfy/prompts.py)
  2. HTML rendering and template selection (src/docsfy/renderer.py, src/docsfy/templates/)
  3. Frontend behavior and styling (src/docsfy/static/ plus shared template partials)
  4. 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 to assets/ - 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 expecting navigation/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() deletes output_dir before 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: toggles data-theme and persists localStorage["theme"]
  • search.js: loads search-index.json and provides Cmd/Ctrl+K modal search
  • codelabels.js: maps language-* classes to human labels
  • style.css: centralized design tokens (:root and [data-theme="dark"])

Tip: To add a new docs-page behavior, add a file under src/docsfy/static/, then include it in src/docsfy/templates/page.html and src/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=false so 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/workflows and .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.py for generation/rendering changes