'); expect(mainIndex).toBeGreaterThan(-1); expect(h1Index).toBeGreaterThan(mainIndex); expect(articleIndex).toBeGreaterThan(mainIndex); expect(h1Index).toBeLessThan(articleIndex); }); it('renders categories and tags as small bubbles on single post pages with category-first order and tag color override', async () => { tempDir = await mkdtemp(path.join(tmpdir(), 'bds-preview-taxonomy-')); await mkdir(path.join(tempDir, 'meta'), { recursive: true }); await writeFile(path.join(tempDir, 'meta', 'tags.json'), JSON.stringify([ { name: 'css-only', color: '#22aa88' }, { name: 'default-color' }, ]), 'utf-8'); const post = makePost({ id: 'taxonomy-post', title: 'Taxonomy Post', slug: 'taxonomy-post', createdAt: new Date('2025-02-14T10:00:00.000Z'), categories: ['article', 'news'], tags: ['css-only', 'default-color'], content: 'Body', }); server = new PreviewServer({ postEngine: makeEngine([post]), settingsEngine: makeSettings(50), mediaEngine: makeMediaEngine([]) as any, postMediaEngine: makePostMediaEngine({}) as any, menuEngine: makeMenuEngine({ items: [] }) as any, getActiveProjectContext: async () => ({ projectId: 'default', dataDir: tempDir || undefined }), }); await server.start(0); const response = await fetch(`${server.getBaseUrl()}/2025/2/14/taxonomy-post/`); expect(response.status).toBe(200); const html = await response.text(); expect(html).toContain('class="single-post-taxonomy"'); expect(html).toContain('aria-label="Taxonomy"'); expect(html).toContain('class="single-post-taxonomy-bubble single-post-taxonomy-bubble-category"'); expect(html).toContain('class="single-post-taxonomy-bubble single-post-taxonomy-bubble-tag"'); expect(html).toContain('href="/category/article/"'); expect(html).toContain('href="/tag/css-only/"'); expect(html).toContain('style="--bubble-accent: #22aa88;"'); expect(html).toContain('background: var(--bubble-bg, var(--bubble-accent));'); expect(html).toContain('color: #000;'); expect(html).toContain('.single-post-taxonomy-bubble-category {'); expect(html).toContain('--bubble-accent: var(--pico-ins-color'); expect(html).toContain('--bubble-bg: var(--pico-ins-color'); expect(html).toContain('.single-post-taxonomy-bubble-tag {'); expect(html).toContain('--bubble-accent: var(--pico-del-color'); expect(html).toContain('--bubble-bg: var(--pico-del-color'); const categoryIndex = html.indexOf('single-post-taxonomy-bubble-category'); const tagIndex = html.indexOf('single-post-taxonomy-bubble-tag'); expect(categoryIndex).toBeGreaterThan(-1); expect(tagIndex).toBeGreaterThan(-1); expect(categoryIndex).toBeLessThan(tagIndex); }); it('renders backlinks section at the bottom of a single post with slug bubbles linking to source posts', async () => { const sourcePost = makePost({ id: 'source-post', title: 'Source Post', slug: 'source-post', createdAt: new Date('2025-03-10T10:00:00.000Z'), content: 'Links to [target](/2025/2/14/target-post)', }); const longSlugPost = makePost({ id: 'long-slug-post', title: 'A Very Long Slug Post Title', slug: 'a-very-long-slug-post-that-exceeds-thirty-characters', createdAt: new Date('2025-03-12T10:00:00.000Z'), content: 'Links to [target](/2025/2/14/target-post)', }); const targetPost = makePost({ id: 'target-post', title: 'Target Post', slug: 'target-post', createdAt: new Date('2025-02-14T10:00:00.000Z'), content: 'This is the target post', }); const engine = makeEngine([sourcePost, longSlugPost, targetPost]); (engine as any).getLinkedBy = async (postId: string) => { if (postId === 'target-post') { return [ { id: 'source-post', title: 'Source Post', slug: 'source-post' }, { id: 'long-slug-post', title: 'A Very Long Slug Post Title', slug: 'a-very-long-slug-post-that-exceeds-thirty-characters' }, ]; } return []; }; server = new PreviewServer({ postEngine: engine, settingsEngine: makeSettings(50), mediaEngine: makeMediaEngine([]) as any, postMediaEngine: makePostMediaEngine({}) as any, menuEngine: makeMenuEngine({ items: [] }) as any, getActiveProjectContext: async () => ({ projectId: 'default' }), }); await server.start(0); const response = await fetch(`${server.getBaseUrl()}/2025/2/14/target-post/`); expect(response.status).toBe(200); const html = await response.text(); // Backlinks section exists expect(html).toContain('class="single-post-backlinks"'); // "Linked from" label expect(html).toContain('Linked from'); // Backlink bubbles with correct class expect(html).toContain('class="single-post-taxonomy-bubble single-post-backlink-bubble"'); // Slug text appears expect(html).toContain('source-post'); // Long slugs are truncated to 30 chars + "..." in display text expect(html).toContain('a-very-long-slug-post-that-exc...'); // The display text should NOT show the full slug (only the href contains it) expect(html).toContain('>a-very-long-slug-post-that-exc...'); expect(html).not.toContain('>a-very-long-slug-post-that-exceeds-thirty-characters'); // Links point to the canonical post path expect(html).toContain('href="/2025/03/10/source-post"'); expect(html).toContain('href="/2025/03/12/a-very-long-slug-post-that-exceeds-thirty-characters"'); // Backlinks use pico accent color expect(html).toContain('.single-post-backlink-bubble'); expect(html).toContain('--pico-primary'); // Backlinks section is after the article const articleEndIndex = html.indexOf('