diff --git a/packages/techdocs-common/src/stages/publish/local.test.ts b/packages/techdocs-common/src/stages/publish/local.test.ts
index 9609e4f12d..4372dcdc1f 100644
--- a/packages/techdocs-common/src/stages/publish/local.test.ts
+++ b/packages/techdocs-common/src/stages/publish/local.test.ts
@@ -117,6 +117,13 @@ describe('local publisher', () => {
[resolvedDir]: {
'unsafe.html': '',
'unsafe.svg': '',
+ default: {
+ testkind: {
+ testname: {
+ 'index.html': 'found it',
+ },
+ },
+ },
},
});
});
@@ -138,5 +145,37 @@ describe('local publisher', () => {
'content-type': 'text/plain; charset=utf-8',
});
});
+
+ it('should redirect case-sensitive triplet path to lower-case', async () => {
+ await request(app)
+ .get('/default/TestKind/TestName/index.html')
+ .expect(301)
+ .expect('Location', '/default/testkind/testname/index.html');
+ });
+
+ it('should resolve lower-case triplet path content eventually', async () => {
+ const response = await request(app)
+ .get('/default/TestKind/TestName/index.html')
+ .redirects(1);
+ expect(response.text).toEqual('found it');
+ });
+
+ it('should not redirect when legacy case setting is used', async () => {
+ const legacyConfig = new ConfigReader({
+ techdocs: {
+ legacyUseCaseSensitiveTripletPaths: true,
+ },
+ });
+ const legacyPublisher = new LocalPublish(
+ legacyConfig,
+ logger,
+ testDiscovery,
+ );
+ app = express().use(legacyPublisher.docsRouter());
+
+ await request(app)
+ .get('/default/TestKind/TestName/index.html')
+ .expect(404);
+ });
});
});
diff --git a/packages/techdocs-common/src/stages/publish/local.ts b/packages/techdocs-common/src/stages/publish/local.ts
index b59bae88bb..c547b65406 100644
--- a/packages/techdocs-common/src/stages/publish/local.ts
+++ b/packages/techdocs-common/src/stages/publish/local.ts
@@ -142,16 +142,55 @@ export class LocalPublish implements PublisherBase {
}
docsRouter(): express.Handler {
- return express.static(staticDocsDir, {
- // Handle content-type header the same as all other publishers.
- setHeaders: (res, filePath) => {
- const fileExtension = path.extname(filePath);
- const headers = getHeadersForFileExtension(fileExtension);
- for (const [header, value] of Object.entries(headers)) {
- res.setHeader(header, value);
- }
- },
+ const router = express.Router();
+
+ // Redirect middleware ensuring that requests to case-sensitive entity
+ // triplet paths are always sent to lower-case versions.
+ router.use((req, res, next) => {
+ // If legacy path casing is on, let the request immediately continue.
+ if (this.legacyPathCasing) {
+ return next();
+ }
+
+ // Generate a lower-case entity triplet path.
+ const [_, namespace, kind, name, ...rest] = req.path.split('/');
+
+ // Ignore non-triplet objects.
+ if (!namespace || !kind || !name) {
+ return next();
+ }
+
+ const newPath = [
+ _,
+ namespace.toLowerCase(),
+ kind.toLowerCase(),
+ name.toLowerCase(),
+ ...rest,
+ ].join('/');
+
+ // If there was no change, then let express.static() handle the request.
+ if (newPath === req.path) {
+ return next();
+ }
+
+ // Otherwise, redirect to the new path.
+ return res.redirect(req.baseUrl + newPath, 301);
});
+
+ router.use(
+ express.static(staticDocsDir, {
+ // Handle content-type header the same as all other publishers.
+ setHeaders: (res, filePath) => {
+ const fileExtension = path.extname(filePath);
+ const headers = getHeadersForFileExtension(fileExtension);
+ for (const [header, value] of Object.entries(headers)) {
+ res.setHeader(header, value);
+ }
+ },
+ }),
+ );
+
+ return router;
}
async hasDocsBeenGenerated(entity: Entity): Promise {