├── .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  
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 |
--------------------------------------------------------------------------------