Component pages
On this page
OverviewYou're free to use React, Vue, Svelte, and more to create page-level templates. Let's learn how!
Prerequisites
Section titled "Prerequisites"First, install Preact + the Slinkity Preact renderer:
npm i -D preact @slinkity/preact
Then, add this renderer to a slinkity.config.js
at the base of your project:
// .eleventy.js or eleventy.config.js
const slinkity = require('slinkity')
const preact = require('@slinkity/preact')
module.exports = function(eleventyConfig) {
eleventyConfig.addPlugin(
slinkity.plugin,
slinkity.defineConfig({
renderers: [preact()],
})
)
}
First, install React's suite of dependencies + the Slinkity React renderer:
npm i -D react react-dom @slinkity/react
Then, add this renderer to a slinkity.config.js
at the base of your project:
// .eleventy.js or eleventy.config.js
const slinkity = require('slinkity')
const react = require('@slinkity/react')
module.exports = function(eleventyConfig) {
eleventyConfig.addPlugin(
slinkity.plugin,
slinkity.defineConfig({
renderers: [react()],
})
)
}
Slinkity is designed with Vue 3 in mind. Use Vue 2.x at your own risk!
First, install Vue 3 + the Slinkity Vue renderer:
npm i -D vue@3 @slinkity/vue
Then, add this renderer to a slinkity.config.js
at the base of your project:
// .eleventy.js or eleventy.config.js
const slinkity = require('slinkity')
const vue = require('@slinkity/vue')
module.exports = function(eleventyConfig) {
eleventyConfig.addPlugin(
slinkity.plugin,
slinkity.defineConfig({
renderers: [vue()],
})
)
}
First, install Svelte + the Slinkity Svelte renderer:
npm i -D svelte @slinkity/svelte
Then, add this renderer to a slinkity.config.js
at the base of your project:
// .eleventy.js or eleventy.config.js
const slinkity = require('slinkity')
const svelte = require('@slinkity/svelte')
module.exports = function(eleventyConfig) {
eleventyConfig.addPlugin(
slinkity.plugin,
slinkity.defineConfig({
renderers: [svelte()],
})
)
}
Create a component page
Section titled "Create a component page"Think of component pages like any other template on your 11ty site. For instance, you can add a component-driven /about
page alongside your others routes like so:
index.html
blog.md
about.jsx|.vue|.svelte
...And you're ready to start templating. If you're following along at home, you'll want to add some content to this file:
// about.jsx
export default function About() {
return (
<article>
<h2>A tragic tale</h2>
<p>Did YOU ever hear the Tragedy of Darth Plagueis the Wise?</p>
</article>
)
}
// about.jsx
export default function About() {
return (
<article>
<h2>A tragic tale</h2>
<p>Did YOU ever hear the Tragedy of Darth Plagueis the Wise?</p>
</article>
)
}
<!--about.vue-->
<template>
<article>
<h2>A tragic tale</h2>
<p>Did YOU ever hear the Tragedy of Darth Plagueis the Wise?</p>
</article>
</template>
<!--about.svelte-->
<article>
<h2>A tragic tale</h2>
<p>Did YOU ever hear the Tragedy of Darth Plagueis the Wise?</p>
</article>
Now, you should see a tragic tale on /about
.
Before frantically Googling "state variables don't work in Slinkity template," This is intentional! We avoid hydrating your component clientside by default. To opt-in to using
useState
, vueref
s, and the like, jump to our hydration section ๐ง
Apply front matter
Section titled "Apply front matter"If you're familiar with 11ty, you've likely worked with front matter before. It allows you to associate "data" with your current template, which can be picked up by layouts, 11ty's collections API, and more. See 11ty's front matter documentation for full details.
For example, let's say you have a simple layout in your project called _includes/base.njk
. This layout will:
- Inject a given route's
title
property into the page<title>
- Apply the content of that layout between some
<body>
tags
<!--_includes/base.njk-->
<html lang="en">
<head>
<title>{{ title }}</title>
</head>
<body>
{{ content | safe }}
</body>
</html>
You can apply this layout to your /about
page using front matter:
// about.jsx
export const frontmatter = {
title: 'A tragic tale',
layout: 'base.njk',
}
function About() {...}
// about.jsx
export const frontmatter = {
title: 'A tragic tale',
layout: 'base.njk',
}
function About() {...}
<!--about.vue-->
<template>...</template>
<script>
export default {
frontmatter: {
title: "A tragic tale",
layout: "base.njk",
},
};
</script>
Don't forget
context="module"
here! This allows us to export data from our component. See the Svelte docs for more.
<!--about.svelte-->
<script context="module">
export const frontmatter = {
title: "A tragic tale",
layout: "base.njk",
};
</script>
<article>...</article>
Use 11ty data as props
Section titled "Use 11ty data as props"You've pushed data up into the data cascade using front matter. So how do you pull data back down in your components?
Assuming your page isn't hydrated (see how hydrated props work), all 11ty data is magically available as props.
Say you have a list of incredible, amazing, intelligent Slinkity contributors in a global data file called _data/contributors.json
:
[
{ "name": "Ben Myers", "ghProfile": "https://github.com/BenDMyers" },
{ "name": "Anthony Campolo", "ghProfile": "https://github.com/ajcwebdev" },
{ "name": "Thomas Semmler", "ghProfile": "https://github.com/nachtfunke" }
]
Since all _data
files are piped into 11ty's data cascade, this is now available to your component page via the contributors
prop:
// about.jsx
export default function About({ contributors }) {
return (
<ul>
{contributors.map(({ name, ghProfile }) => (
<li><a href={ghProfile}>{name}</a></li>
))}
</ul>
)
}
// about.jsx
export default function About({ contributors }) {
return (
<ul>
{contributors.map(({ name, ghProfile }) => (
<li><a href={ghProfile}>{name}</a></li>
))}
</ul>
)
}
<!--about.vue-->
<template>
<ul v-for="contributor in contributors">
<li>
<a href="{{contributor.ghProfile}}">{{ contributor.name }}</a>
</li>
</ul>
</template>
<script>
export default {
props: ["contributors"],
};
</script>
<!--about.svelte-->
<script>
export let contributors = [];
</script>
<article>
<ul>
{#each contributors as contributor}
<li>
<a href={contributor.ghProfile}>{contributor.name}</a>
</li>
{/each}
</ul>
</article>
To get the most out of these data props, we recommend learning more about the 11ty data cascade. Here's some helpful resources:
- ๐ The official 11ty docs
- ๐ A beginner-friendly walkthrough by Ben Myers
Access shortcodes and filters
Section titled "Access shortcodes and filters"All shortcodes and filters are accessible with the useFunctions
helper. This includes any shortcode or filter created using the following eleventy config helpers:
// .eleventy.js
module.exports = function(eleventyConfig) {
// Universal filter
eleventyConfig.addFilter("capitalize", function(value) { โฆ });
// Universal shortcode
eleventyConfig.addShortcode("capitalized", function(value) { โฆ });
// Universal paired shortcode
eleventyConfig.addPairedShortcode("capitalized", function(content, value) { โฆ });
// JavaScript Template Function
eleventyConfig.addJavaScriptFunction("capitalize", function(value) { โฆ });
};
For instance, say you registered a capitalize
filter like so:
// .eleventy.js
module.exports = function(eleventyConfig) {
eleventyConfig.addFilter('capitalize', function(value) {
const firstLetter = value[0]
const restOfPhrase = value.slice(1)
return firstLetter.toUpperCase() + restOfPhrase
})
}
You can access this filter across your component pages like so:
// about.jsx
import { useFunctions } from '@slinkity/preact/server';
export default function About({ __functions }) {
const { capitalize } = useFunctions();
const name = 'darth Plagueis the Wise'
return (
<article>
<h2>A tragic tale</h2>
<p>Did YOU ever hear the Tragedy of {capitalize(name)}?</p>
</article>
)
}
// about.jsx
import { useFunctions } from '@slinkity/react/server';
export default function About({ __functions }) {
const { capitalize } = useFunctions();
const name = 'darth Plagueis the Wise'
return (
<article>
<h2>A tragic tale</h2>
<p>Did YOU ever hear the Tragedy of {capitalize(name)}?</p>
</article>
)
}
๐ง Vue support in progress
๐ง Svelte support in progress
Handle dynamic permalinks
Section titled "Handle dynamic permalinks"Dynamic permalinks are useful when generating a URL from 11ty data. You may be used to template strings when using plain 11ty (say, using Nunjucks to output a URL). But with Slinkity, you have the power of JavaScript functions at your disposal ๐
Example - Generate a permalink from a page title
Section titled "Example - Generate a permalink from a page title"Say you want to generate a blog post's URL from its title. Since front matter is available from the 11ty data object, you can use a permalink()
function like so:
// about.jsx
export const frontmatter = {
title: 'A tragic tale',
permalink(eleventyData, functions) {
// note: shortcodes and filters are available
// from `functions`. We're using 11ty's built-in
// `slugify` filter here.
return \`/${functions.slugify(eleventyData.title)}/\`
},
}
export default function About() {...}
// about.jsx
export const frontmatter = {
title: 'A tragic tale',
permalink(eleventyData, functions) {
// note: shortcodes and filters are available
// from `functions`. We're using 11ty's built-in
// `slugify` filter here.
return \`/${functions.slugify(eleventyData.title)}/\`
},
}
export default function About() {...}
<!--about.vue-->
<template>...</template>
<script>
export default {
frontmatter: {
title: "A tragic tale",
permalink(eleventyData, functions) {
// note: shortcodes and filters are available
// from `functions`. We're using 11ty's built-in
// `slugify` filter here.
return \`/${functions.slugify(eleventyData.title)}/\`
},
},
}
</script>
<!--about.svelte-->
<script context="module">
export const frontmatter = {
title: "A tragic tale",
permalink(eleventyData, functions) {
// note: shortcodes and filters are available
// from `functions`. We're using 11ty's built-in
// `slugify` filter here.
return \`/${functions.slugify(eleventyData.title)}/\`
},
};
</script>
<article>...</article>
Here's how your site's input / output directories will look, assuming _site
is your output and src
is your input:
โโโ _site
โ โโโ a-tragic-tale
โ โโโindex.html
โโโ src
โ โโโ about.jsx|vue|svelte
Example - Dynamic permalinks with pagination
Section titled "Example - Dynamic permalinks with pagination"Pagination is another common use case for dynamic permalinks. We won't go too in depth on 11ty's pagination options (see their docs for full details), but we'll cover the primary use case: generate routes from an array of data.
Say that:
- You have an array of T-shirts to sell on your e-commerce site
- You want to generate a unique route to preview each T-shirt
That list of T-shirts may look like this (_data/tshirts.json
):
[
{
"name": "Me and the Possum Posse",
"slug": "possum-posse",
"image": "assets/possum-posse.jpg"
},
{
"name": "It possumtimes be like that",
"slug": "possumtimes",
"image": "assets/possumtimes.jpg"
}
]
You can generate routes for each of these T-shirts using the pagination
and permalink
properties like so:
// tshirt.jsx
export const frontmatter = {
pagination: {
// name of your data
data: 'tshirts',
// number of routes per array element
size: 1,
// variable to access array element values
// from your permalink fn and your component page
alias: 'tshirt'
},
// note the trailing "/" here!
permalink: ({ tshirt }) => \`/${tshirt.slug}/\`
}
export default function Tshirt({ tshirt }) {
return (
<article>
<h1>{tshirt.name}</h1>
<img src={tshirt.image} alt={tshirt.name} />
</article>
)
}
// tshirt.jsx
export const frontmatter = {
pagination: {
// name of your data
data: 'tshirts',
// number of routes per array element
size: 1,
// variable to access array element values
// from your permalink fn and your component page
alias: 'tshirt'
},
// note the trailing "/" here!
permalink: ({ tshirt }) => \`/${tshirt.slug}/\`
}
export default function Tshirt({ tshirt }) {
return (
<article>
<h1>{tshirt.name}</h1>
<img src={tshirt.image} alt={tshirt.name} />
</article>
)
}
<!--tshirt.vue-->
<template>
<article>
<h1>{{ tshirt.name }}</h1>
<img src="{{ tshirt.image }}" alt="{{ tshirt.name }}" />
</article>
</template>
<script>
export default {
frontmatter: {
pagination: {
// name of your data
data: "tshirts",
// number of routes per array element
size: 1,
// variable to access array element values
// from your permalink fn and your component page
alias: "tshirt",
},
// note the trailing "/" here!
permalink: ({ tshirt }) => \`/${tshirt.slug}/\`,
},
props: ["tshirt"],
};
</script>
<!--tshirt.svelte-->
<script context="module">
export const frontmatter = {
pagination: {
// name of your data
data: "tshirts",
// number of routes per array element
size: 1,
// variable to access array element values
// from your permalink fn and your component page
alias: "tshirt",
},
// note the trailing "/" here!
permalink: ({ tshirt }) => \`/${tshirt.slug}/\`,
};
</script>
<script>
export let tshirt = {};
</script>
<article>
<h1>{tshirt.name}</h1>
<img src={tshirt.image} alt={tshirt.name} />
</article>
Here's how your site's input / output directories will look, assuming _site
is your output and src
is your input:
โโโ _site
โ โโโ possum-posse
โ โโโ index.html
โ โโโ possumtimes
โ โโโ index.html
โโโ src
โ โโโ tshirt.jsx|vue|svelte
Hydrate your page
Section titled "Hydrate your page"We've used components as build-time templating languages. Now let's add some JavaScript into the mix ๐ฅ
You can enable client-side rendering using the island
export:
// about.jsx
import { useState } from 'react'
export const island = {
when: 'client:load',
}
export default function About() {
const [count, setCount] = useState(0)
return (
<>
<p>You've had {count} glasses of water ๐ง</p>
<button onClick={() => setCount(count + 1)}>Add one</button>
</>
)
}
<!--about.vue-->
<template>
<p>You've had {{ count }} glasses of water ๐ง</p>
<button @click="add()">Add one</button>
</template>
<script>
import { ref } from "vue";
export default {
island: {
when: 'client:load',
},
setup() {
const count = ref(0);
const add = () => (count.value = count.value + 1);
return { count, add };
},
};
</script>
<!--about.svelte-->
<script context="module">
export const island = {
when: 'client:load',
}
</script>
<script>
let count = 0;
function add() {
count += 1;
}
</script>
<p>You've had {count} glasses of water ๐ง</p>
<button on:click={add}>Add one</button>
Like component shortcodes, you're free to use any of our client:
directives (client:visible
, client:idle
, and more).
Handle props on hydrated components
Section titled "Handle props on hydrated components"Props work a bit differently now that JS is involved. In order to access 11ty data from your component, you'll need to choose which pieces of data you need.
For instance, say you need to access that same global contributors
list from earlier. You can add a props
function to your island
export to access this data and pass the pieces you need to the client:
// about.jsx
export const island = {
when: 'client:load',
props: (eleventyData) => ({
contributors: eleventyData.contributors,
})
}
export default function About({ contributors }) {
return (
<ul>
{contributors.map(({ name, ghProfile }) => (
<li><a href={ghProfile}>{name}</a></li>
))}
</ul>
)
}
// about.jsx
export const island = {
when: 'client:load',
props: (eleventyData) => ({
contributors: eleventyData.contributors,
})
}
export default function About({ contributors }) {
return (
<ul>
{contributors.map(({ name, ghProfile }) => (
<li><a href={ghProfile}>{name}</a></li>
))}
</ul>
)
}
<!--about.vue-->
<template>
<ul v-for="contributor in contributors">
<li>
<a href="{{contributor.ghProfile}}">{{ contributor.name }}</a>
</li>
</ul>
</template>
<script>
export default {
props: ["contributors"],
island: {
when: 'client:load',
props: (eleventyData) => ({
contributors: eleventyData.contributors,
})
}
};
</script>
<!--about.svelte-->
<script context="module">
export const island = {
when: 'client:load',
props: (eleventyData) => ({
contributors: eleventyData.contributors,
})
}
</script>
<script>
export let contributors = [];
</script>
<article>
<ul>
{#each contributors as contributor}
<li>
<a href={contributor.ghProfile}>{contributor.name}</a>
</li>
{/each}
</ul>
</article>
A few takeaways here:
- We include an
island.props
function for Slinkity to decide which props our component needs - Slinkity runs this function at build time (not on the client!) to decide which props to generate
- These props are accessible from the browser-rendered component
๐จ (Important!) Be mindful about your data
Section titled "๐จ (Important!) Be mindful about your data"You may be wondering, "why can't all the eleventyData
get passed to my component as props? This seems like an extra step."
Well, it all comes down to the end user's experience. Remember that we're sending your JS-driven component to the browser so pages can be interactive. If we sent all that eleventyData
along with it, the user would have to download that huge data blob for every component on your site. ๐ฎ
So, we added hydrate.props
as a way to pick the data that you need, and "filter out" the data that you don't.
11ty's collections object is a prime example of where hydrate.props
shines. This object contains references to every page on your site, plus all the data those pages receive. Needless to say, that blob can get pretty big! We suggest you:
// โ Don't pass everything
props({ collections }) {
return { collections }
}
// โ
Map out the pieces you need
props({ collections }) {
return {
blogPostUrls: collections.blogPosts.map(
blogPost => blogPost.page.url
)
}
}
Now that we've discussed page hydration, let's learn how to choose if and when that JS loads.