├── .eleventy.js ├── .gitignore ├── LICENSE ├── README.md ├── netlify.toml ├── now.json ├── package-lock.json ├── package.json ├── postcss.config.js ├── site ├── .eleventyignore ├── 404.njk ├── _data │ ├── now.js │ ├── seo.json │ ├── site.json │ └── urls.js ├── blog │ ├── blog.json │ ├── post-1.md │ └── site.json ├── feed │ └── feed.njk ├── includes │ ├── favicon.html │ ├── footer.njk │ ├── head.njk │ ├── header.njk │ ├── layouts │ │ ├── default.njk │ │ ├── page.njk │ │ └── post.njk │ └── packs.njk ├── index.njk ├── robots.njk └── sitemap.njk ├── src ├── _redirects ├── controllers │ └── .gitkeep ├── images │ ├── 404.svg │ ├── favicons │ │ ├── favicon-16x16.png │ │ ├── favicon-32x32.png │ │ ├── favicon.ico │ │ └── favicon.png │ ├── hybrid-uGP_6CAD-14-unsplash.jpg │ └── origin.png ├── index.js ├── styles │ ├── main.scss │ ├── styles.scss │ └── syntax.css ├── templates │ └── packs.html └── utils │ ├── debugging.js │ ├── excerpts.js │ ├── filters.js │ ├── markdown.js │ └── seo.js ├── tailwind.config.js └── webpack.config.js /.eleventy.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const pluginRss = require("@11ty/eleventy-plugin-rss"); 3 | const pluginSyntaxHighlight = require("@11ty/eleventy-plugin-syntaxhighlight"); 4 | const debugging = require("./src/utils/debugging"); 5 | const seo = require("./src/utils/seo"); 6 | const excerpts = require("./src/utils/excerpts"); 7 | const markdown = require("./src/utils/markdown"); 8 | const { loadFilters } = require("./src/utils/filters"); 9 | 10 | module.exports = function(eleventyConfig) { 11 | // we need site/includes/packs.njk to be ignored in git 12 | // however, we still need it to watched for changes. 13 | // the .eleventyignore is used to tell Eleventy what to ignore 14 | eleventyConfig.setUseGitIgnore(false); 15 | eleventyConfig.setDataDeepMerge(true); 16 | 17 | const markdownIt = require("markdown-it"); 18 | const markdownItEmoji = require("markdown-it-emoji"); 19 | const markdownItFootnotes = require("markdown-it-footnote"); 20 | const options = { 21 | html: true, 22 | breaks: true, 23 | linkify: true 24 | }; 25 | 26 | const md = markdownIt(options) 27 | .use(markdownItEmoji) 28 | .use(markdownItFootnotes); 29 | 30 | debugging(eleventyConfig); 31 | seo(eleventyConfig); 32 | excerpts(eleventyConfig); 33 | markdown(eleventyConfig, md); 34 | loadFilters(eleventyConfig); 35 | eleventyConfig.setLibrary("md", md); 36 | 37 | eleventyConfig.addPairedShortcode("markdown", function(content) { 38 | return md.render(content); 39 | }); 40 | eleventyConfig.addPlugin(pluginRss); 41 | eleventyConfig.addPlugin(pluginSyntaxHighlight); 42 | eleventyConfig.setDataDeepMerge(true); 43 | 44 | eleventyConfig.addLayoutAlias("default", "layouts/default.njk"); 45 | eleventyConfig.addLayoutAlias("post", "layouts/post.njk"); 46 | eleventyConfig.addLayoutAlias("page", "layouts/page.njk"); 47 | 48 | eleventyConfig.addCollection("feed", collection => { 49 | return collection 50 | .getFilteredByTag("blog") 51 | .reverse() 52 | .slice(0, 20); 53 | }); 54 | 55 | // move to head so that it does not interfere 56 | // with turbolinks in development 57 | eleventyConfig.setBrowserSyncConfig({ 58 | // show 404s in dev. Borrowed from eleventy blog starter 59 | callbacks: { 60 | ready: function(_, browserSync) { 61 | // A bit of chicken and egg. This is keeps the exception 62 | // from showing during the first local build 63 | const generated404Exists = fs.existsSync("dist/404.html"); 64 | const content_404 = generated404Exists 65 | ? fs.readFileSync("dist/404.html") 66 | : "

File Does Not Exist

"; 67 | 68 | browserSync.addMiddleware("*", (_, res) => { 69 | // Provides the 404 content without redirect. 70 | res.write(content_404); 71 | res.end(); 72 | }); 73 | } 74 | }, 75 | // scripts in body conflict with Turbolinks 76 | snippetOptions: { 77 | rule: { 78 | match: /<\/head>/i, 79 | fn: function(snippet, match) { 80 | return snippet + match; 81 | } 82 | } 83 | } 84 | }); 85 | 86 | return { 87 | dir: { input: "site", output: "dist", data: "_data", includes: "includes" }, 88 | passthroughFileCopy: true, 89 | templateFormats: ["njk", "md", "css", "html", "yml"], 90 | htmlTemplateEngine: "njk" 91 | }; 92 | }; 93 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /dist 3 | .netlify/ 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Scott Watermasysk @scottw 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Eleventy Origin 2 | 3 | ![Origin Logo](src/images/origin.png) 4 | 5 | Origin is an opinionated starter template for [Eleventy](https://www.11ty.io). It was assembled using many of the tools and libraries I use often on Rails projects. 6 | 7 | ## Features 8 | 9 | It is preconfigured with the following: 10 | 11 | - [Webpack](https://webpack.js.org/) - for managing all of the assets 12 | - [Tailwind](https://tailwindcss.com/) - baked in for utility-first CSS 13 | - [Tailwind Forms](https://tailwindcss-custom-forms.netlify.com/) - a better starting point for form elements that looks good out of the box. 14 | - [Stimulus](https://stimulusjs.org/) - as a lightweight javascript framework 15 | - [PurgeCSS](https://www.purgecss.com/) - removes all unused CSS classes 16 | - [Turbolinks](https://github.com/turbolinks/turbolinks) - used to make navigating from page to page more efficient. No need to host a router/etc. 17 | - [Syntax Highlighting](https://github.com/11ty/eleventy-plugin-syntaxhighlight) - preconfigured syntax highlights 18 | - SEO - Under src/utils/seo.js is the basic starting point for a SEO plugin (similar to Jekyll SEO). It pulls data from the site.json file, but can be overridden with a seo.json file when want settings for bots. 19 | - Excerpt short code - extract an excerpt from your frontmatter or document body 20 | - Easily deploy to Netlify & Now 21 | 22 | ## UI 23 | 24 | There is no true default template. However, the default layout is configured to have both a sticky header and footer. 25 | 26 | ## Setup 27 | 28 | 1. `npm install` 29 | 30 | ## Directory Structure 31 | 32 | ### `src` 33 | 34 | All of the CSS, JS and images are stored in the `src` directory, which is 35 | managed by Webpack. 36 | 37 | - controllers - any stimulus controllers will automatically be picked up from the folder 38 | - images - all of your site's images. These will be copied to dist/assets/images when you build 39 | - styles 40 | - styles.scss - imports all other style sheets & sets up Tailwind CSS 41 | - main.scss - some minor styles to provide basic margins for markdown content. 42 | - sytax.css - the default CSS for code 43 | - templates - for now, a single template which contains the JavaScript and CSS packaged by webpack. 44 | - utils - JavaScript used to help build the site (such as the SEO custom tag) 45 | 46 | ### `site` 47 | 48 | All content and templates in in the `site` directory. This is managed and processed by Eleventy. 49 | 50 | ### `dist` 51 | 52 | Both Webpack and Eleventy push content and assets here. 53 | 54 | ## Webpack and Eleventy 55 | 56 | Webpack generates a main.js file and main.css file. Both saved to a file called `site/layouts/pack.njk`. This file ignored in Git and based on the template src/templates/pack.html. 57 | 58 | ## Usage 59 | 60 | ### Development 61 | 62 | You need to have both Webpack and Eleventy running. 63 | 64 | `npm run dev` 65 | 66 | _The first time you run this on a clean `dist` folder you may see an error about a missing pack.njk file. There is a bit of a chicken and egg. This file is generated by webpack (with hashed file names in production) may not exist when both are running concurrently. Longer term, I think we can drop this necessity by using Netlify build plugins to add hashes to assets._ 67 | 68 | You can also run them separately: 69 | 70 | 1. `npm run package` (I recommend starting this one first) 71 | 2. `npm run serve` 72 | 73 | ### Production 74 | 75 | This starter is also preconfigured to be easily deployable to Netlify and Now. If you need to deploy somewhere else: 76 | 77 | 1. `npm run build` 78 | 2. Point your webserver and/or deploy/etc the `/dist` folder. 79 | 80 | ## Prior Art 81 | 82 | - [Eleventy Base Blog](https://github.com/11ty/eleventy-base-blog) - good starting point. Borrowed lots from here. 83 | - [Jekyll-fun](https://github.com/joeybeninghove/jekyll-fun) - the core workflow (especially Webpack) is based off of Joey's original project. 84 | - [Skeleventy](https://skeleventy.netlify.com/) - A good boilerplate for Eleventy and Tailwind. Having something simple to refer back to was a big help. 85 | - [Deventy](https://github.com/ianrose/deventy) - A minimal 11ty starting point for building static websites with modern tools. 86 | 87 | ## Thanks 88 | 89 | Thanks to everyone who contributes to Eleventy, the numerous packages it depends on. 90 | -------------------------------------------------------------------------------- /netlify.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | publish = "dist" 3 | command = "npm run build" 4 | -------------------------------------------------------------------------------- /now.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 2, 3 | "builds": [ 4 | { 5 | "src": "package.json", 6 | "use": "@now/static-build", 7 | "config": { 8 | "distDir": "/dist" 9 | } 10 | } 11 | ], 12 | "build": { 13 | "env": {} 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "license": "MIT", 3 | "name": "origin-eleventy", 4 | "devDependencies": { 5 | "@11ty/eleventy": "^0.11.0", 6 | "@11ty/eleventy-plugin-rss": "^1.0.9", 7 | "@11ty/eleventy-plugin-syntaxhighlight": "^3.0.1", 8 | "@babel/core": "^7.11.4", 9 | "@babel/preset-env": "^7.11.0", 10 | "babel-loader": "^8.1.0", 11 | "babel-plugin-transform-class-properties": "^6.24.1", 12 | "copy-webpack-plugin": "^6.0.3", 13 | "css-loader": "^4.2.1", 14 | "cssnano": "^4.1.10", 15 | "file-loader": "^6.0.0", 16 | "luxon": "^1.25.0", 17 | "markdown-it-emoji": "^1.4.0", 18 | "markdown-it-footnote": "^3.0.2", 19 | "mini-css-extract-plugin": "^0.10.0", 20 | "node-sass": "^4.14.1", 21 | "npm-run-all": "^4.1.5", 22 | "postcss": "^7.0.32", 23 | "postcss-import": "^12.0.1", 24 | "postcss-loader": "^3.0.0", 25 | "postcss-preset-env": "^6.7.0", 26 | "purgecss": "^2.3.0", 27 | "sass-loader": "^9.0.3", 28 | "stimulus": "^1.1.1", 29 | "style-loader": "^1.2.1", 30 | "tailwindcss": "^1.7", 31 | "@tailwindcss/ui": "^0.5.0", 32 | "turbolinks": "^5.2.0", 33 | "typeface-inter": "^3.15.0", 34 | "webpack": "^4.44.1", 35 | "webpack-cli": "^3.3.12", 36 | "webpack-dev-server": "^3.11.0" 37 | }, 38 | "scripts": { 39 | "build": "run-s -l clean generate package:prod", 40 | "build:serve": "run-s -l clean generate package:prod serve:dist", 41 | "clean": "rm -rf dist", 42 | "clean:assets": "rm -rf dist/assets", 43 | "dev": "run-p -l package serve", 44 | "generate": "eleventy", 45 | "package": "webpack --mode development --watch --env.NODE_ENV=development", 46 | "package:prod": "webpack --mode production --env.NODE_ENV=production", 47 | "serve": "eleventy --serve", 48 | "serve:dist": "npx http-server dist" 49 | }, 50 | "dependencies": { 51 | "alpinejs": "^2.6.0" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = ({ env }) => { 2 | let environment = { 3 | plugins: [ 4 | require("postcss-import"), 5 | require("tailwindcss")("./tailwind.config.js"), 6 | require("postcss-preset-env"), 7 | ], 8 | }; 9 | 10 | if (env === "production") { 11 | environment.plugins.push( 12 | require("cssnano")({ 13 | preset: "default", 14 | }) 15 | ); 16 | } 17 | 18 | return environment; 19 | }; 20 | -------------------------------------------------------------------------------- /site/.eleventyignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /dist 3 | -------------------------------------------------------------------------------- /site/404.njk: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | permalink: 404.html 4 | eleventyExcludeFromCollections: true 5 | --- 6 |

Content Not Found

7 | 8 | Go home. 9 | 10 | Page Missing 11 | -------------------------------------------------------------------------------- /site/_data/now.js: -------------------------------------------------------------------------------- 1 | module.exports = () => { 2 | const date = new Date(); 3 | year = date.getFullYear(); 4 | return { 5 | copyright: (startYear = year) => { 6 | if (startYear === year) { 7 | return `© ${year}`; 8 | } 9 | return `© ${startYear}-${year}`; 10 | }, 11 | date, 12 | year 13 | }; 14 | }; 15 | -------------------------------------------------------------------------------- /site/_data/seo.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /site/_data/site.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Origin", 3 | "url": "https://example.com", 4 | "title": "Eleventy Origin", 5 | "description": "The Origin starter pays homage to Rails. While Eleventy itself is very opinionated and `just works`, Origin is quite a opinionated and leverages many of the libraries and tools I enjoy working in Rails", 6 | "feed": { 7 | "subtitle": "Origin - ", 8 | "filename": "feed.xml", 9 | "path": "/feed.xml" 10 | }, 11 | "author": { 12 | "name": "Scott Watermasysk", 13 | "email": "origin@scottw.com" 14 | }, 15 | "twitter": "@scottw" 16 | } 17 | -------------------------------------------------------------------------------- /site/_data/urls.js: -------------------------------------------------------------------------------- 1 | module.exports = () => { 2 | const site = require("./site.json"); 3 | return { 4 | site: process.env.URL || site.url 5 | }; 6 | }; 7 | -------------------------------------------------------------------------------- /site/blog/blog.json: -------------------------------------------------------------------------------- 1 | { 2 | "layout": "layouts/post" 3 | } 4 | -------------------------------------------------------------------------------- /site/blog/post-1.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: My First Post 3 | date: 2019-10-27 4 | featured_image: /assets/images/hybrid-uGP_6CAD-14-unsplash.jpg 5 | featured_image_alt: Orange and pink ballons with smiles 6 | image_caption: Photo by Dylan Gillis on Unsplash 7 | excerpt: A meeting is a gathering of two or more people that has been convened for the purpose of achieving a common goal through verbal interaction. 8 | tags: 9 | - blog 10 | --- 11 | 12 | ![Ballons](/assets/images/hybrid-uGP_6CAD-14-unsplash.jpg) 13 | 14 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras cursus sapien at neque pulvinar pretium. Curabitur id nunc sapien. Duis ac lorem vestibulum, tempor nisl ut, vehicula risus. Vestibulum fringilla sapien diam, at consequat ante luctus eu. Donec sodales ut felis ac venenatis. Quisque dictum sagittis rutrum. Curabitur dignissim odio ut eros lobortis, eu varius ligula placerat. Maecenas aliquam mauris ac eros blandit, et maximus massa faucibus. Vestibulum non elit aliquet, aliquet nibh in, consequat nisi. Vivamus semper pretium varius. Maecenas ornare quis nisl quis consectetur. Quisque bibendum, turpis sit amet eleifend auctor, dolor orci ullamcorper mauris, sed gravida diam eros at mi. Cras laoreet interdum nunc, dictum consectetur orci malesuada id. 15 | 16 | Curabitur vitae lorem id quam egestas efficitur. Etiam commodo velit id sodales tristique. Etiam nisi arcu, molestie sed pharetra a, fringilla eu nibh. Nulla eleifend leo luctus diam condimentum pretium. Vivamus blandit felis suscipit massa maximus, nec tincidunt ligula dictum. Mauris vel fermentum dolor, at mattis justo. Phasellus pellentesque erat mi, sit amet euismod nulla aliquet non. Praesent sed lacinia ligula, in pretium lectus. Mauris feugiat velit nec gravida laoreet. Maecenas vulputate euismod aliquet. Aenean ullamcorper sit amet ligula eu placerat. Donec elementum nibh at euismod mollis. 17 | 18 | Aenean et urna sagittis, malesuada lacus eget, semper libero. Aenean commodo maximus odio eget faucibus. In laoreet, eros vel dictum laoreet, enim quam feugiat elit, et tincidunt dui risus vel elit. Morbi ac mollis tellus, eget ullamcorper nisl. Nullam sit amet semper urna, eget blandit velit. Morbi pulvinar eget metus sit amet aliquam. Pellentesque sed urna sit amet erat luctus pretium in eget augue. Quisque convallis vehicula eros, vel tristique mi. In hac habitasse platea dictumst. 19 | 20 | Quisque sed tortor ac risus lacinia malesuada non ac sem. Donec consequat sagittis ultrices. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Sed venenatis rhoncus diam, vitae efficitur tortor vehicula a. Curabitur vel erat eu libero sodales fermentum in non arcu. Ut blandit mollis tempus. Nulla dapibus sapien vel scelerisque vulputate. Phasellus vulputate eu mauris id tempus. Pellentesque nunc massa, aliquam ut placerat id, pretium et leo. Ut ultrices neque ut nisl tristique, vel feugiat lorem suscipit. Vestibulum id bibendum risus. Pellentesque est mauris, sodales in scelerisque non, posuere ac nunc. 21 | 22 | Integer at justo in lacus tincidunt pharetra. Nullam magna erat, sagittis vitae pharetra vel, fermentum et nisl. Cras vulputate felis felis, id luctus lacus condimentum et. Suspendisse potenti. Morbi quis sem at nisl vulputate lacinia et ac elit. Fusce vel interdum dui. Curabitur sit amet ante ac nulla feugiat finibus. Proin interdum neque augue, eu eleifend velit sodales ac. Nulla facilisi. Maecenas sed egestas urna. Pellentesque pellentesque accumsan accumsan. Cras eu tincidunt nisi. Sed non pharetra urna. Sed elementum arcu vitae faucibus varius. Nulla dui arcu, porta non molestie in, rutrum in tellus. 23 | -------------------------------------------------------------------------------- /site/blog/site.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "VIA BLOG", 3 | "newone": true 4 | } 5 | -------------------------------------------------------------------------------- /site/feed/feed.njk: -------------------------------------------------------------------------------- 1 | --- 2 | permalink: feed.xml 3 | eleventyExcludeFromCollections: true 4 | --- 5 | 6 | 7 | {{ site.title }} 8 | {{ site.feed.subtitle }} 9 | 10 | {% set feedUrl %}{{ site.feed.url or site.feed.path | absoluteUrl(urls.site) }}{% endset %} 11 | 12 | 13 | {{ collections.blog | rssLastUpdatedDate }} 14 | {{ feedUrl }} 15 | 16 | {{ site.author.name }} 17 | {{ site.author.email }} 18 | 19 | {%- for post in collections.feed %} 20 | {% set absolutePostUrl %}{{ post.url | absoluteUrl(urls.site) | ts }}{% endset %} 21 | 22 | {{ post.data.title }} 23 | 24 | {{ post.date | rssDate }} 25 | {{ absolutePostUrl }} 26 | {{ post.templateContent | htmlToAbsoluteUrls(absolutePostUrl) }} 27 | 28 | {%- endfor %} 29 | 30 | -------------------------------------------------------------------------------- /site/includes/favicon.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /site/includes/footer.njk: -------------------------------------------------------------------------------- 1 | 5 | -------------------------------------------------------------------------------- /site/includes/head.njk: -------------------------------------------------------------------------------- 1 | {{ title or site.title }} 2 | 3 | 4 | 5 | 6 | 7 | {% seo %} 8 | 9 | 10 | {% include "packs.njk" %} 11 | -------------------------------------------------------------------------------- /site/includes/header.njk: -------------------------------------------------------------------------------- 1 |
2 | {{site.title}} 3 |
4 | -------------------------------------------------------------------------------- /site/includes/layouts/default.njk: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {% include "head.njk" %} 5 | 6 | 7 | 8 | {% include "header.njk" %} 9 |
10 | {{ content | safe }} 11 |
12 | {% include "footer.njk" %} 13 | 14 | 15 | -------------------------------------------------------------------------------- /site/includes/layouts/page.njk: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | --- 4 |

{{ title }}

5 | {{ content | safe }} 6 | -------------------------------------------------------------------------------- /site/includes/layouts/post.njk: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | --- 4 |

{{ title }}

5 | {{ content | safe }} 6 | -------------------------------------------------------------------------------- /site/includes/packs.njk: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /site/index.njk: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | featuredImage: /assets/images/origin.png 4 | title: HELLO THIS THING 5 | --- 6 | origin logo 7 |
8 | {% set postslist = collections.blog | reverse %} 9 |
    10 | {% for post in postslist %} 11 |
  1. {{post.data.title}}
  2. 12 | {% endfor %} 13 |
14 |
15 | -------------------------------------------------------------------------------- /site/robots.njk: -------------------------------------------------------------------------------- 1 | --- 2 | permalink: robots.txt 3 | hidden: true 4 | --- 5 | User-agent: * 6 | Sitemap: {{urls.site}}/sitemap.xml 7 | -------------------------------------------------------------------------------- /site/sitemap.njk: -------------------------------------------------------------------------------- 1 | --- 2 | permalink: sitemap.xml 3 | hidden: true 4 | --- 5 | 6 | 7 | {%- for page in collections.all %} 8 | {%- if not page.data.hidden %} 9 | 10 | {{ urls.site }}{{ page.url | url }} 11 | {{ page.date.toISOString() }} 12 | 13 | {%- endif %} 14 | {%- endfor %} 15 | 16 | -------------------------------------------------------------------------------- /src/_redirects: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scottwater/eleventy-origin/24ecff48830982fc11ea270aeb5f559ecbbb80c2/src/_redirects -------------------------------------------------------------------------------- /src/controllers/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scottwater/eleventy-origin/24ecff48830982fc11ea270aeb5f559ecbbb80c2/src/controllers/.gitkeep -------------------------------------------------------------------------------- /src/images/404.svg: -------------------------------------------------------------------------------- 1 | page not found -------------------------------------------------------------------------------- /src/images/favicons/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scottwater/eleventy-origin/24ecff48830982fc11ea270aeb5f559ecbbb80c2/src/images/favicons/favicon-16x16.png -------------------------------------------------------------------------------- /src/images/favicons/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scottwater/eleventy-origin/24ecff48830982fc11ea270aeb5f559ecbbb80c2/src/images/favicons/favicon-32x32.png -------------------------------------------------------------------------------- /src/images/favicons/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scottwater/eleventy-origin/24ecff48830982fc11ea270aeb5f559ecbbb80c2/src/images/favicons/favicon.ico -------------------------------------------------------------------------------- /src/images/favicons/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scottwater/eleventy-origin/24ecff48830982fc11ea270aeb5f559ecbbb80c2/src/images/favicons/favicon.png -------------------------------------------------------------------------------- /src/images/hybrid-uGP_6CAD-14-unsplash.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scottwater/eleventy-origin/24ecff48830982fc11ea270aeb5f559ecbbb80c2/src/images/hybrid-uGP_6CAD-14-unsplash.jpg -------------------------------------------------------------------------------- /src/images/origin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scottwater/eleventy-origin/24ecff48830982fc11ea270aeb5f559ecbbb80c2/src/images/origin.png -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import "./styles/styles.scss"; 2 | 3 | import { Application } from "stimulus"; 4 | import { definitionsFromContext } from "stimulus/webpack-helpers"; 5 | import turbolinks from "turbolinks"; 6 | import "typeface-inter"; 7 | import "alpinejs"; 8 | const application = Application.start(); 9 | const context = require.context("./controllers", true, /\.js$/); 10 | application.load(definitionsFromContext(context)); 11 | turbolinks.start(); 12 | -------------------------------------------------------------------------------- /src/styles/main.scss: -------------------------------------------------------------------------------- 1 | main { 2 | > *, 3 | blockquote > * { 4 | @apply mb-6; 5 | } 6 | 7 | .highlight-line { 8 | @apply pl-3; 9 | } 10 | 11 | > :last-child, 12 | blockquote > :last-child { 13 | @apply mb-0; 14 | } 15 | 16 | li { 17 | @apply mb-3; 18 | } 19 | 20 | li:last-child { 21 | @apply mb-0; 22 | } 23 | 24 | ul { 25 | list-style-type: disc; 26 | padding-left: theme("padding.6"); 27 | } 28 | 29 | ol { 30 | list-style-type: decimal; 31 | padding-left: theme("padding.6"); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/styles/styles.scss: -------------------------------------------------------------------------------- 1 | @import "tailwindcss/base"; 2 | @import "tailwindcss/components"; 3 | @import "main"; 4 | @import "syntax"; 5 | @import "tailwindcss/utilities"; 6 | -------------------------------------------------------------------------------- /src/styles/syntax.css: -------------------------------------------------------------------------------- 1 | code[class*="language-"], 2 | pre[class*="language-"] { 3 | font-size: 14px; 4 | line-height: 1.375; 5 | direction: ltr; 6 | text-align: left; 7 | white-space: pre; 8 | word-spacing: normal; 9 | word-break: normal; 10 | -moz-tab-size: 2; 11 | -o-tab-size: 2; 12 | tab-size: 2; 13 | -webkit-hyphens: none; 14 | -moz-hyphens: none; 15 | -ms-hyphens: none; 16 | hyphens: none; 17 | background: #272822; 18 | color: #f8f8f2; 19 | } 20 | pre[class*="language-"] { 21 | padding: 1.5em 0; 22 | margin: 0.5em 0; 23 | overflow: auto; 24 | } 25 | :not(pre) > code[class*="language-"] { 26 | padding: 0.1em; 27 | border-radius: 0.3em; 28 | } 29 | .token.comment, 30 | .token.prolog, 31 | .token.doctype, 32 | .token.cdata { 33 | color: #75715e; 34 | } 35 | .token.punctuation { 36 | color: #f8f8f2; 37 | } 38 | .token.namespace { 39 | opacity: 0.7; 40 | } 41 | .token.operator, 42 | .token.boolean, 43 | .token.number { 44 | color: #fd971f; 45 | } 46 | .token.property { 47 | color: #f4bf75; 48 | } 49 | .token.tag { 50 | color: #66d9ef; 51 | } 52 | .token.string { 53 | color: #a1efe4; 54 | } 55 | .token.selector { 56 | color: #ae81ff; 57 | } 58 | .token.attr-name { 59 | color: #fd971f; 60 | } 61 | .token.entity, 62 | .token.url, 63 | .language-css .token.string, 64 | .style .token.string { 65 | color: #a1efe4; 66 | } 67 | .token.attr-value, 68 | .token.keyword, 69 | .token.control, 70 | .token.directive, 71 | .token.unit { 72 | color: #a6e22e; 73 | } 74 | .token.statement, 75 | .token.regex, 76 | .token.atrule { 77 | color: #a1efe4; 78 | } 79 | .token.placeholder, 80 | .token.variable { 81 | color: #66d9ef; 82 | } 83 | .token.deleted { 84 | text-decoration: line-through; 85 | } 86 | .token.inserted { 87 | border-bottom: 1px dotted #f9f8f5; 88 | text-decoration: none; 89 | } 90 | .token.italic { 91 | font-style: italic; 92 | } 93 | .token.important, 94 | .token.bold { 95 | font-weight: bold; 96 | } 97 | .token.important { 98 | color: #f92672; 99 | } 100 | .token.entity { 101 | cursor: help; 102 | } 103 | pre > code.highlight { 104 | outline: 0.4em solid #f92672; 105 | outline-offset: 0.4em; 106 | } 107 | -------------------------------------------------------------------------------- /src/templates/packs.html: -------------------------------------------------------------------------------- 1 | <%= htmlWebpackPlugin.files.css.map((css) => ``).join("") %> 2 | <%= htmlWebpackPlugin.files.js.map((js) => ``).join("") %> 3 | -------------------------------------------------------------------------------- /src/utils/debugging.js: -------------------------------------------------------------------------------- 1 | module.exports = eleventyConfig => { 2 | eleventyConfig.addLiquidTag("pageData", function() { 3 | return { 4 | parse: function() {}, 5 | render: function(scope) { 6 | const { collections, pkg, scripts, ...pageData } = scope["contexts"][0]; 7 | let ret = `
${JSON.stringify(pageData, null, 2)}
`; 8 | return Promise.resolve(ret); 9 | } 10 | }; 11 | }); 12 | 13 | eleventyConfig.addNunjucksTag("pageData", function(nunjucksEngine) { 14 | return new (function() { 15 | this.tags = ["pageData"]; 16 | 17 | this.parse = function(parser, nodes, _) { 18 | var tok = parser.nextToken(); 19 | var args = parser.parseSignature(null, true); 20 | 21 | // fake it until you make it! 22 | // https://github.com/mozilla/nunjucks/issues/158#issuecomment-34919343 23 | if (args.children.length === 0) { 24 | args.addChild(new nodes.Literal(0, 0, "")); 25 | } 26 | 27 | parser.advanceAfterBlockEnd(tok.value); 28 | return new nodes.CallExtensionAsync(this, "run", args); 29 | }; 30 | 31 | this.run = function(context, _, callback) { 32 | // exclude the things we do not want 33 | // and cause circular references (collections) :( 34 | const { collections, pkg, scripts, ...pageData } = context["ctx"]; 35 | let ret = new nunjucksEngine.runtime.SafeString( 36 | `
${JSON.stringify(pageData, null, 2)}
` 37 | ); 38 | callback(null, ret); 39 | }; 40 | })(); 41 | }); 42 | 43 | //Nunjucks has a dump filter, but it is not available in Liquid 44 | eleventyConfig.addFilter("dump", function(value, spaces = 2) { 45 | return `
${JSON.stringify(value || "NOTHING", null, spaces)}
`; 46 | }); 47 | }; 48 | -------------------------------------------------------------------------------- /src/utils/excerpts.js: -------------------------------------------------------------------------------- 1 | // Based code found here: https://github.com/11ty/eleventy/issues/179#issuecomment-413119342 2 | 3 | module.exports = function(eleventyConfig, options = {}) { 4 | options.stripTags = options.stripTags || false; 5 | options.excerptMinimumLength = options.excerptMinimumLength || 140; 6 | options.excerptSeparator = options.excerptSeparator || ""; 7 | options.frontMatterKey = options.frontMatterKey || "excerpt"; 8 | eleventyConfig.addShortcode("excerpt", post => findAndCleanExcerpt(post)); 9 | 10 | function findAndCleanExcerpt(post) { 11 | const rawExcerpt = extractExcerpt(post); 12 | if (options.stripTags) { 13 | return stripHTML(rawExcerpt); 14 | } 15 | return rawExcerpt; 16 | } 17 | 18 | /** 19 | * Extracts the excerpt from a document. 20 | * 21 | * @param {*} doc A real big object full of all sorts of information about a document. 22 | * @returns {String} the excerpt. 23 | */ 24 | function extractExcerpt(doc) { 25 | if (doc.data[options.frontMatterKey]) { 26 | return doc.data[options.frontMatterKey]; 27 | } 28 | if (!doc.hasOwnProperty("templateContent")) { 29 | console.warn( 30 | "Failed to extract excerpt: Document has no property `templateContent`." 31 | ); 32 | return ""; 33 | } 34 | 35 | const content = doc.templateContent.replace(/

/, ""); 36 | const excerptIndex = content.indexOf(options.excerptSeparator); 37 | if (excerptIndex > -1) { 38 | return content.substring(0, excerptIndex).trim(); 39 | } else if (content.length <= options.excerptMinimumLength) { 40 | return content.trim(); 41 | } 42 | 43 | const excerptEnd = findExcerptEnd(content); 44 | return content.substring(0, excerptEnd).trim(); 45 | } 46 | 47 | /** 48 | * Finds the end position of the excerpt of a given piece of content. 49 | * This should only be used when there is no excerpt marker in the content (e.g. no ``). 50 | * 51 | * @param {String} content The full text of a piece of content (e.g. a blog post) 52 | * @param {Number?} skipLength Amount of characters to skip before starting to look for a `

` 53 | * tag. This is used when calling this method recursively. 54 | * @returns {Number} the end position of the excerpt 55 | */ 56 | function findExcerptEnd(content, skipLength = 0) { 57 | if (content === "") { 58 | return 0; 59 | } 60 | 61 | const paragraphEnd = content.indexOf("

", skipLength) + 4; 62 | 63 | if (paragraphEnd < options.excerptMinimumLength) { 64 | return ( 65 | paragraphEnd + 66 | findExcerptEnd(content.substring(paragraphEnd), paragraphEnd) 67 | ); 68 | } 69 | 70 | return paragraphEnd; 71 | } 72 | 73 | function stripHTML(content) { 74 | if (content === "") { 75 | return ""; 76 | } 77 | 78 | return content.replace( 79 | /<\/?([a-z][a-z0-9]*)\b[^>]*>|/gi, 80 | "" 81 | ); 82 | } 83 | }; 84 | -------------------------------------------------------------------------------- /src/utils/filters.js: -------------------------------------------------------------------------------- 1 | function removeTrailingSlash(url) { 2 | if (url && url.endsWith("/")) { 3 | return url.slice(0, url.length - 1); 4 | } 5 | return url; 6 | } 7 | 8 | function loadFilters(eleventyConfig) { 9 | eleventyConfig.addFilter("ts", removeTrailingSlash); 10 | } 11 | 12 | module.exports = { removeTrailingSlash, loadFilters }; 13 | -------------------------------------------------------------------------------- /src/utils/markdown.js: -------------------------------------------------------------------------------- 1 | module.exports = (eleventyConfig, md) => { 2 | if (!md) { 3 | throw "Please pass an instance of MarkdownIt to the markdown filters"; 4 | } 5 | 6 | eleventyConfig.addPairedShortcode("markdown", function(content) { 7 | return md.render(content.toString()); 8 | }); 9 | 10 | eleventyConfig.addFilter("md", function(content) { 11 | return md.render(content.toString()); 12 | }); 13 | }; 14 | -------------------------------------------------------------------------------- /src/utils/seo.js: -------------------------------------------------------------------------------- 1 | const { DateTime } = require("luxon"); 2 | 3 | module.exports = (eleventyConfig, options = {}) => { 4 | // borrowed from Eleventy Rss feed plugin 5 | function absoluteUrl(url, base) { 6 | try { 7 | return new URL(url, base).toString(); 8 | } catch (e) { 9 | console.log(`Failed on ${url} ${base}`); 10 | return url; 11 | } 12 | } 13 | 14 | // borrowed from Eleventy Rss feed plugin 15 | function isoDate(dateObj) { 16 | return DateTime.fromJSDate(dateObj).toISO({ 17 | includeOffset: true, 18 | suppressMilliseconds: true 19 | }); 20 | } 21 | 22 | eleventyConfig.addNunjucksTag("seo", function(nunjucksEngine) { 23 | return new (function() { 24 | this.tags = ["seo"]; 25 | 26 | this.parse = function(parser, nodes, _) { 27 | var tok = parser.nextToken(); 28 | var args = parser.parseSignature(null, true); 29 | 30 | // fake it until you make it! 31 | // https://github.com/mozilla/nunjucks/issues/158#issuecomment-34919343 32 | if (args.children.length === 0) { 33 | args.addChild(new nodes.Literal(0, 0, "")); 34 | } 35 | 36 | parser.advanceAfterBlockEnd(tok.value); 37 | return new nodes.CallExtensionAsync(this, "run", args); 38 | }; 39 | 40 | this.run = function(context, _, callback) { 41 | const { 42 | page, 43 | date, 44 | excerpt, 45 | description, 46 | keywords, 47 | og_image, 48 | og_image_alt, 49 | featured_image, 50 | featured_image_alt, 51 | image, 52 | image_alt, 53 | type, 54 | title 55 | } = context["ctx"]; 56 | 57 | const site = 58 | typeof options.site === "object" 59 | ? options.site 60 | : context["ctx"][options.site || "site"]; 61 | 62 | const seo = 63 | typeof options.seo === "object" 64 | ? options.seo 65 | : context["ctx"][options.seo || "seo"]; 66 | 67 | // note to future self - inline so that seo & site do 68 | // not need to be arguments 69 | const pullWithBackup = (key, defaultValue = null) => { 70 | return (seo && seo[key]) || (site && site[key]) || defaultValue; 71 | }; 72 | const pageTitle = title || site.title; 73 | const metaDescription = (description || excerpt || "").replace( 74 | /<\/?([a-z][a-z0-9]*)\b[^>]*>|/gi, 75 | "" 76 | ); 77 | const local = pullWithBackup("local", "en_US"); 78 | const metaKeywords = keywords || pullWithBackup("keywords"); 79 | const baseUrl = process.env.URL || pullWithBackup("url"); 80 | const pageUrl = `${baseUrl}${page.url}`; 81 | const publishedTime = isoDate(date); 82 | const siteTitle = pullWithBackup("title", title); 83 | const twitter = `@${pullWithBackup("twitter")}`.replace(/^@@/, "@"); 84 | const rawImage = og_image || featured_image || image; 85 | const alt = og_image_alt || featured_image_alt || image_alt; 86 | const resolvedOgImage = rawImage 87 | ? absoluteUrl(rawImage, baseUrl) 88 | : null; 89 | const typeOfContent = 90 | type || (page.url === "/" ? "website" : "article"); 91 | const typeOfTwitterCard = pullWithBackup("twitterCardType", "summary"); 92 | 93 | const structuredData = { 94 | description: metaDescription, 95 | headline: title, 96 | "@type": "WebSite", 97 | image: resolvedOgImage, 98 | url: pageUrl, 99 | name: siteTitle, 100 | "@context": "https://schema.org" 101 | }; 102 | const template = ` 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | {% if resolvedOgImage %} 115 | 116 | 117 | 118 | 119 | 120 | {% endif %} 121 | 124 | `.replace(/^\s+/gm, ""); 125 | 126 | const output = nunjucksEngine.renderString(template, { 127 | pageTitle, 128 | local, 129 | metaDescription, 130 | metaKeywords, 131 | pageUrl, 132 | siteTitle, 133 | typeOfContent, 134 | publishedTime, 135 | resolvedOgImage, 136 | alt, 137 | typeOfTwitterCard, 138 | twitter, 139 | structuredData: JSON.stringify(structuredData) 140 | }); 141 | 142 | let ret = new nunjucksEngine.runtime.SafeString(output); 143 | callback(null, ret); 144 | }; 145 | })(); 146 | }); 147 | }; 148 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | const defaultTheme = require("tailwindcss/defaultTheme"); 2 | const fonts = defaultTheme.fontFamily.sans; 3 | module.exports = { 4 | plugins: [require("@tailwindcss/ui")], 5 | purge: { 6 | mode: "all", 7 | content: ["./dist/**/*.html"], 8 | }, 9 | theme: { 10 | extend: { 11 | fontFamily: { 12 | sans: ["Inter", ...fonts], 13 | }, 14 | screens: { 15 | dm: { raw: "(prefers-color-scheme: dark)" }, 16 | }, 17 | }, 18 | }, 19 | }; 20 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const MiniCssExtractPlugin = require("mini-css-extract-plugin"); 3 | const CopyWebpackPlugin = require("copy-webpack-plugin"); 4 | 5 | module.exports = (env) => { 6 | process.env.NODE_ENV = (env && env.NODE_ENV) || "development"; 7 | return { 8 | entry: path.resolve(__dirname, "src/index.js"), 9 | devtool: "source-map", 10 | output: { 11 | filename: "index.js", 12 | path: path.resolve(__dirname, "dist/assets"), 13 | publicPath: "/assets/", 14 | }, 15 | module: { 16 | rules: [ 17 | { 18 | test: /\.js$/, 19 | exclude: [/node_modules/], 20 | use: [ 21 | { 22 | loader: "babel-loader", 23 | options: { 24 | presets: ["@babel/preset-env"], 25 | plugins: ["transform-class-properties"], 26 | }, 27 | }, 28 | ], 29 | }, 30 | { 31 | test: /\.((s[ac]ss)|(css))$/, 32 | use: [ 33 | { loader: MiniCssExtractPlugin.loader }, 34 | { loader: "css-loader" }, 35 | { loader: "postcss-loader" }, 36 | { loader: "sass-loader" }, 37 | ], 38 | }, 39 | { 40 | test: /\.(woff(2)?|ttf|eot|svg)(\?v=\d+\.\d+\.\d+)?$/, 41 | use: [ 42 | { 43 | loader: "file-loader", 44 | options: { 45 | name: "[name].[ext]", 46 | outputPath: "fonts/", 47 | }, 48 | }, 49 | ], 50 | }, 51 | ], 52 | }, 53 | plugins: [ 54 | new MiniCssExtractPlugin(), 55 | new CopyWebpackPlugin({ 56 | patterns: [ 57 | { 58 | from: path.resolve("src/images"), 59 | to: path.resolve("./dist/assets/images"), 60 | }, 61 | { 62 | from: path.resolve("src/images/favicons/favicon.ico"), 63 | to: path.resolve("./dist"), 64 | }, 65 | { 66 | from: path.resolve("src/_redirects"), 67 | to: path.resolve("./dist"), 68 | }, 69 | ], 70 | }), 71 | ], 72 | }; 73 | }; 74 | --------------------------------------------------------------------------------