Implement support for HEIC/HEIF image formats and increase body size limit to 3M

This commit is contained in:
Raj Nandan Sharma
2026-03-19 12:28:58 +05:30
parent 0b2cd5fc8a
commit a8841ad8a3
10 changed files with 216 additions and 94 deletions
+1
View File
@@ -135,6 +135,7 @@ ARG KENER_BASE_PATH=
ENV NODE_ENV=production \ ENV NODE_ENV=production \
PORT=${PORT} \ PORT=${PORT} \
KENER_BASE_PATH=${KENER_BASE_PATH} \ KENER_BASE_PATH=${KENER_BASE_PATH} \
BODY_SIZE_LIMIT=3M \
TZ=UTC \ TZ=UTC \
# Required so Node can import .ts migration/seed files at runtime # Required so Node can import .ts migration/seed files at runtime
NODE_OPTIONS="--experimental-strip-types" NODE_OPTIONS="--experimental-strip-types"
+3
View File
@@ -1,6 +1,9 @@
#!/bin/sh #!/bin/sh
set -e set -e
# Default body size limit for SvelteKit adapter-node (512K default is too small for image uploads)
export BODY_SIZE_LIMIT="${BODY_SIZE_LIMIT:-3M}"
# Index documentation into Redis when docs are bundled in the image # Index documentation into Redis when docs are bundled in the image
if [ -f /app/scripts/index-docs.ts ]; then if [ -f /app/scripts/index-docs.ts ]; then
echo "[kener] Indexing documentation into Redis..." echo "[kener] Indexing documentation into Redis..."
+59
View File
@@ -51,6 +51,7 @@
"front-matter": "^4.0.2", "front-matter": "^4.0.2",
"gamedig": "^5.3.2", "gamedig": "^5.3.2",
"glob": "^13.0.6", "glob": "^13.0.6",
"heic-convert": "^2.1.0",
"highlight.js": "^11.11.1", "highlight.js": "^11.11.1",
"ioredis": "^5.8.2", "ioredis": "^5.8.2",
"js-yaml": "^4.1.1", "js-yaml": "^4.1.1",
@@ -97,6 +98,7 @@
"@types/d3-shape": "^3.1.8", "@types/d3-shape": "^3.1.8",
"@types/dns2": "^2.0.10", "@types/dns2": "^2.0.10",
"@types/express": "^5.0.6", "@types/express": "^5.0.6",
"@types/heic-convert": "^2.1.0",
"@types/jsonwebtoken": "^9.0.10", "@types/jsonwebtoken": "^9.0.10",
"@types/mustache": "^4.2.6", "@types/mustache": "^4.2.6",
"@types/node": "^25.0.3", "@types/node": "^25.0.3",
@@ -2756,6 +2758,13 @@
"@types/send": "*" "@types/send": "*"
} }
}, },
"node_modules/@types/heic-convert": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@types/heic-convert/-/heic-convert-2.1.0.tgz",
"integrity": "sha512-Cf5Sdc2Gm2pfZ0uN1zjj35wcf3mF1lJCMIzws5OdJynrdMJRTIRUGa5LegbVg0hatzOPkH2uAf2JRjPYgl9apg==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/http-cache-semantics": { "node_modules/@types/http-cache-semantics": {
"version": "4.2.0", "version": "4.2.0",
"resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz",
@@ -5760,6 +5769,32 @@
"he": "bin/he" "he": "bin/he"
} }
}, },
"node_modules/heic-convert": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/heic-convert/-/heic-convert-2.1.0.tgz",
"integrity": "sha512-1qDuRvEHifTVAj3pFIgkqGgJIr0M3X7cxEPjEp0oG4mo8GFjq99DpCo8Eg3kg17Cy0MTjxpFdoBHOatj7ZVKtg==",
"license": "ISC",
"dependencies": {
"heic-decode": "^2.0.0",
"jpeg-js": "^0.4.4",
"pngjs": "^6.0.0"
},
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/heic-decode": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/heic-decode/-/heic-decode-2.1.0.tgz",
"integrity": "sha512-0fB3O3WMk38+PScbHLVp66jcNhsZ/ErtQ6u2lMYu/YxXgbBtl+oKOhGQHa4RpvE68k8IzbWkABzHnyAIjR758A==",
"license": "ISC",
"dependencies": {
"libheif-js": "^1.19.8"
},
"engines": {
"node": ">=8.0.0"
}
},
"node_modules/highlight.js": { "node_modules/highlight.js": {
"version": "11.11.1", "version": "11.11.1",
"resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.11.1.tgz", "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.11.1.tgz",
@@ -6399,6 +6434,12 @@
"jiti": "lib/jiti-cli.mjs" "jiti": "lib/jiti-cli.mjs"
} }
}, },
"node_modules/jpeg-js": {
"version": "0.4.4",
"resolved": "https://registry.npmjs.org/jpeg-js/-/jpeg-js-0.4.4.tgz",
"integrity": "sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg==",
"license": "BSD-3-Clause"
},
"node_modules/js-yaml": { "node_modules/js-yaml": {
"version": "4.1.1", "version": "4.1.1",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
@@ -6638,6 +6679,15 @@
"integrity": "sha512-GgOXd0Eo6phYgh0DJtjQ2tO8dc0IVINtZJeARPeiIJqge+HdsWSuaDTe8ztQ7j/cONByDZ3zeB325AHiv5O0dg==", "integrity": "sha512-GgOXd0Eo6phYgh0DJtjQ2tO8dc0IVINtZJeARPeiIJqge+HdsWSuaDTe8ztQ7j/cONByDZ3zeB325AHiv5O0dg==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/libheif-js": {
"version": "1.19.8",
"resolved": "https://registry.npmjs.org/libheif-js/-/libheif-js-1.19.8.tgz",
"integrity": "sha512-vQJWusIxO7wavpON1dusciL8Go9jsIQ+EUrckauFYAiSTjcmLAsuJh3SszLpvkwPci3JcL41ek2n+LUZGFpPIQ==",
"license": "LGPL-3.0",
"engines": {
"node": ">=8.0.0"
}
},
"node_modules/libmime": { "node_modules/libmime": {
"version": "5.3.7", "version": "5.3.7",
"resolved": "https://registry.npmjs.org/libmime/-/libmime-5.3.7.tgz", "resolved": "https://registry.npmjs.org/libmime/-/libmime-5.3.7.tgz",
@@ -8253,6 +8303,15 @@
"node": ">=22.0.0" "node": ">=22.0.0"
} }
}, },
"node_modules/pngjs": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/pngjs/-/pngjs-6.0.0.tgz",
"integrity": "sha512-TRzzuFRRmEoSW/p1KVAmiOgPco2Irlah+bGFCeNfJXxxYGwSw7YwAOAcd7X28K/m5bjBWKsC29KyoMfHbypayg==",
"license": "MIT",
"engines": {
"node": ">=12.13.0"
}
},
"node_modules/possible-typed-array-names": { "node_modules/possible-typed-array-names": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz",
+2
View File
@@ -64,6 +64,7 @@
"@types/d3-shape": "^3.1.8", "@types/d3-shape": "^3.1.8",
"@types/dns2": "^2.0.10", "@types/dns2": "^2.0.10",
"@types/express": "^5.0.6", "@types/express": "^5.0.6",
"@types/heic-convert": "^2.1.0",
"@types/jsonwebtoken": "^9.0.10", "@types/jsonwebtoken": "^9.0.10",
"@types/mustache": "^4.2.6", "@types/mustache": "^4.2.6",
"@types/node": "^25.0.3", "@types/node": "^25.0.3",
@@ -139,6 +140,7 @@
"front-matter": "^4.0.2", "front-matter": "^4.0.2",
"gamedig": "^5.3.2", "gamedig": "^5.3.2",
"glob": "^13.0.6", "glob": "^13.0.6",
"heic-convert": "^2.1.0",
"highlight.js": "^11.11.1", "highlight.js": "^11.11.1",
"ioredis": "^5.8.2", "ioredis": "^5.8.2",
"js-yaml": "^4.1.1", "js-yaml": "^4.1.1",
+82 -74
View File
@@ -1,6 +1,6 @@
import { handler } from "../build/handler.js";
import dotenv from "dotenv"; import dotenv from "dotenv";
dotenv.config(); dotenv.config();
import express from "express"; import express from "express";
import Startup from "../src/lib/server/startup.ts"; import Startup from "../src/lib/server/startup.ts";
import shutdownSchedulers from "../src/lib/server/schedulers/shutdown.ts"; import shutdownSchedulers from "../src/lib/server/schedulers/shutdown.ts";
@@ -12,82 +12,90 @@ import knexOb from "../knexfile.js";
const PORT = process.env.PORT || 3000; const PORT = process.env.PORT || 3000;
const base = process.env.KENER_BASE_PATH || ""; const base = process.env.KENER_BASE_PATH || "";
const app: any = express(); async function start() {
const db = knex(knexOb); // Dynamic import so BODY_SIZE_LIMIT from .env is available
// before the handler reads it at module top-level
const { handler } = await import("../build/handler.js");
app.get(base + "/healthcheck", (req: any, res: any) => { const app: any = express();
res.end("ok"); const db = knex(knexOb);
});
app.use(handler); app.get(base + "/healthcheck", (req: any, res: any) => {
res.end("ok");
});
//migrations app.use(handler);
async function runMigrations() {
try {
// Rename old .js migration entries to .ts in the knex_migrations table
// so Knex can find the renamed files on disk
const hasTable = await db.schema.hasTable("knex_migrations");
if (hasTable) {
const oldJsMigrations = await db("knex_migrations").where("name", "like", "%.js");
for (const row of oldJsMigrations) {
const newName = row.name.replace(/\.js$/, ".ts");
await db("knex_migrations").where("id", row.id).update({ name: newName });
console.log(`Renamed migration record: ${row.name} -> ${newName}`);
}
}
console.log("Running migrations..."); //migrations
await db.migrate.latest(); // Runs migrations to the latest state async function runMigrations() {
console.log("Migrations completed successfully!"); try {
} catch (err) { // Rename old .js migration entries to .ts in the knex_migrations table
console.error("Error running migrations:", err); // so Knex can find the renamed files on disk
} const hasTable = await db.schema.hasTable("knex_migrations");
if (hasTable) {
const oldJsMigrations = await db("knex_migrations").where("name", "like", "%.js");
for (const row of oldJsMigrations) {
const newName = row.name.replace(/\.js$/, ".ts");
await db("knex_migrations").where("id", row.id).update({ name: newName });
console.log(`Renamed migration record: ${row.name} -> ${newName}`);
}
}
console.log("Running migrations...");
await db.migrate.latest(); // Runs migrations to the latest state
console.log("Migrations completed successfully!");
} catch (err) {
console.error("Error running migrations:", err);
}
}
//seed
async function runSeed() {
try {
console.log("Running seed...");
await db.seed.run(); // Runs seed to the latest state
console.log("Seed completed successfully!");
} catch (err) {
console.error("Error running seed:", err);
}
}
app.listen(PORT, async () => {
await runMigrations();
await runSeed();
await db.destroy();
Startup();
console.log("Kener is running on port " + PORT + "!");
});
// Graceful shutdown handler
async function gracefulShutdown(signal: string) {
console.log(`\nReceived ${signal}. Starting graceful shutdown...`);
try {
console.log("Shutting down schedulers...");
await shutdownSchedulers();
console.log("Schedulers shut down successfully.");
console.log("Shutting down queues...");
await shutdownQueues();
console.log("Queues shut down successfully.");
console.log("Closing database connection...");
await dbInstance.close();
console.log("Database connection closed successfully.");
console.log("Graceful shutdown completed.");
process.exit(0);
} catch (err) {
console.error("Error during graceful shutdown:", err);
process.exit(1);
}
}
// Handle termination signals
process.on("SIGTERM", () => gracefulShutdown("SIGTERM"));
process.on("SIGINT", () => gracefulShutdown("SIGINT"));
} }
//seed start();
async function runSeed() {
try {
console.log("Running seed...");
await db.seed.run(); // Runs seed to the latest state
console.log("Seed completed successfully!");
} catch (err) {
console.error("Error running seed:", err);
}
}
app.listen(PORT, async () => {
await runMigrations();
await runSeed();
await db.destroy();
Startup();
console.log("Kener is running on port " + PORT + "!");
});
// Graceful shutdown handler
async function gracefulShutdown(signal: string) {
console.log(`\nReceived ${signal}. Starting graceful shutdown...`);
try {
console.log("Shutting down schedulers...");
await shutdownSchedulers();
console.log("Schedulers shut down successfully.");
console.log("Shutting down queues...");
await shutdownQueues();
console.log("Queues shut down successfully.");
console.log("Closing database connection...");
await dbInstance.close();
console.log("Database connection closed successfully.");
console.log("Graceful shutdown completed.");
process.exit(0);
} catch (err) {
console.error("Error during graceful shutdown:", err);
process.exit(1);
}
}
// Handle termination signals
process.on("SIGTERM", () => gracefulShutdown("SIGTERM"));
process.on("SIGINT", () => gracefulShutdown("SIGINT"));
@@ -178,6 +178,25 @@ NODE_ENV=production
- Controls logging verbosity - Controls logging verbosity
- Enables/disables development-only features - Enables/disables development-only features
### BODY_SIZE_LIMIT {#body-size-limit}
**Purpose**: Maximum allowed request body size. This is a SvelteKit setting that controls the largest payload the server will accept.
**Default**: `3M` (3 megabytes)
**When to Change**: Increase if you need to upload larger images through the admin panel or send larger API payloads.
**Format**: Number followed by unit — `K` (kilobytes), `M` (megabytes), or `Infinity` for no limit.
**Example**:
```bash
BODY_SIZE_LIMIT=10M
```
> [!NOTE]
> The Docker image sets `BODY_SIZE_LIMIT=3M` by default. Override it by passing the variable to your container.
## Integration Variables {#integration-variables} ## Integration Variables {#integration-variables}
For detailed configuration of these integrations, see their dedicated documentation pages. For detailed configuration of these integrations, see their dedicated documentation pages.
+24 -5
View File
@@ -118,6 +118,7 @@ import sendWebhook from "$lib/server/notification/webhook_notification.js";
import sendEmail from "$lib/server/notification/email_notification.js"; import sendEmail from "$lib/server/notification/email_notification.js";
import sendDiscord from "$lib/server/notification/discord_notification.js"; import sendDiscord from "$lib/server/notification/discord_notification.js";
import sendSlack from "$lib/server/notification/slack_notification.js"; import sendSlack from "$lib/server/notification/slack_notification.js";
import heicConvert from "heic-convert";
import serverResolver from "$lib/server/resolver.js"; import serverResolver from "$lib/server/resolver.js";
function AdminCan(role: string) { function AdminCan(role: string) {
@@ -716,7 +717,7 @@ async function uploadImage(data: ImageUploadData): Promise<{ id: string; url: st
throw new Error("Image data is required"); throw new Error("Image data is required");
} }
const allowedMimeTypes = ["image/png", "image/jpeg", "image/jpg", "image/webp"]; const allowedMimeTypes = ["image/png", "image/jpeg", "image/jpg", "image/webp", "image/heic", "image/heif"];
if (!allowedMimeTypes.includes(mimeType)) { if (!allowedMimeTypes.includes(mimeType)) {
throw new Error(`Invalid image type. Allowed types: ${allowedMimeTypes.join(", ")}`); throw new Error(`Invalid image type. Allowed types: ${allowedMimeTypes.join(", ")}`);
} }
@@ -744,8 +745,21 @@ async function uploadImage(data: ImageUploadData): Promise<{ id: string; url: st
let width: number | undefined; let width: number | undefined;
let height: number | undefined; let height: number | undefined;
// Pre-convert HEIC/HEIF to JPEG before passing to sharp (sharp may lack HEVC codec)
let sharpInputBuffer = imageBuffer;
const heicSignature = imageBuffer.subarray(4, 12).toString("ascii");
const isHeicData = heicSignature.includes("ftyp");
if (isHeicData) {
const converted = await heicConvert({
buffer: new Uint8Array(imageBuffer) as unknown as ArrayBuffer,
format: "JPEG",
quality: 0.85,
});
sharpInputBuffer = Buffer.from(converted);
}
// Process with sharp and normalize output // Process with sharp and normalize output
const image = sharp(imageBuffer, { limitInputPixels: GC.MAX_INPUT_PIXELS }); const image = sharp(sharpInputBuffer, { limitInputPixels: GC.MAX_INPUT_PIXELS });
const metadata = await image.metadata(); const metadata = await image.metadata();
const formatToMime: Record<string, string> = { const formatToMime: Record<string, string> = {
@@ -753,6 +767,8 @@ async function uploadImage(data: ImageUploadData): Promise<{ id: string; url: st
jpeg: "image/jpeg", jpeg: "image/jpeg",
webp: "image/webp", webp: "image/webp",
svg: "image/svg+xml", svg: "image/svg+xml",
heic: "image/heic",
heif: "image/heif",
}; };
const detectedMimeType = metadata.format ? formatToMime[metadata.format] : undefined; const detectedMimeType = metadata.format ? formatToMime[metadata.format] : undefined;
@@ -764,7 +780,10 @@ async function uploadImage(data: ImageUploadData): Promise<{ id: string; url: st
throw new Error("SVG uploads are not allowed"); throw new Error("SVG uploads are not allowed");
} }
if (normalizedRequestedMime !== detectedMimeType) { // HEIC/HEIF files often have .jpg extension (e.g. iPhone photos); allow the mismatch
const isHeicDetected = detectedMimeType === "image/heic" || detectedMimeType === "image/heif";
const isHeicRequested = normalizedRequestedMime === "image/heic" || normalizedRequestedMime === "image/heif";
if (normalizedRequestedMime !== detectedMimeType && !isHeicDetected && !isHeicRequested) {
throw new Error("Image MIME type does not match file content"); throw new Error("Image MIME type does not match file content");
} }
@@ -796,8 +815,8 @@ async function uploadImage(data: ImageUploadData): Promise<{ id: string; url: st
width = newWidth; width = newWidth;
height = newHeight; height = newHeight;
// Keep JPEG as JPEG; convert everything else (WebP/PNG) to PNG. // Keep JPEG as JPEG; convert HEIC/HEIF to JPEG; convert everything else (WebP/PNG) to PNG.
if (detectedMimeType === "image/jpeg") { if (detectedMimeType === "image/jpeg" || isHeicDetected) {
processedBuffer = await image processedBuffer = await image
.resize(newWidth, newHeight, { .resize(newWidth, newHeight, {
fit: forceDimensions ? "cover" : "inside", fit: forceDimensions ? "cover" : "inside",
@@ -204,7 +204,7 @@
<input <input
id="monitor-image-input" id="monitor-image-input"
type="file" type="file"
accept="image/png,image/jpeg,image/jpg,image/svg+xml,image/webp" accept="image/png,image/jpeg,image/jpg,image/svg+xml,image/webp,image/heic,image/heif"
class="hidden" class="hidden"
onchange={handleImageUpload} onchange={handleImageUpload}
disabled={uploadingImage} disabled={uploadingImage}
@@ -86,7 +86,8 @@
// Page settings state // Page settings state
let pageSettings = $state<PageSettingsType>(structuredClone(defaultPageSettings)); let pageSettings = $state<PageSettingsType>(structuredClone(defaultPageSettings));
let savingSettings = $state(false); let savingDisplaySettings = $state(false);
let savingSeoSettings = $state(false);
// Validation // Validation
const isFormValid = $derived(formData.page_title.trim().length > 0 && formData.page_header.trim().length > 0); const isFormValid = $derived(formData.page_title.trim().length > 0 && formData.page_header.trim().length > 0);
@@ -381,6 +382,10 @@
}) })
}); });
if (!response.ok) {
toast.error("Failed to upload logo");
return;
}
const result = await response.json(); const result = await response.json();
if (result.error) { if (result.error) {
toast.error(result.error); toast.error(result.error);
@@ -452,6 +457,10 @@
}) })
}); });
if (!response.ok) {
toast.error("Failed to upload social preview image");
return;
}
const result = await response.json(); const result = await response.json();
if (result.error) { if (result.error) {
toast.error(result.error); toast.error(result.error);
@@ -467,10 +476,11 @@
} }
} }
async function savePageSettings() { async function savePageSettings(source: "display" | "seo") {
if (!currentPage) return; if (!currentPage) return;
savingSettings = true; if (source === "display") savingDisplaySettings = true;
else savingSeoSettings = true;
try { try {
const response = await fetch(clientResolver(resolve, "/manage/api"), { const response = await fetch(clientResolver(resolve, "/manage/api"), {
method: "POST", method: "POST",
@@ -493,7 +503,8 @@
} catch (e) { } catch (e) {
toast.error("Failed to save page settings"); toast.error("Failed to save page settings");
} finally { } finally {
savingSettings = false; if (source === "display") savingDisplaySettings = false;
else savingSeoSettings = false;
} }
} }
@@ -635,7 +646,7 @@
<input <input
id="page-logo-input" id="page-logo-input"
type="file" type="file"
accept="image/png,image/jpeg,image/jpg,image/svg+xml,image/webp" accept="image/png,image/jpeg,image/jpg,image/svg+xml,image/webp,image/heic,image/heif"
class="hidden" class="hidden"
onchange={handleLogoUpload} onchange={handleLogoUpload}
disabled={uploadingLogo} disabled={uploadingLogo}
@@ -830,8 +841,8 @@
</div> </div>
</Card.Content> </Card.Content>
<Card.Footer class="flex justify-end"> <Card.Footer class="flex justify-end">
<Button onclick={savePageSettings} disabled={savingSettings}> <Button onclick={() => savePageSettings("display")} disabled={savingDisplaySettings}>
{#if savingSettings} {#if savingDisplaySettings}
<Loader class="h-4 w-4 animate-spin" /> <Loader class="h-4 w-4 animate-spin" />
Saving... Saving...
{:else} {:else}
@@ -885,7 +896,7 @@
<input <input
id="page-social-preview-input" id="page-social-preview-input"
type="file" type="file"
accept="image/png,image/jpeg,image/jpg,image/webp" accept="image/png,image/jpeg,image/jpg,image/webp,image/heic,image/heif"
class="hidden" class="hidden"
onchange={handleSocialPreviewUpload} onchange={handleSocialPreviewUpload}
disabled={uploadingSocialPreview} disabled={uploadingSocialPreview}
@@ -926,8 +937,8 @@
</div> </div>
</Card.Content> </Card.Content>
<Card.Footer class="flex justify-end"> <Card.Footer class="flex justify-end">
<Button onclick={savePageSettings} disabled={savingSettings}> <Button onclick={() => savePageSettings("seo")} disabled={savingSeoSettings}>
{#if savingSettings} {#if savingSeoSettings}
<Loader class="h-4 w-4 animate-spin" /> <Loader class="h-4 w-4 animate-spin" />
Saving... Saving...
{:else} {:else}
@@ -792,7 +792,7 @@
<input <input
id="logo-input" id="logo-input"
type="file" type="file"
accept="image/png,image/jpeg,image/jpg,image/svg+xml,image/webp" accept="image/png,image/jpeg,image/jpg,image/svg+xml,image/webp,image/heic,image/heif"
class="hidden" class="hidden"
onchange={(e) => handleImageUpload(e, "logo")} onchange={(e) => handleImageUpload(e, "logo")}
disabled={uploadingLogo} disabled={uploadingLogo}
@@ -862,7 +862,7 @@
<input <input
id="favicon-input" id="favicon-input"
type="file" type="file"
accept="image/png,image/jpeg,image/jpg,image/svg+xml,image/webp" accept="image/png,image/jpeg,image/jpg,image/svg+xml,image/webp,image/heic,image/heif"
class="hidden" class="hidden"
onchange={(e) => handleImageUpload(e, "favicon")} onchange={(e) => handleImageUpload(e, "favicon")}
disabled={uploadingFavicon} disabled={uploadingFavicon}
@@ -932,7 +932,7 @@
<input <input
id="social-preview-image-input" id="social-preview-image-input"
type="file" type="file"
accept="image/png,image/jpeg,image/jpg,image/webp" accept="image/png,image/jpeg,image/jpg,image/webp,image/heic,image/heif"
class="hidden" class="hidden"
onchange={(e) => handleImageUpload(e, "socialPreviewImage")} onchange={(e) => handleImageUpload(e, "socialPreviewImage")}
disabled={uploadingSocialPreviewImage} disabled={uploadingSocialPreviewImage}
@@ -1028,7 +1028,7 @@
<input <input
id="nav-icon-input-{index}" id="nav-icon-input-{index}"
type="file" type="file"
accept="image/png,image/jpeg,image/jpg,image/svg+xml,image/webp" accept="image/png,image/jpeg,image/jpg,image/svg+xml,image/webp,image/heic,image/heif"
class="hidden" class="hidden"
onchange={(e) => handleNavIconUpload(e, index)} onchange={(e) => handleNavIconUpload(e, index)}
/> />