diff --git a/build/prepare-sites-build.mjs b/build/prepare-sites-build.mjs index 8d87703..d842cd8 100644 --- a/build/prepare-sites-build.mjs +++ b/build/prepare-sites-build.mjs @@ -1,5 +1,5 @@ import { copyFile, mkdir } from 'node:fs/promises'; -import { resolve } from 'node:path'; +import { dirname, resolve } from 'node:path'; const root = process.cwd(); const outputDirectory = resolve(root, 'dist'); @@ -20,13 +20,17 @@ await copyFile( resolve(serverDirectory, 'index.js'), ); -// Emit an extensionless HTML match for every client-side route. This keeps -// browser refreshes working even when the asset layer handles navigations -// before the Worker gets a chance to return the SPA fallback. +// Emit both clean-URL and directory-index shells for every client-side route. +// The directory copy also supports basic Nginx static configurations that do +// not rewrite missing paths to the root application shell. await Promise.all( browserRoutes.map(async (route) => { const routeFile = resolve(outputDirectory, `${route}.html`); - await mkdir(resolve(routeFile, '..'), { recursive: true }); + const routeDirectory = resolve(outputDirectory, route); + + await mkdir(dirname(routeFile), { recursive: true }); + await mkdir(routeDirectory, { recursive: true }); await copyFile(appShell, routeFile); + await copyFile(appShell, resolve(routeDirectory, 'index.html')); }), ); diff --git a/nginx.conf b/nginx.conf new file mode 100644 index 0000000..6ed80b2 --- /dev/null +++ b/nginx.conf @@ -0,0 +1,18 @@ +server { + listen 80; + listen [::]:80; + server_name _; + + root /usr/share/nginx/html; + index index.html; + + location /assets/ { + try_files $uri =404; + expires 1y; + add_header Cache-Control "public, immutable"; + } + + location / { + try_files $uri $uri/ /index.html; + } +} diff --git a/src/generated/protoDocs.ts b/src/generated/protoDocs.ts index 4d8d987..4a7c5cf 100644 --- a/src/generated/protoDocs.ts +++ b/src/generated/protoDocs.ts @@ -2402,101 +2402,101 @@ export const protoLayers = [ { "id": 0, "name": "IMAGE", - "description": "" + "description": "Изображение" }, { "id": 1, "name": "MESSAGES", - "description": "" + "description": "Сообщения (forwarded/reply)" }, { "id": 2, "name": "FILE", - "description": "" + "description": "Файл" }, { "id": 4, "name": "CALL", - "description": "" + "description": "Звонок" }, { "id": 5, "name": "VOICE", - "description": "" + "description": "Голосовое сообщение" }, { "id": 6, "name": "VIDEO_CIRCLE", - "description": "" + "description": "Видео сообщение (кружок)" }, { "id": 7, "name": "VIDEO", - "description": "" + "description": "Видео" } ] }, { "name": "DeviceSolution", "type": "int8", - "description": "", + "description": "Решение по девайсу, принять или отклонить вход", "yaml": "DeviceSolution:\n type: int8\n values:\n - id: 0\n name: ACCEPT\n - id: 1\n name: DECLINE", "values": [ { "id": 0, "name": "ACCEPT", - "description": "" + "description": "Принять" }, { "id": 1, "name": "DECLINE", - "description": "" + "description": "Отклонить" } ] }, { "name": "HandshakeStage", "type": "int8", - "description": "", + "description": "Статус хэндшейка", "yaml": "HandshakeStage:\n type: int8\n values:\n - id: 0\n name: COMPLETED\n - id: 1\n name: NEED_DEVICE_VERIFICATION", "values": [ { "id": 0, "name": "COMPLETED", - "description": "" + "description": "Пройден" }, { "id": 1, "name": "NEED_DEVICE_VERIFICATION", - "description": "" + "description": "Не пройден (приложение рендерит UI подтвердите с другого устройства)" } ] }, { "name": "GroupStatus", "type": "int8", - "description": "", + "description": "Статус в группе", "yaml": "GroupStatus:\n type: int8\n values:\n - id: 0\n name: JOINED\n - id: 1\n name: INVALID\n - id: 2\n name: NOT_JOINED\n - id: 3\n name: BANNED", "values": [ { "id": 0, "name": "JOINED", - "description": "" + "description": "В группе" }, { "id": 1, "name": "INVALID", - "description": "" + "description": "Не валидная группа" }, { "id": 2, "name": "NOT_JOINED", - "description": "" + "description": "Можно зайт" }, { "id": 3, "name": "BANNED", - "description": "" + "description": "Заблокирован" } ] }, @@ -2615,23 +2615,23 @@ export const protoLayers = [ { "name": "WebRTCType", "type": "int8", - "description": "", + "description": "Тип WebRTC сигнала", "yaml": "WebRTCType:\n type: int8\n values:\n - id: 0\n name: OFFER\n - id: 1\n name: ANSWER\n - id: 2\n name: ICE_CANDIDATE", "values": [ { "id": 0, "name": "OFFER", - "description": "" + "description": "Offer" }, { "id": 1, "name": "ANSWER", - "description": "" + "description": "Answer" }, { "id": 2, "name": "ICE_CANDIDATE", - "description": "" + "description": "Estabilishment candidate" } ] }, @@ -2654,22 +2654,22 @@ export const protoLayers = [ { "id": 2, "name": "INVALID", - "description": "любая ошибка смысл которой зависит от контекста, например в случае с файловой системой - нет файла" + "description": "Любая ошибка смысл которой зависит от контекста, например в случае с файловой системой - нет файла" }, { "id": 3, "name": "USERNAME_TAKEN", - "description": "" + "description": "Имя пользователя занято" }, { "id": 4, "name": "OVERSIZE", - "description": "слишком большой размер файла" + "description": "Слишком большой размер файла" }, { "id": 5, "name": "OVERLIFE", - "description": "слишком большой TTL" + "description": "Слишком большой TTL" } ] }, @@ -2716,35 +2716,35 @@ export const protoLayers = [ "name": "id", "type": "string", "optional": false, - "description": "" + "description": "Случайная строка" }, { "id": 2, "name": "blob", "type": "string", "optional": true, - "description": "" + "description": "Только очень коротких типов" }, { "id": 3, "name": "type", "type": "AttachmentType", "optional": false, - "description": "" + "description": "Тип вложения" }, { "id": 4, "name": "preview", "type": "string", "optional": false, - "description": "" + "description": "Превью" }, { "id": 6, "name": "objectId", "type": "string", "optional": false, - "description": "" + "description": "Выданный сервером objectId от cdn" } ] },