multisite-cms-mcp 1.0.24 → 1.1.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/dist/index.js CHANGED
@@ -12,6 +12,8 @@ const get_example_1 = require("./tools/get-example");
12
12
  const get_conversion_guide_1 = require("./tools/get-conversion-guide");
13
13
  const get_tenant_schema_1 = require("./tools/get-tenant-schema");
14
14
  const list_projects_1 = require("./tools/list-projects");
15
+ const create_site_1 = require("./tools/create-site");
16
+ const deploy_package_1 = require("./tools/deploy-package");
15
17
  const server = new index_js_1.Server({
16
18
  name: 'multisite-cms',
17
19
  version: '1.0.0',
@@ -164,6 +166,46 @@ const TOOLS = [
164
166
  required: ['projectId'],
165
167
  },
166
168
  },
169
+ {
170
+ name: 'create_site',
171
+ description: 'Create a new Fast Mode site/project. Opens browser for authentication if not already logged in. After creation, use deploy_package to upload your website.',
172
+ inputSchema: {
173
+ type: 'object',
174
+ properties: {
175
+ name: {
176
+ type: 'string',
177
+ description: 'The name for your new project',
178
+ },
179
+ subdomain: {
180
+ type: 'string',
181
+ description: 'Optional custom subdomain (auto-generated from name if not provided)',
182
+ },
183
+ },
184
+ required: ['name'],
185
+ },
186
+ },
187
+ {
188
+ name: 'deploy_package',
189
+ description: 'Deploy a website package (.zip file) to Fast Mode. Can deploy to an existing project or create a new one. Opens browser for authentication if not already logged in. Use list_projects first to see existing projects.',
190
+ inputSchema: {
191
+ type: 'object',
192
+ properties: {
193
+ packagePath: {
194
+ type: 'string',
195
+ description: 'Path to the .zip file containing the website package',
196
+ },
197
+ projectId: {
198
+ type: 'string',
199
+ description: 'Optional: Deploy to an existing project with this ID. Use list_projects to get project IDs.',
200
+ },
201
+ projectName: {
202
+ type: 'string',
203
+ description: 'Optional: Create a new project with this name and deploy to it. Required if projectId is not provided and no projects exist.',
204
+ },
205
+ },
206
+ required: ['packagePath'],
207
+ },
208
+ },
167
209
  ];
168
210
  // Handle list tools request
169
211
  server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => {
@@ -210,6 +252,12 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
210
252
  case 'get_tenant_schema':
211
253
  result = await (0, get_tenant_schema_1.getTenantSchema)(params.projectId);
212
254
  break;
255
+ case 'create_site':
256
+ result = await (0, create_site_1.createSite)(params.name, params.subdomain);
257
+ break;
258
+ case 'deploy_package':
259
+ result = await (0, deploy_package_1.deployPackage)(params.packagePath, params.projectId, params.projectName);
260
+ break;
213
261
  default:
214
262
  return {
215
263
  content: [{ type: 'text', text: `Unknown tool: ${name}` }],
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Create a new Fast Mode site/project
3
+ *
4
+ * @param name - The name of the project
5
+ * @param subdomain - Optional: Custom subdomain (auto-generated from name if not provided)
6
+ */
7
+ export declare function createSite(name: string, subdomain?: string): Promise<string>;
8
+ //# sourceMappingURL=create-site.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"create-site.d.ts","sourceRoot":"","sources":["../../src/tools/create-site.ts"],"names":[],"mappings":"AAgBA;;;;;GAKG;AACH,wBAAsB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAyFlF"}
@@ -0,0 +1,127 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createSite = createSite;
4
+ const api_client_1 = require("../lib/api-client");
5
+ const device_flow_1 = require("../lib/device-flow");
6
+ /**
7
+ * Create a new Fast Mode site/project
8
+ *
9
+ * @param name - The name of the project
10
+ * @param subdomain - Optional: Custom subdomain (auto-generated from name if not provided)
11
+ */
12
+ async function createSite(name, subdomain) {
13
+ // Check authentication
14
+ if (await (0, api_client_1.needsAuthentication)()) {
15
+ const authResult = await (0, device_flow_1.ensureAuthenticated)();
16
+ if (!authResult.authenticated) {
17
+ return authResult.message;
18
+ }
19
+ }
20
+ // Validate name
21
+ if (!name || name.trim().length === 0) {
22
+ return `# Invalid Name
23
+
24
+ Please provide a name for your project.
25
+
26
+ Example:
27
+ \`\`\`
28
+ create_site(name: "My Awesome Website")
29
+ \`\`\`
30
+ `;
31
+ }
32
+ // Generate subdomain from name if not provided
33
+ const finalSubdomain = subdomain || name
34
+ .toLowerCase()
35
+ .replace(/[^a-z0-9]+/g, '-')
36
+ .replace(/^-|-$/g, '')
37
+ .slice(0, 30);
38
+ // Create the site
39
+ const response = await (0, api_client_1.apiRequest)('/api/tenants', {
40
+ method: 'POST',
41
+ body: {
42
+ name: name.trim(),
43
+ subdomain: finalSubdomain,
44
+ },
45
+ });
46
+ if ((0, api_client_1.isApiError)(response)) {
47
+ // Check for common errors
48
+ if (response.error.includes('subdomain') && response.error.includes('taken')) {
49
+ return `# Subdomain Already Taken
50
+
51
+ The subdomain "${finalSubdomain}" is already in use.
52
+
53
+ Try creating with a different subdomain:
54
+ \`\`\`
55
+ create_site(name: "${name}", subdomain: "${finalSubdomain}-2")
56
+ \`\`\`
57
+ `;
58
+ }
59
+ if (response.statusCode === 401) {
60
+ // Try to re-authenticate
61
+ const authResult = await (0, device_flow_1.ensureAuthenticated)();
62
+ if (!authResult.authenticated) {
63
+ return authResult.message;
64
+ }
65
+ // Retry
66
+ const retryResponse = await (0, api_client_1.apiRequest)('/api/tenants', {
67
+ method: 'POST',
68
+ body: {
69
+ name: name.trim(),
70
+ subdomain: finalSubdomain,
71
+ },
72
+ });
73
+ if ((0, api_client_1.isApiError)(retryResponse)) {
74
+ return `# Failed to Create Site
75
+
76
+ ${retryResponse.error}
77
+
78
+ Please try again or create manually at app.fastmode.ai
79
+ `;
80
+ }
81
+ return formatSuccess(retryResponse.data);
82
+ }
83
+ return `# Failed to Create Site
84
+
85
+ ${response.error}
86
+
87
+ Please try again or create manually at app.fastmode.ai
88
+ `;
89
+ }
90
+ return formatSuccess(response.data);
91
+ }
92
+ /**
93
+ * Format success message
94
+ */
95
+ function formatSuccess(site) {
96
+ return `# Site Created Successfully! 🎉
97
+
98
+ ## Your New Site
99
+
100
+ - **Name:** ${site.name}
101
+ - **URL:** https://${site.subdomain}.fastmode.ai
102
+ - **Project ID:** \`${site.id}\`
103
+
104
+ ## Next Steps
105
+
106
+ **Option 1: Deploy via MCP**
107
+ \`\`\`
108
+ deploy_package(
109
+ packagePath: "./your-site.zip",
110
+ projectId: "${site.id}"
111
+ )
112
+ \`\`\`
113
+
114
+ **Option 2: Connect GitHub**
115
+ 1. Go to app.fastmode.ai
116
+ 2. Open Settings → Connected Git Repository
117
+ 3. Connect your GitHub account and select a repo
118
+
119
+ **Option 3: Manual Upload**
120
+ 1. Go to app.fastmode.ai
121
+ 2. Upload your website package in the Editor
122
+
123
+ ---
124
+
125
+ Your site is ready at: https://${site.subdomain}.fastmode.ai
126
+ `;
127
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Deploy a website package to Fast Mode
3
+ *
4
+ * @param packagePath - Path to the zip file
5
+ * @param projectId - Optional: Deploy to existing project with this ID
6
+ * @param projectName - Optional: Create new project with this name
7
+ */
8
+ export declare function deployPackage(packagePath: string, projectId?: string, projectName?: string): Promise<string>;
9
+ //# sourceMappingURL=deploy-package.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"deploy-package.d.ts","sourceRoot":"","sources":["../../src/tools/deploy-package.ts"],"names":[],"mappings":"AA+MA;;;;;;GAMG;AACH,wBAAsB,aAAa,CACjC,WAAW,EAAE,MAAM,EACnB,SAAS,CAAC,EAAE,MAAM,EAClB,WAAW,CAAC,EAAE,MAAM,GACnB,OAAO,CAAC,MAAM,CAAC,CA0IjB"}
@@ -0,0 +1,335 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.deployPackage = deployPackage;
37
+ const fs = __importStar(require("fs"));
38
+ const path = __importStar(require("path"));
39
+ const api_client_1 = require("../lib/api-client");
40
+ const device_flow_1 = require("../lib/device-flow");
41
+ const credentials_1 = require("../lib/credentials");
42
+ /**
43
+ * List existing projects for the authenticated user
44
+ */
45
+ async function listExistingProjects() {
46
+ const response = await (0, api_client_1.apiRequest)('/api/tenants');
47
+ if ((0, api_client_1.isApiError)(response)) {
48
+ return [];
49
+ }
50
+ return response.data;
51
+ }
52
+ /**
53
+ * Create a new site/project
54
+ */
55
+ async function createNewSite(name, subdomain) {
56
+ // Generate subdomain from name if not provided
57
+ const finalSubdomain = subdomain || name
58
+ .toLowerCase()
59
+ .replace(/[^a-z0-9]+/g, '-')
60
+ .replace(/^-|-$/g, '')
61
+ .slice(0, 30);
62
+ const response = await (0, api_client_1.apiRequest)('/api/tenants', {
63
+ method: 'POST',
64
+ body: {
65
+ name,
66
+ subdomain: finalSubdomain,
67
+ },
68
+ });
69
+ if ((0, api_client_1.isApiError)(response)) {
70
+ return { error: response.error };
71
+ }
72
+ return response.data;
73
+ }
74
+ /**
75
+ * Upload a package to a specific tenant
76
+ */
77
+ async function uploadPackage(tenantId, zipBuffer) {
78
+ const credentials = await (0, credentials_1.getValidCredentials)();
79
+ if (!credentials) {
80
+ return { error: 'Not authenticated' };
81
+ }
82
+ const apiUrl = (0, api_client_1.getApiUrl)();
83
+ const url = `${apiUrl}/api/upload/package`;
84
+ // Generate a unique boundary for multipart form data
85
+ const boundary = `----FormBoundary${Date.now().toString(16)}`;
86
+ // Build multipart form data manually
87
+ const header = Buffer.from(`--${boundary}\r\n` +
88
+ `Content-Disposition: form-data; name="package"; filename="package.zip"\r\n` +
89
+ `Content-Type: application/zip\r\n\r\n`);
90
+ const footer = Buffer.from(`\r\n--${boundary}--\r\n`);
91
+ const body = Buffer.concat([header, zipBuffer, footer]);
92
+ try {
93
+ const response = await fetch(url, {
94
+ method: 'POST',
95
+ headers: {
96
+ 'Authorization': `Bearer ${credentials.accessToken}`,
97
+ 'X-Tenant-Id': tenantId,
98
+ 'Content-Type': `multipart/form-data; boundary=${boundary}`,
99
+ },
100
+ body: body,
101
+ });
102
+ const data = await response.json();
103
+ if (!response.ok) {
104
+ return { error: data.error || `Upload failed with status ${response.status}` };
105
+ }
106
+ return data.data;
107
+ }
108
+ catch (error) {
109
+ return { error: error instanceof Error ? error.message : 'Upload failed' };
110
+ }
111
+ }
112
+ /**
113
+ * Read a package from path (zip file or directory)
114
+ */
115
+ async function readPackage(packagePath) {
116
+ const absolutePath = path.resolve(packagePath);
117
+ // Check if path exists
118
+ if (!fs.existsSync(absolutePath)) {
119
+ return { error: `Path not found: ${absolutePath}` };
120
+ }
121
+ const stats = fs.statSync(absolutePath);
122
+ if (stats.isFile()) {
123
+ // It's a file - check if it's a zip
124
+ if (!absolutePath.endsWith('.zip')) {
125
+ return { error: 'File must be a .zip archive' };
126
+ }
127
+ return fs.readFileSync(absolutePath);
128
+ }
129
+ // It's a directory - we need to zip it
130
+ // For now, require a zip file
131
+ return { error: 'Please provide a .zip file. Directory upload is not yet supported.' };
132
+ }
133
+ /**
134
+ * Format project list for user selection
135
+ */
136
+ function formatProjectChoice(projects) {
137
+ let output = `# Select a Project
138
+
139
+ Found ${projects.length} existing project${projects.length > 1 ? 's' : ''}:
140
+
141
+ `;
142
+ projects.forEach((project, index) => {
143
+ const url = project.customDomain || `${project.subdomain}.fastmode.ai`;
144
+ output += `${index + 1}. **${project.name}**
145
+ - ID: \`${project.id}\`
146
+ - URL: ${url}
147
+ - Status: ${project.site?.status || 'pending'}
148
+
149
+ `;
150
+ });
151
+ output += `---
152
+
153
+ ## How to Deploy
154
+
155
+ **To an existing project:**
156
+ \`\`\`
157
+ deploy_package(
158
+ packagePath: "./your-site.zip",
159
+ projectId: "${projects[0]?.id || 'project-id-here'}"
160
+ )
161
+ \`\`\`
162
+
163
+ **To a new project:**
164
+ \`\`\`
165
+ deploy_package(
166
+ packagePath: "./your-site.zip",
167
+ projectName: "My New Site"
168
+ )
169
+ \`\`\`
170
+ `;
171
+ return output;
172
+ }
173
+ /**
174
+ * Deploy a website package to Fast Mode
175
+ *
176
+ * @param packagePath - Path to the zip file
177
+ * @param projectId - Optional: Deploy to existing project with this ID
178
+ * @param projectName - Optional: Create new project with this name
179
+ */
180
+ async function deployPackage(packagePath, projectId, projectName) {
181
+ // Check authentication
182
+ if (await (0, api_client_1.needsAuthentication)()) {
183
+ const authResult = await (0, device_flow_1.ensureAuthenticated)();
184
+ if (!authResult.authenticated) {
185
+ return authResult.message;
186
+ }
187
+ }
188
+ // If no projectId specified, help user choose
189
+ if (!projectId && !projectName) {
190
+ const projects = await listExistingProjects();
191
+ if (projects.length > 0) {
192
+ return formatProjectChoice(projects);
193
+ }
194
+ // No projects exist - need a projectName
195
+ return `# No Projects Found
196
+
197
+ You don't have any FastMode projects yet.
198
+
199
+ To create a new project and deploy, provide a project name:
200
+
201
+ \`\`\`
202
+ deploy_package(
203
+ packagePath: "${packagePath}",
204
+ projectName: "My Website"
205
+ )
206
+ \`\`\`
207
+ `;
208
+ }
209
+ // Read the package first
210
+ const packageResult = await readPackage(packagePath);
211
+ if ('error' in packageResult) {
212
+ return `# Package Error
213
+
214
+ ${packageResult.error}
215
+
216
+ Please provide a valid .zip file path.
217
+ `;
218
+ }
219
+ const zipBuffer = packageResult;
220
+ let targetProjectId = projectId;
221
+ let subdomain = '';
222
+ // Create new project if needed
223
+ if (!targetProjectId && projectName) {
224
+ const createResult = await createNewSite(projectName);
225
+ if ('error' in createResult) {
226
+ // Check if it's an auth error
227
+ if (createResult.error.includes('401') || createResult.error.includes('auth')) {
228
+ const authResult = await (0, device_flow_1.ensureAuthenticated)();
229
+ if (!authResult.authenticated) {
230
+ return authResult.message;
231
+ }
232
+ // Retry
233
+ const retryResult = await createNewSite(projectName);
234
+ if ('error' in retryResult) {
235
+ return `# Failed to Create Project
236
+
237
+ ${retryResult.error}
238
+
239
+ Please try again or create the project manually at app.fastmode.ai
240
+ `;
241
+ }
242
+ targetProjectId = retryResult.id;
243
+ subdomain = retryResult.subdomain;
244
+ }
245
+ else {
246
+ return `# Failed to Create Project
247
+
248
+ ${createResult.error}
249
+
250
+ Please try again or create the project manually at app.fastmode.ai
251
+ `;
252
+ }
253
+ }
254
+ else {
255
+ targetProjectId = createResult.id;
256
+ subdomain = createResult.subdomain;
257
+ console.error(`Created new project: ${projectName} (${subdomain}.fastmode.ai)`);
258
+ }
259
+ }
260
+ // Get subdomain for existing project
261
+ if (!subdomain && targetProjectId) {
262
+ const projects = await listExistingProjects();
263
+ const project = projects.find(p => p.id === targetProjectId);
264
+ subdomain = project?.subdomain || '';
265
+ }
266
+ // Upload the package
267
+ console.error(`Deploying to ${subdomain}.fastmode.ai...`);
268
+ const uploadResult = await uploadPackage(targetProjectId, zipBuffer);
269
+ if ('error' in uploadResult) {
270
+ // Check if it's an auth error
271
+ if ((0, api_client_1.needsAuthError)({ success: false, error: uploadResult.error, statusCode: 401 })) {
272
+ const authResult = await (0, device_flow_1.ensureAuthenticated)();
273
+ if (!authResult.authenticated) {
274
+ return authResult.message;
275
+ }
276
+ // Retry upload
277
+ const retryResult = await uploadPackage(targetProjectId, zipBuffer);
278
+ if ('error' in retryResult) {
279
+ return `# Upload Failed
280
+
281
+ ${retryResult.error}
282
+
283
+ Please check:
284
+ 1. The package is a valid .zip file
285
+ 2. It contains manifest.json, pages/, and public/ folders
286
+ 3. Try again or upload manually at app.fastmode.ai
287
+ `;
288
+ }
289
+ return formatSuccess(subdomain, retryResult, !!projectName);
290
+ }
291
+ return `# Upload Failed
292
+
293
+ ${uploadResult.error}
294
+
295
+ Please check:
296
+ 1. The package is a valid .zip file
297
+ 2. It contains manifest.json, pages/, and public/ folders
298
+ 3. Try again or upload manually at app.fastmode.ai
299
+ `;
300
+ }
301
+ return formatSuccess(subdomain, uploadResult, !!projectName);
302
+ }
303
+ /**
304
+ * Format success message
305
+ */
306
+ function formatSuccess(subdomain, result, isNewProject) {
307
+ return `# Deployment Successful! 🚀
308
+
309
+ ${isNewProject ? '**New project created and deployed!**' : '**Package deployed!**'}
310
+
311
+ ## Live Site
312
+
313
+ **URL:** https://${subdomain}.fastmode.ai
314
+
315
+ ## Details
316
+
317
+ - **Files Uploaded:** ${result.filesUploaded}
318
+ - **Pages:** ${result.pages}
319
+ - **Version:** ${result.site.packageVersion}
320
+ - **Status:** ${result.site.status}
321
+
322
+ ## Next Steps
323
+
324
+ 1. **View your site:** https://${subdomain}.fastmode.ai
325
+ 2. **Edit content:** Go to app.fastmode.ai to manage your CMS
326
+ 3. **Connect GitHub:** For automatic deploys on push
327
+
328
+ ---
329
+
330
+ To update this site later:
331
+ \`\`\`
332
+ deploy_package(packagePath: "./updated-site.zip", projectId: "${result.site.id}")
333
+ \`\`\`
334
+ `;
335
+ }
@@ -1,4 +1,4 @@
1
- type Section = 'full' | 'analysis' | 'structure' | 'manifest' | 'templates' | 'tokens' | 'forms' | 'assets' | 'checklist';
1
+ type Section = 'full' | 'analysis' | 'structure' | 'seo' | 'manifest' | 'templates' | 'tokens' | 'forms' | 'assets' | 'checklist';
2
2
  /**
3
3
  * Returns the conversion guide, either full or a specific section
4
4
  */
@@ -1 +1 @@
1
- {"version":3,"file":"get-conversion-guide.d.ts","sourceRoot":"","sources":["../../src/tools/get-conversion-guide.ts"],"names":[],"mappings":"AAAA,KAAK,OAAO,GAAG,MAAM,GAAG,UAAU,GAAG,WAAW,GAAG,UAAU,GAAG,WAAW,GAAG,QAAQ,GAAG,OAAO,GAAG,QAAQ,GAAG,WAAW,CAAC;AA80B1H;;GAEG;AACH,wBAAsB,kBAAkB,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,MAAM,CAAC,CAkD1E"}
1
+ {"version":3,"file":"get-conversion-guide.d.ts","sourceRoot":"","sources":["../../src/tools/get-conversion-guide.ts"],"names":[],"mappings":"AAAA,KAAK,OAAO,GAAG,MAAM,GAAG,UAAU,GAAG,WAAW,GAAG,KAAK,GAAG,UAAU,GAAG,WAAW,GAAG,QAAQ,GAAG,OAAO,GAAG,QAAQ,GAAG,WAAW,CAAC;AA24BlI;;GAEG;AACH,wBAAsB,kBAAkB,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,MAAM,CAAC,CAsD1E"}
@@ -120,6 +120,58 @@ repo/
120
120
  - CMS-powered pages
121
121
  - Use {{tokens}} for dynamic content
122
122
  - Named by collection type`,
123
+ seo: `# SEO Tags (CRITICAL - Do NOT Include)
124
+
125
+ Fast Mode automatically manages all SEO meta tags. Do NOT include these in your HTML templates:
126
+
127
+ ## Tags to EXCLUDE
128
+
129
+ | Tag | Reason |
130
+ |-----|--------|
131
+ | \`<title>...\` | Managed via Settings → SEO & Design |
132
+ | \`<meta name="description">\` | Managed via Settings → SEO & Design |
133
+ | \`<meta name="keywords">\` | Managed via Settings → SEO & Design |
134
+ | \`<meta property="og:*">\` | Open Graph tags auto-generated |
135
+ | \`<meta name="twitter:*">\` | Twitter cards auto-generated |
136
+ | \`<link rel="icon">\` | Favicon managed in settings |
137
+ | \`<link rel="shortcut icon">\` | Favicon managed in settings |
138
+ | \`<link rel="apple-touch-icon">\` | Managed by Fast Mode |
139
+ | \`<meta name="google-site-verification">\` | Managed in settings |
140
+
141
+ ## Correct \`<head>\` Structure
142
+
143
+ \`\`\`html
144
+ <head>
145
+ <meta charset="UTF-8">
146
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
147
+ <!-- SEO managed by Fast Mode -->
148
+ <link rel="stylesheet" href="/public/css/style.css">
149
+ <!-- Font imports, external scripts, etc. are OK -->
150
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600&display=swap" rel="stylesheet">
151
+ </head>
152
+ \`\`\`
153
+
154
+ ## How Fast Mode SEO Works
155
+
156
+ 1. **Global SEO** (Settings → SEO & Design)
157
+ - Site-wide defaults: title, description, OG image, favicon
158
+ - Applied to all pages
159
+
160
+ 2. **Page-Level SEO** (Editor → click page → SEO tab)
161
+ - Override specific pages with custom title/description
162
+ - Page-level settings take priority over global
163
+
164
+ 3. **Automatic Injection**
165
+ - Fast Mode strips any hardcoded SEO tags from templates
166
+ - Injects correct SEO from settings at render time
167
+ - Guarantees single source of truth
168
+
169
+ ## Why This Matters
170
+
171
+ - No duplicate meta tags (bad for SEO ranking)
172
+ - Content managers can update SEO without touching code
173
+ - Consistent SEO across all pages
174
+ - Works retroactively on existing templates`,
123
175
  manifest: `# manifest.json Configuration
124
176
 
125
177
  ## Basic Structure
@@ -783,6 +835,14 @@ Keep external URLs unchanged:
783
835
  - [ ] Assets in public/ folder
784
836
  - [ ] Templates in templates/ (or pages/ if configuring via Settings UI)
785
837
 
838
+ ## ✓ SEO (CRITICAL)
839
+ - [ ] NO \`<title>\` tags in HTML
840
+ - [ ] NO \`<meta name="description">\` tags
841
+ - [ ] NO \`<meta property="og:*">\` tags
842
+ - [ ] NO \`<meta name="twitter:*">\` tags
843
+ - [ ] NO \`<link rel="icon">\` tags
844
+ - [ ] Include \`<!-- SEO managed by Fast Mode -->\` comment in head
845
+
786
846
  ## ✓ manifest.json
787
847
  - [ ] All static pages listed in pages array
788
848
  - [ ] Each page has path, file, title
@@ -851,6 +911,10 @@ ${SECTIONS.structure}
851
911
 
852
912
  ---
853
913
 
914
+ ${SECTIONS.seo}
915
+
916
+ ---
917
+
854
918
  ${SECTIONS.manifest}
855
919
 
856
920
  ---
@@ -1,4 +1,4 @@
1
- type ExampleType = 'manifest_basic' | 'manifest_custom_paths' | 'manifest_minimal_with_ui' | 'blog_index_template' | 'blog_post_template' | 'team_template' | 'downloads_template' | 'authors_template' | 'author_detail_template' | 'custom_collection_template' | 'form_handling' | 'asset_paths' | 'image_handling' | 'data_edit_keys' | 'each_loop' | 'conditional_if' | 'nested_fields' | 'featured_posts' | 'parent_context' | 'equality_comparison';
1
+ type ExampleType = 'manifest_basic' | 'manifest_custom_paths' | 'manifest_minimal_with_ui' | 'blog_index_template' | 'blog_post_template' | 'team_template' | 'downloads_template' | 'authors_template' | 'author_detail_template' | 'custom_collection_template' | 'form_handling' | 'asset_paths' | 'image_handling' | 'relation_fields' | 'data_edit_keys' | 'each_loop' | 'conditional_if' | 'nested_fields' | 'featured_posts' | 'parent_context' | 'equality_comparison';
2
2
  /**
3
3
  * Returns example code for a specific pattern
4
4
  */
@@ -1 +1 @@
1
- {"version":3,"file":"get-example.d.ts","sourceRoot":"","sources":["../../src/tools/get-example.ts"],"names":[],"mappings":"AAAA,KAAK,WAAW,GACZ,gBAAgB,GAChB,uBAAuB,GACvB,0BAA0B,GAC1B,qBAAqB,GACrB,oBAAoB,GACpB,eAAe,GACf,oBAAoB,GACpB,kBAAkB,GAClB,wBAAwB,GACxB,4BAA4B,GAC5B,eAAe,GACf,aAAa,GACb,gBAAgB,GAChB,gBAAgB,GAChB,WAAW,GACX,gBAAgB,GAChB,eAAe,GACf,gBAAgB,GAChB,gBAAgB,GAChB,qBAAqB,CAAC;AA+gC1B;;GAEG;AACH,wBAAsB,UAAU,CAAC,WAAW,EAAE,WAAW,GAAG,OAAO,CAAC,MAAM,CAAC,CAE1E"}
1
+ {"version":3,"file":"get-example.d.ts","sourceRoot":"","sources":["../../src/tools/get-example.ts"],"names":[],"mappings":"AAAA,KAAK,WAAW,GACZ,gBAAgB,GAChB,uBAAuB,GACvB,0BAA0B,GAC1B,qBAAqB,GACrB,oBAAoB,GACpB,eAAe,GACf,oBAAoB,GACpB,kBAAkB,GAClB,wBAAwB,GACxB,4BAA4B,GAC5B,eAAe,GACf,aAAa,GACb,gBAAgB,GAChB,iBAAiB,GACjB,gBAAgB,GAChB,WAAW,GACX,gBAAgB,GAChB,eAAe,GACf,gBAAgB,GAChB,gBAAgB,GAChB,qBAAqB,CAAC;AA0pC1B;;GAEG;AACH,wBAAsB,UAAU,CAAC,WAAW,EAAE,WAAW,GAAG,OAAO,CAAC,MAAM,CAAC,CAE1E"}