To try out Ink, run the following commands in terminal:
npm init -y && npm install --save @stackpress/ink && npm install --save-dev ts-node typescript @types/node
Recommended:
Download the Ink editor plugin at the Visual Studio Marketplace.
Create a server file called
src/index.ts
with the following code that uses the compiler.
import ink from '@stackpress/ink/compiler';
// make a ink compiler
const compiler = ink();
// render HTML
compiler.render('./src/page.ink').then(console.log);
// render CSS
compiler.styles('./src/page.ink').then(console.log);
// render JS
compiler.client('./src/page.ink').then(console.log);
Last, create a document file called
src/page.ink
with the following template code.
<style>
.center { text-align: center; }
</style>
<script>
import { env } from '@stackpress/ink';
const { BUILD_ID, APP_DATA } = env();
const title = 'Hello World';
</script>
<html>
<head>
<title>{title}</title>
<link rel="stylesheet" type="text/css" href={`/build/${BUILD_ID}.css`} />
<script data-app={APP_DATA} src={`/build/${BUILD_ID}.js`}></script>
</head>
<body>
<h1 class="center">{title}</h1>
</body>
</html>
To try out the basic implementation of Ink and see the
results, just run the following command in terminal.
npx ts-node src/index.ts
1. Add HTTP
In most cases Ink will be used to render a front end from
a server framework. In this example, we will use the native
NodeJS HTTP module to create a server that renders a page
using Ink. Start by replacing the
'src/index.ts'
file with the following code.
Optional: You can also check your other
files to make sure you are following along.
src/index.ts
src/page.ink
package.json
src
index.ts
page.ink
package.json
import http from 'http';
import ink from '@stackpress/ink/compiler';
// create ink compiler
const compiler = ink();
// create http server
const server = http.createServer(async (req, res) => {
// if build asset...
if (req.url?.startsWith('/build/')) {
// get filename ie. abc123.js
const filename = req.url.substring(7);
// get asset
const { type, content } = await compiler.asset(filename);
// send response
res.writeHead(200, { 'Content-Type': type });
return res.end(content);
// if home page
} else if (req.url === '/') {
// render and send response
res.writeHead(200, { 'Content-Type': 'text/html' });
return res.end(await compiler.render('./src/page.ink', {
title: 'Hello World'
}));
}
});
// listen on port 3000
server.listen(3000);
<style>
.center { text-align: center; }
</style>
<script>
import { env, props } from '@stackpress/ink';
const { BUILD_ID, APP_DATA } = env();
const { title } = props();
</script>
<html>
<head>
<title>{title}</title>
<link rel="stylesheet" type="text/css" href={`/build/${BUILD_ID}.css`} />
<script data-app={APP_DATA} src={`/build/${BUILD_ID}.js`}></script>
</head>
<body>
<h1 class="center">{title}</h1>
</body>
</html>
{
"name": "my-project",
"version": "1.0.0",
"private": true,
"scripts": {
"dev": "ts-node ./src/index.ts"
},
"dependencies": {
"@stackpress/ink": "0.3.24"
},
"devDependencies": {
"@types/node": "22.1.0",
"ts-node": "10.9.2",
"typescript": "5.5.4"
}
}
To run your first Ink web app, just run the following
command in terminal.
npx ts-node src/index.ts
You can now check
http://localhost:3000/
in your browser to see your Ink application. The
ink()
function takes in the following options, all of
which are optional.
2. Add Developer Tools
Ink provides a separate package for a better development
experience when working with server frameworks that utilize
the native http module. Start by installing adding
@stackpress/ink-dev
to your project.
npm install --save-dev @stackpress/ink-dev
Next, import the dev()
function from the package and use it in your existing
src/index.ts
file to create a development server as shown in the example below.
// ...
import { dev } from '@stackpress/ink-dev';
// ...create ink compiler...
// 1. create dev tools
const { router, refresh } = dev();
const server = http.createServer(async (req, res) => {
// 2. Add dev router
if (router(req, res)) return;
if (req.url?.startsWith('/build/')) {
// ...
} else if (req.url === '/') {
// 3. sync builder with refresh server
refresh.sync(compiler.fromSource('./src/page.ink'));
// ... compile and send response ...
}
});
//...listen on port 3000...
The dev() export
from @stackpress/ink-dev
exports tools that supports development mode and accepts the
following options.
This returns several tools you can use in your server app.
Lastly, update the document file
src/page.ink
to include the development script
<script src="/dev.js"></script>
as shown below.
<style>
/* ... */
</style>
<script>
//...
</script>
<html>
<head>
<!-- ... -->
<!-- 4. include dev script -->
<script src="/dev.js"></script>
</head>
<body>
<!-- ... -->
</body>
</html>
The project should now look like the example below.
src/index.ts
src/page.ink
package.json
src
index.ts
page.ink
package.json
import http from 'http';
import ink from '@stackpress/ink/compiler';
import { dev } from '@stackpress/ink-dev';
const compiler = ink();
// 1. create dev tools
const { router, refresh } = dev();
const server = http.createServer(async (req, res) => {
// 2. Add dev router
if (router(req, res)) return;
if (req.url?.startsWith('/build/')) {
const filename = req.url.substring(7);
const { type, content } = await compiler.asset(filename);
res.writeHead(200, { 'Content-Type': type });
return res.end(content);
} else if (req.url === '/') {
// 3. sync builder with refresh server
refresh.sync(compiler.fromSource('./src/page.ink'));
res.writeHead(200, { 'Content-Type': 'text/html' });
return res.end(await compiler.render('./src/page.ink', {
title: 'Hello World'
}));
}
});
server.listen(3000);
<style>
.center { text-align: center; }
</style>
<script>
import { env, props } from '@stackpress/ink';
const { BUILD_ID, APP_DATA } = env();
const { title } = props();
</script>
<html>
<head>
<title>{title}</title>
<link rel="stylesheet" type="text/css" href={`/build/${BUILD_ID}.css`} />
<script data-app={APP_DATA} src={`/build/${BUILD_ID}.js`}></script>
<script src="/dev.js"></script>
</head>
<body>
<h1 class="center">{title}</h1>
</body>
</html>
{
"name": "my-project",
"version": "1.0.0",
"private": true,
"scripts": {
"dev": "ts-node ./src/index.ts"
},
"dependencies": {
"@stackpress/ink": "0.3.24"
},
"devDependencies": {
"@stackpress/ink-dev": "0.3.24",
"@types/node": "22.1.0",
"ts-node": "10.9.2",
"typescript": "5.5.4"
}
}
Re-run the following command in terminal. It shouldn't look
like anything has changed, but the development server is now
running in the background. Try to change
src/page.ink.
npx ts-node src/index.ts
Whenever src/page.ink
is saved, the development server will automatically refresh
the page. Components will also be updated in real-time without
the page reloading.
3. Add Cache Files
Ink has an out-of-the-box cache and build strategy that
can be used to store and serve pre-compiled files. To use the
cache, you just need to import it from the
@stackpress/ink/compiler
module and use it like the following example.
// ...
import path from 'path';
import { cache } from '@stackpress/ink/compiler';
// ...create ink compiler...
// 1. use cache
compiler.use(cache({
buildPath: path.join(__dirname, '../build')
}));
// ...create dev tools...
// ...create http server...
// ...listen on port 3000...
The src/index.ts
file should now look like the example below.
import path from 'path';
import http from 'http';
import ink, { cache } from '@stackpress/ink/compiler';
import { dev } from '@stackpress/ink-dev';
const compiler = ink();
// 1. use cache
compiler.use(cache({
buildPath: path.join(__dirname, '../build')
}));
const { router, refresh } = dev();
const server = http.createServer(async (req, res) => {
if (router(req, res)) return;
if (req.url?.startsWith('/build/')) {
const filename = req.url.substring(7);
const { type, content } = await compiler.asset(filename);
res.writeHead(200, { 'Content-Type': type });
return res.end(content);
} else if (req.url === '/') {
refresh.sync(compiler.fromSource('./src/page.ink'));
res.writeHead(200, { 'Content-Type': 'text/html' });
return res.end(await compiler.render('./src/page.ink', {
title: 'Hello World'
}));
}
});
server.listen(3000);
Re-run the following command in terminal to start the cache
server.
npx ts-node src/index.ts
Load
http://localhost:3000/
in your browser. After loading you should see files that were
generated in a new build folder found in your project root.
The cache() plugin is
just a wrapper that listens for build related events and
stores the generated files in the specified build path.
emitter.on('manifest-resolved', (event: Event<string>) => {
const manifest = event.params.manifest as Manifest
//write the manifest to the file system
writeFile(paths.manifest, manifest.toJson());
});
// on pre render, try to use cache if live
emitter.on('render', (event: Event<string>) => {
//if not live, dont retrieve from cache
if (environment !== 'production') return;
//extract props and builder from params
const props = (event.params.props || {}) as Hash;
const builder = event.params.builder as Builder;
//get fs and id ie. abc123c
const { fs, id } = builder.document;
//get cache file path ie. /path/to/docs/build/client/abc123c.js
const cache = path.join(paths.build, 'server', `${id}.js`);
//if production and cache file exists
if (fs.existsSync(cache)) {
//get the build object
const build = compiler.fromCache(cache);
//render the document
const html = build.document.render(props);
//return the cached content
event.set(html);
}
});
// on post render, cache (dev and live)
emitter.on('rendered', (event: Event<string>) => {
//extract build and builder from params
const builder = event.params.builder as Builder;
const html = event.params.html as string;
//get fs and id ie. abc123c
const { id } = builder.document;
//get cache file path ie. /path/to/docs/build/client/abc123c.html
const cache = path.join(paths.build, 'client', `${id}.html`);
//write the server source code to cache
writeFile(cache, html);
});
// on pre client build, try to use cache if live
emitter.on('build-client', (event: Event<string>) => {
//if not live, dont retrieve from cache
if (environment !== 'production') return;
//extract builder from params
const builder = event.params.builder as Builder;
//get fs and id ie. abc123c
const id = builder.document.id;
//get cache file path ie. /path/to/docs/build/client/abc123c.js
const cache = path.join(paths.build, 'client', `${id}.js`);
//if cache file exists, send it
if (fs.existsSync(cache)) {
event.set(fs.readFileSync(cache, 'utf8'));
}
});
// on post client build, cache (dev and live)
emitter.on('built-client', (event: Event<string>) => {
//extract builder and sourcecode from params
const builder = event.params.builder as Builder;
const sourceCode = event.params.sourceCode as string;
//get fs and id ie. abc123c
const id = builder.document.id;
//get cache file path ie. /path/to/docs/build/client/abc123c.js
const cache = path.join(paths.build, 'client', `${id}.js`);
//write the client source code to cache
writeFile(cache, sourceCode);
});
// on pre markup build, try to use cache if live
emitter.on('build-markup', /* ... */);
//on post markup build, cache (dev and live)
emitter.on('built-markup', /* ... */);
//on pre server build, try to use cache if live
emitter.on('build-server', /* ... */);
//on post server build, cache (dev and live)
emitter.on('built-server', /* ... */);
//on pre styles build, try to use cache if live
emitter.on('build-styles', /* ... */);
//on post styles build, cache (dev and live)
emitter.on('built-styles', /* ... */);
// Initialize: if there's a manifest
if (fs.existsSync(paths.manifest)) {
//load the manifest file
compiler.manifest.load(
JSON.parse(fs.readFileSync(paths.manifest, 'utf-8'))
);
}
This means you can also use your own cache strategy by
listening to the events emitted by the compiler. The
following table lists all the events that the compiler
emits during the build cycle of a document.
4. Add TailwindCSS
Tailwind is an atomic CSS collection of styles that favours
small, single-purpose classes with their selector names based
on its visual function. It works by using a build process to
read your source files to generate its styles based only on
what is being used. This makes using Tailwind optimal because
it doesn't bloat your CSS with unused styles.
At the same time, web components with the
<style> tag imply using the
component's shadow DOM which will encapsulate the styles within
the component and not be affected by global styles. Since
Tailwind in turn implies that you do not need to (necessarily)
define styles, you do not need to use the shadow DOM at all if
you are using Tailwind.
Warning:
The caveat for using TailwindCSS, means that web components
using it will not be shippable to other projects that do not
use Tailwind. It all comes down to preference in the end.
Ink has a separate package called
@stackpress/ink-tailwind
to use TailwindCSS with Ink. This is just another wrapper
class that listens to the compiler's build events. You can
install this plugin by running the following command in terminal.
npm install --save-dev @stackpress/ink-tailwind autoprefixer postcss tailwindcss
Next, in src/index.ts
import the tailwind()
plugin from the package and use it in the compiler as shown
in the example below.
// ...
import { tailwind } from '@stackpress/ink-tailwind';
// ...create ink compiler...
// ...use cache...
// 1. Use Tailwind
compiler.use(tailwind({
darkMode: 'class',
theme: { extend: {} },
plugins: [],
content: []
}));
// ...create dev tools...
// ...create http server...
// ...listen on port 3000...
Lastly, in src/page.ink
add the Tailwind directives inside the
<style> tag like the code
below. Also add a tailwind class, (like
<style>) to the
markup to verify that the plugin is working and the styles
are being applied.
<style>
/* 2. Add tailwind directives */
@tailwind base;
@tailwind components;
@tailwind utilities;
/* ...Other styles... */
</style>
<script>
//...
</script>
<html>
<head>
<!-- ... -->
</head>
<body>
<h1 class="text-center">{title}</h1>
</body>
</html>
Check to see if the project files look like the example below.
src/index.ts
src/page.ink
package.json
src
index.ts
page.ink
package.json
import path from 'path';
import http from 'http';
import ink, { cache } from '@stackpress/ink/compiler';
import { dev } from '@stackpress/ink-dev';
import { tailwind } from '@stackpress/ink-tailwind';
const compiler = ink();
compiler.use(cache({
buildPath: path.join(__dirname, '../build')
}));
// 1. use tailwind
compiler.use(tailwind({
darkMode: 'class',
theme: { extend: {} },
plugins: [],
content: []
}));
const { router, refresh } = dev();
const server = http.createServer(async (req, res) => {
if (router(req, res)) return;
if (req.url?.startsWith('/build/')) {
const filename = req.url.substring(7);
const { type, content } = await compiler.asset(filename);
res.writeHead(200, { 'Content-Type': type });
return res.end(content);
} else if (req.url === '/') {
refresh.sync(compiler.fromSource('./src/page.ink'));
res.writeHead(200, { 'Content-Type': 'text/html' });
return res.end(await compiler.render('./src/page.ink', {
title: 'Hello World'
}));
}
});
server.listen(3000);
<style>
/* 2. Add tailwind directives */
@tailwind base;
@tailwind components;
@tailwind utilities;
</style>
<script>
import { env, props } from '@stackpress/ink';
const { BUILD_ID, APP_DATA } = env();
const { title } = props();
</script>
<html>
<head>
<title>{title}</title>
<link rel="stylesheet" type="text/css" href={`/build/${BUILD_ID}.css`} />
<script data-app={APP_DATA} src={`/build/${BUILD_ID}.js`}></script>
<script src="/dev.js"></script>
</head>
<body>
<h1 class="text-center">{title}</h1>
</body>
</html>
{
"name": "my-project",
"version": "1.0.0",
"private": true,
"scripts": {
"dev": "ts-node ./src/index.ts"
},
"dependencies": {
"@stackpress/ink": "0.3.24"
},
"devDependencies": {
"@stackpress/ink-dev": "0.3.24",
"@stackpress/ink-tailwind": "0.3.24",
"@types/node": "22.1.0",
"autoprefixer": "10.4.20",
"postcss": "8.4.44",
"tailwindcss": "3.4.10",
"ts-node": "10.9.2",
"typescript": "5.5.4"
}
}
Re-run the following command in terminal to initialize the
tailwind plugin.
npx ts-node src/index.ts
Load
http://localhost:3000/
in your browser. After loading you should see files that were
generated in a new build folder found in your project root.
Try to add a Tailwind class to the markup in
src/page.ink and
save. The development server will automatically refresh
the styles and component styles will also be update in
real-time without the page reloading.
5. Add ExpressJS
Ink has a separate package called
@stackpress/ink-express
to use Express with Ink. You can install this plugin by
running the following command in terminal.
npm install --save @stackpress/ink-express express && npm install --save-dev @types/express
The package
@stackpress/ink-express
exports two plugins for express.
view() is the view
engine for production (live) environments. It can be used with
an express app like
app.use(view(compiler)).
The other export, dev()
is the same export from the Developer Tools documentation above,
but returns several tools used to integrate with express.
Example logic to use the all the Ink Express tools together
with Ink developer tools could look like the following code
that cases for
development and
production modes.
import { view, dev } from '@stackpress/ink-express';
//create ink compiler
const compiler = ink({ cwd: __dirname, minify: false });
//create express app
const app = express();
//set the view engine to ink
app.set('views', path.join(__dirname, 'pages'));
app.set('view engine', 'ink');
//if production (live)
if (process.env.NODE_ENV === 'production') {
//let's use express' template engine feature
app.engine('ink', view(compiler));
//...other production settings...
//if development mode
} else {
//get development middleware
const { router, view } = dev({ cwd: __dirname });
//use development middleware
app.use(router);
//let's use express' template engine feature
app.engine('ink', view(compiler));
}
And you can now case for development mode in
src/page.ink
like in the example below
<style>
/* ... */
</style>
<script>
import { env } from '@stackpress/ink';
const { NODE_ENV } = env();
</script>
<html>
<head>
<!-- ... -->
<if true={NODE_ENV !== 'production'}>
<script src="/dev.js"></script>
</if>
</head>
<body>
<!-- ... -->
</body>
</html>
Check to see if the project files look like the example below.
src/index.ts
src/page.ink
package.json
src
index.ts
page.ink
package.json
import path from 'path';
import express from 'express';
import ink, { cache } from '@stackpress/ink/compiler';
import { view, dev } from '@stackpress/ink-express';
import { tailwind } from '@stackpress/ink-tailwind';
//create ink compiler
const compiler = ink();
//use tailwind
compiler.use(tailwind({
darkMode: 'class',
theme: { extend: {} },
plugins: [],
content: []
}));
//use build cache
compiler.use(cache({
environment: process.env.NODE_ENV,
buildPath: path.join(__dirname, '../build')
}));
//create express app
const app = express();
//set the view engine to ink
app.set('views', __dirname);
app.set('view engine', 'ink');
//if production (live)
if (process.env.NODE_ENV === 'production') {
//let's use express' template engine feature
app.engine('ink', view(compiler));
//...other production settings...
//if development mode
} else {
//get development middleware
const { router, view } = dev({ cwd: __dirname });
//use development middleware
app.use(router);
//let's use express' template engine feature
app.engine('ink', view(compiler));
}
//routes
app.get('/build/:build', async (req, res) => {
//get filename ie. abc123.js
const filename = req.params.build;
//get asset
const { type, content } = await compiler.asset(filename);
//send response
res.type(type).send(content);
});
app.get('/', (req, res) => {
//now use the ink template engine
res.render('page', { title: 'Hello World' });
res.type('text/html');
});
//listen
app.listen(3000, () => {
console.log('HTTP server is running on http://localhost:3000');
});
<style>
@tailwind base;
@tailwind components;
@tailwind utilities;
</style>
<script>
import { env, props } from '@stackpress/ink';
const { BUILD_ID, APP_DATA, NODE_ENV } = env();
const { title } = props();
</script>
<html>
<head>
<title>{title}</title>
<link rel="stylesheet" type="text/css" href={`/build/${BUILD_ID}.css`} />
<script data-app={APP_DATA} src={`/build/${BUILD_ID}.js`}></script>
<if true={NODE_ENV !== 'production'}>
<script src="/dev.js"></script>
</if>
</head>
<body>
<h1 class="text-center">{title}</h1>
</body>
</html>
{
"name": "my-project",
"version": "1.0.0",
"private": true,
"scripts": {
"dev": "ts-node ./src/index.ts"
},
"dependencies": {
"@stackpress/ink": "^0.1.8",
"@stackpress/ink-express": "^0.1.8",
"express": "^4.19.2"
},
"devDependencies": {
"@stackpress/ink-dev": "^0.1.8",
"@stackpress/ink-tailwind": "^0.1.8",
"@types/express": "^4.17.21",
"@types/node": "^22.5.3",
"autoprefixer": "^10.4.20",
"postcss": "^8.4.45",
"tailwindcss": "^3.4.10",
"ts-node": "^10.9.2",
"typescript": "^5.5.4"
}
}
Re-run the following command in terminal to initialize the
re-run your application using Express.
npx ts-node src/index.ts
Load
http://localhost:3000/
in your browser. After loading you should see everything is
exactly as it was, but you now benefit from using ExpressJS.
-- Read On --
To see other getting started examples with various frameworks,
you can check out the following project examples in the
official repository.