@brainfish-ai/devdoc 0.1.49 → 0.1.50
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/commands/deploy.js +72 -4
- package/dist/cli/commands/domain.js +134 -59
- package/package.json +1 -1
- package/renderer/app/api/collections/route.js +16 -7
- package/renderer/app/api/deploy/route.js +3 -3
- package/renderer/app/api/domains/add/route.js +118 -40
- package/renderer/app/api/domains/remove/route.js +16 -3
- package/renderer/app/api/domains/verify/route.js +82 -17
- package/renderer/app/api/schema/route.js +12 -11
- package/renderer/components/docs-header.js +11 -11
- package/renderer/lib/docs/config/domain-schema.js +23 -1
- package/renderer/lib/docs/config/index.js +1 -1
- package/renderer/lib/storage/blob.js +4 -2
- package/renderer/lib/vercel/domains.js +275 -0
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
import { NextResponse } from 'next/server';
|
|
2
|
-
import { validateApiKey, addCustomDomain, isCustomDomainRegistered } from '@/lib/storage/blob';
|
|
3
|
-
import { isValidDomain, normalizeDomain
|
|
2
|
+
import { validateApiKey, addCustomDomain, isCustomDomainRegistered, updateCustomDomainStatus } from '@/lib/storage/blob';
|
|
3
|
+
import { isValidDomain, normalizeDomain } from '@/lib/docs/config';
|
|
4
|
+
import { addDomainToProject, isVercelIntegrationEnabled, formatVerificationInstructions, getDomainType } from '@/lib/vercel/domains';
|
|
4
5
|
/**
|
|
5
6
|
* POST /api/domains/add
|
|
6
7
|
*
|
|
7
8
|
* Add a custom domain to a project.
|
|
8
9
|
* Each project can have ONE custom domain (free).
|
|
9
10
|
*
|
|
11
|
+
* When Vercel integration is enabled (VERCEL_API_TOKEN + VERCEL_PROJECT_ID):
|
|
12
|
+
* - Domain is added to Vercel project
|
|
13
|
+
* - Real DNS verification instructions from Vercel are returned
|
|
14
|
+
* - SSL is provisioned automatically by Vercel after verification
|
|
15
|
+
*
|
|
10
16
|
* Headers:
|
|
11
17
|
* Authorization: Bearer <api_key>
|
|
12
18
|
*
|
|
@@ -18,10 +24,9 @@ import { isValidDomain, normalizeDomain, getDnsInstructions } from '@/lib/docs/c
|
|
|
18
24
|
* success: true,
|
|
19
25
|
* domain: "docs.example.com",
|
|
20
26
|
* status: "pending",
|
|
21
|
-
* verification:
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
* }
|
|
27
|
+
* verification: [
|
|
28
|
+
* { type: "TXT", domain: "_vercel.docs.example.com", value: "vc-domain-verify=..." }
|
|
29
|
+
* ]
|
|
25
30
|
* }
|
|
26
31
|
*/ export async function POST(request) {
|
|
27
32
|
try {
|
|
@@ -63,7 +68,7 @@ import { isValidDomain, normalizeDomain, getDnsInstructions } from '@/lib/docs/c
|
|
|
63
68
|
status: 400
|
|
64
69
|
});
|
|
65
70
|
}
|
|
66
|
-
// Check if domain is already registered
|
|
71
|
+
// Check if domain is already registered in our registry
|
|
67
72
|
const isRegistered = await isCustomDomainRegistered(customDomain);
|
|
68
73
|
if (isRegistered) {
|
|
69
74
|
return NextResponse.json({
|
|
@@ -72,42 +77,115 @@ import { isValidDomain, normalizeDomain, getDnsInstructions } from '@/lib/docs/c
|
|
|
72
77
|
status: 409
|
|
73
78
|
});
|
|
74
79
|
}
|
|
75
|
-
//
|
|
76
|
-
|
|
77
|
-
|
|
80
|
+
// Check if Vercel integration is enabled
|
|
81
|
+
if (isVercelIntegrationEnabled()) {
|
|
82
|
+
// Add domain to Vercel project
|
|
83
|
+
const vercelResult = await addDomainToProject(customDomain);
|
|
84
|
+
if (!vercelResult.success) {
|
|
85
|
+
return NextResponse.json({
|
|
86
|
+
error: vercelResult.error,
|
|
87
|
+
vercelError: vercelResult.vercelError
|
|
88
|
+
}, {
|
|
89
|
+
status: 400
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
const vercelDomain = vercelResult.domain;
|
|
93
|
+
// Add to our internal registry with Vercel data
|
|
94
|
+
const result = await addCustomDomain(projectSlug, customDomain);
|
|
95
|
+
if (!result.success) {
|
|
96
|
+
// Rollback: Try to remove from Vercel if we couldn't add to registry
|
|
97
|
+
console.error('[Domains API] Failed to add to registry, domain added to Vercel:', customDomain);
|
|
98
|
+
return NextResponse.json({
|
|
99
|
+
error: result.error
|
|
100
|
+
}, {
|
|
101
|
+
status: 400
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
// Update with Vercel domain ID
|
|
105
|
+
await updateCustomDomainStatus(customDomain, vercelDomain.verified ? 'active' : 'pending', {
|
|
106
|
+
vercelDomainId: vercelDomain.projectId
|
|
107
|
+
});
|
|
108
|
+
// If Vercel already verified the domain (e.g., previously configured DNS)
|
|
109
|
+
if (vercelDomain.verified) {
|
|
110
|
+
return NextResponse.json({
|
|
111
|
+
success: true,
|
|
112
|
+
domain: customDomain,
|
|
113
|
+
projectSlug,
|
|
114
|
+
status: 'active',
|
|
115
|
+
verified: true,
|
|
116
|
+
message: 'Domain is already verified and active!'
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
// Format Vercel's verification instructions
|
|
120
|
+
const verification = vercelDomain.verification || [];
|
|
121
|
+
const { records, instructions } = formatVerificationInstructions(verification);
|
|
122
|
+
// Add CNAME instruction for subdomains (Vercel may not always include it)
|
|
123
|
+
const domainType = getDomainType(customDomain);
|
|
124
|
+
const parts = customDomain.split('.');
|
|
125
|
+
const subdomain = parts.length > 2 ? parts[0] : '@';
|
|
126
|
+
const cnameInstruction = domainType === 'subdomain' ? `\nAlso add a CNAME record:\n Name: ${subdomain}\n Value: cname.vercel-dns.com` : `\nFor apex domains, add an A record:\n Name: @\n Value: 76.76.21.21`;
|
|
78
127
|
return NextResponse.json({
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
128
|
+
success: true,
|
|
129
|
+
domain: customDomain,
|
|
130
|
+
projectSlug,
|
|
131
|
+
status: 'pending',
|
|
132
|
+
verified: false,
|
|
133
|
+
verification: records,
|
|
134
|
+
instructions: [
|
|
135
|
+
...instructions,
|
|
136
|
+
cnameInstruction
|
|
137
|
+
],
|
|
138
|
+
vercelVerification: verification
|
|
139
|
+
});
|
|
140
|
+
} else {
|
|
141
|
+
// Fallback: No Vercel integration, use legacy behavior
|
|
142
|
+
console.warn('[Domains API] Vercel integration not configured, using legacy mode');
|
|
143
|
+
const result = await addCustomDomain(projectSlug, customDomain);
|
|
144
|
+
if (!result.success) {
|
|
145
|
+
return NextResponse.json({
|
|
146
|
+
error: result.error
|
|
147
|
+
}, {
|
|
148
|
+
status: 400
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
// Legacy hardcoded DNS instructions
|
|
152
|
+
const parts = customDomain.split('.');
|
|
153
|
+
const subdomain = parts.length > 2 ? parts[0] : '@';
|
|
154
|
+
return NextResponse.json({
|
|
155
|
+
success: true,
|
|
156
|
+
domain: customDomain,
|
|
157
|
+
projectSlug,
|
|
158
|
+
status: result.entry.status,
|
|
159
|
+
verified: false,
|
|
160
|
+
verification: [
|
|
161
|
+
{
|
|
162
|
+
type: 'CNAME',
|
|
163
|
+
name: subdomain,
|
|
164
|
+
value: 'cname.vercel-dns.com'
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
type: 'TXT',
|
|
168
|
+
name: `_devdoc-verify.${customDomain}`,
|
|
169
|
+
value: result.entry.verificationToken || ''
|
|
170
|
+
}
|
|
171
|
+
],
|
|
172
|
+
instructions: [
|
|
173
|
+
'Add the following DNS records to your domain:',
|
|
174
|
+
'',
|
|
175
|
+
'1. CNAME Record:',
|
|
176
|
+
` Name: ${subdomain}`,
|
|
177
|
+
' Value: cname.vercel-dns.com',
|
|
178
|
+
'',
|
|
179
|
+
'2. TXT Record (for verification):',
|
|
180
|
+
` Name: _devdoc-verify.${customDomain}`,
|
|
181
|
+
` Value: ${result.entry.verificationToken || ''}`,
|
|
182
|
+
'',
|
|
183
|
+
'After adding DNS records, run "devdoc domain verify" to verify.',
|
|
184
|
+
'',
|
|
185
|
+
'Note: Vercel integration not configured. Set VERCEL_API_TOKEN and VERCEL_PROJECT_ID for automatic SSL.'
|
|
186
|
+
]
|
|
82
187
|
});
|
|
83
188
|
}
|
|
84
|
-
// Get DNS instructions
|
|
85
|
-
const dnsInstructions = getDnsInstructions(customDomain);
|
|
86
|
-
// Add verification token to TXT record
|
|
87
|
-
dnsInstructions.txt.value = result.entry.verificationToken || '';
|
|
88
|
-
return NextResponse.json({
|
|
89
|
-
success: true,
|
|
90
|
-
domain: customDomain,
|
|
91
|
-
projectSlug,
|
|
92
|
-
status: result.entry.status,
|
|
93
|
-
verification: {
|
|
94
|
-
cname: dnsInstructions.cname,
|
|
95
|
-
txt: dnsInstructions.txt
|
|
96
|
-
},
|
|
97
|
-
instructions: [
|
|
98
|
-
'Add the following DNS records to your domain:',
|
|
99
|
-
'',
|
|
100
|
-
`1. CNAME Record:`,
|
|
101
|
-
` Name: ${dnsInstructions.cname.name}`,
|
|
102
|
-
` Value: ${dnsInstructions.cname.value}`,
|
|
103
|
-
'',
|
|
104
|
-
`2. TXT Record (for verification):`,
|
|
105
|
-
` Name: ${dnsInstructions.txt.name}`,
|
|
106
|
-
` Value: ${dnsInstructions.txt.value}`,
|
|
107
|
-
'',
|
|
108
|
-
'After adding DNS records, run "devdoc domain verify" to verify.'
|
|
109
|
-
]
|
|
110
|
-
});
|
|
111
189
|
} catch (error) {
|
|
112
190
|
console.error('[Domains API] Error adding domain:', error);
|
|
113
191
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
import { NextResponse } from 'next/server';
|
|
2
2
|
import { validateApiKey, getProjectCustomDomain, removeCustomDomain } from '@/lib/storage/blob';
|
|
3
3
|
import { normalizeDomain } from '@/lib/docs/config';
|
|
4
|
+
import { removeDomain as vercelRemoveDomain, isVercelIntegrationEnabled } from '@/lib/vercel/domains';
|
|
4
5
|
/**
|
|
5
6
|
* DELETE /api/domains/remove
|
|
6
7
|
*
|
|
7
8
|
* Remove a custom domain from a project.
|
|
8
9
|
*
|
|
10
|
+
* When Vercel integration is enabled:
|
|
11
|
+
* - Removes domain from Vercel project
|
|
12
|
+
* - Removes from internal registry
|
|
13
|
+
*
|
|
9
14
|
* Headers:
|
|
10
15
|
* Authorization: Bearer <api_key>
|
|
11
16
|
*
|
|
@@ -52,7 +57,17 @@ import { normalizeDomain } from '@/lib/docs/config';
|
|
|
52
57
|
}
|
|
53
58
|
customDomain = projectDomain.customDomain;
|
|
54
59
|
}
|
|
55
|
-
// Remove
|
|
60
|
+
// Remove from Vercel if integration is enabled
|
|
61
|
+
if (isVercelIntegrationEnabled()) {
|
|
62
|
+
const vercelResult = await vercelRemoveDomain(customDomain);
|
|
63
|
+
if (!vercelResult.success) {
|
|
64
|
+
// Log warning but continue - domain might not exist in Vercel
|
|
65
|
+
console.warn('[Domains API] Failed to remove from Vercel:', vercelResult.error);
|
|
66
|
+
} else {
|
|
67
|
+
console.log('[Domains API] Domain removed from Vercel:', customDomain);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
// Remove from internal registry
|
|
56
71
|
const result = await removeCustomDomain(customDomain, projectSlug);
|
|
57
72
|
if (!result.success) {
|
|
58
73
|
return NextResponse.json({
|
|
@@ -61,8 +76,6 @@ import { normalizeDomain } from '@/lib/docs/config';
|
|
|
61
76
|
status: 400
|
|
62
77
|
});
|
|
63
78
|
}
|
|
64
|
-
// TODO: In production, also remove from Vercel via API
|
|
65
|
-
// await vercelApi.removeDomain(customDomain)
|
|
66
79
|
return NextResponse.json({
|
|
67
80
|
success: true,
|
|
68
81
|
message: `Domain ${customDomain} removed successfully`,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { NextResponse } from 'next/server';
|
|
2
2
|
import { validateApiKey, getProjectCustomDomain, getCustomDomainEntry, updateCustomDomainStatus } from '@/lib/storage/blob';
|
|
3
3
|
import { normalizeDomain } from '@/lib/docs/config';
|
|
4
|
+
import { verifyDomain as vercelVerifyDomain, getDomainConfig, isVercelIntegrationEnabled, formatVerificationInstructions } from '@/lib/vercel/domains';
|
|
4
5
|
import dns from 'dns';
|
|
5
6
|
import { promisify } from 'util';
|
|
6
7
|
const resolveCname = promisify(dns.resolveCname);
|
|
@@ -9,7 +10,13 @@ const resolveTxt = promisify(dns.resolveTxt);
|
|
|
9
10
|
* POST /api/domains/verify
|
|
10
11
|
*
|
|
11
12
|
* Verify DNS configuration for a custom domain.
|
|
12
|
-
*
|
|
13
|
+
*
|
|
14
|
+
* When Vercel integration is enabled:
|
|
15
|
+
* - Calls Vercel's verify endpoint directly
|
|
16
|
+
* - Vercel handles DNS verification and SSL provisioning
|
|
17
|
+
*
|
|
18
|
+
* Legacy mode (no Vercel integration):
|
|
19
|
+
* - Checks CNAME and TXT records manually
|
|
13
20
|
*
|
|
14
21
|
* Headers:
|
|
15
22
|
* Authorization: Bearer <api_key>
|
|
@@ -21,11 +28,9 @@ const resolveTxt = promisify(dns.resolveTxt);
|
|
|
21
28
|
* {
|
|
22
29
|
* success: true,
|
|
23
30
|
* domain: "docs.example.com",
|
|
24
|
-
* status: "
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
* txt: { found: true, verified: true }
|
|
28
|
-
* }
|
|
31
|
+
* status: "active",
|
|
32
|
+
* verified: true,
|
|
33
|
+
* message: "Domain verified! SSL will be provisioned automatically."
|
|
29
34
|
* }
|
|
30
35
|
*/ export async function POST(request) {
|
|
31
36
|
try {
|
|
@@ -62,7 +67,7 @@ const resolveTxt = promisify(dns.resolveTxt);
|
|
|
62
67
|
}
|
|
63
68
|
customDomain = projectDomain.customDomain;
|
|
64
69
|
}
|
|
65
|
-
// Get domain entry
|
|
70
|
+
// Get domain entry from our registry
|
|
66
71
|
const domainEntry = await getCustomDomainEntry(customDomain);
|
|
67
72
|
if (!domainEntry) {
|
|
68
73
|
return NextResponse.json({
|
|
@@ -79,12 +84,74 @@ const resolveTxt = promisify(dns.resolveTxt);
|
|
|
79
84
|
status: 403
|
|
80
85
|
});
|
|
81
86
|
}
|
|
82
|
-
//
|
|
87
|
+
// Use Vercel integration if available
|
|
88
|
+
if (isVercelIntegrationEnabled()) {
|
|
89
|
+
// Call Vercel's verify endpoint
|
|
90
|
+
const verifyResult = await vercelVerifyDomain(customDomain);
|
|
91
|
+
if (!verifyResult.success) {
|
|
92
|
+
// Get current domain config to show what's needed
|
|
93
|
+
const configResult = await getDomainConfig(customDomain);
|
|
94
|
+
const verification = configResult.domain?.verification || [];
|
|
95
|
+
let instructions = [];
|
|
96
|
+
if (verification.length > 0) {
|
|
97
|
+
const formatted = formatVerificationInstructions(verification);
|
|
98
|
+
instructions = formatted.instructions;
|
|
99
|
+
}
|
|
100
|
+
return NextResponse.json({
|
|
101
|
+
success: false,
|
|
102
|
+
domain: customDomain,
|
|
103
|
+
projectSlug,
|
|
104
|
+
status: domainEntry.status,
|
|
105
|
+
verified: false,
|
|
106
|
+
message: verifyResult.error || 'DNS verification failed. Please check your DNS records.',
|
|
107
|
+
verification: verification.map((v)=>({
|
|
108
|
+
type: v.type,
|
|
109
|
+
name: v.domain,
|
|
110
|
+
value: v.value
|
|
111
|
+
})),
|
|
112
|
+
instructions
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
// Domain verified successfully
|
|
116
|
+
if (verifyResult.verified) {
|
|
117
|
+
// Update our registry to mark as active
|
|
118
|
+
await updateCustomDomainStatus(customDomain, 'active');
|
|
119
|
+
return NextResponse.json({
|
|
120
|
+
success: true,
|
|
121
|
+
domain: customDomain,
|
|
122
|
+
projectSlug,
|
|
123
|
+
status: 'active',
|
|
124
|
+
verified: true,
|
|
125
|
+
message: 'Domain verified! SSL certificate will be provisioned automatically by Vercel.'
|
|
126
|
+
});
|
|
127
|
+
} else {
|
|
128
|
+
// Vercel didn't verify yet, return what's needed
|
|
129
|
+
const verification = verifyResult.domain?.verification || [];
|
|
130
|
+
const { instructions } = formatVerificationInstructions(verification);
|
|
131
|
+
return NextResponse.json({
|
|
132
|
+
success: false,
|
|
133
|
+
domain: customDomain,
|
|
134
|
+
projectSlug,
|
|
135
|
+
status: 'pending',
|
|
136
|
+
verified: false,
|
|
137
|
+
message: 'DNS records not yet propagated. This can take up to 48 hours.',
|
|
138
|
+
verification: verification.map((v)=>({
|
|
139
|
+
type: v.type,
|
|
140
|
+
name: v.domain,
|
|
141
|
+
value: v.value
|
|
142
|
+
})),
|
|
143
|
+
instructions
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
// Legacy verification (no Vercel integration)
|
|
148
|
+
console.warn('[Domains API] Vercel integration not configured, using legacy DNS verification');
|
|
149
|
+
// Check DNS records manually
|
|
83
150
|
const checks = {
|
|
84
151
|
cname: {
|
|
85
152
|
found: false,
|
|
86
153
|
value: null,
|
|
87
|
-
expected: 'cname.
|
|
154
|
+
expected: 'cname.vercel-dns.com'
|
|
88
155
|
},
|
|
89
156
|
txt: {
|
|
90
157
|
found: false,
|
|
@@ -116,18 +183,15 @@ const resolveTxt = promisify(dns.resolveTxt);
|
|
|
116
183
|
// TXT not found or DNS error
|
|
117
184
|
}
|
|
118
185
|
// Determine overall status
|
|
119
|
-
const cnameValid = checks.cname.found && checks.cname.value?.toLowerCase().includes('
|
|
186
|
+
const cnameValid = checks.cname.found && checks.cname.value?.toLowerCase().includes('vercel');
|
|
120
187
|
const txtValid = checks.txt.found && checks.txt.verified;
|
|
121
188
|
let newStatus = domainEntry.status;
|
|
122
189
|
let message = '';
|
|
123
190
|
if (cnameValid && txtValid) {
|
|
124
|
-
// DNS is verified
|
|
125
|
-
newStatus = '
|
|
126
|
-
message = 'DNS verified!
|
|
127
|
-
await updateCustomDomainStatus(customDomain, '
|
|
128
|
-
// In production, this would trigger Vercel API to add domain
|
|
129
|
-
// For now, we simulate SSL provisioning by updating status
|
|
130
|
-
// TODO: Integrate with Vercel Domains API
|
|
191
|
+
// DNS is verified
|
|
192
|
+
newStatus = 'active';
|
|
193
|
+
message = 'DNS verified! Domain is now active.';
|
|
194
|
+
await updateCustomDomainStatus(customDomain, 'active');
|
|
131
195
|
} else if (cnameValid && !txtValid) {
|
|
132
196
|
message = 'CNAME record found, but TXT verification record is missing or incorrect.';
|
|
133
197
|
} else if (!cnameValid && txtValid) {
|
|
@@ -140,6 +204,7 @@ const resolveTxt = promisify(dns.resolveTxt);
|
|
|
140
204
|
domain: customDomain,
|
|
141
205
|
projectSlug,
|
|
142
206
|
status: newStatus,
|
|
207
|
+
verified: cnameValid && txtValid,
|
|
143
208
|
message,
|
|
144
209
|
checks: {
|
|
145
210
|
cname: {
|
|
@@ -6,10 +6,6 @@ import { getProjectContent } from '@/lib/storage/blob';
|
|
|
6
6
|
const STARTER_PATH = process.env.STARTER_PATH || 'devdoc-docs';
|
|
7
7
|
// Get the docs directory - respects STARTER_PATH for local development
|
|
8
8
|
function getDocsDir() {
|
|
9
|
-
const projectSlug = process.env.BRAINFISH_PROJECT_SLUG;
|
|
10
|
-
if (projectSlug) {
|
|
11
|
-
return join(process.cwd(), '.devdoc', projectSlug);
|
|
12
|
-
}
|
|
13
9
|
// Use STARTER_PATH (can be absolute or relative)
|
|
14
10
|
if (isAbsolute(STARTER_PATH)) {
|
|
15
11
|
return STARTER_PATH;
|
|
@@ -43,22 +39,27 @@ export async function GET(request) {
|
|
|
43
39
|
});
|
|
44
40
|
}
|
|
45
41
|
try {
|
|
46
|
-
//
|
|
47
|
-
const projectSlug = process.env.BRAINFISH_PROJECT_SLUG;
|
|
42
|
+
// Get project slug from middleware header (multi-tenant) or env var (single-tenant)
|
|
43
|
+
const projectSlug = request.headers.get('x-devdoc-project') || process.env.BRAINFISH_PROJECT_SLUG;
|
|
44
|
+
// Try blob storage first (for deployed/multi-tenant projects)
|
|
48
45
|
if (projectSlug) {
|
|
49
46
|
const projectContent = await getProjectContent(projectSlug);
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
47
|
+
// Check dedicated graphqlSchemas map first (like OpenAPI specs)
|
|
48
|
+
if (projectContent?.graphqlSchemas) {
|
|
49
|
+
const schemaContent = projectContent.graphqlSchemas[path] || projectContent.graphqlSchemas[path.replace(/^\//, '')] // Try without leading slash
|
|
50
|
+
;
|
|
51
|
+
if (schemaContent) {
|
|
52
|
+
return new NextResponse(schemaContent, {
|
|
54
53
|
headers: {
|
|
55
54
|
'Content-Type': 'text/plain'
|
|
56
55
|
}
|
|
57
56
|
});
|
|
58
57
|
}
|
|
58
|
+
// Log for debugging - schema not found
|
|
59
|
+
console.log('[Schema API] Schema not found in graphqlSchemas:', path, 'Available:', Object.keys(projectContent.graphqlSchemas));
|
|
59
60
|
}
|
|
60
61
|
}
|
|
61
|
-
// Try local filesystem
|
|
62
|
+
// Try local filesystem (for local development)
|
|
62
63
|
const docsDir = getDocsDir();
|
|
63
64
|
const fullPath = join(docsDir, path);
|
|
64
65
|
if (!existsSync(fullPath)) {
|
|
@@ -7,9 +7,8 @@ import { ThemeToggle } from "@/components/theme-toggle";
|
|
|
7
7
|
import { useMobile } from "@/lib/api-docs/mobile-context";
|
|
8
8
|
import { Button } from "@/components/ui/button";
|
|
9
9
|
import { cn } from "@/lib/utils";
|
|
10
|
-
// Default logo
|
|
11
|
-
const
|
|
12
|
-
url: 'https://cdn.prod.website-files.com/639181964f4fe88697a20a0a/67af64a5ba42bb659b8560c9_Logo%20(3).svg',
|
|
10
|
+
// Default logo dimensions (no default URL - show name only if no logo configured)
|
|
11
|
+
const DEFAULT_LOGO_DIMENSIONS = {
|
|
13
12
|
alt: 'Logo',
|
|
14
13
|
width: 80,
|
|
15
14
|
height: 24
|
|
@@ -19,13 +18,14 @@ export function DocsHeader({ navigationTabs = [], activeTab, onTabChange, docsNa
|
|
|
19
18
|
// Use config values with defaults
|
|
20
19
|
// Support both single URL and separate light/dark logos
|
|
21
20
|
const hasLightDarkLogos = docsLogo?.light && docsLogo?.dark;
|
|
21
|
+
const hasAnyLogo = docsLogo?.url || hasLightDarkLogos;
|
|
22
22
|
const logo = {
|
|
23
|
-
url: docsLogo?.url
|
|
23
|
+
url: docsLogo?.url,
|
|
24
24
|
light: docsLogo?.light,
|
|
25
25
|
dark: docsLogo?.dark,
|
|
26
|
-
alt: docsLogo?.alt ||
|
|
27
|
-
width: docsLogo?.width ||
|
|
28
|
-
height: docsLogo?.height ||
|
|
26
|
+
alt: docsLogo?.alt || DEFAULT_LOGO_DIMENSIONS.alt,
|
|
27
|
+
width: docsLogo?.width || DEFAULT_LOGO_DIMENSIONS.width,
|
|
28
|
+
height: docsLogo?.height || DEFAULT_LOGO_DIMENSIONS.height
|
|
29
29
|
};
|
|
30
30
|
const showSearch = docsHeader?.showSearch !== false // Default true
|
|
31
31
|
;
|
|
@@ -87,7 +87,7 @@ export function DocsHeader({ navigationTabs = [], activeTab, onTabChange, docsNa
|
|
|
87
87
|
priority: true
|
|
88
88
|
})
|
|
89
89
|
]
|
|
90
|
-
}) : /*#__PURE__*/ _jsx(Image, {
|
|
90
|
+
}) : logo.url ? /*#__PURE__*/ _jsx(Image, {
|
|
91
91
|
src: logo.url,
|
|
92
92
|
alt: logo.alt,
|
|
93
93
|
width: logo.width,
|
|
@@ -97,14 +97,14 @@ export function DocsHeader({ navigationTabs = [], activeTab, onTabChange, docsNa
|
|
|
97
97
|
height: logo.height
|
|
98
98
|
},
|
|
99
99
|
priority: true
|
|
100
|
-
}),
|
|
100
|
+
}) : null,
|
|
101
101
|
docsName && /*#__PURE__*/ _jsxs(_Fragment, {
|
|
102
102
|
children: [
|
|
103
|
-
/*#__PURE__*/ _jsx("div", {
|
|
103
|
+
hasAnyLogo && /*#__PURE__*/ _jsx("div", {
|
|
104
104
|
className: "hidden sm:block h-5 w-px bg-border"
|
|
105
105
|
}),
|
|
106
106
|
/*#__PURE__*/ _jsx("span", {
|
|
107
|
-
className: "docs-header-title
|
|
107
|
+
className: cn("docs-header-title text-sm font-medium pointer-events-none", hasAnyLogo ? "hidden sm:inline text-muted-foreground" : "text-foreground font-semibold"),
|
|
108
108
|
children: docsName
|
|
109
109
|
})
|
|
110
110
|
]
|
|
@@ -145,13 +145,26 @@ export const domainConfigSchema = z.object({
|
|
|
145
145
|
}
|
|
146
146
|
/**
|
|
147
147
|
* Get DNS instructions for a custom domain
|
|
148
|
+
*
|
|
149
|
+
* NOTE: When Vercel integration is enabled (VERCEL_API_TOKEN + VERCEL_PROJECT_ID),
|
|
150
|
+
* the actual DNS instructions come from Vercel's API response.
|
|
151
|
+
* This function is used as a fallback for legacy/local development mode.
|
|
152
|
+
*
|
|
153
|
+
* For Vercel-hosted projects:
|
|
154
|
+
* - Subdomains: CNAME to cname.vercel-dns.com
|
|
155
|
+
* - Apex domains: A record to 76.76.21.21
|
|
156
|
+
*
|
|
157
|
+
* @param customDomain - The custom domain (e.g., "docs.example.com")
|
|
158
|
+
* @returns DNS instructions for CNAME and TXT records
|
|
148
159
|
*/ export function getDnsInstructions(customDomain) {
|
|
149
160
|
const parts = customDomain.split('.');
|
|
150
161
|
const subdomain = parts.length > 2 ? parts[0] : '@';
|
|
162
|
+
const isApexDomain = parts.length <= 2;
|
|
151
163
|
return {
|
|
152
164
|
cname: {
|
|
153
165
|
name: subdomain === '@' ? customDomain : subdomain,
|
|
154
|
-
|
|
166
|
+
// Use Vercel's actual DNS target
|
|
167
|
+
value: isApexDomain ? '76.76.21.21' : 'cname.vercel-dns.com'
|
|
155
168
|
},
|
|
156
169
|
txt: {
|
|
157
170
|
name: `_devdoc-verify.${customDomain}`,
|
|
@@ -159,3 +172,12 @@ export const domainConfigSchema = z.object({
|
|
|
159
172
|
}
|
|
160
173
|
};
|
|
161
174
|
}
|
|
175
|
+
/**
|
|
176
|
+
* Get human-readable DNS type based on domain
|
|
177
|
+
*
|
|
178
|
+
* @param customDomain - The custom domain
|
|
179
|
+
* @returns 'A' for apex domains, 'CNAME' for subdomains
|
|
180
|
+
*/ export function getDnsRecordType(customDomain) {
|
|
181
|
+
const parts = customDomain.split('.');
|
|
182
|
+
return parts.length <= 2 ? 'A' : 'CNAME';
|
|
183
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Configuration Module Exports
|
|
3
3
|
*/ export { docsConfigSchema, parseDocsConfig, safeParseDocsConfig, getDefaultDocsConfig } from './schema';
|
|
4
|
-
export { domainConfigSchema, parseDomainConfig, safeParseDomainConfig, isValidDomain, normalizeDomain, getDnsInstructions } from './domain-schema';
|
|
4
|
+
export { domainConfigSchema, parseDomainConfig, safeParseDomainConfig, isValidDomain, normalizeDomain, getDnsInstructions, getDnsRecordType } from './domain-schema';
|
|
5
5
|
export { loadDocsConfig, safeLoadDocsConfig, clearConfigCache, hasDocsConfig, getContentDir, resolvePagePath, loadPageContent, listMdxFiles } from './loader';
|
|
6
6
|
export { isDevMode, isProductionMode, shouldShowItem } from './environment';
|
|
@@ -30,7 +30,7 @@ function _getFileBlobPath(slug, filePath) {
|
|
|
30
30
|
}
|
|
31
31
|
/**
|
|
32
32
|
* Store project content in Vercel Blob (or local filesystem in dev)
|
|
33
|
-
*/ export async function storeProjectContent(slug, name, docsJson, files, themeJson, openApiSpecs) {
|
|
33
|
+
*/ export async function storeProjectContent(slug, name, docsJson, files, themeJson, openApiSpecs, graphqlSchemas) {
|
|
34
34
|
const now = new Date().toISOString();
|
|
35
35
|
const content = {
|
|
36
36
|
slug,
|
|
@@ -41,6 +41,7 @@ function _getFileBlobPath(slug, filePath) {
|
|
|
41
41
|
k,
|
|
42
42
|
JSON.stringify(v)
|
|
43
43
|
])) : undefined,
|
|
44
|
+
graphqlSchemas,
|
|
44
45
|
files,
|
|
45
46
|
createdAt: now,
|
|
46
47
|
updatedAt: now
|
|
@@ -92,7 +93,7 @@ function _getFileBlobPath(slug, filePath) {
|
|
|
92
93
|
}
|
|
93
94
|
/**
|
|
94
95
|
* Update existing project content
|
|
95
|
-
*/ export async function updateProjectContent(slug, docsJson, files, themeJson, openApiSpecs) {
|
|
96
|
+
*/ export async function updateProjectContent(slug, docsJson, files, themeJson, openApiSpecs, graphqlSchemas) {
|
|
96
97
|
// Get existing content to preserve createdAt
|
|
97
98
|
const existing = await getProjectContent(slug);
|
|
98
99
|
const now = new Date().toISOString();
|
|
@@ -105,6 +106,7 @@ function _getFileBlobPath(slug, filePath) {
|
|
|
105
106
|
k,
|
|
106
107
|
JSON.stringify(v)
|
|
107
108
|
])) : undefined,
|
|
109
|
+
graphqlSchemas,
|
|
108
110
|
files,
|
|
109
111
|
createdAt: existing?.createdAt || now,
|
|
110
112
|
updatedAt: now
|