diff --git a/docs/docs/pages/config.mdx b/docs/docs/pages/config.mdx index c2e850a..2e035b9 100644 --- a/docs/docs/pages/config.mdx +++ b/docs/docs/pages/config.mdx @@ -14,7 +14,9 @@ | `pdsUrl` | `string` | No | `"https://bsky.social"` | PDS server URL, generated automatically | | `identity` | `string` | No | - | Which stored identity to use | | `frontmatter` | `object` | No | - | Custom frontmatter field mappings | +| `frontmatter.slugField` | `string` | No | - | Frontmatter field to use for slug (defaults to filepath) | | `ignore` | `string[]` | No | - | Glob patterns for files to ignore | +| `removeIndexFromSlug` | `boolean` | No | `false` | Remove `/index` or `/_index` suffix from slugs | | `bluesky` | `object` | No | - | Bluesky posting configuration | | `bluesky.enabled` | `boolean` | No | `false` | Post to Bluesky when publishing documents | | `bluesky.maxAgeDays` | `number` | No | `30` | Only post documents published within this many days | @@ -80,6 +82,20 @@ Override default field names in `sequoia.json`: } ``` +### Slug Configuration + +By default, slugs are generated from the filepath (e.g., `posts/my-post.md` becomes `posts/my-post`). To use a frontmatter field instead: + +```json +{ + "frontmatter": { + "slugField": "url" + } +} +``` + +If the frontmatter field is not found, it falls back to the filepath. + ### Ignoring Files Some frameworks use special files like `_index.md` (Zola) for section pages that aren't actual blog posts. Use the `ignore` field to skip these files during publishing: diff --git a/packages/cli/src/commands/publish.ts b/packages/cli/src/commands/publish.ts index 6996aa9..5f9b3c8 100644 --- a/packages/cli/src/commands/publish.ts +++ b/packages/cli/src/commands/publish.ts @@ -108,8 +108,7 @@ export const publishCommand = command({ const posts = await scanContentDirectory(contentDir, { frontmatterMapping: config.frontmatter, ignorePatterns: config.ignore, - slugSource: config.slugSource, - slugField: config.slugField, + slugField: config.frontmatter?.slugField, removeIndexFromSlug: config.removeIndexFromSlug, }); s.stop(`Found ${posts.length} posts`); diff --git a/packages/cli/src/commands/sync.ts b/packages/cli/src/commands/sync.ts index d133cf8..9c64376 100644 --- a/packages/cli/src/commands/sync.ts +++ b/packages/cli/src/commands/sync.ts @@ -103,8 +103,7 @@ export const syncCommand = command({ const localPosts = await scanContentDirectory(contentDir, { frontmatterMapping: config.frontmatter, ignorePatterns: config.ignore, - slugSource: config.slugSource, - slugField: config.slugField, + slugField: config.frontmatter?.slugField, removeIndexFromSlug: config.removeIndexFromSlug, }); s.stop(`Found ${localPosts.length} local posts`); diff --git a/packages/cli/src/lib/config.ts b/packages/cli/src/lib/config.ts index 175fb17..030d8d4 100644 --- a/packages/cli/src/lib/config.ts +++ b/packages/cli/src/lib/config.ts @@ -81,8 +81,6 @@ export function generateConfigTemplate(options: { pdsUrl?: string; frontmatter?: FrontmatterMapping; ignore?: string[]; - slugSource?: "filename" | "path" | "frontmatter"; - slugField?: string; removeIndexFromSlug?: boolean; textContentField?: string; bluesky?: BlueskyConfig; @@ -122,14 +120,6 @@ export function generateConfigTemplate(options: { config.ignore = options.ignore; } - if (options.slugSource && options.slugSource !== "filename") { - config.slugSource = options.slugSource; - } - - if (options.slugField && options.slugField !== "slug") { - config.slugField = options.slugField; - } - if (options.removeIndexFromSlug) { config.removeIndexFromSlug = options.removeIndexFromSlug; } diff --git a/packages/cli/src/lib/markdown.ts b/packages/cli/src/lib/markdown.ts index 1aa27d6..73883ea 100644 --- a/packages/cli/src/lib/markdown.ts +++ b/packages/cli/src/lib/markdown.ts @@ -176,7 +176,6 @@ export function getSlugFromFilename(filename: string): string { } export interface SlugOptions { - slugSource?: "filename" | "path" | "frontmatter"; slugField?: string; removeIndexFromSlug?: boolean; } @@ -186,43 +185,32 @@ export function getSlugFromOptions( rawFrontmatter: Record, options: SlugOptions = {}, ): string { - const { - slugSource = "filename", - slugField = "slug", - removeIndexFromSlug = false, - } = options; + const { slugField, removeIndexFromSlug = false } = options; let slug: string; - switch (slugSource) { - case "path": - // Use full relative path without extension + // If slugField is set, try to get the value from frontmatter + if (slugField) { + const frontmatterValue = rawFrontmatter[slugField]; + if (frontmatterValue && typeof frontmatterValue === "string") { + // Remove leading slash if present + slug = frontmatterValue + .replace(/^\//, "") + .toLowerCase() + .replace(/\s+/g, "-"); + } else { + // Fallback to filepath if frontmatter field not found slug = relativePath .replace(/\.mdx?$/, "") .toLowerCase() .replace(/\s+/g, "-"); - break; - - case "frontmatter": { - // Use frontmatter field (slug or url) - const frontmatterValue = - rawFrontmatter[slugField] || rawFrontmatter.slug || rawFrontmatter.url; - if (frontmatterValue && typeof frontmatterValue === "string") { - // Remove leading slash if present - slug = frontmatterValue - .replace(/^\//, "") - .toLowerCase() - .replace(/\s+/g, "-"); - } else { - // Fallback to filename if frontmatter field not found - slug = getSlugFromFilename(path.basename(relativePath)); - } - break; } - - default: - slug = getSlugFromFilename(path.basename(relativePath)); - break; + } else { + // Default: use filepath + slug = relativePath + .replace(/\.mdx?$/, "") + .toLowerCase() + .replace(/\s+/g, "-"); } // Remove /index or /_index suffix if configured @@ -253,7 +241,6 @@ function shouldIgnore(relativePath: string, ignorePatterns: string[]): boolean { export interface ScanOptions { frontmatterMapping?: FrontmatterMapping; ignorePatterns?: string[]; - slugSource?: "filename" | "path" | "frontmatter"; slugField?: string; removeIndexFromSlug?: boolean; } @@ -267,9 +254,9 @@ export async function scanContentDirectory( let options: ScanOptions; if ( frontmatterMappingOrOptions && - ("slugSource" in frontmatterMappingOrOptions || - "frontmatterMapping" in frontmatterMappingOrOptions || - "ignorePatterns" in frontmatterMappingOrOptions) + ("frontmatterMapping" in frontmatterMappingOrOptions || + "ignorePatterns" in frontmatterMappingOrOptions || + "slugField" in frontmatterMappingOrOptions) ) { options = frontmatterMappingOrOptions as ScanOptions; } else { @@ -285,7 +272,6 @@ export async function scanContentDirectory( const { frontmatterMapping, ignorePatterns: ignore = [], - slugSource, slugField, removeIndexFromSlug, } = options; @@ -314,7 +300,6 @@ export async function scanContentDirectory( frontmatterMapping, ); const slug = getSlugFromOptions(relativePath, rawFrontmatter, { - slugSource, slugField, removeIndexFromSlug, }); diff --git a/packages/cli/src/lib/types.ts b/packages/cli/src/lib/types.ts index a49bfe6..220a248 100644 --- a/packages/cli/src/lib/types.ts +++ b/packages/cli/src/lib/types.ts @@ -5,6 +5,7 @@ export interface FrontmatterMapping { coverImage?: string; // Field name for cover image (default: "ogImage") tags?: string; // Field name for tags (default: "tags") draft?: string; // Field name for draft status (default: "draft") + slugField?: string; // Frontmatter field to use for slug (if set, uses frontmatter value; otherwise uses filepath) } // Strong reference for Bluesky post (com.atproto.repo.strongRef) @@ -31,8 +32,6 @@ export interface PublisherConfig { identity?: string; // Which stored identity to use (matches identifier) frontmatter?: FrontmatterMapping; // Custom frontmatter field mappings ignore?: string[]; // Glob patterns for files to ignore (e.g., ["_index.md", "**/drafts/**"]) - slugSource?: "filename" | "path" | "frontmatter"; // How to generate slugs (default: "filename") - slugField?: string; // Frontmatter field to use when slugSource is "frontmatter" (default: "slug") removeIndexFromSlug?: boolean; // Remove "/index" or "/_index" suffix from paths (default: false) textContentField?: string; // Frontmatter field to use for textContent instead of markdown body bluesky?: BlueskyConfig; // Optional Bluesky posting configuration