@gscdump/sdk 0.22.4 → 0.23.1
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/dist/_chunks/chunk.mjs +25 -0
- package/dist/_chunks/query.d.mts +2 -0
- package/dist/index.d.mts +364 -1
- package/dist/index.mjs +914 -1
- package/dist/query.mjs +5 -1
- package/package.json +6 -3
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
import { query_exports } from "./query.mjs";
|
|
1
2
|
import { CANONICAL_WEBHOOK_EVENTS, VALID_WEBHOOK_EVENTS, WEBHOOK_CONTRACT_VERSION, WEBHOOK_CONTRACT_VERSION_HEADER, WEBHOOK_CONTRACT_VERSION_HEADER as WEBHOOK_CONTRACT_VERSION_HEADER$1, WEBHOOK_DELIVERY_HEADER, WEBHOOK_DELIVERY_HEADER as WEBHOOK_DELIVERY_HEADER$1, WEBHOOK_EVENT_HEADER, WEBHOOK_EVENT_HEADER as WEBHOOK_EVENT_HEADER$1, WEBHOOK_SIGNATURE_HEADER, WEBHOOK_SIGNATURE_HEADER as WEBHOOK_SIGNATURE_HEADER$1, WEBHOOK_TIMESTAMP_HEADER, WEBHOOK_TIMESTAMP_HEADER as WEBHOOK_TIMESTAMP_HEADER$1, analyticsRoutes, partnerEndpointSchemas, partnerRoutes, partnerWebhookEnvelopeSchema } from "@gscdump/contracts";
|
|
2
3
|
import { ofetch } from "ofetch";
|
|
4
|
+
import { resolveWindow } from "@gscdump/engine/period";
|
|
5
|
+
import { endOfMonth, format, startOfMonth, startOfQuarter, startOfWeek, subDays, subMonths } from "date-fns";
|
|
3
6
|
export * from "@gscdump/contracts";
|
|
4
7
|
var PartnerApiError = class extends Error {
|
|
5
8
|
kind;
|
|
@@ -200,6 +203,20 @@ function createAnalyticsClient(options = {}) {
|
|
|
200
203
|
}
|
|
201
204
|
};
|
|
202
205
|
}
|
|
206
|
+
function defineGscAnalyzer(def) {
|
|
207
|
+
return def;
|
|
208
|
+
}
|
|
209
|
+
function weightedAnonPct(days, window = 28) {
|
|
210
|
+
if (!days?.length) return null;
|
|
211
|
+
const trailing = days.slice(-window);
|
|
212
|
+
let totalImpressions = 0;
|
|
213
|
+
let weighted = 0;
|
|
214
|
+
for (const d of trailing) {
|
|
215
|
+
totalImpressions += d.impressions;
|
|
216
|
+
weighted += d.impressions * d.anonymizedImpressionsPct;
|
|
217
|
+
}
|
|
218
|
+
return totalImpressions > 0 ? weighted / totalImpressions : null;
|
|
219
|
+
}
|
|
203
220
|
const ARCHETYPE_EXECUTION_CLASS = {
|
|
204
221
|
"site-daily-timeseries": "r2-sql",
|
|
205
222
|
"entity-daily-timeseries": "r2-sql-resolved",
|
|
@@ -693,6 +710,645 @@ function createPartnerClient(options = {}) {
|
|
|
693
710
|
};
|
|
694
711
|
}
|
|
695
712
|
const createGscdumpClient = createPartnerClient;
|
|
713
|
+
const COUNTRY_NAMES = {
|
|
714
|
+
US: "United States",
|
|
715
|
+
GB: "United Kingdom",
|
|
716
|
+
DE: "Germany",
|
|
717
|
+
FR: "France",
|
|
718
|
+
CA: "Canada",
|
|
719
|
+
AU: "Australia",
|
|
720
|
+
IN: "India",
|
|
721
|
+
BR: "Brazil",
|
|
722
|
+
JP: "Japan",
|
|
723
|
+
IT: "Italy",
|
|
724
|
+
ES: "Spain",
|
|
725
|
+
NL: "Netherlands",
|
|
726
|
+
SE: "Sweden",
|
|
727
|
+
CH: "Switzerland",
|
|
728
|
+
MX: "Mexico",
|
|
729
|
+
KR: "South Korea",
|
|
730
|
+
RU: "Russia",
|
|
731
|
+
PL: "Poland",
|
|
732
|
+
BE: "Belgium",
|
|
733
|
+
AT: "Austria",
|
|
734
|
+
NO: "Norway",
|
|
735
|
+
DK: "Denmark",
|
|
736
|
+
FI: "Finland",
|
|
737
|
+
PT: "Portugal",
|
|
738
|
+
IE: "Ireland",
|
|
739
|
+
NZ: "New Zealand",
|
|
740
|
+
SG: "Singapore",
|
|
741
|
+
HK: "Hong Kong",
|
|
742
|
+
TW: "Taiwan",
|
|
743
|
+
IL: "Israel",
|
|
744
|
+
ZA: "South Africa",
|
|
745
|
+
AR: "Argentina",
|
|
746
|
+
CL: "Chile",
|
|
747
|
+
CO: "Colombia",
|
|
748
|
+
TH: "Thailand",
|
|
749
|
+
PH: "Philippines",
|
|
750
|
+
MY: "Malaysia",
|
|
751
|
+
ID: "Indonesia",
|
|
752
|
+
VN: "Vietnam",
|
|
753
|
+
TR: "Turkey",
|
|
754
|
+
CZ: "Czech Republic",
|
|
755
|
+
RO: "Romania",
|
|
756
|
+
HU: "Hungary",
|
|
757
|
+
GR: "Greece",
|
|
758
|
+
UA: "Ukraine",
|
|
759
|
+
EG: "Egypt",
|
|
760
|
+
NG: "Nigeria",
|
|
761
|
+
KE: "Kenya",
|
|
762
|
+
PK: "Pakistan",
|
|
763
|
+
BD: "Bangladesh",
|
|
764
|
+
AE: "United Arab Emirates",
|
|
765
|
+
SA: "Saudi Arabia"
|
|
766
|
+
};
|
|
767
|
+
const countryName = (code) => COUNTRY_NAMES[code] || code;
|
|
768
|
+
const CWV_GOOD_LCP = 2500;
|
|
769
|
+
const CWV_POOR_LCP = 4e3;
|
|
770
|
+
const CWV_GOOD_INP = 200;
|
|
771
|
+
const CWV_POOR_INP = 500;
|
|
772
|
+
const CWV_GOOD_CLS = .1;
|
|
773
|
+
const CWV_POOR_CLS = .25;
|
|
774
|
+
function cwvBucket(metric, v) {
|
|
775
|
+
if (metric === "lcp") return v <= 2500 ? "good" : v <= 4e3 ? "ni" : "poor";
|
|
776
|
+
if (metric === "inp") return v <= 200 ? "good" : v <= 500 ? "ni" : "poor";
|
|
777
|
+
return v <= .1 ? "good" : v <= .25 ? "ni" : "poor";
|
|
778
|
+
}
|
|
779
|
+
function truncateQuery(q, max = 48) {
|
|
780
|
+
return q.length > max ? `${q.slice(0, max - 1)}…` : q;
|
|
781
|
+
}
|
|
782
|
+
function siteUrlToHostname(url) {
|
|
783
|
+
if (!url) return void 0;
|
|
784
|
+
try {
|
|
785
|
+
return new URL(url.startsWith("http") ? url : `https://${url}`).hostname;
|
|
786
|
+
} catch {
|
|
787
|
+
return;
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
function splitOpportunityTitle(title) {
|
|
791
|
+
return title.split(/("[^"]+")/g).filter(Boolean).map((part) => ({
|
|
792
|
+
text: part.startsWith("\"") && part.endsWith("\"") ? part.slice(1, -1) : part,
|
|
793
|
+
highlight: part.startsWith("\"") && part.endsWith("\"")
|
|
794
|
+
}));
|
|
795
|
+
}
|
|
796
|
+
function gscConsoleUrl(opts) {
|
|
797
|
+
const base = "https://search.google.com/search-console";
|
|
798
|
+
const resource = opts.resource ?? "performance";
|
|
799
|
+
const params = new URLSearchParams();
|
|
800
|
+
const siteLabel = /^(?:https?:|sc-domain:)/.test(opts.siteLabel) ? opts.siteLabel : `sc-domain:${opts.siteLabel}`;
|
|
801
|
+
params.set("resource_id", siteLabel);
|
|
802
|
+
if (resource === "url-inspection") {
|
|
803
|
+
if (opts.page) params.set("id", opts.page);
|
|
804
|
+
} else {
|
|
805
|
+
if (opts.page) params.set("page", `*${opts.page}`);
|
|
806
|
+
if (opts.query) params.set("query", `*${opts.query}`);
|
|
807
|
+
}
|
|
808
|
+
return `${base}${resource === "performance" ? "/performance/search-analytics" : resource === "url-inspection" ? "/inspect" : resource === "sitemaps" ? "/sitemaps" : "/index"}?${params.toString()}`;
|
|
809
|
+
}
|
|
810
|
+
const GSC_STABLE_LATENCY_DAYS = 3;
|
|
811
|
+
function classifyGscError(e) {
|
|
812
|
+
const code = e?.statusCode ?? e?.status;
|
|
813
|
+
const message = extractMessage(e);
|
|
814
|
+
if (code === 401 || code === 403) return {
|
|
815
|
+
status: "auth-missing",
|
|
816
|
+
code,
|
|
817
|
+
message
|
|
818
|
+
};
|
|
819
|
+
if (code === 429) {
|
|
820
|
+
const retry = e?.data?.retryAfter;
|
|
821
|
+
return {
|
|
822
|
+
status: "rate-limited",
|
|
823
|
+
code,
|
|
824
|
+
message,
|
|
825
|
+
retryAfter: typeof retry === "number" ? retry : void 0
|
|
826
|
+
};
|
|
827
|
+
}
|
|
828
|
+
if (code == null && !isAbort(e)) return {
|
|
829
|
+
status: "network",
|
|
830
|
+
message
|
|
831
|
+
};
|
|
832
|
+
return {
|
|
833
|
+
status: "error",
|
|
834
|
+
code,
|
|
835
|
+
message
|
|
836
|
+
};
|
|
837
|
+
}
|
|
838
|
+
function extractMessage(e) {
|
|
839
|
+
if (!e || typeof e !== "object") return void 0;
|
|
840
|
+
const data = e.data;
|
|
841
|
+
if (data && typeof data === "object") {
|
|
842
|
+
const m = data.message ?? data.error;
|
|
843
|
+
if (typeof m === "string") return m;
|
|
844
|
+
}
|
|
845
|
+
const m = e.message;
|
|
846
|
+
return typeof m === "string" ? m : void 0;
|
|
847
|
+
}
|
|
848
|
+
function isAbort(e) {
|
|
849
|
+
return e?.name === "AbortError";
|
|
850
|
+
}
|
|
851
|
+
function dateFilter(r) {
|
|
852
|
+
return {
|
|
853
|
+
type: "between",
|
|
854
|
+
column: "date",
|
|
855
|
+
from: r.start,
|
|
856
|
+
to: r.end
|
|
857
|
+
};
|
|
858
|
+
}
|
|
859
|
+
function andFilter(...filters) {
|
|
860
|
+
return {
|
|
861
|
+
type: "and",
|
|
862
|
+
filters: filters.filter((f) => f != null)
|
|
863
|
+
};
|
|
864
|
+
}
|
|
865
|
+
const PERIOD_PRESETS = [
|
|
866
|
+
{
|
|
867
|
+
value: "7d",
|
|
868
|
+
label: "Last 7 days",
|
|
869
|
+
shortLabel: "7d",
|
|
870
|
+
group: "rolling"
|
|
871
|
+
},
|
|
872
|
+
{
|
|
873
|
+
value: "28d",
|
|
874
|
+
label: "Last 28 days",
|
|
875
|
+
shortLabel: "28d",
|
|
876
|
+
group: "rolling"
|
|
877
|
+
},
|
|
878
|
+
{
|
|
879
|
+
value: "3m",
|
|
880
|
+
label: "Last 3 months",
|
|
881
|
+
shortLabel: "3m",
|
|
882
|
+
group: "rolling"
|
|
883
|
+
},
|
|
884
|
+
{
|
|
885
|
+
value: "6m",
|
|
886
|
+
label: "Last 6 months",
|
|
887
|
+
shortLabel: "6m",
|
|
888
|
+
group: "rolling"
|
|
889
|
+
},
|
|
890
|
+
{
|
|
891
|
+
value: "12m",
|
|
892
|
+
label: "Last 12 months",
|
|
893
|
+
shortLabel: "12m",
|
|
894
|
+
group: "rolling"
|
|
895
|
+
},
|
|
896
|
+
{
|
|
897
|
+
value: "this-week",
|
|
898
|
+
label: "This week",
|
|
899
|
+
shortLabel: "Week",
|
|
900
|
+
group: "calendar"
|
|
901
|
+
},
|
|
902
|
+
{
|
|
903
|
+
value: "this-month",
|
|
904
|
+
label: "This month",
|
|
905
|
+
shortLabel: "Month",
|
|
906
|
+
group: "calendar"
|
|
907
|
+
},
|
|
908
|
+
{
|
|
909
|
+
value: "last-month",
|
|
910
|
+
label: "Last month",
|
|
911
|
+
shortLabel: "Last mo",
|
|
912
|
+
group: "calendar"
|
|
913
|
+
},
|
|
914
|
+
{
|
|
915
|
+
value: "this-quarter",
|
|
916
|
+
label: "This quarter",
|
|
917
|
+
shortLabel: "Qtr",
|
|
918
|
+
group: "calendar"
|
|
919
|
+
},
|
|
920
|
+
{
|
|
921
|
+
value: "this-year",
|
|
922
|
+
label: "This year",
|
|
923
|
+
shortLabel: "Year",
|
|
924
|
+
group: "calendar"
|
|
925
|
+
}
|
|
926
|
+
];
|
|
927
|
+
const COMPARE_OPTIONS = [
|
|
928
|
+
{
|
|
929
|
+
value: "previous",
|
|
930
|
+
label: "Previous period",
|
|
931
|
+
description: "Compare to the period immediately before"
|
|
932
|
+
},
|
|
933
|
+
{
|
|
934
|
+
value: "year",
|
|
935
|
+
label: "Year over year",
|
|
936
|
+
description: "Compare to the same period last year"
|
|
937
|
+
},
|
|
938
|
+
{
|
|
939
|
+
value: "none",
|
|
940
|
+
label: "No comparison",
|
|
941
|
+
description: "Disable comparison"
|
|
942
|
+
}
|
|
943
|
+
];
|
|
944
|
+
const GSC_PERIOD_OPTIONS = PERIOD_PRESETS.filter((p) => p.group === "rolling").map((p) => ({
|
|
945
|
+
label: p.shortLabel,
|
|
946
|
+
value: p.value,
|
|
947
|
+
longLabel: p.label
|
|
948
|
+
}));
|
|
949
|
+
const GSC_PERIOD_OPTIONS_LONG = GSC_PERIOD_OPTIONS.map((o) => ({
|
|
950
|
+
label: o.longLabel,
|
|
951
|
+
value: o.value
|
|
952
|
+
}));
|
|
953
|
+
const GSC_COLUMN_OPTIONS = [
|
|
954
|
+
{
|
|
955
|
+
key: "clicks",
|
|
956
|
+
label: "Clicks",
|
|
957
|
+
icon: "i-lucide-mouse-pointer-click",
|
|
958
|
+
color: "blue"
|
|
959
|
+
},
|
|
960
|
+
{
|
|
961
|
+
key: "impressions",
|
|
962
|
+
label: "Views",
|
|
963
|
+
icon: "i-lucide-eye",
|
|
964
|
+
color: "purple"
|
|
965
|
+
},
|
|
966
|
+
{
|
|
967
|
+
key: "ctr",
|
|
968
|
+
label: "CTR",
|
|
969
|
+
icon: "i-lucide-percent",
|
|
970
|
+
color: "green"
|
|
971
|
+
},
|
|
972
|
+
{
|
|
973
|
+
key: "position",
|
|
974
|
+
label: "Pos",
|
|
975
|
+
icon: "i-lucide-hash",
|
|
976
|
+
color: "orange"
|
|
977
|
+
}
|
|
978
|
+
];
|
|
979
|
+
function coerceRowMetrics(row) {
|
|
980
|
+
return {
|
|
981
|
+
...row,
|
|
982
|
+
sum_position: row.sum_position ?? (row.position ?? 0) * row.impressions
|
|
983
|
+
};
|
|
984
|
+
}
|
|
985
|
+
function summarizeDailyRows(raw) {
|
|
986
|
+
const daily = raw.map(coerceRowMetrics).map((r) => ({
|
|
987
|
+
date: r.date,
|
|
988
|
+
clicks: r.clicks,
|
|
989
|
+
impressions: r.impressions,
|
|
990
|
+
sum_position: r.sum_position
|
|
991
|
+
})).sort((a, b) => a.date.localeCompare(b.date));
|
|
992
|
+
let clicks = 0;
|
|
993
|
+
let impressions = 0;
|
|
994
|
+
let weightedPosition = 0;
|
|
995
|
+
for (const d of daily) {
|
|
996
|
+
clicks += d.clicks;
|
|
997
|
+
impressions += d.impressions;
|
|
998
|
+
weightedPosition += d.sum_position;
|
|
999
|
+
}
|
|
1000
|
+
return {
|
|
1001
|
+
daily,
|
|
1002
|
+
totals: {
|
|
1003
|
+
clicks,
|
|
1004
|
+
impressions,
|
|
1005
|
+
ctr: impressions > 0 ? clicks / impressions : 0,
|
|
1006
|
+
position: impressions > 0 ? weightedPosition / impressions + 1 : 0
|
|
1007
|
+
},
|
|
1008
|
+
chartData: daily.map((d) => ({
|
|
1009
|
+
date: d.date,
|
|
1010
|
+
clicks: d.clicks,
|
|
1011
|
+
impressions: d.impressions
|
|
1012
|
+
}))
|
|
1013
|
+
};
|
|
1014
|
+
}
|
|
1015
|
+
function positionFor(r) {
|
|
1016
|
+
return r.impressions > 0 ? r.sum_position / r.impressions + 1 : 0;
|
|
1017
|
+
}
|
|
1018
|
+
const issueDetails = {
|
|
1019
|
+
crawled_not_indexed: {
|
|
1020
|
+
description: "Google crawled these pages but decided not to add them to the index. This often means the content was deemed low-quality, duplicate, or not useful enough.",
|
|
1021
|
+
fix: "Improve content quality and uniqueness. Add internal links pointing to these pages. Ensure they have clear, distinct value compared to other pages on your site."
|
|
1022
|
+
},
|
|
1023
|
+
discovered_not_indexed: {
|
|
1024
|
+
description: "Google knows these URLs exist but hasn't crawled them yet. This is usually a crawl-budget or priority signal — Google deemed other pages more important.",
|
|
1025
|
+
fix: "Add strong internal links from indexed pages. Submit the URL via Search Console's URL Inspection > Request Indexing. Improve site authority and crawl-budget signals; check no resource constraints (slow server, large response sizes) are deterring the crawl."
|
|
1026
|
+
},
|
|
1027
|
+
server_error: {
|
|
1028
|
+
description: "Google encountered 5xx server errors when trying to crawl these URLs. The pages were unreachable at crawl time.",
|
|
1029
|
+
fix: "Check your server logs for errors. Ensure your hosting can handle Googlebot traffic. Fix any backend issues causing 500/502/503 errors."
|
|
1030
|
+
},
|
|
1031
|
+
unknown_to_google: {
|
|
1032
|
+
description: "These URLs exist on your site but Google hasn't discovered them yet. They may be orphaned pages or missing from your sitemap.",
|
|
1033
|
+
fix: "Add these URLs to your sitemap. Create internal links to them from well-indexed pages. Submit the sitemap in Google Search Console."
|
|
1034
|
+
},
|
|
1035
|
+
stale_crawl: {
|
|
1036
|
+
description: "Google hasn't re-crawled these pages in over 30 days. They may have low perceived value or your crawl budget may be exhausted.",
|
|
1037
|
+
fix: "Update content on these pages to signal freshness. Improve internal linking. Ensure your site loads quickly to maximize crawl budget efficiency."
|
|
1038
|
+
},
|
|
1039
|
+
very_stale_crawl: {
|
|
1040
|
+
description: "Google hasn't visited these pages in over 60 days. They are at risk of being dropped from the index entirely.",
|
|
1041
|
+
fix: "Prioritize updating these pages immediately. Add fresh internal links. Consider requesting re-indexing via Google Search Console's URL Inspection tool."
|
|
1042
|
+
},
|
|
1043
|
+
not_found: {
|
|
1044
|
+
description: "These URLs return 404 errors. Google previously knew about them but they no longer exist.",
|
|
1045
|
+
fix: "If the content moved, add 301 redirects to the new URLs. If intentionally removed, ensure no internal links still point to them. The 404s will clear over time."
|
|
1046
|
+
},
|
|
1047
|
+
soft_404: {
|
|
1048
|
+
description: "These pages return a 200 status but Google detects them as effectively empty or error pages — \"soft\" 404s.",
|
|
1049
|
+
fix: "Return a proper 404 status code for missing pages. If the pages should exist, add meaningful content. Avoid thin placeholder pages."
|
|
1050
|
+
},
|
|
1051
|
+
blocked_robots: {
|
|
1052
|
+
description: "Your robots.txt file is preventing Google from crawling these URLs.",
|
|
1053
|
+
fix: "Review your robots.txt rules. Remove Disallow directives for pages you want indexed. Remember that blocked pages can't be indexed even if linked."
|
|
1054
|
+
},
|
|
1055
|
+
noindex: {
|
|
1056
|
+
description: "These pages have a noindex meta tag or X-Robots-Tag header, telling Google not to include them in search results.",
|
|
1057
|
+
fix: "If these pages should be indexed, remove the noindex directive. Check for noindex in meta tags, HTTP headers, and any SEO plugin configuration."
|
|
1058
|
+
},
|
|
1059
|
+
redirect: {
|
|
1060
|
+
description: "These URLs redirect to other pages. Google follows the redirect and indexes the destination instead.",
|
|
1061
|
+
fix: "This is usually expected behavior. Ensure redirects point to the correct destination. Update internal links to point directly to the final URL to save crawl budget."
|
|
1062
|
+
},
|
|
1063
|
+
canonical_mismatch: {
|
|
1064
|
+
description: "The canonical URL declared on these pages points to a different URL. Google may index the canonical target instead.",
|
|
1065
|
+
fix: "Ensure each page's canonical tag points to itself, or intentionally to the preferred version. Fix any unintended canonical tags added by CMS plugins."
|
|
1066
|
+
},
|
|
1067
|
+
fragment_url: {
|
|
1068
|
+
description: "These URLs contain fragment identifiers (#). Googlebot typically ignores fragments as they're client-side only.",
|
|
1069
|
+
fix: "Avoid using fragment URLs as unique pages. If using client-side routing with hashes, migrate to proper URL paths for better indexability."
|
|
1070
|
+
},
|
|
1071
|
+
not_indexed: {
|
|
1072
|
+
description: "These URLs are not in Google's index. This is a general category — the specific reason may vary.",
|
|
1073
|
+
fix: "Check individual URLs in Google Search Console's URL Inspection tool for specific reasons. Common causes include quality, duplicate content, or crawl issues."
|
|
1074
|
+
}
|
|
1075
|
+
};
|
|
1076
|
+
const nuxtSeoTips = {
|
|
1077
|
+
blocked_robots: {
|
|
1078
|
+
modules: ["@nuxtjs/robots", "@nuxtjs/seo"],
|
|
1079
|
+
tip: "Check nuxt.config robots rules and route rules for Disallow:\n\nexport default defineNuxtConfig({\n robots: { disallow: ['/admin'] },\n routeRules: {\n '/secret/**': { robots: false }\n }\n})"
|
|
1080
|
+
},
|
|
1081
|
+
noindex: {
|
|
1082
|
+
modules: ["@nuxtjs/seo"],
|
|
1083
|
+
tip: "Check for noindex in route rules or page meta:\n\n// nuxt.config.ts\nrouteRules: { '/draft/**': { index: false } }\n\n// pages/draft.vue\ndefinePageMeta({ robots: 'noindex' })"
|
|
1084
|
+
},
|
|
1085
|
+
unknown_to_google: {
|
|
1086
|
+
modules: ["@nuxtjs/sitemap", "@nuxtjs/seo"],
|
|
1087
|
+
tip: "Ensure @nuxtjs/sitemap includes these routes. Dynamic routes need sources:\n\nexport default defineNuxtConfig({\n sitemap: {\n sources: ['/api/__sitemap__/urls']\n }\n})\n\nVerify at /sitemap.xml that these URLs appear."
|
|
1088
|
+
},
|
|
1089
|
+
canonical_mismatch: {
|
|
1090
|
+
modules: ["@nuxtjs/seo"],
|
|
1091
|
+
tip: "@nuxtjs/seo auto-generates canonicals from site.url. Check for conflicts:\n\n// nuxt.config.ts — set your canonical origin\nsite: { url: 'https://example.com' }\n\n// Override per page if needed\nuseHead({ link: [{ rel: 'canonical', href: 'https://example.com/preferred' }] })"
|
|
1092
|
+
},
|
|
1093
|
+
soft_404: {
|
|
1094
|
+
modules: ["@nuxtjs/seo"],
|
|
1095
|
+
tip: "Ensure pages render content server-side, not just client-side. Check for missing data:\n\n// pages/[slug].vue\nconst { data } = await useAsyncData(() => fetchContent(slug))\nif (!data.value)\n throw createError({ statusCode: 404 }) // Return real 404, not empty page"
|
|
1096
|
+
}
|
|
1097
|
+
};
|
|
1098
|
+
const severityOrder = [
|
|
1099
|
+
"error",
|
|
1100
|
+
"warning",
|
|
1101
|
+
"info"
|
|
1102
|
+
];
|
|
1103
|
+
const investigationStatusConfig = {
|
|
1104
|
+
investigated: {
|
|
1105
|
+
label: "Investigated",
|
|
1106
|
+
icon: "i-lucide-check-circle",
|
|
1107
|
+
color: "success"
|
|
1108
|
+
},
|
|
1109
|
+
fixed: {
|
|
1110
|
+
label: "Fixed",
|
|
1111
|
+
icon: "i-lucide-wrench",
|
|
1112
|
+
color: "success"
|
|
1113
|
+
},
|
|
1114
|
+
false_positive: {
|
|
1115
|
+
label: "False positive",
|
|
1116
|
+
icon: "i-lucide-shield-check",
|
|
1117
|
+
color: "info"
|
|
1118
|
+
},
|
|
1119
|
+
wont_fix: {
|
|
1120
|
+
label: "Won't fix",
|
|
1121
|
+
icon: "i-lucide-ban",
|
|
1122
|
+
color: "neutral"
|
|
1123
|
+
},
|
|
1124
|
+
monitoring: {
|
|
1125
|
+
label: "Monitoring",
|
|
1126
|
+
icon: "i-lucide-eye",
|
|
1127
|
+
color: "warning"
|
|
1128
|
+
}
|
|
1129
|
+
};
|
|
1130
|
+
const coverageLabels = {
|
|
1131
|
+
"Crawled - currently not indexed": {
|
|
1132
|
+
short: "Crawled, not indexed",
|
|
1133
|
+
color: "text-error"
|
|
1134
|
+
},
|
|
1135
|
+
"Discovered - currently not indexed": {
|
|
1136
|
+
short: "Discovered, not indexed",
|
|
1137
|
+
color: "text-warning"
|
|
1138
|
+
},
|
|
1139
|
+
"Server error (5xx)": {
|
|
1140
|
+
short: "Server error",
|
|
1141
|
+
color: "text-error"
|
|
1142
|
+
},
|
|
1143
|
+
"Not found (404)": {
|
|
1144
|
+
short: "404",
|
|
1145
|
+
color: "text-error"
|
|
1146
|
+
},
|
|
1147
|
+
"Soft 404": {
|
|
1148
|
+
short: "Soft 404",
|
|
1149
|
+
color: "text-error"
|
|
1150
|
+
},
|
|
1151
|
+
"URL is unknown to Google": {
|
|
1152
|
+
short: "Unknown",
|
|
1153
|
+
color: "text-warning"
|
|
1154
|
+
},
|
|
1155
|
+
"Blocked by robots.txt": {
|
|
1156
|
+
short: "Robots blocked",
|
|
1157
|
+
color: "text-warning"
|
|
1158
|
+
}
|
|
1159
|
+
};
|
|
1160
|
+
function coverageLabel(state) {
|
|
1161
|
+
return coverageLabels[state] || {
|
|
1162
|
+
short: state,
|
|
1163
|
+
color: "text-muted"
|
|
1164
|
+
};
|
|
1165
|
+
}
|
|
1166
|
+
const issueGroups = [
|
|
1167
|
+
{
|
|
1168
|
+
id: "quick-wins",
|
|
1169
|
+
label: "Quick Wins",
|
|
1170
|
+
icon: "i-lucide-zap",
|
|
1171
|
+
description: "Configuration changes you can make right now",
|
|
1172
|
+
effort: "quick",
|
|
1173
|
+
controlLevel: "full",
|
|
1174
|
+
education: "These issues are caused by your site's configuration preventing Google from indexing certain pages. If these pages should be indexed, the fix is usually a one-line config change — remove a robots.txt rule or fix a canonical URL. Highest-ROI fixes, zero content work.",
|
|
1175
|
+
issueTypes: ["blocked_robots", "canonical_mismatch"]
|
|
1176
|
+
},
|
|
1177
|
+
{
|
|
1178
|
+
id: "technical",
|
|
1179
|
+
label: "Technical Fixes",
|
|
1180
|
+
icon: "i-lucide-wrench",
|
|
1181
|
+
description: "Server and URL issues to resolve",
|
|
1182
|
+
effort: "moderate",
|
|
1183
|
+
controlLevel: "full",
|
|
1184
|
+
education: "These are infrastructure problems — your server is returning errors, pages have been deleted without redirects, or pages appear empty to Google. Fix server errors first (they affect crawl budget), then handle 404s with redirects, and ensure pages with real content return proper status codes.",
|
|
1185
|
+
issueTypes: [
|
|
1186
|
+
"server_error",
|
|
1187
|
+
"not_found",
|
|
1188
|
+
"soft_404"
|
|
1189
|
+
]
|
|
1190
|
+
},
|
|
1191
|
+
{
|
|
1192
|
+
id: "content-discovery",
|
|
1193
|
+
label: "Content & Discovery",
|
|
1194
|
+
icon: "i-lucide-file-search",
|
|
1195
|
+
description: "Help Google find and value your pages",
|
|
1196
|
+
effort: "involved",
|
|
1197
|
+
controlLevel: "partial",
|
|
1198
|
+
education: "Google found these pages but either didn't think they were worth indexing, or hasn't discovered them yet. For crawled-but-not-indexed pages, improving content quality and internal linking helps — but Google ultimately decides what to index. For undiscovered pages, adding them to your sitemap and linking to them from indexed pages is the fix.",
|
|
1199
|
+
issueTypes: [
|
|
1200
|
+
"crawled_not_indexed",
|
|
1201
|
+
"discovered_not_indexed",
|
|
1202
|
+
"unknown_to_google",
|
|
1203
|
+
"stale_crawl",
|
|
1204
|
+
"very_stale_crawl"
|
|
1205
|
+
]
|
|
1206
|
+
},
|
|
1207
|
+
{
|
|
1208
|
+
id: "expected",
|
|
1209
|
+
label: "Expected Behavior",
|
|
1210
|
+
icon: "i-lucide-info",
|
|
1211
|
+
description: "Usually intentional — review but likely fine",
|
|
1212
|
+
effort: "quick",
|
|
1213
|
+
controlLevel: "none",
|
|
1214
|
+
education: "These aren't really \"issues\" — they're usually intentional. Noindex tags are set deliberately to keep pages out of search. Redirects are normal when you move pages. Fragment URLs are stripped by Google by design. Review to make sure nothing unexpected is here.",
|
|
1215
|
+
issueTypes: [
|
|
1216
|
+
"noindex",
|
|
1217
|
+
"redirect",
|
|
1218
|
+
"fragment_url"
|
|
1219
|
+
]
|
|
1220
|
+
}
|
|
1221
|
+
];
|
|
1222
|
+
const issueTypeToGroup = Object.fromEntries(issueGroups.flatMap((g) => g.issueTypes.map((t) => [t, g.id])));
|
|
1223
|
+
function enrichIssueDetails(modules) {
|
|
1224
|
+
const moduleNames = new Set(modules?.map((m) => m.name) ?? []);
|
|
1225
|
+
const result = { ...issueDetails };
|
|
1226
|
+
for (const [issueType, nuxtTip] of Object.entries(nuxtSeoTips)) if (nuxtTip.modules.some((m) => moduleNames.has(m)) && result[issueType]) result[issueType] = {
|
|
1227
|
+
...result[issueType],
|
|
1228
|
+
fix: `${result[issueType].fix}\n\nNuxt SEO: ${nuxtTip.tip}`
|
|
1229
|
+
};
|
|
1230
|
+
return result;
|
|
1231
|
+
}
|
|
1232
|
+
function isCustomPeriod(p) {
|
|
1233
|
+
return typeof p === "string" && p.startsWith("custom:");
|
|
1234
|
+
}
|
|
1235
|
+
function parseCustomPeriod(p) {
|
|
1236
|
+
if (!isCustomPeriod(p)) return null;
|
|
1237
|
+
const [, start, end, prevStart, prevEnd] = p.split(":");
|
|
1238
|
+
if (!start || !end) return null;
|
|
1239
|
+
if (prevStart && prevEnd) return {
|
|
1240
|
+
start,
|
|
1241
|
+
end,
|
|
1242
|
+
prevStart,
|
|
1243
|
+
prevEnd
|
|
1244
|
+
};
|
|
1245
|
+
return {
|
|
1246
|
+
start,
|
|
1247
|
+
end
|
|
1248
|
+
};
|
|
1249
|
+
}
|
|
1250
|
+
function todayInPST() {
|
|
1251
|
+
return /* @__PURE__ */ new Date(`${(0, query_exports.currentPstDate)()}T00:00:00`);
|
|
1252
|
+
}
|
|
1253
|
+
const ROLLING_TO_UPSTREAM = {
|
|
1254
|
+
"7d": "last-7d",
|
|
1255
|
+
"28d": "last-28d",
|
|
1256
|
+
"3m": "last-90d",
|
|
1257
|
+
"6m": "last-180d",
|
|
1258
|
+
"12m": "last-365d"
|
|
1259
|
+
};
|
|
1260
|
+
const CALENDAR_TO_UPSTREAM = {
|
|
1261
|
+
"this-month": "mtd",
|
|
1262
|
+
"this-year": "ytd"
|
|
1263
|
+
};
|
|
1264
|
+
function fmt(d) {
|
|
1265
|
+
return format(d, "yyyy-MM-dd");
|
|
1266
|
+
}
|
|
1267
|
+
function buildResultFromIso(start, end) {
|
|
1268
|
+
const startDate = /* @__PURE__ */ new Date(`${start}T00:00:00`);
|
|
1269
|
+
const endDate = /* @__PURE__ */ new Date(`${end}T00:00:00`);
|
|
1270
|
+
const days = Math.round((endDate.getTime() - startDate.getTime()) / 864e5) + 1;
|
|
1271
|
+
const prev = resolveWindow({
|
|
1272
|
+
preset: "custom",
|
|
1273
|
+
start,
|
|
1274
|
+
end,
|
|
1275
|
+
comparison: "prev-period"
|
|
1276
|
+
});
|
|
1277
|
+
const yoy = resolveWindow({
|
|
1278
|
+
preset: "custom",
|
|
1279
|
+
start,
|
|
1280
|
+
end,
|
|
1281
|
+
comparison: "yoy"
|
|
1282
|
+
});
|
|
1283
|
+
return {
|
|
1284
|
+
start,
|
|
1285
|
+
end,
|
|
1286
|
+
prevStart: prev.comparison.start,
|
|
1287
|
+
prevEnd: prev.comparison.end,
|
|
1288
|
+
yearStart: yoy.comparison.start,
|
|
1289
|
+
yearEnd: yoy.comparison.end,
|
|
1290
|
+
days
|
|
1291
|
+
};
|
|
1292
|
+
}
|
|
1293
|
+
function periodToDateRange(period, stableData = true) {
|
|
1294
|
+
const custom = parseCustomPeriod(period);
|
|
1295
|
+
if (custom) {
|
|
1296
|
+
const result = buildResultFromIso(custom.start, custom.end);
|
|
1297
|
+
if (custom.prevStart && custom.prevEnd) return {
|
|
1298
|
+
...result,
|
|
1299
|
+
prevStart: custom.prevStart,
|
|
1300
|
+
prevEnd: custom.prevEnd,
|
|
1301
|
+
yearStart: custom.prevStart,
|
|
1302
|
+
yearEnd: custom.prevEnd
|
|
1303
|
+
};
|
|
1304
|
+
return result;
|
|
1305
|
+
}
|
|
1306
|
+
const today = todayInPST();
|
|
1307
|
+
const end = stableData ? subDays(today, 3) : subDays(today, 1);
|
|
1308
|
+
const endIso = fmt(end);
|
|
1309
|
+
const upstreamPreset = ROLLING_TO_UPSTREAM[period] ?? CALENDAR_TO_UPSTREAM[period];
|
|
1310
|
+
if (upstreamPreset) {
|
|
1311
|
+
const win = resolveWindow({
|
|
1312
|
+
preset: upstreamPreset,
|
|
1313
|
+
anchor: endIso
|
|
1314
|
+
});
|
|
1315
|
+
return buildResultFromIso(win.start, win.end);
|
|
1316
|
+
}
|
|
1317
|
+
let start;
|
|
1318
|
+
switch (period) {
|
|
1319
|
+
case "this-week":
|
|
1320
|
+
start = startOfWeek(end, { weekStartsOn: 1 });
|
|
1321
|
+
break;
|
|
1322
|
+
case "last-month": {
|
|
1323
|
+
const prevMonth = subMonths(end, 1);
|
|
1324
|
+
return buildResultFromIso(fmt(startOfMonth(prevMonth)), fmt(endOfMonth(prevMonth)));
|
|
1325
|
+
}
|
|
1326
|
+
case "this-quarter":
|
|
1327
|
+
start = startOfQuarter(end);
|
|
1328
|
+
break;
|
|
1329
|
+
default: start = subDays(end, 27);
|
|
1330
|
+
}
|
|
1331
|
+
return buildResultFromIso(fmt(start), endIso);
|
|
1332
|
+
}
|
|
1333
|
+
function periodToDays(period) {
|
|
1334
|
+
return periodToDateRange(period).days;
|
|
1335
|
+
}
|
|
1336
|
+
function compareRange(range, mode) {
|
|
1337
|
+
if (mode === "none") return null;
|
|
1338
|
+
if (mode === "year") return {
|
|
1339
|
+
start: range.yearStart,
|
|
1340
|
+
end: range.yearEnd
|
|
1341
|
+
};
|
|
1342
|
+
return {
|
|
1343
|
+
start: range.prevStart,
|
|
1344
|
+
end: range.prevEnd
|
|
1345
|
+
};
|
|
1346
|
+
}
|
|
1347
|
+
function getGscUnstableCutoffDate() {
|
|
1348
|
+
const [y, m, d] = (/* @__PURE__ */ new Date()).toLocaleDateString("en-CA", { timeZone: "America/Los_Angeles" }).split("-").map(Number);
|
|
1349
|
+
const cutoff = new Date(y, m - 1, d - 3);
|
|
1350
|
+
return `${cutoff.getFullYear()}-${String(cutoff.getMonth() + 1).padStart(2, "0")}-${String(cutoff.getDate()).padStart(2, "0")}`;
|
|
1351
|
+
}
|
|
696
1352
|
const HTTP_PROTOCOL_RE = /^http/i;
|
|
697
1353
|
const API_SUFFIX_RE = /\/api\/?$/;
|
|
698
1354
|
const TRAILING_SLASH_RE = /\/+$/;
|
|
@@ -822,6 +1478,263 @@ function createPartnerRealtimeClient(options) {
|
|
|
822
1478
|
};
|
|
823
1479
|
}
|
|
824
1480
|
const createGscdumpRealtimeClient = createPartnerRealtimeClient;
|
|
1481
|
+
function formatCount(value) {
|
|
1482
|
+
return new Intl.NumberFormat("en").format(Math.max(0, Math.round(value)));
|
|
1483
|
+
}
|
|
1484
|
+
function issueCount(issues, ...types) {
|
|
1485
|
+
if (!issues?.length) return 0;
|
|
1486
|
+
const wanted = new Set(types);
|
|
1487
|
+
return issues.reduce((sum, issue) => sum + (wanted.has(issue.type) ? issue.count : 0), 0);
|
|
1488
|
+
}
|
|
1489
|
+
function totalSitemapErrors(sitemaps) {
|
|
1490
|
+
return (sitemaps ?? []).reduce((sum, sitemap) => {
|
|
1491
|
+
return sum + (sitemap.errors ?? 0) + (sitemap.lastError ? 1 : 0);
|
|
1492
|
+
}, 0);
|
|
1493
|
+
}
|
|
1494
|
+
function stage(key, evidence) {
|
|
1495
|
+
return {
|
|
1496
|
+
key,
|
|
1497
|
+
evidence,
|
|
1498
|
+
...{
|
|
1499
|
+
not_connected: {
|
|
1500
|
+
label: "Not connected",
|
|
1501
|
+
severity: "neutral",
|
|
1502
|
+
summary: "Search Console is not connected for this site yet.",
|
|
1503
|
+
primaryAction: "Connect Google Search Console so we can read discovery, indexing, and performance data.",
|
|
1504
|
+
nextStage: "waiting_for_data",
|
|
1505
|
+
sprintFindingTypes: []
|
|
1506
|
+
},
|
|
1507
|
+
waiting_for_data: {
|
|
1508
|
+
label: "Waiting for data",
|
|
1509
|
+
severity: "info",
|
|
1510
|
+
summary: "Search Console is connected, but there is not enough indexing data to diagnose the site yet.",
|
|
1511
|
+
primaryAction: "Let the first sync finish, then make sure a sitemap is submitted.",
|
|
1512
|
+
nextStage: "weak_discovery",
|
|
1513
|
+
sprintFindingTypes: []
|
|
1514
|
+
},
|
|
1515
|
+
weak_discovery: {
|
|
1516
|
+
label: "Weak discovery",
|
|
1517
|
+
severity: "warning",
|
|
1518
|
+
summary: "Google does not have a clean map of the pages you want indexed.",
|
|
1519
|
+
primaryAction: "Submit a clean sitemap that contains only canonical, indexable 200 URLs.",
|
|
1520
|
+
nextStage: "discovery_backlog",
|
|
1521
|
+
sprintFindingTypes: ["search-console-stage", "sitemap-missing"]
|
|
1522
|
+
},
|
|
1523
|
+
discovery_backlog: {
|
|
1524
|
+
label: "Discovery backlog",
|
|
1525
|
+
severity: "warning",
|
|
1526
|
+
summary: "Google knows these pages exist, but is not crawling them fast enough.",
|
|
1527
|
+
primaryAction: "Add internal links from indexed pages and remove low-value URLs from the sitemap.",
|
|
1528
|
+
nextStage: "crawl_blocked",
|
|
1529
|
+
sprintFindingTypes: ["search-console-stage"]
|
|
1530
|
+
},
|
|
1531
|
+
crawl_blocked: {
|
|
1532
|
+
label: "Crawl blocked",
|
|
1533
|
+
severity: "error",
|
|
1534
|
+
summary: "Google is trying to access pages, but technical access problems are blocking progress.",
|
|
1535
|
+
primaryAction: "Fix robots.txt blocks, server errors, broken URLs, and access failures before content work.",
|
|
1536
|
+
nextStage: "indexability_blocked",
|
|
1537
|
+
sprintFindingTypes: ["search-console-stage", "noindex-block"]
|
|
1538
|
+
},
|
|
1539
|
+
indexability_blocked: {
|
|
1540
|
+
label: "Indexability blocked",
|
|
1541
|
+
severity: "error",
|
|
1542
|
+
summary: "Google can reach pages, but index directives or canonical signals are preventing clean indexing.",
|
|
1543
|
+
primaryAction: "Remove accidental noindex directives and make canonical signals agree.",
|
|
1544
|
+
nextStage: "index_rejection",
|
|
1545
|
+
sprintFindingTypes: [
|
|
1546
|
+
"search-console-stage",
|
|
1547
|
+
"noindex-block",
|
|
1548
|
+
"canonicalisation"
|
|
1549
|
+
]
|
|
1550
|
+
},
|
|
1551
|
+
index_rejection: {
|
|
1552
|
+
label: "Index rejection",
|
|
1553
|
+
severity: "warning",
|
|
1554
|
+
summary: "Google is crawling pages but skipping too many of them from the index.",
|
|
1555
|
+
primaryAction: "Improve or consolidate crawled-but-not-indexed pages before publishing more.",
|
|
1556
|
+
nextStage: "partially_indexed",
|
|
1557
|
+
sprintFindingTypes: ["search-console-stage", "pages-not-indexed"]
|
|
1558
|
+
},
|
|
1559
|
+
partially_indexed: {
|
|
1560
|
+
label: "Partially indexed",
|
|
1561
|
+
severity: "warning",
|
|
1562
|
+
summary: "A meaningful share of the site is indexed, but coverage is still below a healthy level.",
|
|
1563
|
+
primaryAction: "Work through the largest remaining indexing blocker first.",
|
|
1564
|
+
nextStage: "indexed_invisible",
|
|
1565
|
+
sprintFindingTypes: ["search-console-stage", "pages-not-indexed"]
|
|
1566
|
+
},
|
|
1567
|
+
indexed_invisible: {
|
|
1568
|
+
label: "Indexed but invisible",
|
|
1569
|
+
severity: "warning",
|
|
1570
|
+
summary: "Pages are indexed, but too many are not earning impressions in Search.",
|
|
1571
|
+
primaryAction: "Improve query targeting, titles, headings, internal links, and page depth.",
|
|
1572
|
+
nextStage: "visible_not_clicked",
|
|
1573
|
+
sprintFindingTypes: ["search-console-stage"]
|
|
1574
|
+
},
|
|
1575
|
+
visible_not_clicked: {
|
|
1576
|
+
label: "Visible but not clicked",
|
|
1577
|
+
severity: "warning",
|
|
1578
|
+
summary: "Google is showing your pages, but searchers are not clicking often enough.",
|
|
1579
|
+
primaryAction: "Rewrite titles and descriptions for the queries already producing impressions.",
|
|
1580
|
+
nextStage: "ranking_stalled",
|
|
1581
|
+
sprintFindingTypes: ["search-console-stage", "ctr-outliers"]
|
|
1582
|
+
},
|
|
1583
|
+
ranking_stalled: {
|
|
1584
|
+
label: "Ranking but stalled",
|
|
1585
|
+
severity: "info",
|
|
1586
|
+
summary: "The site has search visibility, but many pages are not yet ranking in useful positions.",
|
|
1587
|
+
primaryAction: "Prioritise striking-distance pages, refresh content, and add internal links.",
|
|
1588
|
+
nextStage: "healthy_growth_ready",
|
|
1589
|
+
sprintFindingTypes: ["striking-distance", "internal-linking"]
|
|
1590
|
+
},
|
|
1591
|
+
declining_visibility: {
|
|
1592
|
+
label: "Declining visibility",
|
|
1593
|
+
severity: "error",
|
|
1594
|
+
summary: "Search visibility is dropping compared with the previous period.",
|
|
1595
|
+
primaryAction: "Review affected pages, recent releases, competitors, and SERP changes before expanding.",
|
|
1596
|
+
nextStage: "healthy_growth_ready",
|
|
1597
|
+
sprintFindingTypes: ["search-console-stage", "negative-movers"]
|
|
1598
|
+
},
|
|
1599
|
+
healthy_growth_ready: {
|
|
1600
|
+
label: "Healthy, growth ready",
|
|
1601
|
+
severity: "success",
|
|
1602
|
+
summary: "Google can discover, index, and show the site. The next work is growth, not cleanup.",
|
|
1603
|
+
primaryAction: "Use expansion work: striking-distance pages, content gaps, and authority building.",
|
|
1604
|
+
nextStage: null,
|
|
1605
|
+
sprintFindingTypes: ["striking-distance", "competitor-content-gap"]
|
|
1606
|
+
}
|
|
1607
|
+
}[key]
|
|
1608
|
+
};
|
|
1609
|
+
}
|
|
1610
|
+
function classifySearchConsoleStage(input) {
|
|
1611
|
+
const issues = input.issues ?? [];
|
|
1612
|
+
const summary = input.summary ?? null;
|
|
1613
|
+
const sitemaps = input.sitemaps ?? [];
|
|
1614
|
+
const totalUrls = summary?.totalUrls ?? 0;
|
|
1615
|
+
const indexed = summary?.indexed ?? 0;
|
|
1616
|
+
const indexedPercent = summary?.indexedPercent ?? 0;
|
|
1617
|
+
const notIndexed = Math.max(0, totalUrls - indexed);
|
|
1618
|
+
if (!input.connected) return stage("not_connected", [{
|
|
1619
|
+
label: "Connection",
|
|
1620
|
+
value: "Not connected",
|
|
1621
|
+
source: "connection"
|
|
1622
|
+
}]);
|
|
1623
|
+
if (input.indexingStatus === "pending" || !summary || totalUrls === 0) return stage("waiting_for_data", [{
|
|
1624
|
+
label: "Indexing sync",
|
|
1625
|
+
value: input.indexingStatus === "pending" ? "Pending" : "No URLs yet",
|
|
1626
|
+
source: "indexing"
|
|
1627
|
+
}]);
|
|
1628
|
+
const sitemapErrors = totalSitemapErrors(sitemaps);
|
|
1629
|
+
const sitemapUrlCount = sitemaps.reduce((sum, sitemap) => sum + (sitemap.urlCount ?? 0), 0);
|
|
1630
|
+
const unknown = issueCount(issues, "unknown_to_google");
|
|
1631
|
+
const discovered = issueCount(issues, "discovered_not_indexed");
|
|
1632
|
+
const crawled = issueCount(issues, "crawled_not_indexed");
|
|
1633
|
+
const crawlBlocks = issueCount(issues, "blocked_robots", "server_error", "not_found", "soft_404", "access_denied", "forbidden");
|
|
1634
|
+
const indexBlocks = issueCount(issues, "noindex", "canonical_mismatch");
|
|
1635
|
+
const canonicalMismatches = input.canonicalMismatchCount ?? issueCount(issues, "canonical_mismatch");
|
|
1636
|
+
const zeroImpressionPages = (input.pageInventory ?? []).filter((page) => page.impressions === 0).length;
|
|
1637
|
+
const visibleNoClickPages = (input.pageInventory ?? []).filter((page) => page.impressions >= 50 && page.clicks === 0).length;
|
|
1638
|
+
const poorPositionPages = (input.pageInventory ?? []).filter((page) => page.impressions >= 50 && (page.position ?? 0) > 20).length;
|
|
1639
|
+
const pageMoverDropCount = input.pageMoverDropCount ?? 0;
|
|
1640
|
+
const ctrOutlierCount = input.ctrOutlierCount ?? 0;
|
|
1641
|
+
if (summary.change7d != null && summary.change7d <= -5) return stage("declining_visibility", [{
|
|
1642
|
+
label: "Index rate change",
|
|
1643
|
+
value: `${summary.change7d.toFixed(1)}% in 7 days`,
|
|
1644
|
+
source: "indexing"
|
|
1645
|
+
}, {
|
|
1646
|
+
label: "Indexed pages",
|
|
1647
|
+
value: `${formatCount(indexed)} of ${formatCount(totalUrls)}`,
|
|
1648
|
+
source: "indexing"
|
|
1649
|
+
}]);
|
|
1650
|
+
if (pageMoverDropCount > 0) return stage("declining_visibility", [{
|
|
1651
|
+
label: "Falling pages",
|
|
1652
|
+
value: formatCount(pageMoverDropCount),
|
|
1653
|
+
source: "performance"
|
|
1654
|
+
}]);
|
|
1655
|
+
if (sitemaps.length === 0 || sitemapUrlCount === 0 || sitemapErrors > 0 || unknown > Math.max(5, totalUrls * .1)) return stage("weak_discovery", [{
|
|
1656
|
+
label: "Sitemaps",
|
|
1657
|
+
value: sitemaps.length === 0 ? "None registered" : `${formatCount(sitemapErrors)} errors`,
|
|
1658
|
+
source: "sitemap"
|
|
1659
|
+
}, ...unknown > 0 ? [{
|
|
1660
|
+
label: "Unknown URLs",
|
|
1661
|
+
value: formatCount(unknown),
|
|
1662
|
+
source: "indexing"
|
|
1663
|
+
}] : []]);
|
|
1664
|
+
if (discovered > Math.max(10, totalUrls * .15)) return stage("discovery_backlog", [{
|
|
1665
|
+
label: "Discovered, not crawled",
|
|
1666
|
+
value: formatCount(discovered),
|
|
1667
|
+
source: "indexing"
|
|
1668
|
+
}, {
|
|
1669
|
+
label: "Indexed pages",
|
|
1670
|
+
value: `${indexedPercent.toFixed(1)}%`,
|
|
1671
|
+
source: "indexing"
|
|
1672
|
+
}]);
|
|
1673
|
+
if (crawlBlocks > Math.max(5, totalUrls * .05)) return stage("crawl_blocked", [{
|
|
1674
|
+
label: "Crawl blockers",
|
|
1675
|
+
value: formatCount(crawlBlocks),
|
|
1676
|
+
source: "indexing"
|
|
1677
|
+
}, {
|
|
1678
|
+
label: "Indexed pages",
|
|
1679
|
+
value: `${formatCount(indexed)} of ${formatCount(totalUrls)}`,
|
|
1680
|
+
source: "indexing"
|
|
1681
|
+
}]);
|
|
1682
|
+
if (indexBlocks > Math.max(5, totalUrls * .05) || canonicalMismatches > Math.max(5, totalUrls * .05)) return stage("indexability_blocked", [...indexBlocks > 0 ? [{
|
|
1683
|
+
label: "Index signal blockers",
|
|
1684
|
+
value: formatCount(indexBlocks),
|
|
1685
|
+
source: "indexing"
|
|
1686
|
+
}] : [], ...canonicalMismatches > 0 ? [{
|
|
1687
|
+
label: "Canonical mismatches",
|
|
1688
|
+
value: formatCount(canonicalMismatches),
|
|
1689
|
+
source: "canonical"
|
|
1690
|
+
}] : []]);
|
|
1691
|
+
if (crawled > Math.max(10, totalUrls * .15)) return stage("index_rejection", [{
|
|
1692
|
+
label: "Crawled, not indexed",
|
|
1693
|
+
value: formatCount(crawled),
|
|
1694
|
+
source: "indexing"
|
|
1695
|
+
}, {
|
|
1696
|
+
label: "Not indexed",
|
|
1697
|
+
value: formatCount(notIndexed),
|
|
1698
|
+
source: "indexing"
|
|
1699
|
+
}]);
|
|
1700
|
+
if (indexedPercent < 80) return stage("partially_indexed", [{
|
|
1701
|
+
label: "Indexed",
|
|
1702
|
+
value: `${indexedPercent.toFixed(1)}%`,
|
|
1703
|
+
source: "indexing"
|
|
1704
|
+
}, {
|
|
1705
|
+
label: "Not indexed",
|
|
1706
|
+
value: formatCount(notIndexed),
|
|
1707
|
+
source: "indexing"
|
|
1708
|
+
}]);
|
|
1709
|
+
if (zeroImpressionPages >= Math.max(1, (input.pageInventory?.length ?? 0) * .25)) return stage("indexed_invisible", [{
|
|
1710
|
+
label: "No-impression pages",
|
|
1711
|
+
value: formatCount(zeroImpressionPages),
|
|
1712
|
+
source: "performance"
|
|
1713
|
+
}]);
|
|
1714
|
+
if (ctrOutlierCount > 0 || visibleNoClickPages >= Math.max(1, (input.pageInventory?.length ?? 0) * .25)) return stage("visible_not_clicked", [...ctrOutlierCount > 0 ? [{
|
|
1715
|
+
label: "CTR outliers",
|
|
1716
|
+
value: formatCount(ctrOutlierCount),
|
|
1717
|
+
source: "performance"
|
|
1718
|
+
}] : [], ...visibleNoClickPages > 0 ? [{
|
|
1719
|
+
label: "Visible, no clicks",
|
|
1720
|
+
value: formatCount(visibleNoClickPages),
|
|
1721
|
+
source: "performance"
|
|
1722
|
+
}] : []]);
|
|
1723
|
+
if (poorPositionPages >= Math.max(1, (input.pageInventory?.length ?? 0) * .25)) return stage("ranking_stalled", [{
|
|
1724
|
+
label: "Low-ranking visible pages",
|
|
1725
|
+
value: formatCount(poorPositionPages),
|
|
1726
|
+
source: "performance"
|
|
1727
|
+
}]);
|
|
1728
|
+
return stage("healthy_growth_ready", [{
|
|
1729
|
+
label: "Indexed",
|
|
1730
|
+
value: `${indexedPercent.toFixed(1)}%`,
|
|
1731
|
+
source: "indexing"
|
|
1732
|
+
}, {
|
|
1733
|
+
label: "Critical blockers",
|
|
1734
|
+
value: "0",
|
|
1735
|
+
source: "indexing"
|
|
1736
|
+
}]);
|
|
1737
|
+
}
|
|
825
1738
|
const encoder = new TextEncoder();
|
|
826
1739
|
function toPayloadString(payload) {
|
|
827
1740
|
return typeof payload === "string" ? payload : JSON.stringify(payload);
|
|
@@ -896,4 +1809,4 @@ function readWebhookHeaders(headers) {
|
|
|
896
1809
|
signature: headers.signature ?? null
|
|
897
1810
|
};
|
|
898
1811
|
}
|
|
899
|
-
export { ARCHETYPE_EXECUTION_CLASS, CANONICAL_WEBHOOK_EVENTS, PartnerApiError, VALID_WEBHOOK_EVENTS, WEBHOOK_CONTRACT_VERSION, WEBHOOK_CONTRACT_VERSION_HEADER, WEBHOOK_DELIVERY_HEADER, WEBHOOK_EVENT_HEADER, WEBHOOK_SIGNATURE_HEADER, WEBHOOK_TIMESTAMP_HEADER, analyticsStatusToSyncStatus, createAnalyticsClient, createGscdumpClient, createGscdumpRealtimeClient, createPartnerClient, createPartnerRealtimeClient, findLifecycleSite, lifecycleSiteToSyncStatus, lifecycleSiteToUserSite, parseWebhookPayload, readWebhookHeaders, serializeWebhookPayload, toPartnerError, verifyWebhookSignature };
|
|
1812
|
+
export { ARCHETYPE_EXECUTION_CLASS, CANONICAL_WEBHOOK_EVENTS, COMPARE_OPTIONS, COUNTRY_NAMES, CWV_GOOD_CLS, CWV_GOOD_INP, CWV_GOOD_LCP, CWV_POOR_CLS, CWV_POOR_INP, CWV_POOR_LCP, GSC_COLUMN_OPTIONS, GSC_PERIOD_OPTIONS, GSC_PERIOD_OPTIONS_LONG, GSC_STABLE_LATENCY_DAYS, PERIOD_PRESETS, PartnerApiError, VALID_WEBHOOK_EVENTS, WEBHOOK_CONTRACT_VERSION, WEBHOOK_CONTRACT_VERSION_HEADER, WEBHOOK_DELIVERY_HEADER, WEBHOOK_EVENT_HEADER, WEBHOOK_SIGNATURE_HEADER, WEBHOOK_TIMESTAMP_HEADER, analyticsStatusToSyncStatus, andFilter, classifyGscError, classifySearchConsoleStage, coerceRowMetrics, compareRange, countryName, coverageLabel, coverageLabels, createAnalyticsClient, createGscdumpClient, createGscdumpRealtimeClient, createPartnerClient, createPartnerRealtimeClient, cwvBucket, dateFilter, defineGscAnalyzer, enrichIssueDetails, findLifecycleSite, getGscUnstableCutoffDate, gscConsoleUrl, investigationStatusConfig, isCustomPeriod, issueDetails, issueGroups, issueTypeToGroup, lifecycleSiteToSyncStatus, lifecycleSiteToUserSite, nuxtSeoTips, parseCustomPeriod, parseWebhookPayload, periodToDateRange, periodToDays, positionFor, readWebhookHeaders, serializeWebhookPayload, severityOrder, siteUrlToHostname, splitOpportunityTitle, summarizeDailyRows, toPartnerError, truncateQuery, verifyWebhookSignature, weightedAnonPct };
|