diff --git a/src/build/next.ts b/src/build/next.ts index fdc18bd..7481be1 100644 --- a/src/build/next.ts +++ b/src/build/next.ts @@ -39,6 +39,9 @@ const copyAssets = async (outputPath: string, appPath: string, appRelativePath: recursive: true } ) + await fs.cp(path.join(appPath, 'public'), path.join(outputPath, '.next', 'standalone', appRelativePath, 'public'), { + recursive: true + }) } const getRewritesConfig = (manifestRules: RoutesManifest['rewrites']): NextRewrites => { diff --git a/src/cacheHandler/strategy/s3.ts b/src/cacheHandler/strategy/s3.ts index b742eec..f155a86 100644 --- a/src/cacheHandler/strategy/s3.ts +++ b/src/cacheHandler/strategy/s3.ts @@ -78,6 +78,7 @@ export class S3Cache implements CacheStrategy { }) } const input: PutObjectCommandInput = { ...baseInput } + const tagsValue = [headersTags, this.buildTagKeys(data.tags)].filter(Boolean).join('&') const promises = [ this.#dynamoDBClient.putItem({ @@ -86,8 +87,9 @@ export class S3Cache implements CacheStrategy { pageKey: { S: pageKey }, cacheKey: { S: cacheKey }, s3Key: { S: baseInput.Key! }, - tags: { S: [headersTags, this.buildTagKeys(data.tags)].filter(Boolean).join('&') }, - createdAt: { S: new Date().toISOString() } + createdAt: { S: new Date().toISOString() }, + // TODO: check for empty tags + ...(tagsValue && { tags: { S: tagsValue } }) } }) ] diff --git a/src/cdk/constructs/CloudFrontDistribution.ts b/src/cdk/constructs/CloudFrontDistribution.ts index 1592f49..684a887 100644 --- a/src/cdk/constructs/CloudFrontDistribution.ts +++ b/src/cdk/constructs/CloudFrontDistribution.ts @@ -4,7 +4,7 @@ import * as cloudfront from 'aws-cdk-lib/aws-cloudfront' import * as s3 from 'aws-cdk-lib/aws-s3' import * as origins from 'aws-cdk-lib/aws-cloudfront-origins' import { addOutput } from '../../common/cdk' -import { CacheConfig } from '../../types' +import { DeployConfig } from '../../types' import { HEADER_DEVICE_TYPE } from '../../constants' interface CloudFrontPropsDistribution { @@ -13,7 +13,7 @@ interface CloudFrontPropsDistribution { requestEdgeFunction: cloudfront.experimental.EdgeFunction viewerResponseEdgeFunction: cloudfront.experimental.EdgeFunction viewerRequestLambdaEdge: cloudfront.experimental.EdgeFunction - cacheConfig: CacheConfig + deployConfig: DeployConfig imageTTL?: number } @@ -35,7 +35,7 @@ export class CloudFrontDistribution extends Construct { requestEdgeFunction, viewerResponseEdgeFunction, viewerRequestLambdaEdge, - cacheConfig, + deployConfig, renderServerDomain, imageTTL } = props @@ -43,17 +43,19 @@ export class CloudFrontDistribution extends Construct { const splitCachePolicy = new cloudfront.CachePolicy(this, 'SplitCachePolicy', { cachePolicyName: `${id}-SplitCachePolicy`, queryStringBehavior: cloudfront.CacheQueryStringBehavior.allowList( - ...defaultNextQueries.concat(cacheConfig.cacheQueries ?? []) + ...defaultNextQueries.concat(deployConfig.cache.cacheQueries ?? []) ), - cookieBehavior: cacheConfig.cacheCookies?.length - ? cloudfront.CacheCookieBehavior.allowList(...cacheConfig.cacheCookies) + cookieBehavior: deployConfig.cache.cacheCookies?.length + ? cloudfront.CacheCookieBehavior.allowList(...deployConfig.cache.cacheCookies) : cloudfront.CacheCookieBehavior.none(), headerBehavior: cloudfront.CacheHeaderBehavior.allowList( ...defaultNextHeaders, ...Object.values(HEADER_DEVICE_TYPE) ), minTtl: NoCache, - defaultTtl: NoCache // no caching by default, cache value is going to be used from Cache-Control header. + defaultTtl: NoCache, // no caching by default, cache value is going to be used from Cache-Control header. + enableAcceptEncodingBrotli: true, + enableAcceptEncodingGzip: true }) const longCachePolicy = new cloudfront.CachePolicy(this, 'LongCachePolicy', { @@ -63,7 +65,9 @@ export class CloudFrontDistribution extends Construct { headerBehavior: cloudfront.CacheHeaderBehavior.none(), defaultTtl: OneMonthCache, maxTtl: OneMonthCache, - minTtl: OneMonthCache + minTtl: OneMonthCache, + enableAcceptEncodingBrotli: true, + enableAcceptEncodingGzip: true }) const imageTTLValue = imageTTL ? Duration.seconds(imageTTL) : OneDayCache @@ -75,10 +79,24 @@ export class CloudFrontDistribution extends Construct { headerBehavior: cloudfront.CacheHeaderBehavior.allowList(...defaultNextHeaders), defaultTtl: imageTTLValue, maxTtl: imageTTLValue, - minTtl: imageTTLValue + minTtl: imageTTLValue, + enableAcceptEncodingBrotli: true, + enableAcceptEncodingGzip: true + }) + + const publicAssetsCachePolicy = new cloudfront.CachePolicy(this, 'PublicAssetsCachePolicy', { + cachePolicyName: `${id}-PublicAssetsCachePolicy`, + defaultTtl: deployConfig.publicAssets?.ttl ? Duration.seconds(deployConfig.publicAssets.ttl) : NoCache, + maxTtl: deployConfig.publicAssets?.ttl ? Duration.seconds(deployConfig.publicAssets.ttl) : NoCache, + minTtl: deployConfig.publicAssets?.ttl ? Duration.seconds(deployConfig.publicAssets.ttl) : NoCache, + enableAcceptEncodingBrotli: true, + enableAcceptEncodingGzip: true }) const s3Origin = new origins.S3Origin(staticBucket) + const publicFolderS3Origin = new origins.S3Origin(staticBucket, { + originPath: '/public' + }) const nextServerOrigin = new origins.HttpOrigin(renderServerDomain, { protocolPolicy: cloudfront.OriginProtocolPolicy.HTTP_ONLY, httpPort: 80 @@ -101,7 +119,8 @@ export class CloudFrontDistribution extends Construct { eventType: cloudfront.LambdaEdgeEventType.VIEWER_REQUEST } ], - cachePolicy: splitCachePolicy + cachePolicy: splitCachePolicy, + compress: true }, defaultRootObject: '', additionalBehaviors: { @@ -113,7 +132,8 @@ export class CloudFrontDistribution extends Construct { eventType: cloudfront.LambdaEdgeEventType.ORIGIN_REQUEST } ], - cachePolicy: splitCachePolicy + cachePolicy: splitCachePolicy, + compress: true }, '/_next/image*': { origin: nextServerOrigin, @@ -121,8 +141,17 @@ export class CloudFrontDistribution extends Construct { }, '/_next/*': { origin: s3Origin, - cachePolicy: longCachePolicy - } + cachePolicy: longCachePolicy, + compress: true + }, + ...(deployConfig.publicAssets + ? { + [`${deployConfig.publicAssets.prefix}/*`]: { + origin: publicFolderS3Origin, + cachePolicy: publicAssetsCachePolicy + } + } + : {}) } }) diff --git a/src/cdk/stacks/NextCloudfrontStack.ts b/src/cdk/stacks/NextCloudfrontStack.ts index 1e4f166..4626797 100644 --- a/src/cdk/stacks/NextCloudfrontStack.ts +++ b/src/cdk/stacks/NextCloudfrontStack.ts @@ -77,7 +77,7 @@ export class NextCloudfrontStack extends Stack { requestEdgeFunction: this.originRequestLambdaEdge.lambdaEdge, viewerResponseEdgeFunction: this.viewerResponseLambdaEdge.lambdaEdge, viewerRequestLambdaEdge: this.viewerRequestLambdaEdge.lambdaEdge, - cacheConfig: deployConfig.cache, + deployConfig: deployConfig, imageTTL }) diff --git a/src/commands/deploy.ts b/src/commands/deploy.ts index 4fff6cb..d9bb13a 100644 --- a/src/commands/deploy.ts +++ b/src/commands/deploy.ts @@ -208,6 +208,18 @@ export const deploy = async (config: DeployConfig) => { folderRootPath: path.join(outputPath, '.next', 'static') }) + await uploadFolderToS3(s3Client, { + Bucket: nextRenderServerStackOutput.StaticBucketName, + Key: 'public', + folderRootPath: path.join( + outputPath, + '.next', + 'standalone', + path.relative(projectSettings.root, projectSettings.projectPath), + 'public' + ) + }) + // upload code version to bucket. await uploadFileToS3(s3Client, { Bucket: nextRenderServerStackOutput.RenderServerVersionsBucketName, diff --git a/src/types/index.ts b/src/types/index.ts index 1a933cd..c7cfba6 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -10,6 +10,10 @@ export interface CacheConfig { export interface DeployConfig { cache: CacheConfig + publicAssets?: { + prefix: string + ttl?: number + } } export type NextRedirects = Awaited['redirects']>>