diff --git a/.env.example b/.env.example
index 3b81bf3..078c1a8 100644
--- a/.env.example
+++ b/.env.example
@@ -7,4 +7,4 @@ MATRIX_OIDC_CLIENT_ID=replace-with-your-registered-client-id
# MATRIX_OIDC_CLIENT_SECRET=only-if-the-registered-client-has-one
MATRIX_OIDC_REDIRECT_URI=https://your-public-host/api/auth/matrix/callback
SESSION_COOKIE_SECURE=false # set to true when using HTTPS in production
-VITE_DOCS_URL=docs_url
\ No newline at end of file
+VITE_DOCS_URL=http://127.0.0.1:8001
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index 80d2aaa..65d6813 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -37,7 +37,7 @@ jobs:
- name: Install dependencies
run: |
python -m pip install --upgrade pip
- python -m pip install -e ".[dev]"
+ python -m pip install -e ".[dev,docs]"
- name: Check formatting
run: black --check app db tests
@@ -48,6 +48,9 @@ jobs:
- name: Run tests
run: pytest -q
+ - name: Build documentation
+ run: cd ../docs && mkdocs build --strict
+
frontend:
name: Frontend quality
runs-on: ubuntu-latest
@@ -74,4 +77,4 @@ jobs:
run: npm run lint
- name: Type-check and build
- run: npm run build
\ No newline at end of file
+ run: npm run build
diff --git a/.gitignore b/.gitignore
index 7d1318a..ea8a673 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,6 +8,7 @@ __pycache__/
coverage.xml
htmlcov/
backend/matrix_directory.db
+backend/static/
frontend/node_modules/
frontend/dist/
frontend/.env.*
@@ -18,4 +19,4 @@ frontend/vite.config.d.ts
.vscode
matrix_directory_backend.egg-info
.mypy_cache
-docs/site
\ No newline at end of file
+docs/site/
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 5e2dbe4..a0ecd2d 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -35,7 +35,7 @@ Docker with Docker Compose is required.
The services will be available at:
- Frontend: http://localhost:5173
-- API documentation: http://localhost:8000/docs
+- Swagger UI: http://localhost:8000/api/docs
To stop the environment:
diff --git a/README.md b/README.md
index 781d5fe..b50a80e 100644
--- a/README.md
+++ b/README.md
@@ -52,7 +52,7 @@ docker compose up --build
The services will be available at:
- [Frontend](http://localhost:5173)
-- [API documentation](http://localhost:8000/docs)
+- [Swagger UI](http://localhost:8000/api/docs)
The backend automatically applies database migrations on startup.
@@ -89,7 +89,7 @@ See [Authentication Architecture](docs/docs/architecture/authentication.md) for
Interactive API documentation is available at:
-- [Swagger UI](http://localhost:8000/docs)
+- [Swagger UI](http://localhost:8000/api/docs)
Core endpoints include:
diff --git a/backend/app/main.py b/backend/app/main.py
index 94cf7b2..3134189 100644
--- a/backend/app/main.py
+++ b/backend/app/main.py
@@ -12,7 +12,13 @@
from app.config import get_settings
settings = get_settings()
-app = FastAPI(title=settings.app_name, version="0.1.0")
+app = FastAPI(
+ title=settings.app_name,
+ version="0.1.0",
+ docs_url="/api/docs",
+ redoc_url="/api/redoc",
+ openapi_url="/api/openapi.json",
+)
app.add_middleware(
SessionMiddleware,
@@ -44,28 +50,40 @@ def health() -> dict[str, str]:
return {"status": "ok"}
-frontend_directory = Path(__file__).resolve().parent.parent / "static"
-frontend_index = frontend_directory / "index.html"
-frontend_assets = frontend_directory / "assets"
+def mount_static_sites(application: FastAPI, static_directory: Path) -> None:
+ """Mount the built documentation and frontend when their files exist."""
+ documentation_directory = static_directory / "docs"
+ frontend_index = static_directory / "index.html"
+ frontend_assets = static_directory / "assets"
+
+ if documentation_directory.is_dir():
+ application.mount(
+ "/docs",
+ StaticFiles(directory=documentation_directory, html=True),
+ name="documentation",
+ )
+
+ if not frontend_index.is_file():
+ return
-if frontend_index.is_file():
if frontend_assets.is_dir():
- app.mount(
+ application.mount(
"/assets",
StaticFiles(directory=frontend_assets),
name="frontend-assets",
)
- @app.get("/{path:path}", include_in_schema=False)
+ @application.get("/{path:path}", include_in_schema=False)
def frontend(path: str) -> FileResponse:
- if path == "api" or path.startswith("api/"):
+ reserved_path = path in {"api", "docs"} or path.startswith(("api/", "docs/"))
+ if reserved_path:
raise HTTPException(status_code=404, detail="Not Found")
- requested_file = (frontend_directory / path).resolve()
- if (
- requested_file.is_relative_to(frontend_directory)
- and requested_file.is_file()
- ):
+ requested_file = (static_directory / path).resolve()
+ if requested_file.is_relative_to(static_directory) and requested_file.is_file():
return FileResponse(requested_file)
return FileResponse(frontend_index)
+
+
+mount_static_sites(app, Path(__file__).resolve().parent.parent / "static")
diff --git a/backend/tests/test_static_sites.py b/backend/tests/test_static_sites.py
new file mode 100644
index 0000000..4e87c6e
--- /dev/null
+++ b/backend/tests/test_static_sites.py
@@ -0,0 +1,73 @@
+from pathlib import Path
+
+from fastapi import FastAPI
+from fastapi.testclient import TestClient
+
+from app.main import app, mount_static_sites
+
+
+def test_api_documentation_routes__expect_swagger_and_schema() -> None:
+ client = TestClient(app)
+
+ swagger_response = client.get("/api/docs")
+ schema_response = client.get("/api/openapi.json")
+
+ assert swagger_response.status_code == 200
+ assert "swagger-ui" in swagger_response.text
+ assert schema_response.status_code == 200
+ assert schema_response.json()["info"]["title"] == "Matrix Directory API"
+
+
+def test_documentation_static_files__expect_served_under_docs(
+ tmp_path: Path,
+) -> None:
+ static_directory = tmp_path / "static"
+ documentation_directory = static_directory / "docs"
+ guide_directory = documentation_directory / "guide"
+ guide_directory.mkdir(parents=True)
+ (documentation_directory / "index.html").write_text(
+ "
Documentation home
",
+ encoding="utf-8",
+ )
+ (guide_directory / "index.html").write_text(
+ "Documentation guide
",
+ encoding="utf-8",
+ )
+
+ application = FastAPI()
+ mount_static_sites(application, static_directory)
+ client = TestClient(application)
+
+ home_response = client.get("/docs/")
+ guide_response = client.get("/docs/guide/")
+
+ assert home_response.status_code == 200
+ assert "Documentation home" in home_response.text
+ assert guide_response.status_code == 200
+ assert "Documentation guide" in guide_response.text
+
+
+def test_reserved_routes__expect_not_fall_through_to_frontend(
+ tmp_path: Path,
+) -> None:
+ static_directory = tmp_path / "static"
+ static_directory.mkdir()
+ (static_directory / "index.html").write_text(
+ "Frontend application
",
+ encoding="utf-8",
+ )
+
+ application = FastAPI()
+ mount_static_sites(application, static_directory)
+ client = TestClient(application)
+
+ api_response = client.get("/api/not-a-real-route")
+ docs_response = client.get("/docs/not-a-real-page")
+ frontend_response = client.get("/bots/example")
+
+ assert api_response.status_code == 404
+ assert "Frontend application" not in api_response.text
+ assert docs_response.status_code == 404
+ assert "Frontend application" not in docs_response.text
+ assert frontend_response.status_code == 200
+ assert "Frontend application" in frontend_response.text
diff --git a/docs/.dockerignore b/docs/.dockerignore
deleted file mode 100644
index 98e8236..0000000
--- a/docs/.dockerignore
+++ /dev/null
@@ -1,3 +0,0 @@
-site/
-__pycache__/
-*.py[cod]
diff --git a/docs/Dockerfile b/docs/Dockerfile
deleted file mode 100644
index 3ae7183..0000000
--- a/docs/Dockerfile
+++ /dev/null
@@ -1,19 +0,0 @@
-FROM python:3.13-slim AS builder
-
-WORKDIR /docs
-
-COPY requirements.txt .
-RUN python -m pip install --no-cache-dir -r requirements.txt
-
-COPY . .
-RUN mkdocs build --strict
-
-
-FROM nginx:alpine AS runtime
-
-COPY nginx.conf.template /etc/nginx/templates/default.conf.template
-COPY --from=builder /docs/site /usr/share/nginx/html
-
-ENV PORT=8080
-
-EXPOSE 8080
diff --git a/docs/docs/development.md b/docs/docs/development.md
index 162af6f..89ca35e 100644
--- a/docs/docs/development.md
+++ b/docs/docs/development.md
@@ -41,7 +41,7 @@ services are ready, open:
| Service | Address |
| --- | --- |
| Frontend | |
-| API documentation | |
+| Swagger UI | |
You now have the frontend, backend, and database running together. The frontend
reloads as its source changes; restart the API service after backend changes.
diff --git a/docs/docs/index.md b/docs/docs/index.md
index 0b8655c..d835cd5 100644
--- a/docs/docs/index.md
+++ b/docs/docs/index.md
@@ -93,7 +93,7 @@ profiles, project ownership, and directory data.
Once the services are ready, open:
- [Matrix Directory](http://localhost:5173)
- - [Interactive API documentation](http://localhost:8000/docs)
+ - [Interactive API documentation](http://localhost:8000/api/docs)
=== "Stop or reset"
diff --git a/docs/docs/reference/api.md b/docs/docs/reference/api.md
index a5d59be..6d3ac60 100644
--- a/docs/docs/reference/api.md
+++ b/docs/docs/reference/api.md
@@ -3,7 +3,7 @@
The FastAPI backend exposes JSON application endpoints under `/api`. This
page describes the stable behaviors a client needs; the running application
provides the complete generated schemas in its
-[Swagger UI](https://matrix-directory.codesociety.xyz/docs).
+[Swagger UI](https://matrix-directory.codesociety.xyz/api/docs).
## At a glance
@@ -217,4 +217,4 @@ Changing `matrix_id` clears any existing verification. Clients cannot set
- [Development guide](../development.md)
- [Authentication architecture](../architecture/authentication.md)
-- [Interactive API documentation](https://matrix-directory.codesociety.xyz/docs)
+- [Interactive API documentation](https://matrix-directory.codesociety.xyz/api/docs)
diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml
index 63e5827..8708c66 100644
--- a/docs/mkdocs.yml
+++ b/docs/mkdocs.yml
@@ -2,7 +2,7 @@
site_name: Matrix Directory
site_description: A community directory for projects in the Matrix ecosystem.
-site_url: https://docs.matrix-directory.codesociety.xyz/
+site_url: https://matrix-directory.codesociety.xyz/docs/
dev_addr: 127.0.0.1:8001
repo_url: https://github.com/Code-Society-Lab/matrix-directory
repo_name: Code-Society-Lab/matrix-directory
@@ -79,6 +79,7 @@ markdown_extensions:
emoji_generator: !!python/name:material.extensions.emoji.to_svg
- pymdownx.snippets:
base_path: [".."]
+
nav:
- Home: index.md
- Guides:
diff --git a/docs/nginx.conf.template b/docs/nginx.conf.template
deleted file mode 100644
index 74f0628..0000000
--- a/docs/nginx.conf.template
+++ /dev/null
@@ -1,19 +0,0 @@
-server {
- listen ${PORT};
- listen [::]:${PORT};
-
- server_name _;
-
- root /usr/share/nginx/html;
- index index.html;
-
- location / {
- try_files $uri $uri/ =404;
- }
-
- error_page 404 /404.html;
-
- location = /404.html {
- internal;
- }
-}
diff --git a/docs/requirements.txt b/docs/requirements.txt
deleted file mode 100644
index 65732f9..0000000
--- a/docs/requirements.txt
+++ /dev/null
@@ -1,2 +0,0 @@
-mkdocs==1.6.1
-mkdocs-material==9.7.6
diff --git a/frontend/src/components/SiteHeader.vue b/frontend/src/components/SiteHeader.vue
index 651cff6..4d25117 100644
--- a/frontend/src/components/SiteHeader.vue
+++ b/frontend/src/components/SiteHeader.vue
@@ -19,7 +19,8 @@ import logoUrl from '../assets/matrix-directory-mark.svg'
const docsUrl =
- import.meta.env.VITE_DOCS_URL ?? 'http://127.0.0.1:8001'
+ import.meta.env.VITE_DOCS_URL ??
+ (import.meta.env.DEV ? 'http://127.0.0.1:8001' : '/docs/')
const router = useRouter()
const route = useRoute()
diff --git a/railpack.json b/railpack.json
index f330474..8bfaf1d 100644
--- a/railpack.json
+++ b/railpack.json
@@ -16,7 +16,7 @@
{ "src": ".", "dest": "." },
"python3 -m venv .venv",
".venv/bin/pip install --upgrade pip",
- ".venv/bin/pip install ./backend",
+ ".venv/bin/pip install ./backend[docs]",
"cd frontend && npm ci"
]
},
@@ -26,12 +26,16 @@
{ "step": "install" }
],
"variables": {
- "VITE_API_URL": "/api"
+ "VITE_API_URL": "/api",
+ "VITE_DOCS_URL": "/docs/"
},
"commands": [
"cd frontend && npm run build",
+ "cd docs && ../.venv/bin/mkdocs build --strict",
"mkdir -p backend/static",
- "cp -R frontend/dist/. backend/static/"
+ "cp -R frontend/dist/. backend/static/",
+ "mkdir -p backend/static/docs",
+ "cp -R docs/site/. backend/static/docs/"
]
}
},