@prenta/core 5.3.0 → 5.4.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/CHANGELOG.md +32 -0
- package/dist/ai/content-writing-generate.d.ts.map +1 -1
- package/dist/ai/content-writing-generate.js +3 -0
- package/dist/ai/content-writing-generate.js.map +1 -1
- package/dist/ai/feature-defaults.d.ts +1 -1
- package/dist/ai/feature-defaults.d.ts.map +1 -1
- package/dist/ai/feature-defaults.js +3 -13
- package/dist/ai/feature-defaults.js.map +1 -1
- package/dist/ai/governance.d.ts +31 -0
- package/dist/ai/governance.d.ts.map +1 -0
- package/dist/ai/governance.js +56 -0
- package/dist/ai/governance.js.map +1 -0
- package/dist/ai/index.d.ts +2 -1
- package/dist/ai/index.d.ts.map +1 -1
- package/dist/ai/index.js +2 -1
- package/dist/ai/index.js.map +1 -1
- package/dist/ai/providers/custom.d.ts +2 -1
- package/dist/ai/providers/custom.d.ts.map +1 -1
- package/dist/ai/providers/custom.js +7 -1
- package/dist/ai/providers/custom.js.map +1 -1
- package/dist/ai/providers/http.d.ts +13 -0
- package/dist/ai/providers/http.d.ts.map +1 -1
- package/dist/ai/providers/http.js +33 -0
- package/dist/ai/providers/http.js.map +1 -1
- package/dist/ai/providers/index.d.ts +5 -2
- package/dist/ai/providers/index.d.ts.map +1 -1
- package/dist/ai/providers/index.js +10 -0
- package/dist/ai/providers/index.js.map +1 -1
- package/dist/ai/providers/openai.d.ts +2 -1
- package/dist/ai/providers/openai.d.ts.map +1 -1
- package/dist/ai/providers/openai.js +4 -1
- package/dist/ai/providers/openai.js.map +1 -1
- package/dist/ai/runtime.d.ts +28 -1
- package/dist/ai/runtime.d.ts.map +1 -1
- package/dist/ai/runtime.js +73 -2
- package/dist/ai/runtime.js.map +1 -1
- package/dist/ai/usage.d.ts +6 -0
- package/dist/ai/usage.d.ts.map +1 -1
- package/dist/ai/usage.js +29 -0
- package/dist/ai/usage.js.map +1 -1
- package/dist/api/route-helpers.d.ts +2 -0
- package/dist/api/route-helpers.d.ts.map +1 -1
- package/dist/api/route-helpers.js +14 -0
- package/dist/api/route-helpers.js.map +1 -1
- package/dist/api/routes/ai-authoring.js +1 -1
- package/dist/api/routes/ai-authoring.js.map +1 -1
- package/dist/api/routes/ai-seo.d.ts.map +1 -1
- package/dist/api/routes/ai-seo.js +13 -5
- package/dist/api/routes/ai-seo.js.map +1 -1
- package/dist/api/routes/ai-settings.d.ts.map +1 -1
- package/dist/api/routes/ai-settings.js +39 -1
- package/dist/api/routes/ai-settings.js.map +1 -1
- package/dist/api/routes/redirects.d.ts.map +1 -1
- package/dist/api/routes/redirects.js +3 -2
- package/dist/api/routes/redirects.js.map +1 -1
- package/dist/api/routes/scheduling.d.ts.map +1 -1
- package/dist/api/routes/scheduling.js +17 -0
- package/dist/api/routes/scheduling.js.map +1 -1
- package/dist/api/routes/sections.d.ts.map +1 -1
- package/dist/api/routes/sections.js +9 -40
- package/dist/api/routes/sections.js.map +1 -1
- package/dist/api/routes/seo-public.d.ts.map +1 -1
- package/dist/api/routes/seo-public.js +205 -1
- package/dist/api/routes/seo-public.js.map +1 -1
- package/dist/api/routes/seo.d.ts.map +1 -1
- package/dist/api/routes/seo.js +216 -16
- package/dist/api/routes/seo.js.map +1 -1
- package/dist/db/model-types.d.ts +32 -0
- package/dist/db/model-types.d.ts.map +1 -1
- package/dist/generated/browser.d.ts +17 -0
- package/dist/generated/client.d.ts +17 -0
- package/dist/generated/commonInputTypes.d.ts +30 -30
- package/dist/generated/internal/class.d.ts +22 -0
- package/dist/generated/internal/class.js +4 -4
- package/dist/generated/internal/prismaNamespace.d.ts +198 -1
- package/dist/generated/internal/prismaNamespace.js +43 -0
- package/dist/generated/internal/prismaNamespaceBrowser.d.ts +47 -0
- package/dist/generated/internal/prismaNamespaceBrowser.js +43 -0
- package/dist/generated/models/ApiRequestMetric.d.ts +0 -7
- package/dist/generated/models/SearchPageInsight.d.ts +1460 -0
- package/dist/generated/models/SearchPageInsight.js +1 -0
- package/dist/generated/models/SearchPerfDaily.d.ts +1167 -0
- package/dist/generated/models/SearchPerfDaily.js +1 -0
- package/dist/generated/models.d.ts +2 -0
- package/dist/redirects/recovery.d.ts +12 -0
- package/dist/redirects/recovery.d.ts.map +1 -1
- package/dist/redirects/recovery.js +42 -2
- package/dist/redirects/recovery.js.map +1 -1
- package/dist/redirects/rerank.d.ts +14 -1
- package/dist/redirects/rerank.d.ts.map +1 -1
- package/dist/redirects/rerank.js +66 -37
- package/dist/redirects/rerank.js.map +1 -1
- package/dist/seo/audit-engine.d.ts +21 -3
- package/dist/seo/audit-engine.d.ts.map +1 -1
- package/dist/seo/audit-engine.js +51 -6
- package/dist/seo/audit-engine.js.map +1 -1
- package/dist/seo/audit-runner.d.ts.map +1 -1
- package/dist/seo/audit-runner.js +18 -0
- package/dist/seo/audit-runner.js.map +1 -1
- package/dist/seo/config-store.d.ts +13 -0
- package/dist/seo/config-store.d.ts.map +1 -1
- package/dist/seo/config-store.js +6 -0
- package/dist/seo/config-store.js.map +1 -1
- package/dist/seo/crux.d.ts +58 -0
- package/dist/seo/crux.d.ts.map +1 -0
- package/dist/seo/crux.js +101 -0
- package/dist/seo/crux.js.map +1 -0
- package/dist/seo/issue-content-brief.d.ts.map +1 -1
- package/dist/seo/issue-content-brief.js +11 -0
- package/dist/seo/issue-content-brief.js.map +1 -1
- package/dist/seo/issue-content-research.d.ts.map +1 -1
- package/dist/seo/issue-content-research.js +3 -0
- package/dist/seo/issue-content-research.js.map +1 -1
- package/dist/seo/issue-fix-ai.d.ts.map +1 -1
- package/dist/seo/issue-fix-ai.js +3 -0
- package/dist/seo/issue-fix-ai.js.map +1 -1
- package/dist/seo/issue-fix-redirect.d.ts.map +1 -1
- package/dist/seo/issue-fix-redirect.js +1 -0
- package/dist/seo/issue-fix-redirect.js.map +1 -1
- package/dist/seo/search-console/client.d.ts +39 -0
- package/dist/seo/search-console/client.d.ts.map +1 -0
- package/dist/seo/search-console/client.js +226 -0
- package/dist/seo/search-console/client.js.map +1 -0
- package/dist/seo/search-console/prompt-context.d.ts +21 -0
- package/dist/seo/search-console/prompt-context.d.ts.map +1 -0
- package/dist/seo/search-console/prompt-context.js +93 -0
- package/dist/seo/search-console/prompt-context.js.map +1 -0
- package/dist/seo/search-console/sync.d.ts +67 -0
- package/dist/seo/search-console/sync.d.ts.map +1 -0
- package/dist/seo/search-console/sync.js +469 -0
- package/dist/seo/search-console/sync.js.map +1 -0
- package/dist/seo/search-console/types.d.ts +75 -0
- package/dist/seo/search-console/types.d.ts.map +1 -0
- package/dist/seo/search-console/types.js +16 -0
- package/dist/seo/search-console/types.js.map +1 -0
- package/dist/seo/search-insight-signals.d.ts +29 -0
- package/dist/seo/search-insight-signals.d.ts.map +1 -0
- package/dist/seo/search-insight-signals.js +103 -0
- package/dist/seo/search-insight-signals.js.map +1 -0
- package/package.json +1 -1
- package/prisma/migrations/0016_search_console_tables/migration.sql +56 -0
- package/prisma/schema.prisma +49 -0
package/dist/api/routes/seo.js
CHANGED
|
@@ -637,10 +637,47 @@ export function registerSeoRoutes(router) {
|
|
|
637
637
|
};
|
|
638
638
|
}
|
|
639
639
|
// Demo/seed search-performance rows live in a hidden collection so they can
|
|
640
|
-
// be inserted by the dev seed script and never ship to npm.
|
|
641
|
-
//
|
|
640
|
+
// be inserted by the dev seed script and never ship to npm. With a connected
|
|
641
|
+
// Search Console the synced `searchPageInsight` table takes precedence.
|
|
642
642
|
const SEO_PERF_COLLECTION = '__seo_perf__';
|
|
643
|
-
|
|
643
|
+
/** Synced GSC data (trailing 28 days). Returns null when there is none yet. */
|
|
644
|
+
async function loadRealSearchPerformance(limit) {
|
|
645
|
+
if (!hasModel(db(), 'searchPageInsight'))
|
|
646
|
+
return null;
|
|
647
|
+
try {
|
|
648
|
+
const rows = await db().searchPageInsight.findMany({
|
|
649
|
+
where: { impressions28d: { gt: 0 } },
|
|
650
|
+
orderBy: { impressions28d: 'desc' },
|
|
651
|
+
take: limit,
|
|
652
|
+
});
|
|
653
|
+
if (rows.length === 0)
|
|
654
|
+
return null;
|
|
655
|
+
const ids = rows.map((r) => r.entityId).filter(Boolean);
|
|
656
|
+
const titles = new Map();
|
|
657
|
+
if (ids.length > 0) {
|
|
658
|
+
const docs = await db().document.findMany({
|
|
659
|
+
where: { id: { in: ids } },
|
|
660
|
+
select: { id: true, title: true },
|
|
661
|
+
});
|
|
662
|
+
for (const d of docs) {
|
|
663
|
+
if (d.title)
|
|
664
|
+
titles.set(d.id, d.title);
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
return rows.map((r) => ({
|
|
668
|
+
url: r.path,
|
|
669
|
+
title: (r.entityId && titles.get(r.entityId)) || r.path,
|
|
670
|
+
impressions: r.impressions28d,
|
|
671
|
+
clicks: r.clicks28d,
|
|
672
|
+
ctr: r.impressions28d > 0 ? r.clicks28d / r.impressions28d : 0,
|
|
673
|
+
averagePosition: r.avgPosition28d ?? 0,
|
|
674
|
+
}));
|
|
675
|
+
}
|
|
676
|
+
catch {
|
|
677
|
+
return null;
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
async function loadDemoSearchPerformance(limit) {
|
|
644
681
|
try {
|
|
645
682
|
const docs = await db().document.findMany({
|
|
646
683
|
where: { collection: SEO_PERF_COLLECTION, deletedAt: null },
|
|
@@ -666,6 +703,65 @@ export function registerSeoRoutes(router) {
|
|
|
666
703
|
return [];
|
|
667
704
|
}
|
|
668
705
|
}
|
|
706
|
+
/** Real synced data when connected; `__seo_perf__` demo rows otherwise. */
|
|
707
|
+
async function loadSearchPerformance(limit = 10, connected = false) {
|
|
708
|
+
if (connected) {
|
|
709
|
+
const real = await loadRealSearchPerformance(limit);
|
|
710
|
+
if (real)
|
|
711
|
+
return real;
|
|
712
|
+
}
|
|
713
|
+
return loadDemoSearchPerformance(limit);
|
|
714
|
+
}
|
|
715
|
+
/**
|
|
716
|
+
* Site-level 28d totals + prior-window deltas from the insight table for the
|
|
717
|
+
* overview metric cards. Position is impressions-weighted. Null when no
|
|
718
|
+
* synced data exists (metrics then fall back to the perf-row estimate).
|
|
719
|
+
*/
|
|
720
|
+
async function loadSearchInsightStats() {
|
|
721
|
+
if (!hasModel(db(), 'searchPageInsight'))
|
|
722
|
+
return null;
|
|
723
|
+
try {
|
|
724
|
+
const rows = (await db().searchPageInsight.findMany({
|
|
725
|
+
where: { OR: [{ impressions28d: { gt: 0 } }, { impressionsPrev28d: { gt: 0 } }] },
|
|
726
|
+
select: {
|
|
727
|
+
impressions28d: true,
|
|
728
|
+
impressionsPrev28d: true,
|
|
729
|
+
avgPosition28d: true,
|
|
730
|
+
avgPositionPrev28d: true,
|
|
731
|
+
},
|
|
732
|
+
take: 2000,
|
|
733
|
+
}));
|
|
734
|
+
if (rows.length === 0)
|
|
735
|
+
return null;
|
|
736
|
+
let impressions28d = 0;
|
|
737
|
+
let impressionsPrev28d = 0;
|
|
738
|
+
let posWeight = 0;
|
|
739
|
+
let posWeightBase = 0;
|
|
740
|
+
let prevPosWeight = 0;
|
|
741
|
+
let prevPosWeightBase = 0;
|
|
742
|
+
for (const r of rows) {
|
|
743
|
+
impressions28d += r.impressions28d;
|
|
744
|
+
impressionsPrev28d += r.impressionsPrev28d;
|
|
745
|
+
if (r.avgPosition28d !== null && r.impressions28d > 0) {
|
|
746
|
+
posWeight += r.avgPosition28d * r.impressions28d;
|
|
747
|
+
posWeightBase += r.impressions28d;
|
|
748
|
+
}
|
|
749
|
+
if (r.avgPositionPrev28d !== null && r.impressionsPrev28d > 0) {
|
|
750
|
+
prevPosWeight += r.avgPositionPrev28d * r.impressionsPrev28d;
|
|
751
|
+
prevPosWeightBase += r.impressionsPrev28d;
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
return {
|
|
755
|
+
impressions28d,
|
|
756
|
+
impressionsPrev28d,
|
|
757
|
+
avgPosition: posWeightBase > 0 ? posWeight / posWeightBase : null,
|
|
758
|
+
avgPositionPrev: prevPosWeightBase > 0 ? prevPosWeight / prevPosWeightBase : null,
|
|
759
|
+
};
|
|
760
|
+
}
|
|
761
|
+
catch {
|
|
762
|
+
return null;
|
|
763
|
+
}
|
|
764
|
+
}
|
|
669
765
|
router.post('/seo/audit', async (request) => {
|
|
670
766
|
try {
|
|
671
767
|
const auth = await requireAuth(request);
|
|
@@ -1794,7 +1890,10 @@ export function registerSeoRoutes(router) {
|
|
|
1794
1890
|
? meta.indexableCount
|
|
1795
1891
|
: total;
|
|
1796
1892
|
const auditTruncated = meta.truncated === true;
|
|
1797
|
-
const perf = await loadSearchPerformance(10);
|
|
1893
|
+
const perf = await loadSearchPerformance(10, moduleSettings.searchConsoleConnected);
|
|
1894
|
+
const searchStats = moduleSettings.searchConsoleConnected
|
|
1895
|
+
? await loadSearchInsightStats()
|
|
1896
|
+
: null;
|
|
1798
1897
|
const summary = {
|
|
1799
1898
|
total: openIssues.length,
|
|
1800
1899
|
critical: openIssues.filter((i) => i.severity === 'critical').length,
|
|
@@ -1804,10 +1903,30 @@ export function registerSeoRoutes(router) {
|
|
|
1804
1903
|
const scoreAfter = lastRun?.siteScoreAfter ?? null;
|
|
1805
1904
|
const scoreBefore = lastRun?.siteScoreBefore ?? null;
|
|
1806
1905
|
const breakdown = lastRun?.metadata?.breakdown ?? [];
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1906
|
+
// Real site-level stats (weighted by impressions, with prior-window
|
|
1907
|
+
// trends) when synced data exists; the perf-row estimate otherwise.
|
|
1908
|
+
const avgPosition = searchStats?.avgPosition ??
|
|
1909
|
+
(perf.length > 0
|
|
1910
|
+
? perf.reduce((s, r) => s + r.averagePosition, 0) / perf.length
|
|
1911
|
+
: null);
|
|
1912
|
+
const impressions28d = searchStats?.impressions28d ?? perf.reduce((s, r) => s + r.impressions, 0);
|
|
1913
|
+
// Position: lower is better, so an "up" trend means the number dropped.
|
|
1914
|
+
let positionTrend = 'flat';
|
|
1915
|
+
if (searchStats?.avgPosition != null && searchStats.avgPositionPrev != null) {
|
|
1916
|
+
const delta = searchStats.avgPosition - searchStats.avgPositionPrev;
|
|
1917
|
+
if (delta <= -0.5)
|
|
1918
|
+
positionTrend = 'up';
|
|
1919
|
+
else if (delta >= 0.5)
|
|
1920
|
+
positionTrend = 'down';
|
|
1921
|
+
}
|
|
1922
|
+
let impressionsTrend = 'flat';
|
|
1923
|
+
if (searchStats && searchStats.impressionsPrev28d > 0) {
|
|
1924
|
+
const ratio = searchStats.impressions28d / searchStats.impressionsPrev28d;
|
|
1925
|
+
if (ratio >= 1.05)
|
|
1926
|
+
impressionsTrend = 'up';
|
|
1927
|
+
else if (ratio <= 0.95)
|
|
1928
|
+
impressionsTrend = 'down';
|
|
1929
|
+
}
|
|
1811
1930
|
const metrics = [
|
|
1812
1931
|
{
|
|
1813
1932
|
label: 'Pages Indexed',
|
|
@@ -1824,16 +1943,42 @@ export function registerSeoRoutes(router) {
|
|
|
1824
1943
|
{
|
|
1825
1944
|
label: 'Avg. Position',
|
|
1826
1945
|
value: avgPosition !== null ? avgPosition.toFixed(1) : '—',
|
|
1827
|
-
helper: moduleSettings.searchConsoleConnected
|
|
1828
|
-
|
|
1946
|
+
helper: moduleSettings.searchConsoleConnected
|
|
1947
|
+
? searchStats?.avgPositionPrev != null
|
|
1948
|
+
? `was ${searchStats.avgPositionPrev.toFixed(1)} the prior 28 days`
|
|
1949
|
+
: 'last 28 days'
|
|
1950
|
+
: 'Connect Search Console',
|
|
1951
|
+
trend: positionTrend,
|
|
1829
1952
|
},
|
|
1830
1953
|
{
|
|
1831
|
-
label: '
|
|
1832
|
-
value:
|
|
1833
|
-
helper: moduleSettings.searchConsoleConnected
|
|
1834
|
-
|
|
1954
|
+
label: 'Impressions',
|
|
1955
|
+
value: impressions28d > 0 ? formatCompactNumber(impressions28d) : '—',
|
|
1956
|
+
helper: moduleSettings.searchConsoleConnected
|
|
1957
|
+
? searchStats && searchStats.impressionsPrev28d > 0
|
|
1958
|
+
? `was ${formatCompactNumber(searchStats.impressionsPrev28d)} the prior 28 days`
|
|
1959
|
+
: 'last 28 days'
|
|
1960
|
+
: 'Connect Search Console',
|
|
1961
|
+
trend: impressionsTrend,
|
|
1835
1962
|
},
|
|
1836
1963
|
];
|
|
1964
|
+
// Weight quick wins by real traffic when synced search data exists.
|
|
1965
|
+
const impressionsByEntityId = {};
|
|
1966
|
+
if (moduleSettings.searchConsoleConnected && hasModel(db(), 'searchPageInsight')) {
|
|
1967
|
+
try {
|
|
1968
|
+
const impressionRows = (await db().searchPageInsight.findMany({
|
|
1969
|
+
where: { entityId: { not: null }, impressions28d: { gt: 0 } },
|
|
1970
|
+
select: { entityId: true, impressions28d: true },
|
|
1971
|
+
take: 2000,
|
|
1972
|
+
}));
|
|
1973
|
+
for (const r of impressionRows) {
|
|
1974
|
+
if (r.entityId)
|
|
1975
|
+
impressionsByEntityId[r.entityId] = r.impressions28d;
|
|
1976
|
+
}
|
|
1977
|
+
}
|
|
1978
|
+
catch {
|
|
1979
|
+
/* weighting is optional */
|
|
1980
|
+
}
|
|
1981
|
+
}
|
|
1837
1982
|
const quickWins = buildQuickWins(openIssues.map((i) => ({
|
|
1838
1983
|
issueKey: i.issueKey,
|
|
1839
1984
|
entityType: i.entityType,
|
|
@@ -1850,7 +1995,7 @@ export function registerSeoRoutes(router) {
|
|
|
1850
1995
|
fixable: i.fixable ?? false,
|
|
1851
1996
|
fixActionType: i.fixActionType ?? null,
|
|
1852
1997
|
relatedEntityIds: Array.isArray(i.relatedEntityIds) ? i.relatedEntityIds : [],
|
|
1853
|
-
}))).map((w) => ({
|
|
1998
|
+
})), { impressionsByEntityId }).map((w) => ({
|
|
1854
1999
|
id: w.id,
|
|
1855
2000
|
title: w.title,
|
|
1856
2001
|
explanation: w.explanation,
|
|
@@ -1888,13 +2033,68 @@ export function registerSeoRoutes(router) {
|
|
|
1888
2033
|
return auth.error;
|
|
1889
2034
|
const url = new URL(request.url);
|
|
1890
2035
|
const limit = Math.min(50, Math.max(1, Number(url.searchParams.get('limit') ?? 10)));
|
|
1891
|
-
const
|
|
2036
|
+
const { getSeoOverrides, resolveSeoModuleSettings } = await import('../../seo/config-store.js');
|
|
2037
|
+
const settings = resolveSeoModuleSettings(await getSeoOverrides(db()));
|
|
2038
|
+
const rows = await loadSearchPerformance(limit, settings.searchConsoleConnected);
|
|
1892
2039
|
return json({ data: { rows } });
|
|
1893
2040
|
}
|
|
1894
2041
|
catch (err) {
|
|
1895
2042
|
return internalError(err, 'seo/search-performance');
|
|
1896
2043
|
}
|
|
1897
2044
|
});
|
|
2045
|
+
// Per-document search insight (synced GSC + CrUX) for the editor pane's
|
|
2046
|
+
// read-only stats block. `insight: null` when not synced or no data.
|
|
2047
|
+
router.get('/seo/search-insights/:entityId', async (request, params) => {
|
|
2048
|
+
try {
|
|
2049
|
+
const auth = await requireAuth(request);
|
|
2050
|
+
if (auth.error)
|
|
2051
|
+
return auth.error;
|
|
2052
|
+
const entityId = params.entityId;
|
|
2053
|
+
if (!entityId)
|
|
2054
|
+
return errorResponse('entityId is required', 400);
|
|
2055
|
+
const { getSeoOverrides, resolveSeoModuleSettings } = await import('../../seo/config-store.js');
|
|
2056
|
+
const settings = resolveSeoModuleSettings(await getSeoOverrides(db()));
|
|
2057
|
+
const connected = settings.searchConsoleConnected;
|
|
2058
|
+
if (!connected || !hasModel(db(), 'searchPageInsight')) {
|
|
2059
|
+
return json({ data: { connected, insight: null } });
|
|
2060
|
+
}
|
|
2061
|
+
const row = await db().searchPageInsight.findFirst({
|
|
2062
|
+
where: { entityId },
|
|
2063
|
+
select: {
|
|
2064
|
+
path: true,
|
|
2065
|
+
clicks28d: true,
|
|
2066
|
+
impressions28d: true,
|
|
2067
|
+
avgPosition28d: true,
|
|
2068
|
+
clicksPrev28d: true,
|
|
2069
|
+
impressionsPrev28d: true,
|
|
2070
|
+
avgPositionPrev28d: true,
|
|
2071
|
+
topQueries: true,
|
|
2072
|
+
lcpMs: true,
|
|
2073
|
+
inpMs: true,
|
|
2074
|
+
cls: true,
|
|
2075
|
+
cwvStatus: true,
|
|
2076
|
+
lastSyncedAt: true,
|
|
2077
|
+
},
|
|
2078
|
+
});
|
|
2079
|
+
return json({
|
|
2080
|
+
data: {
|
|
2081
|
+
connected,
|
|
2082
|
+
insight: row
|
|
2083
|
+
? {
|
|
2084
|
+
...row,
|
|
2085
|
+
topQueries: Array.isArray(row.topQueries) ? row.topQueries : [],
|
|
2086
|
+
lastSyncedAt: row.lastSyncedAt instanceof Date
|
|
2087
|
+
? row.lastSyncedAt.toISOString()
|
|
2088
|
+
: row.lastSyncedAt,
|
|
2089
|
+
}
|
|
2090
|
+
: null,
|
|
2091
|
+
},
|
|
2092
|
+
});
|
|
2093
|
+
}
|
|
2094
|
+
catch (err) {
|
|
2095
|
+
return internalError(err, 'seo/search-insights');
|
|
2096
|
+
}
|
|
2097
|
+
});
|
|
1898
2098
|
router.get('/seo/content', async (request) => {
|
|
1899
2099
|
try {
|
|
1900
2100
|
const auth = await requireAuth(request);
|