symphony-joy v0.0.5
SYMPHONY
Symphony-js is a minimalistic framework for server-rendered React applications.
How to use
Setup
Install it:
npm install --save symphony-joy react react-dom
symphony-joy only supports React 16.
and add a script to your package.json like this:
{
"scripts": {
"dev": "symphony",
"build": "symphony build",
"start": "symphony start"
}
}
After that, populating ./src/index.js
inside your project:
export default () => <div>Welcome to symphony!</div>
and then just run npm run dev
and go to http://localhost:3000
. To use another port, you can run npm run dev -- -p <your port here>
.
So far, we get:
- Automatic compilation and bundling (with webpack and babel)
- Hot code reloading
- Entry point of app, exported by
./src/index.js
- Server rendering, and auto restart
- Static file serving.
./static/
is mapped to/static/
To see how simple this is, check out the ./example/hello-world
CSS
Built-in CSS support
We bundle styled-jsx to provide support for isolated scoped CSS. The aim is to support "shadow CSS" similar to Web Components, which unfortunately do not support server-rendering and are JS-only.
export default () =>
<div>
Hello world
<p>scoped!</p>
<style jsx>{`
p {
color: blue;
}
div {
background: red;
}
@media (max-width: 600px) {
div {
background: blue;
}
}
`}</style>
<style global jsx>{`
body {
background: black;
}
`}</style>
</div>
Please see the styled-jsx documentation for more examples.
Importing CSS / Sass / Less files
To support importing .css
.scss
or .less
files you can use these modules, which configure sensible defaults for server rendered applications.
Static file serving (e.g.: images)
Create a folder called static
in your project root directory. From your code you can then reference those files with /static/
URLs:
export default () => <img src="/static/my-image.png" />
Populating <head>
We expose a built-in component for appending elements to the <head>
of the page.
import Head from 'symphony/head'
export default () =>
<div>
<Head>
<title>My page title</title>
<meta name="viewport" content="initial-scale=1.0, width=device-width" />
</Head>
<p>Hello world!</p>
</div>
To avoid duplicate tags in your <head>
you can use the key
property, which will make sure the tag is only rendered once:
import Head from 'next/head'
export default () => (
<div>
<Head>
<title>My page title</title>
<meta name="viewport" content="initial-scale=1.0, width=device-width" key="viewport" />
</Head>
<Head>
<meta name="viewport" content="initial-scale=1.2, width=device-width" key="viewport" />
</Head>
<p>Hello world!</p>
</div>
)
In this case only the second <meta name="viewport" />
is rendered.
Note: The contents of <head>
get cleared upon unmounting the component, so make sure each page completely defines what it needs in <head>
, without making assumptions about what other pages added
Fetching data and component lifecycle
TODO
Routing
symphony has integrate react-router-4
Custom server and routing
Typically you start your next server with next start
. It's possible, however, to start a server 100% programmatically in order to customize routes, use route patterns, etc.
When using a custom server with a server file, for example called server.js
, make sure you update the scripts key in package.json
to:
{
"scripts": {
"dev": "node server.js",
"build": "next build",
"start": "NODE_ENV=production node server.js"
}
}
This example makes /a
resolve to ./b
;
// This file doesn't go through babel or webpack transformation.
// Make sure the syntax and sources this file requires are compatible with the current node version you are running
// See https://github.com/zeit/next.js/issues/1245 for discussions on Universal Webpack or universal Babel
const { createServer } = require('http')
const { parse } = require('url')
const next = require('next')
const dev = process.env.NODE_ENV !== 'production'
const app = next({ dev })
const handle = app.getRequestHandler()
app.prepare().then(() => {
createServer((req, res) => {
// Be sure to pass `true` as the second argument to `url.parse`.
// This tells it to parse the query portion of the URL.
const parsedUrl = parse(req.url, true)
const { pathname, query } = parsedUrl
if (pathname === '/a') {
app.render(req, res, '/b', query)
} else {
handle(req, res, parsedUrl)
}
}).listen(3000, err => {
if (err) throw err
console.log('> Ready on http://localhost:3000')
})
})
The next
API is as follows:
next(opts: object)
Supported options:
dev
(bool
) whether to launch Next.js in dev mode - defaultfalse
main
(string
) the root component of app, - defaultsrc/config
dir
(string
) where the Next project is located - default'.'
quiet
(bool
) Hide error messages containing server information - defaultfalse
conf
(object
) the same object you would use innext.config.js
- default{}
Then, change your start
script to NODE_ENV=production node server.js
.
Dynamic assetPrefix
Sometimes we need to set the assetPrefix
dynamically. This is useful when changing the assetPrefix
based on incoming requests.
For that, we can use app.setAssetPrefix
.
Here's an example usage of it:
const next = require('next')
const micro = require('micro')
const dev = process.env.NODE_ENV !== 'production'
const app = next({ dev })
const handle = app.getRequestHandler()
app.prepare().then(() => {
const server = micro((req, res) => {
// Add assetPrefix support based on the hostname
if (req.headers.host === 'my-app.com') {
app.setAssetPrefix('http://cdn.com/myapp')
} else {
app.setAssetPrefix('')
}
handleNextRequests(req, res)
})
server.listen(port, (err) => {
if (err) {
throw err
}
console.log(`> Ready on http://localhost:${port}`)
})
})
Dynamic Import
Next.js supports TC39 dynamic import proposal for JavaScript. With that, you could import JavaScript modules (inc. React Components) dynamically and work with them.
You can think dynamic imports as another way to split your code into manageable chunks. Since Next.js supports dynamic imports with SSR, you could do amazing things with it.
Here are a few ways to use dynamic imports.
1. Basic Usage (Also does SSR)
import dynamic from 'next/dynamic'
const DynamicComponent = dynamic(import('../components/hello'))
export default () =>
<div>
<Header />
<DynamicComponent />
<p>HOME PAGE is here!</p>
</div>
2. With Custom Loading Component
import dynamic from 'next/dynamic'
const DynamicComponentWithCustomLoading = dynamic(
import('../components/hello2'),
{
loading: () => <p>...</p>
}
)
export default () =>
<div>
<Header />
<DynamicComponentWithCustomLoading />
<p>HOME PAGE is here!</p>
</div>
3. With Multiple Modules At Once
import dynamic from 'next/dynamic'
const HelloBundle = dynamic({
modules: props => {
const components = {
Hello1: import('../components/hello1'),
Hello2: import('../components/hello2')
}
// Add remove components based on props
return components
},
render: (props, { Hello1, Hello2 }) =>
<div>
<h1>
{props.title}
</h1>
<Hello1 />
<Hello2 />
</div>
})
export default () => <HelloBundle title="Dynamic Bundle" />
Custom <Document>
Pages in Next.js
skip the definition of the surrounding document's markup. For example, you never include <html>
, <body>
, etc. To override that default behavior, you must create a file at ./pages/_document.js
, where you can extend the Document
class:
// ./pages/_document.js
import Document, { Head, Main, NextScript } from 'next/document'
import flush from 'styled-jsx/server'
export default class MyDocument extends Document {
static getInitialProps({ renderPage }) {
const { html, head, errorHtml, chunks } = renderPage()
const styles = flush()
return { html, head, errorHtml, chunks, styles }
}
render() {
return (
<html>
<Head>
<style>{`body { margin: 0 } /* custom! */`}</style>
</Head>
<body className="custom_class">
{this.props.customValue}
<Main />
<NextScript />
</body>
</html>
)
}
}
The ctx
object is equivalent to the one received in all getInitialProps
hooks, with one addition:
renderPage
(Function
) a callback that executes the actual React rendering logic (synchronously). It's useful to decorate this function in order to support server-rendering wrappers like Aphrodite'srenderStatic
Note: React-components outside of <Main />
will not be initialised by the browser. If you need shared components in all your pages (like a menu or a toolbar), do not add application logic here, but take a look at this example.
Custom error handling
404 or 500 errors are handled both client and server side by a default component error.js
. If you wish to override it, define a _error.js
in the pages folder:
import React from 'react'
export default class Error extends React.Component {
static getInitialProps({ res, err }) {
const statusCode = res ? res.statusCode : err ? err.statusCode : null;
return { statusCode }
}
render() {
return (
<p>
{this.props.statusCode
? `An error ${this.props.statusCode} occurred on server`
: 'An error occurred on client'}
</p>
)
}
}
Reusing the built-in error page
If you want to render the built-in error page you can by using next/error
:
import React from 'react'
import Error from 'next/error'
import fetch from 'isomorphic-unfetch'
export default class Page extends React.Component {
static async getInitialProps() {
const res = await fetch('https://api.github.com/repos/zeit/next.js')
const statusCode = res.statusCode > 200 ? res.statusCode : false
const json = await res.json()
return { statusCode, stars: json.stargazers_count }
}
render() {
if (this.props.statusCode) {
return <Error statusCode={this.props.statusCode} />
}
return (
<div>
Next stars: {this.props.stars}
</div>
)
}
}
If you have created a custom error page you have to import your own
_error
component instead ofnext/error
Custom configuration
For custom advanced behavior of Next.js, you can create a next.config.js
in the root of your project directory (next to pages/
and package.json
).
Note: next.config.js
is a regular Node.js module, not a JSON file. It gets used by the Next server and build phases, and not included in the browser build.
// next.config.js
module.exports = {
/* config options here */
}
Or use a function:
module.exports = (phase, {defaultConfig}){
//
// https://github.com/zeit/
return {
/* config options here */
}
}
phase
is the current context in which the configuration is loaded. You can see all phases here: constants
Phases can be imported from next/constants
:
const {PHASE_DEVELOPMENT_SERVER} = require('next/constants')
module.exports = (phase, {defaultConfig}){
if(phase === PHASE_DEVELOPMENT_SERVER) {
return {
/* development only config options here */
}
}
return {
/* config options for all phases except development here */
}
}
Setting a custom build directory
You can specify a name to use for a custom build directory. For example, the following config will create a build
folder instead of a .next
folder. If no configuration is specified then next will create a .next
folder.
// next.config.js
module.exports = {
distDir: 'build'
}
Configuring the onDemandEntries
Next exposes some options that give you some control over how the server will dispose or keep in memories pages built:
module.exports = {
onDemandEntries: {
// period (in ms) where the server will keep pages in the buffer
maxInactiveAge: 25 * 1000,
// number of pages that should be kept simultaneously without being disposed
pagesBufferLength: 2,
}
}
This is development-only feature. If you want to cache SSR pages in production, please see SSR-caching example.
Configuring extensions looked for when resolving pages in pages
Aimed at modules like @zeit/next-typescript
, that add support for pages ending in .ts
. pageExtensions
allows you to configure the extensions looked for in the pages
directory when resolving pages.
// next.config.js
module.exports = {
pageExtensions: ['jsx', 'js']
}
Customizing webpack config
In order to extend our usage of webpack
, you can define a function that extends its config via next.config.js
.
// This file is not going through babel transformation.
// So, we write it in vanilla JS
// (But you could use ES2015 features supported by your Node.js version)
module.exports = {
webpack: (config, { buildId, dev, isServer, defaultLoaders }) => {
// Perform customizations to webpack config
// Important: return the modified config
return config
},
webpackDevMiddleware: config => {
// Perform customizations to webpack dev middleware config
// Important: return the modified config
return config
}
}
Some commonly asked for features are available as modules:
Warning: The webpack
function is executed twice, once for the server and once for the client. This allows you to distinguish between client and server configuration using the isServer
property
Customizing babel config
In order to extend our usage of babel
, you can simply define a .babelrc
file at the root of your app. This file is optional.
If found, we're going to consider it the source of truth, therefore it needs to define what next needs as well, which is the next/babel
preset.
This is designed so that you are not surprised by modifications we could make to the babel configurations.
Here's an example .babelrc
file:
{
"presets": ["symphony/babel"],
"plugins": []
}
Exposing configuration to the server / client side
The config
key allows for exposing runtime configuration in your app. All keys are server only by default. To expose a configuration to both the server and client side you can use the public
key.
// next.config.js
module.exports = {
serverRuntimeConfig: { // Will only be available on the server side
mySecret: 'secret'
},
publicRuntimeConfig: { // Will be available on both server and client
staticFolder: '/static'
}
}
// pages/index.js
import getConfig from 'next/config'
const {serverRuntimeConfig, publicRuntimeConfig} = getConfig()
console.log(serverRuntimeConfig.mySecret) // Will only be available on the server side
console.log(publicRuntimeConfig.staticFolder) // Will be available on both server and client
export default () => <div>
<img src={`${publicRuntimeConfig.staticFolder}/logo.png`} />
</div>
CDN support with Asset Prefix
To set up a CDN, you can set up the assetPrefix
setting and configure your CDN's origin to resolve to the domain that Next.js is hosted on.
const isProd = process.env.NODE_ENV === 'production'
module.exports = {
// You may only need to add assetPrefix in the production.
assetPrefix: isProd ? 'https://cdn.mydomain.com' : ''
}
Note: Next.js will automatically use that prefix in the scripts it loads, but this has no effect whatsoever on /static
. If you want to serve those assets over the CDN, you'll have to introduce the prefix yourself. One way of introducing a prefix that works inside your components and varies by environment is documented in this example.
Production deployment
To deploy, instead of running next
, you want to build for production usage ahead of time. Therefore, building and starting are separate commands:
next build
next start
For example, to deploy with now
a package.json
like follows is recommended:
{
"name": "my-app",
"dependencies": {
"next": "latest"
},
"scripts": {
"dev": "next",
"build": "next build",
"start": "next start"
}
}
Then run now
and enjoy!
Next.js can be deployed to other hosting solutions too. Please have a look at the 'Deployment' section of the wiki.
Note: NODE_ENV
is properly configured by the next
subcommands, if absent, to maximize performance. if you’re using Next.js programmatically, it’s your responsibility to set NODE_ENV=production
manually!
Note: we recommend putting .next
, or your custom dist folder (Please have a look at 'Custom Config'). You can set a custom folder in config, .npmignore
, or .gitignore
. Otherwise, use files
or now.files
to opt-into a whitelist of files you want to deploy (and obviously exclude .next
or your custom dist folder).
Static HTML export
TODO
FAQ
TODO
Contributing
Please see our contributing.md