A CLI for publishing standard.site documents to ATProto sequoia.pub
standard site lexicon cli publishing

chore: updated inject to handle new slug options

+29 -50
+27 -50
packages/cli/src/commands/inject.ts
··· 44 // Load state to get atUri mappings 45 const state = await loadState(configDir); 46 47 - // Generic filenames where the slug is the parent directory, not the filename 48 - // Covers: SvelteKit (+page), Astro/Hugo (index), Next.js (page), etc. 49 - const genericFilenames = new Set([ 50 - "+page", 51 - "index", 52 - "_index", 53 - "page", 54 - "readme", 55 - ]); 56 - 57 - // Build a map of slug/path to atUri from state 58 - const pathToAtUri = new Map<string, string>(); 59 for (const [filePath, postState] of Object.entries(state.posts)) { 60 - if (postState.atUri) { 61 - // Extract slug from file path (e.g., ./content/blog/my-post.md -> my-post) 62 - let basename = path.basename(filePath, path.extname(filePath)); 63 64 - // If the filename is a generic convention name, use the parent directory as slug 65 - if (genericFilenames.has(basename.toLowerCase())) { 66 - // Split path and filter out route groups like (blog-article) 67 - const pathParts = filePath 68 - .split(/[/\\]/) 69 - .filter((p) => p && !(p.startsWith("(") && p.endsWith(")"))); 70 - // The slug should be the second-to-last part (last is the filename) 71 - if (pathParts.length >= 2) { 72 - const slug = pathParts[pathParts.length - 2]; 73 - if (slug && slug !== "." && slug !== "content" && slug !== "routes" && slug !== "src") { 74 - basename = slug; 75 - } 76 - } 77 } 78 - 79 - pathToAtUri.set(basename, postState.atUri); 80 - 81 - // Also add variations that might match HTML file paths 82 - // e.g., /blog/my-post, /posts/my-post, my-post/index 83 - const dirName = path.basename(path.dirname(filePath)); 84 - // Skip route groups and common directory names 85 - if (dirName !== "." && dirName !== "content" && !(dirName.startsWith("(") && dirName.endsWith(")"))) { 86 - pathToAtUri.set(`${dirName}/${basename}`, postState.atUri); 87 - } 88 } 89 } 90 91 - if (pathToAtUri.size === 0) { 92 log.warn( 93 "No published posts found in state. Run 'sequoia publish' first.", 94 ); 95 return; 96 } 97 98 - log.info(`Found ${pathToAtUri.size} published posts in state`); 99 100 // Scan for HTML files 101 const htmlFiles = await glob("**/*.html", { ··· 125 let atUri: string | undefined; 126 127 // Strategy 1: Direct basename match (e.g., my-post.html -> my-post) 128 - atUri = pathToAtUri.get(htmlBasename); 129 130 - // Strategy 2: Directory name for index.html (e.g., my-post/index.html -> my-post) 131 if (!atUri && htmlBasename === "index" && htmlDir !== ".") { 132 - const slug = path.basename(htmlDir); 133 - atUri = pathToAtUri.get(slug); 134 135 - // Also try parent/slug pattern 136 if (!atUri) { 137 - const parentDir = path.dirname(htmlDir); 138 - if (parentDir !== ".") { 139 - atUri = pathToAtUri.get(`${path.basename(parentDir)}/${slug}`); 140 - } 141 } 142 } 143 144 // Strategy 3: Full path match (e.g., blog/my-post.html -> blog/my-post) 145 if (!atUri && htmlDir !== ".") { 146 - atUri = pathToAtUri.get(`${htmlDir}/${htmlBasename}`); 147 } 148 149 if (!atUri) {
··· 44 // Load state to get atUri mappings 45 const state = await loadState(configDir); 46 47 + // Build a map of slug to atUri from state 48 + // The slug is stored in state by the publish command, using the configured slug options 49 + const slugToAtUri = new Map<string, string>(); 50 for (const [filePath, postState] of Object.entries(state.posts)) { 51 + if (postState.atUri && postState.slug) { 52 + // Use the slug stored in state (computed by publish with config options) 53 + slugToAtUri.set(postState.slug, postState.atUri); 54 55 + // Also add the last segment for simpler matching 56 + // e.g., "40th-puzzle-box/what-a-gift" -> also map "what-a-gift" 57 + const lastSegment = postState.slug.split("/").pop(); 58 + if (lastSegment && lastSegment !== postState.slug) { 59 + slugToAtUri.set(lastSegment, postState.atUri); 60 } 61 + } else if (postState.atUri) { 62 + // Fallback for older state files without slug field 63 + // Extract slug from file path (e.g., ./content/blog/my-post.md -> my-post) 64 + const basename = path.basename(filePath, path.extname(filePath)); 65 + slugToAtUri.set(basename.toLowerCase(), postState.atUri); 66 } 67 } 68 69 + if (slugToAtUri.size === 0) { 70 log.warn( 71 "No published posts found in state. Run 'sequoia publish' first.", 72 ); 73 return; 74 } 75 76 + log.info(`Found ${slugToAtUri.size} slug mappings from published posts`); 77 78 // Scan for HTML files 79 const htmlFiles = await glob("**/*.html", { ··· 103 let atUri: string | undefined; 104 105 // Strategy 1: Direct basename match (e.g., my-post.html -> my-post) 106 + atUri = slugToAtUri.get(htmlBasename); 107 108 + // Strategy 2: For index.html, try the directory path 109 + // e.g., posts/40th-puzzle-box/what-a-gift/index.html -> 40th-puzzle-box/what-a-gift 110 if (!atUri && htmlBasename === "index" && htmlDir !== ".") { 111 + // Try full directory path (for nested subdirectories) 112 + atUri = slugToAtUri.get(htmlDir); 113 114 + // Also try just the last directory segment 115 if (!atUri) { 116 + const lastDir = path.basename(htmlDir); 117 + atUri = slugToAtUri.get(lastDir); 118 } 119 } 120 121 // Strategy 3: Full path match (e.g., blog/my-post.html -> blog/my-post) 122 if (!atUri && htmlDir !== ".") { 123 + atUri = slugToAtUri.get(`${htmlDir}/${htmlBasename}`); 124 } 125 126 if (!atUri) {
+1
packages/cli/src/commands/publish.ts
··· 221 contentHash, 222 atUri, 223 lastPublished: new Date().toISOString(), 224 }; 225 } catch (error) { 226 const errorMessage = error instanceof Error ? error.message : String(error);
··· 221 contentHash, 222 atUri, 223 lastPublished: new Date().toISOString(), 224 + slug: post.slug, 225 }; 226 } catch (error) { 227 const errorMessage = error instanceof Error ? error.message : String(error);
+1
packages/cli/src/lib/types.ts
··· 67 contentHash: string; 68 atUri?: string; 69 lastPublished?: string; 70 } 71 72 export interface PublicationRecord {
··· 67 contentHash: string; 68 atUri?: string; 69 lastPublished?: string; 70 + slug?: string; // The generated slug for this post (used by inject command) 71 } 72 73 export interface PublicationRecord {