@digestseo/mcp-gsc 0.4.4 → 0.4.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.4.5] - 2026-09-21
9
+
10
+ ### Fixed
11
+ - Search Console property inputs now reject malformed, relative, unsupported-scheme, credential-bearing, and malformed `sc-domain:` identifiers locally across all tools instead of deferring obvious property-identifier errors to Google.
12
+ - Sitemap get/list-filter/submit/delete inputs now reject malformed, relative, and non-HTTP(S) sitemap URLs locally instead of deferring obvious contract errors to Google.
13
+
8
14
  ## [0.4.4] - 2026-09-21
9
15
 
10
16
  ### Fixed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@digestseo/mcp-gsc",
3
- "version": "0.4.4",
3
+ "version": "0.4.5",
4
4
  "private": false,
5
5
  "main": "scripts/glama-start.mjs",
6
6
  "bin": {
package/src/index.ts CHANGED
@@ -56,6 +56,11 @@ import {
56
56
  import { CONTENT_DECAY_COMPARE_DAYS_SCHEMA } from './content-decay-schema';
57
57
  import { resolveIndexedPagesDateRange } from './indexed-pages-range';
58
58
  import { createQuickWinsInputSchema } from './quick-wins-schema';
59
+ import { SITEMAP_URL_SCHEMA } from './sitemap-url-schema';
60
+ import {
61
+ SEARCH_CONSOLE_PROPERTY_DESCRIPTION,
62
+ SEARCH_CONSOLE_PROPERTY_SCHEMA,
63
+ } from './search-console-property-schema';
59
64
  import { WRITE_TOOL_ANNOTATIONS } from './write-tool-annotations';
60
65
  import {
61
66
  getToolCatalogForAccessMode,
@@ -104,9 +109,6 @@ const READ_ONLY_ANNOTATIONS = {
104
109
  openWorldHint: true,
105
110
  } as const;
106
111
 
107
- const SITE_URL_DESCRIPTION =
108
- "The Search Console property identifier, exactly as returned by sites.list. Two formats exist: domain properties use 'sc-domain:example.com'; URL-prefix properties use the full URL including protocol and trailing slash, e.g. 'https://www.example.com/'. Passing the wrong format returns a permission error even when the user owns the site — call sites.list first if unsure.";
109
-
110
112
  const METRIC_OUTPUT_SCHEMA = {
111
113
  clicks: z.number(),
112
114
  impressions: z.number(),
@@ -720,7 +722,7 @@ class GscMcpRuntime {
720
722
  description:
721
723
  "Retrieve one exact Google Search Console property and the connected account's permission level for it. Use this when the user has already named a property and you need to confirm that exact property or its access level without listing every property first.",
722
724
  inputSchema: {
723
- site_url: z.string().describe(SITE_URL_DESCRIPTION),
725
+ site_url: SEARCH_CONSOLE_PROPERTY_SCHEMA,
724
726
  },
725
727
  outputSchema: SITE_DETAIL_OUTPUT_SCHEMA,
726
728
  annotations: READ_ONLY_ANNOTATIONS,
@@ -739,7 +741,7 @@ class GscMcpRuntime {
739
741
  title: 'Inspect URL index status',
740
742
  description: `Inspect the version of a single URL currently known in Google's index. Returns Google's index-status analysis, last crawl, page-fetch/indexing state, canonicals, and rich-results/AMP analysis where available. This API does not test the live URL or prove current live-page indexability, and its mobile-usability result is deprecated. Use this when the user asks 'is X indexed?' or wants Google's indexed-state evidence for one page. For a bounded group of 2-10 URLs, prefer urls.inspect_many; Google still processes one URL Inspection request per URL and applies the same quota semantics.`,
741
743
  inputSchema: {
742
- site_url: z.string().describe(SITE_URL_DESCRIPTION),
744
+ site_url: SEARCH_CONSOLE_PROPERTY_SCHEMA,
743
745
  inspection_url: z
744
746
  .string()
745
747
  .describe(
@@ -779,7 +781,7 @@ class GscMcpRuntime {
779
781
  title: 'Inspect multiple URLs',
780
782
  description: `Inspect the versions of up to 10 URLs currently known in Google's index from one Search Console property. This API does not run live URL tests. Google still processes one URL Inspection request per URL, so every requested URL consumes one quota unit and one unit of this server's shared URL-inspection safety budget. Requests are sent sequentially to avoid unnecessary bursts. Use this for a small group of important or debugging-target URLs; do not use it to crawl an entire site.`,
781
783
  inputSchema: {
782
- site_url: z.string().describe(SITE_URL_DESCRIPTION),
784
+ site_url: SEARCH_CONSOLE_PROPERTY_SCHEMA,
783
785
  inspection_urls: z
784
786
  .array(z.string().url())
785
787
  .min(1)
@@ -842,10 +844,8 @@ class GscMcpRuntime {
842
844
  description:
843
845
  'List sitemaps submitted for a Search Console property. Optionally filter to entries included by one sitemap index. Returns sitemap URLs, last submitted/downloaded dates, submitted URL counts, warning and error counts, and sitemap status. Google\'s deprecated sitemap indexed count is intentionally omitted. Use this when the user asks about sitemap health, submission status, wants to audit which sitemaps are working, or needs the child sitemaps belonging to a specific sitemap index.',
844
846
  inputSchema: {
845
- site_url: z.string().describe(SITE_URL_DESCRIPTION),
846
- sitemap_index: z
847
- .string()
848
- .url()
847
+ site_url: SEARCH_CONSOLE_PROPERTY_SCHEMA,
848
+ sitemap_index: SITEMAP_URL_SCHEMA
849
849
  .optional()
850
850
  .describe(
851
851
  'Optional sitemap index URL. When supplied, Google returns sitemap entries included in that index.',
@@ -928,7 +928,7 @@ class GscMcpRuntime {
928
928
  ' non-preliminary data.',
929
929
  ].join('\n'),
930
930
  inputSchema: {
931
- site_url: z.string().describe(SITE_URL_DESCRIPTION),
931
+ site_url: SEARCH_CONSOLE_PROPERTY_SCHEMA,
932
932
  start_date: SEARCH_CONSOLE_DATE_SCHEMA.describe('Start date (inclusive) in YYYY-MM-DD format.'),
933
933
  end_date: SEARCH_CONSOLE_DATE_SCHEMA.describe(
934
934
  'End date (inclusive) in YYYY-MM-DD format. Note the 2-3 day data lag: the most recent complete date is usually 3 days ago.',
@@ -1086,7 +1086,7 @@ class GscMcpRuntime {
1086
1086
  title: 'Find queries for a page',
1087
1087
  description: 'For one exact page URL, return the Search Console queries that produced impressions for it over a date range. This wraps an exact page dimension filter so agents do not need to construct analytics.query filter groups manually. Exact page matching is case-sensitive in Search Console. Use row_limit and start_row to page through bounded results; Search Console can still omit anonymized queries. Google Discover is intentionally unavailable because Discover does not expose query data.',
1088
1088
  inputSchema: {
1089
- site_url: z.string().describe(SITE_URL_DESCRIPTION),
1089
+ site_url: SEARCH_CONSOLE_PROPERTY_SCHEMA,
1090
1090
  page_url: z.string().url().describe('Exact fully-qualified page URL to filter on, e.g. https://example.com/guides/seo/. Search Console exact page filters are case-sensitive.'),
1091
1091
  start_date: SEARCH_CONSOLE_DATE_SCHEMA.describe('Start date (inclusive) in YYYY-MM-DD format.'),
1092
1092
  end_date: SEARCH_CONSOLE_DATE_SCHEMA.describe('End date (inclusive) in YYYY-MM-DD format. Note the 2-3 day GSC data lag.'),
@@ -1164,7 +1164,7 @@ class GscMcpRuntime {
1164
1164
  title: 'Find pages for a query',
1165
1165
  description: 'For one exact search query, return the site pages that received impressions for it over a date range. This wraps an exact query dimension filter so agents do not need to construct analytics.query filter groups manually. Exact query matching is case-sensitive in Search Console. Use row_limit and start_row to page through bounded results and verify which URL Google is surfacing before diagnosing cannibalization or content targeting. Google Discover is intentionally unavailable because Discover does not expose query data.',
1166
1166
  inputSchema: {
1167
- site_url: z.string().describe(SITE_URL_DESCRIPTION),
1167
+ site_url: SEARCH_CONSOLE_PROPERTY_SCHEMA,
1168
1168
  query: z.string().min(1).describe('Exact Search Console query text to filter on. Exact query filters are case-sensitive.'),
1169
1169
  start_date: SEARCH_CONSOLE_DATE_SCHEMA.describe('Start date (inclusive) in YYYY-MM-DD format.'),
1170
1170
  end_date: SEARCH_CONSOLE_DATE_SCHEMA.describe('End date (inclusive) in YYYY-MM-DD format. Note the 2-3 day GSC data lag.'),
@@ -1243,7 +1243,7 @@ class GscMcpRuntime {
1243
1243
  title: 'Add Search Console property',
1244
1244
  description: 'Add a new website property to your Google Search Console account. Note: Domain properties require sc-domain prefix (e.g., sc-domain:example.com), URL-prefix properties require full URL (e.g., https://example.com/).',
1245
1245
  inputSchema: {
1246
- site_url: z.string().describe(SITE_URL_DESCRIPTION),
1246
+ site_url: SEARCH_CONSOLE_PROPERTY_SCHEMA,
1247
1247
  },
1248
1248
  outputSchema: MESSAGE_OUTPUT_SCHEMA,
1249
1249
  annotations: WRITE_TOOL_ANNOTATIONS['sites.add'],
@@ -1265,7 +1265,7 @@ class GscMcpRuntime {
1265
1265
  title: 'Delete Search Console property',
1266
1266
  description: 'Remove an existing website property from your Google Search Console account.',
1267
1267
  inputSchema: {
1268
- site_url: z.string().describe(SITE_URL_DESCRIPTION),
1268
+ site_url: SEARCH_CONSOLE_PROPERTY_SCHEMA,
1269
1269
  },
1270
1270
  outputSchema: MESSAGE_OUTPUT_SCHEMA,
1271
1271
  annotations: WRITE_TOOL_ANNOTATIONS['sites.delete'],
@@ -1287,8 +1287,8 @@ class GscMcpRuntime {
1287
1287
  title: 'Submit sitemap',
1288
1288
  description: 'Submit a new sitemap to your Google Search Console account.',
1289
1289
  inputSchema: {
1290
- site_url: z.string().describe(SITE_URL_DESCRIPTION),
1291
- feedpath: z.string().describe('The full URL of the sitemap file to submit, e.g. https://example.com/sitemap.xml'),
1290
+ site_url: SEARCH_CONSOLE_PROPERTY_SCHEMA,
1291
+ feedpath: SITEMAP_URL_SCHEMA.describe('The full HTTP/HTTPS URL of the sitemap file to submit, e.g. https://example.com/sitemap.xml'),
1292
1292
  },
1293
1293
  outputSchema: MESSAGE_OUTPUT_SCHEMA,
1294
1294
  annotations: WRITE_TOOL_ANNOTATIONS['sitemaps.submit'],
@@ -1310,8 +1310,8 @@ class GscMcpRuntime {
1310
1310
  title: 'Delete sitemap',
1311
1311
  description: 'Remove/delete a submitted sitemap from your Google Search Console account.',
1312
1312
  inputSchema: {
1313
- site_url: z.string().describe(SITE_URL_DESCRIPTION),
1314
- feedpath: z.string().describe('The full URL of the sitemap file to delete, e.g. https://example.com/sitemap.xml'),
1313
+ site_url: SEARCH_CONSOLE_PROPERTY_SCHEMA,
1314
+ feedpath: SITEMAP_URL_SCHEMA.describe('The full HTTP/HTTPS URL of the sitemap file to delete, e.g. https://example.com/sitemap.xml'),
1315
1315
  },
1316
1316
  outputSchema: MESSAGE_OUTPUT_SCHEMA,
1317
1317
  annotations: WRITE_TOOL_ANNOTATIONS['sitemaps.delete'],
@@ -1334,8 +1334,8 @@ class GscMcpRuntime {
1334
1334
  title: 'Get sitemap details',
1335
1335
  description: 'Get status and details of a single sitemap submitted to Google Search Console.',
1336
1336
  inputSchema: {
1337
- site_url: z.string().describe(SITE_URL_DESCRIPTION),
1338
- feedpath: z.string().describe('The full URL of the sitemap file, e.g. https://example.com/sitemap.xml'),
1337
+ site_url: SEARCH_CONSOLE_PROPERTY_SCHEMA,
1338
+ feedpath: SITEMAP_URL_SCHEMA.describe('The full HTTP/HTTPS URL of the sitemap file, e.g. https://example.com/sitemap.xml'),
1339
1339
  },
1340
1340
  outputSchema: { sitemap: SITEMAP_OUTPUT_SCHEMA },
1341
1341
  annotations: READ_ONLY_ANNOTATIONS,
@@ -1353,7 +1353,7 @@ class GscMcpRuntime {
1353
1353
  {
1354
1354
  title: 'Identify SEO Quick Wins',
1355
1355
  description: 'Find observed query/page Search Analytics rows with at least the requested impressions whose average position falls in a configurable opportunity range (8-20 by default). Average position is an aggregate Search Console metric, not a literal current rank; CTR is returned for context and is not an eligibility filter. Results are ordered deterministically by impressions, then bounded with limit/start_row and explicit result_page metadata. Source pagination separately flags the local 100,000-row safety ceiling.',
1356
- inputSchema: createQuickWinsInputSchema(SITE_URL_DESCRIPTION),
1356
+ inputSchema: createQuickWinsInputSchema(),
1357
1357
  outputSchema: z.object(QUICK_WIN_OUTPUT_SCHEMA),
1358
1358
  annotations: READ_ONLY_ANNOTATIONS,
1359
1359
  },
@@ -1387,7 +1387,7 @@ class GscMcpRuntime {
1387
1387
  title: 'Detect Keyword Cannibalization',
1388
1388
  description: 'Analyze query/page Search Analytics to find queries split across multiple pages. Candidates are ranked deterministically by the observed query/page impression sum, each candidate bounds its page list, and limit/start_row plus result_page provide safe pagination. total_clicks, total_impressions, and impression_share are calculated from observed query/page rows and are not true query-level property aggregates; multiple pages can make that row sum exceed the query-level Search Console total. Multiple ranking URLs can also reflect legitimate locale or intent differences, so treat candidates as evidence to investigate rather than proof of harmful cannibalization.',
1389
1389
  inputSchema: {
1390
- site_url: z.string().describe(SITE_URL_DESCRIPTION),
1390
+ site_url: SEARCH_CONSOLE_PROPERTY_SCHEMA,
1391
1391
  start_date: SEARCH_CONSOLE_DATE_SCHEMA.describe('Start date (inclusive) in YYYY-MM-DD format.'),
1392
1392
  end_date: SEARCH_CONSOLE_DATE_SCHEMA.describe('End date (inclusive) in YYYY-MM-DD format. Note the 2-3 day GSC data lag.'),
1393
1393
  min_impressions: CANNIBALIZATION_MIN_IMPRESSIONS_SCHEMA,
@@ -1445,7 +1445,7 @@ class GscMcpRuntime {
1445
1445
  title: 'Detect Content Decay',
1446
1446
  description: 'Assess page-level click declines across two contiguous periods without treating every small click change as content decay. Each result is classified as likely_decay, weak_insufficient_evidence, or improving_visibility_with_click_volatility using deterministic click-volume, impression, and average-position signals. This is a heuristic assessment, not statistical proof. Results are bounded with limit/start_row; source pagination is reported separately.',
1447
1447
  inputSchema: {
1448
- site_url: z.string().describe(SITE_URL_DESCRIPTION),
1448
+ site_url: SEARCH_CONSOLE_PROPERTY_SCHEMA,
1449
1449
  compare_days: CONTENT_DECAY_COMPARE_DAYS_SCHEMA,
1450
1450
  limit: ANALYSIS_RESULT_LIMIT_SCHEMA,
1451
1451
  start_row: RESULT_START_ROW_SCHEMA,
@@ -1519,7 +1519,7 @@ class GscMcpRuntime {
1519
1519
  title: 'Request Indexing',
1520
1520
  description: "Requests indexing through Google's Indexing API for a URL within an owner-level Search Console property accessible to the connected Google account. Google currently restricts this API to pages containing JobPosting structured data or livestream pages containing BroadcastEvent inside VideoObject. It is not available for general webpage submission.",
1521
1521
  inputSchema: {
1522
- site_url: z.string().describe(`${SITE_URL_DESCRIPTION} The connected Google account must be an owner of this property.`),
1522
+ site_url: SEARCH_CONSOLE_PROPERTY_SCHEMA.describe(`${SEARCH_CONSOLE_PROPERTY_DESCRIPTION} The connected Google account must be an owner of this property.`),
1523
1523
  url: z.string().superRefine((value, ctx) => {
1524
1524
  try {
1525
1525
  assertIndexingRequestUrl(value);
@@ -1557,7 +1557,7 @@ class GscMcpRuntime {
1557
1557
  title: 'List Search-Visible Pages',
1558
1558
  description: `${SEARCH_VISIBLE_PAGES_DESCRIPTION} When one date boundary is omitted, the server derives the other to target an inclusive 30-day range; generated end dates are capped at the latest complete date. Responses are bounded and pageable with row_limit/start_row.`,
1559
1559
  inputSchema: {
1560
- site_url: z.string().describe(SITE_URL_DESCRIPTION),
1560
+ site_url: SEARCH_CONSOLE_PROPERTY_SCHEMA,
1561
1561
  start_date: SEARCH_CONSOLE_DATE_SCHEMA.optional().describe('Start date (inclusive) in YYYY-MM-DD format. If end_date is omitted, the generated end date is 29 days later, capped at the latest complete date.'),
1562
1562
  end_date: SEARCH_CONSOLE_DATE_SCHEMA.optional().describe('End date (inclusive) in YYYY-MM-DD format. If start_date is omitted, the generated start date is 29 days earlier. Defaults to 3 days ago. Note the 2-3 day data lag.'),
1563
1563
  row_limit: z.number().int().min(1).max(25000).default(1000).describe(`Maximum pages requested for this logical response (1-25000). Output is safely bounded; continue with result_page.next_start_row while result_page.has_more is true.`),
@@ -1617,7 +1617,7 @@ class GscMcpRuntime {
1617
1617
  title: 'Compare Performance Between Periods',
1618
1618
  description: 'Compare Search Console performance metrics (clicks, impressions, CTR, average position) between two distinct date ranges (Period A vs Period B) for a selected dimension (query, page, country, device). Apply the same search type and optional dimension filters to both periods. Results are ranked by largest absolute click change, then impression change, and safely paged with limit/start_row. Percentage change is null when the baseline is zero and the comparison value differs. Discover is intentionally unavailable because this comparison contract includes average position, which the Discover report does not support.',
1619
1619
  inputSchema: {
1620
- site_url: z.string().describe(SITE_URL_DESCRIPTION),
1620
+ site_url: SEARCH_CONSOLE_PROPERTY_SCHEMA,
1621
1621
  start_date_a: SEARCH_CONSOLE_DATE_SCHEMA.describe('Start date of Period A (recent, YYYY-MM-DD)'),
1622
1622
  end_date_a: SEARCH_CONSOLE_DATE_SCHEMA.describe('End date of Period A (recent, YYYY-MM-DD)'),
1623
1623
  start_date_b: SEARCH_CONSOLE_DATE_SCHEMA.describe('Start date of Period B (previous, YYYY-MM-DD)'),
@@ -1723,7 +1723,7 @@ class GscMcpRuntime {
1723
1723
  description:
1724
1724
  'Generate a plain-language weekly SEO report for one Google Search Console property. Returns a markdown digest covering the 7 days ending on end_date, with week-over-week comparison, top pages, queries gaining or losing traction, and one specific action item. Defaults end_date to 3 days ago so the report uses the latest usually-complete Search Console data; pass end_date explicitly to include fresher preliminary data.',
1725
1725
  inputSchema: {
1726
- site_url: z.string().describe(SITE_URL_DESCRIPTION),
1726
+ site_url: SEARCH_CONSOLE_PROPERTY_SCHEMA,
1727
1727
  end_date: SEARCH_CONSOLE_DATE_SCHEMA
1728
1728
  .optional()
1729
1729
  .describe('End date (inclusive) in YYYY-MM-DD format. Defaults to 3 days ago, which is usually the latest complete Search Console date. Pass a more recent date explicitly to include preliminary data.'),
@@ -1,5 +1,6 @@
1
1
  import { z } from 'zod';
2
2
  import { SEARCH_CONSOLE_DATE_SCHEMA } from './date-validation';
3
+ import { SEARCH_CONSOLE_PROPERTY_SCHEMA } from './search-console-property-schema';
3
4
  import {
4
5
  DEFAULT_ANALYSIS_RESULT_LIMIT,
5
6
  MAX_ANALYSIS_RESULT_LIMIT,
@@ -16,10 +17,10 @@ const QUICK_WINS_THRESHOLDS_SHAPE = {
16
17
  max_position: z.number().positive().default(20).describe('Maximum Search Console average position for the opportunity range (inclusive). This is an aggregate metric, not a literal current rank. Must be greater than 0. Default is 20.'),
17
18
  };
18
19
 
19
- export function createQuickWinsInputSchema(siteUrlDescription: string) {
20
+ export function createQuickWinsInputSchema() {
20
21
  return z
21
22
  .object({
22
- site_url: z.string().describe(siteUrlDescription),
23
+ site_url: SEARCH_CONSOLE_PROPERTY_SCHEMA,
23
24
  start_date: SEARCH_CONSOLE_DATE_SCHEMA.describe('Start date (inclusive) in YYYY-MM-DD format.'),
24
25
  end_date: SEARCH_CONSOLE_DATE_SCHEMA.describe('End date (inclusive) in YYYY-MM-DD format. Note the 2-3 day GSC data lag.'),
25
26
  ...QUICK_WINS_THRESHOLDS_SHAPE,
@@ -0,0 +1,45 @@
1
+ import { z } from 'zod';
2
+
3
+ export const SEARCH_CONSOLE_PROPERTY_DESCRIPTION =
4
+ "The Search Console property identifier, exactly as returned by sites.list. Domain properties use 'sc-domain:example.com'; URL-prefix properties use an HTTP or HTTPS URL, typically including the trailing slash, e.g. 'https://www.example.com/'. Passing the wrong property form can return a permission error even when the user owns the site - call sites.list first if unsure.";
5
+
6
+ function isDomainPropertyIdentifier(value: string): boolean {
7
+ if (!value.startsWith('sc-domain:')) return false;
8
+ const domain = value.slice('sc-domain:'.length);
9
+ return (
10
+ domain.length > 0 &&
11
+ !/\s/.test(domain) &&
12
+ !domain.includes('/') &&
13
+ !domain.includes('?') &&
14
+ !domain.includes('#') &&
15
+ !domain.includes('@') &&
16
+ !domain.includes(':')
17
+ );
18
+ }
19
+
20
+ function isUrlPrefixPropertyIdentifier(value: string): boolean {
21
+ if (value !== value.trim() || /\s/.test(value)) return false;
22
+ try {
23
+ const parsed = new URL(value);
24
+ return (
25
+ (parsed.protocol === 'http:' || parsed.protocol === 'https:') &&
26
+ parsed.hostname.length > 0 &&
27
+ parsed.username === '' &&
28
+ parsed.password === ''
29
+ );
30
+ } catch {
31
+ return false;
32
+ }
33
+ }
34
+
35
+ export function isSearchConsolePropertyIdentifier(value: string): boolean {
36
+ return isDomainPropertyIdentifier(value) || isUrlPrefixPropertyIdentifier(value);
37
+ }
38
+
39
+ export const SEARCH_CONSOLE_PROPERTY_SCHEMA = z
40
+ .string()
41
+ .refine(isSearchConsolePropertyIdentifier, {
42
+ message:
43
+ "Expected a Search Console property identifier such as 'sc-domain:example.com' or an HTTP/HTTPS URL-prefix property such as 'https://www.example.com/'.",
44
+ })
45
+ .describe(SEARCH_CONSOLE_PROPERTY_DESCRIPTION);
@@ -0,0 +1,13 @@
1
+ import { z } from 'zod';
2
+
3
+ export const SITEMAP_URL_SCHEMA = z
4
+ .string()
5
+ .url()
6
+ .refine((value) => {
7
+ try {
8
+ const protocol = new URL(value).protocol;
9
+ return protocol === 'http:' || protocol === 'https:';
10
+ } catch {
11
+ return false;
12
+ }
13
+ }, 'Sitemap URL must use http:// or https://.');