feat: add BASE_PATH support for deployment and update environment variables (#165)
This commit is contained in:
parent
30ede17f80
commit
2fe35fafcc
13
.env.example
13
.env.example
@ -1,2 +1,13 @@
|
|||||||
|
# Environment variables for MiniQR
|
||||||
|
|
||||||
|
# Crowdin configuration for translations
|
||||||
CROWDIN_PERSONAL_TOKEN="your_personal_token"
|
CROWDIN_PERSONAL_TOKEN="your_personal_token"
|
||||||
CROWDIN_PROJECT_ID="your_project_id"
|
CROWDIN_PROJECT_ID="your_project_id"
|
||||||
|
|
||||||
|
# Base path for deployment (e.g., /mini-qr for deploying at domain.com/mini-qr)
|
||||||
|
# Default: /
|
||||||
|
BASE_PATH=/
|
||||||
|
|
||||||
|
# Hide credits in the footer
|
||||||
|
# Default: false
|
||||||
|
VITE_HIDE_CREDITS=false
|
18
Dockerfile
18
Dockerfile
@ -3,16 +3,22 @@
|
|||||||
# Build stage
|
# Build stage
|
||||||
FROM node:lts-alpine AS builder
|
FROM node:lts-alpine AS builder
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Accept BASE_PATH as build argument
|
||||||
|
ARG BASE_PATH=/
|
||||||
|
ENV BASE_PATH=${BASE_PATH}
|
||||||
|
|
||||||
COPY package*.json ./
|
COPY package*.json ./
|
||||||
RUN npm install --frozen-lockfile
|
RUN npm install --frozen-lockfile
|
||||||
COPY . .
|
COPY . .
|
||||||
RUN npm run build
|
RUN npm run build
|
||||||
|
|
||||||
# Production stage
|
# Production stage
|
||||||
FROM nginx:alpine AS production
|
FROM node:lts-alpine AS production
|
||||||
LABEL org.opencontainers.image.source https://github.com/lyqht/mini-qr
|
WORKDIR /app
|
||||||
COPY --from=builder /app/dist /usr/share/nginx/html
|
COPY --from=builder /app/dist ./dist
|
||||||
COPY --from=builder /app/public /usr/share/nginx/html/public
|
COPY --from=builder /app/public ./public
|
||||||
COPY --from=builder /app/nginx.conf /etc/nginx/nginx.conf
|
COPY --from=builder /app/package.json ./
|
||||||
|
RUN npm install -g serve
|
||||||
EXPOSE 8080
|
EXPOSE 8080
|
||||||
CMD ["nginx", "-g", "daemon off;"]
|
CMD ["serve", "-s", "dist", "-l", "8080"]
|
26
README.md
26
README.md
@ -142,6 +142,32 @@ docker run -d -p 8081:8080 mini-qr
|
|||||||
- The production image uses Nginx for optimal static file serving.
|
- The production image uses Nginx for optimal static file serving.
|
||||||
- The `.dockerignore` file is included for smaller, faster builds.
|
- The `.dockerignore` file is included for smaller, faster builds.
|
||||||
- Set `HIDE_CREDITS=1` to remove the maintainer credit from the footer.
|
- Set `HIDE_CREDITS=1` to remove the maintainer credit from the footer.
|
||||||
|
- Set `BASE_PATH=/your-path` to deploy the app under a subdirectory (e.g., for hosting at `domain.com/your-path`).
|
||||||
|
|
||||||
|
#### Environment Variables
|
||||||
|
|
||||||
|
| Variable | Description | Default |
|
||||||
|
|----------|-------------|---------|
|
||||||
|
| `HIDE_CREDITS` | Hide credits in the footer | `false` |
|
||||||
|
| `BASE_PATH` | Base path for deployment | `/` |
|
||||||
|
|
||||||
|
#### Examples
|
||||||
|
|
||||||
|
Deploy at root path (default):
|
||||||
|
```bash
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
Deploy at subdirectory `/mini-qr`:
|
||||||
|
```bash
|
||||||
|
BASE_PATH=/mini-qr docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
For custom builds with specific BASE_PATH:
|
||||||
|
```bash
|
||||||
|
docker build --build-arg BASE_PATH=/mini-qr -t mini-qr .
|
||||||
|
docker run -d -p 8081:8080 mini-qr
|
||||||
|
```
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
|
@ -1,17 +1,37 @@
|
|||||||
---
|
---
|
||||||
services:
|
services:
|
||||||
mini-qr:
|
mini-qr:
|
||||||
image: ghcr.io/lyqht/mini-qr:latest
|
# image: ghcr.io/lyqht/mini-qr:latest
|
||||||
container_name: mini-qr
|
container_name: mini-qr
|
||||||
ports:
|
|
||||||
- 8081:8080
|
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
environment:
|
environment:
|
||||||
- VITE_HIDE_CREDITS=${HIDE_CREDITS:-false}
|
- VITE_HIDE_CREDITS=${HIDE_CREDITS:-false}
|
||||||
|
- BASE_PATH=${BASE_PATH:-/}
|
||||||
# Uncomment the following lines to build locally instead of pulling from ghcr.io
|
# Uncomment the following lines to build locally instead of pulling from ghcr.io
|
||||||
# build:
|
build:
|
||||||
# context: .
|
context: .
|
||||||
# dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
|
args:
|
||||||
|
- BASE_PATH=${BASE_PATH:-/}
|
||||||
# volumes:
|
# volumes:
|
||||||
# - ./public:/usr/share/nginx/html/public:ro
|
# - ./public:/usr/share/nginx/html/public:ro
|
||||||
# - ./nginx.conf:/etc/nginx/nginx.conf:ro
|
# - ./nginx.conf:/etc/nginx/nginx.conf:ro
|
||||||
|
networks:
|
||||||
|
- mini-qr-network
|
||||||
|
|
||||||
|
nginx-proxy:
|
||||||
|
image: nginx:alpine
|
||||||
|
container_name: nginx-proxy
|
||||||
|
ports:
|
||||||
|
- 80:80
|
||||||
|
restart: unless-stopped
|
||||||
|
volumes:
|
||||||
|
- ./nginx-proxy.conf:/etc/nginx/nginx.conf:ro
|
||||||
|
depends_on:
|
||||||
|
- mini-qr
|
||||||
|
networks:
|
||||||
|
- mini-qr-network
|
||||||
|
|
||||||
|
networks:
|
||||||
|
mini-qr-network:
|
||||||
|
driver: bridge
|
||||||
|
1
env.d.ts
vendored
1
env.d.ts
vendored
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
interface ImportMetaEnv {
|
interface ImportMetaEnv {
|
||||||
readonly VITE_HIDE_CREDITS?: string
|
readonly VITE_HIDE_CREDITS?: string
|
||||||
|
readonly BASE_PATH?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ImportMeta {
|
interface ImportMeta {
|
||||||
|
68
nginx-proxy.conf
Normal file
68
nginx-proxy.conf
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
events {
|
||||||
|
worker_connections 1024;
|
||||||
|
}
|
||||||
|
|
||||||
|
http {
|
||||||
|
include /etc/nginx/mime.types;
|
||||||
|
default_type application/octet-stream;
|
||||||
|
sendfile on;
|
||||||
|
keepalive_timeout 65;
|
||||||
|
|
||||||
|
# Upstream for mini-qr service
|
||||||
|
upstream mini-qr-backend {
|
||||||
|
server mini-qr:8080;
|
||||||
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name localhost;
|
||||||
|
|
||||||
|
# Root path - redirect to mini-qr
|
||||||
|
location = / {
|
||||||
|
return 301 /mini-qr/;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Mini QR application with subpath
|
||||||
|
location /mini-qr/ {
|
||||||
|
# Remove /mini-qr prefix before forwarding to backend
|
||||||
|
rewrite ^/mini-qr/(.*) /$1 break;
|
||||||
|
|
||||||
|
proxy_pass http://mini-qr-backend;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_set_header X-Forwarded-Host $host;
|
||||||
|
proxy_set_header X-Forwarded-Port $server_port;
|
||||||
|
|
||||||
|
# Handle assets specifically
|
||||||
|
location ~* ^/mini-qr/assets/ {
|
||||||
|
rewrite ^/mini-qr/(.*) /$1 break;
|
||||||
|
proxy_pass http://mini-qr-backend;
|
||||||
|
expires 1y;
|
||||||
|
add_header Cache-Control "public, immutable";
|
||||||
|
}
|
||||||
|
|
||||||
|
# Handle static files
|
||||||
|
location ~* ^/mini-qr/.*\.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
|
||||||
|
rewrite ^/mini-qr/(.*) /$1 break;
|
||||||
|
proxy_pass http://mini-qr-backend;
|
||||||
|
expires 1y;
|
||||||
|
add_header Cache-Control "public, immutable";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Health check endpoint
|
||||||
|
location /health {
|
||||||
|
access_log off;
|
||||||
|
return 200 "healthy\n";
|
||||||
|
add_header Content-Type text/plain;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Error pages
|
||||||
|
error_page 500 502 503 504 /50x.html;
|
||||||
|
location = /50x.html {
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
31
nginx.conf
31
nginx.conf
@ -10,20 +10,45 @@ pid /var/run/nginx.pid;
|
|||||||
|
|
||||||
http {
|
http {
|
||||||
include /etc/nginx/mime.types;
|
include /etc/nginx/mime.types;
|
||||||
default_type application/octet-stream;
|
default_type application/octet-stream; sendfile on;
|
||||||
sendfile on;
|
|
||||||
keepalive_timeout 65;
|
keepalive_timeout 65;
|
||||||
server {
|
server {
|
||||||
listen 8080;
|
listen 8080;
|
||||||
server_name localhost;
|
server_name localhost;
|
||||||
root /usr/share/nginx/html;
|
root /usr/share/nginx/html;
|
||||||
index index.html;
|
index index.html;
|
||||||
|
|
||||||
|
# Handle JavaScript modules with correct MIME type for any base path
|
||||||
|
location ~* ^/.*/assets/.*\.(js|mjs)$ {
|
||||||
|
add_header Content-Type application/javascript;
|
||||||
|
try_files $uri =404;
|
||||||
|
expires 1y;
|
||||||
|
add_header Cache-Control "public, immutable";
|
||||||
|
}
|
||||||
|
|
||||||
|
# Handle CSS files with correct MIME type for any base path
|
||||||
|
location ~* ^/.*/assets/.*\.css$ {
|
||||||
|
add_header Content-Type text/css;
|
||||||
|
try_files $uri =404;
|
||||||
|
expires 1y;
|
||||||
|
add_header Cache-Control "public, immutable";
|
||||||
|
}
|
||||||
|
|
||||||
|
# Handle other static assets
|
||||||
|
location ~* \.(png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
|
||||||
|
try_files $uri =404;
|
||||||
|
expires 1y;
|
||||||
|
add_header Cache-Control "public, immutable";
|
||||||
|
}
|
||||||
|
|
||||||
location / {
|
location / {
|
||||||
try_files $uri $uri/ /index.html;
|
try_files $uri $uri/ /index.html;
|
||||||
}
|
}
|
||||||
|
|
||||||
location /public/ {
|
location /public/ {
|
||||||
alias /usr/share/nginx/html/public/;
|
alias /usr/share/nginx/html/public/;
|
||||||
}
|
}
|
||||||
|
|
||||||
error_page 500 502 503 504 /50x.html;
|
error_page 500 502 503 504 /50x.html;
|
||||||
location = /50x.html {
|
location = /50x.html {
|
||||||
root /usr/share/nginx/html;
|
root /usr/share/nginx/html;
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
import { marked } from 'marked'
|
import { marked } from 'marked'
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import { fetchWithBasePath } from '@/utils/basePath'
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
@ -17,15 +18,13 @@ const { t } = useI18n()
|
|||||||
const version = ref('...')
|
const version = ref('...')
|
||||||
const changelogContent = ref<string | null>(null)
|
const changelogContent = ref<string | null>(null)
|
||||||
const isLoading = ref(true)
|
const isLoading = ref(true)
|
||||||
const hideCredits = ['1', 'true'].includes(
|
const hideCredits = ['1', 'true'].includes((import.meta.env.VITE_HIDE_CREDITS ?? '').toLowerCase())
|
||||||
(import.meta.env.VITE_HIDE_CREDITS ?? '').toLowerCase()
|
|
||||||
)
|
|
||||||
|
|
||||||
async function fetchAndProcessChangelog() {
|
async function fetchAndProcessChangelog() {
|
||||||
if (changelogContent.value === null) {
|
if (changelogContent.value === null) {
|
||||||
isLoading.value = true
|
isLoading.value = true
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/CHANGELOG.md')
|
const response = await fetchWithBasePath('/CHANGELOG.md')
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`HTTP error! status: ${response.status}`)
|
throw new Error(`HTTP error! status: ${response.status}`)
|
||||||
}
|
}
|
||||||
@ -69,33 +68,33 @@ onMounted(() => {
|
|||||||
>
|
>
|
||||||
<span>|</span>
|
<span>|</span>
|
||||||
<Dialog>
|
<Dialog>
|
||||||
<DialogTrigger as-child>
|
<DialogTrigger as-child>
|
||||||
<button class="secondary-button" :aria-label="t('View changelog')" :disabled="isLoading">
|
<button class="secondary-button" :aria-label="t('View changelog')" :disabled="isLoading">
|
||||||
{{ isLoading ? '...' : version }}
|
{{ isLoading ? '...' : version }}
|
||||||
</button>
|
</button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent class="flex max-h-[80vh] flex-col sm:max-w-md" @open-auto-focus.prevent>
|
<DialogContent class="flex max-h-[80vh] flex-col sm:max-w-md" @open-auto-focus.prevent>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>{{ t('Changelog') }}</DialogTitle>
|
<DialogTitle>{{ t('Changelog') }}</DialogTitle>
|
||||||
<DialogClose
|
<DialogClose
|
||||||
class="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground"
|
class="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground"
|
||||||
>
|
>
|
||||||
<X class="size-4" />
|
<X class="size-4" />
|
||||||
<span class="sr-only">{{ t('Close') }}</span>
|
<span class="sr-only">{{ t('Close') }}</span>
|
||||||
</DialogClose>
|
</DialogClose>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
<div class="flex-1 overflow-y-auto pr-2">
|
<div class="flex-1 overflow-y-auto pr-2">
|
||||||
<DialogDescription
|
<DialogDescription
|
||||||
as="div"
|
as="div"
|
||||||
class="prose prose-sm max-w-none text-start dark:prose-invert prose-li:my-1"
|
class="prose prose-sm max-w-none text-start dark:prose-invert prose-li:my-1"
|
||||||
>
|
>
|
||||||
<div v-if="isLoading">Loading...</div>
|
<div v-if="isLoading">Loading...</div>
|
||||||
<div v-else-if="changelogContent" v-html="changelogContent"></div>
|
<div v-else-if="changelogContent" v-html="changelogContent"></div>
|
||||||
<div v-else>{{ t('Failed to load changelog') }}</div>
|
<div v-else>{{ t('Failed to load changelog') }}</div>
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</div>
|
</div>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
@ -4,6 +4,7 @@ import { useFloating, offset, flip, shift, autoUpdate } from '@floating-ui/vue'
|
|||||||
import LanguageSelector from '@/components/LanguageSelector.vue'
|
import LanguageSelector from '@/components/LanguageSelector.vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { marked } from 'marked'
|
import { marked } from 'marked'
|
||||||
|
import { fetchWithBasePath } from '@/utils/basePath'
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
@ -33,15 +34,13 @@ const floating = ref<HTMLElement | null>(null)
|
|||||||
const version = ref('...')
|
const version = ref('...')
|
||||||
const changelogContent = ref<string | null>(null)
|
const changelogContent = ref<string | null>(null)
|
||||||
const isLoadingChangelog = ref(true)
|
const isLoadingChangelog = ref(true)
|
||||||
const hideCredits = ['1', 'true'].includes(
|
const hideCredits = ['1', 'true'].includes((import.meta.env.VITE_HIDE_CREDITS ?? '').toLowerCase())
|
||||||
(import.meta.env.VITE_HIDE_CREDITS ?? '').toLowerCase()
|
|
||||||
)
|
|
||||||
|
|
||||||
async function fetchAndProcessChangelog() {
|
async function fetchAndProcessChangelog() {
|
||||||
if (changelogContent.value === null) {
|
if (changelogContent.value === null) {
|
||||||
isLoadingChangelog.value = true
|
isLoadingChangelog.value = true
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/CHANGELOG.md')
|
const response = await fetchWithBasePath('/CHANGELOG.md')
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`HTTP error! status: ${response.status}`)
|
throw new Error(`HTTP error! status: ${response.status}`)
|
||||||
}
|
}
|
||||||
|
40
src/utils/basePath.ts
Normal file
40
src/utils/basePath.ts
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
// Base path utility for handling runtime paths
|
||||||
|
// Uses Vite's environment variable system to get BASE_PATH at build time
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the base path for the application
|
||||||
|
* This reads from Vite's environment variables which are set during build time
|
||||||
|
*/
|
||||||
|
export function getBasePath(): string {
|
||||||
|
// Vite makes BASE_PATH available at build time through import.meta.env
|
||||||
|
// If BASE_PATH is not set, default to '/'
|
||||||
|
return import.meta.env.BASE_PATH || '/'
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve a relative path with the application base path
|
||||||
|
* @param path - The path to resolve (should start with /)
|
||||||
|
* @returns The resolved path with base path prepended
|
||||||
|
*/
|
||||||
|
export function resolvePath(path: string): string {
|
||||||
|
const basePath = getBasePath()
|
||||||
|
|
||||||
|
// Remove trailing slash from base path (except for root)
|
||||||
|
const cleanBasePath = basePath === '/' ? '' : basePath.replace(/\/$/, '')
|
||||||
|
|
||||||
|
// Ensure path starts with /
|
||||||
|
const cleanPath = path.startsWith('/') ? path : `/${path}`
|
||||||
|
|
||||||
|
return `${cleanBasePath}${cleanPath}`
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a fetch function that respects the base path
|
||||||
|
* @param path - The path to fetch (should start with /)
|
||||||
|
* @param options - Fetch options
|
||||||
|
* @returns Promise<Response>
|
||||||
|
*/
|
||||||
|
export function fetchWithBasePath(path: string, options?: RequestInit): Promise<Response> {
|
||||||
|
const resolvedPath = resolvePath(path)
|
||||||
|
return fetch(resolvedPath, options)
|
||||||
|
}
|
182
vite.config.js
182
vite.config.js
@ -1,82 +1,120 @@
|
|||||||
import { fileURLToPath, URL } from 'node:url'
|
import { fileURLToPath, URL } from 'node:url'
|
||||||
|
import { defineConfig, loadEnv } from 'vite'
|
||||||
import { defineConfig } from 'vite'
|
|
||||||
import vue from '@vitejs/plugin-vue'
|
import vue from '@vitejs/plugin-vue'
|
||||||
import vueJsx from '@vitejs/plugin-vue-jsx'
|
import vueJsx from '@vitejs/plugin-vue-jsx'
|
||||||
import { VitePWA } from 'vite-plugin-pwa'
|
import { VitePWA } from 'vite-plugin-pwa'
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig(({ mode }) => {
|
||||||
plugins: [
|
// Load environment variables
|
||||||
vue(),
|
const env = loadEnv(mode, '.', '')
|
||||||
vueJsx(),
|
// Get BASE_PATH from environment variable, default to '/'
|
||||||
VitePWA({
|
// Ensure base path ends with slash for proper URL construction
|
||||||
registerType: 'autoUpdate',
|
let base = env.BASE_PATH || '/'
|
||||||
includeAssets: [
|
if (base !== '/' && !base.endsWith('/')) {
|
||||||
'app_icons/web/favicon.ico',
|
base = base + '/'
|
||||||
'app_icons/web/splash-750x1334@2x.png',
|
}
|
||||||
'app_icons/web/splash-1170x2532@3x.png',
|
|
||||||
'app_icons/web/splash-1290x2796@3x.png',
|
return {
|
||||||
'app_icons/web/splash-2048x2732@2x.png'
|
base,
|
||||||
],
|
define: {
|
||||||
manifest: {
|
// Make BASE_PATH available to client-side code through import.meta.env
|
||||||
name: 'MiniQR',
|
'import.meta.env.BASE_PATH': JSON.stringify(base)
|
||||||
short_name: 'MiniQR',
|
},
|
||||||
description: 'A minimal QR code generator and scanner',
|
plugins: [
|
||||||
theme_color: '#ffffff',
|
vue(),
|
||||||
background_color: '#ffffff',
|
vueJsx(),
|
||||||
display: 'standalone',
|
VitePWA({
|
||||||
orientation: 'portrait',
|
registerType: 'autoUpdate',
|
||||||
icons: [
|
base: base, // Make sure PWA respects the base path
|
||||||
{
|
includeAssets: [
|
||||||
src: 'app_icons/web/icon-192.png',
|
'app_icons/web/favicon.ico',
|
||||||
sizes: '192x192',
|
'app_icons/web/splash-750x1334@2x.png',
|
||||||
type: 'image/png'
|
'app_icons/web/splash-1170x2532@3x.png',
|
||||||
},
|
'app_icons/web/splash-1290x2796@3x.png',
|
||||||
{
|
'app_icons/web/splash-2048x2732@2x.png'
|
||||||
src: 'app_icons/web/icon-192-maskable.png',
|
|
||||||
sizes: '192x192',
|
|
||||||
type: 'image/png',
|
|
||||||
purpose: 'maskable'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
src: 'app_icons/web/icon-512.png',
|
|
||||||
sizes: '512x512',
|
|
||||||
type: 'image/png'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
src: 'app_icons/web/icon-512-maskable.png',
|
|
||||||
sizes: '512x512',
|
|
||||||
type: 'image/png',
|
|
||||||
purpose: 'maskable'
|
|
||||||
}
|
|
||||||
],
|
],
|
||||||
screenshots: [
|
manifest: {
|
||||||
{
|
name: 'MiniQR',
|
||||||
src: 'app_icons/web/screenshot-narrow.png',
|
short_name: 'MiniQR',
|
||||||
sizes: '3510x7596',
|
description: 'A minimal QR code generator and scanner',
|
||||||
type: 'image/png',
|
theme_color: '#ffffff',
|
||||||
form_factor: 'narrow'
|
background_color: '#ffffff',
|
||||||
},
|
display: 'standalone',
|
||||||
{
|
orientation: 'portrait',
|
||||||
src: 'app_icons/web/screenshot-wide.png',
|
start_url: base, // Use the base path as start URL
|
||||||
sizes: '7596x3510',
|
icons: [
|
||||||
type: 'image/png',
|
{
|
||||||
form_factor: 'wide'
|
src: 'app_icons/web/icon-192.png',
|
||||||
}
|
sizes: '192x192',
|
||||||
]
|
type: 'image/png'
|
||||||
},
|
},
|
||||||
workbox: {
|
{
|
||||||
navigateFallback: 'index.html'
|
src: 'app_icons/web/icon-192-maskable.png',
|
||||||
},
|
sizes: '192x192',
|
||||||
devOptions: {
|
type: 'image/png',
|
||||||
// enabled: true,
|
purpose: 'maskable'
|
||||||
type: 'module'
|
},
|
||||||
|
{
|
||||||
|
src: 'app_icons/web/icon-512.png',
|
||||||
|
sizes: '512x512',
|
||||||
|
type: 'image/png'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: 'app_icons/web/icon-512-maskable.png',
|
||||||
|
sizes: '512x512',
|
||||||
|
type: 'image/png',
|
||||||
|
purpose: 'maskable'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
screenshots: [
|
||||||
|
{
|
||||||
|
src: 'app_icons/web/screenshot-narrow.png',
|
||||||
|
sizes: '3510x7596',
|
||||||
|
type: 'image/png',
|
||||||
|
form_factor: 'narrow'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: 'app_icons/web/screenshot-wide.png',
|
||||||
|
sizes: '7596x3510',
|
||||||
|
type: 'image/png',
|
||||||
|
form_factor: 'wide'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
workbox: {
|
||||||
|
globPatterns: ['**/*.{js,css,svg,png,jpg,jpeg,gif,ico,woff,woff2}'], // Removed html from patterns
|
||||||
|
// Exclude large files from precaching and HTML files to avoid base path issues
|
||||||
|
globIgnores: ['**/app_preview.*', '**/presets/*.svg', '**/*.html'],
|
||||||
|
maximumFileSizeToCacheInBytes: 5 * 1024 * 1024, // 5MB limit
|
||||||
|
// Don't precache index.html to avoid base path issues
|
||||||
|
dontCacheBustURLsMatching: /\.\w{8}\./,
|
||||||
|
navigateFallback: null, // Disable navigate fallback to avoid issues
|
||||||
|
navigateFallbackDenylist: [/^\/_/, /\/[^/?]+\.[^/]+$/],
|
||||||
|
// Remove modifyURLPrefix as it's causing conflicts with the base path
|
||||||
|
runtimeCaching: [
|
||||||
|
{
|
||||||
|
urlPattern: ({ request }) => request.destination === 'document',
|
||||||
|
handler: 'NetworkFirst',
|
||||||
|
options: {
|
||||||
|
cacheName: 'pages',
|
||||||
|
expiration: {
|
||||||
|
maxEntries: 10,
|
||||||
|
maxAgeSeconds: 86400 // 1 day
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
devOptions: {
|
||||||
|
// enabled: true,
|
||||||
|
type: 'module'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
],
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@': fileURLToPath(new URL('./src', import.meta.url))
|
||||||
}
|
}
|
||||||
})
|
|
||||||
],
|
|
||||||
resolve: {
|
|
||||||
alias: {
|
|
||||||
'@': fileURLToPath(new URL('./src', import.meta.url))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
Loading…
x
Reference in New Issue
Block a user