├── .gitignore
├── package.json
├── README.md
├── example.js
└── index.js
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .DS_Store
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "alpinejs-ssr",
3 | "version": "1.0.2",
4 | "description": "Dead simple server-side-rendering for Alpine.js.",
5 | "main": "index.js",
6 | "type": "module",
7 | "scripts": {
8 | "test": "echo \"Error: no test specified\" && exit 1"
9 | },
10 | "author": "Dashpilot",
11 | "repository": {
12 | "type": "git",
13 | "url": "https://github.com/dashpilot/alpinejs-ssr"
14 | },
15 | "license": "ISC",
16 | "dependencies": {
17 | "cheerio": "^1.0.0",
18 | "lodash": "^4.17.21"
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Alpine.js SSR
2 | ## Dead simple server-side-rendering for Alpine.js
3 |
4 | Alpine.js SSR allows you to server-side-render (SSR) your Alpine components in lightweight and dead-simple way. No need to set-up a Puppeteer-server, just import the module and off you go. After server-side-rendering your components stay fully hydratable and interactive!
5 |
6 | ## Why?
7 | I love the simplicity and power of Alpine.js, but was missing the option of server-side rendering for SEO and robustness. There were no SSR options available for Alpine.js, so I built my own.
8 |
9 | ## How to?
10 |
11 | First install the package from npm:
12 | ```
13 | npm install alpinejs-ssr
14 | ```
15 | Import the module and call the `compile` function:
16 | ```js
17 | import {compile} from alpinejs-ssr
18 |
19 | const html = `your Alpine.js html`
20 | const data = {"your":"data"}
21 |
22 | compile(html, data);
23 | ```
24 | Check out example.js for a full demo
25 |
26 | ## Supported Alpine.js attributes
27 |
28 | Supports most attributes that make sense in a server context:
29 | `x-text`, `x-html`,`x-for`,`:src`,`:id`
30 |
31 | Let me know if you're missing any. `x-if` has not been implemented for SSR, because it would break interactivity on the client-side if the server would remove those blocks.
32 |
33 | ## Press the :star: button
34 | Don't forget to press the :star: button to let me know I should continue improving this project.
35 |
36 |
37 |
--------------------------------------------------------------------------------
/example.js:
--------------------------------------------------------------------------------
1 | import { compile } from "./index.js";
2 |
3 | const html = `
4 |
5 |
6 |
21 |
22 |
23 |
24 |
25 |
![]()
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 | `;
38 |
39 | const data = {
40 | header: {
41 | title: "Welcome to our site.",
42 | body: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Duis et lacus non turpis congue congue. Integer porttitor non leo.",
43 | },
44 | items: [
45 | {
46 | id: "item-1",
47 | img: "/img/blobs1.png",
48 | title: "Feature One",
49 | body: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus id tortor neque.",
50 | layout: "post",
51 | },
52 | {
53 | id: "item-2",
54 | img: "/img/blobs2.png",
55 | title: "Feature Two",
56 | body: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus id tortor neque.",
57 | layout: "post",
58 | },
59 | {
60 | id: "item-3",
61 | img: "/img/blobs3.png",
62 | title: "Feature Three",
63 | body: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus id tortor neque.",
64 | layout: "post",
65 | },
66 | ],
67 | };
68 |
69 | console.log(compile(html, data));
70 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | import * as cheerio from 'cheerio';
2 | import _ from 'lodash';
3 |
4 | function compile(html, data) {
5 | const $ = cheerio.load(html);
6 |
7 | $('[x-for]').each(function () {
8 | const key = $(this).attr('x-for').split(' in ')[1].slice(5); // Remove 'data.' from the start
9 | const items = _.get(data, key);
10 | const template = $(this).html();
11 |
12 | let result = '';
13 | items.forEach((item) => {
14 | let itemHtml = cheerio.load(template);
15 | for (const prop in item) {
16 | const regex = new RegExp(`item.${prop}`, 'g');
17 | itemHtml('[x-html]').each(function () {
18 | if ($(this).attr('x-html') === `item.${prop}`) {
19 | $(this).html(item[prop]);
20 | }
21 | });
22 | itemHtml('[x-text]').each(function () {
23 | if ($(this).attr('x-text') === `item.${prop}`) {
24 | $(this).text(item[prop]);
25 | }
26 | });
27 | itemHtml('[:src]').each(function () {
28 | if ($(this).attr(':src') === `item.${prop}`) {
29 | $(this).attr('src', item[prop]);
30 | }
31 | });
32 | itemHtml('[:id]').each(function () {
33 | if ($(this).attr(':id') === `item.${prop}`) {
34 | $(this).attr('id', item[prop]);
35 | }
36 | });
37 | }
38 | result += itemHtml.html();
39 | });
40 |
41 | $(this).replaceWith(result);
42 | });
43 |
44 | $('[x-html]').each(function () {
45 | const key = $(this).attr('x-html').slice(5); // Remove 'data.' from the start
46 | const value = _.get(data, key);
47 | if (value) {
48 | $(this).html(value);
49 | }
50 | });
51 |
52 | $('[x-text]').each(function () {
53 | const key = $(this).attr('x-text').slice(5); // Remove 'data.' from the start
54 | const value = _.get(data, key);
55 | if (value) {
56 | $(this).text(value);
57 | }
58 | });
59 |
60 | $('[:src]').each(function () {
61 | const key = $(this).attr(':src').slice(5); // Remove 'data.' from the start
62 | const value = _.get(data, key);
63 | if (value) {
64 | $(this).attr('src', value);
65 | }
66 | });
67 |
68 | $('[:id]').each(function () {
69 | const key = $(this).attr(':id').slice(5); // Remove 'data.' from the start
70 | const value = _.get(data, key);
71 | if (value) {
72 | $(this).attr('id', value);
73 | }
74 | });
75 |
76 | /*
77 | $('[x-if]').each(function() {
78 | const condition = $(this).attr('x-if').slice(5); // Remove 'data.' from the start
79 | const value = _.get(data, condition);
80 | if (!value) {
81 | $(this).remove();
82 | }
83 | });
84 | */
85 |
86 | return $.html();
87 | }
88 |
89 | export { compile };
90 |
--------------------------------------------------------------------------------