···1414| `pdsUrl` | `string` | No | `"https://bsky.social"` | PDS server URL, generated automatically |
1515| `identity` | `string` | No | - | Which stored identity to use |
1616| `frontmatter` | `object` | No | - | Custom frontmatter field mappings |
1717+| `frontmatter.slugField` | `string` | No | - | Frontmatter field to use for slug (defaults to filepath) |
1718| `ignore` | `string[]` | No | - | Glob patterns for files to ignore |
1919+| `removeIndexFromSlug` | `boolean` | No | `false` | Remove `/index` or `/_index` suffix from slugs |
1820| `bluesky` | `object` | No | - | Bluesky posting configuration |
1921| `bluesky.enabled` | `boolean` | No | `false` | Post to Bluesky when publishing documents |
2022| `bluesky.maxAgeDays` | `number` | No | `30` | Only post documents published within this many days |
···7981 }
8082}
8183```
8484+8585+### Slug Configuration
8686+8787+By default, slugs are generated from the filepath (e.g., `posts/my-post.md` becomes `posts/my-post`). To use a frontmatter field instead:
8888+8989+```json
9090+{
9191+ "frontmatter": {
9292+ "slugField": "url"
9393+ }
9494+}
9595+```
9696+9797+If the frontmatter field is not found, it falls back to the filepath.
82988399### Ignoring Files
84100
···176176}
177177178178export interface SlugOptions {
179179- slugSource?: "filename" | "path" | "frontmatter";
180179 slugField?: string;
181180 removeIndexFromSlug?: boolean;
182181}
···186185 rawFrontmatter: Record<string, unknown>,
187186 options: SlugOptions = {},
188187): string {
189189- const {
190190- slugSource = "filename",
191191- slugField = "slug",
192192- removeIndexFromSlug = false,
193193- } = options;
188188+ const { slugField, removeIndexFromSlug = false } = options;
194189195190 let slug: string;
196191197197- switch (slugSource) {
198198- case "path":
199199- // Use full relative path without extension
192192+ // If slugField is set, try to get the value from frontmatter
193193+ if (slugField) {
194194+ const frontmatterValue = rawFrontmatter[slugField];
195195+ if (frontmatterValue && typeof frontmatterValue === "string") {
196196+ // Remove leading slash if present
197197+ slug = frontmatterValue
198198+ .replace(/^\//, "")
199199+ .toLowerCase()
200200+ .replace(/\s+/g, "-");
201201+ } else {
202202+ // Fallback to filepath if frontmatter field not found
200203 slug = relativePath
201204 .replace(/\.mdx?$/, "")
202205 .toLowerCase()
203206 .replace(/\s+/g, "-");
204204- break;
205205-206206- case "frontmatter": {
207207- // Use frontmatter field (slug or url)
208208- const frontmatterValue =
209209- rawFrontmatter[slugField] || rawFrontmatter.slug || rawFrontmatter.url;
210210- if (frontmatterValue && typeof frontmatterValue === "string") {
211211- // Remove leading slash if present
212212- slug = frontmatterValue
213213- .replace(/^\//, "")
214214- .toLowerCase()
215215- .replace(/\s+/g, "-");
216216- } else {
217217- // Fallback to filename if frontmatter field not found
218218- slug = getSlugFromFilename(path.basename(relativePath));
219219- }
220220- break;
221207 }
222222-223223- default:
224224- slug = getSlugFromFilename(path.basename(relativePath));
225225- break;
208208+ } else {
209209+ // Default: use filepath
210210+ slug = relativePath
211211+ .replace(/\.mdx?$/, "")
212212+ .toLowerCase()
213213+ .replace(/\s+/g, "-");
226214 }
227215228216 // Remove /index or /_index suffix if configured
···253241export interface ScanOptions {
254242 frontmatterMapping?: FrontmatterMapping;
255243 ignorePatterns?: string[];
256256- slugSource?: "filename" | "path" | "frontmatter";
257244 slugField?: string;
258245 removeIndexFromSlug?: boolean;
259246}
···267254 let options: ScanOptions;
268255 if (
269256 frontmatterMappingOrOptions &&
270270- ("slugSource" in frontmatterMappingOrOptions ||
271271- "frontmatterMapping" in frontmatterMappingOrOptions ||
272272- "ignorePatterns" in frontmatterMappingOrOptions)
257257+ ("frontmatterMapping" in frontmatterMappingOrOptions ||
258258+ "ignorePatterns" in frontmatterMappingOrOptions ||
259259+ "slugField" in frontmatterMappingOrOptions)
273260 ) {
274261 options = frontmatterMappingOrOptions as ScanOptions;
275262 } else {
···285272 const {
286273 frontmatterMapping,
287274 ignorePatterns: ignore = [],
288288- slugSource,
289275 slugField,
290276 removeIndexFromSlug,
291277 } = options;
···314300 frontmatterMapping,
315301 );
316302 const slug = getSlugFromOptions(relativePath, rawFrontmatter, {
317317- slugSource,
318303 slugField,
319304 removeIndexFromSlug,
320305 });
+1-2
packages/cli/src/lib/types.ts
···55 coverImage?: string; // Field name for cover image (default: "ogImage")
66 tags?: string; // Field name for tags (default: "tags")
77 draft?: string; // Field name for draft status (default: "draft")
88+ slugField?: string; // Frontmatter field to use for slug (if set, uses frontmatter value; otherwise uses filepath)
89}
9101011// Strong reference for Bluesky post (com.atproto.repo.strongRef)
···3132 identity?: string; // Which stored identity to use (matches identifier)
3233 frontmatter?: FrontmatterMapping; // Custom frontmatter field mappings
3334 ignore?: string[]; // Glob patterns for files to ignore (e.g., ["_index.md", "**/drafts/**"])
3434- slugSource?: "filename" | "path" | "frontmatter"; // How to generate slugs (default: "filename")
3535- slugField?: string; // Frontmatter field to use when slugSource is "frontmatter" (default: "slug")
3635 removeIndexFromSlug?: boolean; // Remove "/index" or "/_index" suffix from paths (default: false)
3736 textContentField?: string; // Frontmatter field to use for textContent instead of markdown body
3837 bluesky?: BlueskyConfig; // Optional Bluesky posting configuration