Add SEO data generation and testing for bilingual pages

- Implemented SEO data structures for programmatic tool and collection pages.
- Created functions to build FAQs and content sections for SEO pages.
- Added tests to ensure at least 50 bilingual SEO pages are generated, no duplicate English slugs, and matching Arabic localized paths.
- Verified that both tool and collection SEO inventories are populated adequately.
This commit is contained in:
Your Name
2026-03-21 10:55:43 +02:00
parent a8a7ec55a2
commit c800f707e3
12 changed files with 1920 additions and 22 deletions

View File

@@ -159,6 +159,7 @@ def create_app(config_name=None):
from app.routes.pdf_extra import pdf_extra_bp
from app.routes.image_extra import image_extra_bp
from app.routes.barcode import barcode_bp
from app.routes.sitemap import sitemap_bp
app.register_blueprint(health_bp, url_prefix="/api")
app.register_blueprint(auth_bp, url_prefix="/api/auth")
@@ -192,5 +193,6 @@ def create_app(config_name=None):
app.register_blueprint(pdf_extra_bp, url_prefix="/api/pdf-tools")
app.register_blueprint(image_extra_bp, url_prefix="/api/image")
app.register_blueprint(barcode_bp, url_prefix="/api/barcode")
app.register_blueprint(sitemap_bp)
return app

View File

@@ -0,0 +1,166 @@
"""XML sitemap endpoint for runtime and direct backend access."""
from flask import Blueprint, Response, current_app
from app.extensions import limiter
sitemap_bp = Blueprint("sitemap", __name__)
STATIC_PAGES = [
("/", "daily", "1.0"),
("/about", "monthly", "0.4"),
("/contact", "monthly", "0.4"),
("/privacy", "yearly", "0.3"),
("/terms", "yearly", "0.3"),
("/pricing", "monthly", "0.7"),
("/blog", "weekly", "0.6"),
("/developers", "monthly", "0.5"),
]
BLOG_SLUGS = [
"how-to-compress-pdf-online",
"convert-images-without-losing-quality",
"ocr-extract-text-from-images",
"merge-split-pdf-files",
"ai-chat-with-pdf-documents",
]
TOOL_SLUGS = [
"pdf-to-word",
"word-to-pdf",
"compress-pdf",
"merge-pdf",
"split-pdf",
"rotate-pdf",
"pdf-to-images",
"images-to-pdf",
"watermark-pdf",
"remove-watermark-pdf",
"protect-pdf",
"unlock-pdf",
"page-numbers",
"reorder-pdf",
"extract-pages",
"pdf-editor",
"pdf-flowchart",
"pdf-to-excel",
"sign-pdf",
"crop-pdf",
"flatten-pdf",
"repair-pdf",
"pdf-metadata",
"image-converter",
"image-resize",
"compress-image",
"remove-background",
"image-crop",
"image-rotate-flip",
"ocr",
"chat-pdf",
"summarize-pdf",
"translate-pdf",
"extract-tables",
"html-to-pdf",
"qr-code",
"video-to-gif",
"word-counter",
"text-cleaner",
"pdf-to-pptx",
"excel-to-pdf",
"pptx-to-pdf",
"barcode-generator",
]
SEO_TOOL_SLUGS = [
"pdf-to-word",
"word-to-pdf",
"compress-pdf-online",
"convert-jpg-to-pdf",
"merge-pdf-files",
"remove-pdf-password",
"pdf-to-word-editable",
"convert-pdf-to-text",
"split-pdf-online",
"jpg-to-pdf",
"png-to-pdf",
"images-to-pdf-online",
"pdf-to-jpg",
"pdf-to-png",
"compress-pdf-for-email",
"compress-scanned-pdf",
"merge-pdf-online-free",
"combine-pdf-files",
"extract-pages-from-pdf",
"reorder-pdf-pages",
"rotate-pdf-pages",
"add-page-numbers-to-pdf",
"protect-pdf-with-password",
"unlock-pdf-online",
"watermark-pdf-online",
"remove-watermark-from-pdf",
"edit-pdf-online-free",
"pdf-to-excel-online",
"extract-tables-from-pdf",
"html-to-pdf-online",
"scan-pdf-to-text",
"chat-with-pdf",
"summarize-pdf-online",
"translate-pdf-online",
"convert-image-to-pdf",
"convert-webp-to-jpg",
"resize-image-online",
"compress-image-online",
"remove-image-background",
]
SEO_COLLECTION_SLUGS = [
"best-pdf-tools",
"free-pdf-tools-online",
"convert-files-online",
"pdf-converter-tools",
"secure-pdf-tools",
"ai-document-tools",
"image-to-pdf-tools",
"online-image-tools",
"office-to-pdf-tools",
"scanned-document-tools",
"arabic-pdf-tools",
]
def _site_origin() -> str:
return str(current_app.config.get("SITE_DOMAIN", "https://dociva.io")).strip().rstrip("/")
def _url_tag(loc: str, changefreq: str, priority: str) -> str:
return (
" <url>\n"
f" <loc>{loc}</loc>\n"
" <lastmod>2026-03-21</lastmod>\n"
f" <changefreq>{changefreq}</changefreq>\n"
f" <priority>{priority}</priority>\n"
" </url>"
)
@sitemap_bp.route("/sitemap.xml", methods=["GET"])
@limiter.exempt
def sitemap_xml() -> Response:
"""Return an XML sitemap for direct backend access."""
origin = _site_origin()
entries = []
entries.extend(_url_tag(f"{origin}{path}", changefreq, priority) for path, changefreq, priority in STATIC_PAGES)
entries.extend(_url_tag(f"{origin}/blog/{slug}", "monthly", "0.6") for slug in BLOG_SLUGS)
entries.extend(_url_tag(f"{origin}/tools/{slug}", "weekly", "0.8") for slug in TOOL_SLUGS)
entries.extend(_url_tag(f"{origin}/{slug}", "weekly", "0.88") for slug in SEO_TOOL_SLUGS)
entries.extend(_url_tag(f"{origin}/ar/{slug}", "weekly", "0.8") for slug in SEO_TOOL_SLUGS)
entries.extend(_url_tag(f"{origin}/{slug}", "weekly", "0.82") for slug in SEO_COLLECTION_SLUGS)
entries.extend(_url_tag(f"{origin}/ar/{slug}", "weekly", "0.74") for slug in SEO_COLLECTION_SLUGS)
xml = (
'<?xml version="1.0" encoding="UTF-8"?>\n'
'<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n'
+ "\n".join(entries)
+ "\n</urlset>\n"
)
return Response(xml, mimetype="application/xml")

View File

@@ -0,0 +1,13 @@
"""Tests for XML sitemap exposure."""
def test_sitemap_endpoint(client):
response = client.get('/sitemap.xml')
assert response.status_code == 200
assert 'xml' in response.content_type
body = response.get_data(as_text=True)
assert 'https://dociva.io/tools/pdf-to-word' in body
assert 'https://dociva.io/pdf-to-word' in body
assert 'https://dociva.io/ar/pdf-to-word' in body
assert 'https://dociva.io/arabic-pdf-tools' in body