@ainyc/canonry 1.27.2 → 1.28.0
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/README.md +37 -0
- package/assets/assets/index-B-SO8jsG.js +246 -0
- package/assets/assets/index-DwPC0zVy.css +1 -0
- package/assets/index.html +2 -2
- package/dist/{chunk-727N35MW.js → chunk-QXLGOYDJ.js} +1714 -278
- package/dist/cli.js +799 -8
- package/dist/index.d.ts +15 -0
- package/dist/index.js +1 -1
- package/package.json +9 -9
- package/assets/assets/index-CE8TwvzO.js +0 -246
- package/assets/assets/index-D786SQZN.css +0 -1
|
@@ -19,6 +19,15 @@ function normalizeGoogleConfig(config) {
|
|
|
19
19
|
scopes: connection.scopes ?? []
|
|
20
20
|
}));
|
|
21
21
|
}
|
|
22
|
+
function normalizeWordpressConfig(config) {
|
|
23
|
+
if (!config.wordpress) return;
|
|
24
|
+
config.wordpress.connections = (config.wordpress.connections ?? []).map((connection) => ({
|
|
25
|
+
...connection,
|
|
26
|
+
url: connection.url.replace(/\/$/, ""),
|
|
27
|
+
stagingUrl: connection.stagingUrl?.replace(/\/$/, ""),
|
|
28
|
+
defaultEnv: connection.defaultEnv ?? "live"
|
|
29
|
+
}));
|
|
30
|
+
}
|
|
22
31
|
function getConfigDir() {
|
|
23
32
|
const override = process.env.CANONRY_CONFIG_DIR?.trim();
|
|
24
33
|
if (override) {
|
|
@@ -63,6 +72,7 @@ Do not write config.yaml by hand; use "canonry init", "canonry settings", or "ca
|
|
|
63
72
|
};
|
|
64
73
|
}
|
|
65
74
|
normalizeGoogleConfig(parsed);
|
|
75
|
+
normalizeWordpressConfig(parsed);
|
|
66
76
|
const portOverride = process.env.CANONRY_PORT?.trim();
|
|
67
77
|
if (portOverride) {
|
|
68
78
|
try {
|
|
@@ -125,6 +135,36 @@ function saveConfig(config) {
|
|
|
125
135
|
const yaml = stringify(merged);
|
|
126
136
|
fs.writeFileSync(configPath, yaml, { encoding: "utf-8", mode: 384 });
|
|
127
137
|
}
|
|
138
|
+
function saveConfigPatch(patch) {
|
|
139
|
+
const configDir = getConfigDir();
|
|
140
|
+
if (!fs.existsSync(configDir)) {
|
|
141
|
+
fs.mkdirSync(configDir, { recursive: true });
|
|
142
|
+
}
|
|
143
|
+
const configPath = getConfigPath();
|
|
144
|
+
let base = {};
|
|
145
|
+
if (fs.existsSync(configPath)) {
|
|
146
|
+
try {
|
|
147
|
+
const raw = fs.readFileSync(configPath, "utf-8");
|
|
148
|
+
base = parse(raw) ?? {};
|
|
149
|
+
} catch {
|
|
150
|
+
base = {};
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
const merged = { ...base, ...patch };
|
|
154
|
+
if (base.database) merged.database = base.database;
|
|
155
|
+
if (base.apiKey) merged.apiKey = base.apiKey;
|
|
156
|
+
if (base.anonymousId) merged.anonymousId = base.anonymousId;
|
|
157
|
+
if (base.dashboardPasswordHash) merged.dashboardPasswordHash = base.dashboardPasswordHash;
|
|
158
|
+
if (base.providers && patch.providers) {
|
|
159
|
+
merged.providers = { ...base.providers };
|
|
160
|
+
for (const [key, patchEntry] of Object.entries(patch.providers)) {
|
|
161
|
+
const baseEntry = base.providers[key] ?? {};
|
|
162
|
+
merged.providers[key] = { ...baseEntry, ...patchEntry };
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
const yaml = stringify(merged);
|
|
166
|
+
fs.writeFileSync(configPath, yaml, { encoding: "utf-8", mode: 384 });
|
|
167
|
+
}
|
|
128
168
|
function configExists() {
|
|
129
169
|
return fs.existsSync(getConfigPath());
|
|
130
170
|
}
|
|
@@ -155,7 +195,7 @@ function getOrCreateAnonymousId() {
|
|
|
155
195
|
if (config.anonymousId) return config.anonymousId;
|
|
156
196
|
const id = crypto.randomUUID();
|
|
157
197
|
config.anonymousId = id;
|
|
158
|
-
|
|
198
|
+
saveConfigPatch(config);
|
|
159
199
|
return id;
|
|
160
200
|
} catch {
|
|
161
201
|
return void 0;
|
|
@@ -203,7 +243,7 @@ function trackEvent(event, properties) {
|
|
|
203
243
|
|
|
204
244
|
// src/server.ts
|
|
205
245
|
import { createRequire as createRequire2 } from "module";
|
|
206
|
-
import
|
|
246
|
+
import crypto22 from "crypto";
|
|
207
247
|
import fs5 from "fs";
|
|
208
248
|
import path6 from "path";
|
|
209
249
|
import { fileURLToPath } from "url";
|
|
@@ -439,6 +479,9 @@ function authRequired() {
|
|
|
439
479
|
function authInvalid() {
|
|
440
480
|
return new AppError("AUTH_INVALID", "Invalid API key", 401);
|
|
441
481
|
}
|
|
482
|
+
function providerError(message, details) {
|
|
483
|
+
return new AppError("PROVIDER_ERROR", message, 502, details);
|
|
484
|
+
}
|
|
442
485
|
function runInProgress(projectName) {
|
|
443
486
|
return new AppError("RUN_IN_PROGRESS", `A run is already in progress for '${projectName}'`, 409);
|
|
444
487
|
}
|
|
@@ -584,78 +627,179 @@ var bingSubmitResultDtoSchema = z6.object({
|
|
|
584
627
|
error: z6.string().optional()
|
|
585
628
|
});
|
|
586
629
|
|
|
587
|
-
// ../contracts/src/
|
|
630
|
+
// ../contracts/src/wordpress.ts
|
|
588
631
|
import { z as z7 } from "zod";
|
|
589
|
-
var
|
|
590
|
-
var
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
632
|
+
var wordpressEnvSchema = z7.enum(["live", "staging"]);
|
|
633
|
+
var wordpressConnectionDtoSchema = z7.object({
|
|
634
|
+
projectName: z7.string(),
|
|
635
|
+
url: z7.string(),
|
|
636
|
+
stagingUrl: z7.string().optional(),
|
|
637
|
+
username: z7.string(),
|
|
638
|
+
defaultEnv: wordpressEnvSchema,
|
|
639
|
+
createdAt: z7.string(),
|
|
640
|
+
updatedAt: z7.string()
|
|
641
|
+
});
|
|
642
|
+
var wordpressSiteStatusDtoSchema = z7.object({
|
|
643
|
+
url: z7.string(),
|
|
644
|
+
reachable: z7.boolean(),
|
|
645
|
+
pageCount: z7.number().nullable().optional(),
|
|
646
|
+
version: z7.string().nullable().optional(),
|
|
647
|
+
error: z7.string().nullable().optional(),
|
|
648
|
+
plugins: z7.array(z7.string()).optional()
|
|
649
|
+
});
|
|
650
|
+
var wordpressStatusDtoSchema = z7.object({
|
|
651
|
+
connected: z7.boolean(),
|
|
652
|
+
projectName: z7.string(),
|
|
653
|
+
defaultEnv: wordpressEnvSchema,
|
|
654
|
+
live: wordpressSiteStatusDtoSchema.nullable(),
|
|
655
|
+
staging: wordpressSiteStatusDtoSchema.nullable(),
|
|
656
|
+
adminUrl: z7.string().nullable().optional()
|
|
657
|
+
});
|
|
658
|
+
var wordpressPageSummaryDtoSchema = z7.object({
|
|
659
|
+
id: z7.number(),
|
|
660
|
+
slug: z7.string(),
|
|
661
|
+
title: z7.string(),
|
|
662
|
+
status: z7.string(),
|
|
663
|
+
modifiedAt: z7.string().nullable().optional(),
|
|
664
|
+
link: z7.string().nullable().optional()
|
|
665
|
+
});
|
|
666
|
+
var wordpressSeoStateDtoSchema = z7.object({
|
|
667
|
+
title: z7.string().nullable(),
|
|
668
|
+
description: z7.string().nullable(),
|
|
669
|
+
noindex: z7.boolean().nullable(),
|
|
670
|
+
writable: z7.boolean().default(false),
|
|
671
|
+
writeTargets: z7.array(z7.string()).default([])
|
|
672
|
+
});
|
|
673
|
+
var wordpressSchemaBlockDtoSchema = z7.object({
|
|
674
|
+
type: z7.string(),
|
|
675
|
+
json: z7.record(z7.string(), z7.unknown())
|
|
676
|
+
});
|
|
677
|
+
var wordpressPageDetailDtoSchema = wordpressPageSummaryDtoSchema.extend({
|
|
678
|
+
env: wordpressEnvSchema,
|
|
679
|
+
content: z7.string(),
|
|
680
|
+
seo: wordpressSeoStateDtoSchema,
|
|
681
|
+
schemaBlocks: z7.array(wordpressSchemaBlockDtoSchema).default([])
|
|
682
|
+
});
|
|
683
|
+
var wordpressDiffPageDtoSchema = wordpressPageDetailDtoSchema.extend({
|
|
684
|
+
contentHash: z7.string(),
|
|
685
|
+
contentSnippet: z7.string()
|
|
686
|
+
});
|
|
687
|
+
var wordpressManualAssistDtoSchema = z7.object({
|
|
688
|
+
manualRequired: z7.literal(true),
|
|
689
|
+
targetUrl: z7.string(),
|
|
690
|
+
adminUrl: z7.string().nullable().optional(),
|
|
691
|
+
content: z7.string(),
|
|
692
|
+
nextSteps: z7.array(z7.string()).default([])
|
|
693
|
+
});
|
|
694
|
+
var wordpressAuditIssueDtoSchema = z7.object({
|
|
695
|
+
slug: z7.string(),
|
|
696
|
+
severity: z7.enum(["high", "medium", "low"]),
|
|
697
|
+
code: z7.enum([
|
|
698
|
+
"noindex",
|
|
699
|
+
"missing-seo-title",
|
|
700
|
+
"missing-meta-description",
|
|
701
|
+
"missing-schema",
|
|
702
|
+
"thin-content"
|
|
703
|
+
]),
|
|
704
|
+
message: z7.string()
|
|
705
|
+
});
|
|
706
|
+
var wordpressAuditPageDtoSchema = z7.object({
|
|
707
|
+
slug: z7.string(),
|
|
708
|
+
title: z7.string(),
|
|
709
|
+
status: z7.string(),
|
|
710
|
+
wordCount: z7.number(),
|
|
711
|
+
seo: wordpressSeoStateDtoSchema,
|
|
712
|
+
schemaPresent: z7.boolean(),
|
|
713
|
+
issues: z7.array(wordpressAuditIssueDtoSchema).default([])
|
|
714
|
+
});
|
|
715
|
+
var wordpressDiffDtoSchema = z7.object({
|
|
716
|
+
slug: z7.string(),
|
|
717
|
+
live: wordpressDiffPageDtoSchema,
|
|
718
|
+
staging: wordpressDiffPageDtoSchema,
|
|
719
|
+
hasDifferences: z7.boolean(),
|
|
720
|
+
differences: z7.object({
|
|
721
|
+
title: z7.boolean(),
|
|
722
|
+
slug: z7.boolean(),
|
|
723
|
+
content: z7.boolean(),
|
|
724
|
+
seoTitle: z7.boolean(),
|
|
725
|
+
seoDescription: z7.boolean(),
|
|
726
|
+
noindex: z7.boolean(),
|
|
727
|
+
schema: z7.boolean()
|
|
728
|
+
})
|
|
729
|
+
});
|
|
730
|
+
|
|
731
|
+
// ../contracts/src/run.ts
|
|
732
|
+
import { z as z8 } from "zod";
|
|
733
|
+
var runStatusSchema = z8.enum(["queued", "running", "completed", "partial", "failed", "cancelled"]);
|
|
734
|
+
var runKindSchema = z8.enum(["answer-visibility", "site-audit", "gsc-sync", "inspect-sitemap"]);
|
|
735
|
+
var runTriggerSchema = z8.enum(["manual", "scheduled", "config-apply"]);
|
|
736
|
+
var citationStateSchema = z8.enum(["cited", "not-cited"]);
|
|
737
|
+
var computedTransitionSchema = z8.enum(["new", "cited", "lost", "emerging", "not-cited"]);
|
|
738
|
+
var runDtoSchema = z8.object({
|
|
739
|
+
id: z8.string(),
|
|
740
|
+
projectId: z8.string(),
|
|
597
741
|
kind: runKindSchema,
|
|
598
742
|
status: runStatusSchema,
|
|
599
743
|
trigger: runTriggerSchema.default("manual"),
|
|
600
|
-
location:
|
|
601
|
-
startedAt:
|
|
602
|
-
finishedAt:
|
|
603
|
-
error:
|
|
604
|
-
createdAt:
|
|
744
|
+
location: z8.string().nullable().optional(),
|
|
745
|
+
startedAt: z8.string().nullable().optional(),
|
|
746
|
+
finishedAt: z8.string().nullable().optional(),
|
|
747
|
+
error: z8.string().nullable().optional(),
|
|
748
|
+
createdAt: z8.string()
|
|
605
749
|
});
|
|
606
|
-
var groundingSourceSchema =
|
|
607
|
-
uri:
|
|
608
|
-
title:
|
|
750
|
+
var groundingSourceSchema = z8.object({
|
|
751
|
+
uri: z8.string(),
|
|
752
|
+
title: z8.string()
|
|
609
753
|
});
|
|
610
|
-
var querySnapshotDtoSchema =
|
|
611
|
-
id:
|
|
612
|
-
runId:
|
|
613
|
-
keywordId:
|
|
614
|
-
keyword:
|
|
754
|
+
var querySnapshotDtoSchema = z8.object({
|
|
755
|
+
id: z8.string(),
|
|
756
|
+
runId: z8.string(),
|
|
757
|
+
keywordId: z8.string(),
|
|
758
|
+
keyword: z8.string().optional(),
|
|
615
759
|
provider: providerNameSchema,
|
|
616
760
|
citationState: citationStateSchema,
|
|
617
761
|
transition: computedTransitionSchema.optional(),
|
|
618
|
-
answerText:
|
|
619
|
-
citedDomains:
|
|
620
|
-
competitorOverlap:
|
|
621
|
-
groundingSources:
|
|
622
|
-
searchQueries:
|
|
623
|
-
model:
|
|
624
|
-
location:
|
|
625
|
-
createdAt:
|
|
762
|
+
answerText: z8.string().nullable().optional(),
|
|
763
|
+
citedDomains: z8.array(z8.string()).default([]),
|
|
764
|
+
competitorOverlap: z8.array(z8.string()).default([]),
|
|
765
|
+
groundingSources: z8.array(groundingSourceSchema).default([]),
|
|
766
|
+
searchQueries: z8.array(z8.string()).default([]),
|
|
767
|
+
model: z8.string().nullable().optional(),
|
|
768
|
+
location: z8.string().nullable().optional(),
|
|
769
|
+
createdAt: z8.string()
|
|
626
770
|
});
|
|
627
|
-
var auditLogEntrySchema =
|
|
628
|
-
id:
|
|
629
|
-
projectId:
|
|
630
|
-
actor:
|
|
631
|
-
action:
|
|
632
|
-
entityType:
|
|
633
|
-
entityId:
|
|
634
|
-
diff:
|
|
635
|
-
createdAt:
|
|
771
|
+
var auditLogEntrySchema = z8.object({
|
|
772
|
+
id: z8.string(),
|
|
773
|
+
projectId: z8.string().nullable().optional(),
|
|
774
|
+
actor: z8.string(),
|
|
775
|
+
action: z8.string(),
|
|
776
|
+
entityType: z8.string(),
|
|
777
|
+
entityId: z8.string().nullable().optional(),
|
|
778
|
+
diff: z8.unknown().optional(),
|
|
779
|
+
createdAt: z8.string()
|
|
636
780
|
});
|
|
637
781
|
|
|
638
782
|
// ../contracts/src/schedule.ts
|
|
639
|
-
import { z as
|
|
640
|
-
var scheduleDtoSchema =
|
|
641
|
-
id:
|
|
642
|
-
projectId:
|
|
643
|
-
cronExpr:
|
|
644
|
-
preset:
|
|
645
|
-
timezone:
|
|
646
|
-
enabled:
|
|
647
|
-
providers:
|
|
648
|
-
lastRunAt:
|
|
649
|
-
nextRunAt:
|
|
650
|
-
createdAt:
|
|
651
|
-
updatedAt:
|
|
783
|
+
import { z as z9 } from "zod";
|
|
784
|
+
var scheduleDtoSchema = z9.object({
|
|
785
|
+
id: z9.string(),
|
|
786
|
+
projectId: z9.string(),
|
|
787
|
+
cronExpr: z9.string(),
|
|
788
|
+
preset: z9.string().nullable().optional(),
|
|
789
|
+
timezone: z9.string().default("UTC"),
|
|
790
|
+
enabled: z9.boolean().default(true),
|
|
791
|
+
providers: z9.array(providerNameSchema).default([]),
|
|
792
|
+
lastRunAt: z9.string().nullable().optional(),
|
|
793
|
+
nextRunAt: z9.string().nullable().optional(),
|
|
794
|
+
createdAt: z9.string(),
|
|
795
|
+
updatedAt: z9.string()
|
|
652
796
|
});
|
|
653
|
-
var scheduleUpsertRequestSchema =
|
|
654
|
-
preset:
|
|
655
|
-
cron:
|
|
656
|
-
timezone:
|
|
657
|
-
enabled:
|
|
658
|
-
providers:
|
|
797
|
+
var scheduleUpsertRequestSchema = z9.object({
|
|
798
|
+
preset: z9.string().optional(),
|
|
799
|
+
cron: z9.string().optional(),
|
|
800
|
+
timezone: z9.string().optional().default("UTC"),
|
|
801
|
+
enabled: z9.boolean().optional().default(true),
|
|
802
|
+
providers: z9.array(providerNameSchema).optional().default([])
|
|
659
803
|
}).refine(
|
|
660
804
|
(data) => data.preset && !data.cron || !data.preset && data.cron,
|
|
661
805
|
{ message: 'Exactly one of "preset" or "cron" must be provided' }
|
|
@@ -754,34 +898,34 @@ function categoryLabel(category) {
|
|
|
754
898
|
}
|
|
755
899
|
|
|
756
900
|
// ../contracts/src/ga.ts
|
|
757
|
-
import { z as
|
|
758
|
-
var ga4ConnectionDtoSchema =
|
|
759
|
-
id:
|
|
760
|
-
projectId:
|
|
761
|
-
propertyId:
|
|
762
|
-
clientEmail:
|
|
763
|
-
connected:
|
|
764
|
-
createdAt:
|
|
765
|
-
updatedAt:
|
|
901
|
+
import { z as z10 } from "zod";
|
|
902
|
+
var ga4ConnectionDtoSchema = z10.object({
|
|
903
|
+
id: z10.string(),
|
|
904
|
+
projectId: z10.string(),
|
|
905
|
+
propertyId: z10.string(),
|
|
906
|
+
clientEmail: z10.string(),
|
|
907
|
+
connected: z10.boolean(),
|
|
908
|
+
createdAt: z10.string(),
|
|
909
|
+
updatedAt: z10.string()
|
|
766
910
|
});
|
|
767
|
-
var ga4TrafficSnapshotDtoSchema =
|
|
768
|
-
date:
|
|
769
|
-
landingPage:
|
|
770
|
-
sessions:
|
|
771
|
-
organicSessions:
|
|
772
|
-
users:
|
|
911
|
+
var ga4TrafficSnapshotDtoSchema = z10.object({
|
|
912
|
+
date: z10.string(),
|
|
913
|
+
landingPage: z10.string(),
|
|
914
|
+
sessions: z10.number(),
|
|
915
|
+
organicSessions: z10.number(),
|
|
916
|
+
users: z10.number()
|
|
773
917
|
});
|
|
774
|
-
var ga4TrafficSummaryDtoSchema =
|
|
775
|
-
totalSessions:
|
|
776
|
-
totalOrganicSessions:
|
|
777
|
-
totalUsers:
|
|
778
|
-
topPages:
|
|
779
|
-
landingPage:
|
|
780
|
-
sessions:
|
|
781
|
-
organicSessions:
|
|
782
|
-
users:
|
|
918
|
+
var ga4TrafficSummaryDtoSchema = z10.object({
|
|
919
|
+
totalSessions: z10.number(),
|
|
920
|
+
totalOrganicSessions: z10.number(),
|
|
921
|
+
totalUsers: z10.number(),
|
|
922
|
+
topPages: z10.array(z10.object({
|
|
923
|
+
landingPage: z10.string(),
|
|
924
|
+
sessions: z10.number(),
|
|
925
|
+
organicSessions: z10.number(),
|
|
926
|
+
users: z10.number()
|
|
783
927
|
})),
|
|
784
|
-
lastSyncedAt:
|
|
928
|
+
lastSyncedAt: z10.string().nullable()
|
|
785
929
|
});
|
|
786
930
|
|
|
787
931
|
// ../api-routes/src/auth.ts
|
|
@@ -3546,6 +3690,19 @@ var analyticsWindowParameter = {
|
|
|
3546
3690
|
description: "Time window for analytics queries.",
|
|
3547
3691
|
schema: { type: "string", enum: ["7d", "30d", "90d", "all"] }
|
|
3548
3692
|
};
|
|
3693
|
+
var wordpressEnvQueryParameter = {
|
|
3694
|
+
name: "env",
|
|
3695
|
+
in: "query",
|
|
3696
|
+
description: "WordPress environment to target.",
|
|
3697
|
+
schema: { type: "string", enum: ["live", "staging"] }
|
|
3698
|
+
};
|
|
3699
|
+
var wordpressSlugQueryParameter = {
|
|
3700
|
+
name: "slug",
|
|
3701
|
+
in: "query",
|
|
3702
|
+
required: true,
|
|
3703
|
+
description: "WordPress page slug.",
|
|
3704
|
+
schema: stringSchema
|
|
3705
|
+
};
|
|
3549
3706
|
var routeCatalog = [
|
|
3550
3707
|
{
|
|
3551
3708
|
method: "get",
|
|
@@ -4897,6 +5054,301 @@ var routeCatalog = [
|
|
|
4897
5054
|
404: { description: "Project not found." }
|
|
4898
5055
|
}
|
|
4899
5056
|
},
|
|
5057
|
+
{
|
|
5058
|
+
method: "post",
|
|
5059
|
+
path: "/api/v1/projects/{name}/wordpress/connect",
|
|
5060
|
+
summary: "Connect WordPress REST access",
|
|
5061
|
+
tags: ["wordpress"],
|
|
5062
|
+
parameters: [nameParameter],
|
|
5063
|
+
requestBody: {
|
|
5064
|
+
required: true,
|
|
5065
|
+
content: {
|
|
5066
|
+
"application/json": {
|
|
5067
|
+
schema: {
|
|
5068
|
+
type: "object",
|
|
5069
|
+
required: ["url", "username", "appPassword"],
|
|
5070
|
+
properties: {
|
|
5071
|
+
url: stringSchema,
|
|
5072
|
+
stagingUrl: stringSchema,
|
|
5073
|
+
username: stringSchema,
|
|
5074
|
+
appPassword: stringSchema,
|
|
5075
|
+
defaultEnv: { type: "string", enum: ["live", "staging"] }
|
|
5076
|
+
}
|
|
5077
|
+
}
|
|
5078
|
+
}
|
|
5079
|
+
}
|
|
5080
|
+
},
|
|
5081
|
+
responses: {
|
|
5082
|
+
200: { description: "WordPress connection status returned." },
|
|
5083
|
+
400: { description: "Invalid WordPress connection request." },
|
|
5084
|
+
404: { description: "Project not found." }
|
|
5085
|
+
}
|
|
5086
|
+
},
|
|
5087
|
+
{
|
|
5088
|
+
method: "delete",
|
|
5089
|
+
path: "/api/v1/projects/{name}/wordpress/disconnect",
|
|
5090
|
+
summary: "Disconnect WordPress",
|
|
5091
|
+
tags: ["wordpress"],
|
|
5092
|
+
parameters: [nameParameter],
|
|
5093
|
+
responses: {
|
|
5094
|
+
204: { description: "WordPress connection deleted." },
|
|
5095
|
+
404: { description: "Project or connection not found." }
|
|
5096
|
+
}
|
|
5097
|
+
},
|
|
5098
|
+
{
|
|
5099
|
+
method: "get",
|
|
5100
|
+
path: "/api/v1/projects/{name}/wordpress/status",
|
|
5101
|
+
summary: "Get WordPress connection status",
|
|
5102
|
+
tags: ["wordpress"],
|
|
5103
|
+
parameters: [nameParameter],
|
|
5104
|
+
responses: {
|
|
5105
|
+
200: { description: "WordPress status returned." },
|
|
5106
|
+
404: { description: "Project not found." }
|
|
5107
|
+
}
|
|
5108
|
+
},
|
|
5109
|
+
{
|
|
5110
|
+
method: "get",
|
|
5111
|
+
path: "/api/v1/projects/{name}/wordpress/pages",
|
|
5112
|
+
summary: "List WordPress pages",
|
|
5113
|
+
tags: ["wordpress"],
|
|
5114
|
+
parameters: [nameParameter, wordpressEnvQueryParameter],
|
|
5115
|
+
responses: {
|
|
5116
|
+
200: { description: "WordPress pages returned." },
|
|
5117
|
+
400: { description: "Invalid environment or missing connection." },
|
|
5118
|
+
404: { description: "Project not found." }
|
|
5119
|
+
}
|
|
5120
|
+
},
|
|
5121
|
+
{
|
|
5122
|
+
method: "get",
|
|
5123
|
+
path: "/api/v1/projects/{name}/wordpress/page",
|
|
5124
|
+
summary: "Get a WordPress page by slug",
|
|
5125
|
+
tags: ["wordpress"],
|
|
5126
|
+
parameters: [nameParameter, wordpressSlugQueryParameter, wordpressEnvQueryParameter],
|
|
5127
|
+
responses: {
|
|
5128
|
+
200: { description: "WordPress page returned." },
|
|
5129
|
+
400: { description: "Invalid slug or environment." },
|
|
5130
|
+
404: { description: "Project, connection, or page not found." }
|
|
5131
|
+
}
|
|
5132
|
+
},
|
|
5133
|
+
{
|
|
5134
|
+
method: "post",
|
|
5135
|
+
path: "/api/v1/projects/{name}/wordpress/pages",
|
|
5136
|
+
summary: "Create a WordPress page",
|
|
5137
|
+
tags: ["wordpress"],
|
|
5138
|
+
parameters: [nameParameter],
|
|
5139
|
+
requestBody: {
|
|
5140
|
+
required: true,
|
|
5141
|
+
content: {
|
|
5142
|
+
"application/json": {
|
|
5143
|
+
schema: {
|
|
5144
|
+
type: "object",
|
|
5145
|
+
required: ["title", "slug", "content"],
|
|
5146
|
+
properties: {
|
|
5147
|
+
title: stringSchema,
|
|
5148
|
+
slug: stringSchema,
|
|
5149
|
+
content: stringSchema,
|
|
5150
|
+
status: stringSchema,
|
|
5151
|
+
env: { type: "string", enum: ["live", "staging"] }
|
|
5152
|
+
}
|
|
5153
|
+
}
|
|
5154
|
+
}
|
|
5155
|
+
}
|
|
5156
|
+
},
|
|
5157
|
+
responses: {
|
|
5158
|
+
200: { description: "WordPress page created." },
|
|
5159
|
+
400: { description: "Invalid page creation request." },
|
|
5160
|
+
404: { description: "Project or connection not found." }
|
|
5161
|
+
}
|
|
5162
|
+
},
|
|
5163
|
+
{
|
|
5164
|
+
method: "put",
|
|
5165
|
+
path: "/api/v1/projects/{name}/wordpress/page",
|
|
5166
|
+
summary: "Update a WordPress page by slug",
|
|
5167
|
+
tags: ["wordpress"],
|
|
5168
|
+
parameters: [nameParameter],
|
|
5169
|
+
requestBody: {
|
|
5170
|
+
required: true,
|
|
5171
|
+
content: {
|
|
5172
|
+
"application/json": {
|
|
5173
|
+
schema: {
|
|
5174
|
+
type: "object",
|
|
5175
|
+
required: ["currentSlug"],
|
|
5176
|
+
properties: {
|
|
5177
|
+
currentSlug: stringSchema,
|
|
5178
|
+
title: stringSchema,
|
|
5179
|
+
slug: stringSchema,
|
|
5180
|
+
content: stringSchema,
|
|
5181
|
+
status: stringSchema,
|
|
5182
|
+
env: { type: "string", enum: ["live", "staging"] }
|
|
5183
|
+
}
|
|
5184
|
+
}
|
|
5185
|
+
}
|
|
5186
|
+
}
|
|
5187
|
+
},
|
|
5188
|
+
responses: {
|
|
5189
|
+
200: { description: "WordPress page updated." },
|
|
5190
|
+
400: { description: "Invalid page update request." },
|
|
5191
|
+
404: { description: "Project, connection, or page not found." }
|
|
5192
|
+
}
|
|
5193
|
+
},
|
|
5194
|
+
{
|
|
5195
|
+
method: "post",
|
|
5196
|
+
path: "/api/v1/projects/{name}/wordpress/page/meta",
|
|
5197
|
+
summary: "Update REST-exposed WordPress SEO meta",
|
|
5198
|
+
tags: ["wordpress"],
|
|
5199
|
+
parameters: [nameParameter],
|
|
5200
|
+
requestBody: {
|
|
5201
|
+
required: true,
|
|
5202
|
+
content: {
|
|
5203
|
+
"application/json": {
|
|
5204
|
+
schema: {
|
|
5205
|
+
type: "object",
|
|
5206
|
+
required: ["slug"],
|
|
5207
|
+
properties: {
|
|
5208
|
+
slug: stringSchema,
|
|
5209
|
+
title: stringSchema,
|
|
5210
|
+
description: stringSchema,
|
|
5211
|
+
noindex: booleanSchema,
|
|
5212
|
+
env: { type: "string", enum: ["live", "staging"] }
|
|
5213
|
+
}
|
|
5214
|
+
}
|
|
5215
|
+
}
|
|
5216
|
+
}
|
|
5217
|
+
},
|
|
5218
|
+
responses: {
|
|
5219
|
+
200: { description: "WordPress SEO meta updated." },
|
|
5220
|
+
400: { description: "SEO meta is unsupported or the request is invalid." },
|
|
5221
|
+
404: { description: "Project, connection, or page not found." }
|
|
5222
|
+
}
|
|
5223
|
+
},
|
|
5224
|
+
{
|
|
5225
|
+
method: "get",
|
|
5226
|
+
path: "/api/v1/projects/{name}/wordpress/schema",
|
|
5227
|
+
summary: "Read rendered JSON-LD schema for a page",
|
|
5228
|
+
tags: ["wordpress"],
|
|
5229
|
+
parameters: [nameParameter, wordpressSlugQueryParameter, wordpressEnvQueryParameter],
|
|
5230
|
+
responses: {
|
|
5231
|
+
200: { description: "WordPress schema blocks returned." },
|
|
5232
|
+
400: { description: "Invalid slug or environment." },
|
|
5233
|
+
404: { description: "Project, connection, or page not found." }
|
|
5234
|
+
}
|
|
5235
|
+
},
|
|
5236
|
+
{
|
|
5237
|
+
method: "post",
|
|
5238
|
+
path: "/api/v1/projects/{name}/wordpress/schema/manual",
|
|
5239
|
+
summary: "Generate a manual schema update payload",
|
|
5240
|
+
tags: ["wordpress"],
|
|
5241
|
+
parameters: [nameParameter],
|
|
5242
|
+
requestBody: {
|
|
5243
|
+
required: true,
|
|
5244
|
+
content: {
|
|
5245
|
+
"application/json": {
|
|
5246
|
+
schema: {
|
|
5247
|
+
type: "object",
|
|
5248
|
+
required: ["slug", "json"],
|
|
5249
|
+
properties: {
|
|
5250
|
+
slug: stringSchema,
|
|
5251
|
+
type: stringSchema,
|
|
5252
|
+
json: stringSchema,
|
|
5253
|
+
env: { type: "string", enum: ["live", "staging"] }
|
|
5254
|
+
}
|
|
5255
|
+
}
|
|
5256
|
+
}
|
|
5257
|
+
}
|
|
5258
|
+
},
|
|
5259
|
+
responses: {
|
|
5260
|
+
200: { description: "Manual schema instructions returned." },
|
|
5261
|
+
400: { description: "Invalid schema request." },
|
|
5262
|
+
404: { description: "Project, connection, or page not found." }
|
|
5263
|
+
}
|
|
5264
|
+
},
|
|
5265
|
+
{
|
|
5266
|
+
method: "get",
|
|
5267
|
+
path: "/api/v1/projects/{name}/wordpress/llms-txt",
|
|
5268
|
+
summary: "Read /llms.txt for a WordPress environment",
|
|
5269
|
+
tags: ["wordpress"],
|
|
5270
|
+
parameters: [nameParameter, wordpressEnvQueryParameter],
|
|
5271
|
+
responses: {
|
|
5272
|
+
200: { description: "llms.txt returned." },
|
|
5273
|
+
400: { description: "Invalid environment or missing connection." },
|
|
5274
|
+
404: { description: "Project not found." }
|
|
5275
|
+
}
|
|
5276
|
+
},
|
|
5277
|
+
{
|
|
5278
|
+
method: "post",
|
|
5279
|
+
path: "/api/v1/projects/{name}/wordpress/llms-txt/manual",
|
|
5280
|
+
summary: "Generate a manual llms.txt update payload",
|
|
5281
|
+
tags: ["wordpress"],
|
|
5282
|
+
parameters: [nameParameter],
|
|
5283
|
+
requestBody: {
|
|
5284
|
+
required: true,
|
|
5285
|
+
content: {
|
|
5286
|
+
"application/json": {
|
|
5287
|
+
schema: {
|
|
5288
|
+
type: "object",
|
|
5289
|
+
required: ["content"],
|
|
5290
|
+
properties: {
|
|
5291
|
+
content: stringSchema,
|
|
5292
|
+
env: { type: "string", enum: ["live", "staging"] }
|
|
5293
|
+
}
|
|
5294
|
+
}
|
|
5295
|
+
}
|
|
5296
|
+
}
|
|
5297
|
+
},
|
|
5298
|
+
responses: {
|
|
5299
|
+
200: { description: "Manual llms.txt instructions returned." },
|
|
5300
|
+
400: { description: "Invalid llms.txt request." },
|
|
5301
|
+
404: { description: "Project or connection not found." }
|
|
5302
|
+
}
|
|
5303
|
+
},
|
|
5304
|
+
{
|
|
5305
|
+
method: "get",
|
|
5306
|
+
path: "/api/v1/projects/{name}/wordpress/audit",
|
|
5307
|
+
summary: "Audit WordPress pages for SEO and content issues",
|
|
5308
|
+
tags: ["wordpress"],
|
|
5309
|
+
parameters: [nameParameter, wordpressEnvQueryParameter],
|
|
5310
|
+
responses: {
|
|
5311
|
+
200: { description: "WordPress audit returned." },
|
|
5312
|
+
400: { description: "Invalid environment or missing connection." },
|
|
5313
|
+
404: { description: "Project not found." }
|
|
5314
|
+
}
|
|
5315
|
+
},
|
|
5316
|
+
{
|
|
5317
|
+
method: "get",
|
|
5318
|
+
path: "/api/v1/projects/{name}/wordpress/diff",
|
|
5319
|
+
summary: "Compare live and staging versions of a WordPress page",
|
|
5320
|
+
tags: ["wordpress"],
|
|
5321
|
+
parameters: [nameParameter, wordpressSlugQueryParameter],
|
|
5322
|
+
responses: {
|
|
5323
|
+
200: { description: "WordPress diff returned." },
|
|
5324
|
+
400: { description: "Invalid slug or missing staging configuration." },
|
|
5325
|
+
404: { description: "Project, connection, or page not found." }
|
|
5326
|
+
}
|
|
5327
|
+
},
|
|
5328
|
+
{
|
|
5329
|
+
method: "get",
|
|
5330
|
+
path: "/api/v1/projects/{name}/wordpress/staging/status",
|
|
5331
|
+
summary: "Get WordPress staging configuration status",
|
|
5332
|
+
tags: ["wordpress"],
|
|
5333
|
+
parameters: [nameParameter],
|
|
5334
|
+
responses: {
|
|
5335
|
+
200: { description: "WordPress staging status returned." },
|
|
5336
|
+
400: { description: "WordPress is not configured for this project." },
|
|
5337
|
+
404: { description: "Project not found." }
|
|
5338
|
+
}
|
|
5339
|
+
},
|
|
5340
|
+
{
|
|
5341
|
+
method: "post",
|
|
5342
|
+
path: "/api/v1/projects/{name}/wordpress/staging/push",
|
|
5343
|
+
summary: "Generate a manual staging push handoff",
|
|
5344
|
+
tags: ["wordpress"],
|
|
5345
|
+
parameters: [nameParameter],
|
|
5346
|
+
responses: {
|
|
5347
|
+
200: { description: "Manual staging push instructions returned." },
|
|
5348
|
+
400: { description: "Missing staging configuration." },
|
|
5349
|
+
404: { description: "Project or connection not found." }
|
|
5350
|
+
}
|
|
5351
|
+
},
|
|
4900
5352
|
// GA4 routes
|
|
4901
5353
|
{
|
|
4902
5354
|
method: "post",
|
|
@@ -6086,17 +6538,22 @@ async function googleRoutes(app, opts) {
|
|
|
6086
6538
|
app.get("/projects/:name/google/gsc/coverage", async (request) => {
|
|
6087
6539
|
const project = resolveProject(app.db, request.params.name);
|
|
6088
6540
|
const allInspections = app.db.select().from(gscUrlInspections).where(eq13(gscUrlInspections.projectId, project.id)).orderBy(desc4(gscUrlInspections.inspectedAt)).all();
|
|
6541
|
+
const canonicalUrl = (url) => url.replace(/^http:\/\//, "https://");
|
|
6089
6542
|
const latestByUrl = /* @__PURE__ */ new Map();
|
|
6090
6543
|
const historyByUrl = /* @__PURE__ */ new Map();
|
|
6091
6544
|
for (const row of allInspections) {
|
|
6092
|
-
|
|
6093
|
-
|
|
6094
|
-
|
|
6095
|
-
|
|
6545
|
+
const key = canonicalUrl(row.url);
|
|
6546
|
+
const existing = latestByUrl.get(key);
|
|
6547
|
+
if (!existing) {
|
|
6548
|
+
latestByUrl.set(key, row);
|
|
6549
|
+
} else if (existing.url.startsWith("http://") && row.url.startsWith("https://")) {
|
|
6550
|
+
latestByUrl.set(key, row);
|
|
6551
|
+
}
|
|
6552
|
+
const history = historyByUrl.get(key);
|
|
6096
6553
|
if (history) {
|
|
6097
6554
|
history.push(row);
|
|
6098
6555
|
} else {
|
|
6099
|
-
historyByUrl.set(
|
|
6556
|
+
historyByUrl.set(key, [row]);
|
|
6100
6557
|
}
|
|
6101
6558
|
}
|
|
6102
6559
|
const indexedUrls = [];
|
|
@@ -6636,10 +7093,22 @@ async function bingRoutes(app, opts) {
|
|
|
6636
7093
|
if (!conn) return;
|
|
6637
7094
|
const allInspections = app.db.select().from(bingUrlInspections).where(eq14(bingUrlInspections.projectId, project.id)).orderBy(desc5(bingUrlInspections.inspectedAt)).all();
|
|
6638
7095
|
const latestByUrl = /* @__PURE__ */ new Map();
|
|
7096
|
+
const definitiveByUrl = /* @__PURE__ */ new Map();
|
|
6639
7097
|
for (const row of allInspections) {
|
|
6640
7098
|
if (!latestByUrl.has(row.url)) {
|
|
6641
7099
|
latestByUrl.set(row.url, row);
|
|
6642
7100
|
}
|
|
7101
|
+
if (!definitiveByUrl.has(row.url) && row.inIndex != null) {
|
|
7102
|
+
definitiveByUrl.set(row.url, row);
|
|
7103
|
+
}
|
|
7104
|
+
}
|
|
7105
|
+
for (const [url, latest] of latestByUrl) {
|
|
7106
|
+
if (latest.inIndex == null) {
|
|
7107
|
+
const definitive = definitiveByUrl.get(url);
|
|
7108
|
+
if (definitive) {
|
|
7109
|
+
latestByUrl.set(url, definitive);
|
|
7110
|
+
}
|
|
7111
|
+
}
|
|
6643
7112
|
}
|
|
6644
7113
|
const indexedUrls = [];
|
|
6645
7114
|
const notIndexedUrls = [];
|
|
@@ -7569,22 +8038,1001 @@ async function ga4Routes(app, opts) {
|
|
|
7569
8038
|
});
|
|
7570
8039
|
}
|
|
7571
8040
|
|
|
7572
|
-
// ../
|
|
7573
|
-
|
|
7574
|
-
|
|
7575
|
-
|
|
7576
|
-
|
|
7577
|
-
|
|
7578
|
-
|
|
7579
|
-
|
|
7580
|
-
|
|
7581
|
-
|
|
7582
|
-
|
|
7583
|
-
|
|
7584
|
-
|
|
7585
|
-
|
|
7586
|
-
|
|
7587
|
-
|
|
8041
|
+
// ../integration-wordpress/src/types.ts
|
|
8042
|
+
var WordpressApiError = class extends Error {
|
|
8043
|
+
statusCode;
|
|
8044
|
+
code;
|
|
8045
|
+
constructor(code, message, statusCode) {
|
|
8046
|
+
super(message);
|
|
8047
|
+
this.name = "WordpressApiError";
|
|
8048
|
+
this.code = code;
|
|
8049
|
+
this.statusCode = statusCode;
|
|
8050
|
+
}
|
|
8051
|
+
};
|
|
8052
|
+
|
|
8053
|
+
// ../integration-wordpress/src/wordpress-client.ts
|
|
8054
|
+
import crypto17 from "crypto";
|
|
8055
|
+
var PAGE_FIELDS = "id,slug,status,link,modified,modified_gmt,title,content,meta";
|
|
8056
|
+
var PAGE_LIST_FIELDS = "id,slug,status,link,modified,modified_gmt,title";
|
|
8057
|
+
var VERIFY_PAGE_FIELDS = "id,status";
|
|
8058
|
+
var SEO_TARGETS = [
|
|
8059
|
+
{
|
|
8060
|
+
pluginHints: ["wordpress-seo", "yoast"],
|
|
8061
|
+
titleKey: "_yoast_wpseo_title",
|
|
8062
|
+
descriptionKey: "_yoast_wpseo_metadesc",
|
|
8063
|
+
noindexKey: "_yoast_wpseo_meta-robots-noindex"
|
|
8064
|
+
},
|
|
8065
|
+
{
|
|
8066
|
+
pluginHints: ["all-in-one-seo-pack", "aioseo"],
|
|
8067
|
+
titleKey: "_aioseo_title",
|
|
8068
|
+
descriptionKey: "_aioseo_description",
|
|
8069
|
+
noindexKey: "_aioseo_noindex"
|
|
8070
|
+
},
|
|
8071
|
+
{
|
|
8072
|
+
pluginHints: ["seo-by-rank-math", "rank-math"],
|
|
8073
|
+
titleKey: "rank_math_title",
|
|
8074
|
+
descriptionKey: "rank_math_description",
|
|
8075
|
+
noindexKey: "rank_math_robots"
|
|
8076
|
+
}
|
|
8077
|
+
];
|
|
8078
|
+
var THIN_CONTENT_WORD_COUNT = 250;
|
|
8079
|
+
function normalizeSiteUrl(url) {
|
|
8080
|
+
return url.replace(/\/$/, "");
|
|
8081
|
+
}
|
|
8082
|
+
function encodeBasicAuth(username, appPassword) {
|
|
8083
|
+
return Buffer.from(`${username}:${appPassword}`).toString("base64");
|
|
8084
|
+
}
|
|
8085
|
+
async function fetchJson(connection, siteUrl, path7, init) {
|
|
8086
|
+
const res = await fetch(`${normalizeSiteUrl(siteUrl)}${path7}`, {
|
|
8087
|
+
...init,
|
|
8088
|
+
headers: {
|
|
8089
|
+
"Authorization": `Basic ${encodeBasicAuth(connection.username, connection.appPassword)}`,
|
|
8090
|
+
...init?.body != null ? { "Content-Type": "application/json" } : {},
|
|
8091
|
+
...init?.headers ?? {}
|
|
8092
|
+
}
|
|
8093
|
+
});
|
|
8094
|
+
if (res.status === 401 || res.status === 403) {
|
|
8095
|
+
throw new WordpressApiError("AUTH_INVALID", "WordPress credentials are invalid or lack permission for this action", res.status);
|
|
8096
|
+
}
|
|
8097
|
+
if (res.status === 404) {
|
|
8098
|
+
throw new WordpressApiError("NOT_FOUND", "WordPress endpoint not found", 404);
|
|
8099
|
+
}
|
|
8100
|
+
if (!res.ok) {
|
|
8101
|
+
const text2 = await res.text().catch(() => "");
|
|
8102
|
+
throw new WordpressApiError("UPSTREAM_ERROR", `WordPress API error (${res.status}): ${text2 || res.statusText}`, res.status);
|
|
8103
|
+
}
|
|
8104
|
+
return {
|
|
8105
|
+
body: await res.json(),
|
|
8106
|
+
response: res
|
|
8107
|
+
};
|
|
8108
|
+
}
|
|
8109
|
+
async function fetchPageCollectionSummary(connection, siteUrl, options) {
|
|
8110
|
+
const params = new URLSearchParams({
|
|
8111
|
+
per_page: "1",
|
|
8112
|
+
_fields: VERIFY_PAGE_FIELDS
|
|
8113
|
+
});
|
|
8114
|
+
if (options?.context) {
|
|
8115
|
+
params.set("context", options.context);
|
|
8116
|
+
}
|
|
8117
|
+
const { response } = await fetchJson(
|
|
8118
|
+
connection,
|
|
8119
|
+
siteUrl,
|
|
8120
|
+
`/wp-json/wp/v2/pages?${params.toString()}`
|
|
8121
|
+
);
|
|
8122
|
+
return response;
|
|
8123
|
+
}
|
|
8124
|
+
async function fetchText(url) {
|
|
8125
|
+
try {
|
|
8126
|
+
const res = await fetch(url);
|
|
8127
|
+
if (!res.ok) return null;
|
|
8128
|
+
return await res.text();
|
|
8129
|
+
} catch {
|
|
8130
|
+
return null;
|
|
8131
|
+
}
|
|
8132
|
+
}
|
|
8133
|
+
function stripHtml(input) {
|
|
8134
|
+
return input.replace(/<script[\s\S]*?<\/script>/gi, " ").replace(/<style[\s\S]*?<\/style>/gi, " ").replace(/<[^>]+>/g, " ").replace(/ /gi, " ").replace(/&/gi, "&").replace(/"/gi, '"').replace(/'/gi, "'").replace(/</gi, "<").replace(/>/gi, ">").replace(/\s+/g, " ").trim();
|
|
8135
|
+
}
|
|
8136
|
+
function extractMetaContent(html, name) {
|
|
8137
|
+
const patterns = [
|
|
8138
|
+
new RegExp(`<meta[^>]+name=["']${name}["'][^>]+content=["']([^"']*)["'][^>]*>`, "i"),
|
|
8139
|
+
new RegExp(`<meta[^>]+content=["']([^"']*)["'][^>]+name=["']${name}["'][^>]*>`, "i")
|
|
8140
|
+
];
|
|
8141
|
+
for (const pattern of patterns) {
|
|
8142
|
+
const match = pattern.exec(html);
|
|
8143
|
+
if (match?.[1] != null) return match[1].trim() || null;
|
|
8144
|
+
}
|
|
8145
|
+
return null;
|
|
8146
|
+
}
|
|
8147
|
+
function extractTitle(html) {
|
|
8148
|
+
const match = /<title[^>]*>([\s\S]*?)<\/title>/i.exec(html);
|
|
8149
|
+
return match?.[1] ? stripHtml(match[1]) || null : null;
|
|
8150
|
+
}
|
|
8151
|
+
function extractGeneratorVersion(html) {
|
|
8152
|
+
const generator = extractMetaContent(html, "generator");
|
|
8153
|
+
if (!generator) return null;
|
|
8154
|
+
const match = /WordPress\s+([0-9][^ ]*)/i.exec(generator);
|
|
8155
|
+
return match?.[1] ?? generator;
|
|
8156
|
+
}
|
|
8157
|
+
function extractSchemaBlocks(html) {
|
|
8158
|
+
const blocks = [];
|
|
8159
|
+
const regex = /<script[^>]+type=["']application\/ld\+json["'][^>]*>([\s\S]*?)<\/script>/gi;
|
|
8160
|
+
let match;
|
|
8161
|
+
while ((match = regex.exec(html)) !== null) {
|
|
8162
|
+
const raw = match[1]?.trim();
|
|
8163
|
+
if (!raw) continue;
|
|
8164
|
+
try {
|
|
8165
|
+
const parsed = JSON.parse(raw);
|
|
8166
|
+
const items = Array.isArray(parsed) ? parsed : [parsed];
|
|
8167
|
+
for (const item of items) {
|
|
8168
|
+
const type = typeof item["@type"] === "string" ? String(item["@type"]) : Array.isArray(item["@type"]) ? String(item["@type"][0] ?? "Unknown") : "Unknown";
|
|
8169
|
+
blocks.push({
|
|
8170
|
+
type,
|
|
8171
|
+
json: item
|
|
8172
|
+
});
|
|
8173
|
+
}
|
|
8174
|
+
} catch {
|
|
8175
|
+
continue;
|
|
8176
|
+
}
|
|
8177
|
+
}
|
|
8178
|
+
return blocks;
|
|
8179
|
+
}
|
|
8180
|
+
function summarizeSeoFromHtml(html) {
|
|
8181
|
+
const description = extractMetaContent(html, "description");
|
|
8182
|
+
const robots = extractMetaContent(html, "robots");
|
|
8183
|
+
return {
|
|
8184
|
+
title: extractTitle(html),
|
|
8185
|
+
description,
|
|
8186
|
+
noindex: robots == null ? null : /\bnoindex\b/i.test(robots)
|
|
8187
|
+
};
|
|
8188
|
+
}
|
|
8189
|
+
function computeWordCount(content) {
|
|
8190
|
+
const clean = stripHtml(content);
|
|
8191
|
+
if (!clean) return 0;
|
|
8192
|
+
return clean.split(/\s+/).filter(Boolean).length;
|
|
8193
|
+
}
|
|
8194
|
+
function buildSnippet(content) {
|
|
8195
|
+
const text2 = stripHtml(content);
|
|
8196
|
+
if (text2.length <= 160) return text2;
|
|
8197
|
+
return `${text2.slice(0, 157)}...`;
|
|
8198
|
+
}
|
|
8199
|
+
function contentHash(content) {
|
|
8200
|
+
return crypto17.createHash("sha256").update(content).digest("hex");
|
|
8201
|
+
}
|
|
8202
|
+
function buildAmbiguousSlugMessage(slug, pages) {
|
|
8203
|
+
const candidates = pages.map((page) => {
|
|
8204
|
+
const title = stripHtml(page.title?.rendered ?? "") || "(untitled)";
|
|
8205
|
+
return `#${page.id} "${title}"`;
|
|
8206
|
+
}).join(", ");
|
|
8207
|
+
return `Multiple pages matched slug "${slug}". Candidates: ${candidates}.`;
|
|
8208
|
+
}
|
|
8209
|
+
async function mapWithConcurrency(items, limit, iteratee) {
|
|
8210
|
+
const results = new Array(items.length);
|
|
8211
|
+
let nextIndex = 0;
|
|
8212
|
+
async function worker() {
|
|
8213
|
+
while (nextIndex < items.length) {
|
|
8214
|
+
const index2 = nextIndex;
|
|
8215
|
+
nextIndex += 1;
|
|
8216
|
+
results[index2] = await iteratee(items[index2], index2);
|
|
8217
|
+
}
|
|
8218
|
+
}
|
|
8219
|
+
const workerCount = Math.max(1, Math.min(limit, items.length));
|
|
8220
|
+
await Promise.all(Array.from({ length: workerCount }, () => worker()));
|
|
8221
|
+
return results;
|
|
8222
|
+
}
|
|
8223
|
+
function resolveSeoWriteTargets(meta, plugins) {
|
|
8224
|
+
if (!meta) return [];
|
|
8225
|
+
const keys = new Set(Object.keys(meta));
|
|
8226
|
+
const pluginList = plugins ?? [];
|
|
8227
|
+
const targets = [];
|
|
8228
|
+
for (const target of SEO_TARGETS) {
|
|
8229
|
+
const hinted = target.pluginHints.some((hint) => pluginList.some((plugin) => plugin.includes(hint)));
|
|
8230
|
+
if (!hinted && !keys.has(target.titleKey) && !keys.has(target.descriptionKey) && !keys.has(target.noindexKey)) {
|
|
8231
|
+
continue;
|
|
8232
|
+
}
|
|
8233
|
+
if (keys.has(target.titleKey)) targets.push(target.titleKey);
|
|
8234
|
+
if (keys.has(target.descriptionKey)) targets.push(target.descriptionKey);
|
|
8235
|
+
if (keys.has(target.noindexKey)) targets.push(target.noindexKey);
|
|
8236
|
+
}
|
|
8237
|
+
return [...new Set(targets)];
|
|
8238
|
+
}
|
|
8239
|
+
function buildSeoState(page, html, plugins) {
|
|
8240
|
+
const renderedSeo = html ? summarizeSeoFromHtml(html) : { title: null, description: null, noindex: null };
|
|
8241
|
+
const writeTargets = resolveSeoWriteTargets(page.meta, plugins);
|
|
8242
|
+
return {
|
|
8243
|
+
title: renderedSeo.title,
|
|
8244
|
+
description: renderedSeo.description,
|
|
8245
|
+
noindex: renderedSeo.noindex,
|
|
8246
|
+
writable: writeTargets.length > 0,
|
|
8247
|
+
writeTargets
|
|
8248
|
+
};
|
|
8249
|
+
}
|
|
8250
|
+
function resolveEnvironment(connection, requestedEnv) {
|
|
8251
|
+
const env = requestedEnv ?? connection.defaultEnv;
|
|
8252
|
+
if (env === "staging") {
|
|
8253
|
+
if (!connection.stagingUrl) {
|
|
8254
|
+
throw new WordpressApiError("VALIDATION_ERROR", "No staging URL configured for this project. Reconnect with --staging-url or use --live.", 400);
|
|
8255
|
+
}
|
|
8256
|
+
return { env, siteUrl: normalizeSiteUrl(connection.stagingUrl) };
|
|
8257
|
+
}
|
|
8258
|
+
return { env: "live", siteUrl: normalizeSiteUrl(connection.url) };
|
|
8259
|
+
}
|
|
8260
|
+
function getWpStagingAdminUrl(url) {
|
|
8261
|
+
return `${normalizeSiteUrl(url)}/wp-admin/admin.php?page=wpstg_clone`;
|
|
8262
|
+
}
|
|
8263
|
+
async function verifyWordpressConnection(connection) {
|
|
8264
|
+
const site = resolveEnvironment({ ...connection, defaultEnv: "live" }, "live");
|
|
8265
|
+
const response = await fetchPageCollectionSummary(connection, site.siteUrl, { context: "view" });
|
|
8266
|
+
const homeHtml = await fetchText(site.siteUrl);
|
|
8267
|
+
return {
|
|
8268
|
+
url: site.siteUrl,
|
|
8269
|
+
reachable: true,
|
|
8270
|
+
pageCount: Number.parseInt(response.headers.get("x-wp-total") ?? "0", 10) || 0,
|
|
8271
|
+
version: homeHtml ? extractGeneratorVersion(homeHtml) : null,
|
|
8272
|
+
plugins: []
|
|
8273
|
+
};
|
|
8274
|
+
}
|
|
8275
|
+
async function getSiteStatus(connection, env) {
|
|
8276
|
+
const site = resolveEnvironment(connection, env);
|
|
8277
|
+
try {
|
|
8278
|
+
const response = await fetchPageCollectionSummary(connection, site.siteUrl, { context: "view" });
|
|
8279
|
+
const homeHtml = await fetchText(site.siteUrl);
|
|
8280
|
+
const plugins = await listActivePlugins(connection, env);
|
|
8281
|
+
return {
|
|
8282
|
+
url: site.siteUrl,
|
|
8283
|
+
reachable: true,
|
|
8284
|
+
pageCount: Number.parseInt(response.headers.get("x-wp-total") ?? "0", 10) || 0,
|
|
8285
|
+
version: homeHtml ? extractGeneratorVersion(homeHtml) : null,
|
|
8286
|
+
plugins: plugins ?? []
|
|
8287
|
+
};
|
|
8288
|
+
} catch (error) {
|
|
8289
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
8290
|
+
return {
|
|
8291
|
+
url: site.siteUrl,
|
|
8292
|
+
reachable: false,
|
|
8293
|
+
pageCount: null,
|
|
8294
|
+
version: null,
|
|
8295
|
+
error: message,
|
|
8296
|
+
plugins: []
|
|
8297
|
+
};
|
|
8298
|
+
}
|
|
8299
|
+
}
|
|
8300
|
+
async function listActivePlugins(connection, env) {
|
|
8301
|
+
const site = resolveEnvironment(connection, env);
|
|
8302
|
+
try {
|
|
8303
|
+
const { body } = await fetchJson(
|
|
8304
|
+
connection,
|
|
8305
|
+
site.siteUrl,
|
|
8306
|
+
"/wp-json/wp/v2/plugins?per_page=100&_fields=plugin,status"
|
|
8307
|
+
);
|
|
8308
|
+
return body.filter((plugin) => plugin.status === "active").map((plugin) => plugin.plugin).sort();
|
|
8309
|
+
} catch (error) {
|
|
8310
|
+
if (error instanceof WordpressApiError && (error.statusCode === 403 || error.statusCode === 404)) {
|
|
8311
|
+
return null;
|
|
8312
|
+
}
|
|
8313
|
+
return null;
|
|
8314
|
+
}
|
|
8315
|
+
}
|
|
8316
|
+
async function listPages(connection, env) {
|
|
8317
|
+
const site = resolveEnvironment(connection, env);
|
|
8318
|
+
const pages = [];
|
|
8319
|
+
let page = 1;
|
|
8320
|
+
let totalPages = 1;
|
|
8321
|
+
while (page <= totalPages) {
|
|
8322
|
+
const { body, response } = await fetchJson(
|
|
8323
|
+
connection,
|
|
8324
|
+
site.siteUrl,
|
|
8325
|
+
`/wp-json/wp/v2/pages?per_page=100&page=${page}&context=edit&_fields=${PAGE_LIST_FIELDS}`
|
|
8326
|
+
);
|
|
8327
|
+
totalPages = Number.parseInt(response.headers.get("x-wp-totalpages") ?? "1", 10) || 1;
|
|
8328
|
+
pages.push(...body.map((entry) => ({
|
|
8329
|
+
id: entry.id,
|
|
8330
|
+
slug: entry.slug,
|
|
8331
|
+
title: stripHtml(entry.title?.rendered ?? ""),
|
|
8332
|
+
status: entry.status,
|
|
8333
|
+
modifiedAt: entry.modified ?? entry.modified_gmt ?? null,
|
|
8334
|
+
link: entry.link ?? null
|
|
8335
|
+
})));
|
|
8336
|
+
page += 1;
|
|
8337
|
+
}
|
|
8338
|
+
return pages;
|
|
8339
|
+
}
|
|
8340
|
+
async function getPageBySlug(connection, slug, env) {
|
|
8341
|
+
const site = resolveEnvironment(connection, env);
|
|
8342
|
+
const { body } = await fetchJson(
|
|
8343
|
+
connection,
|
|
8344
|
+
site.siteUrl,
|
|
8345
|
+
`/wp-json/wp/v2/pages?slug=${encodeURIComponent(slug)}&per_page=100&context=edit&_fields=${PAGE_FIELDS}`
|
|
8346
|
+
);
|
|
8347
|
+
if (body.length === 0) {
|
|
8348
|
+
throw new WordpressApiError("NOT_FOUND", `No WordPress page found for slug "${slug}"`, 404);
|
|
8349
|
+
}
|
|
8350
|
+
const exact = body.filter((page) => page.slug === slug);
|
|
8351
|
+
if (exact.length === 1) return exact[0];
|
|
8352
|
+
if (exact.length > 1) {
|
|
8353
|
+
throw new WordpressApiError("VALIDATION_ERROR", buildAmbiguousSlugMessage(slug, exact), 400);
|
|
8354
|
+
}
|
|
8355
|
+
if (body.length > 1) {
|
|
8356
|
+
throw new WordpressApiError("VALIDATION_ERROR", buildAmbiguousSlugMessage(slug, body), 400);
|
|
8357
|
+
}
|
|
8358
|
+
return body[0];
|
|
8359
|
+
}
|
|
8360
|
+
async function fetchRenderedPage(link) {
|
|
8361
|
+
if (!link) return null;
|
|
8362
|
+
return fetchText(link);
|
|
8363
|
+
}
|
|
8364
|
+
async function getPageDetail(connection, slug, env, plugins) {
|
|
8365
|
+
const site = resolveEnvironment(connection, env);
|
|
8366
|
+
const resolvedPlugins = plugins === void 0 ? await listActivePlugins(connection, site.env) : plugins;
|
|
8367
|
+
const page = await getPageBySlug(connection, slug, site.env);
|
|
8368
|
+
const html = await fetchRenderedPage(page.link);
|
|
8369
|
+
const schemaBlocks = html ? extractSchemaBlocks(html) : [];
|
|
8370
|
+
const seo = buildSeoState(page, html, resolvedPlugins);
|
|
8371
|
+
return {
|
|
8372
|
+
id: page.id,
|
|
8373
|
+
slug: page.slug,
|
|
8374
|
+
title: stripHtml(page.title?.rendered ?? ""),
|
|
8375
|
+
status: page.status,
|
|
8376
|
+
modifiedAt: page.modified ?? page.modified_gmt ?? null,
|
|
8377
|
+
link: page.link ?? null,
|
|
8378
|
+
env: site.env,
|
|
8379
|
+
content: page.content?.raw ?? page.content?.rendered ?? "",
|
|
8380
|
+
seo,
|
|
8381
|
+
schemaBlocks
|
|
8382
|
+
};
|
|
8383
|
+
}
|
|
8384
|
+
async function createPage(connection, body, env) {
|
|
8385
|
+
const site = resolveEnvironment(connection, env);
|
|
8386
|
+
const { body: created } = await fetchJson(
|
|
8387
|
+
connection,
|
|
8388
|
+
site.siteUrl,
|
|
8389
|
+
"/wp-json/wp/v2/pages",
|
|
8390
|
+
{
|
|
8391
|
+
method: "POST",
|
|
8392
|
+
body: JSON.stringify({
|
|
8393
|
+
title: body.title,
|
|
8394
|
+
slug: body.slug,
|
|
8395
|
+
content: body.content,
|
|
8396
|
+
status: body.status ?? "draft"
|
|
8397
|
+
})
|
|
8398
|
+
}
|
|
8399
|
+
);
|
|
8400
|
+
return getPageDetail(connection, created.slug, site.env);
|
|
8401
|
+
}
|
|
8402
|
+
async function updatePageBySlug(connection, slug, body, env) {
|
|
8403
|
+
const site = resolveEnvironment(connection, env);
|
|
8404
|
+
const page = await getPageBySlug(connection, slug, site.env);
|
|
8405
|
+
const { body: updated } = await fetchJson(
|
|
8406
|
+
connection,
|
|
8407
|
+
site.siteUrl,
|
|
8408
|
+
`/wp-json/wp/v2/pages/${page.id}`,
|
|
8409
|
+
{
|
|
8410
|
+
method: "POST",
|
|
8411
|
+
body: JSON.stringify(body)
|
|
8412
|
+
}
|
|
8413
|
+
);
|
|
8414
|
+
return getPageDetail(connection, updated.slug, site.env);
|
|
8415
|
+
}
|
|
8416
|
+
function encodeNoindexValue(key, value) {
|
|
8417
|
+
if (key === "rank_math_robots") {
|
|
8418
|
+
return value ? ["noindex"] : ["index"];
|
|
8419
|
+
}
|
|
8420
|
+
return value ? "1" : "0";
|
|
8421
|
+
}
|
|
8422
|
+
async function setSeoMeta(connection, slug, body, env) {
|
|
8423
|
+
const site = resolveEnvironment(connection, env);
|
|
8424
|
+
const plugins = await listActivePlugins(connection, site.env);
|
|
8425
|
+
const page = await getPageBySlug(connection, slug, site.env);
|
|
8426
|
+
const writeTargets = resolveSeoWriteTargets(page.meta, plugins);
|
|
8427
|
+
if (writeTargets.length === 0 || !page.meta) {
|
|
8428
|
+
throw new WordpressApiError("UNSUPPORTED", "This WordPress site does not expose writable SEO meta fields through REST. Update the meta manually in WordPress.", 400);
|
|
8429
|
+
}
|
|
8430
|
+
const patch = {};
|
|
8431
|
+
for (const target of SEO_TARGETS) {
|
|
8432
|
+
if (body.title != null && writeTargets.includes(target.titleKey)) patch[target.titleKey] = body.title;
|
|
8433
|
+
if (body.description != null && writeTargets.includes(target.descriptionKey)) patch[target.descriptionKey] = body.description;
|
|
8434
|
+
if (body.noindex != null && writeTargets.includes(target.noindexKey)) {
|
|
8435
|
+
patch[target.noindexKey] = encodeNoindexValue(target.noindexKey, body.noindex);
|
|
8436
|
+
}
|
|
8437
|
+
}
|
|
8438
|
+
if (Object.keys(patch).length === 0) {
|
|
8439
|
+
throw new WordpressApiError("UNSUPPORTED", "No writable REST-exposed SEO fields matched the requested update.", 400);
|
|
8440
|
+
}
|
|
8441
|
+
await fetchJson(
|
|
8442
|
+
connection,
|
|
8443
|
+
site.siteUrl,
|
|
8444
|
+
`/wp-json/wp/v2/pages/${page.id}`,
|
|
8445
|
+
{
|
|
8446
|
+
method: "POST",
|
|
8447
|
+
body: JSON.stringify({
|
|
8448
|
+
meta: {
|
|
8449
|
+
...page.meta ?? {},
|
|
8450
|
+
...patch
|
|
8451
|
+
}
|
|
8452
|
+
})
|
|
8453
|
+
}
|
|
8454
|
+
);
|
|
8455
|
+
return getPageDetail(connection, slug, site.env, plugins);
|
|
8456
|
+
}
|
|
8457
|
+
async function getLlmsTxt(connection, env) {
|
|
8458
|
+
const site = resolveEnvironment(connection, env);
|
|
8459
|
+
const url = `${site.siteUrl}/llms.txt`;
|
|
8460
|
+
return {
|
|
8461
|
+
env: site.env,
|
|
8462
|
+
url,
|
|
8463
|
+
content: await fetchText(url)
|
|
8464
|
+
};
|
|
8465
|
+
}
|
|
8466
|
+
async function buildManualLlmsTxtUpdate(connection, content, env) {
|
|
8467
|
+
const site = resolveEnvironment(connection, env);
|
|
8468
|
+
return {
|
|
8469
|
+
manualRequired: true,
|
|
8470
|
+
targetUrl: `${site.siteUrl}/llms.txt`,
|
|
8471
|
+
adminUrl: `${site.siteUrl}/wp-admin/`,
|
|
8472
|
+
content,
|
|
8473
|
+
nextSteps: [
|
|
8474
|
+
`Open your hosting file manager or SSH session for ${site.siteUrl}.`,
|
|
8475
|
+
"Create or update the file llms.txt at the site root.",
|
|
8476
|
+
"Paste the generated content exactly as provided.",
|
|
8477
|
+
"Reload /llms.txt in a browser to confirm the file is publicly reachable."
|
|
8478
|
+
]
|
|
8479
|
+
};
|
|
8480
|
+
}
|
|
8481
|
+
async function getPageSchema(connection, slug, env) {
|
|
8482
|
+
const detail = await getPageDetail(connection, slug, env);
|
|
8483
|
+
return {
|
|
8484
|
+
env: detail.env,
|
|
8485
|
+
slug: detail.slug,
|
|
8486
|
+
blocks: detail.schemaBlocks
|
|
8487
|
+
};
|
|
8488
|
+
}
|
|
8489
|
+
async function buildManualSchemaUpdate(connection, slug, body, env) {
|
|
8490
|
+
const site = resolveEnvironment(connection, env);
|
|
8491
|
+
const page = await getPageBySlug(connection, slug, site.env);
|
|
8492
|
+
return {
|
|
8493
|
+
manualRequired: true,
|
|
8494
|
+
targetUrl: page.link ?? `${site.siteUrl}/${slug}`,
|
|
8495
|
+
adminUrl: `${site.siteUrl}/wp-admin/`,
|
|
8496
|
+
content: body.json,
|
|
8497
|
+
nextSteps: [
|
|
8498
|
+
`Open the WordPress editor or theme/plugin settings for ${page.slug}.`,
|
|
8499
|
+
`Add the ${body.type ?? "custom"} JSON-LD block to the page or the site-level schema tool you use.`,
|
|
8500
|
+
"Publish/update the page and refresh the public URL to confirm the JSON-LD block is rendered."
|
|
8501
|
+
]
|
|
8502
|
+
};
|
|
8503
|
+
}
|
|
8504
|
+
async function runAudit(connection, env) {
|
|
8505
|
+
const site = resolveEnvironment(connection, env);
|
|
8506
|
+
const pages = await listPages(connection, site.env);
|
|
8507
|
+
const plugins = await listActivePlugins(connection, site.env);
|
|
8508
|
+
const details = await mapWithConcurrency(
|
|
8509
|
+
pages,
|
|
8510
|
+
5,
|
|
8511
|
+
async (page) => getPageDetail(connection, page.slug, site.env, plugins)
|
|
8512
|
+
);
|
|
8513
|
+
const auditPages = [];
|
|
8514
|
+
const allIssues = [];
|
|
8515
|
+
for (const page of details) {
|
|
8516
|
+
const issues = [];
|
|
8517
|
+
const wordCount = computeWordCount(page.content);
|
|
8518
|
+
const schemaPresent = page.schemaBlocks.length > 0;
|
|
8519
|
+
if (page.status === "publish" && page.seo.noindex === true) {
|
|
8520
|
+
issues.push({
|
|
8521
|
+
slug: page.slug,
|
|
8522
|
+
severity: "high",
|
|
8523
|
+
code: "noindex",
|
|
8524
|
+
message: "Published page is marked noindex."
|
|
8525
|
+
});
|
|
8526
|
+
}
|
|
8527
|
+
if (!page.seo.title) {
|
|
8528
|
+
issues.push({
|
|
8529
|
+
slug: page.slug,
|
|
8530
|
+
severity: "medium",
|
|
8531
|
+
code: "missing-seo-title",
|
|
8532
|
+
message: "Rendered page title is missing."
|
|
8533
|
+
});
|
|
8534
|
+
}
|
|
8535
|
+
if (!page.seo.description) {
|
|
8536
|
+
issues.push({
|
|
8537
|
+
slug: page.slug,
|
|
8538
|
+
severity: "medium",
|
|
8539
|
+
code: "missing-meta-description",
|
|
8540
|
+
message: "Rendered meta description is missing."
|
|
8541
|
+
});
|
|
8542
|
+
}
|
|
8543
|
+
if (!schemaPresent) {
|
|
8544
|
+
issues.push({
|
|
8545
|
+
slug: page.slug,
|
|
8546
|
+
severity: "medium",
|
|
8547
|
+
code: "missing-schema",
|
|
8548
|
+
message: "No JSON-LD schema was detected on the rendered page."
|
|
8549
|
+
});
|
|
8550
|
+
}
|
|
8551
|
+
if (wordCount < THIN_CONTENT_WORD_COUNT) {
|
|
8552
|
+
issues.push({
|
|
8553
|
+
slug: page.slug,
|
|
8554
|
+
severity: "low",
|
|
8555
|
+
code: "thin-content",
|
|
8556
|
+
message: `Page content is thin (${wordCount} words; target at least ${THIN_CONTENT_WORD_COUNT}).`
|
|
8557
|
+
});
|
|
8558
|
+
}
|
|
8559
|
+
auditPages.push({
|
|
8560
|
+
slug: page.slug,
|
|
8561
|
+
title: page.title,
|
|
8562
|
+
status: page.status,
|
|
8563
|
+
wordCount,
|
|
8564
|
+
seo: page.seo,
|
|
8565
|
+
schemaPresent,
|
|
8566
|
+
issues
|
|
8567
|
+
});
|
|
8568
|
+
allIssues.push(...issues);
|
|
8569
|
+
}
|
|
8570
|
+
const severityWeight = { high: 0, medium: 1, low: 2 };
|
|
8571
|
+
allIssues.sort((a, b) => {
|
|
8572
|
+
return severityWeight[a.severity] - severityWeight[b.severity] || a.slug.localeCompare(b.slug);
|
|
8573
|
+
});
|
|
8574
|
+
return {
|
|
8575
|
+
env: site.env,
|
|
8576
|
+
pages: auditPages,
|
|
8577
|
+
issues: allIssues
|
|
8578
|
+
};
|
|
8579
|
+
}
|
|
8580
|
+
async function diffPageAcrossEnvironments(connection, slug) {
|
|
8581
|
+
if (!connection.stagingUrl) {
|
|
8582
|
+
throw new WordpressApiError("VALIDATION_ERROR", "No staging URL configured for this project. Reconnect with --staging-url before using diff.", 400);
|
|
8583
|
+
}
|
|
8584
|
+
const [livePlugins, stagingPlugins] = await Promise.all([
|
|
8585
|
+
listActivePlugins(connection, "live"),
|
|
8586
|
+
listActivePlugins(connection, "staging")
|
|
8587
|
+
]);
|
|
8588
|
+
const live = await getPageDetail(connection, slug, "live", livePlugins);
|
|
8589
|
+
const staging = await getPageDetail(connection, slug, "staging", stagingPlugins);
|
|
8590
|
+
const liveContentHash = contentHash(live.content);
|
|
8591
|
+
const stagingContentHash = contentHash(staging.content);
|
|
8592
|
+
const liveDiff = {
|
|
8593
|
+
...live,
|
|
8594
|
+
contentHash: liveContentHash,
|
|
8595
|
+
contentSnippet: buildSnippet(live.content)
|
|
8596
|
+
};
|
|
8597
|
+
const stagingDiff = {
|
|
8598
|
+
...staging,
|
|
8599
|
+
contentHash: stagingContentHash,
|
|
8600
|
+
contentSnippet: buildSnippet(staging.content)
|
|
8601
|
+
};
|
|
8602
|
+
const differences = {
|
|
8603
|
+
title: live.title !== staging.title,
|
|
8604
|
+
slug: live.slug !== staging.slug,
|
|
8605
|
+
content: liveContentHash !== stagingContentHash,
|
|
8606
|
+
seoTitle: live.seo.title !== staging.seo.title,
|
|
8607
|
+
seoDescription: live.seo.description !== staging.seo.description,
|
|
8608
|
+
noindex: live.seo.noindex !== staging.seo.noindex,
|
|
8609
|
+
schema: JSON.stringify(live.schemaBlocks) !== JSON.stringify(staging.schemaBlocks)
|
|
8610
|
+
};
|
|
8611
|
+
return {
|
|
8612
|
+
slug,
|
|
8613
|
+
live: liveDiff,
|
|
8614
|
+
staging: stagingDiff,
|
|
8615
|
+
hasDifferences: Object.values(differences).some(Boolean),
|
|
8616
|
+
differences
|
|
8617
|
+
};
|
|
8618
|
+
}
|
|
8619
|
+
async function buildManualStagingPush(connection) {
|
|
8620
|
+
const liveStatus = await getSiteStatus(connection, "live");
|
|
8621
|
+
const plugins = liveStatus.plugins ?? [];
|
|
8622
|
+
const wpStagingActive = plugins.some((plugin) => plugin.includes("wp-staging"));
|
|
8623
|
+
return {
|
|
8624
|
+
manualRequired: true,
|
|
8625
|
+
targetUrl: getWpStagingAdminUrl(connection.url),
|
|
8626
|
+
adminUrl: getWpStagingAdminUrl(connection.url),
|
|
8627
|
+
content: JSON.stringify({
|
|
8628
|
+
liveUrl: connection.url,
|
|
8629
|
+
stagingUrl: connection.stagingUrl ?? null,
|
|
8630
|
+
wpStagingActive
|
|
8631
|
+
}, null, 2),
|
|
8632
|
+
nextSteps: [
|
|
8633
|
+
"Open the WP STAGING admin page on the live site.",
|
|
8634
|
+
wpStagingActive ? "Review the staging site snapshot you want to push and use the plugin UI to start the push-to-live workflow." : "Confirm the WP STAGING plugin is installed and active before attempting a push-to-live operation.",
|
|
8635
|
+
"Complete the plugin confirmation steps in wp-admin and verify the live site after the push finishes."
|
|
8636
|
+
]
|
|
8637
|
+
};
|
|
8638
|
+
}
|
|
8639
|
+
function parseEnv(value) {
|
|
8640
|
+
if (typeof value !== "string") return void 0;
|
|
8641
|
+
const parsed = wordpressEnvSchema.safeParse(value);
|
|
8642
|
+
return parsed.success ? parsed.data : void 0;
|
|
8643
|
+
}
|
|
8644
|
+
|
|
8645
|
+
// ../api-routes/src/wordpress.ts
|
|
8646
|
+
function parseEnvInput(value, fieldName = "env") {
|
|
8647
|
+
const env = parseEnv(value);
|
|
8648
|
+
if (!env && value != null) {
|
|
8649
|
+
throw validationError(`${fieldName} must be "live" or "staging"`);
|
|
8650
|
+
}
|
|
8651
|
+
return env;
|
|
8652
|
+
}
|
|
8653
|
+
function sendWordpressError(reply, error) {
|
|
8654
|
+
if (!(error instanceof WordpressApiError)) return false;
|
|
8655
|
+
let appError;
|
|
8656
|
+
switch (error.code) {
|
|
8657
|
+
case "AUTH_INVALID":
|
|
8658
|
+
appError = new AppError("AUTH_INVALID", error.message, error.statusCode);
|
|
8659
|
+
break;
|
|
8660
|
+
case "NOT_FOUND":
|
|
8661
|
+
appError = new AppError("NOT_FOUND", error.message, error.statusCode);
|
|
8662
|
+
break;
|
|
8663
|
+
case "UPSTREAM_ERROR":
|
|
8664
|
+
appError = providerError(error.message, { statusCode: error.statusCode });
|
|
8665
|
+
break;
|
|
8666
|
+
case "UNSUPPORTED":
|
|
8667
|
+
case "VALIDATION_ERROR":
|
|
8668
|
+
appError = validationError(error.message);
|
|
8669
|
+
break;
|
|
8670
|
+
default:
|
|
8671
|
+
appError = providerError(error.message, { statusCode: error.statusCode });
|
|
8672
|
+
break;
|
|
8673
|
+
}
|
|
8674
|
+
reply.status(appError.statusCode).send(appError.toJSON());
|
|
8675
|
+
return true;
|
|
8676
|
+
}
|
|
8677
|
+
async function withWordpressErrorHandling(reply, handler) {
|
|
8678
|
+
try {
|
|
8679
|
+
return await handler();
|
|
8680
|
+
} catch (error) {
|
|
8681
|
+
if (sendWordpressError(reply, error)) return;
|
|
8682
|
+
throw error;
|
|
8683
|
+
}
|
|
8684
|
+
}
|
|
8685
|
+
async function wordpressRoutes(app, opts) {
|
|
8686
|
+
function requireStore(reply) {
|
|
8687
|
+
if (opts.wordpressConnectionStore) return opts.wordpressConnectionStore;
|
|
8688
|
+
const err = validationError("WordPress connection storage is not configured for this deployment");
|
|
8689
|
+
reply.status(err.statusCode).send(err.toJSON());
|
|
8690
|
+
return null;
|
|
8691
|
+
}
|
|
8692
|
+
function requireConnection(store, projectName, reply) {
|
|
8693
|
+
const connection = store.getConnection(projectName);
|
|
8694
|
+
if (!connection) {
|
|
8695
|
+
const err = validationError(`No WordPress connection found for project "${projectName}". Run "canonry wordpress connect ${projectName}" first.`);
|
|
8696
|
+
reply.status(err.statusCode).send(err.toJSON());
|
|
8697
|
+
return null;
|
|
8698
|
+
}
|
|
8699
|
+
return connection;
|
|
8700
|
+
}
|
|
8701
|
+
app.post("/projects/:name/wordpress/connect", async (request, reply) => {
|
|
8702
|
+
return withWordpressErrorHandling(reply, async () => {
|
|
8703
|
+
const store = requireStore(reply);
|
|
8704
|
+
if (!store) return;
|
|
8705
|
+
const project = resolveProject(app.db, request.params.name);
|
|
8706
|
+
const { url, stagingUrl, username, appPassword } = request.body ?? {};
|
|
8707
|
+
if (!url || !username || !appPassword) {
|
|
8708
|
+
const err = validationError("url, username, and appPassword are required");
|
|
8709
|
+
return reply.status(err.statusCode).send(err.toJSON());
|
|
8710
|
+
}
|
|
8711
|
+
const defaultEnv = parseEnvInput(request.body?.defaultEnv, "defaultEnv") ?? (stagingUrl ? "staging" : "live");
|
|
8712
|
+
if (defaultEnv === "staging" && !stagingUrl) {
|
|
8713
|
+
const err = validationError('defaultEnv "staging" requires stagingUrl');
|
|
8714
|
+
return reply.status(err.statusCode).send(err.toJSON());
|
|
8715
|
+
}
|
|
8716
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
8717
|
+
const existing = store.getConnection(project.name);
|
|
8718
|
+
const nextConnection = {
|
|
8719
|
+
projectName: project.name,
|
|
8720
|
+
url,
|
|
8721
|
+
stagingUrl,
|
|
8722
|
+
username,
|
|
8723
|
+
appPassword,
|
|
8724
|
+
defaultEnv,
|
|
8725
|
+
createdAt: existing?.createdAt ?? now,
|
|
8726
|
+
updatedAt: now
|
|
8727
|
+
};
|
|
8728
|
+
await verifyWordpressConnection(nextConnection);
|
|
8729
|
+
const connection = store.upsertConnection(nextConnection);
|
|
8730
|
+
const live = await getSiteStatus(connection, "live");
|
|
8731
|
+
const staging = connection.stagingUrl ? await getSiteStatus(connection, "staging") : null;
|
|
8732
|
+
writeAuditLog(app.db, {
|
|
8733
|
+
projectId: project.id,
|
|
8734
|
+
actor: "api",
|
|
8735
|
+
action: "wordpress.connected",
|
|
8736
|
+
entityType: "wordpress_connection",
|
|
8737
|
+
entityId: project.name
|
|
8738
|
+
});
|
|
8739
|
+
return {
|
|
8740
|
+
connected: true,
|
|
8741
|
+
projectName: project.name,
|
|
8742
|
+
defaultEnv: connection.defaultEnv,
|
|
8743
|
+
live,
|
|
8744
|
+
staging,
|
|
8745
|
+
adminUrl: getWpStagingAdminUrl(connection.url)
|
|
8746
|
+
};
|
|
8747
|
+
});
|
|
8748
|
+
});
|
|
8749
|
+
app.delete("/projects/:name/wordpress/disconnect", async (request, reply) => {
|
|
8750
|
+
const store = requireStore(reply);
|
|
8751
|
+
if (!store) return;
|
|
8752
|
+
const project = resolveProject(app.db, request.params.name);
|
|
8753
|
+
const deleted = store.deleteConnection(project.name);
|
|
8754
|
+
if (!deleted) {
|
|
8755
|
+
const err = notFound("WordPress connection", project.name);
|
|
8756
|
+
return reply.status(err.statusCode).send(err.toJSON());
|
|
8757
|
+
}
|
|
8758
|
+
writeAuditLog(app.db, {
|
|
8759
|
+
projectId: project.id,
|
|
8760
|
+
actor: "api",
|
|
8761
|
+
action: "wordpress.disconnected",
|
|
8762
|
+
entityType: "wordpress_connection",
|
|
8763
|
+
entityId: project.name
|
|
8764
|
+
});
|
|
8765
|
+
return reply.status(204).send();
|
|
8766
|
+
});
|
|
8767
|
+
app.get("/projects/:name/wordpress/status", async (request) => {
|
|
8768
|
+
const project = resolveProject(app.db, request.params.name);
|
|
8769
|
+
const connection = opts.wordpressConnectionStore?.getConnection(project.name);
|
|
8770
|
+
if (!connection) {
|
|
8771
|
+
return {
|
|
8772
|
+
connected: false,
|
|
8773
|
+
projectName: project.name,
|
|
8774
|
+
defaultEnv: "live",
|
|
8775
|
+
live: null,
|
|
8776
|
+
staging: null,
|
|
8777
|
+
adminUrl: null
|
|
8778
|
+
};
|
|
8779
|
+
}
|
|
8780
|
+
const live = await getSiteStatus(connection, "live");
|
|
8781
|
+
const staging = connection.stagingUrl ? await getSiteStatus(connection, "staging") : null;
|
|
8782
|
+
return {
|
|
8783
|
+
connected: true,
|
|
8784
|
+
projectName: project.name,
|
|
8785
|
+
defaultEnv: connection.defaultEnv,
|
|
8786
|
+
live,
|
|
8787
|
+
staging,
|
|
8788
|
+
adminUrl: getWpStagingAdminUrl(connection.url)
|
|
8789
|
+
};
|
|
8790
|
+
});
|
|
8791
|
+
app.get("/projects/:name/wordpress/pages", async (request, reply) => {
|
|
8792
|
+
return withWordpressErrorHandling(reply, async () => {
|
|
8793
|
+
const store = requireStore(reply);
|
|
8794
|
+
if (!store) return;
|
|
8795
|
+
const project = resolveProject(app.db, request.params.name);
|
|
8796
|
+
const connection = requireConnection(store, project.name, reply);
|
|
8797
|
+
if (!connection) return;
|
|
8798
|
+
const env = parseEnvInput(request.query?.env);
|
|
8799
|
+
return {
|
|
8800
|
+
env: env ?? connection.defaultEnv,
|
|
8801
|
+
pages: await listPages(connection, env)
|
|
8802
|
+
};
|
|
8803
|
+
});
|
|
8804
|
+
});
|
|
8805
|
+
app.get("/projects/:name/wordpress/page", async (request, reply) => {
|
|
8806
|
+
return withWordpressErrorHandling(reply, async () => {
|
|
8807
|
+
const store = requireStore(reply);
|
|
8808
|
+
if (!store) return;
|
|
8809
|
+
const project = resolveProject(app.db, request.params.name);
|
|
8810
|
+
const connection = requireConnection(store, project.name, reply);
|
|
8811
|
+
if (!connection) return;
|
|
8812
|
+
const slug = request.query?.slug?.trim();
|
|
8813
|
+
if (!slug) {
|
|
8814
|
+
const err = validationError("slug is required");
|
|
8815
|
+
return reply.status(err.statusCode).send(err.toJSON());
|
|
8816
|
+
}
|
|
8817
|
+
const env = parseEnvInput(request.query?.env);
|
|
8818
|
+
return getPageDetail(connection, slug, env);
|
|
8819
|
+
});
|
|
8820
|
+
});
|
|
8821
|
+
app.post("/projects/:name/wordpress/pages", async (request, reply) => {
|
|
8822
|
+
return withWordpressErrorHandling(reply, async () => {
|
|
8823
|
+
const store = requireStore(reply);
|
|
8824
|
+
if (!store) return;
|
|
8825
|
+
const project = resolveProject(app.db, request.params.name);
|
|
8826
|
+
const connection = requireConnection(store, project.name, reply);
|
|
8827
|
+
if (!connection) return;
|
|
8828
|
+
const { title, slug, content, status } = request.body ?? {};
|
|
8829
|
+
const env = parseEnvInput(request.body?.env);
|
|
8830
|
+
if (!title || !slug || !content) {
|
|
8831
|
+
const err = validationError("title, slug, and content are required");
|
|
8832
|
+
return reply.status(err.statusCode).send(err.toJSON());
|
|
8833
|
+
}
|
|
8834
|
+
const created = await createPage(connection, { title, slug, content, status }, env);
|
|
8835
|
+
writeAuditLog(app.db, {
|
|
8836
|
+
projectId: project.id,
|
|
8837
|
+
actor: "api",
|
|
8838
|
+
action: "wordpress.page-created",
|
|
8839
|
+
entityType: "wordpress_page",
|
|
8840
|
+
entityId: created.slug
|
|
8841
|
+
});
|
|
8842
|
+
return created;
|
|
8843
|
+
});
|
|
8844
|
+
});
|
|
8845
|
+
app.put("/projects/:name/wordpress/page", async (request, reply) => {
|
|
8846
|
+
return withWordpressErrorHandling(reply, async () => {
|
|
8847
|
+
const store = requireStore(reply);
|
|
8848
|
+
if (!store) return;
|
|
8849
|
+
const project = resolveProject(app.db, request.params.name);
|
|
8850
|
+
const connection = requireConnection(store, project.name, reply);
|
|
8851
|
+
if (!connection) return;
|
|
8852
|
+
const currentSlug = request.body?.currentSlug?.trim();
|
|
8853
|
+
if (!currentSlug) {
|
|
8854
|
+
const err = validationError("currentSlug is required");
|
|
8855
|
+
return reply.status(err.statusCode).send(err.toJSON());
|
|
8856
|
+
}
|
|
8857
|
+
const env = parseEnvInput(request.body?.env);
|
|
8858
|
+
const updated = await updatePageBySlug(connection, currentSlug, {
|
|
8859
|
+
title: request.body?.title,
|
|
8860
|
+
slug: request.body?.slug,
|
|
8861
|
+
content: request.body?.content,
|
|
8862
|
+
status: request.body?.status
|
|
8863
|
+
}, env);
|
|
8864
|
+
writeAuditLog(app.db, {
|
|
8865
|
+
projectId: project.id,
|
|
8866
|
+
actor: "api",
|
|
8867
|
+
action: "wordpress.page-updated",
|
|
8868
|
+
entityType: "wordpress_page",
|
|
8869
|
+
entityId: currentSlug
|
|
8870
|
+
});
|
|
8871
|
+
return updated;
|
|
8872
|
+
});
|
|
8873
|
+
});
|
|
8874
|
+
app.post("/projects/:name/wordpress/page/meta", async (request, reply) => {
|
|
8875
|
+
return withWordpressErrorHandling(reply, async () => {
|
|
8876
|
+
const store = requireStore(reply);
|
|
8877
|
+
if (!store) return;
|
|
8878
|
+
const project = resolveProject(app.db, request.params.name);
|
|
8879
|
+
const connection = requireConnection(store, project.name, reply);
|
|
8880
|
+
if (!connection) return;
|
|
8881
|
+
const slug = request.body?.slug?.trim();
|
|
8882
|
+
if (!slug) {
|
|
8883
|
+
const err = validationError("slug is required");
|
|
8884
|
+
return reply.status(err.statusCode).send(err.toJSON());
|
|
8885
|
+
}
|
|
8886
|
+
const env = parseEnvInput(request.body?.env);
|
|
8887
|
+
const updated = await setSeoMeta(connection, slug, {
|
|
8888
|
+
title: request.body?.title,
|
|
8889
|
+
description: request.body?.description,
|
|
8890
|
+
noindex: request.body?.noindex
|
|
8891
|
+
}, env);
|
|
8892
|
+
writeAuditLog(app.db, {
|
|
8893
|
+
projectId: project.id,
|
|
8894
|
+
actor: "api",
|
|
8895
|
+
action: "wordpress.page-meta-updated",
|
|
8896
|
+
entityType: "wordpress_page",
|
|
8897
|
+
entityId: slug
|
|
8898
|
+
});
|
|
8899
|
+
return updated;
|
|
8900
|
+
});
|
|
8901
|
+
});
|
|
8902
|
+
app.get("/projects/:name/wordpress/schema", async (request, reply) => {
|
|
8903
|
+
return withWordpressErrorHandling(reply, async () => {
|
|
8904
|
+
const store = requireStore(reply);
|
|
8905
|
+
if (!store) return;
|
|
8906
|
+
const project = resolveProject(app.db, request.params.name);
|
|
8907
|
+
const connection = requireConnection(store, project.name, reply);
|
|
8908
|
+
if (!connection) return;
|
|
8909
|
+
const slug = request.query?.slug?.trim();
|
|
8910
|
+
if (!slug) {
|
|
8911
|
+
const err = validationError("slug is required");
|
|
8912
|
+
return reply.status(err.statusCode).send(err.toJSON());
|
|
8913
|
+
}
|
|
8914
|
+
const env = parseEnvInput(request.query?.env);
|
|
8915
|
+
return getPageSchema(connection, slug, env);
|
|
8916
|
+
});
|
|
8917
|
+
});
|
|
8918
|
+
app.post("/projects/:name/wordpress/schema/manual", async (request, reply) => {
|
|
8919
|
+
return withWordpressErrorHandling(reply, async () => {
|
|
8920
|
+
const store = requireStore(reply);
|
|
8921
|
+
if (!store) return;
|
|
8922
|
+
const project = resolveProject(app.db, request.params.name);
|
|
8923
|
+
const connection = requireConnection(store, project.name, reply);
|
|
8924
|
+
if (!connection) return;
|
|
8925
|
+
const slug = request.body?.slug?.trim();
|
|
8926
|
+
const json = request.body?.json;
|
|
8927
|
+
if (!slug || !json) {
|
|
8928
|
+
const err = validationError("slug and json are required");
|
|
8929
|
+
return reply.status(err.statusCode).send(err.toJSON());
|
|
8930
|
+
}
|
|
8931
|
+
const env = parseEnvInput(request.body?.env);
|
|
8932
|
+
return buildManualSchemaUpdate(connection, slug, { type: request.body?.type, json }, env);
|
|
8933
|
+
});
|
|
8934
|
+
});
|
|
8935
|
+
app.get("/projects/:name/wordpress/llms-txt", async (request, reply) => {
|
|
8936
|
+
return withWordpressErrorHandling(reply, async () => {
|
|
8937
|
+
const store = requireStore(reply);
|
|
8938
|
+
if (!store) return;
|
|
8939
|
+
const project = resolveProject(app.db, request.params.name);
|
|
8940
|
+
const connection = requireConnection(store, project.name, reply);
|
|
8941
|
+
if (!connection) return;
|
|
8942
|
+
const env = parseEnvInput(request.query?.env);
|
|
8943
|
+
return getLlmsTxt(connection, env);
|
|
8944
|
+
});
|
|
8945
|
+
});
|
|
8946
|
+
app.post("/projects/:name/wordpress/llms-txt/manual", async (request, reply) => {
|
|
8947
|
+
return withWordpressErrorHandling(reply, async () => {
|
|
8948
|
+
const store = requireStore(reply);
|
|
8949
|
+
if (!store) return;
|
|
8950
|
+
const project = resolveProject(app.db, request.params.name);
|
|
8951
|
+
const connection = requireConnection(store, project.name, reply);
|
|
8952
|
+
if (!connection) return;
|
|
8953
|
+
const content = request.body?.content;
|
|
8954
|
+
if (!content) {
|
|
8955
|
+
const err = validationError("content is required");
|
|
8956
|
+
return reply.status(err.statusCode).send(err.toJSON());
|
|
8957
|
+
}
|
|
8958
|
+
const env = parseEnvInput(request.body?.env);
|
|
8959
|
+
return buildManualLlmsTxtUpdate(connection, content, env);
|
|
8960
|
+
});
|
|
8961
|
+
});
|
|
8962
|
+
app.get("/projects/:name/wordpress/audit", async (request, reply) => {
|
|
8963
|
+
return withWordpressErrorHandling(reply, async () => {
|
|
8964
|
+
const store = requireStore(reply);
|
|
8965
|
+
if (!store) return;
|
|
8966
|
+
const project = resolveProject(app.db, request.params.name);
|
|
8967
|
+
const connection = requireConnection(store, project.name, reply);
|
|
8968
|
+
if (!connection) return;
|
|
8969
|
+
const env = parseEnvInput(request.query?.env);
|
|
8970
|
+
return runAudit(connection, env);
|
|
8971
|
+
});
|
|
8972
|
+
});
|
|
8973
|
+
app.get("/projects/:name/wordpress/diff", async (request, reply) => {
|
|
8974
|
+
return withWordpressErrorHandling(reply, async () => {
|
|
8975
|
+
const store = requireStore(reply);
|
|
8976
|
+
if (!store) return;
|
|
8977
|
+
const project = resolveProject(app.db, request.params.name);
|
|
8978
|
+
const connection = requireConnection(store, project.name, reply);
|
|
8979
|
+
if (!connection) return;
|
|
8980
|
+
const slug = request.query?.slug?.trim();
|
|
8981
|
+
if (!slug) {
|
|
8982
|
+
const err = validationError("slug is required");
|
|
8983
|
+
return reply.status(err.statusCode).send(err.toJSON());
|
|
8984
|
+
}
|
|
8985
|
+
return diffPageAcrossEnvironments(connection, slug);
|
|
8986
|
+
});
|
|
8987
|
+
});
|
|
8988
|
+
app.get("/projects/:name/wordpress/staging/status", async (request, reply) => {
|
|
8989
|
+
return withWordpressErrorHandling(reply, async () => {
|
|
8990
|
+
const store = requireStore(reply);
|
|
8991
|
+
if (!store) return;
|
|
8992
|
+
const project = resolveProject(app.db, request.params.name);
|
|
8993
|
+
const connection = requireConnection(store, project.name, reply);
|
|
8994
|
+
if (!connection) return;
|
|
8995
|
+
const plugins = await listActivePlugins(connection, "live");
|
|
8996
|
+
return {
|
|
8997
|
+
stagingConfigured: Boolean(connection.stagingUrl),
|
|
8998
|
+
stagingUrl: connection.stagingUrl ?? null,
|
|
8999
|
+
wpStagingActive: Boolean(plugins?.some((plugin) => plugin.includes("wp-staging"))),
|
|
9000
|
+
adminUrl: getWpStagingAdminUrl(connection.url)
|
|
9001
|
+
};
|
|
9002
|
+
});
|
|
9003
|
+
});
|
|
9004
|
+
app.post("/projects/:name/wordpress/staging/push", async (request, reply) => {
|
|
9005
|
+
return withWordpressErrorHandling(reply, async () => {
|
|
9006
|
+
const store = requireStore(reply);
|
|
9007
|
+
if (!store) return;
|
|
9008
|
+
const project = resolveProject(app.db, request.params.name);
|
|
9009
|
+
const connection = requireConnection(store, project.name, reply);
|
|
9010
|
+
if (!connection) return;
|
|
9011
|
+
if (!connection.stagingUrl) {
|
|
9012
|
+
const err = validationError("No staging URL configured for this project. Reconnect with --staging-url before using staging push.");
|
|
9013
|
+
return reply.status(err.statusCode).send(err.toJSON());
|
|
9014
|
+
}
|
|
9015
|
+
return buildManualStagingPush(connection);
|
|
9016
|
+
});
|
|
9017
|
+
});
|
|
9018
|
+
}
|
|
9019
|
+
|
|
9020
|
+
// ../api-routes/src/index.ts
|
|
9021
|
+
async function apiRoutes(app, opts) {
|
|
9022
|
+
app.decorate("db", opts.db);
|
|
9023
|
+
app.setErrorHandler((error, _request, reply) => {
|
|
9024
|
+
if (error instanceof AppError) {
|
|
9025
|
+
return reply.status(error.statusCode).send(error.toJSON());
|
|
9026
|
+
}
|
|
9027
|
+
const httpStatus = error.statusCode ?? error.status ?? 500;
|
|
9028
|
+
if (httpStatus >= 400 && httpStatus < 500) {
|
|
9029
|
+
return reply.status(httpStatus).send({
|
|
9030
|
+
error: {
|
|
9031
|
+
code: httpStatus === 401 ? "AUTH_INVALID" : httpStatus === 403 ? "FORBIDDEN" : httpStatus === 404 ? "NOT_FOUND" : httpStatus === 429 ? "QUOTA_EXCEEDED" : "VALIDATION_ERROR",
|
|
9032
|
+
message: error.message
|
|
9033
|
+
}
|
|
9034
|
+
});
|
|
9035
|
+
}
|
|
7588
9036
|
app.log.error(error);
|
|
7589
9037
|
return reply.status(500).send({
|
|
7590
9038
|
error: {
|
|
@@ -7655,6 +9103,9 @@ async function apiRoutes(app, opts) {
|
|
|
7655
9103
|
onGscSyncRequested: opts.onGscSyncRequested,
|
|
7656
9104
|
onInspectSitemapRequested: opts.onInspectSitemapRequested
|
|
7657
9105
|
});
|
|
9106
|
+
await api.register(wordpressRoutes, {
|
|
9107
|
+
wordpressConnectionStore: opts.wordpressConnectionStore
|
|
9108
|
+
});
|
|
7658
9109
|
await api.register(cdpRoutes, {
|
|
7659
9110
|
getCdpStatus: opts.getCdpStatus,
|
|
7660
9111
|
onCdpScreenshot: opts.onCdpScreenshot,
|
|
@@ -7667,8 +9118,7 @@ async function apiRoutes(app, opts) {
|
|
|
7667
9118
|
}
|
|
7668
9119
|
|
|
7669
9120
|
// ../provider-gemini/src/normalize.ts
|
|
7670
|
-
import {
|
|
7671
|
-
import { VertexAI } from "@google-cloud/vertexai";
|
|
9121
|
+
import { GoogleGenAI } from "@google/genai";
|
|
7672
9122
|
var DEFAULT_MODEL = "gemini-3-flash";
|
|
7673
9123
|
var VALIDATION_PATTERN = /^gemini-/;
|
|
7674
9124
|
function isVertexConfig(config) {
|
|
@@ -7684,31 +9134,16 @@ function resolveModel(config) {
|
|
|
7684
9134
|
);
|
|
7685
9135
|
return DEFAULT_MODEL;
|
|
7686
9136
|
}
|
|
7687
|
-
function
|
|
7688
|
-
|
|
7689
|
-
|
|
7690
|
-
|
|
7691
|
-
|
|
7692
|
-
|
|
7693
|
-
|
|
7694
|
-
|
|
7695
|
-
|
|
7696
|
-
|
|
7697
|
-
})),
|
|
7698
|
-
usageMetadata: response.usageMetadata
|
|
7699
|
-
};
|
|
7700
|
-
}
|
|
7701
|
-
function createVertexModel(config, model, tools) {
|
|
7702
|
-
const vertexOpts = {
|
|
7703
|
-
project: config.vertexProject,
|
|
7704
|
-
location: config.vertexRegion || "us-central1",
|
|
7705
|
-
googleAuthOptions: config.vertexCredentials ? { keyFilename: config.vertexCredentials } : void 0
|
|
7706
|
-
};
|
|
7707
|
-
const vertexAI = new VertexAI(vertexOpts);
|
|
7708
|
-
return vertexAI.getGenerativeModel({
|
|
7709
|
-
model,
|
|
7710
|
-
...tools ? { tools } : {}
|
|
7711
|
-
});
|
|
9137
|
+
function createClient2(config) {
|
|
9138
|
+
if (isVertexConfig(config)) {
|
|
9139
|
+
return new GoogleGenAI({
|
|
9140
|
+
vertexai: true,
|
|
9141
|
+
project: config.vertexProject,
|
|
9142
|
+
location: config.vertexRegion || "us-central1",
|
|
9143
|
+
...config.vertexCredentials ? { googleAuthOptions: { keyFilename: config.vertexCredentials } } : {}
|
|
9144
|
+
});
|
|
9145
|
+
}
|
|
9146
|
+
return new GoogleGenAI({ apiKey: config.apiKey });
|
|
7712
9147
|
}
|
|
7713
9148
|
function validateConfig(config) {
|
|
7714
9149
|
if (isVertexConfig(config)) {
|
|
@@ -7737,28 +9172,19 @@ async function healthcheck(config) {
|
|
|
7737
9172
|
if (!validation.ok) return validation;
|
|
7738
9173
|
try {
|
|
7739
9174
|
const model = resolveModel(config);
|
|
7740
|
-
|
|
7741
|
-
|
|
7742
|
-
|
|
7743
|
-
|
|
7744
|
-
|
|
7745
|
-
|
|
7746
|
-
|
|
7747
|
-
|
|
7748
|
-
|
|
7749
|
-
|
|
7750
|
-
|
|
7751
|
-
|
|
7752
|
-
|
|
7753
|
-
const result = await generativeModel.generateContent('Say "ok"');
|
|
7754
|
-
const text2 = result.response.text();
|
|
7755
|
-
return {
|
|
7756
|
-
ok: text2.length > 0,
|
|
7757
|
-
provider: "gemini",
|
|
7758
|
-
message: text2.length > 0 ? "gemini api key verified" : "empty response from gemini",
|
|
7759
|
-
model
|
|
7760
|
-
};
|
|
7761
|
-
}
|
|
9175
|
+
const client = createClient2(config);
|
|
9176
|
+
const result = await client.models.generateContent({
|
|
9177
|
+
model,
|
|
9178
|
+
contents: 'Say "ok"'
|
|
9179
|
+
});
|
|
9180
|
+
const text2 = result.text ?? "";
|
|
9181
|
+
const backend = isVertexConfig(config) ? "vertex ai" : "api key";
|
|
9182
|
+
return {
|
|
9183
|
+
ok: text2.length > 0,
|
|
9184
|
+
provider: "gemini",
|
|
9185
|
+
message: text2.length > 0 ? `gemini ${backend} verified` : `empty response from gemini ${backend}`,
|
|
9186
|
+
model
|
|
9187
|
+
};
|
|
7762
9188
|
} catch (err) {
|
|
7763
9189
|
return {
|
|
7764
9190
|
ok: false,
|
|
@@ -7771,40 +9197,23 @@ async function healthcheck(config) {
|
|
|
7771
9197
|
async function executeTrackedQuery(input) {
|
|
7772
9198
|
const model = resolveModel(input.config);
|
|
7773
9199
|
const prompt = buildPrompt(input.keyword, input.location);
|
|
7774
|
-
|
|
7775
|
-
|
|
7776
|
-
|
|
7777
|
-
|
|
7778
|
-
|
|
7779
|
-
|
|
7780
|
-
|
|
7781
|
-
|
|
7782
|
-
|
|
7783
|
-
|
|
7784
|
-
|
|
7785
|
-
|
|
7786
|
-
|
|
7787
|
-
|
|
7788
|
-
|
|
7789
|
-
|
|
7790
|
-
|
|
7791
|
-
const genAI = new GoogleGenerativeAI(input.config.apiKey);
|
|
7792
|
-
const generativeModel = genAI.getGenerativeModel({
|
|
7793
|
-
model,
|
|
7794
|
-
tools: [searchTool]
|
|
7795
|
-
});
|
|
7796
|
-
const result = await generativeModel.generateContent(prompt);
|
|
7797
|
-
const response = result.response;
|
|
7798
|
-
const groundingMetadata = extractGroundingMetadata(response);
|
|
7799
|
-
const searchQueries = extractSearchQueries(response);
|
|
7800
|
-
return {
|
|
7801
|
-
provider: "gemini",
|
|
7802
|
-
rawResponse: responseToRecord(response),
|
|
7803
|
-
model,
|
|
7804
|
-
groundingSources: groundingMetadata,
|
|
7805
|
-
searchQueries
|
|
7806
|
-
};
|
|
7807
|
-
}
|
|
9200
|
+
const client = createClient2(input.config);
|
|
9201
|
+
const result = await client.models.generateContent({
|
|
9202
|
+
model,
|
|
9203
|
+
contents: prompt,
|
|
9204
|
+
config: {
|
|
9205
|
+
tools: [{ googleSearch: {} }]
|
|
9206
|
+
}
|
|
9207
|
+
});
|
|
9208
|
+
const groundingSources = extractGroundingMetadata(result);
|
|
9209
|
+
const searchQueries = extractSearchQueries(result);
|
|
9210
|
+
return {
|
|
9211
|
+
provider: "gemini",
|
|
9212
|
+
rawResponse: responseToRecord(result),
|
|
9213
|
+
model,
|
|
9214
|
+
groundingSources,
|
|
9215
|
+
searchQueries
|
|
9216
|
+
};
|
|
7808
9217
|
}
|
|
7809
9218
|
function normalizeResult(raw) {
|
|
7810
9219
|
const answerText = extractAnswerText(raw.rawResponse);
|
|
@@ -7850,22 +9259,6 @@ function extractGroundingMetadata(response) {
|
|
|
7850
9259
|
return [];
|
|
7851
9260
|
}
|
|
7852
9261
|
}
|
|
7853
|
-
function extractGroundingMetadataFromUnified(response) {
|
|
7854
|
-
try {
|
|
7855
|
-
const candidate = response.candidates?.[0];
|
|
7856
|
-
if (!candidate) return [];
|
|
7857
|
-
const metadata = candidate.groundingMetadata;
|
|
7858
|
-
if (!metadata) return [];
|
|
7859
|
-
const chunks = metadata.groundingChunks;
|
|
7860
|
-
if (!chunks) return [];
|
|
7861
|
-
return chunks.filter((chunk) => chunk.web?.uri).map((chunk) => ({
|
|
7862
|
-
uri: chunk.web.uri,
|
|
7863
|
-
title: chunk.web?.title ?? ""
|
|
7864
|
-
}));
|
|
7865
|
-
} catch {
|
|
7866
|
-
return [];
|
|
7867
|
-
}
|
|
7868
|
-
}
|
|
7869
9262
|
function extractSearchQueries(response) {
|
|
7870
9263
|
try {
|
|
7871
9264
|
const candidate = response.candidates?.[0];
|
|
@@ -7874,14 +9267,6 @@ function extractSearchQueries(response) {
|
|
|
7874
9267
|
return [];
|
|
7875
9268
|
}
|
|
7876
9269
|
}
|
|
7877
|
-
function extractSearchQueriesFromUnified(response) {
|
|
7878
|
-
try {
|
|
7879
|
-
const candidate = response.candidates?.[0];
|
|
7880
|
-
return candidate?.groundingMetadata?.webSearchQueries ?? [];
|
|
7881
|
-
} catch {
|
|
7882
|
-
return [];
|
|
7883
|
-
}
|
|
7884
|
-
}
|
|
7885
9270
|
function extractCitedDomains(raw) {
|
|
7886
9271
|
const domains = /* @__PURE__ */ new Set();
|
|
7887
9272
|
for (const source of raw.groundingSources) {
|
|
@@ -7929,16 +9314,12 @@ function extractDomainFromUri(uri) {
|
|
|
7929
9314
|
}
|
|
7930
9315
|
async function generateText(prompt, config) {
|
|
7931
9316
|
const model = resolveModel(config);
|
|
7932
|
-
|
|
7933
|
-
|
|
7934
|
-
|
|
7935
|
-
|
|
7936
|
-
}
|
|
7937
|
-
|
|
7938
|
-
const generativeModel = genAI.getGenerativeModel({ model });
|
|
7939
|
-
const result = await generativeModel.generateContent(prompt);
|
|
7940
|
-
return result.response.text();
|
|
7941
|
-
}
|
|
9317
|
+
const client = createClient2(config);
|
|
9318
|
+
const result = await client.models.generateContent({
|
|
9319
|
+
model,
|
|
9320
|
+
contents: prompt
|
|
9321
|
+
});
|
|
9322
|
+
return result.text ?? "";
|
|
7942
9323
|
}
|
|
7943
9324
|
function responseToRecord(response) {
|
|
7944
9325
|
try {
|
|
@@ -7958,24 +9339,6 @@ function responseToRecord(response) {
|
|
|
7958
9339
|
return { error: "failed to serialize response" };
|
|
7959
9340
|
}
|
|
7960
9341
|
}
|
|
7961
|
-
function unifiedToRecord(response) {
|
|
7962
|
-
try {
|
|
7963
|
-
const candidates = response.candidates?.map((c) => ({
|
|
7964
|
-
content: c.content,
|
|
7965
|
-
finishReason: c.finishReason,
|
|
7966
|
-
groundingMetadata: c.groundingMetadata ? {
|
|
7967
|
-
webSearchQueries: c.groundingMetadata.webSearchQueries,
|
|
7968
|
-
groundingChunks: c.groundingMetadata.groundingChunks
|
|
7969
|
-
} : void 0
|
|
7970
|
-
}));
|
|
7971
|
-
return {
|
|
7972
|
-
candidates: candidates ?? [],
|
|
7973
|
-
usageMetadata: response.usageMetadata ?? null
|
|
7974
|
-
};
|
|
7975
|
-
} catch {
|
|
7976
|
-
return { error: "failed to serialize response" };
|
|
7977
|
-
}
|
|
7978
|
-
}
|
|
7979
9342
|
|
|
7980
9343
|
// ../provider-gemini/src/adapter.ts
|
|
7981
9344
|
function toGeminiConfig(config) {
|
|
@@ -9648,8 +11011,59 @@ function removeGa4Connection(config, projectName) {
|
|
|
9648
11011
|
return true;
|
|
9649
11012
|
}
|
|
9650
11013
|
|
|
11014
|
+
// src/wordpress-config.ts
|
|
11015
|
+
function ensureConnections3(config) {
|
|
11016
|
+
if (!config.wordpress) config.wordpress = {};
|
|
11017
|
+
if (!config.wordpress.connections) config.wordpress.connections = [];
|
|
11018
|
+
return config.wordpress.connections;
|
|
11019
|
+
}
|
|
11020
|
+
function normalizeConnection(connection) {
|
|
11021
|
+
return {
|
|
11022
|
+
...connection,
|
|
11023
|
+
url: connection.url.replace(/\/$/, ""),
|
|
11024
|
+
stagingUrl: connection.stagingUrl?.replace(/\/$/, ""),
|
|
11025
|
+
defaultEnv: connection.defaultEnv ?? "live"
|
|
11026
|
+
};
|
|
11027
|
+
}
|
|
11028
|
+
function getWordpressConnection(config, projectName) {
|
|
11029
|
+
return (config.wordpress?.connections ?? []).find((connection) => connection.projectName === projectName);
|
|
11030
|
+
}
|
|
11031
|
+
function upsertWordpressConnection(config, connection) {
|
|
11032
|
+
const connections = ensureConnections3(config);
|
|
11033
|
+
const normalized = normalizeConnection(connection);
|
|
11034
|
+
const index2 = connections.findIndex((entry) => entry.projectName === connection.projectName);
|
|
11035
|
+
if (index2 === -1) {
|
|
11036
|
+
connections.push(normalized);
|
|
11037
|
+
return normalized;
|
|
11038
|
+
}
|
|
11039
|
+
connections[index2] = normalized;
|
|
11040
|
+
return normalized;
|
|
11041
|
+
}
|
|
11042
|
+
function patchWordpressConnection(config, projectName, patch) {
|
|
11043
|
+
const existing = getWordpressConnection(config, projectName);
|
|
11044
|
+
if (!existing) return void 0;
|
|
11045
|
+
return upsertWordpressConnection(config, {
|
|
11046
|
+
...existing,
|
|
11047
|
+
...patch,
|
|
11048
|
+
stagingUrl: Object.prototype.hasOwnProperty.call(patch, "stagingUrl") ? patch.stagingUrl : existing.stagingUrl,
|
|
11049
|
+
defaultEnv: patch.defaultEnv ?? existing.defaultEnv ?? "live"
|
|
11050
|
+
});
|
|
11051
|
+
}
|
|
11052
|
+
function removeWordpressConnection(config, projectName) {
|
|
11053
|
+
const connections = config.wordpress?.connections;
|
|
11054
|
+
if (!connections?.length) return false;
|
|
11055
|
+
const next = connections.filter((connection) => connection.projectName !== projectName);
|
|
11056
|
+
if (next.length === connections.length) return false;
|
|
11057
|
+
if (!config.wordpress) return false;
|
|
11058
|
+
config.wordpress.connections = next;
|
|
11059
|
+
if (next.length === 0) {
|
|
11060
|
+
delete config.wordpress;
|
|
11061
|
+
}
|
|
11062
|
+
return true;
|
|
11063
|
+
}
|
|
11064
|
+
|
|
9651
11065
|
// src/job-runner.ts
|
|
9652
|
-
import
|
|
11066
|
+
import crypto18 from "crypto";
|
|
9653
11067
|
import fs4 from "fs";
|
|
9654
11068
|
import path5 from "path";
|
|
9655
11069
|
import os4 from "os";
|
|
@@ -9892,7 +11306,7 @@ var JobRunner = class {
|
|
|
9892
11306
|
const overlap = computeCompetitorOverlap(normalized, competitorDomains);
|
|
9893
11307
|
let screenshotRelPath = null;
|
|
9894
11308
|
if (raw.screenshotPath && fs4.existsSync(raw.screenshotPath)) {
|
|
9895
|
-
const snapshotId =
|
|
11309
|
+
const snapshotId = crypto18.randomUUID();
|
|
9896
11310
|
const screenshotDir = path5.join(os4.homedir(), ".canonry", "screenshots", runId);
|
|
9897
11311
|
if (!fs4.existsSync(screenshotDir)) fs4.mkdirSync(screenshotDir, { recursive: true });
|
|
9898
11312
|
const destPath = path5.join(screenshotDir, `${snapshotId}.png`);
|
|
@@ -9920,7 +11334,7 @@ var JobRunner = class {
|
|
|
9920
11334
|
}).run();
|
|
9921
11335
|
} else {
|
|
9922
11336
|
this.db.insert(querySnapshots).values({
|
|
9923
|
-
id:
|
|
11337
|
+
id: crypto18.randomUUID(),
|
|
9924
11338
|
runId,
|
|
9925
11339
|
keywordId: kw.id,
|
|
9926
11340
|
provider: providerName,
|
|
@@ -10029,7 +11443,7 @@ var JobRunner = class {
|
|
|
10029
11443
|
incrementUsage(scope, metric, count) {
|
|
10030
11444
|
const now = /* @__PURE__ */ new Date();
|
|
10031
11445
|
const period = now.toISOString().slice(0, 10);
|
|
10032
|
-
const id =
|
|
11446
|
+
const id = crypto18.randomUUID();
|
|
10033
11447
|
const existing = this.db.select().from(usageCounters).where(eq17(usageCounters.scope, scope)).all().find((r) => r.period === period && r.metric === metric);
|
|
10034
11448
|
if (existing) {
|
|
10035
11449
|
this.db.update(usageCounters).set({ count: existing.count + count, updatedAt: now.toISOString() }).where(eq17(usageCounters.id, existing.id)).run();
|
|
@@ -10154,7 +11568,7 @@ function computeCompetitorOverlap(normalized, competitorDomains) {
|
|
|
10154
11568
|
}
|
|
10155
11569
|
|
|
10156
11570
|
// src/gsc-sync.ts
|
|
10157
|
-
import
|
|
11571
|
+
import crypto19 from "crypto";
|
|
10158
11572
|
import { eq as eq18, and as and8, sql as sql4 } from "drizzle-orm";
|
|
10159
11573
|
var log2 = createLogger("GscSync");
|
|
10160
11574
|
function formatDate2(d) {
|
|
@@ -10194,7 +11608,7 @@ async function executeGscSync(db, runId, projectId, opts) {
|
|
|
10194
11608
|
tokenExpiresAt: new Date(Date.now() + tokens.expires_in * 1e3).toISOString(),
|
|
10195
11609
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
10196
11610
|
});
|
|
10197
|
-
|
|
11611
|
+
saveConfigPatch(opts.config);
|
|
10198
11612
|
}
|
|
10199
11613
|
const lagOffset = GSC_DATA_LAG_DAYS;
|
|
10200
11614
|
const endDate = formatDate2(daysAgo(lagOffset));
|
|
@@ -10220,7 +11634,7 @@ async function executeGscSync(db, runId, projectId, opts) {
|
|
|
10220
11634
|
for (const row of batch) {
|
|
10221
11635
|
const [query, page, country, device, date] = row.keys;
|
|
10222
11636
|
db.insert(gscSearchData).values({
|
|
10223
|
-
id:
|
|
11637
|
+
id: crypto19.randomUUID(),
|
|
10224
11638
|
projectId,
|
|
10225
11639
|
syncRunId: runId,
|
|
10226
11640
|
date: date ?? "",
|
|
@@ -10254,7 +11668,7 @@ async function executeGscSync(db, runId, projectId, opts) {
|
|
|
10254
11668
|
const rich = ir.richResultsResult;
|
|
10255
11669
|
const inspectedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
10256
11670
|
db.insert(gscUrlInspections).values({
|
|
10257
|
-
id:
|
|
11671
|
+
id: crypto19.randomUUID(),
|
|
10258
11672
|
projectId,
|
|
10259
11673
|
syncRunId: runId,
|
|
10260
11674
|
url: pageUrl,
|
|
@@ -10298,7 +11712,7 @@ async function executeGscSync(db, runId, projectId, opts) {
|
|
|
10298
11712
|
const snapshotDate = formatDate2(/* @__PURE__ */ new Date());
|
|
10299
11713
|
db.delete(gscCoverageSnapshots).where(and8(eq18(gscCoverageSnapshots.projectId, projectId), eq18(gscCoverageSnapshots.date, snapshotDate))).run();
|
|
10300
11714
|
db.insert(gscCoverageSnapshots).values({
|
|
10301
|
-
id:
|
|
11715
|
+
id: crypto19.randomUUID(),
|
|
10302
11716
|
projectId,
|
|
10303
11717
|
syncRunId: runId,
|
|
10304
11718
|
date: snapshotDate,
|
|
@@ -10318,7 +11732,7 @@ async function executeGscSync(db, runId, projectId, opts) {
|
|
|
10318
11732
|
}
|
|
10319
11733
|
|
|
10320
11734
|
// src/gsc-inspect-sitemap.ts
|
|
10321
|
-
import
|
|
11735
|
+
import crypto20 from "crypto";
|
|
10322
11736
|
import { eq as eq19, and as and9 } from "drizzle-orm";
|
|
10323
11737
|
|
|
10324
11738
|
// src/sitemap-parser.ts
|
|
@@ -10415,7 +11829,7 @@ async function executeInspectSitemap(db, runId, projectId, opts) {
|
|
|
10415
11829
|
tokenExpiresAt: new Date(Date.now() + tokens.expires_in * 1e3).toISOString(),
|
|
10416
11830
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
10417
11831
|
});
|
|
10418
|
-
|
|
11832
|
+
saveConfigPatch(opts.config);
|
|
10419
11833
|
}
|
|
10420
11834
|
const sitemapUrl = opts.sitemapUrl || conn.sitemapUrl || `https://${project.canonicalDomain}/sitemap.xml`;
|
|
10421
11835
|
log3.info("sitemap.fetch", { runId, projectId, sitemapUrl });
|
|
@@ -10435,7 +11849,7 @@ async function executeInspectSitemap(db, runId, projectId, opts) {
|
|
|
10435
11849
|
const rich = ir.richResultsResult;
|
|
10436
11850
|
const inspectedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
10437
11851
|
db.insert(gscUrlInspections).values({
|
|
10438
|
-
id:
|
|
11852
|
+
id: crypto20.randomUUID(),
|
|
10439
11853
|
projectId,
|
|
10440
11854
|
syncRunId: runId,
|
|
10441
11855
|
url: pageUrl,
|
|
@@ -10485,7 +11899,7 @@ async function executeInspectSitemap(db, runId, projectId, opts) {
|
|
|
10485
11899
|
const snapshotDate = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
10486
11900
|
db.delete(gscCoverageSnapshots).where(and9(eq19(gscCoverageSnapshots.projectId, projectId), eq19(gscCoverageSnapshots.date, snapshotDate))).run();
|
|
10487
11901
|
db.insert(gscCoverageSnapshots).values({
|
|
10488
|
-
id:
|
|
11902
|
+
id: crypto20.randomUUID(),
|
|
10489
11903
|
projectId,
|
|
10490
11904
|
syncRunId: runId,
|
|
10491
11905
|
date: snapshotDate,
|
|
@@ -10677,7 +12091,7 @@ var Scheduler = class {
|
|
|
10677
12091
|
|
|
10678
12092
|
// src/notifier.ts
|
|
10679
12093
|
import { eq as eq21, desc as desc7, and as and10, or as or2 } from "drizzle-orm";
|
|
10680
|
-
import
|
|
12094
|
+
import crypto21 from "crypto";
|
|
10681
12095
|
var log5 = createLogger("Notifier");
|
|
10682
12096
|
var Notifier = class {
|
|
10683
12097
|
db;
|
|
@@ -10817,7 +12231,7 @@ var Notifier = class {
|
|
|
10817
12231
|
}
|
|
10818
12232
|
logDelivery(projectId, notificationId, event, status, error) {
|
|
10819
12233
|
this.db.insert(auditLog).values({
|
|
10820
|
-
id:
|
|
12234
|
+
id: crypto21.randomUUID(),
|
|
10821
12235
|
projectId,
|
|
10822
12236
|
actor: "scheduler",
|
|
10823
12237
|
action: `notification.${status}`,
|
|
@@ -10915,14 +12329,14 @@ async function fetchSiteText(domain) {
|
|
|
10915
12329
|
if (!redirectCheck.ok) return "";
|
|
10916
12330
|
const redirectResult = await fetchWithPinnedAddress(redirectCheck.target);
|
|
10917
12331
|
if (redirectResult.startsWith("REDIRECT:")) return "";
|
|
10918
|
-
return
|
|
12332
|
+
return stripHtml2(redirectResult);
|
|
10919
12333
|
}
|
|
10920
|
-
return
|
|
12334
|
+
return stripHtml2(result);
|
|
10921
12335
|
} catch {
|
|
10922
12336
|
return "";
|
|
10923
12337
|
}
|
|
10924
12338
|
}
|
|
10925
|
-
function
|
|
12339
|
+
function stripHtml2(html) {
|
|
10926
12340
|
if (!html) return "";
|
|
10927
12341
|
let text2 = html.replace(/<script[\s\S]*?<\/script>/gi, " ");
|
|
10928
12342
|
text2 = text2.replace(/<style[\s\S]*?<\/style>/gi, " ");
|
|
@@ -10974,7 +12388,7 @@ function summarizeProviderConfig(provider, config) {
|
|
|
10974
12388
|
};
|
|
10975
12389
|
}
|
|
10976
12390
|
function hashApiKey(key) {
|
|
10977
|
-
return
|
|
12391
|
+
return crypto22.createHash("sha256").update(key).digest("hex");
|
|
10978
12392
|
}
|
|
10979
12393
|
function parseCookies2(header) {
|
|
10980
12394
|
if (!header) return {};
|
|
@@ -11111,14 +12525,14 @@ async function createServer(opts) {
|
|
|
11111
12525
|
} else {
|
|
11112
12526
|
opts.config.bing.connections.push(connection);
|
|
11113
12527
|
}
|
|
11114
|
-
|
|
12528
|
+
saveConfigPatch(opts.config);
|
|
11115
12529
|
return connection;
|
|
11116
12530
|
},
|
|
11117
12531
|
updateConnection: (domain, patch) => {
|
|
11118
12532
|
const conn = opts.config.bing?.connections?.find((c) => c.domain === domain);
|
|
11119
12533
|
if (!conn) return void 0;
|
|
11120
12534
|
Object.assign(conn, patch);
|
|
11121
|
-
|
|
12535
|
+
saveConfigPatch(opts.config);
|
|
11122
12536
|
return conn;
|
|
11123
12537
|
},
|
|
11124
12538
|
deleteConnection: (domain) => {
|
|
@@ -11126,7 +12540,7 @@ async function createServer(opts) {
|
|
|
11126
12540
|
const idx = opts.config.bing.connections.findIndex((c) => c.domain === domain);
|
|
11127
12541
|
if (idx < 0) return false;
|
|
11128
12542
|
opts.config.bing.connections.splice(idx, 1);
|
|
11129
|
-
|
|
12543
|
+
saveConfigPatch(opts.config);
|
|
11130
12544
|
return true;
|
|
11131
12545
|
}
|
|
11132
12546
|
};
|
|
@@ -11136,32 +12550,52 @@ async function createServer(opts) {
|
|
|
11136
12550
|
},
|
|
11137
12551
|
upsertConnection: (connection) => {
|
|
11138
12552
|
const updated = upsertGa4Connection(opts.config, connection);
|
|
11139
|
-
|
|
12553
|
+
saveConfigPatch(opts.config);
|
|
11140
12554
|
return updated;
|
|
11141
12555
|
},
|
|
11142
12556
|
deleteConnection: (projectName) => {
|
|
11143
12557
|
const removed = removeGa4Connection(opts.config, projectName);
|
|
11144
|
-
if (removed)
|
|
12558
|
+
if (removed) saveConfigPatch(opts.config);
|
|
11145
12559
|
return removed;
|
|
11146
12560
|
}
|
|
11147
12561
|
};
|
|
11148
|
-
const googleStateSecret = process.env.GOOGLE_STATE_SECRET ??
|
|
12562
|
+
const googleStateSecret = process.env.GOOGLE_STATE_SECRET ?? crypto22.randomBytes(32).toString("hex");
|
|
11149
12563
|
const googleConnectionStore = {
|
|
11150
12564
|
listConnections: (domain) => listGoogleConnections(opts.config, domain),
|
|
11151
12565
|
getConnection: (domain, connectionType) => getGoogleConnection(opts.config, domain, connectionType),
|
|
11152
12566
|
upsertConnection: (connection) => {
|
|
11153
12567
|
const updated = upsertGoogleConnection(opts.config, connection);
|
|
11154
|
-
|
|
12568
|
+
saveConfigPatch(opts.config);
|
|
11155
12569
|
return updated;
|
|
11156
12570
|
},
|
|
11157
12571
|
updateConnection: (domain, connectionType, patch) => {
|
|
11158
12572
|
const updated = patchGoogleConnection(opts.config, domain, connectionType, patch);
|
|
11159
|
-
if (updated)
|
|
12573
|
+
if (updated) saveConfigPatch(opts.config);
|
|
11160
12574
|
return updated;
|
|
11161
12575
|
},
|
|
11162
12576
|
deleteConnection: (domain, connectionType) => {
|
|
11163
12577
|
const removed = removeGoogleConnection(opts.config, domain, connectionType);
|
|
11164
|
-
if (removed)
|
|
12578
|
+
if (removed) saveConfigPatch(opts.config);
|
|
12579
|
+
return removed;
|
|
12580
|
+
}
|
|
12581
|
+
};
|
|
12582
|
+
const wordpressConnectionStore = {
|
|
12583
|
+
getConnection: (projectName) => {
|
|
12584
|
+
return getWordpressConnection(opts.config, projectName);
|
|
12585
|
+
},
|
|
12586
|
+
upsertConnection: (connection) => {
|
|
12587
|
+
const updated = upsertWordpressConnection(opts.config, connection);
|
|
12588
|
+
saveConfigPatch(opts.config);
|
|
12589
|
+
return updated;
|
|
12590
|
+
},
|
|
12591
|
+
updateConnection: (projectName, patch) => {
|
|
12592
|
+
const updated = patchWordpressConnection(opts.config, projectName, patch);
|
|
12593
|
+
if (updated) saveConfigPatch(opts.config);
|
|
12594
|
+
return updated;
|
|
12595
|
+
},
|
|
12596
|
+
deleteConnection: (projectName) => {
|
|
12597
|
+
const removed = removeWordpressConnection(opts.config, projectName);
|
|
12598
|
+
if (removed) saveConfigPatch(opts.config);
|
|
11165
12599
|
return removed;
|
|
11166
12600
|
}
|
|
11167
12601
|
};
|
|
@@ -11175,7 +12609,7 @@ async function createServer(opts) {
|
|
|
11175
12609
|
if (!existing) {
|
|
11176
12610
|
const prefix = opts.config.apiKey.slice(0, 12);
|
|
11177
12611
|
opts.db.insert(apiKeys).values({
|
|
11178
|
-
id: `key_${
|
|
12612
|
+
id: `key_${crypto22.randomBytes(8).toString("hex")}`,
|
|
11179
12613
|
name: "default",
|
|
11180
12614
|
keyHash,
|
|
11181
12615
|
keyPrefix: prefix,
|
|
@@ -11199,7 +12633,7 @@ async function createServer(opts) {
|
|
|
11199
12633
|
};
|
|
11200
12634
|
const createSession = (apiKeyId) => {
|
|
11201
12635
|
pruneExpiredSessions();
|
|
11202
|
-
const sessionId =
|
|
12636
|
+
const sessionId = crypto22.randomBytes(32).toString("hex");
|
|
11203
12637
|
sessions.set(sessionId, {
|
|
11204
12638
|
apiKeyId,
|
|
11205
12639
|
expiresAt: Date.now() + SESSION_TTL_MS
|
|
@@ -11256,7 +12690,7 @@ async function createServer(opts) {
|
|
|
11256
12690
|
return reply.status(err.statusCode).send(err.toJSON());
|
|
11257
12691
|
}
|
|
11258
12692
|
opts.config.dashboardPasswordHash = hashApiKey(password);
|
|
11259
|
-
|
|
12693
|
+
saveConfigPatch(opts.config);
|
|
11260
12694
|
if (!createPasswordSession(reply)) {
|
|
11261
12695
|
const err = authInvalid();
|
|
11262
12696
|
return reply.status(err.statusCode).send(err.toJSON());
|
|
@@ -11362,6 +12796,7 @@ async function createServer(opts) {
|
|
|
11362
12796
|
googleSettingsSummary,
|
|
11363
12797
|
bingSettingsSummary,
|
|
11364
12798
|
bingConnectionStore,
|
|
12799
|
+
wordpressConnectionStore,
|
|
11365
12800
|
ga4CredentialStore,
|
|
11366
12801
|
onRunCreated: (runId, projectId, providers2, location) => {
|
|
11367
12802
|
jobRunner.executeRun(runId, projectId, providers2, location).catch((err) => {
|
|
@@ -11387,7 +12822,7 @@ async function createServer(opts) {
|
|
|
11387
12822
|
vertexCredentials: existing?.vertexCredentials
|
|
11388
12823
|
};
|
|
11389
12824
|
try {
|
|
11390
|
-
|
|
12825
|
+
saveConfigPatch(opts.config);
|
|
11391
12826
|
} catch (err) {
|
|
11392
12827
|
app.log.error({ err }, "Failed to save config");
|
|
11393
12828
|
return null;
|
|
@@ -11429,7 +12864,7 @@ async function createServer(opts) {
|
|
|
11429
12864
|
const targetProjectIds = affectedProjectIds.length > 0 ? affectedProjectIds : [null];
|
|
11430
12865
|
const createdAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
11431
12866
|
opts.db.insert(auditLog).values(targetProjectIds.map((projectId) => ({
|
|
11432
|
-
id:
|
|
12867
|
+
id: crypto22.randomUUID(),
|
|
11433
12868
|
projectId,
|
|
11434
12869
|
actor: "api",
|
|
11435
12870
|
action: existing ? "provider.updated" : "provider.created",
|
|
@@ -11449,7 +12884,7 @@ async function createServer(opts) {
|
|
|
11449
12884
|
onGoogleSettingsUpdate: (clientId, clientSecret) => {
|
|
11450
12885
|
try {
|
|
11451
12886
|
setGoogleAuthConfig(opts.config, { clientId, clientSecret });
|
|
11452
|
-
|
|
12887
|
+
saveConfigPatch(opts.config);
|
|
11453
12888
|
googleSettingsSummary.configured = true;
|
|
11454
12889
|
return { ...googleSettingsSummary };
|
|
11455
12890
|
} catch (err) {
|
|
@@ -11461,7 +12896,7 @@ async function createServer(opts) {
|
|
|
11461
12896
|
try {
|
|
11462
12897
|
if (!opts.config.bing) opts.config.bing = {};
|
|
11463
12898
|
opts.config.bing.apiKey = apiKey;
|
|
11464
|
-
|
|
12899
|
+
saveConfigPatch(opts.config);
|
|
11465
12900
|
bingSettingsSummary.configured = true;
|
|
11466
12901
|
return { ...bingSettingsSummary };
|
|
11467
12902
|
} catch (err) {
|
|
@@ -11488,7 +12923,7 @@ async function createServer(opts) {
|
|
|
11488
12923
|
setTelemetryEnabled: (enabled) => {
|
|
11489
12924
|
const config = loadConfig();
|
|
11490
12925
|
config.telemetry = enabled;
|
|
11491
|
-
|
|
12926
|
+
saveConfigPatch(config);
|
|
11492
12927
|
opts.config.telemetry = enabled;
|
|
11493
12928
|
},
|
|
11494
12929
|
onCdpConfigure: async (host, port2) => {
|
|
@@ -11496,7 +12931,7 @@ async function createServer(opts) {
|
|
|
11496
12931
|
opts.config.cdp.host = host;
|
|
11497
12932
|
opts.config.cdp.port = port2;
|
|
11498
12933
|
try {
|
|
11499
|
-
|
|
12934
|
+
saveConfigPatch(opts.config);
|
|
11500
12935
|
} catch (err) {
|
|
11501
12936
|
app.log.error({ err }, "Failed to save CDP config");
|
|
11502
12937
|
throw err;
|
|
@@ -11673,6 +13108,7 @@ export {
|
|
|
11673
13108
|
getConfigPath,
|
|
11674
13109
|
loadConfig,
|
|
11675
13110
|
saveConfig,
|
|
13111
|
+
saveConfigPatch,
|
|
11676
13112
|
configExists,
|
|
11677
13113
|
isTelemetryEnabled,
|
|
11678
13114
|
getOrCreateAnonymousId,
|