Skip to content

Commit

Permalink
Merge branch 'canary' into add/more-stats-pages
Browse files Browse the repository at this point in the history
  • Loading branch information
ijjk committed Jul 15, 2021
2 parents d84df53 + 8151a7e commit ddab46e
Show file tree
Hide file tree
Showing 15 changed files with 460 additions and 49 deletions.
18 changes: 16 additions & 2 deletions docs/basic-features/image-optimization.md
Expand Up @@ -136,9 +136,11 @@ Images are optimized dynamically upon request and stored in the `<distDir>/cache

The expiration (or rather Max Age) is defined by the upstream server's `Cache-Control` header.

If `s-maxage` is found in `Cache-Control`, it is used. If no `s-maxage` is found, then `max-age` is used. If no `max-age` is found, then 60 seconds is used.
If `s-maxage` is found in `Cache-Control`, it is used. If no `s-maxage` is found, then `max-age` is used. If no `max-age` is found, then [`minimumCacheTTL`](#minimum-cache-ttl) is used.

You can configure [`deviceSizes`](#device-sizes) and [`imageSizes`](#device-sizes) to reduce the total number of possible generated images.
You can configure [`minimumCacheTTL`](#minimum-cache-ttl) to increase the cache duration when the upstream image does not include `max-age`.

You can also configure [`deviceSizes`](#device-sizes) and [`imageSizes`](#device-sizes) to reduce the total number of possible generated images.

## Advanced

Expand Down Expand Up @@ -172,6 +174,18 @@ module.exports = {
}
```

### Minimum Cache TTL

You can configure the time to live (TTL) in seconds for cached optimized images. This will configure the server's image cache as well as the `Cache-Control` header sent to the browser. This is a global setting that will affect all images. In most cases, its better to use a [Static Image Import](#image-Imports) which will handle hashing file contents and caching the file. You can also configure the `Cache-Control` header on an individual upstream image instead.

```js
module.exports = {
images: {
minimumCacheTTL: 60,
},
}
```

### Disable Static Imports

The default behavior allows you to import static files such as `import icon from './icon.png` and then pass that to the `src` property.
Expand Down
6 changes: 5 additions & 1 deletion errors/invalid-images-config.md
Expand Up @@ -18,8 +18,12 @@ module.exports = {
// limit of 50 domains values
domains: [],
path: '/_next/image',
// loader can be 'default', 'imgix', 'cloudinary', or 'akamai'
// loader can be 'default', 'imgix', 'cloudinary', 'akamai', or 'custom'
loader: 'default',
// disable static imports for image files
disableStaticImages: false,
// minimumCacheTTL is in seconds, must be integer 0 or more
minimumCacheTTL: 60,
},
}
```
Expand Down
4 changes: 4 additions & 0 deletions errors/manifest.json
Expand Up @@ -279,6 +279,10 @@
"title": "no-document-viewport-meta",
"path": "/errors/no-document-viewport-meta.md"
},
{
"title": "no-duplicate-head",
"path": "/errors/no-duplicate-head.md"
},
{
"title": "no-head-import-in-document",
"path": "/errors/no-head-import-in-document.md"
Expand Down
38 changes: 38 additions & 0 deletions errors/no-duplicate-head.md
@@ -0,0 +1,38 @@
# No Duplicate Head

### Why This Error Occurred

More than a single instance of the `<Head />` component was used in a single custom document. This can cause unexpected behavior in your application.

### Possible Ways to Fix It

Only use a single `<Head />` component in your custom document in `pages/_document.js`.

```jsx
// pages/_document.js
import Document, { Html, Head, Main, NextScript } from 'next/document'

class MyDocument extends Document {
static async getInitialProps(ctx) {
//...
}

render() {
return (
<Html>
<Head />
<body>
<Main />
<NextScript />
</body>
</Html>
)
}
}

export default MyDocument
```

### Useful Links

- [Custom Document](https://nextjs.org/docs/advanced-features/custom-document)
2 changes: 2 additions & 0 deletions packages/eslint-plugin-next/lib/index.js
Expand Up @@ -12,6 +12,7 @@ module.exports = {
'link-passhref': require('./rules/link-passhref'),
'no-document-import-in-page': require('./rules/no-document-import-in-page'),
'no-head-import-in-document': require('./rules/no-head-import-in-document'),
'no-duplicate-head': require('./rules/no-duplicate-head'),
},
configs: {
recommended: {
Expand All @@ -29,6 +30,7 @@ module.exports = {
'@next/next/link-passhref': 1,
'@next/next/no-document-import-in-page': 2,
'@next/next/no-head-import-in-document': 2,
'@next/next/no-duplicate-head': 2,
},
},
},
Expand Down
55 changes: 55 additions & 0 deletions packages/eslint-plugin-next/lib/rules/no-duplicate-head.js
@@ -0,0 +1,55 @@
module.exports = {
meta: {
docs: {
description: 'Enforce no duplicate usage of <Head> in pages/document.js',
recommended: true,
},
},
create: function (context) {
let documentImportName
return {
ImportDeclaration(node) {
if (node.source.value === 'next/document') {
const documentImport = node.specifiers.find(
({ type }) => type === 'ImportDefaultSpecifier'
)
if (documentImport && documentImport.local) {
documentImportName = documentImport.local.name
}
}
},
ReturnStatement(node) {
const ancestors = context.getAncestors()
const documentClass = ancestors.find(
(ancestorNode) =>
ancestorNode.type === 'ClassDeclaration' &&
ancestorNode.superClass &&
ancestorNode.superClass.name === documentImportName
)

if (!documentClass) {
return
}

if (node.argument && node.argument.children) {
const headComponents = node.argument.children.filter(
(childrenNode) =>
childrenNode.openingElement &&
childrenNode.openingElement.name &&
childrenNode.openingElement.name.name === 'Head'
)

if (headComponents.length > 1) {
for (let i = 1; i < headComponents.length; i++) {
context.report({
node: headComponents[i],
message:
'Do not include multiple instances of <Head/>. See: https://nextjs.org/docs/messages/no-duplicate-head',
})
}
}
}
},
}
},
}
15 changes: 11 additions & 4 deletions packages/next/export/worker.ts
Expand Up @@ -199,15 +199,22 @@ export default async function exportPage({
publicRuntimeConfig: renderOpts.runtimeConfig,
})

let htmlFilename = `${filePath}${sep}index.html`
if (!subFolders) htmlFilename = `${filePath}.html`
const getHtmlFilename = (_path: string) =>
subFolders ? `${_path}${sep}index.html` : `${_path}.html`
let htmlFilename = getHtmlFilename(filePath)

const pageExt = extname(page)
const pathExt = extname(path)
// Make sure page isn't a folder with a dot in the name e.g. `v1.2`
if (pageExt !== pathExt && pathExt !== '') {
// If the path has an extension, use that as the filename instead
htmlFilename = path
const isBuiltinPaths = ['/500', '/404'].some(
(p) => p === path || p === path + '.html'
)
// If the ssg path has .html extension, and it's not builtin paths, use it directly
// Otherwise, use that as the filename instead
const isHtmlExtPath =
!serverless && !isBuiltinPaths && path.endsWith('.html')
htmlFilename = isHtmlExtPath ? getHtmlFilename(path) : path
} else if (path === '/') {
// If the path is the root, just use index.html
htmlFilename = 'index.html'
Expand Down
11 changes: 11 additions & 0 deletions packages/next/server/config.ts
Expand Up @@ -291,6 +291,17 @@ function assignDefaults(userConfig: { [key: string]: any }) {
if (images.path === imageConfigDefault.path && result.basePath) {
images.path = `${result.basePath}${images.path}`
}

if (
images.minimumCacheTTL &&
(!Number.isInteger(images.minimumCacheTTL) || images.minimumCacheTTL < 0)
) {
throw new Error(
`Specified images.minimumCacheTTL should be an integer 0 or more
', '
)}), received (${images.minimumCacheTTL}).\nSee more info here: https://nextjs.org/docs/messages/invalid-images-config`
)
}
}

if (result.i18n) {
Expand Down
2 changes: 2 additions & 0 deletions packages/next/server/image-config.ts
Expand Up @@ -15,6 +15,7 @@ export type ImageConfig = {
path: string
domains?: string[]
disableStaticImages?: boolean
minimumCacheTTL?: number
}

export const imageConfigDefault: ImageConfig = {
Expand All @@ -24,4 +25,5 @@ export const imageConfigDefault: ImageConfig = {
loader: 'default',
domains: [],
disableStaticImages: false,
minimumCacheTTL: 60,
}
22 changes: 15 additions & 7 deletions packages/next/server/image-optimizer.ts
Expand Up @@ -39,7 +39,13 @@ export async function imageOptimizer(
isDev = false
) {
const imageData: ImageConfig = nextConfig.images || imageConfigDefault
const { deviceSizes = [], imageSizes = [], domains = [], loader } = imageData
const {
deviceSizes = [],
imageSizes = [],
domains = [],
loader,
minimumCacheTTL = 60,
} = imageData

if (loader !== 'default') {
await server.render404(req, res, parsedUrl)
Expand Down Expand Up @@ -206,7 +212,10 @@ export async function imageOptimizer(
upstreamType =
detectContentType(upstreamBuffer) ||
upstreamRes.headers.get('Content-Type')
maxAge = getMaxAge(upstreamRes.headers.get('Cache-Control'))
maxAge = getMaxAge(
upstreamRes.headers.get('Cache-Control'),
minimumCacheTTL
)
} else {
try {
const resBuffers: Buffer[] = []
Expand Down Expand Up @@ -261,7 +270,7 @@ export async function imageOptimizer(
upstreamBuffer = Buffer.concat(resBuffers)
upstreamType =
detectContentType(upstreamBuffer) || mockRes.getHeader('Content-Type')
maxAge = getMaxAge(mockRes.getHeader('Cache-Control'))
maxAge = getMaxAge(mockRes.getHeader('Cache-Control'), minimumCacheTTL)
} catch (err) {
res.statusCode = 500
res.end('"url" parameter is valid but upstream response is invalid')
Expand Down Expand Up @@ -529,8 +538,7 @@ export function detectContentType(buffer: Buffer) {
return null
}

export function getMaxAge(str: string | null): number {
const minimum = 60
export function getMaxAge(str: string | null, minimumCacheTTL: number): number {
const map = parseCacheControl(str)
if (map) {
let age = map.get('s-maxage') || map.get('max-age') || ''
Expand All @@ -539,8 +547,8 @@ export function getMaxAge(str: string | null): number {
}
const n = parseInt(age, 10)
if (!isNaN(n)) {
return Math.max(n, minimum)
return Math.max(n, minimumCacheTTL)
}
}
return minimum
return minimumCacheTTL
}

0 comments on commit ddab46e

Please sign in to comment.