multisite-cms-mcp 1.0.13 → 1.0.16

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.
@@ -0,0 +1,196 @@
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.getCredentialsPath = getCredentialsPath;
37
+ exports.loadCredentials = loadCredentials;
38
+ exports.saveCredentials = saveCredentials;
39
+ exports.deleteCredentials = deleteCredentials;
40
+ exports.hasCredentials = hasCredentials;
41
+ exports.isTokenExpired = isTokenExpired;
42
+ exports.getApiUrl = getApiUrl;
43
+ exports.refreshAccessToken = refreshAccessToken;
44
+ exports.getValidCredentials = getValidCredentials;
45
+ exports.getAuthToken = getAuthToken;
46
+ const fs = __importStar(require("fs"));
47
+ const path = __importStar(require("path"));
48
+ const os = __importStar(require("os"));
49
+ /**
50
+ * Get the path to the credentials file
51
+ */
52
+ function getCredentialsPath() {
53
+ const configDir = path.join(os.homedir(), '.fastmode');
54
+ return path.join(configDir, 'credentials.json');
55
+ }
56
+ /**
57
+ * Ensure the config directory exists
58
+ */
59
+ function ensureConfigDir() {
60
+ const configDir = path.dirname(getCredentialsPath());
61
+ if (!fs.existsSync(configDir)) {
62
+ fs.mkdirSync(configDir, { recursive: true, mode: 0o700 });
63
+ }
64
+ }
65
+ /**
66
+ * Load stored credentials from disk
67
+ */
68
+ function loadCredentials() {
69
+ const credentialsPath = getCredentialsPath();
70
+ try {
71
+ if (!fs.existsSync(credentialsPath)) {
72
+ return null;
73
+ }
74
+ const content = fs.readFileSync(credentialsPath, 'utf-8');
75
+ const credentials = JSON.parse(content);
76
+ // Validate required fields
77
+ if (!credentials.accessToken || !credentials.refreshToken || !credentials.expiresAt || !credentials.email) {
78
+ return null;
79
+ }
80
+ return credentials;
81
+ }
82
+ catch {
83
+ return null;
84
+ }
85
+ }
86
+ /**
87
+ * Save credentials to disk
88
+ */
89
+ function saveCredentials(credentials) {
90
+ ensureConfigDir();
91
+ const credentialsPath = getCredentialsPath();
92
+ // Write with restricted permissions (owner read/write only)
93
+ fs.writeFileSync(credentialsPath, JSON.stringify(credentials, null, 2), { mode: 0o600 });
94
+ }
95
+ /**
96
+ * Delete stored credentials
97
+ */
98
+ function deleteCredentials() {
99
+ const credentialsPath = getCredentialsPath();
100
+ try {
101
+ if (fs.existsSync(credentialsPath)) {
102
+ fs.unlinkSync(credentialsPath);
103
+ }
104
+ }
105
+ catch {
106
+ // Ignore errors
107
+ }
108
+ }
109
+ /**
110
+ * Check if credentials exist
111
+ */
112
+ function hasCredentials() {
113
+ return loadCredentials() !== null;
114
+ }
115
+ /**
116
+ * Check if the access token is expired or about to expire
117
+ */
118
+ function isTokenExpired(credentials, bufferMinutes = 5) {
119
+ const expiresAt = new Date(credentials.expiresAt);
120
+ const bufferMs = bufferMinutes * 60 * 1000;
121
+ return Date.now() >= expiresAt.getTime() - bufferMs;
122
+ }
123
+ /**
124
+ * Get the API URL from environment or default
125
+ */
126
+ function getApiUrl() {
127
+ return process.env.FASTMODE_API_URL || 'https://api.fastmode.ai';
128
+ }
129
+ /**
130
+ * Refresh the access token using the refresh token
131
+ */
132
+ async function refreshAccessToken(credentials) {
133
+ const apiUrl = getApiUrl();
134
+ try {
135
+ const response = await fetch(`${apiUrl}/api/auth/device/refresh`, {
136
+ method: 'POST',
137
+ headers: {
138
+ 'Content-Type': 'application/json',
139
+ },
140
+ body: JSON.stringify({
141
+ refresh_token: credentials.refreshToken,
142
+ grant_type: 'refresh_token',
143
+ }),
144
+ });
145
+ if (!response.ok) {
146
+ // Refresh token is invalid or expired - need to re-authenticate
147
+ return null;
148
+ }
149
+ const data = await response.json();
150
+ if (!data.success || !data.data) {
151
+ return null;
152
+ }
153
+ const newCredentials = {
154
+ accessToken: data.data.access_token,
155
+ refreshToken: data.data.refresh_token,
156
+ expiresAt: new Date(Date.now() + data.data.expires_in * 1000).toISOString(),
157
+ email: data.data.email,
158
+ name: data.data.name,
159
+ };
160
+ // Save the new credentials
161
+ saveCredentials(newCredentials);
162
+ return newCredentials;
163
+ }
164
+ catch {
165
+ return null;
166
+ }
167
+ }
168
+ /**
169
+ * Get valid credentials, refreshing if needed
170
+ * Returns null if no credentials or refresh fails
171
+ */
172
+ async function getValidCredentials() {
173
+ const credentials = loadCredentials();
174
+ if (!credentials) {
175
+ return null;
176
+ }
177
+ // Check if token is expired or about to expire
178
+ if (isTokenExpired(credentials)) {
179
+ // Try to refresh
180
+ const newCredentials = await refreshAccessToken(credentials);
181
+ if (!newCredentials) {
182
+ // Refresh failed - credentials are invalid
183
+ deleteCredentials();
184
+ return null;
185
+ }
186
+ return newCredentials;
187
+ }
188
+ return credentials;
189
+ }
190
+ /**
191
+ * Get the current auth token (access token) if available and valid
192
+ */
193
+ async function getAuthToken() {
194
+ const credentials = await getValidCredentials();
195
+ return credentials?.accessToken || null;
196
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Start the device authorization flow
3
+ * Returns a message describing the flow status
4
+ */
5
+ export declare function startDeviceFlow(): Promise<string>;
6
+ /**
7
+ * Check if device flow authentication is needed and perform it if so
8
+ * Returns the authentication result message
9
+ */
10
+ export declare function ensureAuthenticated(): Promise<{
11
+ authenticated: boolean;
12
+ message: string;
13
+ }>;
14
+ //# sourceMappingURL=device-flow.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"device-flow.d.ts","sourceRoot":"","sources":["../../src/lib/device-flow.ts"],"names":[],"mappings":"AA0DA;;;GAGG;AACH,wBAAsB,eAAe,IAAI,OAAO,CAAC,MAAM,CAAC,CA+FvD;AA0ED;;;GAGG;AACH,wBAAsB,mBAAmB,IAAI,OAAO,CAAC;IAAE,aAAa,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC,CAuBhG"}
@@ -0,0 +1,235 @@
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.startDeviceFlow = startDeviceFlow;
37
+ exports.ensureAuthenticated = ensureAuthenticated;
38
+ const child_process_1 = require("child_process");
39
+ const credentials_1 = require("./credentials");
40
+ /**
41
+ * Open a URL in the default browser
42
+ */
43
+ function openBrowser(url) {
44
+ return new Promise((resolve) => {
45
+ // Determine the command based on platform
46
+ let command;
47
+ switch (process.platform) {
48
+ case 'darwin':
49
+ command = `open "${url}"`;
50
+ break;
51
+ case 'win32':
52
+ command = `start "" "${url}"`;
53
+ break;
54
+ default:
55
+ // Linux and others
56
+ command = `xdg-open "${url}"`;
57
+ }
58
+ (0, child_process_1.exec)(command, (error) => {
59
+ if (error) {
60
+ // Don't fail if browser can't open - user can manually navigate
61
+ resolve();
62
+ }
63
+ else {
64
+ resolve();
65
+ }
66
+ });
67
+ });
68
+ }
69
+ /**
70
+ * Start the device authorization flow
71
+ * Returns a message describing the flow status
72
+ */
73
+ async function startDeviceFlow() {
74
+ const apiUrl = (0, credentials_1.getApiUrl)();
75
+ // Step 1: Request device authorization
76
+ let authResponse;
77
+ try {
78
+ const response = await fetch(`${apiUrl}/api/auth/device/authorize`, {
79
+ method: 'POST',
80
+ headers: {
81
+ 'Content-Type': 'application/json',
82
+ },
83
+ body: JSON.stringify({
84
+ clientName: 'FastMode MCP',
85
+ }),
86
+ });
87
+ if (!response.ok) {
88
+ const error = await response.json().catch(() => ({ error: 'Unknown error' }));
89
+ return `# Authentication Error
90
+
91
+ Failed to start device authorization: ${error.error || response.statusText}
92
+
93
+ Please check:
94
+ 1. Your network connection
95
+ 2. The API URL is correct (${apiUrl})
96
+ `;
97
+ }
98
+ const data = await response.json();
99
+ authResponse = data.data;
100
+ }
101
+ catch (error) {
102
+ return `# Network Error
103
+
104
+ Unable to connect to FastMode API.
105
+
106
+ **API URL:** ${apiUrl}
107
+ **Error:** ${error instanceof Error ? error.message : 'Unknown error'}
108
+
109
+ Please check your network connection and try again.
110
+ `;
111
+ }
112
+ // Step 2: Open browser for user authorization
113
+ try {
114
+ await openBrowser(authResponse.verification_uri_complete);
115
+ }
116
+ catch {
117
+ // Browser failed to open - user will need to navigate manually
118
+ }
119
+ // Step 3: Poll for authorization
120
+ const pollInterval = (authResponse.interval || 5) * 1000; // seconds to ms
121
+ const expiresAt = Date.now() + authResponse.expires_in * 1000;
122
+ // Log instructions to stderr for user visibility
123
+ console.error(`
124
+ # Device Authorization
125
+
126
+ A browser window should open automatically.
127
+
128
+ If it doesn't, please visit:
129
+ ${authResponse.verification_uri}
130
+
131
+ And enter this code: ${authResponse.user_code}
132
+
133
+ Waiting for authorization...
134
+ `);
135
+ // Start polling
136
+ const pollResult = await pollForToken(apiUrl, authResponse.device_code, pollInterval, expiresAt);
137
+ if (pollResult.success && pollResult.credentials) {
138
+ // Save credentials
139
+ (0, credentials_1.saveCredentials)(pollResult.credentials);
140
+ return `# Authentication Successful
141
+
142
+ Logged in as: **${pollResult.credentials.email}**${pollResult.credentials.name ? ` (${pollResult.credentials.name})` : ''}
143
+
144
+ Credentials saved to ~/.fastmode/credentials.json
145
+
146
+ You can now use FastMode MCP tools.
147
+ `;
148
+ }
149
+ else {
150
+ return `# Authentication Failed
151
+
152
+ ${pollResult.error || 'Authorization timed out or was denied.'}
153
+
154
+ Please try again with \`list_projects\` or \`get_tenant_schema\`.
155
+ `;
156
+ }
157
+ }
158
+ /**
159
+ * Poll the token endpoint until authorization is complete or timeout
160
+ */
161
+ async function pollForToken(apiUrl, deviceCode, interval, expiresAt) {
162
+ while (Date.now() < expiresAt) {
163
+ // Wait for the polling interval
164
+ await new Promise(resolve => setTimeout(resolve, interval));
165
+ try {
166
+ const response = await fetch(`${apiUrl}/api/auth/device/token`, {
167
+ method: 'POST',
168
+ headers: {
169
+ 'Content-Type': 'application/json',
170
+ },
171
+ body: JSON.stringify({
172
+ device_code: deviceCode,
173
+ grant_type: 'urn:ietf:params:oauth:grant-type:device_code',
174
+ }),
175
+ });
176
+ const data = await response.json();
177
+ if (response.ok && data.success && data.data) {
178
+ // Authorization successful!
179
+ const tokenData = data.data;
180
+ const credentials = {
181
+ accessToken: tokenData.access_token,
182
+ refreshToken: tokenData.refresh_token,
183
+ expiresAt: new Date(Date.now() + tokenData.expires_in * 1000).toISOString(),
184
+ email: tokenData.email,
185
+ name: tokenData.name,
186
+ };
187
+ return { success: true, credentials };
188
+ }
189
+ // Check for specific error codes
190
+ if (data.error === 'authorization_pending') {
191
+ // User hasn't authorized yet - keep polling
192
+ continue;
193
+ }
194
+ if (data.error === 'slow_down') {
195
+ // Server is asking us to slow down
196
+ interval = Math.min(interval * 2, 30000); // Max 30 seconds
197
+ continue;
198
+ }
199
+ if (data.error === 'expired_token') {
200
+ return { success: false, error: 'The authorization request expired. Please try again.' };
201
+ }
202
+ if (data.error === 'access_denied') {
203
+ return { success: false, error: 'Authorization was denied. Please try again.' };
204
+ }
205
+ // Unknown error - keep polling
206
+ }
207
+ catch {
208
+ // Network error - keep trying
209
+ }
210
+ }
211
+ return { success: false, error: 'Authorization timed out. Please try again.' };
212
+ }
213
+ /**
214
+ * Check if device flow authentication is needed and perform it if so
215
+ * Returns the authentication result message
216
+ */
217
+ async function ensureAuthenticated() {
218
+ // Import here to avoid circular dependency
219
+ const { getValidCredentials } = await Promise.resolve().then(() => __importStar(require('./credentials')));
220
+ const credentials = await getValidCredentials();
221
+ if (credentials) {
222
+ return {
223
+ authenticated: true,
224
+ message: `Authenticated as ${credentials.email}`,
225
+ };
226
+ }
227
+ // Need to authenticate
228
+ const result = await startDeviceFlow();
229
+ // Check if authentication was successful
230
+ const newCredentials = await getValidCredentials();
231
+ return {
232
+ authenticated: !!newCredentials,
233
+ message: result,
234
+ };
235
+ }
@@ -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;AAopB1H;;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,UAAU,GAAG,WAAW,GAAG,QAAQ,GAAG,OAAO,GAAG,QAAQ,GAAG,WAAW,CAAC;AAysB1H;;GAEG;AACH,wBAAsB,kBAAkB,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,MAAM,CAAC,CAkD1E"}
@@ -422,6 +422,20 @@ Use for rich text content:
422
422
  {{/unless}}
423
423
  \`\`\`
424
424
 
425
+ ## Equality Comparisons
426
+ \`\`\`html
427
+ <!-- Show when equal -->
428
+ {{#if (eq fieldA fieldB)}}...{{/if}}
429
+
430
+ <!-- Show when NOT equal (common for Related Posts) -->
431
+ {{#unless (eq slug ../slug)}}
432
+ <a href="{{url}}">{{name}}</a>
433
+ {{/unless}}
434
+
435
+ <!-- Compare to literal string -->
436
+ {{#eq status "published"}}...{{/eq}}
437
+ \`\`\`
438
+
425
439
  ## Loop Variables
426
440
  Inside {{#each}}:
427
441
  - \`{{@first}}\` - true for first item
@@ -431,12 +445,19 @@ Inside {{#each}}:
431
445
  ## Parent Context (\`../\`)
432
446
  Inside loops, access the parent scope:
433
447
  \`\`\`html
448
+ <!-- Filter posts by current author -->
434
449
  {{#each blogs}}
435
450
  {{#if (eq author.name ../name)}}
436
- <!-- Only show posts by current author -->
437
451
  <h3>{{name}}</h3>
438
452
  {{/if}}
439
453
  {{/each}}
454
+
455
+ <!-- Related Posts (exclude current) -->
456
+ {{#each blogs limit=3}}
457
+ {{#unless (eq slug ../slug)}}
458
+ <a href="{{url}}">{{name}}</a>
459
+ {{/unless}}
460
+ {{/each}}
440
461
  \`\`\`
441
462
 
442
463
  - \`../name\` - Parent item's name field
@@ -481,7 +502,39 @@ Compare two values:
481
502
 
482
503
  **Downloads:**
483
504
  - {{title}}, {{slug}}, {{description}}
484
- - {{fileUrl}}, {{category}}, {{order}}`,
505
+ - {{fileUrl}}, {{category}}, {{order}}
506
+
507
+ ## Custom Collections
508
+
509
+ Tenants can create custom collections (e.g., "services", "testimonials", "products").
510
+ Token syntax is the same as built-in collections:
511
+
512
+ \`\`\`html
513
+ {{#each services}}
514
+ <h2>{{title}}</h2>
515
+ {{{description}}}
516
+ <img src="{{image}}">
517
+ {{/each}}
518
+ \`\`\`
519
+
520
+ **Built-in tokens for custom collections:**
521
+ - \`{{slug}}\` - Item URL slug
522
+ - \`{{url}}\` - Generated full URL
523
+ - \`{{publishedAt}}\` - Publish date (if enabled)
524
+
525
+ ## Custom Fields on Built-in Collections
526
+
527
+ Tenants can add custom fields to Blog Posts, Authors, Team, and Downloads.
528
+ Use the same token syntax:
529
+
530
+ \`\`\`html
531
+ <!-- If "category" field was added to blogs -->
532
+ {{#each blogs}}
533
+ <span class="category">{{category}}</span>
534
+ {{/each}}
535
+ \`\`\`
536
+
537
+ **Use \`get_tenant_schema\` tool to see exact custom fields for a specific tenant.**`,
485
538
  forms: `# Form Handling
486
539
 
487
540
  Forms are automatically captured by the CMS.
@@ -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,WAAW,GACX,gBAAgB,GAChB,eAAe,GACf,gBAAgB,GAChB,gBAAgB,GAChB,qBAAqB,CAAC;AAu0B1B;;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,WAAW,GACX,gBAAgB,GAChB,eAAe,GACf,gBAAgB,GAChB,gBAAgB,GAChB,qBAAqB,CAAC;AA61B1B;;GAEG;AACH,wBAAsB,UAAU,CAAC,WAAW,EAAE,WAAW,GAAG,OAAO,CAAC,MAAM,CAAC,CAE1E"}
@@ -775,33 +775,47 @@ Inside loops, access the **parent scope** (the page's current item) using \`../\
775
775
 
776
776
  Compare two values using \`(eq field1 field2)\` helper:
777
777
 
778
- **Compare Two Fields:**
778
+ ## Show When Equal ({{#if (eq ...)}})
779
779
  \`\`\`html
780
780
  {{#if (eq author.slug ../slug)}}
781
781
  <span class="current-author-badge">✓ Your Post</span>
782
782
  {{/if}}
783
+ \`\`\`
783
784
 
784
- {{#if (eq category selectedCategory)}}
785
- <div class="active">{{name}}</div>
786
- {{/if}}
785
+ ## Show When NOT Equal ({{#unless (eq ...)}}) - IMPORTANT!
786
+ The most common pattern for "Related Posts" or "Other Items" sections:
787
+
788
+ \`\`\`html
789
+ <!-- On a blog post page, show other posts EXCEPT the current one -->
790
+ <h3>Related Posts</h3>
791
+ {{#each blogs limit=3}}
792
+ {{#unless (eq slug ../slug)}}
793
+ <article>
794
+ <a href="{{url}}">{{name}}</a>
795
+ <p>{{postSummary}}</p>
796
+ </article>
797
+ {{/unless}}
798
+ {{/each}}
787
799
  \`\`\`
788
800
 
789
- **Compare Field to Literal String (use {{#eq}}):**
801
+ **How it works:**
802
+ - \`../slug\` accesses the current page's slug (parent context)
803
+ - \`slug\` is the loop item's slug
804
+ - \`{{#unless (eq slug ../slug)}}\` shows content when they're NOT equal
805
+ - This excludes the current post from the "related" list
806
+
807
+ ## Compare Field to Literal String ({{#eq}})
790
808
  \`\`\`html
791
809
  {{#eq status "published"}}
792
810
  <span class="badge badge-success">Published</span>
793
811
  {{/eq}}
794
812
 
795
- {{#eq type "featured"}}
796
- <div class="featured-highlight">⭐ {{name}}</div>
797
- {{/eq}}
798
-
799
813
  {{#eq category "news"}}
800
814
  <span class="news-icon">📰</span>
801
815
  {{/eq}}
802
816
  \`\`\`
803
817
 
804
- **Inside Loops with Parent Context:**
818
+ ## Inside Loops with Parent Context
805
819
  \`\`\`html
806
820
  {{#each blogs}}
807
821
  <article class="{{#if (eq author.name ../name)}}highlight{{/if}}">
@@ -813,11 +827,19 @@ Compare two values using \`(eq field1 field2)\` helper:
813
827
  {{/each}}
814
828
  \`\`\`
815
829
 
830
+ ## Summary Table
831
+
832
+ | Syntax | Shows content when... |
833
+ |--------|----------------------|
834
+ | \`{{#if (eq a b)}}\` | a equals b |
835
+ | \`{{#unless (eq a b)}}\` | a does NOT equal b |
836
+ | \`{{#eq field "value"}}\` | field equals "value" |
837
+
816
838
  **Common Use Cases:**
817
- - Filter items by current category/author/tag
818
- - Highlight active menu items
819
- - Show badges for specific statuses
820
- - Conditional styling based on relationships`,
839
+ - Related posts (exclude current) - use \`{{#unless (eq slug ../slug)}}\`
840
+ - Filter by current author/category - use \`{{#if (eq author.slug ../slug)}}\`
841
+ - Highlight active items - use \`{{#if (eq ...)}}\`
842
+ - Show badges for specific statuses - use \`{{#eq status "value"}}\``,
821
843
  };
822
844
  /**
823
845
  * Returns example code for a specific pattern
@@ -1 +1 @@
1
- {"version":3,"file":"get-schema.d.ts","sourceRoot":"","sources":["../../src/tools/get-schema.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,wBAAsB,SAAS,IAAI,OAAO,CAAC,MAAM,CAAC,CAyQjD"}
1
+ {"version":3,"file":"get-schema.d.ts","sourceRoot":"","sources":["../../src/tools/get-schema.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,wBAAsB,SAAS,IAAI,OAAO,CAAC,MAAM,CAAC,CAqZjD"}