---
title: Layouts
description: Learn how to use layouts in Sapling to create consistent page structures
publishedAt: "2024-01-01"
---
# Layouts
Layouts in Sapling provide a way to create consistent page structures and share common elements across multiple pages.
## Basic Layout
A basic layout in Sapling is a TypeScript function that wraps your page content. Here's a simple example:
```typescript
import { Layout as SaplingLayout, html, type LayoutProps } from "@sapling/sapling";
export type BaseLayoutProps = LayoutProps & {
title?: string;
description?: string;
};
export default async function Layout(props: BaseLayoutProps) {
return await SaplingLayout({
head: html`
${props.title}
`,
children: html`
${props.children}
`
});
}
```
## Layout Properties
Layouts accept properties that extend Sapling's base `LayoutProps`:
```typescript
type LayoutProps = {
head?: unknown; // Additional head content
bodyClass?: string; // Classes to apply to the body
children?: unknown; // Page content
unoConfig?: unknown; // UnoCSS configuration
};
```
## Using Components in Layouts
Layouts can import and use components to create consistent page structures:
```typescript
import { Nav } from "../components/Nav.ts";
import { Footer } from "../components/Footer.ts";
export default async function Layout(props: BaseLayoutProps) {
return await SaplingLayout({
// ... other props
children: html`
${Nav()}
${props.children}
${Footer()}
`
});
}
```
## Styling with UnoCSS
Sapling layouts can include UnoCSS configuration for consistent styling:
```typescript
import { config } from "../uno.config.ts";
export default async function Layout(props: BaseLayoutProps) {
return await SaplingLayout({
unoConfig: config,
bodyClass: `font-sans @dark:bg-black @dark:text-white ${props.bodyClass ?? ''}`,
// ... other props
});
}
```
## Nested Layouts
You can create specialized layouts that extend your base layout:
```typescript
import BaseLayout from "./Layout.ts";
export default async function DocsLayout(props: DocsLayoutProps) {
return await BaseLayout({
...props,
children: html`
${props.children}
`
});
}
```
## Using Layouts in Pages
To use a layout in a page component:
```typescript
import Layout from "../layouts/Layout.ts";
export default async function HomePage() {
return await Layout({
title: "Home - My Site",
description: "Welcome to my website",
children: html`
Welcome!
`
});
}
```
## Best Practices
1. **Type Safety**: Define proper TypeScript interfaces for your layout props
2. **Modularity**: Break down layouts into reusable components
3. **Flexibility**: Make layouts adaptable with optional props
4. **Dark Mode**: Include dark mode support using the `@dark:` prefix
5. **Performance**: Keep layouts lightweight and avoid unnecessary nesting
---
title: Prerendering
description: Learn how to prerender routes in Sapling for static site generation
publishedAt: "2024-01-01"
---
# Prerendering
Sapling is primarily a server-side rendering framework, but it also supports prerendering routes for static site generation when needed. This guide will explain how prerendering works and when to use it.
## Understanding Prerendering
By default, Sapling renders pages on-demand when requests come in. This is fast and efficient for most use cases. However, there are scenarios where you might want to generate static HTML files during build time:
- Pages with content that rarely changes
- Improving initial page load for specific routes
- Reducing server load for high-traffic pages
## Basic Usage
Here's a simple example of prerendering a route:
```typescript
// Define a prerendered route
site.prerender("/about", (c) => {
return c.html("
About Us
");
});
// Prerender a route with dynamic parameters
site.prerender("/blog/:slug", async (c) => {
const post = await getPost(c.req.param("slug"));
return c.html(`
${post.title}
${post.content}`);
}, [
{ slug: "first-post" },
{ slug: "second-post" }
]);
```
## Development vs Production
In development mode (`dev: true`), prerendered routes behave like normal `.get` routes for easier development. In production, these routes are generated as static HTML files during the build process.
## Build Process
To generate prerendered pages, you'll need to set up a build process. Create a `build.ts` or `build.js` file:
```typescript
// build.ts
import { site } from "./index.ts";
async function build() {
// Generate prerendered pages in the dist directory
await site.buildPrerenderRoutes("./dist");
// Run any other build processes
// await optimizeImages();
// await generateSearchIndex();
// await generateSitemap();
console.log("Build completed");
}
build();
```
Add a build script to your `deno.json`:
```json
{
"tasks": {
"build": "deno run -A build.ts"
}
}
```
Or add a build script to your `package.json`:
```json
"scripts": {
"build": "node build.js"
}
```
## Serving Prerendered Pages
To serve the prerendered pages, configure your static file serving in your main application:
```typescript
// index.ts
const site = new Sapling({
dev: isDev,
buildDir: "./dist", // Default build directory
});
// Add this as your last route
site.get("/*", serveStatic({
root: "./dist",
}));
```
## Best Practices
1. **Be Selective**: Only prerender routes that truly benefit from it. Most dynamic content is better served through server-side rendering.
2. **Development Experience**: Keep in mind that prerendered routes act as normal routes in development, allowing for faster iteration.
3. **Build Performance**: Consider the build time impact when prerendering many pages. Each prerendered route adds to your build time.
4. **Data Updates**: Remember that prerendered content is static. If your content needs to be frequently updated, server-side rendering might be more appropriate.
## Example: Blog with Prerendered Posts
Here's a complete example of prerendering blog posts:
```typescript
// index.ts
const site = new Sapling({
dev: isDev,
buildDir: "./dist",
});
// Get list of blog posts
const posts = [
{ slug: "hello-world", title: "Hello World" },
{ slug: "getting-started", title: "Getting Started" }
];
// Prerender each blog post
site.prerender("/blog/:slug", async (c) => {
const slug = c.req.param("slug");
const post = posts.find(p => p.slug === slug);
return c.html(`
${post.title}
Blog content here...
`);
}, posts.map(post => ({ slug: post.slug })));
// build.ts
import { site } from "./index.ts";
async function build() {
await site.buildPrerenderRoutes("./dist");
console.log("Blog posts prerendered!");
}
```
## When to Use Prerendering
Prerendering is ideal for:
- High traffic pages that are static and don't change often
- Documentation pages
- Marketing pages
- Blog posts
- Landing pages
Server-side rendering is better for:
- User-specific content
- Frequently updated data
- Dynamic applications
- Interactive features
Remember: Sapling's server-side rendering is already optimized for performance. Only reach for prerendering when you have a specific need for static file generation.
---
title: Acknowledgements
description: Special thanks to the projects and people that make Sapling possible
publishedAt: "2024-01-01"
---
# Acknowledgements
Sapling stands on the shoulders of giants. We'd like to acknowledge and thank the following projects and communities that make Sapling possible:
## Core Technologies
- [Hono](https://github.com/honojs/hono) - Our HTML helpers (`html` and `raw`) are based on Hono's `html` and `raw` methods. We're also huge fans of their routing API which is why Sapling will feel familiar to anyone who's used Hono.
- [UnoCSS](https://github.com/unocss/unocss) - We use UnoCSS under the hood for atomic CSS.
- [marked](https://github.com/markedjs/marked) - Our markdown parser.
- [shiki](https://github.com/shikijs/shiki) - The code highlighter for syntax highlighting.
## Inspiration
Here are some projects that have inspired us:
- [Astro](https://astro.build) - Astro's approach to building modern websites with zero JavaScript by default was a core concept that we wanted to be central to Sapling.
- [Hono](https://hono.dev) - Hono's routing and middleware system was a big inspiration for Sapling.
- [Fresh](https://fresh.deno.dev) - Fresh's SSR-first, no build by default approach was a huge reason why Sapling is structured the way it is.
- [11ty](https://www.11ty.dev) - 11ty's approach to not tie the framework to the bundler was a big inspiration for Sapling.
- Our [``](/docs/sapling-island) component is heavily inspired by Astro's islands implementation and 11ty's [is-land](https://github.com/11ty/is-land) web component.
- [Svelte Docs](https://svelte.dev/docs) - Thanks Svelte for a good docs page layout.
Each of these projects are amazing in their own right and we're so thankful they exist as fully open source projects that we can all learn from.
## Runtime Support
- **[Deno](https://deno.land)**
- **[Node.js](https://nodejs.org)**
- **[Bun](https://bun.sh)**:
- **[Cloudflare Workers](https://workers.cloudflare.com)**
## Community
We're grateful to all the developers and users who have contributed to Sapling through:
- Bug reports and fixes
- Feature suggestions
- Documentation improvements
- Community support
## License
Sapling is open source software licensed under the MIT license. We believe in the power of open source and are committed to keeping Sapling free and open for everyone.
---
title: Quick Start (Node.js)
description: Get started with Sapling using Node.js
publishedAt: "2024-01-01"
---
# Quick Start with Node.js
This guide will help you create your first Sapling website using Node.js.
## Prerequisites
- [Node.js](https://nodejs.org) (version 18 or higher)
- npm (comes with Node.js)
## Create a New Project
```bash
npm create sapling@latest
```
Select the "Basics" template.
## Start the Development Server
```bash
npm run dev
```
Your site is now running at [http://localhost:8080](http://localhost:8080)!
## Project Structure
```bash
my-website/
├── components/ # Reusable UI components
├── layouts/ # Page layouts
├── pages/ # Your pages/routes
├── static/ # Static assets
├── package.json # Project configuration
└── index.ts # Entry point
```
## Create Your First Page
1. Create a new file `pages/about.ts`:
```typescript
import Layout from "../layouts/Layout.ts";
import { html } from "@sapling/sapling";
export default async function About() {
return await Layout({
children: html`
`,
});
}
```
2. Add the route in `index.ts` underneath the `Home` route:
```typescript
site.get("/about", async (c) => c.html(await About()));
```
Visit [http://localhost:8080/about](http://localhost:8080/about) to see your new page!
## Next Steps
- Learn about [Routing](/docs/routing)
- Explore [Layouts](/docs/layouts)
- Check out the [Examples](https://github.com/saplingjs/examples)
---
title: VS Code Extensions
description: Recommended VS Code extensions for Sapling development
publishedAt: "2024-03-21"
---
# VS Code Extensions
To enhance your development experience with Sapling, we recommend installing the following VS Code extensions:
## Essential Extensions
### 1. UnoCSS
[UnoCSS](https://marketplace.cursorapi.com/items?itemName=antfu.unocss)
This extension provides intelligent autocomplete and hover previews for UnoCSS classes. Since Sapling uses UnoCSS for styling, this extension will greatly improve your development experience by:
- Providing intelligent class name suggestions
- Showing hover previews of the generated CSS
- Supporting custom rules and configurations
- Offering quick completions for utility classes
### 2. Lit Plugin
[Lit Plugin](https://marketplace.cursorapi.com/items?itemName=runem.lit-plugin)
While Sapling doesn't use Lit directly, this extension provides excellent support for HTML template literals, which are a core part of Sapling's templating system. Key features include:
- Syntax highlighting for HTML template literals
- Type checking in template expressions
- Auto-completion for HTML tags and attributes
- Quick info on hover
- Go to definition support
## Installation
The easiest way to install these extensions is to add them to your workspace recommendations. Create or edit `.vscode/extensions.json` in your project root with the following content:
```json
{
"recommendations": [
"antfu.unocss",
"runem.lit-plugin"
]
}
```
VS Code will automatically prompt you to install these recommended extensions when opening the project for the first time.
## Additional Benefits
These extensions work together to provide a seamless development experience:
- **Improved Code Intelligence**: Get better type checking and autocompletion for both your HTML templates and CSS classes
- **Enhanced Productivity**: Save time with intelligent suggestions and quick documentation access
- **Better Error Detection**: Catch potential issues early with built-in type checking and validation
- **Modern Development**: Full support for the modern development features that Sapling provides
---
title: Why Sapling?
description: Learn why Sapling is the ideal choice for modern web development
publishedAt: "2024-01-01"
---
# Why Sapling?
Sapling was created with a simple goal: to make building modern server-side rendered (SSR) websites as straightforward as possible. While web development has grown increasingly complex, we believe that creating a website shouldn't require learning complex build tools or managing heavy client-side JavaScript.
## Core Principles
### 1. Zero JavaScript by Default
Sapling takes a HTML-first approach. Your pages are server-rendered and work without JavaScript enabled. This means:
- Better performance
- Improved accessibility
- Enhanced SEO
- Lower bandwidth usage
Add JavaScript only when you need it, not because your framework requires it.
### 2. No Build Step Required
Unlike many modern frameworks, Sapling doesn't require a build step in development:
- Start coding immediately
- No bundler configuration
- Native TypeScript support
### 3. Multi-Runtime Support
Sapling is designed to work across different JavaScript runtimes:
- **Deno**: For modern, secure applications
- **Node.js**: For traditional Node.js environments
- **Bun**: For maximum performance
Choose the runtime that best fits your needs without changing your code.
### 4. Built-in "Tailwind"
Sapling uses [UnoCSS](https://github.com/unocss/unocss) for CSS, which means you can use just about any Tailwind classes and they will just work. We even include the Tailwind reset styles by default so it feels like you're using Tailwind out of the box.
#### Why UnoCSS instead of Tailwind?
The reason we don't use Tailwind directly is because Tailwind relies on configuration files and a build step. Whereas, [UnoCSS](https://unocss.dev) can generate the classes at runtime or on a Layout request in our case. There are definitely tradeoffs to both approaches, but we believe UnoCSS is the better, faster choice for Sapling.
### 5. Simple but Powerful
Sapling provides powerful features without complexity:
- Type-safe templating
- Type-safe routing
- Built-in atomic CSS support with UnoCSS
- Markdown processing
- Layout system
## When to Use Sapling
Sapling is ideal for:
- **Content-focused websites**: Blogs, documentation, marketing sites
- **Server-rendered applications**: Where SEO and initial load performance are crucial
- **Multi-page applications**: Traditional website structures with multiple pages
- **API-driven websites**: You want to serve a few webpages from your API instead of setting up an entirely new frontend project
## When to Consider Alternatives
Sapling might not be the best choice for:
- Single-page applications (SPAs) with complex client-side state
- Applications requiring real-time updates
- Projects that need extensive client-side JavaScript
## Performance Benefits
### Server-Side Rendering
- Faster Time to First Byte (TTFB)
- Better Core Web Vitals
- Reduced client-side processing
### Minimal JavaScript
- Smaller bundle sizes
- Less parsing and execution time
- Reduced memory usage
### Efficient Asset Handling
- Built-in static file serving
- Automatic HTTP caching headers
- Optimized asset loading
## Developer Experience
Sapling prioritizes developer experience:
```typescript
// Simple component creation
function Greeting() {
return html`
Hello, World!
`;
}
// Easy routing
site.get("/", async (c) => c.html(await Home()));
// Straightforward layouts
export default async function Layout(props: LayoutProps) {
return await SaplingLayout({
title: "My Site",
children: props.children
});
}
```
## Modern Features
Despite its simplicity, Sapling includes modern features:
- **TypeScript First**: Full type safety out of the box
- **UnoCSS Integration**: Modern atomic CSS
- **Dark Mode Support**: Built-in dark mode utilities
- **API Routes**: Easy backend functionality
- **Markdown Support**: A first party markdown to HTML implementation with syntax highlighting
## Inspiration
See our [acknowledgements](/docs/acknowledgements) to learn more about the projects that inspired Sapling.
## Community and Ecosystem
Sapling benefits from:
- Regular updates and improvements
- Growing ecosystem of plugins and tools
- Comprehensive documentation
## Getting Started
Ready to try Sapling? Check out our Quick Start guides:
- [Quick Start (Deno)](/docs/quick-start-deno)
- [Quick Start (Node.js)](/docs/quick-start-node)
---
title: Site Modules
description: Learn how to create and use site modules in Sapling
publishedAt: "2024-01-01"
---
# Site Modules
Site modules are a powerful feature in Sapling that allows you to package and distribute entire websites as reusable modules. This approach enables a clear separation between the site's structure/functionality and its content, making it easier to create templated solutions that others can easily customize.
Another great use case for site modules is for web design agencies to create templates that clients can easily customize but the agency can maintain the underlying structure and components under one single module and codebase.
## How Site Modules Work
A site module typically contains all the components, layouts, and routing logic of your website, while allowing the end user to simply provide the content. This is particularly useful for creating reusable website templates, documentation sites, a reusable APIs, or any other type of website where you want to maintain a consistent structure but allow for custom content.
## Creating a Module
To create a site module, you'll need to:
1. Build your site structure with Sapling
2. Export the necessary configuration functions
3. Publish your module to a registry (like JSR)
Here's a basic example of how to structure a site module:
```ts
import { Sapling, Layout, html, type HtmlContent } from "@sapling/sapling";
export const site: Sapling = new Sapling();
// Define the content types for the site module
export interface LandingLayoutContent {
siteTitle: string;
hero: {
title: string;
subtitle: string;
}
}
// Add a configuration option for the content.
// This could also be something like the configuration for a CMS.
// That way the end user only has to provide an API key.
let siteContent: LandingLayoutContent;
export function configureSite(content: LandingLayoutContent) {
siteContent = content;
}
site.get("/", async (c) => {
if (!siteContent) {
throw new Error("Site content not configured. Call configureSite() before starting the server.");
}
return c.html(
await Layout({
unoConfig: config,
head: html`
${siteContent.siteTitle}
`,
bodyClass: "text-gray-900 font-sans min-h-screen @dark:bg-gray-900 @dark:text-white",
children: html`
${siteContent.hero.title}
${siteContent.hero.subtitle}
`,
}));
});
```
## Publishing a Module
Technically, you could publish your site module to any registry that supports ESM modules. However, we recommend using [JSR](https://jsr.io) because it's free, easy, and has a much better developer experience than NPM.
Follow the documentation from [JSR](https://jsr.io/docs/introduction) to publish your site module. However, it should be pretty much as simple as running `deno publish` from the root of your Sapling site.
## Using a Module
And then the end user can import and configure the site module like this:
```ts
import { site, configureSite } from "jsr:@sapling/site-0";
const content = {
siteTitle: "My Site",
hero: {
title: "Hello, world!",
subtitle: "Welcome to my site.",
},
};
// Configure the site with the content
configureSite(content);
Deno.serve(site.fetch);
```
## Caveats
### Static Files
Static files you are serving from a `static` directory on your Sapling site will not be included in the site module. There are three ways to work around this:
1. Host your files elsewhere such as in a CMS, CDN, etc. And then just use the URLs to the files directly in your site module. **(recommended)**
2. Expect your end users to provide their own URLs for static files. This would be if you don't want to incur the cost of hosting the files yourself.
3. Provide the source files to your end users for them to host themselves which is obviously the most cumbersome but still works.
This is a null point if your site module is primarily designed to hook into a specific CMS or other system because the CMS is likely handling all of the file hosting.
### No Prerendering
Since a site module is sort of designed around the paradigm of no build step, it doesn't support prerendering. This goes hand in hand with the above static files caveat. You could still provide the prerendered HTML to your end users if you'd like.
## Conclusion
And yes, that's really all there is to it. Your site module can be as complex or as simple as you'd like. And because of TypeScript, it's fully typed and you'll have autocompletion for the content you provide.
---
title: Cursor Rules
description: A custom .cursorrules.txt file for Cursor's AI
publishedAt: "2025-01-10"
---
# Cursor Rules
Below is a custom .cursorrules file you can add to your Sapling project. Place this text in a file named `.cursorrules` in the root of your project and it will be used by [Cursor](https://cursor.com) whenever you are using AI.
```txt
You are an expert AI assistant and exceptional senior software developer, specializing in TypeScript with deep knowledge of the Sapling framework, and best practices.
Sapling is a framework for building web applications. It is a server-side framework that allows you to build web applications in a way that is similar to how you would build a static site.
Here are some of the best practices for using Sapling:
1. HTML Templating:
- Use html\` \` template literals for safe HTML rendering
- Use raw() for pre-rendered HTML content
- Support for conditional and list rendering
- HTML template literals automatically join arrays of strings, so there is no need to use .join("")
- Default to Tailwind CSS classes for styling unless it makes more sense to be CSS in a style tag.
- If you need to use CSS, place it in the head: property of the Sapling Layout component
- Use the @dark: prefix for dark mode
- ALWAYS be sure to escape any backticks inside of html\`\` template literals
- IMPORTANT: When using html\`\` template literals inside of
\`,
// the bodyClass property is strictly a string for the class attribute of the body tag
bodyClass: "bg-white text-gray-900 @dark:bg-black @dark:text-white transition-colors duration-200",
// the children property is for all of the content to be rendered inside the body tag
children: html\`
\${HelloWorldComponent()}
This was server rendered at \${time}
Learn More
\`,
})
);
});
Deno.serve(site.fetch);
Documentation:
- https://sapling.land/docs/llms.txt
- https://sapling.land/docs
```
---
title: LLMs Reference File
description: Learn about Sapling's LLMs reference file for AI assistants
publishedAt: "2024-01-01"
---
# LLMs Reference File
Sapling provides a comprehensive reference file for AI language models at https://sapling.land/docs/llms.txt. This file contains the entire documentation in a single, easily accessible format that AI assistants can use to better understand and work with Sapling.
## Purpose
The LLMs reference file serves several purposes:
- Provides AI assistants with complete documentation in one file
- Enables better context and understanding of Sapling's features
- Helps AI tools provide more accurate assistance
- Ensures consistent information across different AI platforms
## Accessing the File
The LLMs reference file is available at:
```txt
https://sapling.land/docs/llms.txt
```
Or download it below:
Download llms.txt
## Contents
The file includes all documentation sections:
- Getting Started guides
- Core Concepts
- API References
- Best Practices
- Examples
- FAQs
## Generation
The LLMs reference file is automatically generated from the documentation markdown files. This ensures it stays up-to-date with the latest documentation changes.
## Usage
When working with AI assistants or tools:
1. Point it to the LLMs file url below or upload it to your project for comprehensive documentation
```txt
https://sapling.land/docs/llms.txt
```
2. Use it as a reference for accurate information about Sapling
3. Include it in your project's AI configuration files
## Example
You could add this to Cursor as a doc reference to use whenever you need to reference Sapling's documentation.
---
title: Middleware
description: Learn how to use middleware in Sapling
publishedAt: "2024-01-01"
---
# Middleware
Middleware in Sapling provides a powerful way to process requests before they reach your route handlers. They can be used for common tasks like authentication, logging, error handling, and request transformation.
## Understanding Middleware
Middleware functions in Sapling receive two parameters:
- A `Context` object (`c`) containing request and response information
- A `next` function that calls the next middleware or route handler in the chain
```typescript
type Next = () => Promise;
type Middleware = (c: Context, next: Next) => Promise;
```
Each middleware can:
- Process the request before calling `next()`
- Modify the response after calling `next()`
- Short-circuit the request by returning a response without calling `next()`
- Pass control to the next middleware by calling `next()`
### Important Note About next()
Unlike some other frameworks like Hono, Sapling requires you to explicitly `return await next()` in your middleware. This is a deliberate design choice that makes the middleware flow more explicit. Sapling wants to know when you are done modifying the response.
```typescript
// ❌ Wrong - will not work in Sapling
site.use(async (c, next) => {
console.log('Before');
await next(); // Response is lost!
console.log('After');
});
// ✅ Correct - explicitly return the response
site.use(async (c, next) => {
console.log('Before');
const response = await next();
console.log('After');
return response; // Must return the response
});
```
This requirement ensures you're always handling the response properly and makes it clear when middleware is modifying or passing through responses.
This could change in the future, but for now it's a deliberate design choice.
## Global Middleware
Global middleware is applied to all routes in your application. Use the `use` method to register global middleware:
```typescript
const site = new Sapling();
// Logging middleware
site.use(async (c, next) => {
console.log(`${c.req.method} ${c.req.url}`);
const start = Date.now();
const response = await next();
const duration = Date.now() - start;
console.log(`Request completed in ${duration}ms`);
return response;
});
// Error handling middleware
site.use(async (c, next) => {
try {
return await next();
} catch (error) {
console.error('Error:', error);
return c.json({ error: 'Internal Server Error' }, 500);
}
});
```
## Route-Specific Middleware
You can apply middleware to specific routes by passing them as arguments before the route handler. Sapling fully supports chaining multiple middleware functions for a single route:
```typescript
// Authentication middleware
async function requireAuth(c: Context, next: Next) {
const token = c.req.header('Authorization');
if (!token) {
return c.json({ error: 'Unauthorized' }, 401);
}
try {
const user = await verifyToken(token);
c.set('user', user); // Store user in context state
return await next();
} catch (error) {
return c.json({ error: 'Invalid token' }, 401);
}
}
// Data validation middleware
async function validatePostData(c: Context, next: Next) {
const data = await c.req.json();
if (!data.title || !data.content) {
return c.json({ error: 'Missing required fields' }, 400);
}
c.set('validatedData', data);
return await next();
}
// Apply single middleware to a route
site.get('/api/profile',
requireAuth,
(c) => {
const user = c.get('user');
return c.json({ user });
}
);
// Chain multiple middleware functions
site.post('/api/posts',
requireAuth, // First, check authentication
validatePostData, // Then, validate the post data
async (c) => { // Finally, handle the request
const user = c.get('user');
const data = c.get('validatedData');
return c.json({ created: true, userId: user.id });
}
);
```
Each middleware in the chain must explicitly return the result of `await next()` or return its own response to short-circuit the chain. The middleware functions are executed in the order they are provided.
## Common Middleware Patterns
### State Management
Middleware can use the context state to pass data between middleware functions and route handlers:
```typescript
// Add data to context state
site.use(async (c, next) => {
c.set('requestId', crypto.randomUUID());
return await next();
});
// Access state in route handlers
site.get('/api/data', (c) => {
const requestId = c.get('requestId');
return c.json({ requestId });
});
```
### Request Transformation
Middleware can transform or validate request data before it reaches your route handlers:
```typescript
async function validateJson(c: Context, next: Next) {
if (c.req.method === 'POST' || c.req.method === 'PUT') {
try {
const body = await c.req.json();
c.set('validatedBody', body);
} catch {
return c.json({ error: 'Invalid JSON' }, 400);
}
}
return await next();
}
site.post('/api/data',
validateJson,
(c) => {
const body = c.get('validatedBody');
return c.json({ received: body });
}
);
```
### Response Headers
Middleware can modify response headers:
```typescript
site.use(async (c, next) => {
// Add security headers
c.res.headers.set('X-Frame-Options', 'DENY');
c.res.headers.set('X-Content-Type-Options', 'nosniff');
return await next();
});
```
### CORS Middleware
Example of a CORS middleware implementation:
```typescript
function cors(origin: string = '*') {
return async (c: Context, next: Next) => {
// Handle preflight requests
if (c.req.method === 'OPTIONS') {
const headers = new Headers({
'Access-Control-Allow-Origin': origin,
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
'Access-Control-Max-Age': '86400',
});
return new Response(null, { headers });
}
// Handle actual requests
const response = await next();
if (response) {
response.headers.set('Access-Control-Allow-Origin', origin);
}
return response;
};
}
// Apply CORS middleware
site.use(cors('https://example.com'));
```
## Best Practices
1. **Order Matters**: Middleware executes in the order it's added. Place middleware that should run first (like logging or error handling) at the beginning.
2. **Error Handling**: Always handle potential errors in middleware, especially when dealing with async operations:
```typescript
site.use(async (c, next) => {
try {
return await next();
} catch (error) {
console.error(error);
return c.json({ error: 'Something went wrong' }, 500);
}
});
```
3. **Type Safety**: Use TypeScript interfaces for better type safety:
```typescript
interface User {
id: string;
role: string;
}
async function requireAdmin(c: Context, next: Next) {
const user = c.get('user');
if (!user || user.role !== 'admin') {
return c.json({ error: 'Forbidden' }, 403);
}
return await next();
}
```
4. **Keep Middleware Focused**: Each middleware should have a single responsibility. Break complex middleware into smaller, more focused functions:
```typescript
// Instead of one large middleware
site.use(async (c, next) => {
// Don't do authentication, logging, and validation in one middleware
});
// Break it down
site.use(requestLogger);
site.use(authenticate);
site.use(validateRequest);
```
5. **Performance**: Be mindful of performance in middleware, especially for operations that run on every request:
```typescript
// Cache expensive operations
const cache = new Map();
site.use(async (c, next) => {
const cacheKey = c.req.url;
if (cache.has(cacheKey)) {
return cache.get(cacheKey);
}
const response = await next();
if (response && c.req.method === 'GET') {
cache.set(cacheKey, response.clone());
}
return response;
});
```
---
title: Routing
description: Learn how routing works in Sapling
publishedAt: "2024-01-01"
---
# Routing
Sapling uses a simple and powerful routing system based on URL pattern matching. Routes are defined explicitly using the Sapling instance methods, providing a clean and type-safe way to handle HTTP requests.
## Route Handlers
Routes are defined using the Sapling instance methods for different HTTP methods:
```typescript
import { Sapling } from '@sapling/sapling';
const site = new Sapling();
// Basic route
site.get("/", (c) => {
return c.html("
Welcome to Sapling
");
});
// Dynamic route with parameters
site.get("/users/:id", (c) => {
const userId = c.req.param("id");
return c.json({ userId });
});
```
Each route handler receives a context object (`c`) and can return one of the following types:
- `Response`
- `Promise`
- `Promise`
- `null`
## URL Pattern Matching
Sapling uses the URLPattern API for route matching. Each route is automatically registered with and without a trailing slash, so both `/users` and `/users/` will match the same handler.
### Route Parameters
Dynamic segments in routes are defined using the `:parameter` syntax. Parameters can be accessed in two ways:
```typescript
site.get("/posts/:slug/comments/:commentId", (c) => {
// Get a single parameter
const slug = c.req.param("slug");
const commentId = c.req.param("commentId");
// Get all parameters as an object
const params = c.req.param();
// params = { slug: "...", commentId: "..." }
return c.json({ slug, commentId });
});
```
## HTTP Methods
Sapling supports all standard HTTP methods:
```typescript
// GET request
site.get("/api/data", (c) => {
return c.json({ data: "here" });
});
// POST request
site.post("/api/users", async (c) => {
const data = await c.req.json();
return c.json({ created: true });
});
// PUT request
site.put("/api/users/:id", async (c) => {
const id = c.req.param("id");
const data = await c.req.json();
return c.json({ updated: id });
});
// DELETE request
site.delete("/api/users/:id", (c) => {
const id = c.req.param("id");
return c.json({ deleted: id });
});
// PATCH request
site.patch("/api/users/:id", async (c) => {
const id = c.req.param("id");
const data = await c.req.json();
return c.json({ patched: id });
});
```
## Request Context
The context object provides various methods and properties for handling requests:
```typescript
interface Context {
req: {
// Get URL parameters
param(): Record;
param(name: string): string;
// Request properties
method: string;
url: string;
headers: Headers;
// Helper methods
header(name: string): string | undefined;
json(): Promise;
formData(): Promise;
text(): Promise;
};
// Response headers
res: {
headers: Headers;
};
// State management
state: Record;
set(key: string, value: T): void;
get(key: string): T | undefined;
// Helper methods
query(): URLSearchParams;
html(content: string | ReadableStream): Response;
json(data: unknown, status?: number): Response;
text(content: string, status?: number): Response;
redirect(location: string, status?: number): Response;
}
```
## Middleware
Sapling supports both global and route-specific middleware:
```typescript
// Global middleware
site.use(async (c, next) => {
console.log("Request started");
const response = await next();
console.log("Request ended");
return response;
});
// Route-specific middleware
site.get("/protected",
async (c, next) => {
const token = c.req.header("Authorization");
if (!token) {
return c.json({ error: "Unauthorized" }, 401);
}
return await next();
},
(c) => {
return c.json({ message: "Protected data" });
}
);
```
## Prerendering
Sapling supports prerendering routes for static site generation. In development mode, prerendered routes behave like normal `.get` routes, while in production they are served as static HTML files.
```typescript
// Static route
site.prerender("/about", (c) => {
return c.html("
About Us
");
});
// Dynamic route with parameters
site.prerender("/blog/:slug", async (c) => {
const post = await getPost(c.req.param("slug"));
return c.html(`
${post.title}
${post.content}`);
}, [
{ slug: "first-post" },
{ slug: "second-post" }
]);
```
For detailed information about prerendering, including build setup and best practices, see the [Prerendering](/docs/prerendering) guide.
## 404 Handling
You can customize the 404 handler using `setNotFoundHandler`:
```typescript
site.notFound((c) => {
return c.html(`
Page Not Found
The requested page could not be found.
`);
});
```
## Best Practices
1. **Type Safety**: Use TypeScript interfaces for request and response data:
```typescript
interface User {
id: string;
name: string;
}
site.post("/api/users", async (c) => {
const data = await c.req.json();
return c.json(data);
});
```
2. **Error Handling**: Implement proper error handling in your routes:
```typescript
site.get("/api/protected", async (c) => {
try {
const data = await fetchData();
return c.json(data);
} catch (error) {
return c.json({ error: "Server Error" }, 500);
}
});
```
3. **Request Validation**: Validate input data before processing:
```typescript
site.post("/api/posts", async (c) => {
const data = await c.req.json<{ title: string; content: string }>();
if (!data.title || !data.content) {
return c.json({ error: "Missing required fields" }, 400);
}
// Process valid data...
});
```
---
title: Quick Start (Deno)
description: Get started with Sapling using Deno
publishedAt: "2024-01-01"
---
# Quick Start with Deno
This guide will help you create your first Sapling website using Deno.
## Prerequisites
- [Deno](https://deno.com) installed on your machine
- A code editor (we recommend VS Code with the Deno extension)
## Create a New Project
```bash
deno -A jsr:@sapling/create
```
Select the "Basics" template.
## Start the Development Server
```bash
deno task dev
```
Your site is now running at [http://localhost:8080](http://localhost:8080)!
## Project Structure
```bash
my-website/
├── components/ # Reusable UI components
├── layouts/ # Page layouts
├── pages/ # Your pages/routes
├── static/ # Static assets
├── deno.json # Project configuration
└── index.ts # Entry point
```
## Create Your First Page
1. Create a new file `pages/about.ts`:
```typescript
import Layout from "../layouts/Layout.ts";
import { html } from "@sapling/sapling";
export default async function About() {
return await Layout({
children: html`
`,
});
}
```
2. Add the route in `index.ts` underneath the `Home` route:
```typescript
site.get("/about", async (c) => c.html(await About()));
```
Visit [http://localhost:8080/about](http://localhost:8080/about) to see your new page!
## Next Steps
- Learn about [Routing](/docs/routing)
- Explore [Layouts](/docs/layouts)
- Check out the [Examples](https://github.com/withsapling/examples)
---
title: API Routes
description: Learn how to create and use API routes in Sapling
publishedAt: "2024-01-01"
---
# API Routes
API routes allow you to create server-side endpoints that can handle HTTP requests and return JSON or other data formats. Sapling makes it easy to create API routes alongside your regular page routes.
## Basic API Route
Create an API route by using the `site.get()`, `site.post()`, or other HTTP method handlers:
```typescript
// routes/api/hello.ts
site.get("/api/hello", (c) => {
return c.json({ message: "Hello from Sapling API!" });
});
```
## HTTP Methods
Sapling supports all standard HTTP methods:
```typescript
// GET request
site.get("/api/users", (c) => {
return c.json({ users: ["Alice", "Bob"] });
});
// POST request
site.post("/api/users", async (c) => {
const body = await c.req.json();
// Handle user creation
return c.json({ success: true });
});
// PUT request
site.put("/api/users/:id", async (c) => {
const id = c.req.param("id");
const body = await c.req.json();
// Handle user update
return c.json({ success: true });
});
// DELETE request
site.delete("/api/users/:id", (c) => {
const id = c.req.param("id");
// Handle user deletion
return c.json({ success: true });
});
// PATCH request
site.patch("/api/users/:id", async (c) => {
const id = c.req.param("id");
const body = await c.req.json();
// Handle partial user update
return c.json({ success: true });
});
```
## Request Handling
### Query Parameters
Access query parameters using `c.query()`:
```typescript
site.get("/api/search", (c) => {
const query = c.query().get("q");
const page = parseInt(c.query().get("page") || "1");
return c.json({
query,
page,
results: []
});
});
```
### Route Parameters
Access route parameters using `c.req.param()`:
```typescript
site.get("/api/users/:id/posts/:postId", (c) => {
const userId = c.req.param("id");
const postId = c.req.param("postId");
return c.json({
userId,
postId
});
});
```
### Request Body
Parse JSON request bodies using `c.jsonData()`:
```typescript
site.post("/api/posts", async (c) => {
const body = await c.jsonData<{ title: string; content: string }>();
// Validate the request body
if (!body.title || !body.content) {
return new Response(JSON.stringify({ error: "Missing required fields" }), {
status: 400,
headers: { "Content-Type": "application/json" }
});
}
// Process the post
return c.json({
id: "new-post-id",
...body
});
});
```
### Form Data
Handle form data using `c.formData()`:
```typescript
site.post("/api/upload", async (c) => {
const form = await c.req.formData();
const file = form.get("file");
// Process the file
return c.json({ success: true });
});
```
## Response Types
### JSON Responses
Return JSON responses using `c.json()`:
```typescript
site.get("/api/data", (c) => {
return c.json({
success: true,
data: {
message: "Hello World"
}
});
});
```
### HTML Responses
Return HTML responses using `c.html()`:
```typescript
site.get("/page", (c) => {
return c.html("
Welcome to my page
");
});
```
### Text Responses
Return plain text responses using `c.text()`:
```typescript
site.get("/plain", (c) => {
return c.text("Hello World");
});
```
### Custom Responses
For custom status codes or headers, use the Response object directly:
```typescript
site.post("/api/items", async (c) => {
// Created successfully
return new Response(JSON.stringify({ id: "new-item" }), {
status: 201,
headers: { "Content-Type": "application/json" }
});
});
site.get("/api/protected", (c) => {
// Unauthorized
return new Response(JSON.stringify({ error: "Unauthorized" }), {
status: 401,
headers: { "Content-Type": "application/json" }
});
});
```
### Redirects
Perform redirects using `c.redirect()`:
```typescript
site.get("/old-page", (c) => {
return c.redirect("/new-page");
});
site.get("/permanent-redirect", (c) => {
return c.redirect("/new-location", 301);
});
```
## Error Handling
Implement error handling for your API routes:
```typescript
site.get("/api/protected-resource", async (c) => {
try {
// Check authentication
const user = await authenticateUser(c);
if (!user) {
return new Response(JSON.stringify({ error: "Unauthorized" }), {
status: 401,
headers: { "Content-Type": "application/json" }
});
}
// Process request
const data = await fetchProtectedData();
return c.json(data);
} catch (error) {
console.error("API Error:", error);
return new Response(JSON.stringify({ error: "Internal Server Error" }), {
status: 500,
headers: { "Content-Type": "application/json" }
});
}
});
```
## TypeScript Support
Use TypeScript interfaces for type-safe API handling:
```typescript
interface User {
id: string;
name: string;
email: string;
}
interface CreateUserBody {
name: string;
email: string;
}
site.post("/api/users", async (c) => {
const body = await c.jsonData();
const user: User = {
id: crypto.randomUUID(),
name: body.name,
email: body.email
};
return c.json(user);
});
```
## Best Practices
1. **Versioning**: Consider versioning your API routes:
```typescript
site.get("/api/v1/users", (c) => {
// V1 implementation
});
site.get("/api/v2/users", (c) => {
// V2 implementation
});
```
2. **Validation**: Validate request data before processing:
```typescript
site.post("/api/comments", async (c) => {
const body = await c.jsonData<{ content: string }>();
if (!body.content || body.content.length < 10) {
return new Response(JSON.stringify({
error: "Comment must be at least 10 characters"
}), {
status: 400,
headers: { "Content-Type": "application/json" }
});
}
// Process valid comment
return c.json({ success: true });
});
```
3. **Custom 404 Handler**: Set up a custom 404 handler:
```typescript
site.notFound((c) => {
return c.html(await NotFoundLayout());
});
```
## Testing API Routes
Create tests for your API routes:
```typescript
// tests/api.test.ts
import { assertEquals } from "https://deno.land/std/testing/asserts.ts";
Deno.test("GET /api/hello returns correct message", async () => {
const response = await fetch("http://localhost:3000/api/hello");
const data = await response.json();
assertEquals(response.status, 200);
assertEquals(data.message, "Hello from Sapling API!");
});
```
For more examples and use cases, check out our [Examples](/docs/examples) section.
---
title: HTML Templating
description: Learn how to use Sapling's HTML templating methods
publishedAt: "2024-01-01"
---
# HTML Templating
Sapling provides two main methods for rendering HTML content: `html` and `raw`. These are based on the `html` and `raw` methods from [Hono](https://hono.dev) and we aim to be as compatible as possible with Hono's API to allow for easy migration between the two.
## The html Template Literal
The `html` template literal allows you to write HTML with dynamic content interpolation:
```typescript
import { html } from "@sapling/sapling";
function Greeting({ name }: { name: string }) {
return html`
Hello, ${name}!
`;
}
```
### Safety Features
The `html` template automatically escapes interpolated values to prevent XSS attacks:
```typescript
const userInput = '';
html`
${userInput}
`; // Safely escaped
```
### Nested Components
You can nest components and interpolate their results:
```typescript
function Header() {
return html`Site Header`;
}
function Page() {
return html`
${Header()}
Content
`;
}
```
## The raw Method
The `raw` method is used when you need to insert pre-rendered HTML content without escaping:
```typescript
import { html, raw } from "@sapling/sapling";
function Article({ content }: { content: string }) {
return html`
${raw(content)}
`;
}
```
### Use Cases for raw
- Rendering markdown content
- Inserting sanitized HTML from a CMS
- Including pre-rendered component output
```typescript
import { renderMarkdown } from "@sapling/markdown";
async function MarkdownContent({ markdown }: { markdown: string }) {
const rendered = await renderMarkdown(markdown);
return html`
${raw(rendered)}
`;
}
```
## Conditional Rendering
You can use standard JavaScript expressions within templates:
```typescript
function ConditionalContent({ isLoggedIn }: { isLoggedIn: boolean }) {
return html`
${isLoggedIn
? html``
: html``
}
`;
}
```
## List Rendering
Render arrays of content using map:
```typescript
function ItemList({ items }: { items: string[] }) {
return html`
${items.map(item => html`
${item}
`)}
`;
}
```
## Best Practices
1. **Use html by Default**: Always use the `html` template literal unless you specifically need `raw`
2. **Sanitize Raw Content**: When using `raw`, ensure the content is from a trusted source or properly sanitized
3. **Type Safety**: Leverage TypeScript for component props
4. **Keep It Simple**: Prefer small, focused components over complex templates
5. **Performance**: Avoid unnecessary nesting of templates
## Security Considerations
- The `html` template literal automatically escapes content to prevent XSS
- Only use `raw` with trusted content
- Always sanitize user-generated content before rendering
- Be cautious when rendering HTML from external sources
---
title: Examples
description: Explore example projects built with Sapling
publishedAt: "2024-01-01"
---
# Examples
Explore our collection of example projects to learn how to build different types of applications with Sapling. All examples are available in our [Examples GitHub Repository](https://github.com/withsapling/examples).
## Basic Examples
- [Basics](https://github.com/withsapling/examples/tree/main/deno/basics) - A basic Sapling application
- [Hello World](https://github.com/withsapling/examples/tree/main/deno/hello-sapling) - A minimal Sapling application
- [Basic Landing Page](https://github.com/withsapling/examples/tree/main/deno/single-file-landing-page) - Simple landing page example
## CMS Examples
- [Sanity](https://github.com/withsapling/examples/tree/main/deno/with-sanity) - [Sanity](https://sanity.io) CMS example
- [Contentful](https://github.com/withsapling/examples/tree/main/deno/with-contentful) - [Contentful](https://www.contentful.com) CMS example
- [Zenblog](https://github.com/withsapling/examples/tree/main/deno/with-zenblog) - [Zenblog](https://zenblog.com) CMS example
... More coming soon (PayloadCMS, Notion, Hygraph, DatoCMS, Storyblok, Prismic, etc.)
## Framework Examples
- [Alpine.js](https://github.com/withsapling/examples/tree/main/deno/with-alpinejs) - Using Sapling with Alpine.js
- [HTMX](https://github.com/withsapling/examples/tree/main/deno/with-htmx) - Using Sapling with HTMX
- [Web Components](https://github.com/withsapling/examples/tree/main/deno/with-web-components) - Using Web Components. We know this is a not a framework (technically) #usetheplatform
- [Petite Vue](https://github.com/withsapling/examples/tree/main/deno/with-petite-vue) - Using Petite Vue
- [React Component](https://github.com/withsapling/examples/tree/main/deno/with-react-component) - Using a React Component bundled with Vite
## Integration Examples
- [Motion](https://github.com/withsapling/examples/tree/main/deno/with-motion) - Animation with Motion
- [Vite + Sapling](https://github.com/withsapling/examples/tree/main/deno/vite-sapling) - Using Sapling with Vite
- [Embedded React App](https://github.com/withsapling/examples/tree/main/deno/with-react-app) - Embedding a client-side React App
- [Gemini Chat](https://github.com/withsapling/examples/tree/main/deno/with-ai-gemini) - Simple AI example to send a message to Gemini
- [OpenAI Chat](https://github.com/withsapling/examples/tree/main/deno/with-ai-openai) - Simple AI example to send a message to ChatGPT
- [Resend Emails](https://github.com/withsapling/examples/tree/main/deno/with-resend) - Sending emails from a form with Resend
- [Iconify Icons](https://github.com/withsapling/examples/tree/main/deno/with-iconify) - Using Iconify Icons
- [Satori Open Graph Images](https://github.com/withsapling/examples/tree/main/deno/satori-og-images) - Generating Open Graph Images Server-Side with Satori
- [Deno KV](https://github.com/withsapling/examples/tree/main/deno/with-deno-kv) - Using Deno KV
## Misc Examples
- [Sitemap](https://github.com/withsapling/examples/tree/main/deno/generate-sitemap) - Generating a Sitemap
## Runtime Examples
- [Deno](https://github.com/withsapling/examples/tree/main/deno) - Deno examples
- [Node](https://github.com/withsapling/examples/tree/main/node) - Node.js examples
- [Cloudflare Workers](https://github.com/withsapling/examples/tree/main/cf-workers) - Cloudflare Workers examples
- [Bun](https://github.com/withsapling/examples/tree/main/bun) - Bun examples
## Deployment Examples
- [Deno Deploy](https://github.com/withsapling/examples/tree/main/deployment/deno-deploy)
- [Cloudflare Workers](https://github.com/withsapling/examples/tree/main/deployment/cf-workers/hello-sapling)
- [Vercel Edge](https://github.com/withsapling/examples/tree/main/deployment/vercel-edge)
- [Google Cloud Run Deno](https://github.com/withsapling/examples/tree/main/deployment/google-cloud-run-deno)
- [Google Cloud Run Bun](https://github.com/withsapling/examples/tree/main/deployment/google-cloud-run-bun)
- [Google Cloud Run Node](https://github.com/withsapling/examples/tree/main/deployment/google-cloud-run-node)
## Running Examples
1. Clone the repository:
```bash
git clone https://github.com/withsapling/examples.git
cd examples
```
2. Navigate to an example:
```bash
cd deno/hello-deno # or any other example
```
3. Start the development server:
```bash
deno task dev # for Deno examples
# or
npm run dev # for Node.js examples
```
Each example includes its own README with specific instructions and explanations.
---
title: FAQ
draft: false
publishedAt: "2024-01-01"
---
# Frequently Asked Questions
## What is Sapling?
Sapling is a lightweight web framework for building server-rendered websites with TypeScript. It's designed to be simple, fast, and easy to learn while providing modern development features.
## Why should I choose Sapling over other frameworks?
Sapling offers:
- Zero client-side JavaScript by default
- Simple, intuitive HTML templating
- First-class TypeScript support
- Fast server-side rendering
- Works with any JS runtime such as Deno, Node.js, Bun, etc.
- No build step required
- Code that embraces web standards rather than fights them
## Can I still use React or Svelte or other frontend frameworks?
Yes, because Sapling is more of a bare bones server side framework that generates HTML you can use any frontend framework you prefer in specific parts of your website/application. We might be biased but we think most sites are better off with plain old HTML, CSS, and JS with a sprinkling of framework specific components.
## Do I need to know TypeScript to use Sapling?
While Sapling is built with TypeScript, you can write your code in JavaScript if you prefer. However, using TypeScript provides better developer experience with type checking and IDE support.
## Do I need to use Deno to use Sapling?
No, Sapling works with all major JavaScript runtime environments including Deno, Node.js, Bun, and CloudFlare Workers. You'll find that we use Deno often in documentation and examples because it's our preferred runtime with built-in TypeScript support which allows us to skip the configuration and build step. However, that doesn't mean Sapling is limited to Deno.
## Do I have to use Sapling for routing?
No, you can use any routing solution you prefer. We aim to be compatible with the most popular server routers out there such as [Hono](https://github.com/withsapling/examples/tree/main/deno/hello-hono), [Express](https://github.com/withsapling/examples/tree/main/node/hello-express), [Oak](https://github.com/withsapling/examples/tree/main/deno/hello-oak), etc. Sapling's routing is designed to be simple, easy to use, and adds features specific to websites, but it may not meet the needs of all applications. In those cases simply use our Layout component inside of your pages to return HTML.
## Why did you build Sapling when Hono already exists?
Hono is a great framework and we love it. However, it became pretty clear that at some point we would need our own router that adds specific features and optimizations for websites and applications. Hono is a great router for APIs and we think it's great for that use case. However, we think/hope you'll see over time that Sapling is a better fit for building websites and applications.
## So when should I use Sapling and when should I use Hono?
It depends.
The best answer we have is if you're primarily building an API then use Hono. If you're primarily building a website or application then use Sapling. We aim to be closely compatible with Hono's context format so you can easily migrate between the two as needed.
## Is it faster than Hono or Express or Koa or Fastify or ...?
We honestly don't care too much about benchmarks. If there is an area we can improve Sapling's routing performance we will do so. However, we're just not interested in the playing the game of who can get the highest number in a synthetic benchmark.
Sapling's Layout is compatible with just about every other router out there so use whatever you're most comfortable with if performance is #1 priority.
**tl;dr:** If you use Sapling's router and feel like it's slow, [use another router](/docs/faq#do-i-need-to-use-deno-to-use-sapling).
## Why not JSX?
Although JSX is a great templating language it adds complexity we don't need for Sapling. We're not trying to compete with React or Svelte. We're trying to build a framework that embraces web standards and allows you to build websites quickly and easily.
## How does routing work in Sapling?
Sapling uses a simple and powerful routing system based on URL pattern matching. Routes are defined explicitly using Sapling instance methods for different HTTP methods. For example:
```typescript
site.get("/", (c) => c.html("
Welcome
"));
site.get("/users/:id", (c) => c.json({ id: c.req.param("id") }));
```
Each route handler receives a context object and can return various response types including HTML, JSON, or custom responses. For more details, see the [routing documentation](/docs/routing).
## Where can I deploy Sapling applications?
Pretty much anywhere!
Sapling applications can be deployed to any platform that supports Deno or Node.js, including:
- [Deno Deploy](https://deno.com/deploy) ([Example](https://github.com/withsapling/examples/tree/main/deployment/deno-deploy))
- [Google Cloud Run](https://cloud.google.com/run) ([Deno Example](https://github.com/withsapling/examples/tree/main/deployment/google-cloud-run-deno), [Bun Example](https://github.com/withsapling/examples/tree/main/deployment/google-cloud-run-bun), [Node Example](https://github.com/withsapling/examples/tree/main/deployment/google-cloud-run-node))
- [Vercel Edge Functions](https://vercel.com/features/edge-functions) ([Example](https://github.com/withsapling/examples/tree/main/deployment/vercel-edge))
- [Cloudflare Workers](https://workers.cloudflare.com/) ([Example](https://github.com/withsapling/examples/tree/main/deployment/cf-workers))
- Netlify Edge Functions
- AWS Lambda
- Render
- Your own server running Deno, Node.js, or Bun
## How do I handle forms and user input?
Sapling provides built-in utilities for handling form submissions and user input through its server-side functions. You can access form data through the request object in your page handlers.
## Is Sapling production-ready?
Yes, Sapling is being used in production by various projects. However, as with any technology choice, make sure to evaluate it against your specific requirements. We are still not at version 1.0 and are actively working on improving the framework.
## Where can I get help?
- Check the documentation
- Open an issue on GitHub
- Follow the examples in our [repository](https://github.com/withsapling/examples)
## How can I contribute to Sapling?
We welcome contributions! You can:
- Submit bug reports and feature requests on GitHub
- Contribute to the documentation
- Submit pull requests
- Share your Sapling projects with the community
---
title: Sapling Islands
description: Learn how to use the sapling-island web component for progressive hydration
publishedAt: "2024-01-01"
---
# Sapling Islands
The `sapling-island` web component is a lightweight solution for implementing islands architecture in your Sapling applications. It allows you to progressively hydrate parts of your page, loading JavaScript and styles only when needed.
## Installation
You can enable Sapling Islands in two ways:
### 1. Layout Configuration (Recommended)
The simplest way is to enable islands in your Layout configuration:
```typescript
export default async function Layout(props: LayoutProps) {
return await SaplingLayout({
enableIslands: true,
// ... other layout options
});
}
```
This will automatically add the necessary script and CSS to your page.
### 2. Manual Installation
Alternatively, you can manually add the required resources. Add the Sapling Islands script to your page via CDN:
```html
```
Add the following CSS to ensure proper rendering of the content inside the islands:
```css
sapling-island {
display: contents;
}
```
There may be some cases where you don't want `display: contents`; however, in most cases it's a good idea to use it.
## Overview
Islands architecture is a pattern where most of your page remains static HTML, with interactive "islands" that get hydrated with JavaScript when needed. This approach can significantly improve initial page load performance by deferring the loading of non-critical JavaScript.
## Basic Usage
Wrap any content that requires JavaScript or additional styles in a `sapling-island` component:
```html
Your interactive content here
```
## Loading Strategies
The component supports several loading strategies through the `loading` attribute:
### Load (Default)
If no loading attribute is specified, or when set to "load", the island loads immediately when the page loads:
```html
```
### Visible
Loads when the component becomes visible in the viewport:
```html
```
### Idle
Loads when the browser is idle:
```html
```
### Media Query
Loads when a media query condition is met:
```html
```
## Hydration State
When an island is hydrated, it receives a `hydrated` attribute that you can use for styling:
```css
sapling-island[hydrated] {
/* Styles for hydrated state */
}
```
## Timeout Option
You can specify a timeout for loading strategies using the `timeout` attribute. The island will hydrate when either the loading strategy condition is met OR the timeout is reached, whichever comes first:
```html
```
In the example above, the island will hydrate either when it becomes visible **OR** after 5 seconds, whichever happens first.
## Events
The component dispatches a `island:hydrated` event when hydration is complete:
```javascript
document.querySelector('sapling-island').addEventListener('island:hydrated', () => {
console.log('Island has been hydrated');
});
```
## Best Practices
1. **Template Content Only**
Place `
...
```
2. **Progressive Enhancement**
Design your islands to enhance existing static content rather than being required for basic functionality:
```html
Static content
Interactive features
```
3. **Performance Considerations**
- Use `visible` for below-the-fold content
- Use `idle` for non-critical enhancements
- Use media queries for device-specific features
- Set appropriate timeouts for critical features
## Example: Interactive Time Display
Here's a complete example of using `sapling-island` for a dynamic time display:
```html
The time updates every second after hydration:
Current time:
```
## Browser Support
The `sapling-island` component uses standard web APIs and includes fallbacks for broader browser support:
- Uses `IntersectionObserver` for visibility detection
- Falls back to `setTimeout` when `requestIdleCallback` is not available
- Supports all modern browsers that implement Custom Elements v1
For more examples and use cases, check out our [Examples](/docs/examples) section.