├── .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 |
7 |
8 |
9 |
10 | 11 |
12 |
13 |
14 |

15 |

16 |
17 |
18 |
19 |
20 |
21 | 22 |
23 | 30 |
31 | 32 | 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 | --------------------------------------------------------------------------------