chore: refactored to use fallback approach if frontmatter.slugField is provided or not
4994ddfe
6 file(s) · +40 −52
| 14 | 14 | | `pdsUrl` | `string` | No | `"https://bsky.social"` | PDS server URL, generated automatically | |
|
| 15 | 15 | | `identity` | `string` | No | - | Which stored identity to use | |
|
| 16 | 16 | | `frontmatter` | `object` | No | - | Custom frontmatter field mappings | |
|
| 17 | + | | `frontmatter.slugField` | `string` | No | - | Frontmatter field to use for slug (defaults to filepath) | |
|
| 17 | 18 | | `ignore` | `string[]` | No | - | Glob patterns for files to ignore | |
|
| 19 | + | | `removeIndexFromSlug` | `boolean` | No | `false` | Remove `/index` or `/_index` suffix from slugs | |
|
| 18 | 20 | | `bluesky` | `object` | No | - | Bluesky posting configuration | |
|
| 19 | 21 | | `bluesky.enabled` | `boolean` | No | `false` | Post to Bluesky when publishing documents | |
|
| 20 | 22 | | `bluesky.maxAgeDays` | `number` | No | `30` | Only post documents published within this many days | |
|
| 79 | 81 | } |
|
| 80 | 82 | } |
|
| 81 | 83 | ``` |
|
| 84 | + | ||
| 85 | + | ### Slug Configuration |
|
| 86 | + | ||
| 87 | + | By default, slugs are generated from the filepath (e.g., `posts/my-post.md` becomes `posts/my-post`). To use a frontmatter field instead: |
|
| 88 | + | ||
| 89 | + | ```json |
|
| 90 | + | { |
|
| 91 | + | "frontmatter": { |
|
| 92 | + | "slugField": "url" |
|
| 93 | + | } |
|
| 94 | + | } |
|
| 95 | + | ``` |
|
| 96 | + | ||
| 97 | + | If the frontmatter field is not found, it falls back to the filepath. |
|
| 82 | 98 | ||
| 83 | 99 | ### Ignoring Files |
|
| 84 | 100 | ||
| 108 | 108 | const posts = await scanContentDirectory(contentDir, { |
|
| 109 | 109 | frontmatterMapping: config.frontmatter, |
|
| 110 | 110 | ignorePatterns: config.ignore, |
|
| 111 | - | slugSource: config.slugSource, |
|
| 112 | - | slugField: config.slugField, |
|
| 111 | + | slugField: config.frontmatter?.slugField, |
|
| 113 | 112 | removeIndexFromSlug: config.removeIndexFromSlug, |
|
| 114 | 113 | }); |
|
| 115 | 114 | s.stop(`Found ${posts.length} posts`); |
| 103 | 103 | const localPosts = await scanContentDirectory(contentDir, { |
|
| 104 | 104 | frontmatterMapping: config.frontmatter, |
|
| 105 | 105 | ignorePatterns: config.ignore, |
|
| 106 | - | slugSource: config.slugSource, |
|
| 107 | - | slugField: config.slugField, |
|
| 106 | + | slugField: config.frontmatter?.slugField, |
|
| 108 | 107 | removeIndexFromSlug: config.removeIndexFromSlug, |
|
| 109 | 108 | }); |
|
| 110 | 109 | s.stop(`Found ${localPosts.length} local posts`); |
| 81 | 81 | pdsUrl?: string; |
|
| 82 | 82 | frontmatter?: FrontmatterMapping; |
|
| 83 | 83 | ignore?: string[]; |
|
| 84 | - | slugSource?: "filename" | "path" | "frontmatter"; |
|
| 85 | - | slugField?: string; |
|
| 86 | 84 | removeIndexFromSlug?: boolean; |
|
| 87 | 85 | textContentField?: string; |
|
| 88 | 86 | bluesky?: BlueskyConfig; |
|
| 120 | 118 | ||
| 121 | 119 | if (options.ignore && options.ignore.length > 0) { |
|
| 122 | 120 | config.ignore = options.ignore; |
|
| 123 | - | } |
|
| 124 | - | ||
| 125 | - | if (options.slugSource && options.slugSource !== "filename") { |
|
| 126 | - | config.slugSource = options.slugSource; |
|
| 127 | - | } |
|
| 128 | - | ||
| 129 | - | if (options.slugField && options.slugField !== "slug") { |
|
| 130 | - | config.slugField = options.slugField; |
|
| 131 | 121 | } |
|
| 132 | 122 | ||
| 133 | 123 | if (options.removeIndexFromSlug) { |
|
| 176 | 176 | } |
|
| 177 | 177 | ||
| 178 | 178 | export interface SlugOptions { |
|
| 179 | - | slugSource?: "filename" | "path" | "frontmatter"; |
|
| 180 | 179 | slugField?: string; |
|
| 181 | 180 | removeIndexFromSlug?: boolean; |
|
| 182 | 181 | } |
|
| 186 | 185 | rawFrontmatter: Record<string, unknown>, |
|
| 187 | 186 | options: SlugOptions = {}, |
|
| 188 | 187 | ): string { |
|
| 189 | - | const { |
|
| 190 | - | slugSource = "filename", |
|
| 191 | - | slugField = "slug", |
|
| 192 | - | removeIndexFromSlug = false, |
|
| 193 | - | } = options; |
|
| 188 | + | const { slugField, removeIndexFromSlug = false } = options; |
|
| 194 | 189 | ||
| 195 | 190 | let slug: string; |
|
| 196 | 191 | ||
| 197 | - | switch (slugSource) { |
|
| 198 | - | case "path": |
|
| 199 | - | // Use full relative path without extension |
|
| 192 | + | // If slugField is set, try to get the value from frontmatter |
|
| 193 | + | if (slugField) { |
|
| 194 | + | const frontmatterValue = rawFrontmatter[slugField]; |
|
| 195 | + | if (frontmatterValue && typeof frontmatterValue === "string") { |
|
| 196 | + | // Remove leading slash if present |
|
| 197 | + | slug = frontmatterValue |
|
| 198 | + | .replace(/^\//, "") |
|
| 199 | + | .toLowerCase() |
|
| 200 | + | .replace(/\s+/g, "-"); |
|
| 201 | + | } else { |
|
| 202 | + | // Fallback to filepath if frontmatter field not found |
|
| 200 | 203 | slug = relativePath |
|
| 201 | 204 | .replace(/\.mdx?$/, "") |
|
| 202 | 205 | .toLowerCase() |
|
| 203 | 206 | .replace(/\s+/g, "-"); |
|
| 204 | - | break; |
|
| 205 | - | ||
| 206 | - | case "frontmatter": { |
|
| 207 | - | // Use frontmatter field (slug or url) |
|
| 208 | - | const frontmatterValue = |
|
| 209 | - | rawFrontmatter[slugField] || rawFrontmatter.slug || rawFrontmatter.url; |
|
| 210 | - | if (frontmatterValue && typeof frontmatterValue === "string") { |
|
| 211 | - | // Remove leading slash if present |
|
| 212 | - | slug = frontmatterValue |
|
| 213 | - | .replace(/^\//, "") |
|
| 214 | - | .toLowerCase() |
|
| 215 | - | .replace(/\s+/g, "-"); |
|
| 216 | - | } else { |
|
| 217 | - | // Fallback to filename if frontmatter field not found |
|
| 218 | - | slug = getSlugFromFilename(path.basename(relativePath)); |
|
| 219 | - | } |
|
| 220 | - | break; |
|
| 221 | 207 | } |
|
| 222 | - | ||
| 223 | - | default: |
|
| 224 | - | slug = getSlugFromFilename(path.basename(relativePath)); |
|
| 225 | - | break; |
|
| 208 | + | } else { |
|
| 209 | + | // Default: use filepath |
|
| 210 | + | slug = relativePath |
|
| 211 | + | .replace(/\.mdx?$/, "") |
|
| 212 | + | .toLowerCase() |
|
| 213 | + | .replace(/\s+/g, "-"); |
|
| 226 | 214 | } |
|
| 227 | 215 | ||
| 228 | 216 | // Remove /index or /_index suffix if configured |
|
| 253 | 241 | export interface ScanOptions { |
|
| 254 | 242 | frontmatterMapping?: FrontmatterMapping; |
|
| 255 | 243 | ignorePatterns?: string[]; |
|
| 256 | - | slugSource?: "filename" | "path" | "frontmatter"; |
|
| 257 | 244 | slugField?: string; |
|
| 258 | 245 | removeIndexFromSlug?: boolean; |
|
| 259 | 246 | } |
|
| 267 | 254 | let options: ScanOptions; |
|
| 268 | 255 | if ( |
|
| 269 | 256 | frontmatterMappingOrOptions && |
|
| 270 | - | ("slugSource" in frontmatterMappingOrOptions || |
|
| 271 | - | "frontmatterMapping" in frontmatterMappingOrOptions || |
|
| 272 | - | "ignorePatterns" in frontmatterMappingOrOptions) |
|
| 257 | + | ("frontmatterMapping" in frontmatterMappingOrOptions || |
|
| 258 | + | "ignorePatterns" in frontmatterMappingOrOptions || |
|
| 259 | + | "slugField" in frontmatterMappingOrOptions) |
|
| 273 | 260 | ) { |
|
| 274 | 261 | options = frontmatterMappingOrOptions as ScanOptions; |
|
| 275 | 262 | } else { |
|
| 285 | 272 | const { |
|
| 286 | 273 | frontmatterMapping, |
|
| 287 | 274 | ignorePatterns: ignore = [], |
|
| 288 | - | slugSource, |
|
| 289 | 275 | slugField, |
|
| 290 | 276 | removeIndexFromSlug, |
|
| 291 | 277 | } = options; |
|
| 314 | 300 | frontmatterMapping, |
|
| 315 | 301 | ); |
|
| 316 | 302 | const slug = getSlugFromOptions(relativePath, rawFrontmatter, { |
|
| 317 | - | slugSource, |
|
| 318 | 303 | slugField, |
|
| 319 | 304 | removeIndexFromSlug, |
|
| 320 | 305 | }); |
|
| 5 | 5 | coverImage?: string; // Field name for cover image (default: "ogImage") |
|
| 6 | 6 | tags?: string; // Field name for tags (default: "tags") |
|
| 7 | 7 | draft?: string; // Field name for draft status (default: "draft") |
|
| 8 | + | slugField?: string; // Frontmatter field to use for slug (if set, uses frontmatter value; otherwise uses filepath) |
|
| 8 | 9 | } |
|
| 9 | 10 | ||
| 10 | 11 | // Strong reference for Bluesky post (com.atproto.repo.strongRef) |
|
| 31 | 32 | identity?: string; // Which stored identity to use (matches identifier) |
|
| 32 | 33 | frontmatter?: FrontmatterMapping; // Custom frontmatter field mappings |
|
| 33 | 34 | ignore?: string[]; // Glob patterns for files to ignore (e.g., ["_index.md", "**/drafts/**"]) |
|
| 34 | - | slugSource?: "filename" | "path" | "frontmatter"; // How to generate slugs (default: "filename") |
|
| 35 | - | slugField?: string; // Frontmatter field to use when slugSource is "frontmatter" (default: "slug") |
|
| 36 | 35 | removeIndexFromSlug?: boolean; // Remove "/index" or "/_index" suffix from paths (default: false) |
|
| 37 | 36 | textContentField?: string; // Frontmatter field to use for textContent instead of markdown body |
|
| 38 | 37 | bluesky?: BlueskyConfig; // Optional Bluesky posting configuration |
|