diff --git a/.env.example b/.env.example index 1ebfc49..5f1222d 100644 --- a/.env.example +++ b/.env.example @@ -74,6 +74,11 @@ POSTGRES_PASSWORD= # Frontend VITE_SITE_DOMAIN=https://dociva.io VITE_SENTRY_DSN= +INDEXNOW_KEY= +INDEXNOW_ENDPOINT=https://www.bing.com/indexnow +INDEXNOW_AUTO_SUBMIT=true +INDEXNOW_STRICT=false +INDEXNOW_FULL_SUBMIT=false # Frontend Analytics / Ads (Vite) VITE_GA_MEASUREMENT_ID=G-XXXXXXXXXX @@ -85,10 +90,9 @@ VITE_ADSENSE_SLOT_HOME_TOP=1234567890 VITE_ADSENSE_SLOT_HOME_BOTTOM=1234567891 VITE_ADSENSE_SLOT_TOP_BANNER=1234567892 VITE_ADSENSE_SLOT_BOTTOM_BANNER=1234567893 +VITE_CLARITY_PROJECT_ID=vzw2jb2ipq # Feature Flags (set to "false" to disable a specific tool) FEATURE_EDITOR=true FEATURE_OCR=true FEATURE_REMOVEBG=true - -VITE_CLARITY_PROJECT_ID=vzw2jb2ipq diff --git a/.gitignore b/.gitignore index fc514a9..90bc5b3 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,7 @@ docs/ node_modules/ frontend/dist/ frontend/build/ +frontend/.indexnow/ .npm *.tsbuildinfo diff --git a/README.md b/README.md index d1c0665..59a8fe5 100644 --- a/README.md +++ b/README.md @@ -79,6 +79,16 @@ docker-compose up --build - `VITE_ADSENSE_SLOT_BOTTOM_BANNER` - `DATABASE_PATH` +## ๐Ÿ”Ž IndexNow + +- Verification file is published at `frontend/public/718dc0aa7c7d4d3ebe71e3f97dacef9c.txt` and copied into the production build. +- Dry-run the payload locally with `cd frontend && npm run indexnow:dry-run`. +- Submit the current sitemap URLs manually with `cd frontend && npm run indexnow:submit`. +- Production deploys call `scripts/deploy.sh`, which runs the same submission step after the health check when `INDEXNOW_AUTO_SUBMIT` is not disabled. +- Successful submissions persist a local state snapshot so later deploys only notify changed or removed URLs instead of re-sending the full sitemap every time. +- `INDEXNOW_STRICT=true` now fails the deployment when the IndexNow request fails. +- Optional env overrides: `INDEXNOW_KEY`, `INDEXNOW_ENDPOINT`, `INDEXNOW_AUTO_SUBMIT`, `INDEXNOW_STRICT`, and `INDEXNOW_FULL_SUBMIT`. + ## ๐Ÿ“ Project Structure ``` diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index b7efb59..3b3ba36 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -137,6 +137,25 @@ services: context: ./frontend dockerfile: Dockerfile target: build + args: + VITE_GA_MEASUREMENT_ID: ${VITE_GA_MEASUREMENT_ID:-} + VITE_PLAUSIBLE_DOMAIN: ${VITE_PLAUSIBLE_DOMAIN:-} + VITE_PLAUSIBLE_SRC: ${VITE_PLAUSIBLE_SRC:-https://plausible.io/js/script.js} + VITE_GOOGLE_SITE_VERIFICATION: ${VITE_GOOGLE_SITE_VERIFICATION:-} + VITE_ADSENSE_CLIENT_ID: ${VITE_ADSENSE_CLIENT_ID:-} + VITE_ADSENSE_SLOT_HOME_TOP: ${VITE_ADSENSE_SLOT_HOME_TOP:-} + VITE_ADSENSE_SLOT_HOME_BOTTOM: ${VITE_ADSENSE_SLOT_HOME_BOTTOM:-} + VITE_ADSENSE_SLOT_TOP_BANNER: ${VITE_ADSENSE_SLOT_TOP_BANNER:-} + VITE_ADSENSE_SLOT_BOTTOM_BANNER: ${VITE_ADSENSE_SLOT_BOTTOM_BANNER:-} + VITE_FEATURE_EDITOR: ${VITE_FEATURE_EDITOR:-true} + VITE_FEATURE_OCR: ${VITE_FEATURE_OCR:-true} + VITE_FEATURE_REMOVEBG: ${VITE_FEATURE_REMOVEBG:-true} + VITE_SITE_DOMAIN: ${VITE_SITE_DOMAIN:-} + VITE_SENTRY_DSN: ${VITE_SENTRY_DSN:-} + VITE_CLARITY_PROJECT_ID: ${VITE_CLARITY_PROJECT_ID:-} + INDEXNOW_KEY: ${INDEXNOW_KEY:-} + INDEXNOW_ENDPOINT: ${INDEXNOW_ENDPOINT:-https://www.bing.com/indexnow} + INDEXNOW_STRICT: ${INDEXNOW_STRICT:-false} environment: - VITE_GA_MEASUREMENT_ID=${VITE_GA_MEASUREMENT_ID:-} - VITE_PLAUSIBLE_DOMAIN=${VITE_PLAUSIBLE_DOMAIN:-} @@ -152,8 +171,15 @@ services: - VITE_FEATURE_REMOVEBG=${VITE_FEATURE_REMOVEBG:-true} - VITE_SITE_DOMAIN=${VITE_SITE_DOMAIN:-} - VITE_SENTRY_DSN=${VITE_SENTRY_DSN:-} + - VITE_CLARITY_PROJECT_ID=${VITE_CLARITY_PROJECT_ID:-} + - INDEXNOW_KEY=${INDEXNOW_KEY:-} + - INDEXNOW_ENDPOINT=${INDEXNOW_ENDPOINT:-https://www.bing.com/indexnow} + - INDEXNOW_STRICT=${INDEXNOW_STRICT:-false} + - INDEXNOW_STATE_DIR=/app/.indexnow + - INDEXNOW_FULL_SUBMIT=${INDEXNOW_FULL_SUBMIT:-false} volumes: - frontend_build:/app/dist + - indexnow_state:/app/.indexnow volumes: postgres_data: @@ -162,3 +188,4 @@ volumes: output_data: db_data: frontend_build: + indexnow_state: diff --git a/frontend/Dockerfile b/frontend/Dockerfile index 5bd129a..c68d1b4 100644 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -3,6 +3,44 @@ FROM node:20-alpine AS build WORKDIR /app +ARG VITE_GA_MEASUREMENT_ID +ARG VITE_PLAUSIBLE_DOMAIN +ARG VITE_PLAUSIBLE_SRC +ARG VITE_GOOGLE_SITE_VERIFICATION +ARG VITE_ADSENSE_CLIENT_ID +ARG VITE_ADSENSE_SLOT_HOME_TOP +ARG VITE_ADSENSE_SLOT_HOME_BOTTOM +ARG VITE_ADSENSE_SLOT_TOP_BANNER +ARG VITE_ADSENSE_SLOT_BOTTOM_BANNER +ARG VITE_FEATURE_EDITOR +ARG VITE_FEATURE_OCR +ARG VITE_FEATURE_REMOVEBG +ARG VITE_SITE_DOMAIN +ARG VITE_SENTRY_DSN +ARG VITE_CLARITY_PROJECT_ID +ARG INDEXNOW_KEY +ARG INDEXNOW_ENDPOINT +ARG INDEXNOW_STRICT + +ENV VITE_GA_MEASUREMENT_ID=$VITE_GA_MEASUREMENT_ID \ + VITE_PLAUSIBLE_DOMAIN=$VITE_PLAUSIBLE_DOMAIN \ + VITE_PLAUSIBLE_SRC=$VITE_PLAUSIBLE_SRC \ + VITE_GOOGLE_SITE_VERIFICATION=$VITE_GOOGLE_SITE_VERIFICATION \ + VITE_ADSENSE_CLIENT_ID=$VITE_ADSENSE_CLIENT_ID \ + VITE_ADSENSE_SLOT_HOME_TOP=$VITE_ADSENSE_SLOT_HOME_TOP \ + VITE_ADSENSE_SLOT_HOME_BOTTOM=$VITE_ADSENSE_SLOT_HOME_BOTTOM \ + VITE_ADSENSE_SLOT_TOP_BANNER=$VITE_ADSENSE_SLOT_TOP_BANNER \ + VITE_ADSENSE_SLOT_BOTTOM_BANNER=$VITE_ADSENSE_SLOT_BOTTOM_BANNER \ + VITE_FEATURE_EDITOR=$VITE_FEATURE_EDITOR \ + VITE_FEATURE_OCR=$VITE_FEATURE_OCR \ + VITE_FEATURE_REMOVEBG=$VITE_FEATURE_REMOVEBG \ + VITE_SITE_DOMAIN=$VITE_SITE_DOMAIN \ + VITE_SENTRY_DSN=$VITE_SENTRY_DSN \ + VITE_CLARITY_PROJECT_ID=$VITE_CLARITY_PROJECT_ID \ + INDEXNOW_KEY=$INDEXNOW_KEY \ + INDEXNOW_ENDPOINT=$INDEXNOW_ENDPOINT \ + INDEXNOW_STRICT=$INDEXNOW_STRICT + # Install dependencies COPY package.json ./ RUN npm install diff --git a/frontend/package-lock.json b/frontend/package-lock.json index f1800a6..56b6cbe 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -2240,29 +2240,6 @@ "license": "MIT", "optional": true }, - "node_modules/base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "optional": true, - "peer": true - }, "node_modules/baseline-browser-mapping": { "version": "2.10.0", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.0.tgz", @@ -2299,20 +2276,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/bl": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", - "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "buffer": "^5.5.0", - "inherits": "^2.0.4", - "readable-stream": "^3.4.0" - } - }, "node_modules/brace-expansion": { "version": "1.1.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", @@ -2371,33 +2334,6 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, - "node_modules/buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" - } - }, "node_modules/call-bind-apply-helpers": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", @@ -2442,23 +2378,6 @@ ], "license": "CC-BY-4.0" }, - "node_modules/canvas": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/canvas/-/canvas-3.2.1.tgz", - "integrity": "sha512-ej1sPFR5+0YWtaVp6S1N1FVz69TQCqmrkGeRvQxZeAB1nAIcjNTHVwrZtYtWFFBmQsF40/uDLehsW5KuYC99mg==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "node-addon-api": "^7.0.0", - "prebuild-install": "^7.1.3" - }, - "engines": { - "node": "^18.12.0 || >= 20.9.0" - } - }, "node_modules/chai": { "version": "6.2.2", "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", @@ -2507,15 +2426,6 @@ "node": ">= 6" } }, - "node_modules/chownr": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", - "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", - "dev": true, - "license": "ISC", - "optional": true, - "peer": true - }, "node_modules/cli-width": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", @@ -2775,36 +2685,6 @@ "devOptional": true, "license": "MIT" }, - "node_modules/decompress-response": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", - "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "mimic-response": "^3.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/deep-extend": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", - "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": ">=4.0.0" - } - }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -2915,18 +2795,6 @@ "devOptional": true, "license": "MIT" }, - "node_modules/end-of-stream": { - "version": "1.4.5", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", - "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "once": "^1.4.0" - } - }, "node_modules/entities": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", @@ -3107,18 +2975,6 @@ "node": ">=0.10.0" } }, - "node_modules/expand-template": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", - "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", - "dev": true, - "license": "(MIT OR WTFPL)", - "optional": true, - "peer": true, - "engines": { - "node": ">=6" - } - }, "node_modules/expect-type": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", @@ -3546,15 +3402,6 @@ "url": "https://github.com/sponsors/rawify" } }, - "node_modules/fs-constants": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", - "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true - }, "node_modules/fs-minipass": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", @@ -3705,15 +3552,6 @@ "node": ">= 0.4" } }, - "node_modules/github-from-package": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", - "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true - }, "node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -3919,29 +3757,6 @@ "node": ">=0.10.0" } }, - "node_modules/ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "BSD-3-Clause", - "optional": true, - "peer": true - }, "node_modules/indent-string": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", @@ -3971,15 +3786,6 @@ "license": "ISC", "optional": true }, - "node_modules/ini": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", - "dev": true, - "license": "ISC", - "optional": true, - "peer": true - }, "node_modules/invariant": { "version": "2.2.4", "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", @@ -4307,21 +4113,6 @@ "node": ">= 0.6" } }, - "node_modules/mimic-response": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", - "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/min-indent": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", @@ -4345,18 +4136,6 @@ "node": "*" } }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/minipass": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", @@ -4414,15 +4193,6 @@ "node": ">=10" } }, - "node_modules/mkdirp-classic": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", - "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true - }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -4523,54 +4293,6 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, - "node_modules/napi-build-utils": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", - "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true - }, - "node_modules/node-abi": { - "version": "3.87.0", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.87.0.tgz", - "integrity": "sha512-+CGM1L1CgmtheLcBuleyYOn7NWPVu0s0EJH2C4puxgEZb9h8QpR9G2dBfZJOAUhi7VQxuBPMd0hiISWcTyiYyQ==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "semver": "^7.3.5" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/node-abi/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "dev": true, - "license": "ISC", - "optional": true, - "peer": true, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/node-addon-api": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", - "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true - }, "node_modules/node-fetch": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", @@ -4989,36 +4711,6 @@ "dev": true, "license": "MIT" }, - "node_modules/prebuild-install": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", - "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", - "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "detect-libc": "^2.0.0", - "expand-template": "^2.0.3", - "github-from-package": "0.0.0", - "minimist": "^1.2.3", - "mkdirp-classic": "^0.5.3", - "napi-build-utils": "^2.0.0", - "node-abi": "^3.3.0", - "pump": "^3.0.0", - "rc": "^1.2.7", - "simple-get": "^4.0.0", - "tar-fs": "^2.0.0", - "tunnel-agent": "^0.6.0" - }, - "bin": { - "prebuild-install": "bin.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/pretty-format": { "version": "27.5.1", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", @@ -5073,19 +4765,6 @@ "url": "https://github.com/sponsors/lupomontero" } }, - "node_modules/pump": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", - "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -5124,24 +4803,6 @@ ], "license": "MIT" }, - "node_modules/rc": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", - "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", - "dev": true, - "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", - "optional": true, - "peer": true, - "dependencies": { - "deep-extend": "^0.6.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - }, - "bin": { - "rc": "cli.js" - } - }, "node_modules/react": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", @@ -5598,34 +5259,6 @@ "license": "MIT", "optional": true }, - "node_modules/simple-get": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", - "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "decompress-response": "^6.0.0", - "once": "^1.3.1", - "simple-concat": "^1.0.0" - } - }, "node_modules/sonner": { "version": "1.7.4", "resolved": "https://registry.npmjs.org/sonner/-/sonner-1.7.4.tgz", @@ -5738,18 +5371,6 @@ "node": ">=8" } }, - "node_modules/strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/sucrase": { "version": "3.35.1", "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", @@ -5863,40 +5484,6 @@ "node": ">=10" } }, - "node_modules/tar-fs": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", - "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "chownr": "^1.1.1", - "mkdirp-classic": "^0.5.2", - "pump": "^3.0.0", - "tar-stream": "^2.1.4" - } - }, - "node_modules/tar-stream": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", - "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "bl": "^4.0.3", - "end-of-stream": "^1.4.1", - "fs-constants": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^3.1.1" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/tar/node_modules/chownr": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", @@ -6084,21 +5671,6 @@ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, - "node_modules/tunnel-agent": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", - "dev": true, - "license": "Apache-2.0", - "optional": true, - "peer": true, - "dependencies": { - "safe-buffer": "^5.0.1" - }, - "engines": { - "node": "*" - } - }, "node_modules/type-fest": { "version": "5.4.4", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-5.4.4.tgz", diff --git a/frontend/package.json b/frontend/package.json index 17503f1..6f301b1 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -7,6 +7,8 @@ "dev": "vite", "prebuild": "node scripts/merge-keywords.mjs && node scripts/generate-seo-assets.mjs", "build": "tsc --noEmit && vite build && node scripts/render-seo-shells.mjs", + "indexnow:submit": "node scripts/submit-indexnow.mjs", + "indexnow:dry-run": "node scripts/submit-indexnow.mjs --dry-run", "preview": "vite preview", "lint": "eslint .", "test": "vitest run", diff --git a/frontend/public/718dc0aa7c7d4d3ebe71e3f97dacef9c.txt b/frontend/public/718dc0aa7c7d4d3ebe71e3f97dacef9c.txt new file mode 100644 index 0000000..874074d --- /dev/null +++ b/frontend/public/718dc0aa7c7d4d3ebe71e3f97dacef9c.txt @@ -0,0 +1 @@ +718dc0aa7c7d4d3ebe71e3f97dacef9c \ No newline at end of file diff --git a/frontend/public/sitemap.xml b/frontend/public/sitemap.xml index 2a6a2b4..8ef029c 100644 --- a/frontend/public/sitemap.xml +++ b/frontend/public/sitemap.xml @@ -2,18 +2,18 @@ https://dociva.io/sitemaps/static.xml - 2026-04-01 + 2026-04-03 https://dociva.io/sitemaps/blog.xml - 2026-04-01 + 2026-04-03 https://dociva.io/sitemaps/tools.xml - 2026-04-01 + 2026-04-03 https://dociva.io/sitemaps/seo.xml - 2026-04-01 + 2026-04-03 diff --git a/frontend/public/sitemaps/blog.xml b/frontend/public/sitemaps/blog.xml index 1f71b5a..ad4c9e2 100644 --- a/frontend/public/sitemaps/blog.xml +++ b/frontend/public/sitemaps/blog.xml @@ -2,31 +2,31 @@ https://dociva.io/blog/how-to-compress-pdf-online - 2026-04-01 + 2026-04-03 monthly 0.6 https://dociva.io/blog/convert-images-without-losing-quality - 2026-04-01 + 2026-04-03 monthly 0.6 https://dociva.io/blog/ocr-extract-text-from-images - 2026-04-01 + 2026-04-03 monthly 0.6 https://dociva.io/blog/merge-split-pdf-files - 2026-04-01 + 2026-04-03 monthly 0.6 https://dociva.io/blog/ai-chat-with-pdf-documents - 2026-04-01 + 2026-04-03 monthly 0.6 diff --git a/frontend/public/sitemaps/seo.xml b/frontend/public/sitemaps/seo.xml index cb8f496..6ca2389 100644 --- a/frontend/public/sitemaps/seo.xml +++ b/frontend/public/sitemaps/seo.xml @@ -2,1129 +2,1129 @@ https://dociva.io/pdf-to-word - 2026-04-01 + 2026-04-03 weekly 0.88 https://dociva.io/ar/pdf-to-word - 2026-04-01 + 2026-04-03 weekly 0.8 https://dociva.io/word-to-pdf - 2026-04-01 + 2026-04-03 weekly 0.88 https://dociva.io/ar/word-to-pdf - 2026-04-01 + 2026-04-03 weekly 0.8 https://dociva.io/compress-pdf-online - 2026-04-01 + 2026-04-03 weekly 0.88 https://dociva.io/ar/compress-pdf-online - 2026-04-01 + 2026-04-03 weekly 0.8 https://dociva.io/convert-jpg-to-pdf - 2026-04-01 + 2026-04-03 weekly 0.88 https://dociva.io/ar/convert-jpg-to-pdf - 2026-04-01 + 2026-04-03 weekly 0.8 https://dociva.io/merge-pdf-files - 2026-04-01 + 2026-04-03 weekly 0.88 https://dociva.io/ar/merge-pdf-files - 2026-04-01 + 2026-04-03 weekly 0.8 https://dociva.io/remove-pdf-password - 2026-04-01 + 2026-04-03 weekly 0.88 https://dociva.io/ar/remove-pdf-password - 2026-04-01 + 2026-04-03 weekly 0.8 https://dociva.io/pdf-to-word-editable - 2026-04-01 + 2026-04-03 weekly 0.88 https://dociva.io/ar/pdf-to-word-editable - 2026-04-01 + 2026-04-03 weekly 0.8 https://dociva.io/convert-pdf-to-text - 2026-04-01 + 2026-04-03 weekly 0.88 https://dociva.io/ar/convert-pdf-to-text - 2026-04-01 + 2026-04-03 weekly 0.8 https://dociva.io/split-pdf-online - 2026-04-01 + 2026-04-03 weekly 0.88 https://dociva.io/ar/split-pdf-online - 2026-04-01 + 2026-04-03 weekly 0.8 https://dociva.io/jpg-to-pdf - 2026-04-01 + 2026-04-03 weekly 0.88 https://dociva.io/ar/jpg-to-pdf - 2026-04-01 + 2026-04-03 weekly 0.8 https://dociva.io/png-to-pdf - 2026-04-01 + 2026-04-03 weekly 0.88 https://dociva.io/ar/png-to-pdf - 2026-04-01 + 2026-04-03 weekly 0.8 https://dociva.io/images-to-pdf-online - 2026-04-01 + 2026-04-03 weekly 0.88 https://dociva.io/ar/images-to-pdf-online - 2026-04-01 + 2026-04-03 weekly 0.8 https://dociva.io/pdf-to-jpg - 2026-04-01 + 2026-04-03 weekly 0.88 https://dociva.io/ar/pdf-to-jpg - 2026-04-01 + 2026-04-03 weekly 0.8 https://dociva.io/pdf-to-png - 2026-04-01 + 2026-04-03 weekly 0.88 https://dociva.io/ar/pdf-to-png - 2026-04-01 + 2026-04-03 weekly 0.8 https://dociva.io/compress-pdf-for-email - 2026-04-01 + 2026-04-03 weekly 0.88 https://dociva.io/ar/compress-pdf-for-email - 2026-04-01 + 2026-04-03 weekly 0.8 https://dociva.io/compress-scanned-pdf - 2026-04-01 + 2026-04-03 weekly 0.88 https://dociva.io/ar/compress-scanned-pdf - 2026-04-01 + 2026-04-03 weekly 0.8 https://dociva.io/merge-pdf-online-free - 2026-04-01 + 2026-04-03 weekly 0.88 https://dociva.io/ar/merge-pdf-online-free - 2026-04-01 + 2026-04-03 weekly 0.8 https://dociva.io/combine-pdf-files - 2026-04-01 + 2026-04-03 weekly 0.88 https://dociva.io/ar/combine-pdf-files - 2026-04-01 + 2026-04-03 weekly 0.8 https://dociva.io/extract-pages-from-pdf - 2026-04-01 + 2026-04-03 weekly 0.88 https://dociva.io/ar/extract-pages-from-pdf - 2026-04-01 + 2026-04-03 weekly 0.8 https://dociva.io/reorder-pdf-pages - 2026-04-01 + 2026-04-03 weekly 0.88 https://dociva.io/ar/reorder-pdf-pages - 2026-04-01 + 2026-04-03 weekly 0.8 https://dociva.io/rotate-pdf-pages - 2026-04-01 + 2026-04-03 weekly 0.88 https://dociva.io/ar/rotate-pdf-pages - 2026-04-01 + 2026-04-03 weekly 0.8 https://dociva.io/add-page-numbers-to-pdf - 2026-04-01 + 2026-04-03 weekly 0.88 https://dociva.io/ar/add-page-numbers-to-pdf - 2026-04-01 + 2026-04-03 weekly 0.8 https://dociva.io/protect-pdf-with-password - 2026-04-01 + 2026-04-03 weekly 0.88 https://dociva.io/ar/protect-pdf-with-password - 2026-04-01 + 2026-04-03 weekly 0.8 https://dociva.io/unlock-pdf-online - 2026-04-01 + 2026-04-03 weekly 0.88 https://dociva.io/ar/unlock-pdf-online - 2026-04-01 + 2026-04-03 weekly 0.8 https://dociva.io/watermark-pdf-online - 2026-04-01 + 2026-04-03 weekly 0.88 https://dociva.io/ar/watermark-pdf-online - 2026-04-01 + 2026-04-03 weekly 0.8 https://dociva.io/remove-watermark-from-pdf - 2026-04-01 + 2026-04-03 weekly 0.88 https://dociva.io/ar/remove-watermark-from-pdf - 2026-04-01 + 2026-04-03 weekly 0.8 https://dociva.io/edit-pdf-online-free - 2026-04-01 + 2026-04-03 weekly 0.88 https://dociva.io/ar/edit-pdf-online-free - 2026-04-01 + 2026-04-03 weekly 0.8 https://dociva.io/pdf-to-excel-online - 2026-04-01 + 2026-04-03 weekly 0.88 https://dociva.io/ar/pdf-to-excel-online - 2026-04-01 + 2026-04-03 weekly 0.8 https://dociva.io/extract-tables-from-pdf - 2026-04-01 + 2026-04-03 weekly 0.88 https://dociva.io/ar/extract-tables-from-pdf - 2026-04-01 + 2026-04-03 weekly 0.8 https://dociva.io/html-to-pdf-online - 2026-04-01 + 2026-04-03 weekly 0.88 https://dociva.io/ar/html-to-pdf-online - 2026-04-01 + 2026-04-03 weekly 0.8 https://dociva.io/scan-pdf-to-text - 2026-04-01 + 2026-04-03 weekly 0.88 https://dociva.io/ar/scan-pdf-to-text - 2026-04-01 + 2026-04-03 weekly 0.8 https://dociva.io/chat-with-pdf - 2026-04-01 + 2026-04-03 weekly 0.88 https://dociva.io/ar/chat-with-pdf - 2026-04-01 + 2026-04-03 weekly 0.8 https://dociva.io/summarize-pdf-online - 2026-04-01 + 2026-04-03 weekly 0.88 https://dociva.io/ar/summarize-pdf-online - 2026-04-01 + 2026-04-03 weekly 0.8 https://dociva.io/translate-pdf-online - 2026-04-01 + 2026-04-03 weekly 0.88 https://dociva.io/ar/translate-pdf-online - 2026-04-01 + 2026-04-03 weekly 0.8 https://dociva.io/convert-image-to-pdf - 2026-04-01 + 2026-04-03 weekly 0.88 https://dociva.io/ar/convert-image-to-pdf - 2026-04-01 + 2026-04-03 weekly 0.8 https://dociva.io/convert-webp-to-jpg - 2026-04-01 + 2026-04-03 weekly 0.88 https://dociva.io/ar/convert-webp-to-jpg - 2026-04-01 + 2026-04-03 weekly 0.8 https://dociva.io/resize-image-online - 2026-04-01 + 2026-04-03 weekly 0.88 https://dociva.io/ar/resize-image-online - 2026-04-01 + 2026-04-03 weekly 0.8 https://dociva.io/compress-image-online - 2026-04-01 + 2026-04-03 weekly 0.88 https://dociva.io/ar/compress-image-online - 2026-04-01 + 2026-04-03 weekly 0.8 https://dociva.io/remove-image-background - 2026-04-01 + 2026-04-03 weekly 0.88 https://dociva.io/ar/remove-image-background - 2026-04-01 + 2026-04-03 weekly 0.8 https://dociva.io/pdf-to-word-editable-free - 2026-04-01 + 2026-04-03 weekly 0.88 https://dociva.io/ar/pdf-to-word-editable-free - 2026-04-01 + 2026-04-03 weekly 0.8 https://dociva.io/compress-pdf-to-100kb - 2026-04-01 + 2026-04-03 weekly 0.88 https://dociva.io/ar/compress-pdf-to-100kb - 2026-04-01 + 2026-04-03 weekly 0.8 https://dociva.io/ai-extract-text-from-pdf - 2026-04-01 + 2026-04-03 weekly 0.88 https://dociva.io/ar/ai-extract-text-from-pdf - 2026-04-01 + 2026-04-03 weekly 0.8 https://dociva.io/pdf-to-excel-accurate-free - 2026-04-01 + 2026-04-03 weekly 0.88 https://dociva.io/ar/pdf-to-excel-accurate-free - 2026-04-01 + 2026-04-03 weekly 0.8 https://dociva.io/split-pdf-online-free - 2026-04-01 + 2026-04-03 weekly 0.88 https://dociva.io/ar/split-pdf-online-free - 2026-04-01 + 2026-04-03 weekly 0.8 https://dociva.io/compress-pdf-online-free - 2026-04-01 + 2026-04-03 weekly 0.88 https://dociva.io/ar/compress-pdf-online-free - 2026-04-01 + 2026-04-03 weekly 0.8 https://dociva.io/unlock-pdf-online-free - 2026-04-01 + 2026-04-03 weekly 0.88 https://dociva.io/ar/unlock-pdf-online-free - 2026-04-01 + 2026-04-03 weekly 0.8 https://dociva.io/summarize-pdf-ai - 2026-04-01 + 2026-04-03 weekly 0.88 https://dociva.io/ar/summarize-pdf-ai - 2026-04-01 + 2026-04-03 weekly 0.8 https://dociva.io/convert-pdf-to-text-ai - 2026-04-01 + 2026-04-03 weekly 0.88 https://dociva.io/ar/convert-pdf-to-text-ai - 2026-04-01 + 2026-04-03 weekly 0.8 https://dociva.io/pdf-to-jpg-high-quality - 2026-04-01 + 2026-04-03 weekly 0.88 https://dociva.io/ar/pdf-to-jpg-high-quality - 2026-04-01 + 2026-04-03 weekly 0.8 https://dociva.io/jpg-to-pdf-online-free - 2026-04-01 + 2026-04-03 weekly 0.88 https://dociva.io/ar/jpg-to-pdf-online-free - 2026-04-01 + 2026-04-03 weekly 0.8 https://dociva.io/reduce-pdf-size-for-email - 2026-04-01 + 2026-04-03 weekly 0.88 https://dociva.io/ar/reduce-pdf-size-for-email - 2026-04-01 + 2026-04-03 weekly 0.8 https://dociva.io/ocr-for-scanned-pdfs - 2026-04-01 + 2026-04-03 weekly 0.88 https://dociva.io/ar/ocr-for-scanned-pdfs - 2026-04-01 + 2026-04-03 weekly 0.8 https://dociva.io/remove-watermark-from-pdf-online - 2026-04-01 + 2026-04-03 weekly 0.88 https://dociva.io/ar/remove-watermark-from-pdf-online - 2026-04-01 + 2026-04-03 weekly 0.8 https://dociva.io/add-watermark-to-pdf-online - 2026-04-01 + 2026-04-03 weekly 0.88 https://dociva.io/ar/add-watermark-to-pdf-online - 2026-04-01 + 2026-04-03 weekly 0.8 https://dociva.io/repair-corrupted-pdf-online - 2026-04-01 + 2026-04-03 weekly 0.88 https://dociva.io/ar/repair-corrupted-pdf-online - 2026-04-01 + 2026-04-03 weekly 0.8 https://dociva.io/rotate-pdf-pages-online - 2026-04-01 + 2026-04-03 weekly 0.88 https://dociva.io/ar/rotate-pdf-pages-online - 2026-04-01 + 2026-04-03 weekly 0.8 https://dociva.io/reorder-pdf-pages-online - 2026-04-01 + 2026-04-03 weekly 0.88 https://dociva.io/ar/reorder-pdf-pages-online - 2026-04-01 + 2026-04-03 weekly 0.8 https://dociva.io/pdf-to-png-online - 2026-04-01 + 2026-04-03 weekly 0.88 https://dociva.io/ar/pdf-to-png-online - 2026-04-01 + 2026-04-03 weekly 0.8 https://dociva.io/images-to-pdf-multiple - 2026-04-01 + 2026-04-03 weekly 0.88 https://dociva.io/ar/images-to-pdf-multiple - 2026-04-01 + 2026-04-03 weekly 0.8 https://dociva.io/split-pdf-by-range-online - 2026-04-01 + 2026-04-03 weekly 0.88 https://dociva.io/ar/split-pdf-by-range-online - 2026-04-01 + 2026-04-03 weekly 0.8 https://dociva.io/compress-scanned-pdf-online - 2026-04-01 + 2026-04-03 weekly 0.88 https://dociva.io/ar/compress-scanned-pdf-online - 2026-04-01 + 2026-04-03 weekly 0.8 https://dociva.io/pdf-metadata-editor-online - 2026-04-01 + 2026-04-03 weekly 0.88 https://dociva.io/ar/pdf-metadata-editor-online - 2026-04-01 + 2026-04-03 weekly 0.8 https://dociva.io/add-page-numbers-to-pdf-online - 2026-04-01 + 2026-04-03 weekly 0.88 https://dociva.io/ar/add-page-numbers-to-pdf-online - 2026-04-01 + 2026-04-03 weekly 0.8 https://dociva.io/protect-pdf-with-password-online - 2026-04-01 + 2026-04-03 weekly 0.88 https://dociva.io/ar/protect-pdf-with-password-online - 2026-04-01 + 2026-04-03 weekly 0.8 https://dociva.io/unlock-encrypted-pdf-online - 2026-04-01 + 2026-04-03 weekly 0.88 https://dociva.io/ar/unlock-encrypted-pdf-online - 2026-04-01 + 2026-04-03 weekly 0.8 https://dociva.io/ocr-table-extraction-from-pdf - 2026-04-01 + 2026-04-03 weekly 0.88 https://dociva.io/ar/ocr-table-extraction-from-pdf - 2026-04-01 + 2026-04-03 weekly 0.8 https://dociva.io/pdf-to-excel-converter-online - 2026-04-01 + 2026-04-03 weekly 0.88 https://dociva.io/ar/pdf-to-excel-converter-online - 2026-04-01 + 2026-04-03 weekly 0.8 https://dociva.io/extract-text-from-protected-pdf - 2026-04-01 + 2026-04-03 weekly 0.88 https://dociva.io/ar/extract-text-from-protected-pdf - 2026-04-01 + 2026-04-03 weekly 0.8 https://dociva.io/bulk-convert-pdf-to-word - 2026-04-01 + 2026-04-03 weekly 0.88 https://dociva.io/ar/bulk-convert-pdf-to-word - 2026-04-01 + 2026-04-03 weekly 0.8 https://dociva.io/compress-pdf-for-web-upload - 2026-04-01 + 2026-04-03 weekly 0.88 https://dociva.io/ar/compress-pdf-for-web-upload - 2026-04-01 + 2026-04-03 weekly 0.8 https://dociva.io/ocr-multi-language-pdf - 2026-04-01 + 2026-04-03 weekly 0.88 https://dociva.io/ar/ocr-multi-language-pdf - 2026-04-01 + 2026-04-03 weekly 0.8 https://dociva.io/summarize-long-pdf-ai - 2026-04-01 + 2026-04-03 weekly 0.88 https://dociva.io/ar/summarize-long-pdf-ai - 2026-04-01 + 2026-04-03 weekly 0.8 https://dociva.io/convert-pdf-to-ppt-online - 2026-04-01 + 2026-04-03 weekly 0.88 https://dociva.io/ar/convert-pdf-to-ppt-online - 2026-04-01 + 2026-04-03 weekly 0.8 https://dociva.io/pdf-to-pptx-free-online - 2026-04-01 + 2026-04-03 weekly 0.88 https://dociva.io/ar/pdf-to-pptx-free-online - 2026-04-01 + 2026-04-03 weekly 0.8 https://dociva.io/ุฏู…ุฌ-ู…ู„ูุงุช-pdf-ู…ุฌุงู†ุงู‹ - 2026-04-01 + 2026-04-03 weekly 0.88 https://dociva.io/ar/ุฏู…ุฌ-ู…ู„ูุงุช-pdf-ู…ุฌุงู†ุงู‹ - 2026-04-01 + 2026-04-03 weekly 0.8 https://dociva.io/ุถุบุท-ุจูŠ-ุฏูŠ-ุงู-ุงูˆู†ู„ุงูŠู† - 2026-04-01 + 2026-04-03 weekly 0.88 https://dociva.io/ar/ุถุบุท-ุจูŠ-ุฏูŠ-ุงู-ุงูˆู†ู„ุงูŠู† - 2026-04-01 + 2026-04-03 weekly 0.8 https://dociva.io/ุชุญูˆูŠู„-pdf-ุงู„ู‰-word-ู‚ุงุจู„-ู„ู„ุชุนุฏูŠู„ - 2026-04-01 + 2026-04-03 weekly 0.88 https://dociva.io/ar/ุชุญูˆูŠู„-pdf-ุงู„ู‰-word-ู‚ุงุจู„-ู„ู„ุชุนุฏูŠู„ - 2026-04-01 + 2026-04-03 weekly 0.8 https://dociva.io/ุชุญูˆูŠู„-jpg-ุงู„ู‰-pdf-ุงูˆู†ู„ุงูŠู† - 2026-04-01 + 2026-04-03 weekly 0.88 https://dociva.io/ar/ุชุญูˆูŠู„-jpg-ุงู„ู‰-pdf-ุงูˆู†ู„ุงูŠู† - 2026-04-01 + 2026-04-03 weekly 0.8 https://dociva.io/ูุตู„-ุตูุญุงุช-pdf-ุงูˆู†ู„ุงูŠู† - 2026-04-01 + 2026-04-03 weekly 0.88 https://dociva.io/ar/ูุตู„-ุตูุญุงุช-pdf-ุงูˆู†ู„ุงูŠู† - 2026-04-01 + 2026-04-03 weekly 0.8 https://dociva.io/ุงุฒุงู„ุฉ-ูƒู„ู…ุฉ-ู…ุฑูˆุฑ-ู…ู†-pdf - 2026-04-01 + 2026-04-03 weekly 0.88 https://dociva.io/ar/ุงุฒุงู„ุฉ-ูƒู„ู…ุฉ-ู…ุฑูˆุฑ-ู…ู†-pdf - 2026-04-01 + 2026-04-03 weekly 0.8 https://dociva.io/ุชุญูˆูŠู„-pdf-ุงู„ู‰-ู†ุต-ุจุงุณุชุฎุฏุงู…-ocr - 2026-04-01 + 2026-04-03 weekly 0.88 https://dociva.io/ar/ุชุญูˆูŠู„-pdf-ุงู„ู‰-ู†ุต-ุจุงุณุชุฎุฏุงู…-ocr - 2026-04-01 + 2026-04-03 weekly 0.8 https://dociva.io/ุชุญูˆูŠู„-pdf-ุงู„ู‰-excel-ุงูˆู†ู„ุงูŠู† - 2026-04-01 + 2026-04-03 weekly 0.88 https://dociva.io/ar/ุชุญูˆูŠู„-pdf-ุงู„ู‰-excel-ุงูˆู†ู„ุงูŠู† - 2026-04-01 + 2026-04-03 weekly 0.8 https://dociva.io/ุชุญูˆูŠู„-pdf-ุงู„ู‰-ุตูˆุฑ - 2026-04-01 + 2026-04-03 weekly 0.88 https://dociva.io/ar/ุชุญูˆูŠู„-pdf-ุงู„ู‰-ุตูˆุฑ - 2026-04-01 + 2026-04-03 weekly 0.8 https://dociva.io/best-pdf-tools - 2026-04-01 + 2026-04-03 weekly 0.82 https://dociva.io/ar/best-pdf-tools - 2026-04-01 + 2026-04-03 weekly 0.74 https://dociva.io/free-pdf-tools-online - 2026-04-01 + 2026-04-03 weekly 0.82 https://dociva.io/ar/free-pdf-tools-online - 2026-04-01 + 2026-04-03 weekly 0.74 https://dociva.io/convert-files-online - 2026-04-01 + 2026-04-03 weekly 0.82 https://dociva.io/ar/convert-files-online - 2026-04-01 + 2026-04-03 weekly 0.74 https://dociva.io/pdf-converter-tools - 2026-04-01 + 2026-04-03 weekly 0.82 https://dociva.io/ar/pdf-converter-tools - 2026-04-01 + 2026-04-03 weekly 0.74 https://dociva.io/secure-pdf-tools - 2026-04-01 + 2026-04-03 weekly 0.82 https://dociva.io/ar/secure-pdf-tools - 2026-04-01 + 2026-04-03 weekly 0.74 https://dociva.io/ai-document-tools - 2026-04-01 + 2026-04-03 weekly 0.82 https://dociva.io/ar/ai-document-tools - 2026-04-01 + 2026-04-03 weekly 0.74 https://dociva.io/image-to-pdf-tools - 2026-04-01 + 2026-04-03 weekly 0.82 https://dociva.io/ar/image-to-pdf-tools - 2026-04-01 + 2026-04-03 weekly 0.74 https://dociva.io/online-image-tools - 2026-04-01 + 2026-04-03 weekly 0.82 https://dociva.io/ar/online-image-tools - 2026-04-01 + 2026-04-03 weekly 0.74 https://dociva.io/office-to-pdf-tools - 2026-04-01 + 2026-04-03 weekly 0.82 https://dociva.io/ar/office-to-pdf-tools - 2026-04-01 + 2026-04-03 weekly 0.74 https://dociva.io/scanned-document-tools - 2026-04-01 + 2026-04-03 weekly 0.82 https://dociva.io/ar/scanned-document-tools - 2026-04-01 + 2026-04-03 weekly 0.74 https://dociva.io/arabic-pdf-tools - 2026-04-01 + 2026-04-03 weekly 0.82 https://dociva.io/ar/arabic-pdf-tools - 2026-04-01 + 2026-04-03 weekly 0.74 diff --git a/frontend/public/sitemaps/static.xml b/frontend/public/sitemaps/static.xml index a168dd8..4b2cfaa 100644 --- a/frontend/public/sitemaps/static.xml +++ b/frontend/public/sitemaps/static.xml @@ -2,55 +2,61 @@ https://dociva.io/ - 2026-04-01 + 2026-04-03 daily 1.0 https://dociva.io/tools - 2026-04-01 + 2026-04-03 weekly 0.8 https://dociva.io/about - 2026-04-01 + 2026-04-03 monthly 0.4 https://dociva.io/contact - 2026-04-01 + 2026-04-03 monthly 0.4 https://dociva.io/privacy - 2026-04-01 + 2026-04-03 yearly 0.3 https://dociva.io/terms - 2026-04-01 + 2026-04-03 yearly 0.3 https://dociva.io/pricing - 2026-04-01 + 2026-04-03 + monthly + 0.7 + + + https://dociva.io/pricing-transparency + 2026-04-03 monthly 0.7 https://dociva.io/blog - 2026-04-01 + 2026-04-03 weekly 0.6 https://dociva.io/developers - 2026-04-01 + 2026-04-03 monthly 0.5 diff --git a/frontend/public/sitemaps/tools.xml b/frontend/public/sitemaps/tools.xml index cd41531..97fd210 100644 --- a/frontend/public/sitemaps/tools.xml +++ b/frontend/public/sitemaps/tools.xml @@ -1,267 +1,4 @@ - - https://dociva.io/tools/pdf-to-word - 2026-04-01 - weekly - 0.9 - - - https://dociva.io/tools/word-to-pdf - 2026-04-01 - weekly - 0.9 - - - https://dociva.io/tools/compress-pdf - 2026-04-01 - weekly - 0.9 - - - https://dociva.io/tools/merge-pdf - 2026-04-01 - weekly - 0.9 - - - https://dociva.io/tools/split-pdf - 2026-04-01 - weekly - 0.8 - - - https://dociva.io/tools/rotate-pdf - 2026-04-01 - weekly - 0.7 - - - https://dociva.io/tools/pdf-to-images - 2026-04-01 - weekly - 0.8 - - - https://dociva.io/tools/images-to-pdf - 2026-04-01 - weekly - 0.8 - - - https://dociva.io/tools/watermark-pdf - 2026-04-01 - weekly - 0.7 - - - https://dociva.io/tools/protect-pdf - 2026-04-01 - weekly - 0.8 - - - https://dociva.io/tools/unlock-pdf - 2026-04-01 - weekly - 0.8 - - - https://dociva.io/tools/page-numbers - 2026-04-01 - weekly - 0.7 - - - https://dociva.io/tools/pdf-editor - 2026-04-01 - weekly - 0.8 - - - https://dociva.io/tools/pdf-flowchart - 2026-04-01 - weekly - 0.7 - - - https://dociva.io/tools/pdf-to-excel - 2026-04-01 - weekly - 0.8 - - - https://dociva.io/tools/remove-watermark-pdf - 2026-04-01 - weekly - 0.7 - - - https://dociva.io/tools/reorder-pdf - 2026-04-01 - weekly - 0.7 - - - https://dociva.io/tools/extract-pages - 2026-04-01 - weekly - 0.7 - - - https://dociva.io/tools/image-converter - 2026-04-01 - weekly - 0.8 - - - https://dociva.io/tools/image-resize - 2026-04-01 - weekly - 0.8 - - - https://dociva.io/tools/compress-image - 2026-04-01 - weekly - 0.8 - - - https://dociva.io/tools/ocr - 2026-04-01 - weekly - 0.8 - - - https://dociva.io/tools/remove-background - 2026-04-01 - weekly - 0.8 - - - https://dociva.io/tools/image-to-svg - 2026-04-01 - weekly - 0.8 - - - https://dociva.io/tools/html-to-pdf - 2026-04-01 - weekly - 0.7 - - - https://dociva.io/tools/chat-pdf - 2026-04-01 - weekly - 0.8 - - - https://dociva.io/tools/summarize-pdf - 2026-04-01 - weekly - 0.8 - - - https://dociva.io/tools/translate-pdf - 2026-04-01 - weekly - 0.8 - - - https://dociva.io/tools/extract-tables - 2026-04-01 - weekly - 0.8 - - - https://dociva.io/tools/qr-code - 2026-04-01 - weekly - 0.7 - - - https://dociva.io/tools/video-to-gif - 2026-04-01 - weekly - 0.7 - - - https://dociva.io/tools/word-counter - 2026-04-01 - weekly - 0.6 - - - https://dociva.io/tools/text-cleaner - 2026-04-01 - weekly - 0.6 - - - https://dociva.io/tools/pdf-to-pptx - 2026-04-01 - weekly - 0.8 - - - https://dociva.io/tools/excel-to-pdf - 2026-04-01 - weekly - 0.8 - - - https://dociva.io/tools/pptx-to-pdf - 2026-04-01 - weekly - 0.8 - - - https://dociva.io/tools/sign-pdf - 2026-04-01 - weekly - 0.8 - - - https://dociva.io/tools/crop-pdf - 2026-04-01 - weekly - 0.7 - - - https://dociva.io/tools/flatten-pdf - 2026-04-01 - weekly - 0.7 - - - https://dociva.io/tools/repair-pdf - 2026-04-01 - weekly - 0.7 - - - https://dociva.io/tools/pdf-metadata - 2026-04-01 - weekly - 0.6 - - - https://dociva.io/tools/image-crop - 2026-04-01 - weekly - 0.7 - - - https://dociva.io/tools/image-rotate-flip - 2026-04-01 - weekly - 0.7 - - - https://dociva.io/tools/barcode-generator - 2026-04-01 - weekly - 0.7 - + diff --git a/frontend/scripts/submit-indexnow.mjs b/frontend/scripts/submit-indexnow.mjs new file mode 100644 index 0000000..f61a83c --- /dev/null +++ b/frontend/scripts/submit-indexnow.mjs @@ -0,0 +1,311 @@ +import { access, mkdir, readdir, readFile, writeFile } from 'node:fs/promises'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const frontendRoot = path.resolve(__dirname, '..'); +const publicDir = path.join(frontendRoot, 'public'); +const distDir = path.join(frontendRoot, 'dist'); +const stateDir = path.resolve(process.env.INDEXNOW_STATE_DIR || path.join(frontendRoot, '.indexnow')); +const stateFile = path.join(stateDir, 'last-submission.json'); +const defaultEndpoint = 'https://www.bing.com/indexnow'; +const keyPattern = /^[A-Za-z0-9-]{8,128}$/; +const dryRun = process.argv.includes('--dry-run') || process.env.INDEXNOW_DRY_RUN === 'true'; +const forceFullSubmit = process.argv.includes('--full') || process.env.INDEXNOW_FULL_SUBMIT === 'true'; +const strictMode = process.env.INDEXNOW_STRICT === 'true'; +const isDirectRun = process.argv[1] ? path.resolve(process.argv[1]) === __filename : false; + +function normalizeOrigin(rawOrigin) { + const normalized = String(rawOrigin || 'https://dociva.io').trim().replace(/\/$/, ''); + return new URL(normalized); +} + +function normalizeEndpoint(rawEndpoint) { + const endpointUrl = new URL(String(rawEndpoint || defaultEndpoint).trim()); + + if (endpointUrl.pathname === '/' || !endpointUrl.pathname) { + endpointUrl.pathname = '/indexnow'; + } + + return endpointUrl.toString(); +} + +async function pathExists(filePath) { + try { + await access(filePath); + return true; + } catch { + return false; + } +} + +async function writeJsonFile(filePath, value) { + await mkdir(path.dirname(filePath), { recursive: true }); + await writeFile(filePath, `${JSON.stringify(value, null, 2)}\n`, 'utf8'); +} + +async function ensureKeyFile(key) { + const keyFileName = `${key}.txt`; + const targets = [publicDir, distDir]; + + for (const targetDir of targets) { + if (!(await pathExists(targetDir))) { + continue; + } + + await mkdir(targetDir, { recursive: true }); + await writeFile(path.join(targetDir, keyFileName), key, 'utf8'); + } +} + +async function resolveKey() { + const envKey = String(process.env.INDEXNOW_KEY || '').trim(); + if (envKey) { + if (!keyPattern.test(envKey)) { + throw new Error('INDEXNOW_KEY is not a valid IndexNow key.'); + } + + await ensureKeyFile(envKey); + return envKey; + } + + for (const baseDir of [distDir, publicDir]) { + if (!(await pathExists(baseDir))) { + continue; + } + + const entries = await readdir(baseDir); + for (const entry of entries.sort()) { + if (!entry.endsWith('.txt')) { + continue; + } + + const candidateKey = entry.slice(0, -4); + if (!keyPattern.test(candidateKey)) { + continue; + } + + const contents = (await readFile(path.join(baseDir, entry), 'utf8')).trim(); + if (contents === candidateKey) { + return candidateKey; + } + } + } + + return ''; +} + +function extractLocs(xml) { + return [...xml.matchAll(/([^<]+)<\/loc>/g)].map((match) => match[1].trim()); +} + +async function collectSitemapFiles() { + const sitemapFiles = []; + + for (const baseDir of [distDir, publicDir]) { + const nestedSitemapDir = path.join(baseDir, 'sitemaps'); + if (await pathExists(nestedSitemapDir)) { + const entries = await readdir(nestedSitemapDir); + for (const entry of entries.sort()) { + if (entry.endsWith('.xml')) { + sitemapFiles.push(path.join(nestedSitemapDir, entry)); + } + } + } + + const rootSitemap = path.join(baseDir, 'sitemap.xml'); + if (await pathExists(rootSitemap)) { + sitemapFiles.push(rootSitemap); + } + + if (sitemapFiles.length > 0) { + break; + } + } + + return sitemapFiles; +} + +async function collectUrls(siteOrigin) { + const urls = new Set(); + const sitemapFiles = await collectSitemapFiles(); + + for (const sitemapFile of sitemapFiles) { + const xml = await readFile(sitemapFile, 'utf8'); + + for (const loc of extractLocs(xml)) { + let parsedUrl; + + try { + parsedUrl = new URL(loc); + } catch { + continue; + } + + if (parsedUrl.host !== siteOrigin.host) { + continue; + } + + if (parsedUrl.pathname.endsWith('.xml')) { + continue; + } + + urls.add(parsedUrl.toString()); + } + } + + return [...urls].sort(); +} + +function chunkUrls(urlList, chunkSize) { + const chunks = []; + + for (let index = 0; index < urlList.length; index += chunkSize) { + chunks.push(urlList.slice(index, index + chunkSize)); + } + + return chunks; +} + +async function loadPreviousUrls() { + if (!(await pathExists(stateFile))) { + return []; + } + + try { + const payload = JSON.parse(await readFile(stateFile, 'utf8')); + if (!Array.isArray(payload.urls)) { + return []; + } + + return payload.urls.filter((url) => typeof url === 'string'); + } catch { + return []; + } +} + +function diffUrlLists(currentUrls, previousUrls, submitAll = false) { + if (submitAll || previousUrls.length === 0) { + return [...new Set(currentUrls)].sort(); + } + + const currentSet = new Set(currentUrls); + const previousSet = new Set(previousUrls); + const changedUrls = new Set(); + + for (const url of currentSet) { + if (!previousSet.has(url)) { + changedUrls.add(url); + } + } + + for (const url of previousSet) { + if (!currentSet.has(url)) { + changedUrls.add(url); + } + } + + return [...changedUrls].sort(); +} + +async function persistSubmittedUrls(currentUrls) { + if (dryRun) { + return false; + } + + await writeJsonFile(stateFile, { + updatedAt: new Date().toISOString(), + urls: [...new Set(currentUrls)].sort(), + }); + + return true; +} + +async function submitBatch(endpoint, payload, batchIndex, totalBatches) { + if (dryRun) { + console.log(`Dry run: batch ${batchIndex}/${totalBatches} -> ${payload.urlList.length} URLs`); + console.log(JSON.stringify(payload, null, 2)); + return; + } + + const response = await fetch(endpoint, { + method: 'POST', + headers: { + 'Content-Type': 'application/json; charset=utf-8', + }, + body: JSON.stringify(payload), + }); + + const responseText = (await response.text()).trim(); + if (!response.ok) { + throw new Error( + `IndexNow request failed with ${response.status}${responseText ? `: ${responseText}` : ''}`, + ); + } + + console.log( + `Submitted IndexNow batch ${batchIndex}/${totalBatches} with ${payload.urlList.length} URLs (${response.status}).`, + ); +} + +async function main() { + const siteOrigin = normalizeOrigin(process.env.VITE_SITE_DOMAIN || process.env.SITE_DOMAIN); + const endpoint = normalizeEndpoint(process.env.INDEXNOW_ENDPOINT); + const key = await resolveKey(); + + if (!key) { + console.log('Skipping IndexNow submission: no verification key was found.'); + return; + } + + const currentUrls = await collectUrls(siteOrigin); + if (currentUrls.length === 0) { + console.log('Skipping IndexNow submission: no sitemap URLs were found.'); + return; + } + + const previousUrls = await loadPreviousUrls(); + const urlList = diffUrlLists(currentUrls, previousUrls, forceFullSubmit); + if (urlList.length === 0) { + console.log('Skipping IndexNow submission: no changed URLs were detected since the previous successful submission.'); + return; + } + + const keyLocation = `${siteOrigin.origin}/${key}.txt`; + const payloads = chunkUrls(urlList, 10000).map((chunk) => ({ + host: siteOrigin.host, + key, + keyLocation, + urlList: chunk, + })); + + console.log( + `${dryRun ? 'Preparing' : 'Submitting'} ${urlList.length} URLs to ${endpoint} using ${keyLocation}.`, + ); + + for (const [index, payload] of payloads.entries()) { + await submitBatch(endpoint, payload, index + 1, payloads.length); + } + + if (await persistSubmittedUrls(currentUrls)) { + console.log(`Saved IndexNow state snapshot to ${stateFile}.`); + } +} + +if (isDirectRun) { + main().catch((error) => { + console.error(`IndexNow submission failed: ${error.message}`); + if (strictMode) { + process.exitCode = 1; + } + }); +} + +export { + diffUrlLists, + extractLocs, + main, + normalizeEndpoint, + normalizeOrigin, +}; \ No newline at end of file diff --git a/frontend/scripts/submit-indexnow.test.mjs b/frontend/scripts/submit-indexnow.test.mjs new file mode 100644 index 0000000..cd89d92 --- /dev/null +++ b/frontend/scripts/submit-indexnow.test.mjs @@ -0,0 +1,40 @@ +import { describe, expect, it } from 'vitest'; + +import { + diffUrlLists, + extractLocs, + normalizeEndpoint, + normalizeOrigin, +} from './submit-indexnow.mjs'; + +describe('submit-indexnow helpers', () => { + it('normalizes endpoints to the /indexnow path', () => { + expect(normalizeEndpoint('https://www.bing.com')).toBe('https://www.bing.com/indexnow'); + expect(normalizeEndpoint('https://www.bing.com/indexnow')).toBe('https://www.bing.com/indexnow'); + }); + + it('normalizes origins without a trailing slash', () => { + expect(normalizeOrigin('https://dociva.io/').toString()).toBe('https://dociva.io/'); + }); + + it('extracts loc entries from sitemap xml', () => { + const xml = `\n\n https://dociva.io/a\n https://dociva.io/b\n`; + expect(extractLocs(xml)).toEqual(['https://dociva.io/a', 'https://dociva.io/b']); + }); + + it('returns the full set on the first submission', () => { + expect(diffUrlLists(['https://dociva.io/a', 'https://dociva.io/b'], [])).toEqual([ + 'https://dociva.io/a', + 'https://dociva.io/b', + ]); + }); + + it('returns only added and removed urls after the first submission', () => { + expect( + diffUrlLists( + ['https://dociva.io/a', 'https://dociva.io/c'], + ['https://dociva.io/a', 'https://dociva.io/b'], + ), + ).toEqual(['https://dociva.io/b', 'https://dociva.io/c']); + }); +}); \ No newline at end of file diff --git a/frontend/src/components/layout/Header.tsx b/frontend/src/components/layout/Header.tsx index a61c5b3..e9421cb 100644 --- a/frontend/src/components/layout/Header.tsx +++ b/frontend/src/components/layout/Header.tsx @@ -1,7 +1,7 @@ import { useState, useEffect, useRef } from 'react'; import { Link } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; -import { FileText, Moon, Sun, Menu, X, ChevronDown, UserRound, Coins } from 'lucide-react'; +import { FileText, Moon, Sun, Menu, X, ChevronDown, UserRound, Coins, ArrowRight } from 'lucide-react'; import { useAuthStore } from '@/stores/authStore'; import { ensureLanguageResources } from '@/i18n'; interface LangOption { @@ -67,49 +67,54 @@ export default function Header() { }; return ( -
+
{/* Logo */} - - - {t('common.appName')} + +
+ +
+ + {t('common.appName')} + {/* Desktop Navigation */} - )}
diff --git a/frontend/src/components/shared/HeroUploadZone.tsx b/frontend/src/components/shared/HeroUploadZone.tsx index 7aae255..979df44 100644 --- a/frontend/src/components/shared/HeroUploadZone.tsx +++ b/frontend/src/components/shared/HeroUploadZone.tsx @@ -2,7 +2,7 @@ import { useState, useCallback } from 'react'; import { useDropzone } from 'react-dropzone'; import { useNavigate } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; -import { Upload, Sparkles, PenLine } from 'lucide-react'; +import { UploadCloud, PenLine, ChevronRight, FileCheck } from 'lucide-react'; import ToolSelectorModal from '@/components/shared/ToolSelectorModal'; import { useFileStore } from '@/stores/fileStore'; import { getToolsForFile, detectFileCategory, getCategoryLabel } from '@/utils/fileRouting'; @@ -24,6 +24,15 @@ const ACCEPTED_TYPES = { 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': ['.docx'], }; +const FORMAT_BADGES = [ + { label: 'PDF', color: 'bg-red-50 text-red-700 ring-red-100 dark:bg-red-900/20 dark:text-red-400 dark:ring-red-800/40' }, + { label: 'Word', color: 'bg-blue-50 text-blue-700 ring-blue-100 dark:bg-blue-900/20 dark:text-blue-400 dark:ring-blue-800/40' }, + { label: 'JPG', color: 'bg-amber-50 text-amber-700 ring-amber-100 dark:bg-amber-900/20 dark:text-amber-400 dark:ring-amber-800/40' }, + { label: 'PNG', color: 'bg-green-50 text-green-700 ring-green-100 dark:bg-green-900/20 dark:text-green-400 dark:ring-green-800/40' }, + { label: 'WebP', color: 'bg-violet-50 text-violet-700 ring-violet-100 dark:bg-violet-900/20 dark:text-violet-400 dark:ring-violet-800/40' }, + { label: 'MP4', color: 'bg-slate-100 text-slate-600 ring-slate-200 dark:bg-slate-700 dark:text-slate-300 dark:ring-slate-600' }, +]; + export default function HeroUploadZone() { const { t } = useTranslation(); const navigate = useNavigate(); @@ -81,37 +90,49 @@ export default function HeroUploadZone() { setMatchedTools([]); }, []); + const iconGlowClass = isDragActive + ? 'bg-primary-300/50 scale-125' + : 'bg-primary-100/0 group-hover:bg-primary-200/40 group-hover:scale-110'; + const iconContainerClass = isDragActive + ? 'bg-primary-100 shadow-primary-200 dark:bg-primary-900/50' + : 'bg-primary-50 shadow-sm dark:bg-slate-700/80'; + const uploadIconClass = isDragActive + ? 'text-primary-600 dark:text-primary-400' + : 'text-primary-400 group-hover:text-primary-600 dark:text-primary-500 dark:group-hover:text-primary-400'; + return ( <>
- {/* Icon */} -
- + {/* Cloud icon with animated ring */} +
+ {/* Outer glow ring */} +
+
+ +
- {/* CTA Text */} -
+ {/* Heading */} +

+ {isDragActive + ? t('home.dropFileHere', 'Drop your file hereโ€ฆ') + : t('home.dragDropTitle', 'Drag & drop your file here')} +

+

+ {t('common.dragDrop', 'or click the button to browse from your device')} +

+ + {/* CTA Buttons */} +
-

- {t('common.dragDrop', 'or drop files here')} -

+ {/* Divider */} +
+
+ + {t('home.supportedFormats', 'Supported formats')} + +
+
- {/* Supported formats */} + {/* Coloured format badges */}
- {['PDF', 'Word', 'JPG', 'PNG', 'WebP', 'MP4'].map((format) => ( + {FORMAT_BADGES.map(({ label, color }) => ( - {format} + {label} ))}
- {/* File size hint */} -

- + {/* Size hint */} +

+ {t('home.uploadSubtitle')}

{/* Error */} {error && ( -
+

{error}

)} diff --git a/frontend/src/components/shared/ToolCard.tsx b/frontend/src/components/shared/ToolCard.tsx index 2ad1406..03a0a83 100644 --- a/frontend/src/components/shared/ToolCard.tsx +++ b/frontend/src/components/shared/ToolCard.tsx @@ -1,5 +1,6 @@ import { Link } from 'react-router-dom'; import type { ReactNode } from 'react'; +import { ArrowRight } from 'lucide-react'; interface ToolCardProps { /** Tool route path */ @@ -23,18 +24,28 @@ export default function ToolCard({ }: ToolCardProps) { return ( -
-
-
- {icon} +
+ {/* Top color accent bar โ€” slides in on hover */} +
+ +
+ {/* Icon + title */} +
+
+ {icon} +
+

+ {title} +

-

- {title} -

+ + {/* Arrow indicator */} +
-

+ +

{description}

diff --git a/frontend/src/pages/HomePage.tsx b/frontend/src/pages/HomePage.tsx index 522aa53..9546fc1 100644 --- a/frontend/src/pages/HomePage.tsx +++ b/frontend/src/pages/HomePage.tsx @@ -1,6 +1,7 @@ import { useDeferredValue } from 'react'; import { useTranslation } from 'react-i18next'; import { useSearchParams } from 'react-router-dom'; +import { Link } from 'react-router-dom'; import SEOHead from '@/components/seo/SEOHead'; import { generateOrganization, generateWebSite, getSiteOrigin } from '@/utils/seo'; import { @@ -38,6 +39,15 @@ import { Wrench, Presentation, Barcode, + ShieldCheck, + Zap, + Globe, + UploadCloud, + MousePointerClick, + Download, + ArrowRight, + Star, + CheckCircle2, } from 'lucide-react'; import ToolCard from '@/components/shared/ToolCard'; import HeroUploadZone from '@/components/shared/HeroUploadZone'; @@ -78,6 +88,39 @@ function manifestToToolInfo(tools: readonly ToolEntry[]): ToolInfo[] { const pdfTools: ToolInfo[] = manifestToToolInfo(getHomepageTools('pdf')); const otherTools: ToolInfo[] = manifestToToolInfo(getHomepageTools('other')); +const HOW_IT_WORKS = [ + { + step: '01', + icon: UploadCloud, + titleKey: 'home.howStep1Title', + titleDefault: 'Upload your file', + descKey: 'home.howStep1Desc', + descDefault: 'Drag & drop or click to select. PDF, Word, images and more โ€” up to 200 MB.', + color: 'bg-blue-600', + glow: 'shadow-blue-200 dark:shadow-blue-900/40', + }, + { + step: '02', + icon: MousePointerClick, + titleKey: 'home.howStep2Title', + titleDefault: 'Choose a tool', + descKey: 'home.howStep2Desc', + descDefault: 'We detect your file type and suggest the best tools automatically.', + color: 'bg-violet-600', + glow: 'shadow-violet-200 dark:shadow-violet-900/40', + }, + { + step: '03', + icon: Download, + titleKey: 'home.howStep3Title', + titleDefault: 'Download instantly', + descKey: 'home.howStep3Desc', + descDefault: 'Your file is ready in seconds. No account needed โ€” files are auto-deleted.', + color: 'bg-emerald-600', + glow: 'shadow-emerald-200 dark:shadow-emerald-900/40', + }, +]; + export default function HomePage() { const { t } = useTranslation(); const siteOrigin = getSiteOrigin(typeof window !== 'undefined' ? window.location.origin : ''); @@ -122,33 +165,103 @@ export default function HomePage() { ]} /> - {/* Hero Section */} -
-
-

+ {/* โ”€โ”€ Hero Section โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */} +
+ {/* Decorative blobs */} +
+
+ +
+ {/* Animated badge */} +
+ + + {t('home.heroBadge', 'Free Online PDF & File Tools')} + +
+ +

{t('home.hero')}

-

+

{t('home.heroSub')}

+ {/* Trust strip */} +
+ {[ + { icon: ShieldCheck, text: t('home.trustNoSignup', 'No sign-up needed') }, + { icon: Zap, text: t('home.trustFast', 'Results in seconds') }, + { icon: Lock, text: t('home.trustSecure', 'Files auto-deleted') }, + { icon: Globe, text: t('home.trust30Tools', '30+ free tools') }, + ].map(({ icon: Icon, text }) => ( +
+ + {text} +
+ ))} +
+ {/* Smart Upload Zone */}
- {/* Ad Slot */} + {/* โ”€โ”€ Ad Slot โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */} + {/* โ”€โ”€ Social Proof Strip โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */} -
+ {/* โ”€โ”€ How It Works โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */} +
+
+

+ {t('home.howItWorksLabel', 'Simple process')} +

+

+ {t('home.howItWorksTitle', 'Convert & edit in 3 steps')} +

+

+ {t('home.howItWorksSubtitle', 'No account, no installation, no waiting. Just upload, choose a tool, and download.')} +

+
+ +
+ {HOW_IT_WORKS.map(({ step, icon: Icon, titleKey, titleDefault, descKey, descDefault, color, glow }, idx) => ( +
+ {/* Connector line (between steps, hidden on mobile) */} + {idx < HOW_IT_WORKS.length - 1 && ( +
+ )} +
+ {/* Numbered icon */} +
+ + + {parseInt(step, 10)} + +
+

+ {t(titleKey, titleDefault)} +

+

+ {t(descKey, descDefault)} +

+
+
+ ))} +
+
+ + {/* โ”€โ”€ Search & Tools โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */} +

{t('common.search')}

-

+

{t('home.searchToolsPlaceholder')}

@@ -176,41 +289,17 @@ export default function HomePage() {
-
-
-
-

- {t('common.developers')} -

-

- {t('pages.developers.ctaTitle')} -

-

- {t('pages.developers.ctaSubtitle')} -

-
- + {/* โ”€โ”€ PDF Tools Grid โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */} +
+
+

+ {t('home.pdfTools')} +

+ + {t('common.allTools')} + +
-
- - {/* Tools Grid */} -
-

- {t('home.pdfTools')} -

{filteredPdfTools.map((tool) => ( -

+

{t('home.otherTools', 'Other Tools')}

@@ -249,49 +338,149 @@ export default function HomePage() { )}
- {/* Features / Why Choose Us */} -
-

- {t('home.featuresTitle', 'A smarter way to convert and edit online')} -

-
-
-
- + {/* โ”€โ”€ Features / Why Choose Us โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */} +
+
+

+ {t('home.whyChooseLabel', 'Why Dociva')} +

+

+ {t('home.featuresTitle', 'A smarter way to work with files')} +

+
+ +
+ {[ + { + icon: Layers, + bg: 'bg-blue-100 dark:bg-blue-900/30', + color: 'text-blue-600 dark:text-blue-400', + titleKey: 'home.feature1Title', + titleDefault: 'One complete workspace', + descKey: 'home.feature1Desc', + descDefault: 'Edit, convert, compress, merge, split โ€” without switching tabs.', + perks: [ + t('home.feature1Perk1', '30+ tools in one place'), + t('home.feature1Perk2', 'PDF, image & video support'), + ], + }, + { + icon: CheckCircle2, + bg: 'bg-emerald-100 dark:bg-emerald-900/30', + color: 'text-emerald-600 dark:text-emerald-400', + titleKey: 'home.feature2Title', + titleDefault: 'Accuracy you can trust', + descKey: 'home.feature2Desc', + descDefault: 'Pixel-perfect, editable output in seconds with zero quality loss.', + perks: [ + t('home.feature2Perk1', 'Preserve fonts & layouts'), + t('home.feature2Perk2', 'Batch-tested quality'), + ], + }, + { + icon: ShieldCheck, + bg: 'bg-violet-100 dark:bg-violet-900/30', + color: 'text-violet-600 dark:text-violet-400', + titleKey: 'home.feature3Title', + titleDefault: 'Built-in security', + descKey: 'home.feature3Desc', + descDefault: 'Files are automatically deleted after processing. No account required.', + perks: [ + t('home.feature3Perk1', 'Auto-deletion after 1 hour'), + t('home.feature3Perk2', 'Encrypted transfers'), + ], + }, + ].map(({ icon: Icon, bg, color, titleKey, titleDefault, descKey, descDefault, perks }) => ( +
+
+ +
+

+ {t(titleKey, titleDefault)} +

+

+ {t(descKey, descDefault)} +

+
    + {perks.map((perk) => ( +
  • + + {perk} +
  • + ))} +
-

- {t('home.feature1Title', 'One complete workspace')} -

-

- {t('home.feature1Desc', 'Edit, convert, compress, merge, split without switching tabs.')} + ))} +

+
+ + {/* โ”€โ”€ Developer API Banner โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */} +
+
+
+

+ {t('common.developers')} +

+

+ {t('pages.developers.ctaTitle')} +

+

+ {t('pages.developers.ctaSubtitle')}

-
-
- 100% -
-

- {t('home.feature2Title', 'Accuracy you can trust')} -

-

- {t('home.feature2Desc', 'Get pixel-perfect, editable files in seconds with zero quality loss.')} -

-
-
-
- -
-

- {t('home.feature3Title', 'Built-in security')} -

-

- {t('home.feature3Desc', 'Access files securely, protected by automatic encryption.')} -

+
+ + {t('pages.developers.openDocs')} + + + + {t('pages.developers.getApiKey')} +
- {/* Ad Slot - Bottom */} + {/* โ”€โ”€ Bottom CTA Banner โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */} +
+ {/* Decorative blobs */} +
+
+ +
+

+ {t('home.ctaBannerLabel', 'Get started today')} +

+

+ {t('home.ctaBannerTitle', 'Ready to convert your files?')} +

+

+ {t('home.ctaBannerSubtitle', 'Join thousands of users who convert, compress, and edit their files every day โ€” completely free.')} +

+
+ + {t('home.ctaBrowseTools', 'Browse All Tools')} + + + + {t('home.ctaCreateAccount', 'Create Free Account')} + +
+
+
+ + {/* โ”€โ”€ Ad Slot - Bottom โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */} ); diff --git a/frontend/src/styles/global.css b/frontend/src/styles/global.css index 6b06ffe..86edbc0 100644 --- a/frontend/src/styles/global.css +++ b/frontend/src/styles/global.css @@ -114,17 +114,84 @@ animation: fadeSlideIn 0.15s ease-out; } -/* Hero upload zone โ€” larger variant for the homepage */ +/* โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + Hero Upload Zone โ€” premium glassmorphism card for the homepage + โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ .hero-upload-zone { - @apply flex flex-col items-center justify-center rounded-2xl border-2 border-dashed border-slate-300 bg-gradient-to-b from-slate-50 to-white p-10 text-center transition-all duration-200 cursor-pointer sm:p-12 dark:border-slate-600 dark:from-slate-800/60 dark:to-slate-800/30; + @apply relative flex flex-col items-center justify-center rounded-3xl border border-slate-200/80 bg-white/80 backdrop-blur-sm p-10 text-center transition-all duration-300 ease-in-out cursor-pointer sm:p-14 shadow-sm dark:border-slate-700/60 dark:bg-slate-800/60 dark:backdrop-blur-sm; + background-image: radial-gradient(ellipse at top, rgba(219, 234, 254, 0.3) 0%, transparent 70%); +} + +.dark .hero-upload-zone { + background-image: radial-gradient(ellipse at top, rgba(30, 58, 138, 0.15) 0%, transparent 70%); +} + +.hero-upload-zone::before { + content: ''; + @apply absolute inset-0 rounded-3xl transition-opacity duration-300 opacity-0; + background: linear-gradient(135deg, rgba(59, 130, 246, 0.06) 0%, rgba(168, 85, 247, 0.04) 100%); +} + +.hero-upload-zone:hover::before { + @apply opacity-100; } .hero-upload-zone:hover { - @apply border-primary-400 bg-gradient-to-b from-primary-50 to-white shadow-lg dark:border-primary-500 dark:from-primary-900/20 dark:to-slate-800/30; + @apply border-primary-300 shadow-lg shadow-primary-100/50 -translate-y-1 dark:border-primary-600/60 dark:shadow-primary-900/30; } .hero-upload-zone.drag-active { - @apply border-primary-500 bg-gradient-to-b from-primary-100 to-primary-50 ring-2 ring-primary-300 shadow-xl dark:border-primary-400 dark:from-primary-900/30 dark:to-primary-900/10 dark:ring-primary-600; + @apply border-primary-500 shadow-2xl shadow-primary-200/60 scale-[1.02] dark:border-primary-400 dark:shadow-primary-900/40; + background-image: radial-gradient(ellipse at top, rgba(191, 219, 254, 0.5) 0%, rgba(219, 234, 254, 0.2) 100%); +} + +/* โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + Glassmorphism card utility + โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ +.glass-card { + @apply bg-white/70 backdrop-blur-md border border-white/50 shadow-sm dark:bg-slate-800/60 dark:border-slate-700/50; +} + +/* โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + Gradient hero mesh background + โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ +.hero-gradient-bg { + background: + radial-gradient(ellipse 80% 60% at 50% -20%, rgba(59, 130, 246, 0.12) 0%, transparent 70%), + radial-gradient(ellipse 60% 40% at 80% 20%, rgba(168, 85, 247, 0.06) 0%, transparent 60%), + linear-gradient(180deg, #f8fafc 0%, #ffffff 100%); +} + +.dark .hero-gradient-bg { + background: + radial-gradient(ellipse 80% 60% at 50% -20%, rgba(30, 58, 138, 0.3) 0%, transparent 70%), + radial-gradient(ellipse 60% 40% at 80% 20%, rgba(88, 28, 135, 0.15) 0%, transparent 60%), + linear-gradient(180deg, #0f172a 0%, #0f172a 100%); +} + +/* โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + Shimmer loading effect + โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ +@keyframes shimmer-sweep { + 0% { background-position: -200% center; } + 100% { background-position: 200% center; } +} + +.shimmer-text { + background: linear-gradient(90deg, #1e40af 30%, #7c3aed 50%, #1e40af 70%); + background-size: 200% auto; + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + animation: shimmer-sweep 4s linear infinite; +} + +/* โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + How it Works โ€” connector line between steps + โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ +.step-connector { + @apply absolute top-8 left-[calc(50%+2.5rem)] hidden h-px w-[calc(100%-5rem)] sm:block; + background: linear-gradient(90deg, rgba(59, 130, 246, 0.4) 0%, rgba(59, 130, 246, 0.1) 100%); } /* Modal animations */ diff --git a/frontend/tailwind.config.js b/frontend/tailwind.config.js index 5f12e73..1f9cc16 100644 --- a/frontend/tailwind.config.js +++ b/frontend/tailwind.config.js @@ -38,6 +38,51 @@ export default { sans: ['Inter', 'Tajawal', 'system-ui', 'sans-serif'], arabic: ['Tajawal', 'Inter', 'sans-serif'], }, + borderRadius: { + '3xl': '1.5rem', + '4xl': '2rem', + }, + boxShadow: { + 'glow': '0 0 20px -4px rgba(59, 130, 246, 0.4)', + 'glow-lg': '0 0 40px -8px rgba(59, 130, 246, 0.5)', + }, + transitionTimingFunction: { + 'smooth': 'cubic-bezier(0.4, 0, 0.2, 1)', + }, + animation: { + 'float': 'float 3s ease-in-out infinite', + 'shimmer': 'shimmer 2.5s linear infinite', + 'fade-up': 'fadeUp 0.5s ease-out forwards', + 'fade-in': 'fadeIn 0.4s ease-out forwards', + 'scale-in': 'scaleIn 0.3s ease-out forwards', + 'pulse-slow': 'pulse 3s cubic-bezier(0.4, 0, 0.6, 1) infinite', + }, + keyframes: { + float: { + '0%, 100%': { transform: 'translateY(0px)' }, + '50%': { transform: 'translateY(-10px)' }, + }, + shimmer: { + '0%': { backgroundPosition: '-200% center' }, + '100%': { backgroundPosition: '200% center' }, + }, + fadeUp: { + '0%': { opacity: '0', transform: 'translateY(24px)' }, + '100%': { opacity: '1', transform: 'translateY(0)' }, + }, + fadeIn: { + '0%': { opacity: '0' }, + '100%': { opacity: '1' }, + }, + scaleIn: { + '0%': { opacity: '0', transform: 'scale(0.92)' }, + '100%': { opacity: '1', transform: 'scale(1)' }, + }, + }, + backgroundImage: { + 'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))', + 'gradient-conic': 'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))', + }, }, }, plugins: [], diff --git a/scripts/deploy.sh b/scripts/deploy.sh index 1926206..9f6e5b3 100644 --- a/scripts/deploy.sh +++ b/scripts/deploy.sh @@ -28,20 +28,44 @@ if [ ! -f ".env" ]; then exit 1 fi -echo -e "${YELLOW}1/7 โ€” Pulling latest code...${NC}" +read_env_value() { + local key="$1" + local fallback="${2:-}" + local shell_value="${!key-}" + local file_value + + file_value="$(grep -E "^${key}=" .env 2>/dev/null | tail -n 1 | cut -d= -f2- || true)" + + if [ -n "$shell_value" ]; then + printf '%s' "$shell_value" + elif [ -n "$file_value" ]; then + printf '%s' "$file_value" + else + printf '%s' "$fallback" + fi +} + +normalize_bool() { + printf '%s' "$1" | tr '[:upper:]' '[:lower:]' +} + +INDEXNOW_AUTO_SUBMIT_VALUE="$(normalize_bool "$(read_env_value INDEXNOW_AUTO_SUBMIT 1)")" +INDEXNOW_STRICT_VALUE="$(normalize_bool "$(read_env_value INDEXNOW_STRICT false)")" + +echo -e "${YELLOW}1/8 โ€” Pulling latest code...${NC}" git pull origin main 2>/dev/null || echo "Not a git repo or no remote, skipping pull." -echo -e "${YELLOW}2/7 โ€” Building Docker images...${NC}" +echo -e "${YELLOW}2/8 โ€” Building Docker images...${NC}" docker compose -f docker-compose.prod.yml build --no-cache -echo -e "${YELLOW}3/7 โ€” Stopping old containers...${NC}" +echo -e "${YELLOW}3/8 โ€” Stopping old containers...${NC}" docker compose -f docker-compose.prod.yml down --remove-orphans -echo -e "${YELLOW}4/7 โ€” Starting services...${NC}" +echo -e "${YELLOW}4/8 โ€” Starting services...${NC}" docker compose -f docker-compose.prod.yml up -d if [ "${SKIP_AI_RUNTIME_CHECKS:-0}" != "1" ]; then - echo -e "${YELLOW}5/7 โ€” Verifying AI runtime in backend + worker...${NC}" + echo -e "${YELLOW}5/8 โ€” Verifying AI runtime in backend + worker...${NC}" for service in backend celery_worker; do if ! docker compose -f docker-compose.prod.yml exec -T "$service" python - <<'PY' import importlib.util @@ -69,10 +93,10 @@ PY fi done else - echo -e "${YELLOW}5/7 โ€” Skipping AI runtime checks (SKIP_AI_RUNTIME_CHECKS=1).${NC}" + echo -e "${YELLOW}5/8 โ€” Skipping AI runtime checks (SKIP_AI_RUNTIME_CHECKS=1).${NC}" fi -echo -e "${YELLOW}6/7 โ€” Waiting for health check...${NC}" +echo -e "${YELLOW}6/8 โ€” Waiting for health check...${NC}" sleep 10 # Health check @@ -84,7 +108,23 @@ else exit 1 fi -echo -e "${YELLOW}7/7 โ€” Current containers:${NC}" +if [ "${INDEXNOW_AUTO_SUBMIT_VALUE:-1}" = "1" ] || [ "${INDEXNOW_AUTO_SUBMIT_VALUE:-true}" = "true" ]; then + echo -e "${YELLOW}7/8 โ€” Submitting URLs to IndexNow...${NC}" + if docker compose -f docker-compose.prod.yml run --rm frontend_build_step node scripts/submit-indexnow.mjs; then + echo -e "${GREEN}โœ“ IndexNow notification completed.${NC}" + else + if [ "$INDEXNOW_STRICT_VALUE" = "1" ] || [ "$INDEXNOW_STRICT_VALUE" = "true" ]; then + echo -e "${RED}โœ— IndexNow notification failed and INDEXNOW_STRICT is enabled.${NC}" + exit 1 + fi + + echo -e "${YELLOW}! IndexNow notification failed; deployment will continue.${NC}" + fi +else + echo -e "${YELLOW}7/8 โ€” Skipping IndexNow notification (INDEXNOW_AUTO_SUBMIT=0).${NC}" +fi + +echo -e "${YELLOW}8/8 โ€” Current containers:${NC}" docker compose -f docker-compose.prod.yml ps echo ""