From 520891a8533e721e7c2d86ad0533ea017a57f0e1 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 22 Jul 2026 12:26:02 +0000 Subject: [PATCH 1/2] feat(create-bestax): set scaffolded index.html title to the project name The stock template title ("Bestax + Vite + React") shipped unchanged in generated apps, and nothing prompted AI agents to fix it (#349): - ProjectCreator.updateIndexHtmlTitle replaces the template with the project name at scaffold time, right after package.json naming. - The generated CLAUDE.md now tells agents to give the title/meta and README.md the app's real identity instead of shipping boilerplate. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Lvpby5yW74N2RPACCqEjpN --- create-bestax/src/__tests__/constants.test.ts | 13 ++++ .../src/__tests__/project-creator.test.ts | 78 +++++++++++++++++++ create-bestax/src/constants.ts | 3 + create-bestax/src/project-creator.ts | 20 +++++ 4 files changed, 114 insertions(+) diff --git a/create-bestax/src/__tests__/constants.test.ts b/create-bestax/src/__tests__/constants.test.ts index 9d7124f61..b16e1237a 100644 --- a/create-bestax/src/__tests__/constants.test.ts +++ b/create-bestax/src/__tests__/constants.test.ts @@ -25,6 +25,7 @@ const { MESSAGES, PROMPTS, LAUNCH_JSON, + CLAUDE_MD, } = await import('../constants.js'); describe('constants', () => { @@ -227,6 +228,18 @@ describe('constants', () => { }); }); + describe('CLAUDE_MD', () => { + it('tells agents to customize the scaffolded title and README', () => { + const content = CLAUDE_MD('my-app', { + bulmaFlavor: 'complete', + iconLibrary: 'none', + }); + expect(content).toContain('# my-app'); + expect(content).toContain('`<title>`'); + expect(content).toContain('README.md'); + }); + }); + describe('DEFAULT_PROJECT_NAME', () => { it('should be a valid project name', () => { expect(DEFAULT_PROJECT_NAME).toBeDefined(); diff --git a/create-bestax/src/__tests__/project-creator.test.ts b/create-bestax/src/__tests__/project-creator.test.ts index 6c92ef67f..23ba7358c 100644 --- a/create-bestax/src/__tests__/project-creator.test.ts +++ b/create-bestax/src/__tests__/project-creator.test.ts @@ -553,6 +553,53 @@ describe('ProjectCreator', () => { }); }); + describe('updateIndexHtmlTitle', () => { + it('replaces the stock <title> with the project name', async () => { + const targetPath = '/test/project'; + const indexHtmlPath = '/test/project/index.html'; + + ( + fs.default.existsSync as jest.MockedFunction<typeof fs.existsSync> + ).mockImplementation(path => path === indexHtmlPath); + ( + fs.default.readFile as jest.MockedFunction<typeof fs.readFile> + ).mockResolvedValue( + '<head><title>Bestax + Vite + React' as unknown + ); + + await projectCreator.updateIndexHtmlTitle(targetPath, 'my-app'); + + expect(fs.default.writeFile).toHaveBeenCalledWith( + indexHtmlPath, + 'my-app' + ); + }); + + it('does nothing when index.html is missing', async () => { + ( + fs.default.existsSync as jest.MockedFunction + ).mockReturnValue(false); + + await projectCreator.updateIndexHtmlTitle('/test/project', 'my-app'); + + expect(fs.default.readFile).not.toHaveBeenCalled(); + expect(fs.default.writeFile).not.toHaveBeenCalled(); + }); + + it('leaves index.html untouched when it has no tag', async () => { + ( + fs.default.existsSync as jest.MockedFunction<typeof fs.existsSync> + ).mockReturnValue(true); + ( + fs.default.readFile as jest.MockedFunction<typeof fs.readFile> + ).mockResolvedValue('<head></head>' as unknown); + + await projectCreator.updateIndexHtmlTitle('/test/project', 'my-app'); + + expect(fs.default.writeFile).not.toHaveBeenCalled(); + }); + }); + describe('setupIconLibrary', () => { it('should skip setup for none', async () => { await projectCreator.setupIconLibrary('/test/project', 'none', 'vite'); @@ -964,6 +1011,37 @@ describe('ProjectCreator', () => { expect(fileSystem.updatePackageJson).toHaveBeenCalled(); }); + it('sets the index.html title to the project name', async () => { + ( + fileSystem.checkDirectoryExists as jest.MockedFunction< + typeof fileSystem.checkDirectoryExists + > + ).mockResolvedValue(false); + ( + fileSystem.isDirectoryEmpty as jest.MockedFunction< + typeof fileSystem.isDirectoryEmpty + > + ).mockResolvedValue(true); + ( + fs.default.existsSync as jest.MockedFunction<typeof fs.existsSync> + ).mockImplementation(path => String(path).endsWith('index.html')); + ( + fs.default.readFile as jest.MockedFunction<typeof fs.readFile> + ).mockResolvedValue( + '<head><title>Bestax + Vite + React' as unknown + ); + + await projectCreator.create('test-project', { yes: true, skills: false }); + + expect( + (fs.default.writeFile as unknown as jest.Mock).mock.calls.some( + ([p, content]) => + String(p).endsWith('index.html') && + String(content).includes('test-project') + ) + ).toBe(true); + }); + it('writes launch.json when the skills opt-in is accepted', async () => { // target dir does not exist; the bundled skills dir does ( diff --git a/create-bestax/src/constants.ts b/create-bestax/src/constants.ts index 4312ae4b4..16000a7b4 100644 --- a/create-bestax/src/constants.ts +++ b/create-bestax/src/constants.ts @@ -140,6 +140,9 @@ ${setupLines.join('\n')} - Compose existing components before writing custom CSS; theme via \`Theme\` and \`--bulma-*\` variables, never hardcoded colors. - There is no test runner or Storybook in this app — don't assume one. +- \`index.html\`'s \`\` starts as the project name and \`README.md\` is stock template + boilerplate — once this app has a real identity, set the title (and any meta tags) to match + it and rewrite the README to describe *this* app, not the template. ## AI skills diff --git a/create-bestax/src/project-creator.ts b/create-bestax/src/project-creator.ts index d9e190045..fad7fd7e7 100644 --- a/create-bestax/src/project-creator.ts +++ b/create-bestax/src/project-creator.ts @@ -104,6 +104,25 @@ export class ProjectCreator { await copyDirectory(templatePath, targetPath); } + async updateIndexHtmlTitle( + targetPath: string, + projectName: string + ): Promise<void> { + const indexHtmlPath = path.join(targetPath, 'index.html'); + if (!fs.existsSync(indexHtmlPath)) return; + + const htmlContent = await fs.readFile(indexHtmlPath, 'utf8'); + // projectName is validated against PROJECT_NAME_REGEX (letters, digits, + // dots, dashes, underscores only), so it needs no HTML escaping. + const updated = htmlContent.replace( + /<title>[^<]*<\/title>/, + `<title>${projectName}` + ); + if (updated !== htmlContent) { + await fs.writeFile(indexHtmlPath, updated); + } + } + async setupSkills( targetPath: string, projectName: string, @@ -559,6 +578,7 @@ export class ProjectCreator { try { await this.copyTemplate(template, targetPath); await updatePackageJson(targetPath, projectName); + await this.updateIndexHtmlTitle(targetPath, projectName); await this.setupBulmaFlavor(targetPath, bulmaFlavor, template); await this.setupIconLibrary(targetPath, iconLibrary, template); await this.setupConfigProvider( From c3bc72f22eb4e68c440e59299bc08fcae8359ee0 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 22 Jul 2026 12:35:31 +0000 Subject: [PATCH 2/2] test(create-bestax): assert full CLAUDE_MD identity guidance Guard the meta-tag instruction, the project-name default, and the README-rewrite nudge, per CodeRabbit review on #356. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01Lvpby5yW74N2RPACCqEjpN --- create-bestax/src/__tests__/constants.test.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/create-bestax/src/__tests__/constants.test.ts b/create-bestax/src/__tests__/constants.test.ts index b16e1237a..758ab56a7 100644 --- a/create-bestax/src/__tests__/constants.test.ts +++ b/create-bestax/src/__tests__/constants.test.ts @@ -237,6 +237,9 @@ describe('constants', () => { expect(content).toContain('# my-app'); expect(content).toContain('``'); expect(content).toContain('README.md'); + expect(content).toContain('starts as the project name'); + expect(content).toContain('meta tags'); + expect(content).toContain('rewrite the README'); }); });