Elder.js: An Opinionated, SEO focused, Svelte Framework

Elder.js is an opinionated static site generator and web framework built with SEO in mind. (Supports SSR and Static Site Generation.)

Features:

  • Build hooks allow you to plug into any part of entire page generation process and customize as needed.
  • A Highly Optimized Build Process: that will span as many CPU cores as you can throw at it to make building your site as fast as possible. For reference Elder.js easily generates a data intensive 18,000 page site in 8 minutes using a budget 4 core VM.
  • Svelte Everywhere: Use Svelte for your SSR templates and with partial hydration on the client for tiny html/bundle sizes.
  • Straightforward Data Flow: By simply associating a 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.
  • Community Plugins: Easily extend what your Elder.js site can do by adding prebuilt plugins to your site.
  • Shortcodes: Future proof your content, whether it lives in a CMS or in static files using smart placeholders.
  • 0KB JS: Defaults to 0KB of JS if your page doesn’t need JS.
  • Partial Hydration: Unlike most frameworks, Elder.js lets you hydrate just the parts of the client that need to be interactive allowing you to dramatically reduce your payloads while still having full control over component lazy-loading, preloading, and eager-loading.

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.

Project Status: Stable

Elder.js is stable and production ready.

It is being used on 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 if breaking changes any time soon, but will be shipping bug fixes and incremental changes that are mostly “under the hood.”

As 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.

Getting Started:

The quickest way to get started is to get started with the Elder.js template using degit:

Here is a demo of the template: https://elderjs.netlify.app/

Step 1: Clone Template

npx degit Elderjs/template elderjs-app
cd elderjs-app

Step 2: Install Dependencies

npm install ## or just yarn

Step 3: Start the Project

npm start

Navigate to http://localhost:3000. You should see your app running.

Developing using the Template:

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.

To Build/Serve HTML:

npm run build

Let the build finish.

npx sirv-cli public

Why We Built 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:

  1. Most SSGs are built for either simple sites/blogs or for full scale “app frameworks” that have added an ‘export’ process added as an afterthought.
  2. Fetching data from multiple sources (dbs, apis, config files, markdown files) can lead to major code spaghetti.
  3. Client side routing adds a huge amount of complexity (and bundle size) to initial loads for very little SEO benefit. If you aren’t building an App, why would we want to fully hydrate our JS framework just for faster routing? Browsers are great at routing… we should only be hydrating things that need to be hydrated.

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 thought 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.

Routes

At the core of any site are it’s “routes” or templates.

In Elder.js a route is made up of 2 files that live in your route folder: ./src/routes/${routeName}/ folder.

They are:

  1. A route.js file. This is where you define route details such as the route’s permalink function, all function and data function.
  2. A Svelte component to be used as a template matches the ${routeName}. eg: ./src/routes/blog/Blog.svelte (from here on out we refer to these specific Svelte components as “Svelte Templates”)

Route.js

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());

Why Routing Differs from Express-like Frameworks

Elder.js’ approach to routing is unconventional but it offers several distinct advantages, the two biggest are:

  1. Unlike traditional ‘parameter based’ routing, Elder.js’ does not have to crawl all of the links of a site to know what pages need to be generated. This allows for fully parallelized build times that scale with CPU resources. (As of October 2020, ElderGuide.com has ~20k pages and builds in 1 minute 22 seconds.)
  2. Users have full control over their URL structure. No complex regex is needed to have /senior-living/:facilityId/ and /senior-living/:articleId/ and /senior-living/:parentCompanyId/. This also makes i18n and l10n much more approachable.

Route.js Best Practices:

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 generate a page.

Skinny request objects. Fat data 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 recommend 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:

  1. The language of the page being generated
  2. The City slug
  3. The Country slug

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 in 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 just data.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](https://elderguide.com/tech/elderjs/#hook-example-1-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 boostrap hook documented later in this guide.

Assuming you have populated the data.cities with an array of cities on the boostrap 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 boostrap is available on all routes.

all() Function Spec

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>;
}

permalink() Function Spec

Here is the function signature for a route.js permalink function:

permalink: ({ request, settings }): 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.
  return String;
};

data() Function Spec

Whether you’re building a personal blog or complex data driven SEO site, a route’s data function is the recommend 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;
};

Hooks: How to Customize Elder.js

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).

The Goal of Elder.js Hooks

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.

Hook Interface: the mutable and props Arrays

Each 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 the mutable array are passed as a Proxy.

Hook Lifecycle

Elder.js hook Lifecycle

#svelte #programming #web-development #developer

What is GEEK

Buddha Community

Elder.js: An Opinionated, SEO focused, Svelte Framework

NBB: Ad-hoc CLJS Scripting on Node.js

Nbb

Not babashka. Node.js babashka!?

Ad-hoc CLJS scripting on Node.js.

Status

Experimental. Please report issues here.

Goals and features

Nbb's main goal is to make it easy to get started with ad hoc CLJS scripting on Node.js.

Additional goals and features are:

  • Fast startup without relying on a custom version of Node.js.
  • Small artifact (current size is around 1.2MB).
  • First class macros.
  • Support building small TUI apps using Reagent.
  • Complement babashka with libraries from the Node.js ecosystem.

Requirements

Nbb requires Node.js v12 or newer.

How does this tool work?

CLJS code is evaluated through SCI, the same interpreter that powers babashka. Because SCI works with advanced compilation, the bundle size, especially when combined with other dependencies, is smaller than what you get with self-hosted CLJS. That makes startup faster. The trade-off is that execution is less performant and that only a subset of CLJS is available (e.g. no deftype, yet).

Usage

Install nbb from NPM:

$ npm install nbb -g

Omit -g for a local install.

Try out an expression:

$ nbb -e '(+ 1 2 3)'
6

And then install some other NPM libraries to use in the script. E.g.:

$ npm install csv-parse shelljs zx

Create a script which uses the NPM libraries:

(ns script
  (:require ["csv-parse/lib/sync$default" :as csv-parse]
            ["fs" :as fs]
            ["path" :as path]
            ["shelljs$default" :as sh]
            ["term-size$default" :as term-size]
            ["zx$default" :as zx]
            ["zx$fs" :as zxfs]
            [nbb.core :refer [*file*]]))

(prn (path/resolve "."))

(prn (term-size))

(println (count (str (fs/readFileSync *file*))))

(prn (sh/ls "."))

(prn (csv-parse "foo,bar"))

(prn (zxfs/existsSync *file*))

(zx/$ #js ["ls"])

Call the script:

$ nbb script.cljs
"/private/tmp/test-script"
#js {:columns 216, :rows 47}
510
#js ["node_modules" "package-lock.json" "package.json" "script.cljs"]
#js [#js ["foo" "bar"]]
true
$ ls
node_modules
package-lock.json
package.json
script.cljs

Macros

Nbb has first class support for macros: you can define them right inside your .cljs file, like you are used to from JVM Clojure. Consider the plet macro to make working with promises more palatable:

(defmacro plet
  [bindings & body]
  (let [binding-pairs (reverse (partition 2 bindings))
        body (cons 'do body)]
    (reduce (fn [body [sym expr]]
              (let [expr (list '.resolve 'js/Promise expr)]
                (list '.then expr (list 'clojure.core/fn (vector sym)
                                        body))))
            body
            binding-pairs)))

Using this macro we can look async code more like sync code. Consider this puppeteer example:

(-> (.launch puppeteer)
      (.then (fn [browser]
               (-> (.newPage browser)
                   (.then (fn [page]
                            (-> (.goto page "https://clojure.org")
                                (.then #(.screenshot page #js{:path "screenshot.png"}))
                                (.catch #(js/console.log %))
                                (.then #(.close browser)))))))))

Using plet this becomes:

(plet [browser (.launch puppeteer)
       page (.newPage browser)
       _ (.goto page "https://clojure.org")
       _ (-> (.screenshot page #js{:path "screenshot.png"})
             (.catch #(js/console.log %)))]
      (.close browser))

See the puppeteer example for the full code.

Since v0.0.36, nbb includes promesa which is a library to deal with promises. The above plet macro is similar to promesa.core/let.

Startup time

$ time nbb -e '(+ 1 2 3)'
6
nbb -e '(+ 1 2 3)'   0.17s  user 0.02s system 109% cpu 0.168 total

The baseline startup time for a script is about 170ms seconds on my laptop. When invoked via npx this adds another 300ms or so, so for faster startup, either use a globally installed nbb or use $(npm bin)/nbb script.cljs to bypass npx.

Dependencies

NPM dependencies

Nbb does not depend on any NPM dependencies. All NPM libraries loaded by a script are resolved relative to that script. When using the Reagent module, React is resolved in the same way as any other NPM library.

Classpath

To load .cljs files from local paths or dependencies, you can use the --classpath argument. The current dir is added to the classpath automatically. So if there is a file foo/bar.cljs relative to your current dir, then you can load it via (:require [foo.bar :as fb]). Note that nbb uses the same naming conventions for namespaces and directories as other Clojure tools: foo-bar in the namespace name becomes foo_bar in the directory name.

To load dependencies from the Clojure ecosystem, you can use the Clojure CLI or babashka to download them and produce a classpath:

$ classpath="$(clojure -A:nbb -Spath -Sdeps '{:aliases {:nbb {:replace-deps {com.github.seancorfield/honeysql {:git/tag "v2.0.0-rc5" :git/sha "01c3a55"}}}}}')"

and then feed it to the --classpath argument:

$ nbb --classpath "$classpath" -e "(require '[honey.sql :as sql]) (sql/format {:select :foo :from :bar :where [:= :baz 2]})"
["SELECT foo FROM bar WHERE baz = ?" 2]

Currently nbb only reads from directories, not jar files, so you are encouraged to use git libs. Support for .jar files will be added later.

Current file

The name of the file that is currently being executed is available via nbb.core/*file* or on the metadata of vars:

(ns foo
  (:require [nbb.core :refer [*file*]]))

(prn *file*) ;; "/private/tmp/foo.cljs"

(defn f [])
(prn (:file (meta #'f))) ;; "/private/tmp/foo.cljs"

Reagent

Nbb includes reagent.core which will be lazily loaded when required. You can use this together with ink to create a TUI application:

$ npm install ink

ink-demo.cljs:

(ns ink-demo
  (:require ["ink" :refer [render Text]]
            [reagent.core :as r]))

(defonce state (r/atom 0))

(doseq [n (range 1 11)]
  (js/setTimeout #(swap! state inc) (* n 500)))

(defn hello []
  [:> Text {:color "green"} "Hello, world! " @state])

(render (r/as-element [hello]))

Promesa

Working with callbacks and promises can become tedious. Since nbb v0.0.36 the promesa.core namespace is included with the let and do! macros. An example:

(ns prom
  (:require [promesa.core :as p]))

(defn sleep [ms]
  (js/Promise.
   (fn [resolve _]
     (js/setTimeout resolve ms))))

(defn do-stuff
  []
  (p/do!
   (println "Doing stuff which takes a while")
   (sleep 1000)
   1))

(p/let [a (do-stuff)
        b (inc a)
        c (do-stuff)
        d (+ b c)]
  (prn d))
$ nbb prom.cljs
Doing stuff which takes a while
Doing stuff which takes a while
3

Also see API docs.

Js-interop

Since nbb v0.0.75 applied-science/js-interop is available:

(ns example
  (:require [applied-science.js-interop :as j]))

(def o (j/lit {:a 1 :b 2 :c {:d 1}}))

(prn (j/select-keys o [:a :b])) ;; #js {:a 1, :b 2}
(prn (j/get-in o [:c :d])) ;; 1

Most of this library is supported in nbb, except the following:

  • destructuring using :syms
  • property access using .-x notation. In nbb, you must use keywords.

See the example of what is currently supported.

Examples

See the examples directory for small examples.

Also check out these projects built with nbb:

API

See API documentation.

Migrating to shadow-cljs

See this gist on how to convert an nbb script or project to shadow-cljs.

Build

Prequisites:

  • babashka >= 0.4.0
  • Clojure CLI >= 1.10.3.933
  • Node.js 16.5.0 (lower version may work, but this is the one I used to build)

To build:

  • Clone and cd into this repo
  • bb release

Run bb tasks for more project-related tasks.

Download Details:
Author: borkdude
Download Link: Download The Source Code
Official Website: https://github.com/borkdude/nbb 
License: EPL-1.0

#node #javascript

Jeremy  Reilly

Jeremy Reilly

1606890730

Exploring Elder.js, the SEO-focused Svelte Framework

Elder.js is a Svelte-based SSG for Jamstack sites, particularly suited to complex, data-intensive websites that require robust SEO.

Welcome to the world of building with the Jamstack. Originally denoting JavaScript, APIs, and markup, the Jamstack architecture enables a faster, more secure, and scalable web. Jamstack applications are very fast because they are pre-rendered, which means all the frontend components and assets are pre-built into highly optimized static pages.

With this setup, sites can now be served via a global CDN. Jamstack apps also support a clean and minimal architecture, eliminating unneeded resource allocation since the entire frontend is isolated from the build process. This, of course, comes with many benefits, least of all cost savings on infrastructure and maintenance.

Elder.js, one among many static site generators for Jamstack sites, provides a better solution for complex, data-intensive websites for which SEO is of particular importance. From its website, Elder.js solves the problem of building highly complex, SEO-oriented sites with anywhere from 10 to 100K pages.

Introducing Elder.js

Built on top of Jamstack technologies, Elder.js was born of the need for a more viable solution to building complex, data-oriented, search-optimized apps. The creator of the framework felt the need for a standard way to build static sites of all sizes, without having to bother with the number of pages or amount of data involved.

Elder.js is based on the Svelte template. Svelte is a JavaScript web framework with a whole new approach to building UIs; templates are built on top of HTML with a data layer. In turn, Svelte converts application code into JavaScript at build/compile time.

According to the author, the scope of Elder.js is limited to building a pluggable static site generator/server-side rendered framework for Svelte with SEO as a major priority. Elder basks in the numerous benefits of this framework and comes with some other impressive features, including:

  1. An optimized and customizable build process, which can span multiple CPU cores
  2. Svelte templates with partial hydration in the mix. Hydration allows us to hydrate parts of the client that need to be interactive, allowing reduced payloads with the benefits of component lazy loading, tiny bundles, etc.
  3. Intuitive data layer with the possibility of multiple data sources
  4. Prioritized support for SSR and SSG
  5. Pre-built hooks present in the entire page generation process, allowing easy page customization at every step of the development process

Prerequisites

Before we begin exploring Elder.js, readers should be quite familiar with Svelte and how to build minimal applications with this web framework. Also, while it would be a big plus to know a little bit of building static sites or working with the Jamstack in general, this title covers these concepts and ideas, and are therefore not absolutely necessary for readers to know.

Getting started with Elder.js

To get started with Elder.js, we can make use of degit, which allows for quick scaffolding of Git-based projects by making copies of Git repositories. You might be wondering why we’re not using the native git clone functionality. Well, degit fetches only the relevant parts from a Git repo by downloading only the latest commit instead of the entire Git history.

#svelte #javascript #seo #web-development #developer

Elder.js: An Opinionated, SEO focused, Svelte Framework

Elder.js is an opinionated static site generator and web framework built with SEO in mind. (Supports SSR and Static Site Generation.)

Features:

  • Build hooks allow you to plug into any part of entire page generation process and customize as needed.
  • A Highly Optimized Build Process: that will span as many CPU cores as you can throw at it to make building your site as fast as possible. For reference Elder.js easily generates a data intensive 18,000 page site in 8 minutes using a budget 4 core VM.
  • Svelte Everywhere: Use Svelte for your SSR templates and with partial hydration on the client for tiny html/bundle sizes.
  • Straightforward Data Flow: By simply associating a 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.
  • Community Plugins: Easily extend what your Elder.js site can do by adding prebuilt plugins to your site.
  • Shortcodes: Future proof your content, whether it lives in a CMS or in static files using smart placeholders.
  • 0KB JS: Defaults to 0KB of JS if your page doesn’t need JS.
  • Partial Hydration: Unlike most frameworks, Elder.js lets you hydrate just the parts of the client that need to be interactive allowing you to dramatically reduce your payloads while still having full control over component lazy-loading, preloading, and eager-loading.

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.

Project Status: Stable

Elder.js is stable and production ready.

It is being used on 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 if breaking changes any time soon, but will be shipping bug fixes and incremental changes that are mostly “under the hood.”

As 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.

Getting Started:

The quickest way to get started is to get started with the Elder.js template using degit:

Here is a demo of the template: https://elderjs.netlify.app/

Step 1: Clone Template

npx degit Elderjs/template elderjs-app
cd elderjs-app

Step 2: Install Dependencies

npm install ## or just yarn

Step 3: Start the Project

npm start

Navigate to http://localhost:3000. You should see your app running.

Developing using the Template:

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.

To Build/Serve HTML:

npm run build

Let the build finish.

npx sirv-cli public

Why We Built 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:

  1. Most SSGs are built for either simple sites/blogs or for full scale “app frameworks” that have added an ‘export’ process added as an afterthought.
  2. Fetching data from multiple sources (dbs, apis, config files, markdown files) can lead to major code spaghetti.
  3. Client side routing adds a huge amount of complexity (and bundle size) to initial loads for very little SEO benefit. If you aren’t building an App, why would we want to fully hydrate our JS framework just for faster routing? Browsers are great at routing… we should only be hydrating things that need to be hydrated.

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 thought 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.

Routes

At the core of any site are it’s “routes” or templates.

In Elder.js a route is made up of 2 files that live in your route folder: ./src/routes/${routeName}/ folder.

They are:

  1. A route.js file. This is where you define route details such as the route’s permalink function, all function and data function.
  2. A Svelte component to be used as a template matches the ${routeName}. eg: ./src/routes/blog/Blog.svelte (from here on out we refer to these specific Svelte components as “Svelte Templates”)

Route.js

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());

Why Routing Differs from Express-like Frameworks

Elder.js’ approach to routing is unconventional but it offers several distinct advantages, the two biggest are:

  1. Unlike traditional ‘parameter based’ routing, Elder.js’ does not have to crawl all of the links of a site to know what pages need to be generated. This allows for fully parallelized build times that scale with CPU resources. (As of October 2020, ElderGuide.com has ~20k pages and builds in 1 minute 22 seconds.)
  2. Users have full control over their URL structure. No complex regex is needed to have /senior-living/:facilityId/ and /senior-living/:articleId/ and /senior-living/:parentCompanyId/. This also makes i18n and l10n much more approachable.

Route.js Best Practices:

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 generate a page.

Skinny request objects. Fat data 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 recommend 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:

  1. The language of the page being generated
  2. The City slug
  3. The Country slug

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 in 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 just data.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](https://elderguide.com/tech/elderjs/#hook-example-1-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 boostrap hook documented later in this guide.

Assuming you have populated the data.cities with an array of cities on the boostrap 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 boostrap is available on all routes.

all() Function Spec

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>;
}

permalink() Function Spec

Here is the function signature for a route.js permalink function:

permalink: ({ request, settings }): 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.
  return String;
};

data() Function Spec

Whether you’re building a personal blog or complex data driven SEO site, a route’s data function is the recommend 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;
};

Hooks: How to Customize Elder.js

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).

The Goal of Elder.js Hooks

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.

Hook Interface: the mutable and props Arrays

Each 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 the mutable array are passed as a Proxy.

Hook Lifecycle

Elder.js hook Lifecycle

#svelte #programming #web-development #developer

Dental SEO Marketing | Dentists SEO Marketing

Medibrandox is the largest company for marketing that provides SEO marketing for dentists and dental clinics and it is a practice of increasing website traffic of quality and quantity both. Dental SEO marketing works by optimizing your site for the search engine that helps to boost your website rank in Google as well as you get a lot of traffic of dentists related. If you are looking dental SEO marketing agency so you should go over the website.

#seo marketing for dentists #seo marketing dental clinics #dental seo marketing company #best dental seo marketing agency #seo marketing for dental clinic #dental seo marketing agency

Junipero IT

Junipero IT

1621594603

SEO Services in Gurgaon, Best SEO Company in Delhi NCR – Juniperoites

Junipero IT Solutions is the top seo services company in Delhi Ncr and India. Provide 100% result and full customer support.

#seo services company #seo services company in delhi #top seo services company in delhi #seo services in gurgaon #best seo company in delhi ncr #seo company in gurgaon