Akte Logov0.4.0 - GitHub

Guide

How to use Akte to achieve what you want.

Defining files

Files are the central part of Akte. You can define two kinds of files.

In any case, for files to be taken into account by your app, you need to register them in your akte.app.ts file.

  import { defineAkteApp } from "akte";
+ import { foo } from "./foo"; // An Akte files

  export const app = defineAkteApp({
- 	files: [],
+ 	files: [foo],
  });

Single files

Define a single file using defineAkteFile.

import { defineAkteFile } from "akte";

const about = defineAkteFile().from({
	path: "/about",
	data(context) { /* Optional, see data section */ },
	render(context) {
		return /* html */ `<h1>About</h1>`;
	},
});

Collection of files

Define a collection of files using defineAkteFiles.

import { defineAkteFiles } from "akte";

const posts = defineAkteFiles().from({
	path: "/posts/:slug",
	data(context) { /* Optional, see data section */ },
	bulkData() {
		// Akte can't guess which paths you want to generate
		// for collection of files, so you need at least to
		// return a map of path to generate. More about
		// working with data below.
		return {
			"/posts/foo": {},
			"/posts/bar": {},
			"/posts/baz": {},
		};
	},
	render(context) {
		return /* html */ `<h1>${context.path}</h1>`;
	},
});

Routing

Akte uses the path property of your file definitions to handle routing and define which file to generate. Route patterns and matching algorithm are powered by radix3.

Static

Static routes are meant to be used with single files.

path valueRoute matchedFile generated
///index.html
/foo/foo/foo.html
/bar//bar//bar/index.html
/baz.json/baz.json/baz.json

Dynamic

Dynamic routes are meant to be used with collection of files.

path valueExample route matchedExample file generated
/:id/foo/foo.html
/:cat/:id/baz/baz/bar/baz.html
/posts/:slug/posts/qux/posts/qux.html
/api/:id.json/api/quux.json/api/quux.json

Typing path parameters

You can type path parameters on your Akte files when defining them. This will enforce correct path value and type the params object of the data function.

defineAkteFiles<GlobalDataType, ["cat", "id"]>().from({
	path: "/posts/:cat/:id",
	/* ... */
};

Catch-all

Catch-all routes are also available for collection of files.

path valueExample route matchedExample file generated
/**/foo/bar/baz/foo/bar/baz.html
/pages/**/pages/qux/quux/pages/qux/quux.html
/api/**.json/api/corge/grault.json/api/corge/grault.json

Catch-all routes root

Catch-all routes will match the root of the catch-all route, e.g. /** also matches / and /pages/** also matches /pages

Routing priority

In some cases, you can end up with multiple files matching the same route (e.g. / and /** both match /). When this happens, the priority is given to the file registered last within your Akte app configuration.

Working with data

Akte works with two kinds of data:

Global data

Global data are defined on your Akte app. It's a function or an asynchronous function that returns them.

import { defineAkteApp } from "akte";

export const app = defineAkteApp({
	files: [/* ... */],
	globalData() {
		// Fetch API, read files, etc.
		return globalData;
	},
});

Bulk data

Bulk data defines all the data needed to render all files defined with defineAkteFiles. It's a function or an asynchronous function that returns a map of path and file data.

import { defineAkteFiles } from "akte";

const posts = defineAkteFiles().from({
	path: "/posts/:slug",
	bulkData(context) {
		// Fetch API, read files, etc.
		return bulkData;
	},
	render(context) {
		return /* html */ `<h1>${context.path}</h1>`;
	},
});

Optimize for performances

While the bulkData function is at least necessary to define which files to render, it also exists to optimize build time. Most APIs allow you to query content in bulk, by doing so, less requests are made and build can be faster.

Context

bulkData functions all receive the same context argument, it contains:

Use it as needed to compute your bulk data appropriately.

Data

Data defines the data needed to render a file at a given path defined with defineAkteFile. It's a function or an asynchronous function that returns the file data.

import { defineAkteFile } from "akte";

const about = defineAkteFile().from({
	path: "/about",
	data(context) {
		// Fetch API, read files, etc.
		return data;
	},
	render(context) {
		return /* html */ `<h1>About</h1>`;
	},
});

Data with collection of files

The data function can optionally be provided for files defined with defineAkteFiles (by default it is inferred from the files bulkData function). This allows to optimize the file rendering process during single renders, e.g. rendering on serverless environment.

Context

data functions all receive the same context argument, it contains:

Use it as needed to compute your bulk data appropriately.

Rendering

Akte uses your file render functions to render them as strings. These functions can be asynchronous, however, it's recommended to keep async behaviors within data and bulkData.

Context

render functions all receive the same context argument, it contains:

Use it to render each of your files appropriately.

Templating

Akte templating is string-based. While this doesn't prevent you from pulling in a templating engine of your liking, ES6 template literals can get you quite far with à-la-JSX syntax.

// JSX
return (<section>
	<h1 className="foo">{ context.path }</h1>
</section>);

// Template literals
return /* html */ `<section>
	<h1 class="foo">${ context.path }</h1>
</section>`;

Getting a better experience

You can get HTML highlighting, autocompletion, IntelliSense, and more for template literals by leveraging extensions like es6-string-html (that's why you've been seeing those /* html */ comments)

Wrapping output

Because rendering is all JavaScript based, you can create your own functions to make templating easier. For example, you can wrap your output within a layout like so.

const wrapInLayout = (slot: string): string => {
	return /* html */ `<!doctype html>
<html lang="en">
	<head>
		<title>My App</title>
	</head>
	<body>
		<header>...</header>
		${slot}
		<footer>...</footer>
	</body>
</html>
`;
};

// Later in your rendering function...
return wrapInLayout(/* html */ `...`);

Or make some "components" of your own.

const header = (args: { displayLogo?: boolean; }): string => {
	return /* html */ `<header>
	${args.displayLogo ? `<img src="/logo.svg" alt="logo" />`: ""}
	<nav>
		<ul>
			<li><a href="/">Home</a></li>
			<li><a href="/posts">Blog</a></li>
			<li><a href="/about">About</a></li>
		</ul>
	</nav>
</header>`;
};

// Later in your rendering function...
return /* html */ `${header({ displayLogo: true })}...`;

Programmatic usage

Your Akte app can be imported and used programmatically. To do so, it exposes various methods you can use to achieve different goals, including serverless usage.

Looking up a path

You can know if a path is handled by your Akte app using the lookup method.

import { app } from "./akte.app";

try {
	const match = app.lookup("/foo");
} catch (error) {
	// Path is not handled...
}

Rendering a path

Pairing lookup with the render method allows you to render a path. The render method resolves global data and the file's data, caches them, then runs your file render function for the request path.

import { app } from "./akte.app";

try {
	const match = app.lookup("/foo");
	const file = await app.render(match);
} catch (error) {
	// Path is not handled...
}

Rendering all files

You can render all your app files with the renderAll method.

import { app } from "./akte.app";

// Record<path, content>
const files = await app.renderAll();

Writing all files

You can write a map of rendered files with the writeAll method.

import { app } from "./akte.app";

const files = await app.renderAll();
await app.writeAll({ files });

All files will, by default, be written to the app build.outDir directory which defaults to dist. When calling writeAll you can specify another directory.

import { app } from "./akte.app";

const files = await app.renderAll();
await app.writeAll({ files, outDir: "out" });

For convenience, the buildAll method is available. It renders and writes all files, then returns an array of files written.

import { app } from "./akte.app";

// Same effect as the previous example, `outDir` is optional
await app.buildAll({ files, outDir: "out" });

Clearing cache

Akte caches all globalData, bulkData, data calls for performance. The clearCache method allows you to clear these caches.

import { app } from "./akte.app";

// Clear global data cache
app.clearCache();

// Clear global data, data, and bulk data cache
app.clearCache(true);

Debugging

Akte reports on what it's doing using the debug package. Track performances and debug your app by setting the DEBUG environment variable before running Akte.

Akte CLI

npx cross-env DEBUG=akte:* node akte.app.js build
npx cross-env DEBUG=akte:* npx tsx akte.app.ts build

Vite

npx cross-env DEBUG=akte:* vite
npx cross-env DEBUG=akte:* vite build