From 700941a24ca7d7102935820b88926d9a229f295e Mon Sep 17 00:00:00 2001
From: Your Name <119736744+aborayan2022@users.noreply.github.com>
Date: Sat, 4 Apr 2026 00:03:46 +0200
Subject: [PATCH] feat: Add IndexNow submission and sitemap updates
- Add IndexNow submit script + state tracking
- Update deploy script to notify IndexNow after healthy deploy
- Publish IndexNow verification file in public
- Update sitemaps and add env placeholders
- Pass analytics/ads/IndexNow env vars into frontend build
---
.env.example | 8 +-
.gitignore | 1 +
README.md | 10 +
docker-compose.prod.yml | 27 ++
frontend/Dockerfile | 38 ++
frontend/package.json | 2 +
.../718dc0aa7c7d4d3ebe71e3f97dacef9c.txt | 1 +
frontend/public/sitemap.xml | 8 +-
frontend/public/sitemaps/blog.xml | 10 +-
frontend/public/sitemaps/seo.xml | 376 +++++++++---------
frontend/public/sitemaps/static.xml | 24 +-
frontend/public/sitemaps/tools.xml | 265 +-----------
frontend/scripts/submit-indexnow.mjs | 311 +++++++++++++++
frontend/scripts/submit-indexnow.test.mjs | 40 ++
scripts/deploy.sh | 56 ++-
15 files changed, 697 insertions(+), 480 deletions(-)
create mode 100644 frontend/public/718dc0aa7c7d4d3ebe71e3f97dacef9c.txt
create mode 100644 frontend/scripts/submit-indexnow.mjs
create mode 100644 frontend/scripts/submit-indexnow.test.mjs
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.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/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 ""