diff --git a/Dockerfile b/Dockerfile
index a7931a588..c6c82756b 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,5 +1,5 @@
# building frontend
-FROM node:22-slim as frontend
+FROM node:22-slim AS frontend
WORKDIR /app/frontend
COPY web/package.json web/yarn.lock ./
@@ -40,8 +40,8 @@ FROM python:3.12-slim
ENV MAX_UPLOAD_SIZE=100M
# set up the system
-RUN apt update && \
- apt install --yes nginx dumb-init libmagic1 gettext && \
+RUN apt-get update && \
+ apt-get install --yes nginx dumb-init libmagic1 gettext && \
rm -rf /var/lib/apt/lists/*
RUN mkdir -p /var/docat/doc
diff --git a/docat/docat/nginx/default b/docat/docat/nginx/default
index 52b354460..655d683da 100644
--- a/docat/docat/nginx/default
+++ b/docat/docat/nginx/default
@@ -15,6 +15,7 @@ server {
location /doc {
root /var/docat;
+ absolute_redirect off;
}
location /api {
@@ -23,6 +24,6 @@ server {
}
location / {
- try_files $uri $uri/ =404;
+ try_files $uri $uri/ /index.html =404;
}
}
diff --git a/docat/docat/utils.py b/docat/docat/utils.py
index 26adf2aef..49818f813 100644
--- a/docat/docat/utils.py
+++ b/docat/docat/utils.py
@@ -126,7 +126,7 @@ def is_forbidden_project_name(name: str) -> bool:
a page on the docat website.
"""
name = name.lower().strip()
- return name in ["upload", "claim", "delete", "help"]
+ return name in ["upload", "claim", "delete", "help", "doc", "api"]
UNITS_MAPPING = [
diff --git a/docat/tests/test_rename.py b/docat/tests/test_rename.py
index 99272f593..f8b6b2763 100644
--- a/docat/tests/test_rename.py
+++ b/docat/tests/test_rename.py
@@ -86,7 +86,7 @@ def test_rename_rejects_forbidden_project_name(client_with_claimed_project):
assert create_response.status_code == 201
with patch("os.rename") as rename_mock:
- for project_name in ["upload", "claim", "Delete ", "help"]:
+ for project_name in ["upload", "claim", "Delete ", "help", "Doc", "API"]:
rename_response = client_with_claimed_project.put(f"/api/some-project/rename/{project_name}", headers={"Docat-Api-Key": "1234"})
assert rename_response.status_code == 400
assert rename_response.json() == {
diff --git a/docat/tests/test_upload.py b/docat/tests/test_upload.py
index 2bd115e33..77c75b4b2 100644
--- a/docat/tests/test_upload.py
+++ b/docat/tests/test_upload.py
@@ -160,7 +160,7 @@ def test_upload_rejects_forbidden_project_name(client_with_claimed_project):
"""
with patch("docat.app.remove_docs") as remove_mock:
- for project_name in ["upload", "claim", " Delete ", "help"]:
+ for project_name in ["upload", "claim", " Delete ", "help", "DOC", "api"]:
response = client_with_claimed_project.post(
f"/api/{project_name}/1.0.0", files={"file": ("index.html", io.BytesIO(b"
Hello World
"), "plain/text")}
)
diff --git a/web/src/App.tsx b/web/src/App.tsx
index 823667410..b65a577d5 100644
--- a/web/src/App.tsx
+++ b/web/src/App.tsx
@@ -1,4 +1,4 @@
-import { createHashRouter, RouterProvider } from 'react-router-dom'
+import { createBrowserRouter, RouterProvider } from 'react-router-dom'
import { ConfigDataProvider } from './data-providers/ConfigDataProvider'
import { MessageBannerProvider } from './data-providers/MessageBannerProvider'
import { ProjectDataProvider } from './data-providers/ProjectDataProvider'
@@ -7,14 +7,13 @@ import { StatsDataProvider } from './data-providers/StatsDataProvider'
import Claim from './pages/Claim'
import Delete from './pages/Delete'
import Docs from './pages/Docs'
-import EscapeSlashForDocsPath from './pages/EscapeSlashForDocsPath'
import Help from './pages/Help'
import Home from './pages/Home'
import NotFound from './pages/NotFound'
import Upload from './pages/Upload'
function App(): JSX.Element {
- const router = createHashRouter([
+ const router = createBrowserRouter([
{
path: '/',
errorElement: ,
@@ -54,17 +53,8 @@ function App(): JSX.Element {
element:
},
{
- path: ':page',
- children: [
- {
- path: '',
- element:
- },
- {
- path: '*',
- element:
- }
- ]
+ path: '*',
+ element:
}
]
}
diff --git a/web/src/components/DocumentControlButtons.tsx b/web/src/components/DocumentControlButtons.tsx
index 214e363f1..22c1ef9a5 100644
--- a/web/src/components/DocumentControlButtons.tsx
+++ b/web/src/components/DocumentControlButtons.tsx
@@ -41,8 +41,10 @@ export default function DocumentControlButtons(props: Props): JSX.Element {
url = url.replace(props.version, 'latest')
}
- if (shareModalHideUi && !url.includes('?hide-ui=true')) {
- url = `${url}?hide-ui=true`
+ if (shareModalHideUi) {
+ const urlObject = new URL(url)
+ urlObject.search = 'hide-ui'
+ url = urlObject.toString()
}
return url
diff --git a/web/src/pages/Docs.tsx b/web/src/pages/Docs.tsx
index d6abbb639..a25ca7125 100644
--- a/web/src/pages/Docs.tsx
+++ b/web/src/pages/Docs.tsx
@@ -1,4 +1,4 @@
-import React, { useEffect, useMemo, useState, useRef } from 'react'
+import { useEffect, useMemo, useState, useRef } from 'react'
import ProjectRepository from '../repositories/ProjectRepository'
import type ProjectDetails from '../models/ProjectDetails'
import LoadingPage from './LoadingPage'
@@ -11,7 +11,7 @@ import IframeNotFound from './IframePageNotFound'
export default function Docs(): JSX.Element {
const params = useParams()
- const searchParams = useSearchParams()[0]
+ const [searchParams] = useSearchParams()
const location = useLocation()
const { showMessage, clearMessages } = useMessageBanner()
@@ -20,14 +20,11 @@ export default function Docs(): JSX.Element {
const [loadingFailed, setLoadingFailed] = useState(false)
const project = useRef(params.project ?? '')
- const page = useRef(params.page ?? 'index.html')
- const hash = useRef(location.hash.split('?')[0] ?? '')
+ const page = useRef(params['*'] ?? '')
+ const hash = useRef(location.hash)
const [version, setVersion] = useState(params.version ?? 'latest')
- const [hideUi, setHideUi] = useState(
- searchParams.get('hide-ui') === 'true' ||
- location.hash.split('?')[1] === 'hide-ui=true'
- )
+ const [hideUi, setHideUi] = useState(searchParams.get('hide-ui') === '' || searchParams.get('hide-ui') === 'true')
const [iframeUpdateTrigger, setIframeUpdateTrigger] = useState(0)
// This provides the url for the iframe.
@@ -91,10 +88,9 @@ export default function Docs(): JSX.Element {
})()
}, [project])
- /** Encodes the url for the current page, and escapes the path part to avoid
- * redirecting to escapeSlashForDocsPath.
+ /** Encodes the url for the current page.
* @example
- * getUrl('project', 'version', 'path/to/page.html', '#hash', false) -> '#/project/version/path%2Fto%2Fpage.html#hash'
+ * getUrl('project', 'version', 'path/to/page.html', '#hash', false) -> '/project/version/path/to/page.html#hash'
*/
const getUrl = (
project: string,
@@ -103,7 +99,7 @@ export default function Docs(): JSX.Element {
hash: string,
hideUi: boolean
): string => {
- return `#/${project}/${version}/${encodeURIComponent(page)}${hash}${hideUi ? '?hide-ui=true' : ''}`
+ return `/${project}/${version}/${encodeURI(page)}${hash}${hideUi ? '?hide-ui' : ''}`
}
const updateUrl = (newVersion: string, hideUi: boolean): void => {
@@ -121,6 +117,11 @@ export default function Docs(): JSX.Element {
document.title = newTitle
}
+ // Keep compatibility with encoded page path
+ useEffect(() => {
+ updateUrl(version, hideUi)
+ }, [])
+
const iFramePageChanged = (urlPage: string, urlHash: string, title?: string): void => {
if (title != null && title !== document.title) {
updateTitle(title)
@@ -156,11 +157,9 @@ export default function Docs(): JSX.Element {
useEffect(() => {
const urlProject = params.project ?? ''
const urlVersion = params.version ?? 'latest'
- const urlPage = params.page ?? 'index.html'
- const urlHash = location.hash.split('?')[0] ?? ''
- const urlHideUi =
- searchParams.get('hide-ui') === 'true' ||
- location.hash.split('?')[1] === 'hide-ui=true'
+ const urlPage = params['*'] ?? ''
+ const urlHash = location.hash
+ const urlHideUi = searchParams.get('hide-ui') === '' || searchParams.get('hide-ui') === 'true'
// update the state to the url params on first loadon
if (urlProject !== project.current) {
diff --git a/web/src/pages/EscapeSlashForDocsPath.tsx b/web/src/pages/EscapeSlashForDocsPath.tsx
deleted file mode 100644
index 401f73839..000000000
--- a/web/src/pages/EscapeSlashForDocsPath.tsx
+++ /dev/null
@@ -1,22 +0,0 @@
-import { Navigate, useLocation } from 'react-router-dom'
-import ProjectRepository from '../repositories/ProjectRepository'
-import React from 'react'
-
-/**
- * This component is used to escape slashes in the path of the docs page.
- * It replaces all slashes with "%2F" and redirects to the new path.
- * @returns
- */
-export default function EscapeSlashForDocsPath(): JSX.Element {
- const location = useLocation()
- return (
-
- )
-}
diff --git a/web/src/pages/Home.tsx b/web/src/pages/Home.tsx
index 0fd9bd454..4a56fd44c 100644
--- a/web/src/pages/Home.tsx
+++ b/web/src/pages/Home.tsx
@@ -1,7 +1,7 @@
import { useEffect, useState } from 'react';
+import { useNavigate } from 'react-router';
import { Delete, ErrorOutline, FileUpload, KeyboardArrowDown, Lock } from '@mui/icons-material';
-import { useLocation } from 'react-router';
import { useProjects } from '../data-providers/ProjectDataProvider';
import { useSearch } from '../data-providers/SearchProvider';
import { type Project } from '../models/ProjectsResponse';
@@ -20,26 +20,19 @@ import styles from './../style/pages/Home.module.css';
export default function Home(): JSX.Element {
+ const navigate = useNavigate()
const { loadingFailed } = useProjects()
const { stats, loadingFailed: statsLoadingFailed } = useStats()
- const { filteredProjects: projects, query, setQuery } = useSearch()
+ const { filteredProjects: projects, query } = useSearch()
const [showAll, setShowAll] = useState(false);
const [favoriteProjects, setFavoriteProjects] = useState([])
- const location = useLocation()
-
document.title = 'Home | docat'
- // insert # into the url if it's missing
- useEffect(() => {
- const nonHostPart = window.location.href.replace(window.location.origin, '')
-
- if (nonHostPart.startsWith('#') || nonHostPart.startsWith('/#')) {
- return
- }
-
- window.location.replace(`/#${nonHostPart}`)
- }, [location, setQuery, projects])
+ // Keep compatibility with hash-based URI
+ if (location.hash.startsWith('#/')) {
+ navigate(location.hash.replace('#', ''), { replace: true })
+ }
const updateFavorites = (): void => {
if (projects == null) return
diff --git a/web/src/pages/IframePageNotFound.tsx b/web/src/pages/IframePageNotFound.tsx
index df5c05d50..4ea34be46 100644
--- a/web/src/pages/IframePageNotFound.tsx
+++ b/web/src/pages/IframePageNotFound.tsx
@@ -11,7 +11,7 @@ interface Props {
}
export default function IframeNotFound(props: Props): JSX.Element {
- const link = `/${props.project}/${props.version}${props.hideUi ? '?hide-ui=true' : ''}`
+ const link = `/${props.project}/${props.version}${props.hideUi ? '?hide-ui' : ''}`
return (
diff --git a/web/src/repositories/ProjectRepository.ts b/web/src/repositories/ProjectRepository.ts
index d12776525..edd74f069 100644
--- a/web/src/repositories/ProjectRepository.ts
+++ b/web/src/repositories/ProjectRepository.ts
@@ -4,34 +4,6 @@ import { type Project } from '../models/ProjectsResponse'
const RESOURCE = 'doc'
-/**
- * Escapes all slashes in a url to the docs page from the point between the version and the path.
- * This is necessary because react-router thinks that the slashes are path separators.
- * The slashes are escaped to %2F and reverted back to slashes by react-router.
- * Example:
- * /doc/project/1.0.0/path/to/page -> /doc/project/1.0.0/path%2Fto%2Fpage
- * @param pathname useLocation().pathname
- * @param search useLocation().search
- * @param hash useLocation().hash
- * @returns a url with escaped slashes
- */
-function escapeSlashesInUrl(
- pathname: string,
- search: string,
- hash: string
-): string {
- const url = pathname + hash + search
- const projectAndVersion = url.split('/', 3).join('/')
- let path = url.substring(projectAndVersion.length + 1)
- path = path.replaceAll('/', '%2F')
-
- if (path.length === 0) {
- return projectAndVersion
- }
-
- return projectAndVersion + '/' + path
-}
-
function dateTimeReviver(key: string, value: any) {
if (key === 'timestamp') {
return new Date(value)
@@ -273,7 +245,6 @@ function setFavorite(projectName: string, shouldBeFavorite: boolean): void {
}
const exp = {
- escapeSlashesInUrl,
getVersions,
getLatestVersion,
filterHiddenVersions,
diff --git a/web/src/tests/repositories/ProjectRepository.test.ts b/web/src/tests/repositories/ProjectRepository.test.ts
index 41a5f909e..fbcf27cb2 100644
--- a/web/src/tests/repositories/ProjectRepository.test.ts
+++ b/web/src/tests/repositories/ProjectRepository.test.ts
@@ -473,54 +473,3 @@ describe('getLatestVersion', () => {
expect(latestVersion).toStrictEqual(versions[0])
})
})
-
-describe('escapeSlashesInUrl', () => {
- test('should ignore version and project name', () => {
- const url = '/project/1.0.0'
-
- expect(ProjectRepository.escapeSlashesInUrl(url, '', '')).toBe(url)
- })
-
- test('should ignore trailing slash', () => {
- const given = '/project/1.0.0/'
- const expected = '/project/1.0.0'
-
- expect(ProjectRepository.escapeSlashesInUrl(given, '', '')).toBe(expected)
- })
-
- test('should escape slashes in path', () => {
- const given = '/project/1.0.0/path/with/slashes'
- const expected = '/project/1.0.0/path%2Fwith%2Fslashes'
-
- expect(ProjectRepository.escapeSlashesInUrl(given, '', '')).toBe(expected)
- })
-
- test('should work with query parameters', () => {
- const given = '/project/1.0.0/path/with/slashes'
- const query = '?param=value'
- const expected = '/project/1.0.0/path%2Fwith%2Fslashes?param=value'
-
- expect(ProjectRepository.escapeSlashesInUrl(given, query, '')).toBe(
- expected
- )
- })
-
- test('should work with hash', () => {
- const given = '/project/1.0.0/path/with/slashes'
- const hash = '#hash'
- const expected = '/project/1.0.0/path%2Fwith%2Fslashes#hash'
-
- expect(ProjectRepository.escapeSlashesInUrl(given, '', hash)).toBe(expected)
- })
-
- test('should work with query parameters and hash', () => {
- const given = '/project/1.0.0/path/with/slashes'
- const query = '?param=value'
- const hash = '#hash'
- const expected = '/project/1.0.0/path%2Fwith%2Fslashes#hash?param=value'
-
- expect(ProjectRepository.escapeSlashesInUrl(given, query, hash)).toBe(
- expected
- )
- })
-})