@@ -128,6 +128,34 @@ export function generateStaticSiteTemplate(config: {
128128 } ,
129129 }
130130
131+ // CloudFront Function for URL rewriting (append .html to URLs without extensions)
132+ resources . UrlRewriteFunction = {
133+ Type : 'AWS::CloudFront::Function' ,
134+ Properties : {
135+ Name : { 'Fn::Sub' : '${AWS::StackName}-url-rewrite' } ,
136+ AutoPublish : true ,
137+ FunctionConfig : {
138+ Comment : 'Append .html extension to URLs without extensions' ,
139+ Runtime : 'cloudfront-js-2.0' ,
140+ } ,
141+ FunctionCode : `function handler(event) {
142+ var request = event.request;
143+ var uri = request.uri;
144+
145+ // If URI ends with /, serve index.html
146+ if (uri.endsWith('/')) {
147+ request.uri = uri + 'index.html';
148+ }
149+ // If URI doesn't have an extension, append .html
150+ else if (!uri.includes('.')) {
151+ request.uri = uri + '.html';
152+ }
153+
154+ return request;
155+ }` ,
156+ } ,
157+ }
158+
131159 // CloudFront Distribution
132160 const distributionConfig : any = {
133161 Enabled : true ,
@@ -152,6 +180,12 @@ export function generateStaticSiteTemplate(config: {
152180 CachedMethods : [ 'GET' , 'HEAD' ] ,
153181 Compress : true ,
154182 CachePolicyId : '658327ea-f89d-4fab-a63d-7e88639e58f6' , // Managed-CachingOptimized
183+ FunctionAssociations : [
184+ {
185+ EventType : 'viewer-request' ,
186+ FunctionARN : { 'Fn::GetAtt' : [ 'UrlRewriteFunction' , 'FunctionARN' ] } ,
187+ } ,
188+ ] ,
155189 } ,
156190 CustomErrorResponses : [
157191 {
@@ -186,7 +220,7 @@ export function generateStaticSiteTemplate(config: {
186220
187221 resources . CloudFrontDistribution = {
188222 Type : 'AWS::CloudFront::Distribution' ,
189- DependsOn : [ 'S3Bucket' , 'CloudFrontOAC' ] ,
223+ DependsOn : [ 'S3Bucket' , 'CloudFrontOAC' , 'UrlRewriteFunction' ] ,
190224 Properties : {
191225 DistributionConfig : distributionConfig ,
192226 } ,
@@ -347,10 +381,12 @@ export async function deployStaticSite(config: StaticSiteConfig): Promise<Deploy
347381
348382 // Check if stack already exists
349383 let stackExists = false
384+ let existingBucketName : string | undefined
350385 try {
351386 const existingStacks = await cf . describeStacks ( { stackName } )
352387 if ( existingStacks . Stacks . length > 0 ) {
353- const stackStatus = existingStacks . Stacks [ 0 ] . StackStatus
388+ const stack = existingStacks . Stacks [ 0 ]
389+ const stackStatus = stack . StackStatus
354390
355391 // If stack is being deleted, wait for it to complete
356392 if ( stackStatus === 'DELETE_IN_PROGRESS' ) {
@@ -363,6 +399,9 @@ export async function deployStaticSite(config: StaticSiteConfig): Promise<Deploy
363399 }
364400 else {
365401 stackExists = true
402+ // Get existing bucket name from stack outputs to ensure consistency during updates
403+ const outputs = stack . Outputs || [ ]
404+ existingBucketName = outputs . find ( o => o . OutputKey === 'BucketName' ) ?. OutputValue
366405 }
367406 }
368407 }
@@ -378,7 +417,8 @@ export async function deployStaticSite(config: StaticSiteConfig): Promise<Deploy
378417
379418 // If stack doesn't exist, check for orphaned resources and clean them up
380419 // Use a unique bucket name suffix if cleanup fails
381- let finalBucket = bucket
420+ // If stack exists, use the existing bucket name to avoid CloudFormation trying to recreate resources
421+ let finalBucket = existingBucketName || bucket
382422 if ( ! stackExists ) {
383423 const s3 = new S3Client ( region )
384424 const cloudfront = new CloudFrontClient ( )
@@ -513,14 +553,13 @@ export async function deployStaticSite(config: StaticSiteConfig): Promise<Deploy
513553 // Create or update stack
514554 let stackId : string
515555 let isUpdate = false
516- console . log ( `Creating CloudFormation stack: ${ stackName } ` )
517- console . log ( `Bucket name: ${ finalBucket } ` )
518- console . log ( `Domain: ${ domain || 'not specified' } ` )
519- console . log ( `Certificate ARN: ${ certificateArn || 'not specified' } ` )
520556
521557 if ( stackExists ) {
522558 isUpdate = true
523- console . log ( 'Stack exists, updating...' )
559+ console . log ( `Updating CloudFormation stack: ${ stackName } ` )
560+ console . log ( `Using existing bucket: ${ finalBucket } ` )
561+ console . log ( `Domain: ${ domain || 'not specified' } ` )
562+ console . log ( `Certificate ARN: ${ certificateArn || 'not specified' } ` )
524563 try {
525564 const result = await cf . updateStack ( {
526565 stackName,
@@ -558,6 +597,10 @@ export async function deployStaticSite(config: StaticSiteConfig): Promise<Deploy
558597 }
559598 }
560599 else {
600+ console . log ( `Creating CloudFormation stack: ${ stackName } ` )
601+ console . log ( `Bucket name: ${ finalBucket } ` )
602+ console . log ( `Domain: ${ domain || 'not specified' } ` )
603+ console . log ( `Certificate ARN: ${ certificateArn || 'not specified' } ` )
561604 console . log ( 'Stack does not exist, creating...' )
562605 const result = await cf . createStack ( {
563606 stackName,
@@ -734,9 +777,10 @@ export async function deleteStaticSite(stackName: string, region: string = 'us-e
734777 */
735778export async function deployStaticSiteFull ( config : StaticSiteConfig & {
736779 sourceDir : string
780+ cleanBucket ?: boolean
737781 onProgress ?: ( stage : string , detail ?: string ) => void
738782} ) : Promise < DeployResult & { filesUploaded ?: number } > {
739- const { sourceDir, onProgress, ...siteConfig } = config
783+ const { sourceDir, cleanBucket = true , onProgress, ...siteConfig } = config
740784
741785 // Step 1: Deploy infrastructure
742786 onProgress ?.( 'infrastructure' , 'Deploying CloudFormation stack...' )
@@ -746,7 +790,20 @@ export async function deployStaticSiteFull(config: StaticSiteConfig & {
746790 return infraResult
747791 }
748792
749- // Step 2: Upload files
793+ // Step 2: Clean bucket before upload (ensures no stale files)
794+ if ( cleanBucket ) {
795+ onProgress ?.( 'clean' , 'Cleaning old files from S3...' )
796+ try {
797+ const s3 = new S3Client ( siteConfig . region || 'us-east-1' )
798+ await s3 . emptyBucket ( infraResult . bucket )
799+ }
800+ catch ( err : any ) {
801+ // Log but don't fail - bucket might be empty
802+ console . log ( `Note: Could not clean bucket: ${ err . message } ` )
803+ }
804+ }
805+
806+ // Step 3: Upload files
750807 onProgress ?.( 'upload' , 'Uploading files to S3...' )
751808 const uploadResult = await uploadStaticFiles ( {
752809 sourceDir,
0 commit comments