Elder.js is an opinionated static site generator and web framework built with SEO in mind. (Supports SSR and Static Site Generation.)
Features:
data
function in your route.js
, you have complete control over how you fetch, prepare, and manipulate data before sending it to your Svelte template. Anything you can do in Node.js, you can do to fetch your data. Multiple data sources, no problem.Project Status: Stable
Elder.js is stable and production ready.
It is being used on this site and 2 other flagship SEO properties that are managed by the maintainers of this project.
We believe Elder.js has reached a level of maturity where we have achieved the majority of the vision we had for the project when we set out to build a static site generator.
Our goal is to keep the hookInterface, plugin interface, and general structure of the project as static as possible.
This is a lot of words to say we’re not looking to ship a bunch of breaking changes any time soon, but will be shipping bug fixes and incremental changes that are mostly “under the hood.”
As of September 2020, the ElderGuide.com team expects to maintain this project at least until 2023-2024. For a clearer vision of what we mean by this and what to expect from the maintainers as far as what is considered "in scope" and what isn't, please see this comment.
Context
Elder.js is the result of our team's work to build this site (ElderGuide.com) and was purpose built to solve the unique challenges of building flagship SEO sites with 10-100k+ pages.
Elder Guide Co-Founder Nick Reese has built or managed 5 major SEO properties over the past 14 years. After leading the transition of several complex sites to static site generators, he loved the benefits of the JAM stack, but wished there was a better solution for complex, data intensive, projects. Elder.js is his vision for how static site generators can become viable for sites of all sizes regardless of the number of pages or how complex the data being presented is.
We hope you find this project useful whether you're building a small personal blog or a flagship SEO site that impacts millions of users.
The quickest way to get started is to get started with the Elder.js template using degit:
Step 1: Clone Template
npx degit Elderjs/template elderjs-app
cd elderjs-app
yarn # or npm install
Step 2: Start the Project
npm start
Navigate to http://localhost:3000. You should see your app running.
You can also see a live demo of this template: https://elderjs.netlify.app/
For development, we recommend running two separate terminals. One for the server and the other for rollup.
Terminal 1: Server
npm run dev:server # `npm start` above starts a server, but doesn't rebuild your Svelte components on change.
Terminal 2: Rollup
npm run dev:rollup # This rebuilds your Svelte components on change.
Once you have these two terminals open, edit a component file in src
, save it, and reload the page to see your changes.
npm run build
Let the build finish.
npx sirv-cli public
The talk below was given at Svelte Summit 2020 and is a great intro to the concepts behind Elder.js.
When we set out to build elderguide.com we tested 6 different static site generators (Gatsby, Next.js, Nuxt.js, 11ty, Sapper and Hydrogen.js) and ultimately realized there wasn’t a solution that ticked all of our boxes.
On our journey, we had 3 major realizations:
Initially, we decided to go with Sapper but hit major data roadblocks and issues unusable build times and development reload times.
In an afternoon of frustration, we whipped up a very rudimentary SSG with a complex and error prone process of adding Svelte components… but it worked. #productionready
After shipping ElderGuide.com to production, we were working on a refactor when a moment of genius from Kevin over at Svelte School prompted a major breakthrough that allowed us to use Svelte 100% for templating and still get partial hydration even though Svelte doesn’t support it.
After much consideration, we decided to open source the project so others could use it.
We can't wait to see what you build with it.
At the core of any site are its "routes" or templates.
In Elder.js a route is made up of 2 files that live in your route folder: ./src/routes/${routeName}/
.
They are:
route.js
file. This is where you define route details such as the route’s permalink
function, all
function and data
function.${routeName}
; eg: ./src/routes/blog/Blog.svelte
(from here on out we refer to these specific Svelte components as "Svelte Templates")route.js
files consist of a permalink
function, an all
function, and a data
function.
Elder.js uses "explicit routing" instead of the more common "parameter based" routing found in most frameworks like express
.
At first, Elder.js' non-conventional routing can be intimidating, but it offers some major benefits discussed below while streamlining data flow in complex sites.
Let's look at an example of how you'd setup a route like /blog/:slug/
where there are only 2 blogposts.
// ./src/routes/blog/route.js
module.exports = {
template: 'Blog.svelte',
permalink: ({ request }) => `/blog/${request.slug}/`, // this is the same as /blog/:slug/ in 'parameter based' routing.
all: async () => {
// The all function returns an array of all possible "request" objects for a route.
// Here we are explicitly defining every possible variation of this route.
return [{ slug: 'blogpost-1' }, {slug: 'blogpost-2'}],
},
data: async ({ request }) => {
// The object returned here will be available in the Blog.svelte as the 'data' prop.
return {
blogpost: `This is the blogpost for the slug: ${request.slug}`.
}
};
Here is what is happening in plain English:
permalink()
: The permalink function is similar to your standard route definition you'd see with placeholders. This means /blog/:slug/
would be defined as /blog/${request.slug}/
. The permalink function's job is to take the request
objects returned from all
and transform them into relative urls.all()
: This async function returns an array of all of the request
objects for a given route. Often this array may come from a data store but in this example, we're explicitly saying we only have 2 blog posts, so only two pages will be generated.data()
: The data function prepares the data required in the Blog.svelte
file. Whatever object is returned will be available as the data
prop. In the example, we are just returning a static string, but you could also hit an external CMS, query a database, or read from the file system. Anything you can do in node, you can do here.In this example, we're just returning a simple object in our data()
function, but we could have easily used node-fetch
and gotten our blogpost from a CMS or used fs
to read from the filesystem:
const blogpost = await fetch(
`https://api.mycms.com/getBySlug/${request.slug}/`
).then((res) => res.json());
Elder.js' approach to routing is unconventional but it offers several distinct advantages, the two biggest are:
/senior-living/:facilityId/
and /senior-living/:articleId/
and /senior-living/:parentCompanyId/
. This also makes i18n and l10n much more approachable.With the simple route.js
example out of the way, let's talk about best practices and let's look at a more complex example of a route.js
file.
Best Practice: A route's all
function should return the minimum viable data points needed to generate a page.
Skinny
request
objects. Fatdata
functions.
When people first encounter Elder.js, there is a strong temptation to load the request
objects returned by a route's all
function with tons of data.
While this approach works, it doesn't scale very well. Fetching, preparing, and processing data should be done in your data
function.
That said, it is recommended that you only include the bare minimum required to query your database, api, file system, or data store on the request
object. From there, do all of the data fetching, preparing, and organization in the route's data
function.
Real World Example
To drive this point home and to show a more complex example of routing, imagine you're building a travel site that lists tourist attractions for major cities throughout the world.
You have a city
route and for each page on that route you need 3 data points to query your API, database, or datastore in order to pull in all of the rest of the page's data.
These data points are:
Here is what a minimal route.js would look like to support /en/spain/barcelona/
and /es/espana/barcelona/
.
// ./src/routes/city/route.js
module.exports = {
permalink: ({ request, settings }) =>
`/${request.lang}/${request.country.slug}/${request.slug}/`,
all: async () => {
return [
{ slug: "barcelona", country: { slug: "spain" }, lang: "en" },
{ slug: "barcelona", country: { slug: "espana" }, lang: "es" },
];
},
data: async ({ request }) => {
// discussed below.
},
};
Problems with Fat Request Objects
Imagine for a moment that we attempted to include all of the additional details needed to generate the page for this route in our request
objects like so:
module.exports = {
// permalink function
all: async () => {
return [
{ slug: 'barcelona', country: { slug: 'spain' }, lang: 'en', data: { hotels: 12, attractions: 14, promotions: ['English promotion'], ...lotsOfData } },
{ slug: 'barcelona', country: { slug: 'espana' }, lang: 'es' data: { hotels: 12, attractions: 14, promotions: ['Spanish promotion'], ...lotsOfData } }
]
}
// data function
}
Now imagine your data
function looks like so and you're getting more data.
module.exports = {
// permalink function
all: async () => {
return [
{ slug: 'barcelona', country: { slug: 'spain' }, lang: 'en', data: { hotels: 12, attractions: 14, promotions: ['English promotion'], ...lotsOfData } },
{ slug: 'barcelona', country: { slug: 'espana' }, lang: 'es' data: { hotels: 12, attractions: 14, promotions: ['Spanish promotion'], ...lotsOfData } }
]
},
data: async ({ request }) => {
const hotels = [
{ ...hotel }, // imagine this has a lot of details
{ ...hotel },
{ ...hotel },
{ ...hotel },
{ ...hotel },
];
// this will now be available in your svelte template as your 'data' param.
// you could access all of the hotel details at `data.hotels`
return {
hotels,
};
},
}
With this implementation, you've now got both request
and data
objects in Svelte templates and you're asking yourself:
Should I be accessing
request.data.hotels
or justdata.hotels.length
to get the number of hotels?
Save yourself this headache by remembering: skinny request
objects, fat data
functions.
Only store the minimum data needed on your request
objects. Instead return all of the data required by the page from the data
function.
Note: If you're interested in i18n please look at this issue as robust support could be offered by a community plugin.
Database Connections, APIs, and External Data Sources
The data
function of each route is designed to be the central place to fetch data for a route but the implementation details are very open ended and up to you.
Just about anything you can do in Node.js, you can do in a data
function.
That said, if you are hitting a DB and want to manage your connection in a reusable fashion, the recommended way of doing so is to populate the query
object on the bootstrap
hook.
Using this pattern allows you to share a database connection across the entire lifecycle of your Elder.js site.
Cache Data Where Possible Within Route.js Files
If you have a data heavy calculation required to generate a page, look into calculating that data and caching it before your module.exports
definition like so:
// ./src/routes/city/route.js
// do heavy calculation here
// this prevents the data from being calculated each request
const cityLookupObject = {
barcelona: {
// lots of data.
}
}
module.exports = {
permalink: ({ request, settings }) =>
`/${request.lang}/${request.country.slug}/${request.slug}/`,
all: async () => {
return [
{ slug: "barcelona", country: { slug: "spain" }, lang: "en" },
{ slug: "barcelona", country: { slug: "espana" }, lang: "es" },
];
},
data: async ({ request }) => {
return {
city: cityLookupObject[request.slug];
}
},
};
Data Used in Multiple Routes
If you have data that is used in multiple routes, you can share that data between routes by populating the data
object on the bootstrap
hook documented later in this guide.
Assuming you have populated the data.cities
with an array of cities on the bootstrap
hook, you could access it like so:
// ./src/routes/city/route.js
module.exports = {
permalink: ({ request }) => `/${request.slug}/`,
all: async ({ data }) => data.cities,
data: async ({ request, data }) => {
return {
city: data.cities.find(city=> city.slug === request.slug);
}
},
};
Data defined in bootstrap
is available on all routes.
Here is the function signature for a route.js
all function:
all: async ({ settings, query, data, helpers }): Array<Object> => {
// settings: this describes the Elder.js settings at initialization.
// query: an empty object that is usually populated on the 'bootstrap' hook with a database connection or api connection. This is sharable throughout all hooks, functions, and shortcodes.
// data: any data set on the 'bootstrap' hook.
return Array<Object>;
}
Here is the function signature for a route.js
permalink function:
permalink: ({ request, settings, helpers }): String => {
// NOTE: permalink must be sync. Async is not supported.
// request: this is the object received from the all() function. Generally, we recommend passing a 'slug' parameter but you can use any naming you want.
// settings: this describes the Elder.js bootstrap settings.
// helpers: Elder.js helpers and user helpers from the ./src/helpers/index.js` file.
// NOTE: You should avoid using helpers here as helpers.permalinks default helper (see below) doesn't support it.
return String;
};
Whether you’re building a personal blog or complex data driven SEO site, a route's data
function is the recommended place to fetch (from a db, api, or other source) and prepare data to be consumed by your Svelte templates.
Here is the function signature for a route.js
data function:
data: async ({
data, // any data set by plugins or hooks on the 'bootstrap' hook
helpers, // Elder.js helpers and user helpers from the ./src/helpers/index.js` file.
allRequests, // all of the `request` objects returned by a route's all() function.
settings, // settings of Elder.js
request, // the requested page's `request` object.
errors, // any errors
perf, // the performance helper.
query, // search for 'query' in these docs for more details on it's use.
}): Object => {
// data is any data set from plugins or hooks.
return Object;
};
Elder.js hooks are designed to be modular, sharable, and easily bundled in to Elder.js plugins for common use cases... while still giving developers of all skill levels an easy way to customize core page generation logic to their own needs.
For a full overview of the hooks available, you can reference the hookInterface.ts or the hooks list below.
In short, there is a hook at every major step of the page generation process from system bootstrap (the bootstrap
hook) all the way to writing html to your computer (on the requestComplete
hook).
No project becomes a 'tangled mess' on day one. It happens over time.
You or someone on your team makes a small "hacky" fix.
This change was intended to be temporary but it falls off your team's radar.
Over time, these "hacky" fixes build up and slowly make a project hard to reason about and hard to work on.
The goal of Elder.js' hook implementation is that any changes that don't fit in a route.js
file are instead aggregated in a single hooks.js
file where anyone on a team will know to expect to find any hidden complexity.
The result of this approach is that of a project's hacky fixes are no longer scattered across a project, but instead live in a single self documenting location where users have complete but predictable control over the Elder.js page generation process.
The added benefit is that plugins can also tap into these hooks offering sharable functionality.
mutable
and props
ArraysEach Elder.js hook explicitly defines which props
are available to a function registered on a hook along with which of those props
are mutable
by that function.
This defines the "contract" that Elder.js' hook interface implements.
props
represents the parameters that are available to a function registered on a hook.mutable
represents which of the props
can be changed on a specific hook.This structure was implemented to keep mutation and side effects predictable.
Under the hood, all items in the
props
array that aren't in themutable
array are passed as a Proxy.
Used to modify what hooks can mutate which properties all hooks.
This hook receives the hookInterface.ts file which defines all hook interactions. You can customize all 'props' and 'mutable' of all hooks by using this hook. This is a power user hook and unless you know Elder.js internals don't mess with it.
Advanced Hook: This hook is designed for plugins and power users who want to custom Elder.js beyond the common use cases.
Routes, plugins, and hooks have been collected and validated.
allRequests which represents all of the request objects have been collected from route and plugins. This makes the 'allRequests' array mutable.
The main use here is to allow users to adjust the requests that Elder.js is aware of.
NOTE: If you are modifying 'allRequests' you must set 'request.route' key for each request.
Fired upon a request that originates from the express/polka middleware version of Elder.js. The hook has access to "req" and "next" common in express like middleware.
If you're looking to use Elder.js with express/polka to build a server rendered website, then you'll be interested in this hook as it includes the familiar 'req' and 'next' objects as often used in Express middleware.
Advanced Hook: This hook is designed for plugins and power users who want to custom Elder.js beyond the common use cases.
This is executed at the beginning the request object being processed.
This hook gives access to the entire state of a request lifecycle before it starts.
This hook is run after the route's "data" function has executed.
This hook is mainly used by plugins/hooks to offer functionality at the route level that is dependent on the route's "data" function has returning but isn't suitable to live in multiple data function across many routes due to code duplication.
Examples of things we (ElderGuide.com) have done or have seen users do:
Stacks are made available here so that strings can be added to the head or footer of the page easily.
Advanced Hook: This hook is designed for plugins and power users who want to custom Elder.js beyond the common use cases.
Executed after the route's html has been compiled, but before the layout html has been compiled.
Elder.js uses this hook to process shortcodes. The vast majority of users won't need to use this hook, but if you were so inclined you could write your own shortcode parser or if you'd like to disable shortcodes completely, you can add 'elderProcessShortcodes' to hooks.disable in your elder.config.js file.
NOTE: Don't use this hook for anything besides shortcodes.
Advanced Hook: This hook is designed for plugins and power users who want to custom Elder.js beyond the common use cases.
Executed just before processing all of the stacks into strings.
Elder.js uses 'stacks' to manage it's string concatenation. If you are unfamiliar, stacks are basically an array of strings, with a priority, and some meta data. This hook let's you manipulate or view the stacks before they are written to the page and is designed for use by plugins.
This hook will mainly be used when you need to add arbitrary strings to the footer. In most cases, users should be using <svelte:head></svelte:head> to add content to the head.
Advanced Hook: This hook is designed for plugins and power users who want to custom Elder.js beyond the common use cases.
Executed just before writing the tag to the page.
This hook's headSting represents everything that will be written to <head> tag.
There are many possible SEO uses to this hook, especially for plugins. That said, we recommend users who want to set common SEO elements such as tags <title> and meta descriptions programmatically to do it from within Svelte templates using the <svelte:head></svelte:head> tag. Chances are you won't need this field unless you're a power user and need access to the raw head.
Advanced Hook: This hook is designed for plugins and power users who want to custom Elder.js beyond the common use cases.
This is where Elder.js merges the html from the Svelte layout with stacks and wraps it in an tag.
This hook should only be used when you need to have full control over the <html> document. Make sure if you use this to add 'elderCompileHtml' to the 'hooks.disable' array in your elder.config.js or your template will be overwritten.
Advanced Hook: This hook is designed for plugins and power users who want to custom Elder.js beyond the common use cases.
Executed when all of the html has been compiled.
This hook receives the full html of the document. With great power comes great responsibility.
This hook marks the end of the request lifecycle.
This hook is triggered on an individual 'request object' completing whether Elder.js is being used in the "build" or a "server" mode.
Executed only if the script has encountered errors and they are pushed to the errors array.
As the script encounters errors, they are collected and presented on this hook at the end of a request and the end of an entire build.
Executed after a build is complete
Contains whether the build was successful. If not it contains errors for the entire build. Also includes average performance details, and a granular performance object. Could be used to fire off additional scripts such as generating a sitemap or copying asset files to the public folder.
Plugins: Because builds are split across processes, a plugin doesn't not have a shared memory space across all processes.
bootstrap
Here is the bootstrap
hook from the hookInterface.ts
file.
// From hookInterface.ts
{
"hook": "bootstrap",
"props": [
"helpers",
"data",
"settings",
"routes",
"hooks",
"query",
"errors"
],
"mutable": [
"errors",
"helpers",
"data",
"settings",
"query"
],
"context": "Routes, plugins, and hooks have been collected and validated.",
"use": "<ul>\n <li>Often used to populate the empty query object with a database or API connection as query is passed to the all() function which is used to generate request objects.</li>\n <li>Internally used to automatically populate the helpers object with the helpers found in './src/helpers/index.js'.</li>\n <li>Can be used to set information on the data object that is needed throughout the entire lifecycle. (sitewide settings)</li>\n </ul>",
"location": "Elder.ts",
"experimental": false,
"advanced": false
};
This hook is executed after Elder.js has bootstrapped itself and lets users run arbitrary functions at that point too.
Internally, Elder.js uses this hook to automatically any user defined helpers ./src/helpers/index.js
to the helpers
prop that is available on other hooks, in Svelte templates, and data
functions.
// ./src/hooks.js
const fetch = require('node-fetch');
module.exports = [
{
hook: 'bootstrap',
name: 'addExternalData',
description: 'Adds arbitrary external data to the data object available in all hooks and routes.',
run: async ({ settings, data }) => {
const externalData = await fetch('https://yourapi.here').then((res) => res.json());
return {
data: {
...data,
externalData, // this data is now available in the `all` and `data` functions of your `/routes/routeName/route.js`.
},
};
},
},
};
query
ObjectHere is what a simple hook defined in your ./src/hook.js
file might look like if you wanted to add a database connection to the query
object which is available every time a hook is called:
// ./src/hooks.js
const db = require('../db');
module.exports = [
{
hook: 'bootstrap',
name: 'addDbToQuery',
description: 'Adds our db object to the query object',
priority: 99, // higher is more important. Since we want to be able to use the DB in other hooks that may be on the bootstrap hook, higher is better.
run: async ({ query }) => {
return {
query: { ...query, db },
};
},
},
};
In plain english:
addDbToQuery
runs on the bootstrap
hook and adds the db
object as a key to the query
object.
If you wanted to initialize a database connection and make it available on all hooks and in data
functions, this is how you'd do it.
USER HOOKS:
We recommend you define all of your hooks in your ./src/hooks.js
file.
It is also recommended that you organize them to be sequential with hook execution as shown above.
If you need to limit a function to only run on a specific route, you can do so by using request.route === 'routeName'
.
Note: If you're finding your
./src/hooks.js
is becoming too big, resist the urge as long as possible to split it into sub files. We've found that even with 20+ hooks, as long as they are organized sequential to match hook execution, things stay maintainable.
SYSTEM HOOKS:
Under the hood, all of the hooks Elder.js runs are defined in the @elderjs/elderjs ./src/hooks/index.ts
.
They can be disabled by adding the hook name to the hooks.disable
array in your elder.config.js
.
// elder.config.js
...
hooks: {
disable: ['elderWriteHtmlFileToPublic'], // this is used to disable internal hooks. Adding this would disabled writing your files on build.
}
...
Plugins are prepackaged hooks and/or routes that can be used to add additional functionality to an Elder.js
site.
Plugins also have the added bonus of having their own isolated closure scope where they can store data between hooks invocations.
To use a plugin, it must be registered in your elder.config.js
and can be loaded from ./src/plugins/${pluginName}/index.js
or from the entry point to an npm package ./node_modules/${pluginName}/
data
object.ref
and referenceList
shortcodes.If you're looking to write your own plugin for Elder.js, we've setup an easy template to clone.
Here is the command you can use to clone it locally without all of the git history using degit.
npx degit Elderjs/plugin-template elderjs-plugin
cd elderjs-plugin
Here is what a plugin looks like:
const plugin: PluginOptions = {
name: "elder-plugin-upload-s3",
description: "Uploads html and/or data.json file to s3",
init: (plugin) => {
// console.log(plugin); => returns this plugin object.
plugin.data = { test: true };
// NOTE! any data added in any hook or in the init function will
// be persisted for the entire lifecycle of elder.js
// this means the closure persists between server.js loads AND between pages during build.
// Temporary data can be stored on this object,
// but it is up to the plugin to clean up after itself and manage its own state.
return plugin;
},
hooks: [
{
hook: "requestComplete",
name: "uploadDataObjectToS3",
description: "Uploads a data.json file to s3",
priority: 1, // we want it to be last
run: async ({ data, settings, request, plugin }) => {
// console.log(plugin.test) => true
if (settings.build === true && settings.deploy === true) {
if (plugin.config.dataBucket && plugin.config.dataBucket.length > 0) {
let dest = `${request.permalink.replace(/^\/+/, "")}data.json`;
if (plugin.config.deployId) {
dest = `${plugin.config.deployId}/${dest}`;
}
await s3Helper.uploadToS3(
dest,
JSON.stringify(data),
"application/json",
plugin.config.dataBucket
);
}
}
},
},
{
hook: "requestComplete",
name: "uploadHtmlToS3",
description: "Uploads a html file to s3 bucket.",
priority: 1, // we want it to be last
run: async ({ settings, request, html, plugin }) => {
if (settings.build === true && settings.deploy === true) {
if (plugin.config.dataBucket && plugin.config.htmlBucket.length > 0) {
let dest = `${request.permalink.replace(/^\/+/, "")}index.html`;
if (plugin.config.deployId) {
dest = `${plugin.config.deployId}/${dest}`;
}
await s3Helper.uploadToS3(
dest,
html,
"text/html",
plugin.config.htmlBucket
);
}
}
},
},
],
config: {
dataBucket: process.env.AWS_S3_DATA_BUCKET || "",
htmlBucket: process.env.AWS_S3_BUCKET || "",
deployId: process.env.DEPLOY_ID || false,
},
};
This plugin registers function executions on two hooks, the dataComplete
hook and the requestComplete
hook. In each, it uploads the data or html to the s3 bucket specified in the user's elder.config.js
.
Within Elder.js, there is a subtle distinction between how different Svelte files are compiled.
./src/components/
folder and are called from within Svelte Templates and Svelte Layouts. (eg: ./src/components/Widget/Widget.svelte
)./src/routes/blog/Blog.svelte
) and are only rendered on the server because they receive props of data
, helpers
, request
, and settings
../src/layouts/
folder and are only rendered on the server because they receive props of data
, helpers
, request
, settings
, and templateHtml
.While this may seem complex, the reasoning for Svelte Templates and Svelte Layouts only being server rendered is because they receive sensitive props that may contain data you don't want written in your html. (database credentials, env variables, auth keys, etc)
On a practical level, most of your Svelte files will live in your ./src/components/
folder and you can hydrate them from within Svelte Templates or Svelte Layouts as defined below.
Elder.js give you fine grained control over what parts of your site are "hydrated" on the client and which aren't.
If you aren't sure what should be hydrated and what shouldn't, the general rule of thumb is that if a component needs to be interactive on the client, you need to hydrate it.
To hydrate a component, simply use the following markup in either your Svelte Template or Svelte Layout files. (eg: ./src/routes/blog/Blog.svelte
or ./src/layouts/Layout.svelte
)
// within any Svelte template
<MyComponent hydrate-client={{somethingCool: true}} />
In this example, the component MyComponent
will receive the props of somethingCool = true
which you'd access from within the component like so:
<!-- ./src/components/MyComponent.svelte -->
<script>
export let somethingCool;
</script>
In the above example, what will happen is that in your HTML, you'll see that Elder.js has mounted a new root component and set the initial props to {somethingCool: true}
.
This means that once the user visits the page generated by Elder.js, Svelte will make that specific component interactive.
There are two differences between partial hydration and the way most frameworks handle hydration:
The end result is generally smaller bundle/page sizes and less work for the main thread because we're only hydrating what is needed by the client instead of all of the data to build the page.
Note: All props needed by the Svelte component must be included in hydrate-client={{}}
and should be JSON.stringify()
friendly. This means no functions, cyclical references, etc.
// Doesn't work
<Component {ssrProp} hydrate-client={{ clientProp }} />
// Works
<Component hydrate-client={{ ssrProp, clientProp }} />
The environment variable 'process.env.componentType' will return browser
or server
depending on where the component is being rendered. process.env.componentType === 'server'
is the correct way to check if a component is rendering on the server.
To give you fine grained control over how a Svelte component behaves when it is mounted, the following hydrate-options
can be defined:
hydrate-options={{ loading: 'lazy' }}
This is the default config, uses intersection observer to 'lazily' mount the Svelte component.hydrate-options={{ loading: 'eager' }}
This would cause the component to be hydrated in a blocking manner as soon as the js is rendered.hydrate-options={{ loading: 'none' }}
This allows you to add the HTML from a Svelte component, but not to hydrate it on the client. (only really useful with helpers.inlineSvelteComponent
and possibly advanced shortcode usages.)hydrate-options={{ preload: true }}
This adds a preload to the head stack as outlined above... could be preloaded without forcing blocking.hydrate-options={{ preload: true, loading: 'eager' }}
This would preload and be blocking.hydrate-options={{ rootMargin: '500px', threshold: 0 }}
This would adjust the root margin of the intersection observer. Only usable with loading: 'lazy'On the homepage of elderguide.com, we use the following code to hydrate the autocomplete component:
// within elderguide.com’s Home.svelte
<HomeAutoComplete hydrate-client={{ nh_count: data.nh_count }} />
You can do the same for a component without any props by using:
<NoPropsHere hydrate-client={{}} />
At a high level, what is happening is that when the Svelte template components are compiled on the server, we've included a preprocessor that causes the Svelte compiler to instead render a div with a few specific elements and the prop passed into hydrate-client
is simply JSON.stringified.
Later when we go to render these templates, we look for the removed components, generate the server rendered version and include the client component in the generated JS with the props that were given in hydrate-client
.
Security Note: Whatever you pass to
hydrate-client
will get written to the HTML shipped to the browser viadevalue
. There are XSS and security considerations of passing data to the client.
If you are curious, the files to look at are: partialHydration.ts
and svelteComponent.ts.
The important thing to note is that still use Svelte variables in hydrate-client
as long as they can be processed by JSON.stringify
.
A common pitfall is to try and use slots while hydrating a component. This won't work because Svelte's mount code doesn't support slots during mounting.
To get around this, create a parent component without slots to hydrate, then import the component that uses slots within that file.
Remember, partial hydration is just a wrapper around Svelte's mount code.
Whether your content lives in markdown files, on Prismic, Contentful, WordPress, Strapi, your own CMS, at some point you or someone who is managing the content will want to add some 'functionality' to this otherwise static content.
These functionalities come in a few flavors:
datapoint
changes.Adding this type of functionality is a nightmare and is a huge source of content debt and tech debt for SEO sites.
To make these situations more approachable, Elder.js offers shortcodes.
If you aren't familiar with shortcodes, they are just strings that can wrap content or have their own attributes:
{{shortcode attribute="" /}}
{{shortcode attribute=""}}wraps{{/shortcode}}
NOTE: The {{
and }}
brackets vary from system to system and can be configured in your elder.config.js
. However the /
prefix for the closing bracket is not configurable. You may therefore need to translate shortcodes written in another format into this format expected by Elder.js, with a simple string replace strategy.
In Elder.js, shortcodes are added by defining them in a project's ./src/shortcode.js
or via plugins.
Adding a Component Directly in Static Content
Imagine you want to empower the content team to embed a Svelte widget
component anywhere within their content.
Out of the box, Elder.js adds a shortcode for this.
Simply tell them to add {{svelteComponent name="widget" props="{blue: true}" /}}
to their markup and Elder.js will hydrate and mount that component.
Adding custom HTML to style/wrap content or achieve design goals.
Imagine you're at work and the design team asks you to wrap small pieces of content with a wrapper of <div class="bg-gray mb-1 p-2">
to 100 pieces of content... but having seen this situation before, you realize that within 3 weeks it will probably need to be: <div class="bg-gray mb-2 p-3 somethingelse">
.
So to future proof this code change, you introduce a box
shortcode and you wrap your content in it like so:
{{box type="gray"}}
Your content here
{{/box}}
Then in your shortcodes.js
you add the following:
module.exports = [
// ./src/shortcodes.js
{
shortcode: "box",
run: async ({ props, content }) => {
if (props.type === "gray") {
return `<div class="bg-gray mb-1 p-2">${content}</div>`;
}
// note that this shortcode returns a string.
return content;
},
},
];
Updating Datapoints in Static Content
The most common type of content debt is data sensitive content debt.
This is where your otherwise static content needs to have some arbitrary data point updated within it.
This is a common use case for us here at ElderGuide.com.
On our content, we often need to write things like: The US has {{numberOfNursingHomes /}} nursing homes nationwide.
Using shortcodes to make sure {{numberOfNursingHomes /}}
is always up-to-date future proofs us from having this content debt.
Since our data comes from a database and all implementations will differ a bit, we'll steal the example from the Elder.js Template instead.
Imagine you need to always show the latest {{numberOfPages /}}
on your site but you don't want to update {{numberOfPages /}}
each time you publish a new blog post.
Here is how you'd create a shortcode using Elder.js internals to do just that:
module.exports = [
// ./src/shortcodes.js
{
shortcode: 'numberOfPages',
run: async ({ allRequests }) => {
// allRequests represents 'request' objects for all of the pages of our site, if we know the length of that we know the length of our site.
return allRequests.length,
},
},
]
Now, you can update your site to use {{numberOfPages /}}
and any time the page count changes, so will the placeholder.
Advanced: A Placeholder For External Data/Content
One of the most powerful usecases for shortcodes is to use them as a placeholder for external data fetching.
Imagine you want the ability to display your latest tweet in a specific spot across multiple pages.
You setup the {{latestTweet /}}
shortcode and instead of just returning the latest tweet, we also want to add some css and js to the page as well.
Here is the full power of how you'd implement this:
// import 'node-fetch', 'axios'
// ./src/shortcodes.js
module.exports = [
{
shortcode: "latestTweet",
run: async () => {
// const latestTweet = await fetch(fromTwitterApi);
// fetching the data is up to you...
// while shortcodes often return strings, they can also return objects like so:
return {
// this is what the shortcode is replaced with. You CAN return an empty string.
html: `<div class="latest-tweet">${latestTweet}</div>`,
// You can add css here and it will get written to the head.
css:
".box{border:1px solid red; padding: 1rem; margin: 1rem 0;} .box.yellow {background: lightyellow;}",
// Javascript that is added to the footer via the customJsStack.
js: "<script>var test = true;</script>",
// Arbitrary HTML that is added to the head via the headStack
head: '<meta test="true"/>',
};
},
},
];
Shortcodes are defined by users by adding them to the array in their ./src/shortcodes.js
.
In the spec below, there is a simple example that returns a string and a full example that returns an object.
// ./src/shortcodes.js
module.exports = [
{
shortcode: "simple", // the shortcode name: results in {{simple /}}
run: async ({
props, // the attributes of the shortcode
content, // the content wrapped by the shortcode.
}) => {
return '<div class="simple">simple</div>';
},
},
{
shortcode: "returnsObject", // this is the shortcode name.
run: async ({
props, // the attributes defined on the shortcode.
content, // the content wrapped by the shortcode.
request, // the 'request' object for the page requested.
query, // the 'query' object
helpers, // the 'helpers' object with any user helpers and Elder.js helpers
settings, // settings
allRequests, // all of the 'request' objects Elder.js has.
}) => {
// while shortcodes often return strings, they can also return objects like so:
return {
html: "", // this is what the shortcode is replaced with. You CAN return an empty string.
css: "", // You can add css here and it will get written to the head.
js: "", // Javascript that is added to the footer via the customJsStack.
head: "", // Arbitrary HTML that is added to the head via the headStack
};
},
},
];
Sometimes you'll want to use a shortcode from within your Svelte route files or layouts.
To do this, there is a helper function to inline shortcodes in a "Svelte Friendly" way:
<!-- Layout.svelte or RouteName.svelte -->
<script>
export let helpers;
</script>
{@html helpers.shortcode({ name: 'shortcodeName', props: { background: "blue" }, content: "Inner content" })}
This results in {{shortcodeName background="blue"}}Inner content{{/shortcodeName}}
being output in the HTML and picked up by the shortcode parser during the page generation process.
Elder.js has 4 different modes of handling CSS each of which can be set in your elder.config.js
by settings the desired value on the css
key.
file
: (default) All of the CSS from Svelte components and imported into Svelte components is written to a file and included in the head.lazy
: All of the CSS from Svelte components and imported into Svelte components is written to a file and included in the head but is lazily loaded. (Designed for use with the Elder.js critical path plugin.)inline
One where all CSS a component depends on will be added to the head. This is how Elder.js started so this is the default. This is a great option for serverless rendering.none
no css handling.Source maps are included where available when process.env.NODE_ENV !== "PRODUCTION"
.
The best practice for including external CSS is to simply import it:
// any svelte file.
import "./path/to/css/file.css";
If the CSS isn't appearing for some reason, try rerunning Rollup.
Here is a detailed overview of how data flows through an Elder.js application from 'bootstrap' all the way to a generated HTML page.
Below is the example route.js
file we'll be following the flow of.
// `/routes/blog/route.js` <-- NOTE: 'blog' is the route name.
module.exports = {
all: async ({ query, settings data, helpers }) => {
// await query.db(`your implementation here`) or await query.api(`get data`);
// something that returns an array of the minimum required data for the route.
return [{slug: 'why-kitten-rock'}];
},
permalink: ({ request, settings }) => {
return `/blog/${request.slug}/`;
},
data: async ({request, query, settings, helpers, data }) =>{
// we'll look at this function below.
}
// template: 'Blog.svelte' is assumed if not defined. (note: capitalized first letter.)
// layout: 'Layout.svelte' is assumed if not defined.
}
During this process, Elder.js validates all of the routes, plugins, hooks. It then runs the 'bootstrap' hook.
Finally, the all
function for each route is executed.
Together, the aggregate result of each route's all
function is referred to as allRequests
.
allRequests
Hook is RunThis allows users to modify the allRequests
array. If you modify or add to this array of objects, make sure each result has a 'request.route' key.
Once Elder.js has a full list of requests, it then builds permalinks and full 'request' objects that will be consumed by hooks, data
functions, Svelte templates, and Svelte layouts.
The full request object will look something like so (truncated for clarity):
request = {
slug: `why-kittens-rock`,
// ... any other keys from the `request` object returned from the `all` function.
// below is then added by Elder.js
permalink: "/blog/why-kittens-rock",
route: "blog",
type: "build", // server or build.
};
It is important to note that all of the params of the 'request' objects returned by the all
function will be present in the 'request' object even though our example only uses slug
.
data
function is ExecutedThe data flows through all of the hooks until it reaches a route's data
function.
How you modify the data in your data
function is up to you. Anything you can do in Node.js, you can do here.
module.exports = async ({ query, settings, request, data }) => {
// do magic to get data from your data store.
// const yourData = await query.db(`SELECT * FROM city WHERE slug = $1`, [request.slug])
// or
// const yourData = await query.api.get(`https://yourdata.com/api/city/${request.slug}`)...
const yourData = {
sweet: "Golden Metal",
};
return yourData;
};
The data hook is generally used by plugins to modify data for a route.
If for some reason you have an empty data object, check that your plugins and hooks aren't returning just their data, instead of using a pattern like so:
return {
data: {
...data, // this is from the parameter of the hook function.
...additionalData, // this data from the hook or plugin.
},
};
You can debug this by setting debug.hooks: true
in your elder.config.js
.
In this example, ./src/routes/blog/Blog.svelte
may look like this:
<script>
export let data; // here is the 'data' object we've been following.
export let settings; // Elder.js settings
export let helpers; // Elder.js helpers and user helpers.
export let request; // 'request' object from above. ....
</script>
Svelte layouts receive the same props as the template file but also include a templateHtml
prop which would be the html from Blog.svelte
in this example.
All further hooks are run until the 'request' has been completed.
This includes user hooks, system hooks, and plugin hooks.
Below are details on common specifications and config requirements.
elder.config.js
By default, Elder.js looks for an elder.config.js
file in your project root and will import any settings there and merge them with the default.
Below is what the default configuration file looks like. This is automatically generated if an elder.config.js
file is missing.
module.exports = {
origin: "", // The domain your site is hosted on. https://yourdomain.com.
rootDir: "process.cwd()", // Here your package.json lives.
distDir: "public", // Where should files be written? This represents the "root" of your site and where your html will be built.
srcDir: "src", // Where Elder.js should find it's expected file structure. If you are using a build step such as typescript on your project you may need to edit this.
debug: {
stacks: false, // Outputs details of stack processing in the console.
hooks: false, // Output details of hook execution in the console.
shortcodes: false, // Output details of shortcode execution in the console.
performance: false, // Outputs a detailed speed report on each pageload.
build: false, // Displays detailed build information for each worker.
automagic: false, // Displays settings or actions that are automagically done to help with debugging.
}
hooks: {
disable: [], // This is an array of hooks to be excluded from execution. To be clear this isn't the "hook" name found in the "hookInterface.ts" file but instead the name of the system user plugin or route hook that is defined. For instance if you wanted to by name to prevent the system hook that writes html to the public folder during builds from being run you would add "internalWriteFile" to this array.
}
server: {
prefix: "", // If Elder.js should serve all pages with a prefix.
}
build: {
numberOfWorkers: -1, // This controls the number of worker processes spun up during build. It accepts negative numbers to represent the number of cpus minus the given number. Or the total number of processes to spin up.
shuffleRequests: false, // If you have some pages that take longer to generate than others you may want to shuffle your requests so they are spread out more evenly across processes when building.
}
shortcodes: {
openPattern: "{{", // Opening pattern for identifying shortcodes in html output.
closePattern: "}}", // closing pattern for identifying shortcodes in html output.
}
plugins: {}, // Used to define Elder.js plugins.
legacy: false, // EXPERIMENTAL: This implementation will not work in all scenarios may change in the future or be dropped completely... but Elder.js will attempt to add an IE11/nomodule friendly iife bundle for each component on production rollup builds. Please note currently shared stores do not work but see this issue: https://github.com/Elderjs/elderjs/issues/44#issue-709580756 and you may need to bring your own polyfills.
css: "file", // How should css found in svelte files be handled? 'inline' emits styles into the head. 'file' adds a file include into the head. 'none' doesn't do anything with the styles.
}
Elder.js expects a specific file structure outlined below. This is why we recommend you start with the Elder.js template.
You can configure or rename your src/build folders in your elder.config.js
.
Project Root
| elder.config.js
| package.json
| rollup.config.js
| ... (other common stuff, .gitignore, svelte.config.js... etc)
| -- src
| -- | -- build.js
| -- | -- server.js
| -- | -- hooks.js
| -- | -- shortcodes.js
| -- helpers
| -- | -- index.js
| -- | -- ...
| -- layouts
| -- | -- Layout.svelte
| -- routes
| -- | -- [route] ('blog' in this example)
| -- | -- | -- Blog.svelte
| -- | -- | -- route.js
| -- plugins
| -- | -- [plugin] ('elderjs-plugin-markdown' for example)
| -- | -- | -- index.js
| -- components
| -- | -- [component] ('Contact' in this example)
| -- | -- | -- Contact.svelte
(optional/recommended)
| -- assets
| -- | -- files to be copied to your public folder.
Hooks are the core of how to make site level customizations. Below is the default spec for a hook.
module.exports = {
hook: "", // The hook the defined "run" function should be executed on.
name: "", // A user friendly name of the function to be run.
description: "", // A description of what the function does.
priority: 50, // The priority level a hook should run at. The highest priority is 100 and 1 is the lowest priority.
run: ()=> {}, // The function to be run on the hook.
$meta: {
type: "", // What type of hook this is. Defined by Elder.js for debugging.
addedBy: "", // Where the hook was added from. Defined by Elder.js for debugging.
}
}
Plugins are a bundle of hooks with their own closure scope based on the object that is returned by the init()
function. This means that all hooks and shortcodes receive the plugin definition returned by the init()
function and are able to store properties in that scope throughout the hook lifecycle. Below is the default specification for a plugin.
module.exports = {
name: "", // The name of the plugin.
description: "", // A description of the plugin.
init: ()=> {}, // A sync function that handles the plugin init. Receives plugin definition. plugin.settings contains Elder.js config. plugin.config contains plugin config
routes: {}, // (optional) Any routes the plugin is adding.
hooks: [], // An array of hooks.
config: {}, // (optional) An object of default configs. These will be used when none are set in their elder.config.js.
shortcodes: [], // Array of shortcodes
}
Routes can be defined by plugins or by including a ./src/[routeName]/route.js
file.
module.exports = {
template: "", // Svelte file for your route. Defaults to RouteName.svelte if not defined.
all: ()=> {}, // A sync/async function that returns an array of all of the 'request objects' for this route.
permalink: ()=> {}, // Sync function that turns request objects from the all() function into permalinks which are relative to the site root
data: ()=> {}, // Async/sync function that returns a JS object. Can also be a plain JS object.
}
Shortcodes are a great way to future proof your content. Below is the default shortcode specification. These should be defined in the array exported by your ./src/shortcodes.js
module.exports = [{
shortcode: "", // The 'name' of the shortcode. {{name /}}
run: ()=> {}, // A sync/async function that returns the html css js and head to be added to the html.
}]
name
and description
fieldsIn various places such as on hooks, plugins, and stacks, you'll see that Elder.js requires name
and description
fields.
While this requirement may seem like an added development burden initially, it makes it extremely easy for you to publicly share your hooks or bundle them into plugins in a way others can use. (Plus your future self will thank you when you need to modify your code a year from now.)
These fields are also used to generate the pretty printouts of how long each hook is adding to your page generation time that you see when you enable debug.performance = true
in your elder.config.js
.
As a team, we've found build times to be especially important when building 10k+ page sites as 100ms adds 16+ minutes to your build time. What gets measured gets managed... and we know faster deploys leads to deploying more often.
In a few places, you may see that Elder.js is using 'stacks.' These are just our internal way of processing strings that need to be concatenated.
Here are the type defs:
export type StackItem = {
source: string;
string: string;
priority: number;
name: string;
};
export type Stack = Array<StackItem>;
In short, a stack is an array of objects that are concatenated together in order. Think of it like a queue. An item with a priority of 1 is the lowest priority. An item with a priority of 100 is the highest.
Hooks can add items to the stack, when the stack is processed, it is sorted in order of priority and then all strings are concatenated.
Elder.js does quite a bit of automatic behavior much of which is based on using the standard filesystem.
Instead of burying this magic, things that happen automagically are logged to the console unless you set debug.automagic = false
in your elder.config.js
file. That said, here are some of the things that happen automatically:
./src/helpers/index.js
are imported and added to the helpers
object which is available in the data
functions and all hooks../src/hooks.js
file, ./routes/[routeName]/route.js
files, and plugins are imported and validated.By default, Elder.js adds a few items to the helpers
object that is available in hooks, Svelte templates/layouts, and data
functions.
permalink
: A permalink resolver: helpers.permalink[routeName]({requestObject})
. Simply pass in a request object and it'll resolve the permalink. It is often used like so helpers.permalink.blog({slug: 'kittens-rock'})
.shortcode
: A more comfortable way to use shortcodes within Svelte files. Common usage within a .svelte file may look like : { @html helpers.shortcode({name: 'box', props: {class: "yellow"}, content: "content string here" }) }
. If you are using the default Elder.js shortcode brackets, this would output {{box class='yellow'}}content string here{{/box}}
which would be parsed like any other shortcode.inlineSvelteComponent
: This helper is mainly useful when needing to add a Svelte component to your html via, hooks, plugins, or custom shortcodes. All of the options available when hydrating a component are available with this helper, it simply outputs the required hydration html so that Elder.js picks it up and hydrates the client. helpers.inlineSvelteComponent({name: 'Foo', props: { anything: true }, options: { preload: true, eager: true }})
server
Elder.js was built as a static site generator, but it offers a built-in server that is pretty snappy past the initial bootstrap phase.
This can be used to power server rendered (SSR) apps or used to preview the output of your static site without having to build.
You can see how this functionality is utilized in the elderjs-template
.
build
The build
function exported from Elder.js uses Node's cluster module to use multiple processes to build your site.
In short, here is how build
works at a high level:
The main process runs through bootstrap and collects allRequests
. It then spins up workers and divides the requests evenly across these workers. It then waits for the workers to complete, runs any hooks for after build and completes.
There are two notable config options: numberOfWorkers
and shuffleRequests
which you can read about in the config section.
getRollupConfig
A function that generates all of the Elder.js required rollup
output.
See the clonable template for the minimum viable rollup config.
If you need to add values to be replaced during bundling, you can do so like this:
const { getRollupConfig } = require("@elderjs/elderjs");
const svelteConfig = require("./svelte.config");
module.exports = [
...getRollupConfig({
svelteConfig,
rollupConfig: {
replacements: { "http://localhost:4020": "https://production.com" },
// object keys are replaced by values.
},
}),
];
getElderConfig
A helper function that returns the user's elder.config.js
with defaults added in where they aren't defined.
Elder.js uses the svelte-preprocess package to enable the use of typescript, scss, postcss or any other preprocessor that the svelte-preprocess package supports. To enable a preprocessor, you need to first install the dependecies for it, for example, to enable typescript and sass, you need to install the following dependecies
npm install -D typescript node-sass
For more info about what dependecies you need to install to enable the preprocessor of your choice, you can look at the installation guide of svelte-preprocess
Then you can start using them in your svelte file by using lang="ts" or lang="scss" in your script and style block respectively. To change the settings of your preprocessor, open the svelte.config.js file and configure it using the object passed to sveltePreprocess like this
const sveltePreprocess = require("svelte-preprocess");
module.exports = {
preprocess: [
sveltePreprocess({
postcss: {
plugins: [require("autoprefixer")],
},
}),
],
};
Go through the svelte-preprocess docs to learn about all the supported features.
You should be able to use typescript in svelte files by following the instructions above but for customizing your typescript config, we recommend you rename the sample.tsconfig.json in the default template to tsconfig.json and use that as your typescript config. You can also extend [svelte/tsconfig] (https://www.npmjs.com/package/@tsconfig/svelte) which is the recommended config by the Svelte team. To run typescript checks as part of your linting process or CI/CD step, install the svelte-check package and add a script to your package.json to run this tool.
"type-check": "svelte-check"
Typescript support for non-svelte files is coming soon, track the progress here
You can disable core Elder.js hooks, just like any other hook. Simply add the hook name
to the hooks.disable
array in your elder.config.js
.
A full list of all of the hooks can be found in the hooks.ts.
For instance, if you wanted to disable the intersection observer, simply make sure your hooks.disable
array includes elderAddDefaultIntersectionObserver
as shown below:
// elder.config.js, truncated
hooks: {
disable: ['elderAddDefaultIntersectionObserver'],
},
By default Elder.js emits hashed files for assets under its control into the ./${distDir}/_elderjs/
folder.
This allows for aggressive caching of client side assets such as cache-control public, max-age=31536000, immutable
.
The template project has a hook that copies your ./assets/
folder to the distDir
location defined in your elder.config.js
.
If you’re familiar with Sapper, you may be accustomed to storing your data fetching and manipulation logic in your Svelte files.
Initially, when designing Elder.js, we experimented with this approach but found that a separate data
function, which returned a single data object ready to be consumed by your svelte template was a cleaner separation of concerns.
data
functions for fetching, manipulating, and returning data. Svelte for handling data presentation, interactivity, and reactivity.
Note: If you really want to do data fetching and manipulation in your templates, nothing is stopping you.
Sapper is a full stack Svelte framework that does server side rendering and supports static exporting of your site.
While we found Sapper to be a nice solution for small sites, when we tried to use it to build out elderguide.com, we hit some major roadblocks:
For data fetching, Sapper uses a preload
function to fetch a page’s initial props and build the page. This is done to enable client side routing and full client side hydration as this preload function is designed to run on both the client and the server.
This is a pretty cool feature for Apps but for a static SEO focused site, it was overkill and cause huge problems with our dataflow when paired with the chosen routing system.
Elder.js solves these roadblocks.
data
function where you can do your database queries, read markdown files, and do whatever magic you need to prepare your data for display.If your project is going to be querying its data from a database, we recommend using the bootstrap hook and adding a connection to your database on the “query” property. See this hook example above.
In certain cases, you may want to customize the shell that Elder.js writes the templateHtml
and layoutHtml
to.
Often a quick regex on the html
hook is enough, but if you'd like complete control, you can overwrite how Elder.js compiles the html by:
elderCompileHtml
to the hooks.disable
array in your elder.config.js
.compileHtml
hook and implement your desired functionality. (Look for elderCompileHtml
in @elderjs/elderjs's hooks.ts
file.)Below are the breaking changes between v1 and earlier versions:
link
helper is removed from templates. You can access it at helpers.permalinks
the same way as before so link.blog({slug: "foo"})
becomes helpers.permalinks.blog({slug: "foo"})
routeHtml
or routeHTML
in the Layout.svelte in the Elder.js template has been changed to templateHtml
. If you're getting no output from your templates, this is your issue. Rename this variable and things should work again.process.browser
replacements in rollup.config are now 'process.env.componentType' which will return browser
or server
. Remember process.env variables are strings. so process.env.componentType === 'server'
is the correct way to check if a component is rendering on the server.elder.config.js
. If you defined anything in the elder.config.js
under the locations
key, you'll need to rework those into the distDir
, srcDir
, and rootDir
.siteUrl
in elder.config.js was changed to origin
and you'll get an error if you don't set it.srcDir
to the build folder found in your tsconfig
../src/assets/
folder has been moved to ./assets/
(project root).copyAssetsToPublic
hook as shown below.rollup.config.js
to be updated as shown below. // hooks.js
// replace your old copyAssetsToPublic
{
hook: 'bootstrap',
name: 'copyAssetsToPublic',
description:
'Copies ./assets/ to the "distDir" defined in the elder.config.js. This function helps support the live reload process.',
run: ({ settings }) => {
// note that this function doesn't manipulate any props or return anything.
// It is just executed on the 'bootstrap' hook which runs once when Elder.js is starting.
// copy assets folder to public destination
glob.sync(path.resolve(settings.rootDir, './assets/**/*')).forEach((file) => {
const parsed = path.parse(file);
// Only write the file/folder structure if it has an extension
if (parsed.ext && parsed.ext.length > 0) {
const relativeToAssetsFolder = path.relative(path.join(settings.rootDir, './assets'), file);
const outputPath = path.resolve(settings.distDir, relativeToAssetsFolder);
fs.ensureDirSync(path.parse(outputPath).dir);
fs.outputFileSync(outputPath, fs.readFileSync(file));
}
});
},
},
// replace your old rollup.config.js
const { getRollupConfig } = require("@elderjs/elderjs");
const svelteConfig = require("./svelte.config");
module.exports = [...getRollupConfig({ svelteConfig })];