Giter Club home page Giter Club logo

elderjs's Introduction

Elder.js

Elder.js: an SEO first Svelte Framework & Static Site Generator


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. These shortcodes can be async!
  • 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 ElderGuide.com 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.”

The ElderGuide.com team expects to maintain this project until 2023-2024. For a clearer vision of what we mean by this and what to expect from the Elder.js team 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:

npx degit Elderjs/template elderjs-app

cd elderjs-app

npm install # or "yarn"

npm start

open http://localhost:3000

This spawns a development server, so simply edit a file in src, save it, and reload the page to see your changes.

Here is a demo of the template: https://elderjs.pages.dev/

To Build/Serve HTML Locally:

npm run build

Let the build finish.

npx sirv-cli public

Full documentation here: https://elderguide.com/tech/elderjs/

elderjs's People

Contributors

bbuhler avatar catgroove avatar christofferkarlsson avatar decrek avatar douglasward avatar edjw avatar eight04 avatar gregod avatar halafi avatar jfowl avatar kfrederix avatar kiukisas avatar markjaquith avatar myrsmedenstorytel avatar netaisllc avatar nickreese avatar schnubor avatar thisislawatts avatar tylerlaws0n avatar widyakumara avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

elderjs's Issues

How can I add typescript?

Hi, is there a guide or documentation somewhere that explains how I can add typescript to the elder.js project template? I'm pretty new to rollup and the config seems pretty complex, I would also like to add sass and postCSS also, so If there is some recommended way of doing this then I think it would be helpful to add it to the docs.

Fix Paths to be Cross Platform (Windows compatible)

Hey, love the idea for this library, but I'm not that great at CSS, and one of the tools I like is Tailwind CSS - I took an initial stab at poking it into rollup here in this Gist Here but I'm having a wierd issue where I don't think my CSS stylesheet is getting put into the dist folder?

do you have any suggested reading materials for 'how to get better at rollup' :D? I'd love to help if able, I think this is a neat idea.

How to edit base template that elderjs uses

How can we update / edit the base template that elderjs uses for page rendering? Say if we wanted to change this <meta charset="UTF-8" /> in the head section to something else? Also the base template seems to be bad, since it's following xhtml style meta tags.

Make HTML template configurable: (Make <head> tag fully customizable)

👏 for starting a static site generator using a modern framework with 0kb client-side JS by default and optional client-side hydration!

Currently the HTML template is statically defined in Page.ts:

    page.htmlString = `<!DOCTYPE html>
      <html lang="en">
        <head>
          <meta charset="UTF-8" />
          <meta name="viewport" content="width=device-width, initial-scale=1" />
          ${page.headString}
        </head>
        <body class="${page.request.route}">
          ${layoutHtml}
          ${page.hydrateStack.length > 0 ? page.beforeHydrate : '' /* page.hydrateStack.length is correct here */}
          ${page.hydrateStack.length > 0 ? page.hydrate : ''}
          ${page.customJsStack.length > 0 ? page.customJs : ''}
          ${page.footerStack.length > 0 ? page.footer : ''}
        </body>
      </html>
    `;

It would be great if this template would be configurable, so for instance html[lang] could easily be changed.
Maybe this can be offered through a hook, or by introducing a src/template.html file like in Sapper.

This would help with #6, as I'm currently forced to overwrite the html[lang] with a hook:

  {
    hook: 'html',
    name: 'setHtmlLang',
    description: 'Overwrite html[lang="en"] with html[lang="{ request.locale }"]',
    priority: 50,
    run: ({ htmlString, request }) => {
      return {
        htmlString: htmlString.replace('<html lang="en">', `<html lang="${ request.locale }">`),
      };
    },
  },

Proposal: First Class Shortcode Support

Hey Elder.js users.

I want to float my current thinking on shortcodes and the plan to add them to the Elder.js core to get buy-in from early users.

Benefit: Shortcodes make static content dynamic and future proof. Elder.js should support them natively.

The Context

Whether your content lives in .md files, on Prismic, Contentful, WordPress, Strapi, your own CMS, or elsehwere, content is generally pretty static.

That said, anytime content lives in a CMS there is always a demand to add 'functionality' to this content.

In my experience thisfunctionalities come in a few flavors.

  • Adding custom HTML to style/wrap content or achieve design goals. (most common)
  • Other times on data driven sites you want to make just a part of the static content dynamic replacing a datapoint with a real statistic. (see below)
  • Embedding an arbitrary Svelte component.

Shortcodes to solve all of these problems.

Shortcodes

If you aren't familiar with shortcodes are strings that can wrap content or have their own attributes:

  • Self Closing: {{shortcode attribute="" /}}
  • Wrapping: {{shortcode}}wraps{{/shortcode}}

NOTE: The {{ and }} brackets vary from system to system.

1. Custom HTML Output

I'm in the process of porting my own website to Elder.js. During this process there are many times where I want a <div class="box">content here</div> to add design flair.

To achieve these I've built a simple shortcode: {{box}}content here{{/box}} allowing me to change the markup as needed should my needs change in the future.

2. Data With Shortcodes

A common use case for shortcodes that we've seen on ElderGuide.com and other properties that we are developing with Elder.js is the need to have an "article" with otherwise static content but that needs a datapoint from a database. (outlined above)

For example we often need to have content that would have this functionality: The US has [numberOfNursingHomes] nursing homes nationwide.

As it currently stands we do this type of replacement within our data functions and have a shortcode like so: {{stat key="numberOfNursingHomes" /}}.

The Problem:

As I've extracting out functionality from our sites, I'm finding more and more cases where plugins could offer shortcodes as well.

I've also found a major need for a shortcode to embed Svelte components, but the ideal implementation doesn't exist within the current Elder.js framework.

A. Minimizing Plugin Interdependence

My initial plan was to release a shortcode plugin using hooks. The shortcode plugin would register and manage the shortcodes from other plugins.

Then if a sister plugin (say the "image plugin") wanted to offer a shortcode it would require the shortcode plugin to also be installed.

The problem with this approach is plugin interdependence.

It just doesn't feel right for a plugin to only offer 1/3 of it's functionality without another plugin installed. There is also an issue of making sure each plugin doesn't overwrite the other's functionality.

B. data.key replacement

Another hurdle is that in a perfect world we'd be able to do {{svelteComponent component="Clock" props="data.key" options="{loading: "eager"}"/}} and the clock component would be hydrated with the values found on data.key.

In order to achieve this, the shortcode plugin would need the context of the data object returned from the route and would need to run between the generation of the route html and the template html.

Currently there is no hook that can support this... and hooks aren't really the right solution because we only want 1 instance of the shortcode parser to run.

The Plan

Our current implementations use a fork of the https://github.com/metaplatform/meta-shortcodes library. I've used this shortcode library in one form or another since 2016. It is battle tested across my sites, allows for open and close bracket customization, but could use a modernization effort. (not the focus of this proposal)

To implement shortcodes the plan is as follows:

Page.ts:

  1. In Page.ts after the page.route.templateComponent function has built the route's HTML, shortcodes would be executed on the returned HTML.
  2. After the shortcodes have executed, the returned html would then be passed into page.route.layout and page HTML generation would continue as it currently does.

The benefits of this location are 3 fold:

  1. Whether content comes from a CMS, Svelte component, or a data store, we can assume that the shortcodes would be in the HTML returned by the route.
  2. Because we still have to process the page.route.layout we can use Elder.js' internal system for inlineSvelteComponent to embed arbitrary svelte components offering a solution to embed Svelte components {{svelteComponent component="Clock" /}}.
  3. Because all data related hooks and functions will have executed, we know that the data object is stable and could offer the ability to pass the data context into shortcodes as well allowing for {{svelteComponent component="Clock" props="data.key"/}} where data.key would be the key taken from the data object.

Defining Shortcodes:

Shortcodes could be defined in two places:

  1. elder.config.js for user defined shortcodes.
  2. plugins could define shortcodes by returning them during plugin initialization.

Proposed Definition (simple)

  • props are the attributes defined on the shortcode.
  • content is the content wrapped in the shortcode.
  • data is the data object.
// elder.config.js

shortcodes: [
        {
          shortcode: 'box',
          run: (props, content, data) => {
            return `<div class="box">${content}</div>`;
          },
        },
]

Proposed Definition (robust)

The biggest limitation to the 'simple' design is there is no access to add css, js, or elements to the <head>.

I've hit this limitation in the past with WordPress and it was limiting, so a more complex design that would be more robust would be to support an API signature as below where css, js, and head are all added to the stacks needed to support them.

NOTE: this requires moving where stacks are processed.

// elder.config.js

shortcodes: [
        {
          shortcode: 'box',
          run: (props, content, data) => {
            return { html: `<div class="box">${content}</div>`, css:'', js: '', head:'' }
          },
        },
]

Shortcodes by Default

By default the only shortcode I'd imagine shipping is the {{svelteComponent component="Clock" props="" options="" /}}.

This does add a small regex call that appears to add about 1.2ms per page generation time in local testing.


This is the first major feature that I'm looking at adding to the core and I'd like community feedback. I'm not sure how OSS projects usually handle this but my goal is to get buy-in from early users. If you have feedback I'd love to hear it.

Having used several static site generators, I do believe this functionality belongs in the core. One of my biggest gripes with Gatsby is that it pushes all of the customization to plugins and solves the plugin interdependence problem with "Themes." While this works for one-off projects, it doesn't allow easy copy and pasting between projects because each Gatsby project is it's own special snowflake due to plugin interdependence. I think Elder.js offering shortcodes from the core is a wise move but would love feedback and some devil's advocates on why it shouldn't be in the core.

CSS Sourcemap

How would I get CSS sourcemaps to work with DevTools > Sources > Filesystem? I don't really know Rollup options as you can probably tell, but as I understand there's the Replacements section in the docs which serves this purpose. What isn't clear to me is if it's just a placeholder name or if that's exactly what you need to type in to change the Rollup config. There's no mention of how to get this specific feature working, or if it's even remotely possible.

I tried adding sourceMap: true to svelte.config.js but maybe that's completely unrelated.

Also, I'm wondering if anyone even uses sourcemaps since I've only seen brief mention of it in documentation on Rollup.

Unable to share Svelte store between hydrated components (in dev mode)

According to the docs, hydrated components should be able to share a store:

With partial hydration you end up with multiple root components instead of one main component root. (Don't worry, you can still use Svelte stores to allow them to easily share state across them.)
https://elderguide.com/tech/elderjs/#understanding-partial-hydration

However I'm unable to get this to work. The hydrated component bundles appear to each have their own copy of the store. Do you have a working example you can share? Maybe even add it to the Elderjs/template?


This is a simplified version of my unsuccessful setup (inspired by the Svelte tutorial: writable stores):

src/routes/app/App.svelte:

<script>
import '../../components/CountDisplay.svelte';
import '../../components/CountIncrementer.svelte';
</script>
<CountDisplay hydrate-client={{ }} />
<CountIncrementer hydrate-client={{ }} />

src/store.mjs:

import { writable } from 'svelte/store';
export const count = writable(0);

CountDisplay:

<script>
import { count } from './stores.js';
let count_value;

count.subscribe(value => {
  count_value = value;
});
</script>

<h1>The count is {count_value}</h1>

src/components/CountIncrementer.svelte:

<script>
import { count } from './stores.js';

function increment() {
  count.update(n => n + 1);
}
</script>

<button on:click={increment}> + </button>

I didn't get this to work. So instead I ended up sharing a store by exposing it to the window object onMount:

src/components/CountStore.svelte:

<script>
import { writable } from 'svelte/store';
const count = writable(0);

onMount(() => {
  window.count = count;
});
</script>

This way other hydrated components have access via window.count.subscribe/window.count.update/etc.

Routing

Hello,

I'm trying to understand the routing with Elder.js. How would you internally link to pages inside your site?

Thanks!

Why is the documentation so broken?

Extra pages seems to be from another website, why is it like that?

There are lots of typos and grammatical errors on documentation pages as well.

Static images

The next issue I had may be due to a lack of understanding of how Elder/Svelte works. However, I encouneted a problem while trying to insert a plain static image.

Using the template layout's css as a guide, it seems that I have to either pass the settings object all the way down to the component (which very much rubs me the wrong way) or somehow make it global. Seems a bit overboard for just an image, doesn't it?

\n being stripped out by overly greedy regex.

I am experiencing some issues with Client Hydration of external Svelte components.
When putting a component inside the components folder it is working well, but if I import my component from a package inside node_modules, I get the following error:

[
  Error: Cannot find module '**DIR**/___ELDER___/compiled/Button.js'
  Require stack:
  - **DIR**/node_modules/@elderjs/elderjs/build/utils/svelteComponent.js
  - **DIR**/node_modules/@elderjs/elderjs/build/utils/index.js
  - **DIR**/node_modules/@elderjs/elderjs/build/routes/routes.js
  - **DIR**/node_modules/@elderjs/elderjs/build/Elder.js
  - **DIR**/node_modules/@elderjs/elderjs/build/index.js
  - **DIR**/src/server.js
      at Function.Module._resolveFilename (internal/modules/cjs/loader.js:952:15)
      at Function.Module._load (internal/modules/cjs/loader.js:835:27)
      at Module.require (internal/modules/cjs/loader.js:1012:19)
      at require (internal/modules/cjs/helpers.js:72:18)
      at **DIR**/node_modules/@elderjs/elderjs/build/utils/svelteComponent.js:38:28
      at Object.templateComponent (**DIR**/node_modules/@elderjs/elderjs/build/utils/svelteComponent.js:74:71)
      at buildPage (**DIR**/node_modules/@elderjs/elderjs/build/utils/Page.js:41:40)
      at processTicksAndRejections (internal/process/task_queues.js:97:5)
      at async Object.run (**DIR**/node_modules/@elderjs/elderjs/build/hooks.js:81:34)
      at async **DIR**/node_modules/@elderjs/elderjs/build/utils/prepareRunHook.js:45:44 {
    code: 'MODULE_NOT_FOUND',
    requireStack: [
      '**DIR**/node_modules/@elderjs/elderjs/build/utils/svelteComponent.js',
      '**DIR**/node_modules/@elderjs/elderjs/build/utils/index.js',
      '**DIR**/node_modules/@elderjs/elderjs/build/routes/routes.js',
      '**DIR**/node_modules/@elderjs/elderjs/build/Elder.js',
      '**DIR**/node_modules/@elderjs/elderjs/build/index.js',
      '**DIR**/src/server.js'
    ]
  }
]

I am trying with importing this Button component.
From what I can see, we only get compiled versions of things inside our src directory and not the external resources. However, without the client hydration, it is working well (except for the hydration part then).

<Button type="secondary" />

This renders fine, but with the hydrate-client property we get this error above.

<Button hydrate-client={{ type: 'secondary' }} />

My initial thought is that we must include the external library in our rollup config so that it is compiled as well. We have followed the instructions here and have the svelte property in our package.json. It works well with Sapper. Any ideas on how we can proceed?

Proposal: ensure slashes in permalink function output

At present a route fails if its permalink does not start or end with a slash. For example, /foo/bar/ works, foo/bar/ fails, /foo/bar also fails. This creates a manageable but annoying amount of repeating code if there is an optional first path segment in the route: /${part1}/${part2}/ creates a double slash if part1 is undefined. I propose adding a bit of post-processing to the permalink to ensure that it starts and ends with a slash (basically, "if not starts with slash, add slash; if not ends with slash, add slash")

Roadmap for v1.0.0

Hi Elder team! Congrats on all the hard work so far! I'm very curious if you have a roadmap - things you think should be added, changed or removed to Elder - before releasing a stable v1.0.0. What are your ideas?

Imho more functionality can always be added later. So while I think short codes (#29) could be a great addition, I personally think they could wait. On the other hand, I think it's very important to reduce the API surface of Elder to a minimum before releasing v1.0.0 as that makes it easier to change things under the hood without developers using the framework being affected. So to me issues like simplifying elder.config.js (#27) and hiding the internals of rollup.config.js (#30) are important to get right before many start using the framework and expect a stable experience.

What are your thoughts?

Templates need unique compile paths to fix conflicts

The Rollup configuration has the same target directory for different Svelte source directories. This causes conflicts when source templates in different directories have the same name.

All these Svelte templates:

  • src/components/Example/Example.svelte
  • src/routes/example/Example.svelte
  • src/layouts/Example.svelte

Compile to the same output:

  • __ELDER__/compiled/Example.js

Maybe change this into __ELDER__/svelte/components/Example.js etc?
(as I can imagine you also want to compile src/routes/:route/route.js to __ELDER__/routes/:route.js in the future).

on:click on Component

Is there anything special you need to do to get an on:click function to work on a Component?

Proposal: move Rollup config into Elderjs/elderjs

Currently an Elder project's Rollup config lives entirely inside the project's code using Elderjs/template > rollup.config.js. This means that when Rollup config changes are needed based on a change in Elderjs/elderjs the Rollup config in Elderjs/template needs to be updated in tandem for new projects. And all Elder projects bootstrapped with the template need to adjust their code when updating their Elder version.

So my proposal would be to move the Rollup config into Elderjs/elderjs and make it available in projects. So inside a project's rollup.config.js it would look something like:

const { getRollupConfig } = require('@elderjs/elderjs');
const elderConfig = require('./elder.config');
const svelteConfig = require('./svelte.config');

module.exports = [
  // project's other Rollup configs
  ...getRollupConfig({ elderConfig, svelteConfig }),
];

The result would be that users can still configure the project using elder.config.js and svelte.config.js and extend their own rollup.config.js. But it will be much easier for the Elder team to make changes to its own bundling configuration (#3).

Configure elderjs-plugin-browser-reload

Hi, I realized today that elderjs-plugin-browser-reload, uses port 8080 for reloading. I would like to be able to configure the port used, as I have a service running on port 8080, and it would be convenient to not need to change it 😄.
Thanks for the great framework!

Component styles not added to cssStack

I have the following issue:
If a component is rendered conditionally and/or not initially, all its related css is not added to the cssStack.

<script>
  import Button from '../Button/Button.svelte'
  let foo
</script>

<div>
  {#if foo}
    <Button>Click</Button>
  {/if}
</div>

The same issue is true when using the following loop

<script>
  import Button from '../Button/Button.svelte'
  let items
</script>

{#each items as item}
  <Button>Click</Button>
{/each}

The components html is rendered fine. Also the classes are in place and have unique hash. And the parent component has hydrate-client={{ }} set.
How can I make sure the css for conditionally rendered components will be added to the cssStack?

Ability to disable hook on a route level

Is there a way currently to disable a hook on a route level as opposed to app wide? If not, is this something you'd be interested in supporting in the future?

<span>s around components

Recently came across this project and loved the idea. However, when I tried to actually use it, I encountered some difficulties, addressed here and in another issue.

While rendering components Elder.js wraps them in s. While have an idea or to as to the reasoning behind this decision, it would be great if the functionality was opt-in or at least allowed one to change the tag. Having a wrap the entire content of a page just doesn't sit well with me.

Running npm run dev:server works after a build

This might be an issue that is only relevant to me as I am getting more familiar with the inner workings of this well made framework, but just wanted to confirm if it is infact something that I have either misconfigured or misunderstood.

Whenever I add files to the asset directory, the files are not copied over to the public folder automatically unless I manually do a npm run build before rerunning npm run dev:server. It seems nodemon, does pick that something has changed, but the files aren't copied over, I also found that that the default hook for copying files over to the public folder based on copyAssetsToPublic hook that comes along with your template doesn't exactly see if the file is actually a file or not a folder, and so if you have a folder named type.js it gets recognized as a file and produces an error, which is what it should do based on the default hook code.

I changed it so it looks like this now and it seems to be handling that edge case fine now, notice fs.lstatSync part in the if condition:

{
    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 && fs.lstatSync(file).isFile()) {
          const relativeToAssetsArray = parsed.dir.split('assets');
          relativeToAssetsArray.shift();

          const relativeToAssetsFolder = `.${relativeToAssetsArray.join()}/`;
          const p = path.parse(path.resolve(settings.distDir, relativeToAssetsFolder));
          fs.ensureDirSync(p.dir);
          try {
            fs.outputFileSync(
              path.resolve(settings.distDir, `${relativeToAssetsFolder}${parsed.base}`),
              fs.readFileSync(file),
            );
          } catch (e) {
            console.log('Error processing: ', file, e);
          }
        }
      });
    }
  }`

Add quickstart guide

The following line in the Getting Started section:

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

seems incomplete. There should be a quick start instruction to get started.

Otherwise change the wording completely.

How ever I'm inclined to have it as is and add a quickstart guide.

Serverless Elder rendering

So, we know Elder is designed to generate static websites 💯 . However my colleague @decrek and I think Elder could be used very well for partially static and partially dynamic websites. You've already proven Elder can be used as a server in Elderjs/template > src/server.js. So we want to take it one step further and make it serverless.

@decrek has already done some work on this. Dynamic serverless Elder rendering in a lambda function would look something like this:

const renderPage = require('../lib/render-elder-page.js');
exports.handler = async (event) => {
  const user = await getUserFromDatabase(event);
  const html = await renderPage({ 
   permalink: '/account/', 
   data: { user },
  });
  return {
    statusCode: 200,
    headers: { 'Content-Type': 'text/html' },
    body: html,
  }
}

with lib/render-elder-page.js doing the actual rendering:

const { Elder, Page } = require('@elderjs/elderjs');
const elder = new Elder({ context: 'server' });
module.exports = async function renderPage({ permalink, data = {} }) {
  await elder.bootstrap();
  const request = elder.serverLookupObject[permalink];
  const route = elder.routes[request.route];
  const hooks = [
    ...elder.hooks,
    {
      hook: 'data',
      name: 'addDynamicData',
      description: 'Adds dynamic data to data object',
      priority: 50,
      run: (opts) => ({ ...opts.data, ...data }),
    }
  ];
  const page = new Page({ ...elder, hooks, request, route });
  return await page.html();
};

The setup is working locally, but we're still having some issues on Vercel and Netlify. The hardest part in the serverless context is reaching all the relevant source and generated files. The automagic behaviour, mainly inside getConfig is making this a bit difficult. The other bit is the use of process.cwd() throughout the code base. To make Elder more flexible to use, 2 things would help a lot:

  • add and use a settings.rootDir throughout the code base. So for instance const srcFolder = path.join(process.cwd(), settings.locations.srcFolder); would become const srcFolder = path.join(settings.rootDir, settings.srcDir). rootDir could still default to process.cwd(). See #27.
  • allow passing the entire config to new Elder(config) to avoid issues caused by automagic behaviour like cosmiconfig. The automagic behavior can still be used as a default for when no config is passed.

on:load from inside <svelte:head> of hydrated component not working

2020-10-18_21-07-05

Hi all,

I've been troubleshooting this issue since the summit and think I've boxed it into this issue. The on:load function that this script tag is supposed to trigger is not being run inside a client hydrated component, but I can verify that onMount is running and on the network tab I can see that the script source is being downloaded -- so before I investigate further I'd like to log this issue and see if there's an obvious bug it relates to.

I'm going to try bundling GSAP and see if that fixes things, but I want that sweet sweet 0 load time CDN cache hit if possible!

Make Intersection Observer / hydrate-client configurable

In some of my use cases the intersection observer used to lazy load components with hydrate-client kicked in a bit late. As I understand it @philipp-tailor had a similar issue in #12 in that he'd like components with hydrate-client to hydrate instantly.

For these use cases it would be great if the hydrate-client behavior would be configurable. I personally like the fact that lazy loading is the default. Maybe the behavior can be extended something like this:

<!-- Lazy load component when in view -->
<Component hydrate-client={{}} />

<!-- Hydrate instantly -->
<Component hydrate-client={{}} hydrate-lazy={ false } />

<!-- Lazy load component with specific intersection observer configuration -->
<Component hydrate-client={{}} hydrate-lazy={{ rootMargin: '500px' }} />

note: I see IntersectionObserver.ts already accepts a distancePx, but it's not accessible from a Svelte template.

Error with NPM START

Hi and thanx for your great work 👍

I'm attempt using your template with windows platform.
All is right except when I'm doing "NPM START".
Having this error :

[email protected] start E:\elderjs-app
npm run build:rollup && npm run dev:server

[email protected] build:rollup E:\elderjs-app
rollup -c

rollup production === true. Because watch is undefined.

./node_modules/intersection-observer/intersection-observer.js → public\dist\static\intersection-observer.js...
created public\dist\static\intersection-observer.js in 1.4s

./node_modules/systemjs/dist/s.min.js → public\dist\static\s.min.js...
created public\dist\static\s.min.js in 1.3s

./src/routes/blog/Blog.svelte → ./ELDER/compiled/...
[!] (plugin svelte) TypeError: content.matchAll(...) is not a function or its return value is not iterable
src\routes\blog\Blog.svelte
TypeError: content.matchAll(...) is not a function or its return value is not iterable
at markup (E:\elderjs-app\node_modules@elderjs\elderjs\build\partialHydration\partialHydration.js:21:37)
at preprocess (E:\elderjs-app\node_modules\svelte\src\compiler\preprocess\index.ts:86:27)

Error: Client side hydrated component includes client side hydrated sub component.

Hi!

First of all, thank you so much for you and your team's work. Elderjs looks really powerful and is a much needed addition to the Svelte ecosystem.

I'm trying to port a Sapper site to Elderjs and I'm running into the issue of having reactive sub-components not being supported (By reactive, I mean client-side hydrated, in order to use Svelte's reactivity features).

To be clearer, I'm trying to do something like:

// Page.svelte
<script>
import Reactive from '@/components/Reactive.svelte';

export let data;
</script>

<Reactive hydrate-client={{ data }} />

And then in the Reactive.svelte file:

// Reactive.svelte
<script>
import SubReactive from '@/components/SubReactive.svelte';

export let data;
</script>

<SubReactive {data.releventData} />

I have several nested components like this, but only the top one has the "hydrate-client" prop. Yet I'm still getting the error message from this issue's title.

Am I doing something wrong or is this really impossible in Elderjs?

Hydrate-client fails with non-camelcase component names

2020-10-18_20-19-18

Ran into what appeared to be an issue with component hydration and after mucking around for a while found out that cleanComponentName returns a camelcase version of the component name. I appreciate this is more or less an edge case and I'll think about where in the docs I might've benefited from reading it, but in case it helps to understand how this happened: I had a component named GSAP and its clientSrc was being set to null because the key in $$internal.hashedComponents was GSAP not Gsap.

content.matchAll is not a function

Hi,

I just followed the getting started, degit & npm install without issues, then the npm start output this :

xxx@yyy ~/Projects/elderjs-app
$ npm start

> [email protected] start C:\Users\xxx\Projects\elderjs-app
> npm run build:rollup && npm run dev:server


> [email protected] build:rollup C:\Users\xxx\Projects\elderjs-app
> rollup -c

rollup production === true. Because watch is undefined.

./node_modules/intersection-observer/intersection-observer.js → public\dist\static\intersection-observer.js...
created public\dist\static\intersection-observer.js in 772ms

./node_modules/systemjs/dist/s.min.js → public\dist\static\s.min.js...
created public\dist\static\s.min.js in 643ms

./src/routes/blog/Blog.svelte → ./___ELDER___/compiled/...
[!] (plugin svelte) TypeError: content.matchAll is not a function
src\routes\blog\Blog.svelte
TypeError: content.matchAll is not a function
    at markup (C:\Users\xxx\Projects\elderjs-app\node_modules\@elderjs\elderjs\build\partialHydration\partialHydration.js:6:33)
    at preprocess (C:\Users\xxx\Projects\elderjs-app\node_modules\svelte\src\compiler\preprocess\index.ts:86:27)

npm ERR! code ELIFECYCLE
npm ERR! errno 1
npm ERR! [email protected] build:rollup: `rollup -c`
npm ERR! Exit status 1
npm ERR!
npm ERR! Failed at the [email protected] build:rollup script.
npm ERR! This is probably not a problem with npm. There is likely additional logging output above.

npm ERR! A complete log of this run can be found in:
npm ERR!     C:\Users\xxx\AppData\Roaming\npm-cache\_logs\2020-08-14T11_21_38_286Z-debug.log
npm ERR! code ELIFECYCLE
npm ERR! errno 1
npm ERR! [email protected] start: `npm run build:rollup && npm run dev:server`
npm ERR! Exit status 1
npm ERR!
npm ERR! Failed at the [email protected] start script.
npm ERR! This is probably not a problem with npm. There is likely additional logging output above.

npm ERR! A complete log of this run can be found in:
npm ERR!     C:\Users\xxx\AppData\Roaming\npm-cache\_logs\2020-08-14T11_21_38_333Z-debug.log

I'm using Node 10.16, Npm 6.9.0 & Windows 10 x64

Adding a base href

How can I add a custom base URL to the exported files, like Sapper's Base URLs?
Even when I add it manually, it doesn't work because e.g. the style.css link has a leading slash. Thanks in advance.

Enhancement: preload System.js

I understand the usage of System.js is an ongoing discussion (#12). But as long as it's in, maybe it's smart to preload it. This ensures components can hydrate more quickly. I'm currently doing this using a custom hook, but I imagine this could be an internal hook:

// src/hooks.js
module.exports = [{
  hook: 'stacks',
  name: 'preloadSystemJs',
  description: 'add link[rel="preload"] for System.js script',
  priority: 50,
  run: ({ headStack, settings }) => {
    const preloadLink = {
      source: 'hooks:preloadFonts',
      string: `<link rel="preload" href="${settings.locations.systemJs}" as="script">`,
    };
    return {
      headStack: [...headStack, preloadLink],
    };
  },
}];

Proposal: simplify elder.config.js

The current elder.config.js exposes a lot of internals, like locations.svelte.ssrComponents and locations.intersectionObserverPoly. Would it be an idea to reduce the surface that's exposed, simplify the API and make it more common to other frameworks like Next.js and NuxtJS?

current elder.config.js:

module.exports = {
  server: {
    prefix: '',
  },
  build: {},
  locations: {
    assets: './public/dist/static/',
    public: './public/',
    svelte: {
      ssrComponents: './___ELDER___/compiled/',
      clientComponents: './public/dist/svelte/',
    },
    systemJs: '/dist/static/s.min.js',
    intersectionObserverPoly: '/dist/static/intersection-observer.js',
  },
  debug: {},
  hooks: {},
  plugins: {},
};

proposal elder.config.js:

module.exports = {
  target: 'static',                   // default, other values could be 'server' and later maybe 'serverless'
  rootDir: process.cwd(),    // default
  srcDir:  'src/',                     // default, relative to rootDir
  distDir: 'dist/',                    // default, relative to rootDir, was 'public/'
  server: {
    prefix: '',
  },
  build: {},
  debug: {},
  hooks: {},
  plugins: {},
};

i18n support ?

Quick question: Does anything concern i18n support is plan for elderjs ?

Question : how to get dynamic .md specific to routes

Hi, in the template example, the .md is put inside blog folder/route, I duplicate it to for example blog2,
I can get all hook up easily with changing @elder.config.js and all markdown data is automatically inside data.markdown in Home.svelte

However, the markdown is centralized (all markdowns from /blog and /blog2 is in data.markdown)

The question, how to split the markdown data, should we use frontmatter data to add route info ? Or there's already solution with it ?

Thanks

Hydrated Nested Svelte Components Losing Styles

I ran across a problem where a hydrated components nested component lost some or all of it's styles. The following component has hydrate-client on it:

<!-- Navigation.svelte --!>
<script>
  import MobileMenu from './MobileMenu.svelte';
  let y;
  $: scrolled = y > 100;
  export let menu;
  let menuOpen = false;
</script>

<style>
  .navcontainer {
    position: fixed;
    top: 0;
    left: 0;
    padding: 25px 0 20px 0;
    width: 100%;
    align-items: center;
    justify-content: center;
    background-color: rgba(0, 0, 0, 0.1);
    transition: background-color 0.3s ease;
    --small: var(--media-lte-sm) none;
    display: var(--small, flex);
  }
  .navcontainer.scrolled {
    background-color: #162b2e;
  }

  a {
    color: #2a8290;
    font-size: 19px;
    margin: 0 24px;
    padding: 4px 20px;
    text-decoration: none;
    display: inline-flex;

    transition: background-color 0.3s ease;
  }

  a:visited {
    color: #2a8290;
  }

  a:hover,
  a:focus {
    background: rgba(255, 255, 255, 0.2);
  }

  a.scrolled {
    color: white;
  }

  a.scrolled:visited {
    color: white;
  }

  .hamburger {
    position: absolute;
    padding: 30px;
    top: 6px;
    right: 6px;

    --small: var(--media-lte-sm) initial;
    display: var(--small, none);
  }
  img {
    height: 16px;
    place-self: center;
  }
  span {
    display: grid;
    grid-gap: 8px;
    grid-template-columns: auto auto;
    justify-items: center;
  }
</style>

<svelte:window bind:scrollY={y} />
<div class="navcontainer" class:scrolled>
  <nav>
    {#each menu as { url, name }}<a href={url} class:scrolled> {name} </a>{/each}

    <a rel="noreferrer" href="https://forms.gle/6PBKXng9jfrvxjhX8" target="_blank" class:scrolled> Sign Up </a>

    <a rel="noreferrer" target="_blank" href="https://twitter.com/sveltesociety" class:scrolled>
      <span> <img src="/dist/static/images/twitter.svg" alt="" /> Twitter </span>
    </a>
  </nav>
</div>
<div class="hamburger" on:click={() => (menuOpen = true)}><img src="/dist/static/images/burger.svg" alt="" /></div>

{#if menuOpen}
  <MobileMenu {menu} on:click={() => (menuOpen = false)} />
{/if}

and

<!-- MobileMenu.svelte --!>
<script>
  export let menu;
</script>

<style>
  .container {
    background: #17353a;
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    overflow: scroll;
  }
  ul {
    padding-top: 100px;
    padding-bottom: 100px;
    display: grid;
    grid-gap: 40px;
    list-style: none;
  }
  li {
    text-transform: uppercase;
    font-family: Anton;
    font-size: 48px;
    line-height: 120%;
  }
  a:hover {
    color: white;
    opacity: 1;
  }
  a {
    color: var(--sky-blue);
    opacity: 0.6;
    text-decoration: none;
    letter-spacing: 0.6px;
  }
  img {
    padding: 30px;
    position: absolute;
    top: 6px;
    right: 6px;
  }
</style>

<div class="container">
  <ul>
    {#each menu as { name, url }}
      <li><a on:click href="/{url}">{name}</a></li>
    {/each}
    <li><a href="https://forms.gle/6PBKXng9jfrvxjhX8" rel="noreferrer" target="_blank"> Sign up </a></li>
    <li><a target="_blank" href="https://twitter.com/sveltesociety">Twitter</a></li>
  </ul>
</div>
<button on:click> <img src="dist/static/images/close.svg" alt="" /> </button>

The workaround I found is to move the content of MobileMenu.svelte into Navigation.svelte.

Documentation on intersection observer and systemjs

Attempted to adapt elderjs for a mostly static blog. I detected that elderjs doesn't suit my needs only after being close to done with the migration: Client side svelte components are only loaded and hydrated once they are part of the viewport, detected via intersection observer. This should be clearly stated in the documentation. I'd open a PR to update the documentation myself, if I'd know the exact reasoning behind this decision. I assume that the design decision was taken as a performance optimisation / viewing dynamic components as progressive enhancement. But for the full picture it's simpler if the maintainers directly update the docs and close this issue.

In the same vain (another default behaviour that isn't really reasoned about) is the usage of systemjs. Here I assume that it's to have IE11 support for interactivity?

This issue is about adding the documentation.

I'm also happy to share the example why the intersection observer thing – if not deactivatable – is a deal-breaker to me.

Hydration not working when not using self-closing tag

Just starting out with Elderjs, but I ran into a weird issue.

This won't hydrate on the client:

<Map hydrate-client={{}} ></Map>

...but this will:

<Map hydrate-client={{}} />

I'm working with the v0.2.0 template.

Support hydrated components in IE11

As a static site generator Elder is very well suited for legacy browsers. However hydrated components don't work in Internet Explorer yet for a couple of reasons:

  • the inlined SystemJS loader invocation needs to be in ES5.
  • IE11 requires a polyfill for Promise and fetch (SystemJS needs them).
  • IE11 requires Svelte components to be compiled to ES5.

SystemJS loader in ES5
The SystemJS loader invocation is currently in ES6:

const clientJs = `
System.import('${clientSrc}').then(({ default: App }) => {
  new App({
    target: document.getElementById('${cleanComponentName.toLowerCase()}-${id}'),
    hydrate: true,
    props: ${devalue(props)}
  });
});
`

This code isn't passed through Rollup, so should just be in ES5:

const clientJs = `
System.import('${clientSrc}').then(function(importedModule) {
  var App = importedModule.default;
  new App({
    target: document.getElementById('${cleanComponentName.toLowerCase()}-${id}'),
    hydrate: true,
    props: ${JSON.stringify(props)}
  });
});
`

IE11 requires a polyfill for Promise and fetch
Elder already conditionally polyfills IntersectionObserver:

<script>
if (!('IntersectionObserver' in window)) {
  var script = document.createElement("script");
  script.src = "/static/intersection-observer.js";
   document.getElementsByTagName('head')[0].appendChild(script);
};
</script>

The same strategy could be used for Promise and fetch:

<script>
(function() {
  function polyfill (feature, src) {
    if (feature in window) return;
     var script = document.createElement('script');
     script.src = src;
     document.head.appendChild(script);
  }
  polyfill('Promise', '/static/promise-polyfill.js');
  polyfill('fetch', '/static/unfetch.js');
  polyfill('IntersectionObserver', '/static/intersection-observer.js');
}());
</script>

I think this would be good lightweight polyfill candidates: promise-polyfill (<1kb) and unfetch (500b).

Side note: I would be a big fan of moving all files generated by Elder (polyfills, Svelte components, ...) to ${distDir}/_elder (just like dist/_nuxt) and have their names revision hashed (like is already done with Svelte components) so that directory can easily and aggressively be cached.

IE11 requires Svelte components to be compiled to ES5
Probably the hardest part to configure. Elder already uses Babel in its Rollup config. Perhaps IE11 support can be added using babel/preset-env like Sapper is doing in its Rollup config for legacy browsers.

I don't have a working setup yet. I'm posting this here as I hope this well help. Maybe this could be part of #42?

[Help Wanted] Simplifying the Rollup Config

Currently, one of the biggest drawbacks to Elder.js is the overly complex rollup config.

At the core we've got 2 issues.

  1. We need to make sure that all components that will be SSR'd have a precompiled template generated.
  2. We need to make sure all templates that will be mounted will have a bundled version we can use to hydrate the client.

This weekend I spent some time studying Svelte's register.js function in an attempt to get on the fly compiling of SSR svelte components.

The biggest roadblock I hit was with packages that use ESM syntax.

Honestly, I don't know anything about ESM or CJS or anything around the debate. It has been something I've buried my head in the sand about for the past 2 years. That said, I think someone more experience could help simplify the rollup process and possibly help us compile SSR components on the fly.

It appears that we could use Rollup from within node, but this is something I haven't had a chance to test yet.

Proposal: Consolidate `${distDir}/static/` and `${distDir}/svelte/ into `${distDir}/_elder/`

Consolidating the folder structure output to the distDir will allow for aggressive caching by users and allow us to add an arbitrary number of folders under it as long as all files are hashed.

Outline of Change:

  1. Add a hash to all rollup bundles as we do in production.
  2. Change where svelte components are output.
  3. Change where polyfills are output.
  4. Modify Elder.ts to find hashed polyfills.
  5. Update polyfill hook to use the hashed files.

Brought up @jbmoelker in #45.

Slug routes not building in development

In development, running npm run dev:rollup and npm run dev:server as instructed, routes in blog/route.js are not being built.

For example, with the following in src/routes/blog/route.js, /blog/hello-world is "Not Found":

module.exports = {
  all: () => [{ slug: 'hello-world' }],
  permalink: ({ request }) => `/blog/${request.slug}`,
};

After building with npm run build and starting the dev server again, the route loads.
Subsequent changes to the Blog template do not cause dev:rollup builds and are not reflected until npm run build is executed again.
Is this expected behavior?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.