├── .eleventy.js ├── .gitignore ├── .prettierrc ├── README.md ├── api └── hello.js ├── data ├── config.js └── env.js ├── lib ├── queries.js ├── sanity.js └── serializers.js ├── netlify.toml ├── package.json ├── postcss.config.js ├── public ├── fonts │ └── .gitkeep └── icons │ └── .gitkeep ├── scripts ├── app.js ├── components │ ├── lazy.js │ ├── scroll.js │ └── sticky.js ├── index.js ├── lib │ ├── allToArray.js │ ├── html.js │ ├── inview.js │ ├── loadFonts.js │ ├── poll.js │ ├── raf.js │ └── signal.js └── transitions │ └── Fade.js ├── studio ├── config │ ├── .checksums │ └── @sanity │ │ ├── data-aspects.json │ │ ├── default-layout.json │ │ ├── default-login.json │ │ └── form-builder.json ├── index.js ├── lib │ └── .gitkeep ├── package.json ├── parts │ ├── dashboard.js │ └── structure.js ├── sanity.json ├── static │ ├── .gitkeep │ └── favicon.ico └── types │ ├── asset.js │ ├── config.js │ └── seo.js ├── styles └── index.css ├── tailwind.config.js ├── templates ├── about.njk ├── includes │ ├── head.njk │ └── header.njk ├── index.njk ├── layouts │ └── base.njk └── tasks │ ├── esbuild.11ty.js │ └── postcss.11ty.js └── yarn.lock /.eleventy.js: -------------------------------------------------------------------------------- 1 | const cx = require('nanoclass') 2 | const blocksToHtml = require('@sanity/block-content-to-html') 3 | const htmlmin = require('html-minifier') 4 | const getSerializers = require('./lib/serializers') 5 | 6 | module.exports = (config) => { 7 | config.setUseGitIgnore(false) 8 | 9 | config.addShortcode('classList', (...all) => cx(all)) 10 | 11 | config.addShortcode( 12 | 'debug', 13 | (value) => 14 | `
${JSON.stringify(
15 |         value,
16 |         null,
17 |         2,
18 |       )}
`, 19 | ) 20 | 21 | config.addFilter('blocksToHtml', (blocks, type, theme) => { 22 | try { 23 | return blocksToHtml({ 24 | blocks, 25 | serializers: getSerializers(theme)[type], 26 | }) 27 | } catch (e) { 28 | console.log('Error converting blocks to HTML in blocksToHtml filter:', e) 29 | return '' 30 | } 31 | }) 32 | 33 | config.addWatchTarget('./tailwind.config.js') 34 | config.addWatchTarget('./lib') 35 | config.addWatchTarget('./styles') 36 | config.addWatchTarget('./scripts') 37 | 38 | config.addPassthroughCopy({ './public': '/' }) 39 | 40 | config.addTransform('htmlmin', (content, outputPath) => { 41 | if (outputPath && outputPath.endsWith('.html')) { 42 | let minified = htmlmin.minify(content, { 43 | useShortDoctype: true, 44 | removeComments: true, 45 | collapseWhitespace: true, 46 | }) 47 | 48 | return minified 49 | } 50 | 51 | return content 52 | }) 53 | 54 | return { 55 | dir: { 56 | input: 'templates', 57 | data: '../data', 58 | includes: 'includes', 59 | layouts: 'layouts', 60 | output: 'build', 61 | }, 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | build 4 | dist -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 80, 3 | "tabWidth": 2, 4 | "useTabs": false, 5 | "semi": false, 6 | "singleQuote": true, 7 | "trailingComma": "all", 8 | "arrowParens": "always", 9 | "endOfLine": "lf" 10 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # sane-eleventy ![Prerequisite](https://img.shields.io/badge/node-12.18.2-red.svg) ![Prerequisite](https://img.shields.io/badge/yarn-1.22.4-blue.svg) 2 | 3 | > Repo template for [Sanity](https://sanity.io) + [Eleventy](https://11ty.dev) projects at [Self Aware](https://selfaware.studio) 4 | 5 | ## 📖 About 6 | 7 | - Templates are authored in the [`templates`](templates) directory using any template language supported by eleventy (Nunjucks by default) 8 | - Eleventy global data files live in [`data`](data). 9 | - Styles are authored using Tailwind and the entry point lives in [`styles`](styles/index.css). We **always** leverage Tailwind utilities before resorting to adding custom styles. 10 | - JavaScript lives in [`scripts`](scripts) 11 | - Place any static files in the [`public`](public) folder 12 | - Netlify serverless functions are authored in [`api`](api) 13 | - Place any eleventy related utilities in [`lib`](lib) 14 | - Sanity Studio lives in the [`studio`](studio) directory 15 | 16 | ## ✨ Install 17 | 18 | ```sh 19 | # Install Yarn 20 | npm i -g yarn 21 | 22 | # Install project dependencies using yarn 23 | yarn 24 | 25 | # Install Sanity Studio dependencies using yarn 26 | cd studio && yarn 27 | ``` 28 | 29 | ## 👩🏻‍💻 Usage 30 | 31 | To set up with Sanity, you will need to set your Sanity `projectId` and `dataset` in two places: 32 | 33 | 1. [`studio/sanity.json`](studio/sanity.json) 34 | 2. [`lib/sanity.js`](lib/sanity.js) 35 | 36 | ### Development 37 | 38 | ```sh 39 | # Start Netlify dev server 40 | yarn dev 41 | 42 | # Start Sanity dev server 43 | cd studio && yarn start 44 | ``` 45 | 46 | ### Production 47 | 48 | ```sh 49 | # Build front-end for production 50 | yarn build 51 | ``` 52 | 53 | ### Deployment 54 | 55 | Deploy the front-end using Netlify 56 | 57 | ```sh 58 | # Deploy Sanity Studio 59 | cd studio && yarn deploy 60 | ``` 61 | 62 | ## 🖼️ Showcase 63 | 64 | The following sites are powered by sane-eleventy: 65 | 66 | - [Off Season](https://offseasoncreative.com) 67 | - [MIT Digital Humanities](https://digitalhumanities.mit.edu) 68 | - [Rosaluna](https://mezcalrosaluna.com) 69 | 70 | ## 💡 Inspiration 71 | 72 | Thanks to [sane-shopify](https://github.com/good-idea/sane-shopify) for the name inspiration 🙂 73 | 74 | ## 🧾 License 75 | 76 | MIT 77 | -------------------------------------------------------------------------------- /api/hello.js: -------------------------------------------------------------------------------- 1 | exports.handler = async () => ({ 2 | statusCode: 200, 3 | body: JSON.stringify({ message: 'Hello World' }), 4 | }) 5 | -------------------------------------------------------------------------------- /data/config.js: -------------------------------------------------------------------------------- 1 | // const groq = require('groq') 2 | // const client = require('../lib/sanity.js') 3 | 4 | // module.exports = async function() { 5 | // return await client.fetch(groq`*[_type == 'config'][0] { 6 | // seo { 7 | // ..., 8 | // image { ...image.asset->, alt } 9 | // } 10 | // }`) 11 | // } 12 | -------------------------------------------------------------------------------- /data/env.js: -------------------------------------------------------------------------------- 1 | module.exports = process.env.NODE_ENV 2 | -------------------------------------------------------------------------------- /lib/queries.js: -------------------------------------------------------------------------------- 1 | // const groq = require('groq') 2 | 3 | // module.exports.menuItem = groq`{ 4 | // title, 5 | // description, 6 | // price, 7 | // 'badge': badge->title, 8 | // }` 9 | 10 | // module.exports.location = groq`{ 11 | // address, 12 | // footerLinks[] { title, url }, 13 | // friends[]-> { 14 | // title, 15 | // link, 16 | // info, 17 | // image { 18 | // alt, 19 | // ...asset-> { 20 | // _id, 21 | // url, 22 | // assetId, 23 | // 'width': metadata.dimensions.width, 24 | // 'height': metadata.dimensions.height, 25 | // 'aspect': metadata.dimensions.aspectRatio, 26 | // 'lqip': metadata.lqip, 27 | // }, 28 | // }, 29 | // }, 30 | // friendsIntroText, 31 | // halfsies[] { 32 | // _type, 33 | // _type == 'infoText' => { 34 | // content, 35 | // }, 36 | // _type == 'carousel' => { 37 | // images[] { alt, ...asset-> }, 38 | // }, 39 | // _type == 'descriptionText' => { 40 | // content, 41 | // }, 42 | // _type == 'bigButton' => { 43 | // title, 44 | // url, 45 | // }, 46 | // }, 47 | // instagramHandle, 48 | // menuCarousel[] { alt, ...asset-> }, 49 | // menuCategories[]-> { 50 | // title, 51 | // 'slug': slug.current, 52 | // 'subcategories': lists[]-> { 53 | // title, 54 | // 'slug': slug.current, 55 | // image { alt, ...asset-> }, 56 | // items[] { 57 | // _type, 58 | // _type == 'menuItem' => { 59 | // ...menuItem-> ${module.exports.menuItem}, 60 | // }, 61 | // _type == 'menuGroup' => { 62 | // title, 63 | // menuItems[]-> ${module.exports.menuItem}, 64 | // }, 65 | // }, 66 | // }, 67 | // }, 68 | // menuIntroText, 69 | // neighborhood, 70 | // orderNowLink, 71 | // 'slug': slug.current, 72 | // tagline, 73 | // title, 74 | // whole[] { 75 | // _type, 76 | // _type == 'carousel' => { 77 | // images[] { alt, ...asset-> }, 78 | // }, 79 | // _type == 'threeUp' => { 80 | // cards[] { 81 | // link { title, url }, 82 | // image { alt, ...asset-> }, 83 | // }, 84 | // }, 85 | // }, 86 | // }` 87 | -------------------------------------------------------------------------------- /lib/sanity.js: -------------------------------------------------------------------------------- 1 | const client = require('@sanity/client') 2 | 3 | module.exports = client({ 4 | projectId: '', 5 | dataset: 'production', 6 | apiVersion: 'v2021-06-07', 7 | useCdn: false, 8 | }) 9 | -------------------------------------------------------------------------------- /lib/serializers.js: -------------------------------------------------------------------------------- 1 | const blocksToHtml = require('@sanity/block-content-to-html') 2 | const h = blocksToHtml.h 3 | 4 | module.exports = { 5 | default: { 6 | types: { 7 | block: (props) => { 8 | const { style = 'normal' } = props.node 9 | 10 | if (style === 'h3') { 11 | return h( 12 | 'h3', 13 | { 14 | className: 'text-50 font-bold mb-20', 15 | }, 16 | props.children, 17 | ) 18 | } 19 | 20 | if (style === 'normal') { 21 | return h( 22 | 'p', 23 | { 24 | className: 'text-18 m:text-22 leading-130 mb-30 m:mb-40', 25 | }, 26 | props.children, 27 | ) 28 | } 29 | 30 | return blocksToHtml.defaultSerializers.types.block(props) 31 | }, 32 | button: ({ node }) => { 33 | return h( 34 | 'a', 35 | { 36 | className: 37 | 'inline-block text-18 m:text-22 rounded-full py-15 px-30 border-2 border-black bg-black text-white hover:bg-white hover:text-black transition duration-300 ease-out-quint', 38 | href: node.url, 39 | }, 40 | node.title, 41 | ) 42 | }, 43 | }, 44 | marks: { 45 | link: ({ children, mark }) => 46 | h( 47 | 'a', 48 | { 49 | className: 'underline hover:no-underline', 50 | href: mark.url, 51 | }, 52 | children, 53 | ), 54 | }, 55 | }, 56 | } 57 | -------------------------------------------------------------------------------- /netlify.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | command = "yarn build" 3 | functions = "api" 4 | publish = "build" 5 | 6 | [dev] 7 | framework = "#custom" 8 | command = "yarn serve" 9 | port = 3000 10 | targetPort = 8080 11 | publish = "build" 12 | autoLaunch = true -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sane-eleventy", 3 | "version": "2.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "scripts": { 7 | "eleventy:serve": "eleventy --serve", 8 | "eleventy:build": "eleventy", 9 | "clean": "rm -rf ./build", 10 | "debug": "DEBUG=Eleventy* NODE_ENV=development npm-run-all clean eleventy:serve", 11 | "serve": "NODE_ENV=development npm-run-all clean eleventy:serve", 12 | "build": "NODE_ENV=production npm-run-all clean eleventy:build", 13 | "dev": "netlify dev" 14 | }, 15 | "devDependencies": { 16 | "@11ty/eleventy": "^0.12.1", 17 | "@sanity/block-content-to-html": "^2.0.0", 18 | "@sanity/client": "^2.15.0", 19 | "autoprefixer": "^10.3.1", 20 | "browser-sync": "^2.27.4", 21 | "cssnano": "^5.0.7", 22 | "cssnano-preset-advanced": "^5.1.3", 23 | "esbuild": "^0.12.15", 24 | "groq": "^2.13.0", 25 | "html-minifier": "^4.0.0", 26 | "nanoclass": "^0.0.2", 27 | "netlify-cli": "^6.2.3", 28 | "npm-run-all": "^4.1.5", 29 | "postcss": "^8.3.6", 30 | "postcss-cli": "^8.3.1", 31 | "postcss-custom-properties": "^11.0.0", 32 | "postcss-focus-visible": "^5.0.0", 33 | "postcss-load-config": "^3.1.0", 34 | "postcss-nested": "^5.0.5", 35 | "tailwindcss": "^2.2.7" 36 | }, 37 | "dependencies": { 38 | "@dogstudio/highway": "^2.2.1", 39 | "choozy": "^1.0.0", 40 | "focus-visible": "^5.2.0", 41 | "fontfaceobserver": "^2.1.0", 42 | "gsap": "^3.7.1", 43 | "martha": "^3.0.3", 44 | "picoapp": "^3.6.2", 45 | "quicklink": "^2.2.0" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | 'postcss-nested': {}, 5 | 'postcss-custom-properties': {}, 6 | 'postcss-focus-visible': {}, 7 | autoprefixer: {}, 8 | cssnano: { preset: 'advanced' }, 9 | }, 10 | } 11 | -------------------------------------------------------------------------------- /public/fonts/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/selfawarestudio/sane-eleventy/81e83e42b884af3e9e421c2b2d5e484bbdfad878/public/fonts/.gitkeep -------------------------------------------------------------------------------- /public/icons/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/selfawarestudio/sane-eleventy/81e83e42b884af3e9e421c2b2d5e484bbdfad878/public/icons/.gitkeep -------------------------------------------------------------------------------- /scripts/app.js: -------------------------------------------------------------------------------- 1 | import { picoapp } from 'picoapp' 2 | import { size, qs } from 'martha' 3 | 4 | import lazy from './components/lazy' 5 | import scroll from './components/scroll' 6 | import sticky from './components/sticky' 7 | 8 | const components = { lazy, scroll, sticky } 9 | 10 | const sizes = size() 11 | const state = { 12 | ...sizes, 13 | mx: sizes.ww / 2, 14 | my: sizes.wh / 2, 15 | dom: { 16 | html: document.documentElement, 17 | body: document.body, 18 | scrollProxy: qs('.js-scroll-proxy'), 19 | }, 20 | fonts: [ 21 | // { family: 'GT Walsheim' }, 22 | // { family: 'GT Walsheim', options: { weight: 300 } }, 23 | // { family: 'GT Walsheim', options: { weight: 300, style: 'italic' } }, 24 | ], 25 | } 26 | 27 | export default picoapp(components, state) 28 | -------------------------------------------------------------------------------- /scripts/components/lazy.js: -------------------------------------------------------------------------------- 1 | import { component } from 'picoapp' 2 | import { noop, on, remove } from 'martha' 3 | import choozy from 'choozy' 4 | 5 | export default component((node, ctx) => { 6 | let offLoad = noop 7 | let offEnd = noop 8 | 9 | ctx.on('enter:completed', () => { 10 | let refs = choozy(node) 11 | 12 | offLoad = on(refs.img, 'load', () => { 13 | offLoad() 14 | offLoad = noop 15 | 16 | if (refs?.lqip) { 17 | offEnd = on(refs.img, 'transitionend', () => { 18 | offEnd() 19 | offEnd = noop 20 | refs.lqip.remove() 21 | }) 22 | } 23 | 24 | remove(refs.img, 'opacity-0') 25 | }) 26 | 27 | refs.img.src = refs.img.dataset.src 28 | refs.img.removeAttribute('data-src') 29 | }) 30 | 31 | return () => { 32 | offLoad() 33 | offEnd() 34 | } 35 | }) 36 | -------------------------------------------------------------------------------- /scripts/components/scroll.js: -------------------------------------------------------------------------------- 1 | import { component } from 'picoapp' 2 | import { on, rect } from 'martha' 3 | import gsap from 'gsap' 4 | 5 | export default component((node, ctx) => { 6 | ctx.on('resize', ({ dom }) => { 7 | gsap.set(dom.scrollProxy, { height: rect(node).height }) 8 | gsap.set(node, { y: 0 }) 9 | ctx.emit('resize:reset') 10 | }) 11 | 12 | ctx.on('tick', ({ scroll }) => { 13 | gsap.set(node, { y: -scroll }) 14 | }) 15 | 16 | let offKeydown = on(document, 'keydown', ({ key }) => { 17 | if (key !== 'Tab') return 18 | 19 | requestAnimationFrame(() => { 20 | const { wh } = ctx.getState() 21 | const activeEl = document.activeElement 22 | const bounds = rect(activeEl) 23 | const isFullyVisible = bounds.top >= 0 && bounds.bottom <= wh 24 | 25 | if (!isFullyVisible) { 26 | const midY = wh * 0.5 - bounds.height * 0.5 27 | const offset = window.scrollY + bounds.top - midY 28 | window.scroll(0, offset) 29 | } 30 | }) 31 | }) 32 | 33 | return () => { 34 | offKeydown() 35 | gsap.set(ctx.getState().dom.scrollProxy, { height: 0 }) 36 | } 37 | }) 38 | -------------------------------------------------------------------------------- /scripts/components/sticky.js: -------------------------------------------------------------------------------- 1 | import { component } from 'picoapp' 2 | import gsap from 'gsap' 3 | import { qs, rect } from 'martha' 4 | 5 | export default component((node, ctx) => { 6 | let el = qs('[data-sticky-el]', node) 7 | let minWidth = ctx.getState().screens?.[node.dataset.screen] 8 | let nodeRect = null 9 | let elRect = null 10 | 11 | ctx.on('resize:reset', () => { 12 | gsap.set(el, { y: 0 }) 13 | nodeRect = rect(node) 14 | elRect = rect(el) 15 | }) 16 | 17 | ctx.on('tick', ({ scroll, ww }) => { 18 | if (typeof minWidth === 'undefined' || ww >= minWidth) { 19 | let top = elRect.top - scroll 20 | if (top <= 0) { 21 | let bottom = nodeRect.top + nodeRect.height - scroll 22 | if (bottom > elRect.height) { 23 | gsap.set(el, { y: scroll - elRect.top }) 24 | } else { 25 | const val = 26 | nodeRect.height - elRect.height - nodeRect.top - elRect.top 27 | gsap.set(el, { y: val }) 28 | } 29 | } else { 30 | gsap.set(el, { y: null }) 31 | } 32 | } else { 33 | gsap.set(el, { clearProps: 'y' }) 34 | } 35 | }) 36 | 37 | return () => {} 38 | }) 39 | -------------------------------------------------------------------------------- /scripts/index.js: -------------------------------------------------------------------------------- 1 | import Highway from '@dogstudio/highway' 2 | import * as quicklink from 'quicklink' 3 | import gsap from 'gsap' 4 | import app from './app' 5 | import raf from './lib/raf' 6 | import loadFonts from './lib/loadFonts' 7 | import { on, once, size, remove } from 'martha' 8 | import Fade from './transitions/Fade' 9 | 10 | class Base extends Highway.Renderer { 11 | onLoad() { 12 | quicklink.listen() 13 | on(window, 'resize', this.resize) 14 | on(document, 'mousemove', this.mousemove) 15 | raf(app) 16 | gsap.set('[data-router-view]', { autoAlpha: 1 }) 17 | loadFonts(app.getState().fonts) 18 | .then(this.onLoadCompleted) 19 | .catch(console.log) 20 | } 21 | 22 | onLoadCompleted = () => { 23 | this.mount() 24 | let { dom } = app.getState() 25 | once(dom.body, 'transitionend', this.onEnterCompleted) 26 | remove(dom.body, 'opacity-0') 27 | } 28 | 29 | onEnter() { 30 | this.mount() 31 | } 32 | 33 | onEnterCompleted() { 34 | app.emit('enter:completed') 35 | } 36 | 37 | onLeave() { 38 | this.unmount() 39 | } 40 | 41 | onLeaveCompleted() {} 42 | 43 | mount = () => { 44 | app.mount() 45 | this.resize() 46 | } 47 | 48 | unmount = () => { 49 | app.unmount() 50 | } 51 | 52 | resize = () => { 53 | app.emit('resize', size()) 54 | } 55 | 56 | mousemove = ({ clientX: mx, clientY: my }) => { 57 | app.emit('mousemove', { mx, my }) 58 | } 59 | 60 | setup() { 61 | this.onLoad() 62 | } 63 | } 64 | 65 | const H = new Highway.Core({ 66 | renderers: { 67 | default: Base, 68 | }, 69 | transitions: { 70 | default: Fade, 71 | contextual: {}, 72 | }, 73 | }) 74 | -------------------------------------------------------------------------------- /scripts/lib/allToArray.js: -------------------------------------------------------------------------------- 1 | export default function allToArray(obj) { 2 | return Object.entries(obj).reduce((acc, [key, val]) => { 3 | acc[key] = [val].flat() 4 | return acc 5 | }, {}) 6 | } 7 | -------------------------------------------------------------------------------- /scripts/lib/html.js: -------------------------------------------------------------------------------- 1 | // a noop tagged template function to enable html syntax highlighting 2 | export default function html(b) { 3 | for (var c = b[0], a = 1, d = arguments.length; a < d; a++) 4 | c += arguments[a] + b[a] 5 | return c 6 | } 7 | -------------------------------------------------------------------------------- /scripts/lib/inview.js: -------------------------------------------------------------------------------- 1 | import { rect, clamp } from 'martha' 2 | 3 | function scrollPercentage(el, wh) { 4 | const bounds = rect(el) 5 | return 1 - clamp(bounds.bottom / (wh + bounds.height), 0, 1) 6 | } 7 | 8 | export default function inview(el, wh) { 9 | const percent = scrollPercentage(el, wh) 10 | return percent > 0 && percent < 1 11 | } 12 | -------------------------------------------------------------------------------- /scripts/lib/loadFonts.js: -------------------------------------------------------------------------------- 1 | const FontFaceObserver = require('fontfaceobserver') 2 | 3 | export default function loadFonts(fontManifest) { 4 | return new Promise((resolve, reject) => { 5 | const observers = fontManifest.map( 6 | (entry) => new FontFaceObserver(entry.family, entry.options), 7 | ) 8 | 9 | Promise.all(observers.map((font) => font.load())) 10 | .then((res) => { 11 | if (process.env.NODE_ENV !== 'production') { 12 | console.group('FontFaceObserver') 13 | res.forEach((font) => 14 | console.log(`Loaded ${font.family} (${font.style})`), 15 | ) 16 | console.groupEnd() 17 | } 18 | resolve(res) 19 | }) 20 | .catch(reject) 21 | }) 22 | } 23 | -------------------------------------------------------------------------------- /scripts/lib/poll.js: -------------------------------------------------------------------------------- 1 | export default function poll(delay, cb, first = true) { 2 | let timeoutId 3 | first ? cb(done) : done() 4 | function done() { 5 | timeoutId = setTimeout(() => cb(done), delay) 6 | } 7 | return () => clearTimeout(timeoutId) 8 | } 9 | -------------------------------------------------------------------------------- /scripts/lib/raf.js: -------------------------------------------------------------------------------- 1 | import gsap from 'gsap' 2 | import ScrollToPlugin from 'gsap/ScrollToPlugin' 3 | import { rect, qs, on, round, lerp } from 'martha' 4 | 5 | gsap.registerPlugin(ScrollToPlugin) 6 | 7 | export default function raf(app) { 8 | let target = 0 9 | let current = 0 10 | let ease = 0.15 11 | 12 | gsap.ticker.fps(-1) 13 | gsap.ticker.add(tick) 14 | 15 | on(window, 'scroll', scroll) 16 | app.on('scroll:to', scrollTo) 17 | app.on('scroll:reset', reset) 18 | app.on('resize:reset', resize) 19 | 20 | function tick() { 21 | current = 22 | app.getState().ww >= 768 23 | ? round(lerp(current, target, ease), 100) 24 | : target 25 | app.emit('tick', { scroll: current }) 26 | } 27 | 28 | function scroll() { 29 | target = window.scrollY 30 | } 31 | 32 | function scrollTo(_, target) { 33 | const top = target.offsetTop 34 | const offset = top === 0 ? target.parentNode.offsetTop : top 35 | const padding = rect(qs('[data-scroll-padding-top]'))?.bottom ?? 0 36 | 37 | gsap.to(window, { 38 | scrollTo: offset - padding, 39 | duration: 0.5, 40 | ease: 'expo.inOut', 41 | }) 42 | } 43 | 44 | function reset() { 45 | target = current = 0 46 | } 47 | 48 | function resize() { 49 | current = target 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /scripts/lib/signal.js: -------------------------------------------------------------------------------- 1 | export default function signal(initial, effect) { 2 | let current = initial 3 | return [ 4 | () => current, 5 | (x) => { 6 | current = x 7 | effect(current) 8 | }, 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /scripts/transitions/Fade.js: -------------------------------------------------------------------------------- 1 | import Highway from '@dogstudio/highway' 2 | import gsap from 'gsap' 3 | 4 | class Fade extends Highway.Transition { 5 | in({ from, to, done }) { 6 | window.scrollTo(0, 0) 7 | from.remove() 8 | gsap.to(to, { 9 | duration: 0.5, 10 | autoAlpha: 1, 11 | onComplete: done, 12 | }) 13 | } 14 | 15 | out({ from, done }) { 16 | gsap.to(from, { 17 | duration: 0.5, 18 | autoAlpha: 0, 19 | onComplete: done, 20 | }) 21 | } 22 | } 23 | 24 | export default Fade 25 | -------------------------------------------------------------------------------- /studio/config/.checksums: -------------------------------------------------------------------------------- 1 | { 2 | "#": "Used by Sanity to keep track of configuration file checksums, do not delete or modify!", 3 | "@sanity/default-layout": "bb034f391ba508a6ca8cd971967cbedeb131c4d19b17b28a0895f32db5d568ea", 4 | "@sanity/default-login": "6fb6d3800aa71346e1b84d95bbcaa287879456f2922372bb0294e30b968cd37f", 5 | "@sanity/form-builder": "b38478227ba5e22c91981da4b53436df22e48ff25238a55a973ed620be5068aa", 6 | "@sanity/data-aspects": "d199e2c199b3e26cd28b68dc84d7fc01c9186bf5089580f2e2446994d36b3cb6" 7 | } 8 | -------------------------------------------------------------------------------- /studio/config/@sanity/data-aspects.json: -------------------------------------------------------------------------------- 1 | { 2 | "listOptions": {} 3 | } 4 | -------------------------------------------------------------------------------- /studio/config/@sanity/default-layout.json: -------------------------------------------------------------------------------- 1 | { 2 | "toolSwitcher": { 3 | "order": [], 4 | "hidden": [] 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /studio/config/@sanity/default-login.json: -------------------------------------------------------------------------------- 1 | { 2 | "providers": { 3 | "mode": "append", 4 | "redirectOnSingle": false, 5 | "entries": [] 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /studio/config/@sanity/form-builder.json: -------------------------------------------------------------------------------- 1 | { 2 | "images": { 3 | "directUploads": true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /studio/index.js: -------------------------------------------------------------------------------- 1 | import createSchema from 'part:@sanity/base/schema-creator' 2 | import schemaTypes from 'all:part:@sanity/base/schema-type' 3 | 4 | import config from './types/config' 5 | 6 | import seo from './types/seo' 7 | import asset from './types/asset' 8 | 9 | const documents = [config] 10 | const objects = [seo, asset] 11 | 12 | export default createSchema({ 13 | name: 'default', 14 | types: schemaTypes.concat([...documents, ...objects]), 15 | }) 16 | -------------------------------------------------------------------------------- /studio/lib/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/selfawarestudio/sane-eleventy/81e83e42b884af3e9e421c2b2d5e484bbdfad878/studio/lib/.gitkeep -------------------------------------------------------------------------------- /studio/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "studio", 3 | "private": true, 4 | "version": "1.0.0", 5 | "main": "package.json", 6 | "author": "Mike Wagz ", 7 | "license": "NONE", 8 | "scripts": { 9 | "start": "sanity start", 10 | "test": "sanity check", 11 | "deploy": "sanity deploy" 12 | }, 13 | "dependencies": { 14 | "@sanity/base": "^1.149.8", 15 | "@sanity/cli": "^2.0.1", 16 | "@sanity/components": "^1.149.8", 17 | "@sanity/core": "^1.149.9", 18 | "@sanity/dashboard": "^1.149.8", 19 | "@sanity/default-layout": "^1.149.8", 20 | "@sanity/default-login": "^1.149.7", 21 | "@sanity/desk-tool": "^1.149.8", 22 | "@sanity/vision": "^1.149.0", 23 | "prop-types": "^15.6", 24 | "react": "^16.2", 25 | "react-dom": "^16.2", 26 | "react-emoji-render": "^1.2.1", 27 | "sanity-plugin-dashboard-widget-netlify": "^1.0.1" 28 | }, 29 | "devDependencies": {} 30 | } 31 | -------------------------------------------------------------------------------- /studio/parts/dashboard.js: -------------------------------------------------------------------------------- 1 | export default { 2 | widgets: [ 3 | { 4 | name: 'netlify', 5 | options: { 6 | title: 'Netlify', 7 | sites: [ 8 | { 9 | title: '', 10 | apiId: '', 11 | buildHookId: '', 12 | name: '', 13 | }, 14 | ], 15 | }, 16 | }, 17 | ], 18 | } 19 | -------------------------------------------------------------------------------- /studio/parts/structure.js: -------------------------------------------------------------------------------- 1 | import S from '@sanity/desk-tool/structure-builder' 2 | import React from 'react' 3 | import Emoji from 'react-emoji-render' 4 | 5 | export default () => 6 | S.list() 7 | .title('Content') 8 | .items([ 9 | S.listItem() 10 | .title('Config') 11 | .icon(() => ) 12 | .child( 13 | S.editor() 14 | .title('Config') 15 | .schemaType('config') 16 | .documentId('config'), 17 | ), 18 | ]) 19 | -------------------------------------------------------------------------------- /studio/sanity.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "project": { 4 | "name": "sane-eleventy" 5 | }, 6 | "api": { 7 | "projectId": "", 8 | "dataset": "production" 9 | }, 10 | "plugins": [ 11 | "@sanity/base", 12 | "@sanity/components", 13 | "@sanity/default-layout", 14 | "@sanity/default-login", 15 | "@sanity/desk-tool", 16 | "@sanity/dashboard", 17 | "dashboard-widget-netlify" 18 | ], 19 | "env": { 20 | "development": { 21 | "plugins": ["@sanity/vision"] 22 | } 23 | }, 24 | "parts": [ 25 | { 26 | "name": "part:@sanity/base/schema", 27 | "path": "./index.js" 28 | }, 29 | { 30 | "name": "part:@sanity/desk-tool/structure", 31 | "path": "./parts/structure.js" 32 | }, 33 | { 34 | "implements": "part:@sanity/dashboard/config", 35 | "path": "./parts/dashboard.js" 36 | } 37 | ] 38 | } 39 | -------------------------------------------------------------------------------- /studio/static/.gitkeep: -------------------------------------------------------------------------------- 1 | Files placed here will be served by the Sanity server under the `/static`-prefix 2 | -------------------------------------------------------------------------------- /studio/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/selfawarestudio/sane-eleventy/81e83e42b884af3e9e421c2b2d5e484bbdfad878/studio/static/favicon.ico -------------------------------------------------------------------------------- /studio/types/asset.js: -------------------------------------------------------------------------------- 1 | export default { 2 | title: 'Image', 3 | name: 'asset', 4 | type: 'object', 5 | fields: [ 6 | { 7 | title: 'Image', 8 | name: 'image', 9 | type: 'image', 10 | }, 11 | { 12 | title: 'Alt Text', 13 | name: 'alt', 14 | type: 'string', 15 | description: 16 | 'A short description of the image that is important for accessibility and SEO', 17 | validation: (Rule) => Rule.required(), 18 | }, 19 | ], 20 | } 21 | -------------------------------------------------------------------------------- /studio/types/config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | title: 'Config', 3 | name: 'config', 4 | type: 'document', 5 | fields: [ 6 | { 7 | title: 'SEO Metadata', 8 | name: 'seo', 9 | type: 'seo', 10 | }, 11 | ], 12 | } 13 | -------------------------------------------------------------------------------- /studio/types/seo.js: -------------------------------------------------------------------------------- 1 | export default { 2 | title: 'SEO Metadata', 3 | name: 'seo', 4 | type: 'object', 5 | fields: [ 6 | { 7 | title: 'Title', 8 | name: 'title', 9 | type: 'string', 10 | }, 11 | { 12 | title: 'Description', 13 | name: 'description', 14 | type: 'string', 15 | }, 16 | { 17 | title: 'URL', 18 | name: 'url', 19 | type: 'url', 20 | }, 21 | { 22 | title: 'Image', 23 | name: 'image', 24 | type: 'asset', 25 | }, 26 | ], 27 | } 28 | -------------------------------------------------------------------------------- /styles/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | /* @font-face { 7 | font-family: 'GT Walsheim'; 8 | src: url('/fonts/GTWalsheim-Regular.woff') format('woff'), 9 | url('/fonts/GTWalsheim-Regular.woff2') format('woff2'); 10 | font-weight: 400; 11 | font-style: normal; 12 | font-display: swap; 13 | } */ 14 | 15 | * { 16 | -webkit-font-smoothing: antialiased; 17 | -moz-osx-font-smoothing: grayscale; 18 | -webkit-tap-highlight-color: rgba(0, 0, 0, 0); 19 | } 20 | 21 | html, 22 | body { 23 | overscroll-behavior: none; 24 | } 25 | 26 | html { 27 | font-size: 10px; 28 | } 29 | 30 | body { 31 | font-size: 1.6rem; 32 | overflow-y: scroll; 33 | overflow-x: hidden; 34 | } 35 | 36 | [data-router-view] { 37 | opacity: 0; 38 | visibility: hidden; 39 | } 40 | } 41 | 42 | @layer utilities { 43 | .no-scrollbar { 44 | /* Hide scrollbar for Chrome, Safari and Opera */ 45 | &::-webkit-scrollbar { 46 | display: none; 47 | } 48 | 49 | /* Hide scrollbar for IE, Edge and Firefox */ 50 | -ms-overflow-style: none; /* IE and Edge */ 51 | scrollbar-width: none; /* Firefox */ 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | mode: 'jit', 3 | purge: { 4 | mode: 'all', 5 | preserveHtmlElements: true, 6 | content: [ 7 | './templates/**/*.njk', 8 | './scripts/**/*.js', 9 | './.eleventy.js', 10 | './lib/serializers.js', 11 | ], 12 | }, 13 | darkMode: false, 14 | theme: { 15 | fontFamily: { 16 | sans: 'sans-serif', 17 | serif: 'serif', 18 | mono: 'monospace', 19 | }, 20 | screens: { 21 | '2xs': '375px', 22 | xs: '400px', 23 | s: '650px', 24 | m: '768px', 25 | l: '1024px', 26 | xl: '1280px', 27 | '2xl': '1536px', 28 | }, 29 | colors: { 30 | white: '#FFF', 31 | black: '#000', 32 | current: 'currentColor', 33 | transparent: 'transparent', 34 | }, 35 | fontSize: { 36 | ...new Array(201) 37 | .fill() 38 | .map((_, i) => i) 39 | .reduce((acc, val) => { 40 | acc[val] = `${val / 10}rem` 41 | return acc 42 | }, {}), 43 | }, 44 | lineHeight: { 45 | ...new Array(61) 46 | .fill() 47 | .map((_, i) => i) 48 | .reduce((acc, val) => { 49 | acc[100 + val] = (100 + val) / 100 50 | return acc 51 | }, {}), 52 | }, 53 | spacing: { 54 | ...new Array(51) 55 | .fill() 56 | .map((_, i) => i) 57 | .reduce((acc, val) => { 58 | acc[val] = `${val / 10}rem` 59 | return acc 60 | }, {}), 61 | ...new Array(50) 62 | .fill() 63 | .map((_, i) => (11 + i) * 5) 64 | .reduce((acc, val) => { 65 | acc[val] = `${val / 10}rem` 66 | return acc 67 | }, {}), 68 | }, 69 | opacity: { 70 | ...new Array(21) 71 | .fill() 72 | .map((_, i) => i * 5) 73 | .reduce((acc, val) => { 74 | acc[val] = val / 100 75 | return acc 76 | }, {}), 77 | }, 78 | zIndex: { 79 | ...new Array(11) 80 | .fill() 81 | .map((_, i) => i) 82 | .reduce((acc, val) => { 83 | acc[val] = val 84 | return acc 85 | }, {}), 86 | }, 87 | transitionTimingFunction: { 88 | DEFAULT: 'cubic-bezier(0.23, 1, 0.32, 1)', 89 | 'in-quad': 'cubic-bezier(0.55, 0.085, 0.68, 0.53)', 90 | 'in-cubic': 'cubic-bezier(0.55, 0.055, 0.675, 0.19)', 91 | 'in-quart': 'cubic-bezier(0.895, 0.03, 0.685, 0.22)', 92 | 'in-quint': 'cubic-bezier(0.755, 0.05, 0.855, 0.06)', 93 | 'in-sine': 'cubic-bezier(0.47, 0, 0.745, 0.715)', 94 | 'in-expo': 'cubic-bezier(0.95, 0.05, 0.795, 0.035)', 95 | 'in-circ': 'cubic-bezier(0.6, 0.04, 0.98, 0.335)', 96 | 'in-back': 'cubic-bezier(0.6, -0.28, 0.735, 0.045)', 97 | 'out-quad': 'cubic-bezier(0.25, 0.46, 0.45, 0.94)', 98 | 'out-cubic': 'cubic-bezier(0.215, 0.61, 0.355, 1)', 99 | 'out-quart': 'cubic-bezier(0.165, 0.84, 0.44, 1)', 100 | 'out-quint': 'cubic-bezier(0.23, 1, 0.32, 1)', 101 | 'out-sine': 'cubic-bezier(0.39, 0.575, 0.565, 1)', 102 | 'out-expo': 'cubic-bezier(0.19, 1, 0.22, 1)', 103 | 'out-circ': 'cubic-bezier(0.075, 0.82, 0.165, 1)', 104 | 'out-back': 'cubic-bezier(0.175, 0.885, 0.32, 1.275)', 105 | 'in-out-quad': 'cubic-bezier(0.455, 0.03, 0.515, 0.955)', 106 | 'in-out-cubic': 'cubic-bezier(0.645, 0.045, 0.355, 1)', 107 | 'in-out-quart': 'cubic-bezier(0.77, 0, 0.175, 1)', 108 | 'in-out-quint': 'cubic-bezier(0.86, 0, 0.07, 1)', 109 | 'in-out-sine': 'cubic-bezier(0.445, 0.05, 0.55, 0.95)', 110 | 'in-out-expo': 'cubic-bezier(1, 0, 0, 1)', 111 | 'in-out-circ': 'cubic-bezier(0.785, 0.135, 0.15, 0.86)', 112 | 'in-out-back': 'cubic-bezier(0.68, -0.55, 0.265, 1.55)', 113 | }, 114 | stroke: (theme) => ({ 115 | white: theme('colors.white'), 116 | black: theme('colors.black'), 117 | current: theme('colors.current'), 118 | transparent: theme('colors.transparent'), 119 | }), 120 | fill: (theme) => ({ 121 | white: theme('colors.white'), 122 | black: theme('colors.black'), 123 | current: theme('colors.current'), 124 | transparent: theme('colors.transparent'), 125 | }), 126 | extend: {}, 127 | }, 128 | variants: { 129 | extend: {}, 130 | }, 131 | plugins: [], 132 | } 133 | -------------------------------------------------------------------------------- /templates/about.njk: -------------------------------------------------------------------------------- 1 | --- 2 | layout: base.njk 3 | title: About 4 | --- 5 |
6 |

About

7 |
-------------------------------------------------------------------------------- /templates/includes/head.njk: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Sane Eleventy 6 | {# {{ config.seo.title }} #} 7 | 8 | {# Preload styles #} 9 | 10 | 11 | {# Preload fonts #} 12 | {# #} 13 | 14 | {# Preload scripts #} 15 | 16 | 17 | {# Facebook Open Graph #} 18 | {# 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | #} 29 | 30 | {# Twitter Card #} 31 | {# 32 | 33 | 34 | 35 | 36 | #} 37 | 38 | {# Favicons #} 39 | 40 | 41 | {# Link stylesheet #} 42 | -------------------------------------------------------------------------------- /templates/includes/header.njk: -------------------------------------------------------------------------------- 1 |
2 | Sane Eleventy 3 | 7 |
-------------------------------------------------------------------------------- /templates/index.njk: -------------------------------------------------------------------------------- 1 | --- 2 | layout: base.njk 3 | title: Home 4 | --- 5 |
6 |

Home

7 |
-------------------------------------------------------------------------------- /templates/layouts/base.njk: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {% include 'head.njk' %} 5 | 6 | 7 |
8 | {% include 'header.njk' %} 9 |
10 | {{ content | safe }} 11 |
12 | {#
#} 13 |
14 | 15 | 16 | -------------------------------------------------------------------------------- /templates/tasks/esbuild.11ty.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const esbuild = require('esbuild') 3 | 4 | module.exports = class { 5 | async data() { 6 | return { 7 | permalink: false, 8 | } 9 | } 10 | 11 | async render() { 12 | await esbuild.build({ 13 | entryPoints: [path.join(__dirname, '..', '..', 'scripts', 'index.js')], 14 | minify: true, 15 | bundle: true, 16 | sourcemap: true, 17 | outfile: path.join(__dirname, '..', '..', 'build', 'app.js'), 18 | }) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /templates/tasks/postcss.11ty.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const fs = require('fs') 3 | const postcss = require('postcss') 4 | const loadConfig = require('postcss-load-config') 5 | 6 | module.exports = class { 7 | async data() { 8 | const rawPath = path.join(__dirname, '..', '..', 'styles', 'index.css') 9 | 10 | return { 11 | permalink: 'app.css', 12 | rawPath, 13 | rawCss: fs.readFileSync(rawPath), 14 | } 15 | } 16 | 17 | async render({ rawCss, rawPath }) { 18 | return await loadConfig().then(({ plugins }) => 19 | postcss(plugins) 20 | .process(rawCss, { from: rawPath }) 21 | .then((result) => result.css), 22 | ) 23 | } 24 | } 25 | --------------------------------------------------------------------------------