Getting Started

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);
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);
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);
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'); });
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. Depending on how you plan to use Ink, you can also look at the following project setups.