···44 // Load state to get atUri mappings
45 const state = await loadState(configDir);
4647- // 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));
6364- // 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 }
9091- if (pathToAtUri.size === 0) {
92 log.warn(
93 "No published posts found in state. Run 'sequoia publish' first.",
94 );
95 return;
96 }
9798- log.info(`Found ${pathToAtUri.size} published posts in state`);
99100 // Scan for HTML files
101 const htmlFiles = await glob("**/*.html", {
···125 let atUri: string | undefined;
126127 // Strategy 1: Direct basename match (e.g., my-post.html -> my-post)
128- atUri = pathToAtUri.get(htmlBasename);
129130- // Strategy 2: Directory name for index.html (e.g., my-post/index.html -> my-post)
0131 if (!atUri && htmlBasename === "index" && htmlDir !== ".") {
132- const slug = path.basename(htmlDir);
133- atUri = pathToAtUri.get(slug);
134135- // 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 }
143144 // 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 }
148149 if (!atUri) {
···44 // Load state to get atUri mappings
45 const state = await loadState(configDir);
4647+ // 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>();
00000000050 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);
5455+ // 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);
0000000060 }
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);
0000066 }
67 }
6869+ if (slugToAtUri.size === 0) {
70 log.warn(
71 "No published posts found in state. Run 'sequoia publish' first.",
72 );
73 return;
74 }
7576+ log.info(`Found ${slugToAtUri.size} slug mappings from published posts`);
7778 // Scan for HTML files
79 const htmlFiles = await glob("**/*.html", {
···103 let atUri: string | undefined;
104105 // Strategy 1: Direct basename match (e.g., my-post.html -> my-post)
106+ atUri = slugToAtUri.get(htmlBasename);
107108+ // 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);
113114+ // Also try just the last directory segment
115 if (!atUri) {
116+ const lastDir = path.basename(htmlDir);
117+ atUri = slugToAtUri.get(lastDir);
00118 }
119 }
120121 // 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 }
125126 if (!atUri) {