Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/lib/components/blog/copy-as-markdown.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@
async function copy() {
copying = true;
if (timeout) clearTimeout(timeout);
const markdown = await getPageMarkdown(page.route.id);
const routeId = page.url.pathname;
const markdown = await getPageMarkdown(routeId);
copyToClipboard(markdown ?? '');
timeout = setTimeout(() => (copied = false), 2000);
copied = true;
Expand Down
61 changes: 61 additions & 0 deletions src/lib/server/markdown-generators.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { generateModelMarkdown } from './model-markdown';
import { generateServiceMarkdown } from './service-markdown';

/**
* A markdown generator function that takes matched parameters and returns markdown content
*/
type MarkdownGenerator = (match: RegExpMatchArray) => Promise<string | null>;

/**
* Registry of route patterns to markdown generators
*/
const markdownGenerators = new Map<RegExp, MarkdownGenerator>();

/**
* Register generator for model reference routes
* Pattern: /docs/references/{version}/models/{model}
* Example: /docs/references/cloud/models/session
*/
markdownGenerators.set(/^\/docs\/references\/([^/]+)\/models\/([^/]+)$/, async (match) => {
const [, versionParam, modelName] = match;
return generateModelMarkdown(versionParam, modelName);
});

/**
* Register generator for service reference routes
* Pattern: /docs/references/{version}/{platform}/{service}
* Example: /docs/references/cloud/client-web/account
*/
markdownGenerators.set(/^\/docs\/references\/([^/]+)\/([^/]+)\/([^/]+)$/, async (match) => {
const [, versionParam, platform, serviceName] = match;
return generateServiceMarkdown(versionParam, platform, serviceName);
});

/**
* Finds and executes the appropriate markdown generator for a given route ID
* @param routeId - The route identifier (pathname)
* @returns Generated markdown content or null if no generator found
*/
export async function generateDynamicMarkdown(routeId: string): Promise<string | null> {
for (const [pattern, generator] of markdownGenerators) {
const match = routeId.match(pattern);
if (match) {
return await generator(match);
}
}
return null;
}

/**
* Checks if a route has a registered markdown generator
* @param routeId - The route identifier (pathname)
* @returns True if a generator exists for this route
*/
export function hasDynamicMarkdownGenerator(routeId: string): boolean {
for (const pattern of markdownGenerators.keys()) {
if (pattern.test(routeId)) {
return true;
}
}
return false;
}
16 changes: 16 additions & 0 deletions src/lib/server/markdown.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,25 @@
import { readFile } from 'fs/promises';
import { join, normalize, resolve } from 'path';
import { generateDynamicMarkdown, hasDynamicMarkdownGenerator } from './markdown-generators';

/**
* Gets markdown content for a route.
* - For dynamic routes with registered generators, generates markdown from data sources
* - For static routes, reads from +page.markdoc files
*
* Dynamic routes include:
* - Model reference pages: /docs/references/{version}/models/{model}
* - Service reference pages: /docs/references/{version}/{platform}/{service}
*/
export const getMarkdownContent = async (routeId: string | null) => {
if (!routeId) return null;
Comment on lines 14 to 15
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot Jan 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

rg -n "getMarkdownContent" -C3

Repository: appwrite/website

Length of output: 1862


🏁 Script executed:

# Search for page.route.id usage
rg "page\.route\.id" -C2

# Also look at the markdown.ts implementation
cat -n src/lib/server/markdown.ts | head -50

Repository: appwrite/website

Length of output: 6499


Remove fallback to page.route.id in copy-as-markdown.svelte.

The getPageMarkdown call uses page.url.pathname || page.route.id, but page.route.id contains the route pattern (e.g., /docs/products/[slug]), not a concrete pathname. This would cause the function to fail when trying to match dynamic markdown generators or load the corresponding file. Always pass page.url.pathname directly since it is always available.

🤖 Prompt for AI Agents
In `@src/lib/server/markdown.ts` around lines 14 - 15, The call site in
copy-as-markdown.svelte should stop falling back to page.route.id because that
contains route patterns (e.g., /docs/products/[slug]) and breaks matching in
getPageMarkdown/getMarkdownContent; update the call to pass page.url.pathname
only (remove the "|| page.route.id" fallback) so getPageMarkdown and
getMarkdownContent receive a concrete pathname string; check that
getPageMarkdown/getMarkdownContent signatures accept a non-null string and
adjust types if necessary (refer to getPageMarkdown, getMarkdownContent,
page.url.pathname, and page.route.id).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!


// Try dynamic markdown generators first (for API reference pages)
if (hasDynamicMarkdownGenerator(routeId)) {
return await generateDynamicMarkdown(routeId);
}

// Fall back to static markdoc files
try {
const routesRoot = join(process.cwd(), 'src', 'routes');
const cleaned = routeId.replace(/^[\\/]+/, '');
Expand Down
140 changes: 140 additions & 0 deletions src/lib/server/model-markdown.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import {
getApi,
getSchema,
getExample,
ModelType,
type AppwriteSchemaObject,
type Property
} from '../../routes/docs/references/[version]/[platform]/[service]/specs';
import type { OpenAPIV3 } from 'openapi-types';

/**
* Generates markdown content for a model reference page from OpenAPI schema data
*/
export async function generateModelMarkdown(
versionParam: string,
modelName: string
): Promise<string | null> {
try {
const version = versionParam === 'cloud' ? '1.8.x' : versionParam;
const api = await getApi(version, 'console-web');
const schema = getSchema(modelName, api);

const props = Object.entries(schema.properties ?? {});
const title = (schema.description as string) || modelName;

let markdown = `# ${title}\n\n`;
if (schema.description) {
markdown += `${schema.description}\n\n`;
}

markdown += `## Properties\n\n`;
markdown += `| Name | Type | Description |\n`;
markdown += `|------|------|-------------|\n`;

for (const [name, data] of props) {
const property = data as AppwriteSchemaObject & Property;
const { type, description } = formatProperty(property, api, versionParam);

// Escape pipe characters and newlines for markdown table
const escapedDescription = description.replace(/\|/g, '\\|').replace(/\n/g, ' ').trim();

markdown += `| ${name} | ${type} | ${escapedDescription} |\n`;
}

markdown += `\n## Example\n\n`;

// Generate example for each model type
for (const type of Object.values(ModelType)) {
const example = getExample(schema, api, type);
markdown += `### ${type}\n\n`;
markdown += `\`\`\`json\n`;
markdown += `${JSON.stringify(example, null, 2)}\n`;
markdown += `\`\`\`\n\n`;
}

return markdown;
} catch {
return null;
}
}

/**
* Formats a property for markdown display, handling types and related models
*/
function formatProperty(
property: AppwriteSchemaObject & Property,
api: OpenAPIV3.Document,
versionParam: string
): { type: string; description: string } {
let type = property.type as string;
let description = property.description || '';
let relatedModels: string[] = [];

// Handle array types with references
if (property.type === 'array' && property.items) {
if ('$ref' in property.items) {
const refModel = (property.items.$ref as string).split('/').pop();
if (refModel) {
const refSchema = getSchema(refModel, api);
relatedModels.push(
`[${refSchema.description} model](/docs/references/${versionParam}/models/${refModel})`
);
type = `array<${refModel}>`;
}
} else if ('anyOf' in property.items) {
const refs = (property.items as OpenAPIV3.SchemaObject).anyOf?.map((item) =>
((item as OpenAPIV3.ReferenceObject).$ref as string).split('/').pop()
);
if (refs && refs.length > 0) {
relatedModels = refs
.map((item) => {
if (!item) return '';
const refSchema = getSchema(item, api);
return `[${refSchema.description} model](/docs/references/${versionParam}/models/${item})`;
})
.filter(Boolean);
type = 'array';
}
}
}

// Handle object types with references
if (property.type === 'object' && property.items) {
const itemsObj = property.items as OpenAPIV3.SchemaObject | OpenAPIV3.ReferenceObject;
if ('$ref' in itemsObj) {
const refModel = (itemsObj.$ref as string).split('/').pop();
if (refModel) {
const refSchema = getSchema(refModel, api);
relatedModels.push(
`[${refSchema.description} model](/docs/references/${versionParam}/models/${refModel})`
);
type = refModel;
}
} else {
const schemaObj = itemsObj as OpenAPIV3.SchemaObject;
if ('oneOf' in schemaObj && schemaObj.oneOf) {
const refs = (schemaObj.oneOf as OpenAPIV3.ReferenceObject[]).map(
(item: OpenAPIV3.ReferenceObject) => (item.$ref as string).split('/').pop()
);
if (refs && refs.length > 0) {
relatedModels = refs
.map((item: string | undefined) => {
if (!item) return '';
const refSchema = getSchema(item, api);
return `[${refSchema.description} model](/docs/references/${versionParam}/models/${item})`;
})
.filter(Boolean);
}
}
}
}

// Combine description with related models
if (relatedModels.length > 0) {
const relatedText = `Can be one of: ${relatedModels.join(', ')}`;
description = description ? `${description} ${relatedText}` : relatedText;
}

return { type, description };
}
156 changes: 156 additions & 0 deletions src/lib/server/service-markdown.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
import { getService } from '../../routes/docs/references/[version]/[platform]/[service]/specs';
import type { SDKMethod } from '../../routes/docs/references/[version]/[platform]/[service]/specs';

/**
* Generates markdown content for a service reference page from OpenAPI schema data
*/
export async function generateServiceMarkdown(
versionParam: string,
platform: string,
serviceName: string
): Promise<string | null> {
try {
const version = versionParam === 'cloud' ? '1.8.x' : versionParam;
const serviceData = await getService(version, platform, serviceName);

const { service, methods } = serviceData;

let markdown = `# ${service.name}\n\n`;
if (service.description) {
// Remove markdown links from description for cleaner output
const cleanDescription = service.description.replace(/\[([^\]]+)]\([^)]+\)/g, '$1');
markdown += `${cleanDescription}\n\n`;
}

markdown += `## Base URL\n\n`;
markdown += `\`\`\`\n`;
markdown += `https://<REGION>.cloud.appwrite.io/v1\n`;
markdown += `\`\`\`\n\n`;

if (methods.length === 0) {
markdown += `*No endpoints found for this version and platform.*\n\n`;
return markdown;
}

// Group methods by their group property
const groupedMethods = groupMethodsByGroup(methods);

markdown += `## Endpoints\n\n`;

for (const [group, groupMethods] of Object.entries(groupedMethods)) {
if (group) {
markdown += `### ${group}\n\n`;
}

const sortedMethods = sortMethods(groupMethods);

for (const method of sortedMethods) {
markdown += `#### ${method.title}\n\n`;

if (method.description) {
// Remove markdown links for cleaner output
const cleanDescription = method.description.replace(
/\[([^\]]+)]\([^)]+\)/g,
'$1'
);
markdown += `${cleanDescription}\n\n`;
}

markdown += `**Endpoint:** \`${method.method.toUpperCase()} ${method.url}\`\n\n`;

// Parameters
if (method.parameters.length > 0) {
markdown += `**Parameters:**\n\n`;
markdown += `| Name | Type | Required | Description |\n`;
markdown += `|------|------|----------|-------------|\n`;

for (const param of method.parameters) {
const required = param.required ? 'Yes' : 'No';
const description =
param.description?.replace(/\|/g, '\\|').replace(/\n/g, ' ').trim() ||
'';
markdown += `| ${param.name} | ${param.type} | ${required} | ${description} |\n`;
}
markdown += `\n`;
}

// Responses
if (method.responses.length > 0) {
markdown += `**Responses:**\n\n`;
for (const response of method.responses) {
markdown += `- **${response.code}**: ${response.contentType || 'no content'}\n`;
if (response.models && response.models.length > 0) {
for (const model of response.models) {
markdown += ` - [${model.name}](/docs/references/${versionParam}/models/${model.id})\n`;
}
}
}
markdown += `\n`;
}

// Rate limits
if (method['rate-limit'] > 0) {
markdown += `**Rate limits:** ${method['rate-limit']} requests per ${method['rate-time']} seconds\n\n`;
}

// Code example
if (method.demo) {
markdown += `**Example:**\n\n`;
markdown += `\`\`\`${platform}\n`;
markdown += `${method.demo}\n`;
markdown += `\`\`\`\n\n`;
}

markdown += `---\n\n`;
}
}

return markdown;
} catch (error) {
console.error('Error generating service markdown:', error);
return null;
}
}

/**
* Groups methods by their group property
*/
function groupMethodsByGroup(methods: SDKMethod[]): Record<string, SDKMethod[]> {
return methods.reduce<Record<string, SDKMethod[]>>((acc, method) => {
const groupKey = method.group || '';
if (!acc[groupKey]) {
acc[groupKey] = [];
}
acc[groupKey].push(method);
return acc;
}, {});
}

/**
* Sorts methods by their operation order and title
*/
function sortMethods(methods: SDKMethod[]): SDKMethod[] {
return methods.sort((a, b) => {
const orderA = getOperationOrder(a.title);
const orderB = getOperationOrder(b.title);
if (orderA === orderB) {
return a.title.localeCompare(b.title);
}
return orderA - orderB;
});
}

/**
* Determines the order of operations based on the method title
*/
function getOperationOrder(methodTitle: string): number {
const title = methodTitle.toLowerCase();
if (title.startsWith('create')) return 1;
if (title.startsWith('read') || title.startsWith('get') || title.startsWith('list')) return 2;
if (title.startsWith('update')) return 3;
if (title.startsWith('upsert')) return 4;
if (title.startsWith('delete')) return 5;
if (title.startsWith('increment')) return 6;
if (title.startsWith('decrement')) return 7;
return 8; // Other operations
}