├── .editorconfig ├── .eleventyignore ├── .gitignore ├── README.md ├── lerna.json ├── now.json ├── package.json ├── studio ├── README.md ├── config │ ├── .checksums │ └── @sanity │ │ ├── data-aspects.json │ │ ├── default-layout.json │ │ └── default-login.json ├── deskStructure.js ├── netlify.toml ├── package.json ├── plugins │ └── .gitkeep ├── sanity.json ├── schemas │ ├── documents │ │ ├── author.js │ │ ├── category.js │ │ ├── post.js │ │ └── siteSettings.js │ ├── objects │ │ ├── authorReference.js │ │ ├── bioPortableText.js │ │ ├── bodyPortableText.js │ │ ├── excerptPortableText.js │ │ └── mainImage.js │ └── schema.js ├── static │ ├── .gitkeep │ └── favicon.ico └── yarn.lock └── web ├── .eleventy.js ├── _data ├── authors.js ├── metadata.js └── posts.js ├── _includes ├── layouts │ ├── author.njk │ ├── base.njk │ ├── home.njk │ └── post.njk └── postslist.njk ├── archive.njk ├── author.njk ├── index.njk ├── package.json ├── post.njk └── utils ├── getAuthors.js ├── getPosts.js ├── sanityClient.js └── serializers.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | insert_final_newline = true 8 | trim_trailing_whitespace = true 9 | charset = utf-8 10 | -------------------------------------------------------------------------------- /.eleventyignore: -------------------------------------------------------------------------------- 1 | README.md 2 | _11ty/ 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | _site/ 2 | node_modules/ 3 | package-lock.json 4 | posts/*.md 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Eleventy and Sanity Blog Boilerplate 2 | 3 | Minimal blog with [Eleventy](https://11ty.io) and [Sanity](https://www.sanity.io). 4 | 5 | This is a monorepo with a pre-configured Sanity Studio (`/studio`) and a very basic setup of Eleventy (`/web`). 6 | 7 | - [Quick start](#quick-start) 8 | - [Deploy on Netlify](#deploy-on-netlify) 9 | - [Studio](#studio) 10 | - [Web](#web) 11 | - [Deploy on `now`](#deploy-on-now) 12 | - [CORS-settings for the Studio](#cors-settings-for-the-studio) 13 | 14 | ## Quick start 15 | 16 | 1. `npm install` in the project root folder on local 17 | 2. `npm run sanity-init` to reconfigure the studio with a new or existing project 18 | 3. `npm run dev` to start the Studio and 11ty in watch mode 19 | - Sanity Studio runs on [localhost:3333](http://localhost:3333) 20 | - 11ty outputs the static files in `_site` 21 | 4. `npm run build` to build to production locally 22 | 23 | ## Deploy on Netlify 24 | 25 | You can host both the studio and the 11ty blog on [Netlify](https://netlify.com) as two apps. Log in to your Netlify account and add them as two separate apps with the following settings: 26 | 27 | ### Studio 28 | 29 | - **Repository**: `` 30 | - **Base directory**: `studio` 31 | - **Build command**: `npm run build && cp ./netlify.toml dist` 32 | - **Publish directory**: `studio/dist` 33 | 34 | You have to add [CORS-settings](#cors-settings-for-the-studio) for the studio deployed on Netlify. 35 | 36 | ### Web 37 | 38 | - **Repository**: `` 39 | - **Base directory**: `web` 40 | - **Build command**: `npm run build-web` 41 | - **Publish directory**: `web/_site` 42 | 43 | ## Deploy on `now` 44 | 45 | The `now.json` has configuration for deploying both the frontend and the studio on _one_ now deployment. The web frontend can be browsed from the root of your now domain. The Studio can be accessed on `https://.now.sh/studio`. 46 | 47 | 1. Add a `"basePath": "/studio"` to `sanity.json`: 48 | 49 | ```json 50 | "project": { 51 | "name": "sanity-tutorial-blog", 52 | "basePath": "/studio" 53 | }, 54 | ``` 55 | 2. You have to add CORS-settings for the studio deployed on `now`. 56 | 57 | ## CORS-settings for the Studio 58 | 59 | Go to your projects API-settings on [manage.sanity.io](https://manage.sanity.io) => Settings => API => CORS origins => Click "Add" => Add domain for the now deployment + Allow credentials. 60 | 61 | or 62 | 63 | ```text 64 | > cd studio 65 | > sanity cors add https://.now.sh` 66 | ``` 67 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": [ 3 | "studio", 4 | "web" 5 | ], 6 | "version": "0.0.0" 7 | } 8 | -------------------------------------------------------------------------------- /now.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 2, 3 | "name": "eleventy-sanity-blog", 4 | "builds": [ 5 | { "src": "web/package.json", "use": "@now/static-build", "config": { "distDir": "_site" } }, 6 | { "src": "studio/package.json", "use": "@now/static-build" } 7 | ], 8 | "routes": [ 9 | { "src": "^/static/favicon.ico", "dest": "/studio/static/favicon.ico" }, 10 | { "src": "^/studio/static/(.*)", "dest": "/studio/static/$1" }, 11 | { "src": "^/studio/(.*)", "dest": "/studio/index.html" }, 12 | { "src": "^/(.*)", "dest": "/web/$1" } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "11ty-sanity-experiment", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "build": "lerna run build --parallel", 8 | "dev": "lerna run dev --parallel", 9 | "format": "lerna run format", 10 | "sanity-init": "(cd studio && npm run init)", 11 | "build-studio": "(cd studio && npm run build)", 12 | "build-web": "(cd web && npm run build)", 13 | "postinstall": "lerna bootstrap", 14 | "test": "echo \"Error: no test specified\" && exit 1" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "git+https://github.com/kmelve/11ty-sanity-experiment.git" 19 | }, 20 | "keywords": [], 21 | "author": "", 22 | "license": "ISC", 23 | "bugs": { 24 | "url": "https://github.com/kmelve/11ty-sanity-experiment/issues" 25 | }, 26 | "homepage": "https://github.com/kmelve/11ty-sanity-experiment#readme", 27 | "devDependencies": { 28 | "lerna": "^3.13.4" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /studio/README.md: -------------------------------------------------------------------------------- 1 | # Sanity Blogging Content Studio 2 | 3 | Congratulations, you have now installed the Sanity Content Studio, an open source real-time content editing environment connected to the Sanity backend. 4 | 5 | Now you can do the following things: 6 | 7 | - [Read “getting started” in the docs](https://www.sanity.io/docs/introduction/getting-started?utm_source=readme) 8 | - Check out the example frontend: [React/Next.js](https://github.com/sanity-io/tutorial-sanity-blog-react-next) 9 | - [Read the blog post about this template](https://www.sanity.io/blog/build-your-own-blog-with-sanity-and-next-js?utm_source=readme) 10 | - [Join the community Slack](https://slack.sanity.io/?utm_source=readme) 11 | - [Extend and build plugins](https://www.sanity.io/docs/content-studio/extending?utm_source=readme) 12 | -------------------------------------------------------------------------------- /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/data-aspects": "d199e2c199b3e26cd28b68dc84d7fc01c9186bf5089580f2e2446994d36b3cb6" 6 | } 7 | -------------------------------------------------------------------------------- /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/deskStructure.js: -------------------------------------------------------------------------------- 1 | import S from '@sanity/desk-tool/structure-builder' 2 | import MdSettings from 'react-icons/lib/md/settings' 3 | import MdPerson from 'react-icons/lib/md/person' 4 | 5 | const hiddenDocTypes = listItem => 6 | !['category', 'author', 'post', 'siteSettings'].includes(listItem.getId()) 7 | 8 | export default () => 9 | S.list() 10 | .title('Content') 11 | .items([ 12 | S.listItem() 13 | .title('Settings') 14 | .icon(MdSettings) 15 | .child( 16 | S.editor() 17 | .id('siteSettings') 18 | .schemaType('siteSettings') 19 | .documentId('siteSettings') 20 | ), 21 | S.listItem() 22 | .title('Blog posts') 23 | .schemaType('post') 24 | .child(S.documentTypeList('post').title('Blog posts')), 25 | S.listItem() 26 | .title('Authors') 27 | .icon(MdPerson) 28 | .schemaType('author') 29 | .child(S.documentTypeList('author').title('Authors')), 30 | S.listItem() 31 | .title('Categories') 32 | .schemaType('category') 33 | .child(S.documentTypeList('category').title('Categories')), 34 | // This returns an array of all the document types 35 | // defined in schema.js. We filter out those that we have 36 | // defined the structure above 37 | ...S.documentTypeListItems().filter(hiddenDocTypes) 38 | ]) 39 | -------------------------------------------------------------------------------- /studio/netlify.toml: -------------------------------------------------------------------------------- 1 | [[redirects]] 2 | from = "/*" 3 | to = "/" 4 | status = 200 5 | -------------------------------------------------------------------------------- /studio/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sanitytutorialblog", 3 | "private": true, 4 | "version": "1.0.0", 5 | "description": "", 6 | "main": "package.json", 7 | "author": "Knut Melvær ", 8 | "license": "UNLICENSED", 9 | "scripts": { 10 | "init": "sanity init --reconfigure", 11 | "dev": "sanity start", 12 | "format": "prettier-eslint --write \"**/*.js\" \"!node_modules/**\"", 13 | "build": "sanity build", 14 | "now-build": "sanity build", 15 | "graphql-deploy": "sanity graphql deploy --playground", 16 | "lint": "eslint .", 17 | "test": "sanity check" 18 | }, 19 | "keywords": [ 20 | "sanity" 21 | ], 22 | "dependencies": { 23 | "@sanity/base": "^0.140.17", 24 | "@sanity/cli": "^0.140.17", 25 | "@sanity/code-input": "^0.140.12", 26 | "@sanity/components": "^0.140.20", 27 | "@sanity/core": "^0.140.20", 28 | "@sanity/default-layout": "^0.140.20", 29 | "@sanity/default-login": "^0.140.15", 30 | "@sanity/desk-tool": "^0.140.20", 31 | "prop-types": "^15.7.2", 32 | "react": "^16.8.6", 33 | "react-dom": "^16.8.6" 34 | }, 35 | "devDependencies": { 36 | "babel-eslint": "^10.0.1", 37 | "eslint": "^5.16.0", 38 | "eslint-config-standard": "^12.0.0", 39 | "eslint-config-standard-react": "^7.0.2", 40 | "eslint-plugin-import": "^2.17.2", 41 | "eslint-plugin-node": "^9.0.1", 42 | "eslint-plugin-promise": "^4.1.1", 43 | "eslint-plugin-react": "^7.13.0", 44 | "eslint-plugin-standard": "^4.0.0", 45 | "prettier-eslint-cli": "^4.7.1" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /studio/plugins/.gitkeep: -------------------------------------------------------------------------------- 1 | User-specific packages can be placed here 2 | -------------------------------------------------------------------------------- /studio/sanity.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "project": { 4 | "name": "sanity-tutorial-blog" 5 | }, 6 | "api": { 7 | "projectId": "anokeucs", 8 | "dataset": "eleventy" 9 | }, 10 | "plugins": [ 11 | "@sanity/base", 12 | "@sanity/components", 13 | "@sanity/default-layout", 14 | "@sanity/default-login", 15 | "@sanity/desk-tool", 16 | "@sanity/code-input" 17 | ], 18 | "parts": [ 19 | { 20 | "name": "part:@sanity/base/schema", 21 | "path": "./schemas/schema.js" 22 | }, 23 | { 24 | "name": "part:@sanity/desk-tool/structure", 25 | "path": "./deskStructure.js" 26 | } 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /studio/schemas/documents/author.js: -------------------------------------------------------------------------------- 1 | export default { 2 | type: 'document', 3 | name: 'author', 4 | title: 'Author', 5 | fields: [ 6 | { 7 | name: 'name', 8 | title: 'Name', 9 | type: 'string' 10 | }, 11 | { 12 | name: 'slug', 13 | title: 'Slug', 14 | type: 'slug', 15 | description: 'Some frontends will require a slug to be set to be able to show the person', 16 | options: { 17 | source: 'name', 18 | maxLength: 96 19 | } 20 | }, 21 | { 22 | name: 'image', 23 | title: 'Image', 24 | type: 'mainImage' 25 | }, 26 | { 27 | name: 'bio', 28 | type: 'bioPortableText', 29 | title: 'Biography' 30 | } 31 | ], 32 | preview: { 33 | select: { 34 | title: 'name', 35 | subtitle: 'slug.current', 36 | media: 'image' 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /studio/schemas/documents/category.js: -------------------------------------------------------------------------------- 1 | export default { 2 | type: 'document', 3 | name: 'category', 4 | title: 'Category', 5 | fields: [ 6 | { 7 | name: 'title', 8 | title: 'Title', 9 | type: 'string' 10 | }, 11 | { 12 | name: 'description', 13 | title: 'Description', 14 | type: 'text' 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /studio/schemas/documents/post.js: -------------------------------------------------------------------------------- 1 | import {format} from 'date-fns' 2 | 3 | export default { 4 | type: 'document', 5 | name: 'post', 6 | title: 'Blog Post', 7 | fields: [ 8 | { 9 | name: 'title', 10 | type: 'string', 11 | title: 'Title', 12 | description: 'Titles should be catchy, descriptive, and not too long' 13 | }, 14 | { 15 | name: 'slug', 16 | type: 'slug', 17 | title: 'Slug', 18 | description: 'Some frontends will require a slug to be set to be able to show the post', 19 | options: { 20 | source: 'title', 21 | maxLength: 96 22 | } 23 | }, 24 | { 25 | name: 'publishedAt', 26 | type: 'datetime', 27 | title: 'Published at', 28 | description: 'This can be used to schedule post for publishing' 29 | }, 30 | { 31 | name: 'mainImage', 32 | title: 'Main image', 33 | type: 'mainImage' 34 | }, 35 | { 36 | name: 'excerpt', 37 | type: 'excerptPortableText', 38 | title: 'Excerpt', 39 | description: 40 | 'This ends up on summary pages, on Google, when people share your post in social media.' 41 | }, 42 | { 43 | name: 'authors', 44 | title: 'Authors', 45 | type: 'array', 46 | of: [ 47 | { 48 | type: 'authorReference' 49 | } 50 | ] 51 | }, 52 | { 53 | name: 'categories', 54 | title: 'Categories', 55 | type: 'array', 56 | of: [ 57 | { 58 | type: 'reference', 59 | to: { 60 | type: 'category' 61 | } 62 | } 63 | ] 64 | }, 65 | { 66 | name: 'body', 67 | type: 'bodyPortableText', 68 | title: 'Body' 69 | } 70 | ], 71 | orderings: [ 72 | { 73 | title: 'Publishing date new–>old', 74 | name: 'publishingDateAsc', 75 | by: [ 76 | { 77 | field: 'publishedAt', 78 | direction: 'asc' 79 | }, 80 | { 81 | field: 'title', 82 | direction: 'asc' 83 | } 84 | ] 85 | }, 86 | { 87 | title: 'Publishing date old->new', 88 | name: 'publishingDateDesc', 89 | by: [ 90 | { 91 | field: 'publishedAt', 92 | direction: 'desc' 93 | }, 94 | { 95 | field: 'title', 96 | direction: 'asc' 97 | } 98 | ] 99 | } 100 | ], 101 | preview: { 102 | select: { 103 | title: 'title', 104 | publishedAt: 'publishedAt', 105 | slug: 'slug', 106 | media: 'mainImage' 107 | }, 108 | prepare ({title = 'No title', publishedAt, slug, media}) { 109 | const dateSegment = format(publishedAt, 'YYYY/MM') 110 | const path = `/${dateSegment}/${slug.current}/` 111 | return { 112 | title, 113 | media, 114 | subtitle: publishedAt ? path : 'Missing publishing date' 115 | } 116 | } 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /studio/schemas/documents/siteSettings.js: -------------------------------------------------------------------------------- 1 | export default { 2 | name: 'siteSettings', 3 | type: 'document', 4 | title: 'Site Settings', 5 | fields: [ 6 | { 7 | name: 'title', 8 | type: 'string', 9 | title: 'Title' 10 | }, 11 | { 12 | name: 'description', 13 | type: 'text', 14 | title: 'Description', 15 | description: 'Describe your blog for search engines and social media.' 16 | }, 17 | { 18 | name: 'keywords', 19 | type: 'array', 20 | title: 'Keywords', 21 | description: 'Add keywords that describes your blog.', 22 | of: [{type: 'string'}], 23 | options: { 24 | layout: 'tags' 25 | } 26 | }, 27 | { 28 | name: 'author', 29 | type: 'reference', 30 | description: 'Publish an author and set a reference to them here.', 31 | title: 'Author', 32 | to: [{type: 'author'}] 33 | } 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /studio/schemas/objects/authorReference.js: -------------------------------------------------------------------------------- 1 | export default { 2 | name: 'authorReference', 3 | type: 'object', 4 | title: 'Author reference', 5 | fields: [ 6 | { 7 | type: 'reference', 8 | name: 'author', 9 | to: [ 10 | { 11 | type: 'author' 12 | } 13 | ] 14 | } 15 | ], 16 | preview: { 17 | select: { 18 | title: 'author.name', 19 | media: 'author.image.asset' 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /studio/schemas/objects/bioPortableText.js: -------------------------------------------------------------------------------- 1 | export default { 2 | name: 'bioPortableText', 3 | type: 'array', 4 | title: 'Excerpt', 5 | of: [ 6 | { 7 | title: 'Block', 8 | type: 'block', 9 | styles: [{title: 'Normal', value: 'normal'}], 10 | lists: [], 11 | marks: { 12 | decorators: [ 13 | {title: 'Strong', value: 'strong'}, 14 | {title: 'Emphasis', value: 'em'}, 15 | {title: 'Code', value: 'code'} 16 | ] 17 | } 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /studio/schemas/objects/bodyPortableText.js: -------------------------------------------------------------------------------- 1 | export default { 2 | name: 'bodyPortableText', 3 | type: 'array', 4 | title: 'Post body', 5 | of: [ 6 | { 7 | type: 'block', 8 | title: 'Block', 9 | // Styles let you set what your user can mark up blocks with. These 10 | // corrensponds with HTML tags, but you can set any title or value 11 | // you want and decide how you want to deal with it where you want to 12 | // use your content. 13 | styles: [ 14 | {title: 'Normal', value: 'normal'}, 15 | {title: 'H1', value: 'h1'}, 16 | {title: 'H2', value: 'h2'}, 17 | {title: 'H3', value: 'h3'}, 18 | {title: 'H4', value: 'h4'}, 19 | {title: 'Quote', value: 'blockquote'} 20 | ], 21 | lists: [{title: 'Bullet', value: 'bullet'}, {title: 'Numbered', value: 'number'}], 22 | // Marks let you mark up inline text in the block editor. 23 | marks: { 24 | // Decorators usually describe a single property – e.g. a typographic 25 | // preference or highlighting by editors. 26 | decorators: [{title: 'Strong', value: 'strong'}, {title: 'Emphasis', value: 'em'}], 27 | // Annotations can be any object structure – e.g. a link or a footnote. 28 | annotations: [ 29 | { 30 | name: 'link', 31 | type: 'object', 32 | title: 'URL', 33 | fields: [ 34 | { 35 | title: 'URL', 36 | name: 'href', 37 | type: 'url' 38 | } 39 | ] 40 | } 41 | ] 42 | }, 43 | of: [{type: 'authorReference'}] 44 | }, 45 | // You can add additional types here. Note that you can't use 46 | // primitive types such as 'string' and 'number' in the same array 47 | // as a block type. 48 | { 49 | type: 'image', 50 | options: {hotspot: true} 51 | }, 52 | { 53 | type: 'code' 54 | } 55 | ] 56 | } 57 | -------------------------------------------------------------------------------- /studio/schemas/objects/excerptPortableText.js: -------------------------------------------------------------------------------- 1 | export default { 2 | name: 'excerptPortableText', 3 | type: 'array', 4 | title: 'Excerpt', 5 | of: [ 6 | { 7 | title: 'Block', 8 | type: 'block', 9 | styles: [{title: 'Normal', value: 'normal'}], 10 | lists: [], 11 | marks: { 12 | decorators: [ 13 | {title: 'Strong', value: 'strong'}, 14 | {title: 'Emphasis', value: 'em'}, 15 | {title: 'Code', value: 'code'} 16 | ], 17 | annotations: [] 18 | } 19 | } 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /studio/schemas/objects/mainImage.js: -------------------------------------------------------------------------------- 1 | export default { 2 | name: 'mainImage', 3 | title: 'Image', 4 | type: 'image', 5 | options: { 6 | hotspot: true 7 | }, 8 | fields: [ 9 | { 10 | title: 'Caption', 11 | name: 'caption', 12 | type: 'string', 13 | options: { 14 | isHighlighted: true 15 | } 16 | }, 17 | { 18 | name: 'alt', 19 | type: 'string', 20 | title: 'Alternative text', 21 | description: 'Important for SEO and accessiblity.', 22 | options: { 23 | isHighlighted: true 24 | } 25 | } 26 | ], 27 | preview: { 28 | select: { 29 | imageUrl: 'asset.url', 30 | title: 'caption' 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /studio/schemas/schema.js: -------------------------------------------------------------------------------- 1 | // First, we must import the schema creator 2 | import createSchema from 'part:@sanity/base/schema-creator' 3 | 4 | // Then import schema types from any plugins that might expose them 5 | import schemaTypes from 'all:part:@sanity/base/schema-type' 6 | 7 | // document schemas 8 | import author from './documents/author' 9 | import category from './documents/category' 10 | import post from './documents/post' 11 | import siteSettings from './documents/siteSettings' 12 | 13 | // Object types 14 | import bodyPortableText from './objects/bodyPortableText' 15 | import bioPortableText from './objects/bioPortableText' 16 | import excerptPortableText from './objects/excerptPortableText' 17 | import mainImage from './objects/mainImage' 18 | import authorReference from './objects/authorReference' 19 | 20 | // Then we give our schema to the builder and provide the result to Sanity 21 | export default createSchema({ 22 | // We name our schema 23 | name: 'blog', 24 | // Then proceed to concatenate our our document type 25 | // to the ones provided by any plugins that are installed 26 | types: schemaTypes.concat([ 27 | // The following are document types which will appear 28 | // in the studio. 29 | siteSettings, 30 | post, 31 | category, 32 | author, 33 | mainImage, 34 | authorReference, 35 | bodyPortableText, 36 | bioPortableText, 37 | excerptPortableText 38 | 39 | // When added to this list, object types can be used as 40 | // { type: 'typename' } in other document schemas 41 | ]) 42 | }) 43 | -------------------------------------------------------------------------------- /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/kmelve/eleventy-sanity-blog-boilerplate/13c1c50b000392c8ca68da4407a9441df104dd4a/studio/static/favicon.ico -------------------------------------------------------------------------------- /web/.eleventy.js: -------------------------------------------------------------------------------- 1 | const { DateTime } = require("luxon"); 2 | const util = require('util') 3 | 4 | module.exports = function(eleventyConfig) { 5 | 6 | eleventyConfig.addFilter("debug", function(value) { 7 | return util.inspect(value, {compact: false}) 8 | }); 9 | 10 | eleventyConfig.addFilter("readableDate", dateObj => { 11 | return new Date(dateObj).toDateString() 12 | }); 13 | 14 | // https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#valid-date-string 15 | eleventyConfig.addFilter('htmlDateString', (dateObj) => { 16 | return DateTime.fromJSDate(dateObj, {zone: 'utc'}).toFormat('yyyy-LL-dd'); 17 | }); 18 | 19 | let markdownIt = require("markdown-it"); 20 | let markdownItAnchor = require("markdown-it-anchor"); 21 | let options = { 22 | html: true, 23 | breaks: true, 24 | linkify: true 25 | }; 26 | let opts = { 27 | permalink: true, 28 | permalinkClass: "direct-link", 29 | permalinkSymbol: "#" 30 | }; 31 | 32 | eleventyConfig.setLibrary("md", markdownIt(options) 33 | .use(markdownItAnchor, opts) 34 | ); 35 | 36 | eleventyConfig.addFilter("markdownify", function(value) { 37 | const md = new markdownIt(options) 38 | return md.render(value) 39 | }) 40 | return { 41 | templateFormats: [ 42 | "md", 43 | "njk", 44 | "html", 45 | "liquid" 46 | ], 47 | 48 | // If your site lives in a different subdirectory, change this. 49 | // Leading or trailing slashes are all normalized away, so don’t worry about it. 50 | // If you don’t have a subdirectory, use "" or "/" (they do the same thing) 51 | // This is only used for URLs (it does not affect your file structure) 52 | pathPrefix: "/", 53 | 54 | markdownTemplateEngine: "liquid", 55 | htmlTemplateEngine: "njk", 56 | dataTemplateEngine: "njk", 57 | passthroughFileCopy: true, 58 | dir: { 59 | input: ".", 60 | includes: "_includes", 61 | data: "_data", 62 | output: "_site" 63 | } 64 | }; 65 | } 66 | -------------------------------------------------------------------------------- /web/_data/authors.js: -------------------------------------------------------------------------------- 1 | const getAuthors = require("../utils/getAuthors"); 2 | 3 | module.exports = async function() { 4 | return await getAuthors() 5 | } 6 | -------------------------------------------------------------------------------- /web/_data/metadata.js: -------------------------------------------------------------------------------- 1 | const groq = require('groq') 2 | const client = require('../utils/sanityClient') 3 | module.exports = async function() { 4 | return await client.fetch(groq` 5 | *[_id == "siteSettings"]{ 6 | ..., 7 | author-> 8 | }[0] 9 | `) 10 | } 11 | -------------------------------------------------------------------------------- /web/_data/posts.js: -------------------------------------------------------------------------------- 1 | const getPosts = require("../utils/getPosts"); 2 | 3 | module.exports = async function() { 4 | return await getPosts() 5 | } 6 | -------------------------------------------------------------------------------- /web/_includes/layouts/author.njk: -------------------------------------------------------------------------------- 1 | --- 2 | layout: layouts/base.njk 3 | templateClass: tmpl-author 4 | --- 5 |

{{ author.name }}

6 | 7 | {{ author.bio | markdownify | safe }} 8 | 9 |

← Home

10 | -------------------------------------------------------------------------------- /web/_includes/layouts/base.njk: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | {{ renderData.title or title or metadata.title }} 7 | 8 | {# #} 9 | 10 | 11 |
12 |

{{ metadata.title }}

13 | 18 |
19 | 20 | 21 | {{ content | safe }} 22 | 23 | 24 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /web/_includes/layouts/home.njk: -------------------------------------------------------------------------------- 1 | --- 2 | layout: layouts/base.njk 3 | templateClass: tmpl-home 4 | --- 5 | {{ content | safe }} 6 | -------------------------------------------------------------------------------- /web/_includes/layouts/post.njk: -------------------------------------------------------------------------------- 1 | --- 2 | layout: layouts/base.njk 3 | templateClass: tmpl-post 4 | --- 5 |

{{ post.title }}

6 |

Written by {% for author in post.authors %}{{author.name}}{% endfor %}

7 | {{ post.body | markdownify | safe }} 8 | 9 |

← Home

10 | -------------------------------------------------------------------------------- /web/_includes/postslist.njk: -------------------------------------------------------------------------------- 1 |
    2 | {% for post in postslist | reverse %} 3 | {% set currentPost = post.data.post %} 4 |
  1. 5 | {% if currentPost.title %}{{ currentPost.title }}{% else %}{{ post.url }}{% endif %} 6 | 7 |
  2. 8 | {% endfor %} 9 |
10 | -------------------------------------------------------------------------------- /web/archive.njk: -------------------------------------------------------------------------------- 1 | --- 2 | layout: layouts/home.njk 3 | navtitle: Archive 4 | permalink: /posts/ 5 | --- 6 | 7 |

Archive

8 | 9 | {% set postslist = collections.myPosts %} 10 | {% include "postslist.njk" %} 11 | -------------------------------------------------------------------------------- /web/author.njk: -------------------------------------------------------------------------------- 1 | --- 2 | layout: layouts/author 3 | tags: 4 | - myAuthors 5 | pagination: 6 | alias: author 7 | data: authors 8 | size: 1 9 | addAllPagesToCollections: true 10 | permalink: authors/{{ author.name | slug }}/index.html 11 | --- 12 | 13 | 14 | -------------------------------------------------------------------------------- /web/index.njk: -------------------------------------------------------------------------------- 1 | --- 2 | layout: layouts/home.njk 3 | tags: 4 | - nav 5 | navtitle: Home 6 | --- 7 | 8 |

Latest Posts

9 | 10 | {% set postslist = collections.myPosts %} 11 | {% include "postslist.njk" %} 12 | 13 |

More posts can be found in the archive.

14 | -------------------------------------------------------------------------------- /web/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "web", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "build": "npx eleventy", 8 | "watch": "npx eleventy --watch", 9 | "dev": "npm run watch", 10 | "now-build": "npm run build", 11 | "debug": "DEBUG=* npx eleventy" 12 | }, 13 | "keywords": [], 14 | "author": "", 15 | "license": "ISC", 16 | "devDependencies": { 17 | "@11ty/eleventy": "^0.8.3", 18 | "@11ty/eleventy-plugin-syntaxhighlight": "^2.0.3", 19 | "@sanity/block-content-to-markdown": "0.0.5", 20 | "@sanity/client": "^0.140.17", 21 | "groq": "^0.140.0", 22 | "luxon": "^1.13.2", 23 | "markdown-it": "^8.4.2", 24 | "markdown-it-anchor": "^5.0.2" 25 | }, 26 | "dependencies": { 27 | "@sanity/cli": "^0.140.17" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /web/post.njk: -------------------------------------------------------------------------------- 1 | --- 2 | layout: layouts/post 3 | tags: 4 | - myPosts 5 | pagination: 6 | alias: post 7 | data: posts 8 | size: 1 9 | addAllPagesToCollections: true 10 | permalink: posts/{{ post.title | slug }}/index.html 11 | --- 12 | 13 | 14 | -------------------------------------------------------------------------------- /web/utils/getAuthors.js: -------------------------------------------------------------------------------- 1 | const groq = require('groq') 2 | const client = require('./sanityClient.js') 3 | const BlocksToMarkdown = require('@sanity/block-content-to-markdown') 4 | const serializers = require('./serializers') 5 | 6 | function generateAuthor (author) { 7 | return { 8 | ...author, 9 | bio: BlocksToMarkdown(author.bio, { serializers, ...client.config() }) 10 | } 11 | } 12 | 13 | async function getAuthors () { 14 | const filter = groq`*[_type == "author"]` 15 | const docs = await client.fetch(filter).catch(err => console.error(err)) 16 | const authors = docs.map(generateAuthor) 17 | return authors 18 | } 19 | 20 | module.exports = getAuthors 21 | -------------------------------------------------------------------------------- /web/utils/getPosts.js: -------------------------------------------------------------------------------- 1 | const BlocksToMarkdown = require('@sanity/block-content-to-markdown') 2 | const groq = require('groq') 3 | const client = require('./sanityClient.js') 4 | const serializers = require('./serializers') 5 | 6 | function generatePost (post) { 7 | return { 8 | ...post, 9 | body: BlocksToMarkdown(post.body, { serializers, ...client.config() }) 10 | } 11 | } 12 | 13 | async function getPosts () { 14 | const filter = groq`*[_type == "post" && defined(slug) && publishedAt < now()]` 15 | const projection = groq`{ 16 | _id, 17 | publishedAt, 18 | title, 19 | slug, 20 | body[]{ 21 | ..., 22 | children[]{ 23 | ..., 24 | // Join inline reference 25 | _type == "authorReference" => { 26 | // check /studio/documents/authors.js for more fields 27 | "name": @.author->name, 28 | "slug": @.author->slug 29 | } 30 | } 31 | }, 32 | "authors": authors[].author-> 33 | }` 34 | const order = `| order(publishedAt desc)` 35 | const query = [filter, projection, order].join(' ') 36 | const docs = await client.fetch(query).catch(err => console.error(err)) 37 | const preparePosts = docs.map(generatePost) 38 | return preparePosts 39 | } 40 | 41 | module.exports = getPosts 42 | -------------------------------------------------------------------------------- /web/utils/sanityClient.js: -------------------------------------------------------------------------------- 1 | const sanityClient = require("@sanity/client"); 2 | 3 | /** 4 | * May break in certain build tools 5 | * if ../studio is not accessible 6 | */ 7 | const { api } = require('../../studio/sanity.json') 8 | 9 | /** 10 | * Set manually. Find configuration in 11 | * studio/sanity.json or on manage.sanity.io 12 | */ 13 | 14 | /* 15 | const config = { 16 | projectId: 'anokeucs', 17 | dataset: 'eleventy', 18 | useCdn: true 19 | } 20 | */ 21 | 22 | module.exports = sanityClient({...api, useCdn: true}); 23 | -------------------------------------------------------------------------------- /web/utils/serializers.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | types: { 3 | authorReference: (props) => `[${props.node.name}](/authors/${props.node.slug.current})`, // JSON.stringify(props, null, 2) 4 | code: props => 5 | '```' + props.node.language + '\n' + props.node.code + '\n```' 6 | } 7 | } 8 | --------------------------------------------------------------------------------