├── .eleventy.js ├── .eleventyignore ├── .github └── ISSUE_TEMPLATE │ ├── feature_request.md │ ├── i-have-a-question.md │ └── i-m-having-trouble-with-this-template.md ├── .gitignore ├── .nvmrc ├── LICENSE ├── README.md ├── fernfolio-preview.jpeg ├── netlify.toml ├── package.json └── src ├── 404.md ├── _11ty ├── filters │ ├── date-filters.js │ ├── markdown-filter.js │ └── svg-filter.js ├── libraries │ └── markdown-library.js ├── shortcodes │ └── image-shortcode.js └── utils │ ├── browser-sync-config.js │ └── minify-html.js ├── _data ├── global.json ├── home.json └── metadata.json ├── _includes ├── macros │ ├── articlelist.njk │ └── projectlist.njk └── partials │ ├── drawer.njk │ ├── footer.njk │ ├── form.njk │ ├── head.njk │ ├── pagination.njk │ └── site-header.njk ├── _layouts ├── base.njk ├── blog.njk ├── contact.njk ├── page.njk ├── post.njk ├── project.njk └── projects.njk ├── admin ├── config.yml ├── index.html └── preview-templates │ ├── about.js │ ├── article.js │ ├── blog.js │ ├── contact.js │ ├── home.js │ ├── index.js │ ├── project.js │ └── projects.js ├── assets ├── img │ ├── 1177px-cat_august_2010-4.jpg │ ├── fern-forest.jpeg │ ├── fern-in-hand.jpeg │ ├── icon-close.svg │ ├── icon-fern.svg │ ├── icon-hamburger.svg │ ├── icon-sun.svg │ └── logo.png ├── js │ ├── dark-mode.js │ ├── drawer.js │ └── main.js └── scss │ ├── components │ ├── drawer.scss │ ├── site-footer.scss │ └── site-header.scss │ ├── global │ ├── base-styles.scss │ ├── utilites.scss │ └── variables.scss │ ├── layouts │ ├── blog.scss │ ├── contact.scss │ ├── home.scss │ └── projects.scss │ ├── main.scss │ └── third-party │ ├── normalize.scss │ └── prisma-theme.scss ├── favicon.ico ├── index.njk ├── pages ├── about.md ├── blog.md ├── contact.md ├── pages.json └── projects.md ├── posts ├── code-sample.md ├── customizations.md ├── fourthpost.md ├── posts.json ├── secondpost.md └── thirdpost.md ├── projects ├── first-project.md ├── projects.json ├── second-project.md └── vue-component.md └── tags ├── project-tags.njk └── tags.njk /.eleventy.js: -------------------------------------------------------------------------------- 1 | const eleventyNavigationPlugin = require('@11ty/eleventy-navigation'); 2 | const syntaxHighlight = require('@11ty/eleventy-plugin-syntaxhighlight'); 3 | const imageShortcode = require('./src/_11ty/shortcodes/image-shortcode'); 4 | const markdownLibrary = require('./src/_11ty/libraries/markdown-library'); 5 | const minifyHtml = require('./src/_11ty/utils/minify-html'); 6 | const markdownFilter = require('./src/_11ty/filters/markdown-filter'); 7 | const svgFilter = require('./src/_11ty/filters/svg-filter'); 8 | const browserSyncConfig = require('./src/_11ty/utils/browser-sync-config'); 9 | const { readableDateFilter, machineDateFilter } = require('./src/_11ty/filters/date-filters'); 10 | 11 | module.exports = function (eleventyConfig) { 12 | // Plugins 13 | eleventyConfig.addPlugin(eleventyNavigationPlugin); 14 | eleventyConfig.addPlugin(syntaxHighlight); 15 | 16 | // Filters 17 | eleventyConfig.addFilter('markdown', markdownFilter); 18 | eleventyConfig.addFilter('readableDate', readableDateFilter); 19 | eleventyConfig.addFilter('machineDate', machineDateFilter); 20 | eleventyConfig.addFilter('svg', svgFilter); 21 | 22 | // Shortcodes 23 | eleventyConfig.addNunjucksAsyncShortcode('image', imageShortcode); 24 | 25 | // Libraries 26 | eleventyConfig.setLibrary('md', markdownLibrary); 27 | 28 | // Merge data instead of overriding 29 | eleventyConfig.setDataDeepMerge(true); 30 | 31 | // Trigger a build when files in this directory change 32 | eleventyConfig.addWatchTarget('./src/assets/scss/'); 33 | 34 | // Minify HTML output 35 | eleventyConfig.addTransform('htmlmin', minifyHtml); 36 | 37 | // Don't process folders with static assets 38 | eleventyConfig.addPassthroughCopy('./src/favicon.ico'); 39 | eleventyConfig.addPassthroughCopy('./src/admin'); 40 | eleventyConfig.addPassthroughCopy('./src/assets/img'); 41 | 42 | // Allow Turbolinks to work in development mode 43 | eleventyConfig.setBrowserSyncConfig(browserSyncConfig); 44 | 45 | return { 46 | templateFormats: ['md', 'njk', 'html'], 47 | markdownTemplateEngine: 'njk', 48 | htmlTemplateEngine: 'njk', 49 | dataTemplateEngine: 'njk', 50 | passthroughFileCopy: true, 51 | dir: { 52 | input: 'src', 53 | layouts: "_layouts" 54 | }, 55 | }; 56 | }; 57 | -------------------------------------------------------------------------------- /.eleventyignore: -------------------------------------------------------------------------------- 1 | README.md 2 | .github/ 3 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Enhancements. e.g. “I wish this template did this.” Suggest an idea! 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/i-have-a-question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: I have a question 3 | about: General education e.g. “How do I do this?” or “Can this template do this?” 4 | title: '' 5 | labels: question, education 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/i-m-having-trouble-with-this-template.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: I'm having trouble with this template 3 | about: Have a problem? It might be a bug! Create a report to help us improve. 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | _site/ 2 | node_modules/ 3 | package-lock.json 4 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 16 -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Tyler M. Roderick 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Netlify Status](https://api.netlify.com/api/v1/badges/5702ba89-7242-490e-b04d-e4a691faced5/deploy-status)](https://app.netlify.com/sites/fernfolio/deploys) 2 | 3 | # Fernfolio — An 11ty Portfolio Template 4 | Launch your personal portfolio in minutes and modify content without opening a code editor! 5 | 6 | fernfolio screenshot 7 | 8 | ###
🖥  [Demo](https://fernfolio.netlify.app/)
9 | 10 | ## 🤔 What is this? 11 | An [Eleventy](https://www.11ty.io/) theme designed to simplify the process of creating a beautiful portfolio and blog. Tightly integrated with [Netlify CMS](https://www.netlifycms.org/) for flexible, Git-powered content management. 12 | 13 | ## ✨ Features 14 | * Deep integration with [Netlify CMS](https://www.netlifycms.org/). Modify content without opening a code editor. 15 | * Customizable blog and project pages with tag support 16 | * Working contact form powered by [Netlify Forms](https://www.netlify.com/products/forms/) 17 | * Fast page speeds and high lighthouse scores 18 | * Uses Markdown for content files and Nunjucks for layouts 19 | * 100% Javascript framework free 20 | * SCSS support with sane base styles 21 | * Continuous Deployment workflow via [Netlify](https://www.netlify.com/) 22 | * Responsive images generated at build time 23 | * Minified HTML with [HTMLMinifier](https://github.com/kangax/html-minifier) 24 | * Minified CSS with [cssnano](https://github.com/cssnano/cssnano) 25 | * [Turbolinks](https://github.com/turbolinks/turbolinks) integration to enable instant navigation without page refresh 26 | * Useful Nunjuck filters built in 27 | 28 | 29 | ## 🚀 Quick Start 30 | ### 1. Click the "Deploy to Netlify" button below 31 | 32 | [![Deploy to Netlify](https://www.netlify.com/img/deploy/button.svg)](https://app.netlify.com/start/deploy?repository=https://github.com/TylerMRoderick/fernfolio-11ty-template&stack=cms) 33 | 34 | This will clone this repo to your github account and will deploy a copy of the demo website to your Netlify 35 | account (you can create an account during this process if you don't have one). 36 | 37 | ### 2. Setup authentication 38 | 39 | After deploying this project, Netlify Identity will add you as a CMS user and 40 | will email you an invite. Open that email, hit the "Accept the invite" link, and that should redirect you to the deployed site. From there, you can add your password to finish user setup. 41 | 42 | ### 3. Edit some content 43 | Now that you are added as a CMS user, add `/admin` to the end of your site url, refesh the page, and log in using your new credentials. You should now see the content dashboard. Now you can start editing content! 44 | 45 | Any changes saved in the CMS will trigger a git commit in your repo. That new commit will then trigger an auto-deployplent on Netlify. 46 | 47 | ## 🏠 Local Development 48 | If you want to test things locally before deploying, follow the steps below: 49 | 50 | - open your terminal 51 | - Clone the repo locally `git clone https://github.com/TylerMRoderick/fernfolio-11ty-template.git` 52 | - Navigate to root folder: `cd fernfolio-11ty-template/` 53 | - Install the goods: `npm install` 54 | - Run it: `npm start` 55 | - You should now be able to see everything running on localhost:8080 56 | 57 | ## 💻 Development Scripts 58 | 59 | **`npm start`** 60 | 61 | > Run 11ty with hot reload at localhost:8080 62 | 63 | **`npm run build`** 64 | 65 | > Generate minified production build 66 | 67 | Use this as the "Publish command" if needed by hosting such as Netlify. 68 | 69 | ## 💡 Dark mode 70 | 71 | To enable switching from light to dark mode, `global.json` has some settings: 72 | 73 | - `enable_theme_switch`: set to `true` if you want your visitors to be able to switch theme 74 | - `default_theme`: set to `dark` or another value (which always means `light`) 75 | - `use_system_theme`: set to `true` if you want the system preference to be enforced 76 | 77 | ## 🎩 Common issues 78 | 79 | If you change the repo that was created at deploy time from public to private, you'll need to regenerate your token, 80 | as the token generated using the deploy to Netlify button can only access public repositories. To 81 | regenerate your token, head to "Settings" in your Netlify site dashboard, go to the "Identity" 82 | section, then scroll to "Services" where you'll see an "Edit settings" button. Click that and you'll 83 | see a text link to "Generate access token in GitHub". 84 | 85 | ## 🗣 Bug reports, feature requests, etc. 86 | 87 | This is a fun side project for me and I always welcome questions/comments. If you run into any problems or have a feature request, please open an issue. I try to read every one and will gladly assist you whenever possible. 88 | 89 | ## Credit 90 | *This project was originally forked from [eleventy-netlify-boilerplate](https://github.com/danurbanowicz/eleventy-netlify-boilerplate), but completely revamped to match the needs of a modern porfolio.* 91 | -------------------------------------------------------------------------------- /fernfolio-preview.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TylerMRoderick/fernfolio-11ty-template/af491fe90e3030f8a9c08f13f9f808d7b4af2bb3/fernfolio-preview.jpeg -------------------------------------------------------------------------------- /netlify.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | publish = "_site" 3 | command = "npm run build" 4 | 5 | # REDIRECT and HEADERS examples 6 | 7 | # Redirect rule example 8 | # For more information see:- https://www.netlify.com/docs/netlify-toml-reference/ 9 | 10 | #[[redirects]] 11 | # from = "/*" 12 | # to = "/blog/:splat" 13 | 14 | # The default HTTP status code is 301, but you can define a different one e.g. 15 | # status = 302 16 | 17 | # Headers rule example 18 | # For more information see:- https://www.netlify.com/docs/netlify-toml-reference/ 19 | 20 | #[[headers]] 21 | # Define which paths this specific [[headers]] block will cover. 22 | # for = "/*" 23 | 24 | #[headers.values] 25 | # X-Frame-Options = "DENY" 26 | # X-XSS-Protection = "1; mode=block" 27 | # Content-Security-Policy = "frame-ancestors https://www.facebook.com" 28 | 29 | # Redirects and headers are GLOBAL for all builds – they do not get scoped to 30 | # contexts no matter where you define them in the file. 31 | # For context-specific rules, use _headers or _redirects files, which are 32 | # applied on a PER-DEPLOY basis. 33 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fernfolio-11ty-template", 3 | "version": "1.0.0", 4 | "description": "The super simple portfolio template built with Eleventy and Netlify CMS", 5 | "scripts": { 6 | "start": "cross-env npm-run-all build:sass --parallel watch:*", 7 | "build": "cross-env npm-run-all build:sass build:scripts build:eleventy", 8 | "watch:scripts": "esbuild \"./src/assets/js/main.js\" --target=es6 --bundle --outfile=\"./_site/assets/js/main.bundle.js\"", 9 | "build:scripts": "esbuild \"./src/assets/js/main.js\" --target=es6 --bundle --minify --outfile=\"./_site/assets/js/main.bundle.js\"", 10 | "watch:sass": "sass --no-source-map --watch src/assets/scss:_site/assets/css/", 11 | "build:sass": "sass --no-source-map src/assets/scss/main.scss _site/assets/css/main.css", 12 | "watch:eleventy": "eleventy --serve --incremental", 13 | "build:eleventy": "eleventy", 14 | "postbuild": "postcss _site/assets/css/*.css -u autoprefixer cssnano -r --no-map", 15 | "clean": "rimraf './_site'" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "https://github.com/tylerMRoderick/fernfolio-11ty-template" 20 | }, 21 | "author": { 22 | "name": "Tyler M. Roderick", 23 | "email": "troderick@protonmail.com", 24 | "url": "https://www.tylerroderick.com/" 25 | }, 26 | "license": "MIT", 27 | "bugs": { 28 | "url": "https://github.com/tylerMRoderick/fernfolio-11ty-template/issues" 29 | }, 30 | "homepage": "https://github.com/tylerMRoderick/fernfolio-11ty-template", 31 | "devDependencies": { 32 | "@11ty/eleventy": "^2.0.1", 33 | "@11ty/eleventy-img": "^2.0.1", 34 | "@11ty/eleventy-navigation": "^0.3.3", 35 | "@11ty/eleventy-plugin-syntaxhighlight": "^4.2.0", 36 | "autoprefixer": "^10.4.8", 37 | "cross-env": "^7.0.3", 38 | "cssnano": "^5.1.12", 39 | "esbuild": "^0.14.53", 40 | "html-minifier": "^4.0.0", 41 | "luxon": "^1.25.0", 42 | "markdown-it": "^13.0.1", 43 | "markdown-it-anchor": "^8.6.6", 44 | "npm-run-all": "^4.1.5", 45 | "path": "^0.12.7", 46 | "postcss": "^8.4.16", 47 | "postcss-cli": "^10.0.0", 48 | "postcss-scss": "^4.0.4", 49 | "rimraf": "^3.0.2", 50 | "sass": "^1.54.3", 51 | "turbolinks": "^5.2.0" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/404.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 404 3 | layout: page.njk 4 | permalink: /404.html 5 | --- 6 | Uh oh, the resource you requested isn't here. Sorry. 7 | -------------------------------------------------------------------------------- /src/_11ty/filters/date-filters.js: -------------------------------------------------------------------------------- 1 | const { DateTime } = require('luxon'); 2 | 3 | // Date formatting (human readable) 4 | function readableDateFilter (dateObj) { 5 | return DateTime.fromJSDate(dateObj).toFormat('DDD'); 6 | }; 7 | 8 | // Date formatting (machine readable) 9 | function machineDateFilter (dateObj) { 10 | return DateTime.fromJSDate(dateObj).toFormat('yyyy-MM-dd'); 11 | } 12 | 13 | module.exports = { 14 | readableDateFilter, 15 | machineDateFilter, 16 | }; -------------------------------------------------------------------------------- /src/_11ty/filters/markdown-filter.js: -------------------------------------------------------------------------------- 1 | let markdownIt = require('markdown-it'); 2 | 3 | module.exports = function (str) { 4 | return markdownIt().render(str); 5 | }; -------------------------------------------------------------------------------- /src/_11ty/filters/svg-filter.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | 4 | module.exports = function(filePath) { 5 | if (path.extname(filePath) !== '.svg') { 6 | throw new Error('svg filter requires .svg file extension'); 7 | } 8 | 9 | const data = fs.readFileSync(filePath, function(err, contents) { 10 | if (err) { 11 | throw new Error(err) ; 12 | } 13 | 14 | return contents 15 | }); 16 | 17 | return data.toString('utf8'); 18 | } 19 | -------------------------------------------------------------------------------- /src/_11ty/libraries/markdown-library.js: -------------------------------------------------------------------------------- 1 | let markdownIt = require('markdown-it'); 2 | let anchor = require('markdown-it-anchor'); 3 | const Image = require('@11ty/eleventy-img'); 4 | 5 | // Customize Markdown library and settings 6 | let markdown = markdownIt({ 7 | html: true, 8 | breaks: true, 9 | linkify: true 10 | }).use(anchor, { 11 | permalink: anchor.permalink.linkInsideHeader({ 12 | symbol: '#', 13 | placement: 'before' 14 | }) 15 | }); 16 | 17 | 18 | // Add responsive image suppport to markdown files 19 | // borrowed from: https://tomichen.com/blog/posts/20220416-responsive-images-in-markdown-with-eleventy-image 20 | markdown.renderer.rules.image = function (tokens, idx) { 21 | const token = tokens[idx]; 22 | const isRemoteImage = token.attrGet('src').startsWith('http'); 23 | const imgSrc = isRemoteImage 24 | ? token.attrGet('src') 25 | : `.${token.attrGet('src')}`; 26 | const imgAlt = token.content; 27 | const imgTitle = token.attrGet('title'); 28 | const htmlOpts = { alt: imgAlt, loading: 'lazy', decoding: 'async' }; 29 | const parsed = (imgTitle || '').match( 30 | /^(?@skip(?:\[(?\d+)x(?\d+)\])? ?)?(?:\?\[(?.*?)\] ?)?(?.*)/ 31 | ).groups; 32 | 33 | // handle skipped and remote images 34 | if (parsed.skip || isRemoteImage) { 35 | const options = { ...htmlOpts }; 36 | const metadata = { jpeg: [{ url: imgSrc }] }; 37 | 38 | if (parsed.sizes) { 39 | options.sizes = parsed.sizes; 40 | } 41 | 42 | if (parsed.width && parsed.height) { 43 | metadata.jpeg[0].width = parsed.width 44 | metadata.jpeg[0].height = parsed.height 45 | } 46 | 47 | return Image.generateHTML(metadata, options); 48 | } 49 | 50 | const widths = [250, 316, 426, 460, 580, 768]; 51 | const imgOpts = { 52 | widths: widths 53 | .concat(widths.map((w) => w * 2)) // generate 2x sizes 54 | .filter((v, i, s) => s.indexOf(v) === i), // dedupe 55 | formats: ['webp', 'jpeg'], 56 | urlPath: '/assets/img/', 57 | outputDir: './_site/assets/img/', 58 | }; 59 | 60 | Image(imgSrc, imgOpts); 61 | 62 | const metadata = Image.statsSync(imgSrc, imgOpts); 63 | 64 | return Image.generateHTML(metadata, { 65 | sizes: parsed.sizes || '(max-width: 768px) 100vw, 768px', 66 | ...htmlOpts 67 | }); 68 | } 69 | 70 | module.exports = markdown; -------------------------------------------------------------------------------- /src/_11ty/shortcodes/image-shortcode.js: -------------------------------------------------------------------------------- 1 | const Image = require("@11ty/eleventy-img"); 2 | 3 | async function imageShortcode(src, alt, sizes, classes, loading = "lazy") { 4 | // Remove leading slash if it exists 5 | src = src.startsWith("/") ? src.slice(1) : src; 6 | 7 | let metadata = await Image(src, { 8 | widths: [25, 320, 640, 960, 1200, 1800, 2400], 9 | formats: ["webp", "jpeg"], 10 | urlPath: "/assets/img/", 11 | outputDir: "_site/assets/img/", 12 | }); 13 | 14 | let imageAttributes = { 15 | class: classes, 16 | alt, 17 | sizes, 18 | loading, 19 | decoding: "async", 20 | }; 21 | 22 | return Image.generateHTML(metadata, imageAttributes); 23 | } 24 | 25 | module.exports = imageShortcode; 26 | -------------------------------------------------------------------------------- /src/_11ty/utils/browser-sync-config.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | 3 | // Custom config required to get Turbolinks to work in development mode 4 | module.exports = { 5 | // show 404s in dev 6 | callbacks: { 7 | ready: function(_, browserSync) { 8 | // This is keeps the exception from showing during the first local build 9 | const generated404Exists = fs.existsSync('_site/404.html'); 10 | const content_404 = generated404Exists 11 | ? fs.readFileSync('_site/404.html') 12 | : '

File Does Not Exist

'; 13 | 14 | browserSync.addMiddleware('*', (_, res) => { 15 | // Provides the 404 content without redirect. 16 | res.write(content_404); 17 | res.end(); 18 | }); 19 | } 20 | }, 21 | // scripts in body conflict with Turbolinks 22 | snippetOptions: { 23 | rule: { 24 | match: /<\/head>/i, 25 | fn: function(snippet, match) { 26 | return snippet + match; 27 | } 28 | } 29 | } 30 | }; -------------------------------------------------------------------------------- /src/_11ty/utils/minify-html.js: -------------------------------------------------------------------------------- 1 | const htmlmin = require('html-minifier'); 2 | 3 | // Minify HTML output 4 | module.exports = (content, outputPath) => { 5 | if (outputPath && outputPath.indexOf('.html') > -1) { 6 | let minified = htmlmin.minify(content, { 7 | useShortDoctype: true, 8 | removeComments: true, 9 | collapseWhitespace: true, 10 | }); 11 | return minified; 12 | } 13 | return content; 14 | } 15 | -------------------------------------------------------------------------------- /src/_data/global.json: -------------------------------------------------------------------------------- 1 | { 2 | "logo": "src/assets/img/icon-fern.svg", 3 | "menu_icon": "src/assets/img/icon-hamburger.svg", 4 | "close_icon": "src/assets/img/icon-close.svg", 5 | "enable_theme_switch": true, 6 | "switch_theme_icon": "src/assets/img/icon-sun.svg", 7 | "default_theme": "light", 8 | "use_system_theme": false 9 | } -------------------------------------------------------------------------------- /src/_data/home.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Fernfolio", 3 | "image": "/src/assets/img/fern-in-hand.jpeg", 4 | "image_alt": "Fern in hand", 5 | "body": "The super simple portfolio template built with [Eleventy](https://www.11ty.dev/) and [Netlify CMS](https://www.netlifycms.org/)." 6 | } -------------------------------------------------------------------------------- /src/_data/metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Fernfolio", 3 | "description": "A template for building a simple porfolio website with the Eleventy static site generator, Netlify CMS, and with deployment to Netlify." 4 | } 5 | -------------------------------------------------------------------------------- /src/_includes/macros/articlelist.njk: -------------------------------------------------------------------------------- 1 | {% macro articlelist(articles, show_tags = true, limit) %} 2 |
3 | {% for article in articles %} 4 | {% if limit === undefined or loop.index0 < limit %} 5 | 28 | {% endif %} 29 | {% endfor %} 30 |
31 | {% endmacro %} -------------------------------------------------------------------------------- /src/_includes/macros/projectlist.njk: -------------------------------------------------------------------------------- 1 | {% macro projectlist(projects) %} 2 |
3 |
4 | {% for project in projects %} 5 |
6 |

7 | 8 | {% if project.data.emoji %} 9 | {{ project.data.emoji }} 10 | {% endif %} 11 | {% if project.data.title %} 12 | {{ project.data.title }} 13 | {% else %} 14 | Untitled 15 | {% endif %} 16 | 17 |

18 | {% if project.data.summary %} 19 |

20 | {{ project.data.summary }} 21 |

22 | {% endif %} 23 | {% if project.data.tags %} 24 |

25 | {% for tag in project.data.tags %} 26 | {%- if tag != "project" -%} 27 | {% set tagUrl %}/project-tags/{{ tag }}/{% endset %} 28 | 29 | {%- endif -%} 30 | {% endfor %} 31 |

32 | {% endif %} 33 |
34 | {% endfor %} 35 |
36 |
37 | {% endmacro %} 38 | -------------------------------------------------------------------------------- /src/_includes/partials/drawer.njk: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 | 8 |
9 |
10 | {% set navPages = collections.all | eleventyNavigation %} 11 | 12 | 32 |
33 |
34 |
-------------------------------------------------------------------------------- /src/_includes/partials/footer.njk: -------------------------------------------------------------------------------- 1 |
2 | 3 | Website built with Eleventy. 4 | Source code on GitHub. 5 | 6 |
7 | -------------------------------------------------------------------------------- /src/_includes/partials/form.njk: -------------------------------------------------------------------------------- 1 |
2 | 3 | 12 | 13 | 23 | 24 | 25 | 26 |
-------------------------------------------------------------------------------- /src/_includes/partials/head.njk: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{ renderData.title or title or metadata.title }} 5 | 6 | 7 | 8 | {# Canonical URL #} 9 | {% if canonical %} 10 | 11 | {% endif %} 12 | 13 | 14 | {# Third-party scripts #} 15 | 16 | 17 | {# Fonts #} 18 | 19 | 20 | 21 | {# CSS #} 22 | 23 | 24 | {# Javascript #} 25 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /src/_includes/partials/pagination.njk: -------------------------------------------------------------------------------- 1 | {% if pagination.pages.length > 1 %} 2 | 28 | {% endif %} -------------------------------------------------------------------------------- /src/_includes/partials/site-header.njk: -------------------------------------------------------------------------------- 1 | 43 | -------------------------------------------------------------------------------- /src/_layouts/base.njk: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | {% include "partials/head.njk" %} 11 | 12 | 13 |
14 |
15 |
16 | 17 | {% include "partials/site-header.njk" %} 18 | {% include "partials/drawer.njk" %} 19 |
20 | {{ layoutContent | safe }} 21 |
22 | 23 | {% include "partials/footer.njk" %} 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/_layouts/blog.njk: -------------------------------------------------------------------------------- 1 | --- 2 | layout: base.njk 3 | section: blog 4 | permalink: /blog/index.html 5 | --- 6 |
7 |

{{title}}

8 | {{ layoutContent | safe }} 9 |
10 | 11 | {# Article List #} 12 | {% from "macros/articlelist.njk" import articlelist %} 13 | {{ articlelist( 14 | articles = pagination.items | reverse, 15 | show_tags = true 16 | ) }} 17 | 18 | {# Pagination #} 19 | {% include 'partials/pagination.njk' %} 20 |
21 | -------------------------------------------------------------------------------- /src/_layouts/contact.njk: -------------------------------------------------------------------------------- 1 | --- 2 | layout: base.njk 3 | section: contact 4 | --- 5 |
6 |

{{ title }}

7 |
8 | {{ layoutContent | safe }} 9 | 10 | {% include "partials/form.njk" %} 11 |
-------------------------------------------------------------------------------- /src/_layouts/page.njk: -------------------------------------------------------------------------------- 1 | --- 2 | layout: base.njk 3 | section: page 4 | --- 5 |
6 |

{{ title }}

7 | {% if subtitle %} 8 |

{{ subtitle }}

9 | {% endif %} 10 |
11 | {{ layoutContent | safe }} 12 |
13 | -------------------------------------------------------------------------------- /src/_layouts/post.njk: -------------------------------------------------------------------------------- 1 | --- 2 | layout: base.njk 3 | section: post 4 | --- 5 |
6 |
7 |

{{ title }}

8 |

9 | 10 |

11 |
12 | 13 | {{ layoutContent | safe }} 14 | 15 | {% if tags %} 16 |

17 | {% for tag in tags %} 18 | {%- if tag != "post" -%} 19 | {% set tagUrl %}/tags/{{ tag }}/{% endset %} 20 | 21 | {%- endif -%} 22 | {% endfor %} 23 |

24 | {% endif %} 25 | 26 |
27 | 28 | 31 |
-------------------------------------------------------------------------------- /src/_layouts/project.njk: -------------------------------------------------------------------------------- 1 | --- 2 | layout: base.njk 3 | section: project 4 | --- 5 |
6 |

7 | {% if emoji %} 8 | {{ emoji }} 9 | {% endif %} 10 | {{title}}

11 |
12 | {{ layoutContent | safe }} 13 | 16 |
-------------------------------------------------------------------------------- /src/_layouts/projects.njk: -------------------------------------------------------------------------------- 1 | --- 2 | layout: base.njk 3 | section: projects 4 | --- 5 |
6 |

{{title}}

7 | 8 | {% if subtitle %} 9 |

{{ subtitle }}

10 | {% endif %} 11 | 12 |
13 | 14 | {{ layoutContent | safe }} 15 | 16 | {# Projects #} 17 | {% from "macros/projectlist.njk" import projectlist %} 18 | {{ projectlist(projects = collections.project | reverse) }} 19 |
-------------------------------------------------------------------------------- /src/admin/config.yml: -------------------------------------------------------------------------------- 1 | backend: 2 | name: git-gateway 3 | branch: master # Branch to update (optional; defaults to master) 4 | 5 | # Uncomment below to enable drafts 6 | # publish_mode: editorial_workflow 7 | 8 | media_folder: "src/assets/img" # Media files will be stored in the repo under images/uploads 9 | 10 | collections: 11 | #Global Settings 12 | - name: "global-settings" 13 | label: "Global Settings" 14 | slug: "{{slug}}" 15 | files: 16 | 17 | # Site Metadata 18 | - label: "Metadata" 19 | name: "metadata" 20 | file: "src/_data/metadata.json" 21 | fields: 22 | - { label: "Title", name: "title", widget: "string" } 23 | - { label: "Description", name: "description", widget: "string" } 24 | 25 | # Global Settings 26 | - label: "Global Settings" 27 | name: "global" 28 | file: "src/_data/global.json" 29 | fields: 30 | - { label: "Enable Theme switch", name: "enable_theme_switch", widget: "boolean", required: true } 31 | - { label: "Switch Theme Icon (.svg)", name: "switch_theme_icon", widget: "image", required: true } 32 | - { label: "Use System Theme", name: "use_system_theme", widget: "boolean", required: true } 33 | - { 34 | label: "Default Theme", 35 | name: "default_theme", 36 | widget: "select", 37 | options: ["light", "dark"], 38 | default: "light", 39 | required: true 40 | } 41 | - { label: "Logo (.svg)", name: "logo", widget: "image", required: true } 42 | - { label: "Mobile Menu Icon (.svg)", name: "menu_icon", widget: "image", required: true } 43 | - { label: "Close Icon (.svg)", name: "close_icon", widget: "image", required: true } 44 | 45 | # Articles 46 | - name: "article" # Used in routes, e.g., /admin/collections/blog 47 | label: "Post" # Used in the UI 48 | folder: "src/posts" # The path to the folder where the documents are stored 49 | create: true # Allow users to create new documents in this collection 50 | slug: "{{slug}}" # Filename template, e.g., YYYY-MM-DD-title.md 51 | fields: # The fields for each document, usually in front matter 52 | - { label: "Title", name: "title", widget: "string" } 53 | - { label: "Publish Date", name: "date", widget: "datetime" } 54 | - { label: "Body", name: "body", widget: "markdown" } 55 | - { label: "Summary", name: "summary", widget: "text" } 56 | - { 57 | label: "Meta Description", 58 | name: "metaDescription", 59 | widget: "text", 60 | required: false, 61 | } 62 | - { label: "Tags", name: "tags", widget: "list", default: ["post"] } 63 | 64 | # Projects 65 | - name: "project" 66 | label: "Project" 67 | folder: "src/projects" 68 | create: true 69 | slug: "{{slug}}" 70 | fields: 71 | - { label: "Title", name: "title", widget: "string" } 72 | - { label: "Emoji", name: "emoji", widget: "string" } 73 | - { label: "Publish Date", name: "date", widget: "datetime" } 74 | - { label: "Body", name: "body", widget: "markdown" } 75 | - { label: "Summary", name: "summary", widget: "text" } 76 | - { 77 | label: "Meta Description", 78 | name: "metaDescription", 79 | widget: "text", 80 | required: false, 81 | } 82 | - { label: "Tags", name: "tags", widget: "list", default: ["post"] } 83 | 84 | # Pages 85 | - name: "pages" 86 | label: "Page" 87 | slug: "{{slug}}" 88 | files: 89 | 90 | # Home page 91 | - label: "Home Page" 92 | name: "home" 93 | file: "src/_data/home.json" 94 | fields: 95 | - { label: Title, name: title, widget: string } 96 | - { label: "Body", name: "body", widget: "markdown", required: false } 97 | - { label: Image, name: image, widget: image, required: false } 98 | - { 99 | label: Image Alt Text, 100 | name: image_alt, 101 | widget: string, 102 | required: false, 103 | } 104 | 105 | # About page 106 | - label: "About Page" 107 | name: "about" 108 | file: "src/pages/about.md" 109 | fields: 110 | - { label: "Title", name: "title", widget: "string" } 111 | - { label: Subtitle, name: subtitle, widget: string, required: false } 112 | - { label: "Body", name: "body", widget: "markdown", required: false } 113 | - { 114 | label: "Meta Description", 115 | name: "metaDescription", 116 | widget: "text", 117 | required: false, 118 | } 119 | - { label: "Publish Date", name: "date", widget: "datetime" } 120 | - { label: "Permalink", name: "permalink", widget: "string" } 121 | - label: "Navigation" # https://www.11ty.dev/docs/plugins/navigation/ 122 | name: "eleventyNavigation" 123 | widget: "object" 124 | fields: 125 | - { label: "Key", name: "key", widget: "string" } 126 | - { label: "Order", name: "order", widget: "number", default: 0 } 127 | 128 | # Blog Page 129 | - label: "Blog Page" 130 | name: "blog" 131 | file: "src/pages/blog.md" 132 | fields: 133 | - { label: "Title", name: "title", widget: "string" } 134 | - { label: "Body", name: "body", widget: "markdown", required: false } 135 | - { 136 | label: "Meta Description", 137 | name: "metaDescription", 138 | widget: "text", 139 | required: false, 140 | } 141 | - { label: "Publish Date", name: "date", widget: "datetime" } 142 | - { label: "Permalink", name: "permalink", widget: "string" } 143 | - label: "Navigation" # https://www.11ty.dev/docs/plugins/navigation/ 144 | name: "eleventyNavigation" 145 | widget: "object" 146 | fields: 147 | - { label: "Key", name: "key", widget: "string" } 148 | - { label: "Order", name: "order", widget: "number", default: 0 } 149 | 150 | # Projects Page 151 | - label: "Projects Page" 152 | name: "projects" 153 | file: "src/pages/projects.md" 154 | fields: 155 | - { label: "Title", name: "title", widget: "string" } 156 | - { label: Subtitle, name: subtitle, widget: string, required: false } 157 | - { label: "Body", name: "body", widget: "markdown", required: false } 158 | - { 159 | label: "Meta Description", 160 | name: "metaDescription", 161 | widget: "text", 162 | required: false, 163 | } 164 | - { label: "Publish Date", name: "date", widget: "datetime" } 165 | - { label: "Permalink", name: "permalink", widget: "string" } 166 | - label: "Navigation" # https://www.11ty.dev/docs/plugins/navigation/ 167 | name: "eleventyNavigation" 168 | widget: "object" 169 | fields: 170 | - { label: "Key", name: "key", widget: "string" } 171 | - { label: "Order", name: "order", widget: "number", default: 0 } 172 | 173 | # Contact Page 174 | - label: "Contact Page" 175 | name: "contact" 176 | file: "src/pages/contact.md" 177 | fields: 178 | - { label: "Title", name: "title", widget: "string" } 179 | - { label: "Body", name: "body", widget: "markdown", required: false } 180 | - { 181 | label: "Meta Description", 182 | name: "metaDescription", 183 | widget: "text", 184 | required: false, 185 | } 186 | - { label: "Publish Date", name: "date", widget: "datetime" } 187 | - { label: "Permalink", name: "permalink", widget: "string" } 188 | - label: "Navigation" # https://www.11ty.dev/docs/plugins/navigation/ 189 | name: "eleventyNavigation" 190 | widget: "object" 191 | fields: 192 | - { label: "Key", name: "key", widget: "string" } 193 | - { label: "Order", name: "order", widget: "number", default: 0 } 194 | -------------------------------------------------------------------------------- /src/admin/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Netlify CMS 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/admin/preview-templates/about.js: -------------------------------------------------------------------------------- 1 | import htm from 'https://unpkg.com/htm?module'; 2 | 3 | const html = htm.bind(h); 4 | 5 | // Preview component for About Page 6 | const About = createClass({ 7 | render() { 8 | const entry = this.props.entry; 9 | 10 | return html` 11 |
12 |

${entry.getIn(['data', 'title'], null)}

13 |

${entry.getIn(['data', 'subtitle'], null)}

14 |
15 | 16 | ${this.props.widgetFor('body')} 17 |
18 | `; 19 | }, 20 | }); 21 | 22 | export default About; 23 | -------------------------------------------------------------------------------- /src/admin/preview-templates/article.js: -------------------------------------------------------------------------------- 1 | import htm from 'https://unpkg.com/htm?module'; 2 | import format from 'https://unpkg.com/date-fns@2.7.0/esm/format/index.js?module'; 3 | 4 | const html = htm.bind(h); 5 | 6 | // Preview component for an Article 7 | const Article = createClass({ 8 | render() { 9 | const entry = this.props.entry; 10 | 11 | return html` 12 |
13 |
14 |

${entry.getIn(['data', 'title'], null)}

15 |

16 | 17 | 23 | 24 |

25 |
26 | 27 | ${this.props.widgetFor('body')} 28 |

29 | ${entry 30 | .getIn(['data', 'tags'], []) 31 | .map((tag) => html` `)} 32 |

33 |
34 |
35 | `; 36 | }, 37 | }); 38 | 39 | export default Article; 40 | -------------------------------------------------------------------------------- /src/admin/preview-templates/blog.js: -------------------------------------------------------------------------------- 1 | import htm from 'https://unpkg.com/htm?module'; 2 | 3 | const html = htm.bind(h); 4 | 5 | // Preview component for top-level Blog Page 6 | const Blog = createClass({ 7 | render() { 8 | const entry = this.props.entry; 9 | 10 | return html` 11 |
12 |

${entry.getIn(['data', 'title'], null)}

13 | ${this.props.widgetFor('body')} 14 |
15 | 16 | 17 |
18 | 31 | 44 |
45 |
46 | `; 47 | }, 48 | }); 49 | 50 | export default Blog; 51 | -------------------------------------------------------------------------------- /src/admin/preview-templates/contact.js: -------------------------------------------------------------------------------- 1 | import htm from 'https://unpkg.com/htm?module'; 2 | 3 | const html = htm.bind(h); 4 | 5 | // Preview component for Contact Page 6 | const Contact = createClass({ 7 | render() { 8 | const entry = this.props.entry; 9 | 10 | return html` 11 |
12 |

${entry.getIn(['data', 'title'], null)}

13 |
14 | 15 | ${this.props.widgetFor('body')} 16 | 17 | 18 |
19 | 20 | 29 | 30 | 39 | 40 | 41 | 42 |
43 |
44 | `; 45 | }, 46 | }); 47 | 48 | export default Contact; 49 | -------------------------------------------------------------------------------- /src/admin/preview-templates/home.js: -------------------------------------------------------------------------------- 1 | import htm from 'https://unpkg.com/htm?module'; 2 | 3 | const html = htm.bind(h); 4 | 5 | // Preview component for the Home page 6 | const Home = createClass({ 7 | render() { 8 | const entry = this.props.entry; 9 | const image = entry.getIn(['data', 'image']); 10 | const imageSrc = this.props.getAsset(image); 11 | 12 | return html` 13 |
14 |
15 |
16 | 17 | 18 |
19 |
20 |

${entry.getIn(['data', 'title'], null)}

21 |
${this.props.widgetFor('body')}
22 |
23 |
24 | ${entry.getIn(['data', 'image_alt'], null)} 31 |
32 |
33 | 34 | 35 |
36 |

Projects

37 | View All 38 |
39 |
40 |
41 |
42 |

43 | 44 | 👾 45 | Placeholder Project 46 | 47 |

48 |

This is not a real project and is only visible in the CMS.

49 |

50 | 51 | 52 |

53 |
54 |
55 |

56 | 57 | 👾 58 | Another Placeholder 59 | 60 |

61 |

This is the summary for the second placeholder project.

62 |

63 | 64 | 65 |

66 |
67 |
68 |
69 | 70 | 71 | 72 |
73 |

Articles

74 | View All 75 |
76 |
77 | 85 | 93 |
94 |
95 |
96 | `; 97 | }, 98 | }); 99 | 100 | export default Home; 101 | -------------------------------------------------------------------------------- /src/admin/preview-templates/index.js: -------------------------------------------------------------------------------- 1 | import Home from '/admin/preview-templates/home.js'; 2 | import About from '/admin/preview-templates/about.js'; 3 | import Blog from '/admin/preview-templates/blog.js'; 4 | import Article from '/admin/preview-templates/article.js'; 5 | import Projects from '/admin/preview-templates/projects.js'; 6 | import Project from '/admin/preview-templates/project.js'; 7 | import Contact from '/admin/preview-templates/contact.js'; 8 | 9 | // Register preview templates 10 | CMS.registerPreviewTemplate('home', Home); 11 | CMS.registerPreviewTemplate('about', About); 12 | CMS.registerPreviewTemplate('blog', Blog); 13 | CMS.registerPreviewTemplate('article', Article); 14 | CMS.registerPreviewTemplate('projects', Projects); 15 | CMS.registerPreviewTemplate('project', Project); 16 | CMS.registerPreviewTemplate('contact', Contact); 17 | 18 | // Register CSS 19 | fetch('/') 20 | .then((response) => response.text()) 21 | .then((html) => { 22 | const f = document.createElement('html'); 23 | f.innerHTML = html; 24 | Array.from(f.getElementsByTagName('link')).forEach((tag) => { 25 | if (tag.rel == 'stylesheet' && !tag.media) { 26 | CMS.registerPreviewStyle(tag.href); 27 | } 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /src/admin/preview-templates/project.js: -------------------------------------------------------------------------------- 1 | import htm from 'https://unpkg.com/htm?module'; 2 | import format from 'https://unpkg.com/date-fns@2.7.0/esm/format/index.js?module'; 3 | 4 | const html = htm.bind(h); 5 | 6 | // Preview component for a Project 7 | const Project = createClass({ 8 | render() { 9 | const entry = this.props.entry; 10 | 11 | return html` 12 |
13 |

14 | ${entry.getIn(['data', 'emoji'], null)} 15 | ${entry.getIn(['data', 'title'], null)} 16 |

17 |
18 | 19 | ${this.props.widgetFor('body')} 20 | 21 | 24 |
25 | `; 26 | }, 27 | }); 28 | 29 | export default Project; 30 | -------------------------------------------------------------------------------- /src/admin/preview-templates/projects.js: -------------------------------------------------------------------------------- 1 | import htm from 'https://unpkg.com/htm?module'; 2 | 3 | const html = htm.bind(h); 4 | 5 | // Preview component for top-level Projects Page 6 | const Projects = createClass({ 7 | render() { 8 | const entry = this.props.entry; 9 | 10 | return html` 11 |
12 |

${entry.getIn(['data', 'title'], null)}

13 |

${entry.getIn(['data', 'subtitle'], null)}

14 |
15 | 16 | ${this.props.widgetFor('body')} 17 | 18 | 19 |
20 |
21 |
22 |

23 | 24 | 👾 25 | Placeholder Project 26 | 27 |

28 |

This is not a real project and is only visible in the CMS.

29 |

30 | 31 | 32 |

33 |
34 |
35 |

36 | 37 | 👾 38 | Another Placeholder 39 | 40 |

41 |

This is the summary for the second placeholder project.

42 |

43 | 44 | 45 |

46 |
47 |
48 |
49 |
50 | `; 51 | }, 52 | }); 53 | 54 | export default Projects; 55 | -------------------------------------------------------------------------------- /src/assets/img/1177px-cat_august_2010-4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TylerMRoderick/fernfolio-11ty-template/af491fe90e3030f8a9c08f13f9f808d7b4af2bb3/src/assets/img/1177px-cat_august_2010-4.jpg -------------------------------------------------------------------------------- /src/assets/img/fern-forest.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TylerMRoderick/fernfolio-11ty-template/af491fe90e3030f8a9c08f13f9f808d7b4af2bb3/src/assets/img/fern-forest.jpeg -------------------------------------------------------------------------------- /src/assets/img/fern-in-hand.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TylerMRoderick/fernfolio-11ty-template/af491fe90e3030f8a9c08f13f9f808d7b4af2bb3/src/assets/img/fern-in-hand.jpeg -------------------------------------------------------------------------------- /src/assets/img/icon-close.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/img/icon-fern.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/img/icon-hamburger.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/assets/img/icon-sun.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TylerMRoderick/fernfolio-11ty-template/af491fe90e3030f8a9c08f13f9f808d7b4af2bb3/src/assets/img/logo.png -------------------------------------------------------------------------------- /src/assets/js/dark-mode.js: -------------------------------------------------------------------------------- 1 | const darkMode = () => { 2 | // Selectors 3 | const selectors = { 4 | toggleButton: '[data-theme-switch]', 5 | } 6 | 7 | // Toggle theme 8 | const toggleTheme = () => { 9 | const themeSettings = document.documentElement.dataset; 10 | const newTheme = themeSettings.theme === 'dark' ? 'light' : 'dark'; 11 | 12 | themeSettings.theme = newTheme; 13 | localStorage.setItem('theme', newTheme); 14 | }; 15 | 16 | // Handle click event 17 | const handleClick = (event) => { 18 | const shouldToggle = event.target.closest(selectors.toggleButton); 19 | 20 | if (shouldToggle) { 21 | toggleTheme(); 22 | } 23 | }; 24 | 25 | // Event Listeners 26 | document.addEventListener('click', handleClick); 27 | } 28 | 29 | module.exports = darkMode; -------------------------------------------------------------------------------- /src/assets/js/drawer.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Drawer 3 | */ 4 | const drawer = () => { 5 | // Settings 6 | const settings = { 7 | speedOpen: 50, 8 | speedClose: 350, 9 | activeClass: 'is-active', 10 | visibleClass: 'is-visible', 11 | selectorTarget: '[data-drawer-target]', 12 | selectorTrigger: '[data-drawer-trigger]', 13 | selectorClose: '[data-drawer-close]', 14 | }; 15 | 16 | /** 17 | * Element.closest() polyfill 18 | * https://developer.mozilla.org/en-US/docs/Web/API/Element/closest#Polyfill 19 | */ 20 | if (!Element.prototype.closest) { 21 | if (!Element.prototype.matches) { 22 | Element.prototype.matches = Element.prototype.msMatchesSelector || Element.prototype.webkitMatchesSelector; 23 | } 24 | Element.prototype.closest = (s) => { 25 | const el = this; 26 | let ancestor = this; 27 | if (!document.documentElement.contains(el)) return null; 28 | do { 29 | if (ancestor.matches(s)) return ancestor; 30 | ancestor = ancestor.parentElement; 31 | } while (ancestor !== null); 32 | return null; 33 | }; 34 | } 35 | 36 | /** 37 | * Trap Focus 38 | * https://hiddedevries.nl/en/blog/2017-01-29-using-javascript-to-trap-focus-in-an-element 39 | */ 40 | const trapFocus = (element) => { 41 | const focusableEls = element.querySelectorAll('a[href]:not([disabled]), button:not([disabled]), textarea:not([disabled]), input[type="text"]:not([disabled]), input[type="radio"]:not([disabled]), input[type="checkbox"]:not([disabled]), select:not([disabled])'); 42 | const firstFocusableEl = focusableEls[0]; 43 | const lastFocusableEl = focusableEls[focusableEls.length - 1]; 44 | const KEYCODE_TAB = 9; 45 | 46 | element.addEventListener('keydown', (e) => { 47 | const isTabPressed = (e.key === 'Tab' || e.keyCode === KEYCODE_TAB); 48 | 49 | if (!isTabPressed) { 50 | return; 51 | } 52 | 53 | if ( e.shiftKey ) /* shift + tab */ { 54 | if (document.activeElement === firstFocusableEl) { 55 | lastFocusableEl.focus(); 56 | e.preventDefault(); 57 | } 58 | } else /* tab */ { 59 | if (document.activeElement === lastFocusableEl) { 60 | firstFocusableEl.focus(); 61 | e.preventDefault(); 62 | } 63 | } 64 | }); 65 | } 66 | 67 | // Toggle accessibility 68 | const toggleAccessibility = (event) => { 69 | if (event.getAttribute('aria-expanded') === 'true') { 70 | event.setAttribute('aria-expanded', false); 71 | } else { 72 | event.setAttribute('aria-expanded', true); 73 | } 74 | }; 75 | 76 | // Remove body overflow 77 | const removeOverlay = () => { 78 | document.documentElement.style.overflow = ''; 79 | }; 80 | 81 | // Open Drawer 82 | const openDrawer = (trigger) => { 83 | const target = document.getElementById(trigger.getAttribute('aria-controls')); 84 | 85 | target.classList.add(settings.activeClass); 86 | document.documentElement.style.overflow = 'hidden'; 87 | toggleAccessibility(trigger); 88 | 89 | setTimeout(() => { 90 | target.classList.add(settings.visibleClass); 91 | trapFocus(target); 92 | }, settings.speedOpen); 93 | }; 94 | 95 | // Close Drawer 96 | const closeDrawer = (event) => { 97 | const closestParent = event.closest(settings.selectorTarget); 98 | const childrenTrigger = document.querySelector('[aria-controls="' + closestParent.id + '"'); 99 | 100 | closestParent.classList.remove(settings.visibleClass); 101 | removeOverlay(); 102 | toggleAccessibility(childrenTrigger); 103 | 104 | setTimeout(function () { 105 | closestParent.classList.remove(settings.activeClass); 106 | }, settings.speedClose); 107 | }; 108 | 109 | // Click Handler 110 | const clickHandler = (event) => { 111 | const toggle = event.target; 112 | const open = toggle.closest(settings.selectorTrigger); 113 | const close = toggle.closest(settings.selectorClose); 114 | 115 | if (open) { 116 | openDrawer(open); 117 | } 118 | 119 | if (close) { 120 | closeDrawer(close); 121 | } 122 | 123 | if (open || close) { 124 | event.preventDefault(); 125 | } 126 | }; 127 | 128 | // Keydown Handler, handle Escape button 129 | const keydownHandler = (event) => { 130 | if (event.key === 'Escape' || event.keyCode === 27) { 131 | const drawers = document.querySelectorAll(settings.selectorTarget); 132 | 133 | // Find active drawers and close them when escape is clicked 134 | for (let i = 0; i < drawers.length; ++i) { 135 | if (drawers[i].classList.contains(settings.activeClass)) { 136 | closeDrawer(drawers[i]); 137 | } 138 | } 139 | } 140 | }; 141 | 142 | // Inits & Event Listeners 143 | document.addEventListener('click', clickHandler, false); 144 | document.addEventListener('keydown', keydownHandler, false); 145 | document.addEventListener('turbolinks:click', removeOverlay, false); 146 | }; 147 | 148 | module.exports = drawer; 149 | -------------------------------------------------------------------------------- /src/assets/js/main.js: -------------------------------------------------------------------------------- 1 | const Turbolinks = require('turbolinks'); 2 | const drawer = require('./drawer'); 3 | const darkMode = require('./dark-mode'); 4 | 5 | // Initialize Turbolinks 6 | Turbolinks.start(); 7 | 8 | // Initialize mobile nav drawer 9 | drawer(); 10 | 11 | // Initialize dark mode toggle 12 | const { enableThemeSwitch } = document.documentElement.dataset; 13 | 14 | if (enableThemeSwitch) { 15 | darkMode(); 16 | } 17 | 18 | // Handle Netlify Identity Login 19 | if (window.netlifyIdentity) { 20 | window.netlifyIdentity.on('init', (user) => { 21 | if (!user) { 22 | window.netlifyIdentity.on('login', () => { 23 | document.location.href = '/admin/'; 24 | }); 25 | } 26 | }); 27 | } -------------------------------------------------------------------------------- /src/assets/scss/components/drawer.scss: -------------------------------------------------------------------------------- 1 | .drawer { 2 | display: none; 3 | } 4 | 5 | .drawer__overlay { 6 | position: fixed; 7 | top: 0; 8 | right: 0; 9 | bottom: 0; 10 | left: 0; 11 | width: 100%; 12 | z-index: 200; 13 | opacity: 0; 14 | transition: opacity 0.3s; 15 | will-change: opacity; 16 | background-color: #000; 17 | user-select: none; 18 | } 19 | 20 | .drawer__header { 21 | padding: 2.5rem; 22 | display: flex; 23 | justify-content: flex-end; 24 | align-items: center; 25 | border-bottom: 1px solid var(--color-border); 26 | 27 | :root[data-theme="dark"] &{ 28 | border-bottom: 1px solid var(--color-border-dark); 29 | } 30 | } 31 | 32 | .drawer__close { 33 | display: flex; 34 | align-items: center; 35 | margin: 0; 36 | padding: 0; 37 | border: none; 38 | background-color: transparent; 39 | color: var(--color-text); 40 | cursor: pointer; 41 | width: 18px; 42 | height: 18px; 43 | flex-shrink: 0; 44 | margin-left: 1rem; 45 | 46 | svg { 47 | width: 100%; 48 | } 49 | 50 | :root[data-theme="dark"] &{ 51 | color: var(--color-text-dark); 52 | background-color:transparent 53 | } 54 | } 55 | 56 | .drawer__wrapper { 57 | position: fixed; 58 | top: 0; 59 | right: 0; 60 | bottom: 0; 61 | height: 100%; 62 | width: 100%; 63 | max-width: 340px; 64 | z-index: 9999; 65 | overflow: auto; 66 | transition: transform 0.3s; 67 | will-change: transform; 68 | background-color: #fff; 69 | display: flex; 70 | flex-direction: column; 71 | transform: translate3d(103%, 0, 0); /* extra 3% because of box-shadow */ 72 | box-shadow: 0 2px 6px #777; 73 | 74 | :root[data-theme="dark"] &{ 75 | background-color: var(--color-bg-wrapper-bg-dark); 76 | box-shadow: none; 77 | } 78 | } 79 | 80 | .drawer__content { 81 | position: relative; 82 | overflow-x: hidden; 83 | overflow-y: auto; 84 | height: 100%; 85 | flex-grow: 1; 86 | padding: 1.5rem; 87 | } 88 | 89 | .drawer--left .drawer__wrapper { 90 | left: 0; 91 | right: auto; 92 | transform: translate3d(-100%, 0, 0); 93 | } 94 | 95 | .drawer.is-active { 96 | display: block; 97 | } 98 | 99 | .drawer.is-visible .drawer__wrapper { 100 | transform: translate3d(0, 0, 0); 101 | } 102 | 103 | .drawer.is-visible .drawer__overlay { 104 | opacity: 0.5; 105 | } 106 | 107 | .drawer__nav { 108 | padding-left: 0; 109 | list-style: none; 110 | } 111 | 112 | .drawer__nav-item { 113 | margin: 0; 114 | padding: 15px 0; 115 | border-bottom: 1px solid var(--color-border); 116 | font-size: 1.75em; 117 | font-weight: 700; 118 | 119 | :root[data-theme="dark"] &{ 120 | border-bottom: 1px solid var(--color-border-dark); 121 | } 122 | } -------------------------------------------------------------------------------- /src/assets/scss/components/site-footer.scss: -------------------------------------------------------------------------------- 1 | .site-footer { 2 | display: flex; 3 | flex-wrap: wrap; 4 | align-items: center; 5 | justify-content: center; 6 | padding: 15px; 7 | border-top: 1px solid var(--color-border); 8 | 9 | @media (min-width: 1024px) { 10 | padding: 15px 30px; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/assets/scss/components/site-header.scss: -------------------------------------------------------------------------------- 1 | .site-header { 2 | position: fixed; 3 | width: 100%; 4 | border-top: 3px solid var(--color-secondary); 5 | z-index: var(--z-header); 6 | 7 | :root[data-theme="dark"] & { 8 | border-color: var(--color-bg-accent-dark); 9 | } 10 | } 11 | 12 | .site-header__container { 13 | display: flex; 14 | flex-wrap: wrap; 15 | align-items: center; 16 | justify-content: space-between; 17 | margin: 3px 12px 0; 18 | padding: 8px 10px; 19 | border: 1px solid var(--color-border); 20 | border-radius: var(--border-radius); 21 | background-color: var(--color-bg); 22 | box-shadow: var(--shadow); 23 | 24 | @media (min-width: 1024px) { 25 | max-width: var(--site-width); 26 | margin: 3px auto 0; 27 | } 28 | 29 | :root[data-theme="dark"] & { 30 | background-color: var(--color-bg-secondary-dark); 31 | } 32 | } 33 | 34 | .site-header__logo-container { 35 | margin: 0; 36 | font-weight: 700; 37 | } 38 | 39 | .site-header__logo { 40 | display: flex; 41 | align-items: center; 42 | justify-content: center; 43 | width: 30px; 44 | height: 30px; 45 | padding: 6px 8px; 46 | border-radius: var(--border-radius-sm); 47 | color: var(--color-text); 48 | 49 | @media (min-width: 1024px) { 50 | width: 40px; 51 | height: 40px; 52 | } 53 | 54 | &:hover { 55 | border-bottom: none; 56 | background-color: var(--color-bg-secondary); 57 | color: var(--color-secondary); 58 | 59 | :root[data-theme="dark"] & { 60 | background-color: var(--color-bg-wrapper-bg-dark); 61 | color: var(--color-title-dark); 62 | } 63 | } 64 | 65 | :root[data-theme="dark"] & { 66 | color: var(--color-title-dark); 67 | } 68 | } 69 | 70 | .site-header__nav { 71 | display: none; 72 | width: 100%; 73 | margin-top: 15px; 74 | 75 | @media (min-width: 1024px) { 76 | display: block; 77 | width: auto; 78 | margin-top: 0; 79 | } 80 | } 81 | 82 | .site-header__mobile-nav { 83 | display: flex; 84 | align-items: center; 85 | border: none; 86 | background: none; 87 | color: var(--color-text); 88 | transition: opacity 0.2s ease; 89 | 90 | &:hover { 91 | opacity: 0.8; 92 | } 93 | 94 | svg { 95 | width: 20px; 96 | height: auto; 97 | } 98 | 99 | @media (min-width: 1024px) { 100 | display: none; 101 | } 102 | 103 | :root[data-theme="dark"] & { 104 | color: var(--color-text-dark); 105 | background-color: var(--color-bg-secondary-dark); 106 | } 107 | } 108 | 109 | .site-header__mobile-nav-label { 110 | margin-right: 12px; 111 | } 112 | 113 | .site-header__items { 114 | display: flex; 115 | align-items: center; 116 | flex-wrap: wrap; 117 | justify-content: center; 118 | margin: 0; 119 | padding: 0; 120 | list-style: none; 121 | } 122 | 123 | .site-header__item { 124 | margin: 0 6px; 125 | font-weight: 500; 126 | font-size: 14px; 127 | 128 | @media (min-width: 1024px) { 129 | margin: 0 15px; 130 | font-size: 15px; 131 | } 132 | } 133 | 134 | .site-header__link { 135 | display: flex; 136 | align-items: center; 137 | padding: 6px 8px; 138 | border-radius: var(--border-radius-sm); 139 | color: var(--color-text); 140 | 141 | &:hover { 142 | border-bottom: none; 143 | background-color: var(--color-bg-secondary); 144 | color: var(--color-secondary); 145 | 146 | :root[data-theme="dark"] & { 147 | background-color: var(--color-bg-wrapper-bg-dark); 148 | color: var(--color-title-dark); 149 | } 150 | } 151 | 152 | &[aria-current] { 153 | background-color: var(--color-bg-secondary); 154 | color: var(--color-secondary); 155 | 156 | :root[data-theme="dark"] & { 157 | background-color: var(--color-bg-wrapper-bg-dark); 158 | color: var(--color-title-dark); 159 | } 160 | } 161 | 162 | :root[data-theme="dark"] & { 163 | color: var(--color-title-dark); 164 | } 165 | 166 | &--theme-switch { 167 | border: none; 168 | background: none; 169 | 170 | :root[data-theme="dark"] & { 171 | background: none; 172 | } 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /src/assets/scss/global/base-styles.scss: -------------------------------------------------------------------------------- 1 | /* Modified version of Sakura.css v1.3.1 2 | * ================ 3 | * Minimal css theme. 4 | * Project: https://github.com/oxalorg/sakura/ 5 | */ 6 | 7 | /* Body */ 8 | html { 9 | scroll-padding-top: 100px; 10 | font-size: 62.5%; 11 | font-family: var(--font-family-base); 12 | } 13 | 14 | body { 15 | display: flex; 16 | flex-direction: column; 17 | min-height: 100vh; 18 | margin: 0; 19 | font-size: 1.8rem; 20 | line-height: 1.618; 21 | color: var(--color-text); 22 | background-color: var(--color-bg); 23 | 24 | :root[data-theme="dark"] & { 25 | background-color: var(--color-bg-dark); 26 | color: var(--color-text-dark); 27 | } 28 | } 29 | 30 | .wrapper-bg-wrapper { 31 | display:none; 32 | 33 | :root[data-theme="dark"] & { 34 | display:flex; 35 | position:fixed; 36 | height:100%; 37 | width:100%; 38 | z-index:-1; 39 | justify-content: center; 40 | 41 | .wrapper-bg{ 42 | width:calc(var(--site-width) + 100px); 43 | background-color: var(--color-bg-wrapper-bg-dark); 44 | border-right: 1px solid var(--color-bg-secondary-dark); 45 | border-left: 1px solid var(--color-bg-secondary-dark); 46 | } 47 | } 48 | } 49 | 50 | main { 51 | padding: var(--header-height) 13px 13px; 52 | 53 | @media (min-width: 1024px) { 54 | padding-top: var(--header-height-lg); 55 | } 56 | } 57 | 58 | footer { 59 | margin-top: auto; 60 | } 61 | 62 | h1, 63 | h2, 64 | h3, 65 | h4, 66 | h5, 67 | h6 { 68 | line-height: 1.1; 69 | font-family: var(--font-family-heading); 70 | font-weight: 800; 71 | margin-top: 3rem; 72 | margin-bottom: 1.5rem; 73 | overflow-wrap: break-word; 74 | word-wrap: break-word; 75 | -ms-word-break: break-all; 76 | word-break: break-word; 77 | 78 | &:focus, &:hover { 79 | .header-anchor { 80 | border-bottom: none; 81 | opacity: 1; 82 | } 83 | } 84 | 85 | :root[data-theme="dark"] & { 86 | color: var(--color-title-dark); 87 | } 88 | } 89 | 90 | h1 { 91 | font-size: 2em; 92 | } 93 | 94 | h2 { 95 | font-size: 1.75em; 96 | } 97 | 98 | h3 { 99 | font-size: 1.5em; 100 | } 101 | 102 | h4 { 103 | font-size: 1.25em; 104 | } 105 | 106 | h5 { 107 | font-size: 1em; 108 | } 109 | 110 | h6 { 111 | font-size: 0.8em; 112 | } 113 | 114 | p { 115 | margin-top: 0; 116 | margin-bottom: 2.5rem; 117 | } 118 | 119 | small, 120 | sub, 121 | sup { 122 | font-size: 75%; 123 | } 124 | 125 | hr { 126 | border:none; 127 | border-bottom: 2px solid var(--color-border); 128 | 129 | :root[data-theme="dark"] & { 130 | border-color: var(--color-border-dark); 131 | } 132 | } 133 | 134 | a { 135 | text-decoration: none; 136 | color: var(--color-secondary); 137 | 138 | :root[data-theme="dark"] & { 139 | color: var(--color-link-dark); 140 | } 141 | 142 | &:hover { 143 | color: var(--color-secondary); 144 | border-bottom: 2px solid var(--color-secondary); 145 | 146 | :root[data-theme="dark"] & { 147 | color: var(--color-link-dark); 148 | border-color: var(--color-link-dark); 149 | } 150 | } 151 | } 152 | 153 | ul { 154 | padding-left: 1.4em; 155 | margin-top: 0; 156 | margin-bottom: 2.5rem; 157 | } 158 | 159 | li { 160 | margin-bottom: 0.4em; 161 | } 162 | 163 | blockquote { 164 | margin-left: 0; 165 | margin-right: 0; 166 | padding-left: 1em; 167 | padding-top: 0.8em; 168 | padding-bottom: 0.8em; 169 | padding-right: 0.8em; 170 | border-left: 5px solid var(--color-primary); 171 | margin-bottom: 2.5rem; 172 | background-color: var(--color-bg-secondary); 173 | } 174 | 175 | blockquote h1, 176 | blockquote h2, 177 | blockquote h3, 178 | blockquote h4, 179 | blockquote h5, 180 | blockquote h6 { 181 | margin: 0.4em 0; 182 | } 183 | 184 | blockquote p { 185 | margin-bottom: 0; 186 | } 187 | 188 | img, 189 | video { 190 | height: auto; 191 | max-width: 100%; 192 | margin-top: 0; 193 | margin-bottom: 2.5rem; 194 | } 195 | 196 | /* Pre and Code */ 197 | pre { 198 | background-color: var(--color-bg-secondary); 199 | display: block; 200 | padding: 1em; 201 | overflow-x: auto; 202 | margin-top: 0; 203 | margin-bottom: 2.5rem; 204 | 205 | :root[data-theme="dark"] & { 206 | background-color: var(--color-bg-secondary-dark); 207 | } 208 | } 209 | 210 | code { 211 | font-size: 0.9em; 212 | padding: 0 0.5em; 213 | background-color: var(--color-bg-secondary); 214 | white-space: pre-wrap; 215 | 216 | :root[data-theme="dark"] & { 217 | background-color: var(--color-bg-secondary-dark); 218 | } 219 | } 220 | 221 | pre > code { 222 | padding: 0; 223 | background-color: transparent; 224 | white-space: pre; 225 | } 226 | 227 | /* Tables */ 228 | table { 229 | text-align: justify; 230 | width: 100%; 231 | border-collapse: collapse; 232 | } 233 | 234 | td, 235 | th { 236 | padding: 0.5em; 237 | border-bottom: 1px solid var(--color-bg-secondary); 238 | } 239 | 240 | /* Buttons, forms and input */ 241 | input, 242 | textarea { 243 | border: 1px solid var(--color-text); 244 | } 245 | 246 | input:focus, 247 | textarea:focus { 248 | border: 1px solid var(--color-primary); 249 | } 250 | 251 | textarea { 252 | width: 100%; 253 | max-width: 100%; 254 | min-height: 35px; 255 | } 256 | 257 | .btn, 258 | button, 259 | input[type='submit'], 260 | input[type='reset'], 261 | input[type='button'] { 262 | display: inline-block; 263 | padding: 5px 10px; 264 | text-align: center; 265 | text-decoration: none; 266 | white-space: nowrap; 267 | background-color: var(--color-primary); 268 | color: var(--color-bg); 269 | border-radius: 1px; 270 | cursor: pointer; 271 | box-sizing: border-box; 272 | 273 | :root[data-theme="dark"] & { 274 | color: var(--color-title-dark); 275 | background-color: var(--color-link-dark); 276 | } 277 | } 278 | 279 | .btn[disabled], 280 | button[disabled], 281 | input[type='submit'][disabled], 282 | input[type='reset'][disabled], 283 | input[type='button'][disabled] { 284 | cursor: default; 285 | opacity: 0.5; 286 | } 287 | 288 | .btn:focus:enabled, 289 | .btn:hover:enabled, 290 | button:focus:enabled, 291 | button:hover:enabled, 292 | input[type='submit']:focus:enabled, 293 | input[type='submit']:hover:enabled, 294 | input[type='reset']:focus:enabled, 295 | input[type='reset']:hover:enabled, 296 | input[type='button']:focus:enabled, 297 | input[type='button']:hover:enabled { 298 | outline: 0; 299 | } 300 | 301 | textarea, 302 | select, 303 | input { 304 | color: var(--color-text); 305 | padding: 6px 10px; 306 | 307 | /* The 6px vertically centers text on FF, ignored by Webkit */ 308 | margin-bottom: 10px; 309 | background-color: var(--color-bg-secondary); 310 | border: 1px solid var(--color-bg-secondary); 311 | border-radius: 4px; 312 | box-shadow: none; 313 | box-sizing: border-box; 314 | font-family: var(--font-family-base); 315 | 316 | &:focus { 317 | border: 1px solid var(--color-primary); 318 | outline: 0; 319 | } 320 | 321 | :root[data-theme="dark"] & { 322 | background-color: var(--color-bg-secondary-dark); 323 | border: 1px solid var(--color-bg-secondary-dark); 324 | color: var(--color-text-dark); 325 | } 326 | } 327 | 328 | input[type='checkbox']:focus { 329 | outline: 1px dotted var(--color-primary); 330 | } 331 | 332 | label, 333 | legend, 334 | fieldset { 335 | display: block; 336 | margin-bottom: 0.5rem; 337 | font-weight: 600; 338 | 339 | :root[data-theme="dark"] & { 340 | color: var(--color-title-dark); 341 | } 342 | } 343 | -------------------------------------------------------------------------------- /src/assets/scss/global/utilites.scss: -------------------------------------------------------------------------------- 1 | .page-container { 2 | max-width: var(--site-width); 3 | margin-right: auto; 4 | margin-left: auto; 5 | padding: 1rem; 6 | 7 | img { 8 | display: flex; 9 | width: 100%; 10 | max-width: 700px; 11 | margin: 4rem auto; 12 | } 13 | } 14 | 15 | .shadow { 16 | box-shadow: var(--shadow); 17 | } 18 | 19 | .gradient-text { 20 | background: linear-gradient(130deg, #5183f5, #af002d 41.07%, #c79191 76.05%); 21 | background-clip: text; 22 | -webkit-background-clip: text; 23 | -webkit-text-fill-color: transparent; 24 | } 25 | 26 | .btn { 27 | padding: 10px 15px; 28 | border: none; 29 | border-radius: var(--border-radius-sm); 30 | background-color: var(--color-secondary); 31 | color: var(--color-text-alt); 32 | font-size: 14px; 33 | white-space: nowrap; 34 | 35 | &:hover, &:focus { 36 | border: none; 37 | filter: brightness(90%); 38 | color: var(--color-text-alt); 39 | } 40 | } 41 | 42 | .btn--sm { 43 | padding: 6px 8px; 44 | } 45 | 46 | .btn--outline { 47 | border: 1px solid var(--color-border); 48 | background-color: var(--color-bg); 49 | color: var(--color-text); 50 | 51 | :root[data-theme="dark"] & { 52 | border-color: var(--color-bg-secondary-dark); 53 | background-color: var(--color-bg-secondary-dark); 54 | color: var(--color-text-dark); 55 | } 56 | 57 | &:hover, &:focus { 58 | border: 1px solid var(--color-secondary); 59 | filter: none; 60 | background-color: var(--color-bg); 61 | color: var(--color-secondary); 62 | 63 | :root[data-theme="dark"] & { 64 | border: 1px solid var(--color-bg-secondary-dark); 65 | background-color: var(--color-bg-wrapper-bg-dark); 66 | color: var(--color-title-dark); 67 | } 68 | } 69 | } 70 | 71 | .btn--danger { 72 | background-color: var(--color-danger); 73 | color: var(--color-text-alt); 74 | 75 | :root[data-theme="dark"] & { 76 | background-color: var(--color-danger); 77 | color: var(--color-text-alt); 78 | } 79 | } 80 | 81 | .btn--danger:hover, 82 | .btn--danger:focus { 83 | border: none; 84 | background-color: var(--color-danger); 85 | color: var(--color-text-alt); 86 | filter: brightness(90%); 87 | 88 | :root[data-theme="dark"] & { 89 | color: var(--color-text-alt); 90 | } 91 | } 92 | 93 | .header-anchor { 94 | float: left; 95 | font-size: .85em; 96 | padding-right: 0.23em; 97 | margin-top: 0.125em; 98 | 99 | @media (min-width: 1024px) { 100 | margin-left: -0.87em; 101 | opacity: 0; 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/assets/scss/global/variables.scss: -------------------------------------------------------------------------------- 1 | :root { 2 | /* Brand Colors */ 3 | --color-primary: #222; 4 | --color-secondary: #2f5bc2; 5 | 6 | /* Background Colors */ 7 | --color-bg: #fff; 8 | --color-bg-secondary: #f7f7f7; 9 | --color-bg-accent: rgb(174, 182, 225); 10 | --color-bg-hover: #f3f4f6cc; 11 | 12 | /* Text and Border Colors */ 13 | --color-text: #222; 14 | --color-text-alt: #fff; 15 | --color-text-gray: #6b7280; 16 | --color-border: rgb(0 0 0 / 6%); 17 | 18 | /* Status Colors */ 19 | --color-danger: #e03e3e; 20 | 21 | /* Typography */ 22 | --font-family-base: InterVariable, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica Neue, Arial, Noto Sans, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji; 23 | --font-family-heading: var(--font-family-base); 24 | 25 | /* Sizes */ 26 | --site-width: 850px; 27 | --header-height: 66px; 28 | --header-height-lg: 76px; 29 | 30 | /* Z-index */ 31 | --z-header: 10; 32 | 33 | /* Other */ 34 | --border-radius: 12px; 35 | --border-radius-sm: 5px; 36 | --shadow: 0 10px 15px -3px rgb(0 0 0 / 10%), 0 4px 6px -2px rgb(0 0 0 / 5%); 37 | 38 | // Dark mode colors 39 | --color-secondary-dark: rgb(22, 78, 99); 40 | 41 | --color-link-dark: rgb(8, 145, 178); 42 | 43 | --color-bg-dark: #000; 44 | --color-bg-wrapper-bg-dark: rgb(24 24 27); 45 | --color-bg-secondary-dark: rgb(42 42 43); 46 | --color-bg-accent-dark: rgb(8, 51, 68); 47 | 48 | --color-title-dark: rgb(244 244 245); 49 | --color-text-dark: rgb(161 161 170); 50 | 51 | --color-border-dark: rgb(39, 39, 42); 52 | 53 | } 54 | -------------------------------------------------------------------------------- /src/assets/scss/layouts/blog.scss: -------------------------------------------------------------------------------- 1 | /* Article card */ 2 | .article-card { 3 | margin-top: 20px; 4 | padding: 4px 6px; 5 | border-radius: var(--border-radius); 6 | 7 | &:hover { 8 | color: var(--color-text); 9 | background-color: var(--color-bg-hover); 10 | 11 | :root[data-theme="dark"] & { 12 | background-color: var(--color-bg-secondary-dark); 13 | } 14 | } 15 | 16 | &--tags { 17 | border-bottom: 2px solid var(--color-border); 18 | border-radius: 0; 19 | 20 | :root[data-theme="dark"] & { 21 | border-bottom: 2px solid var(--color-border-dark); 22 | } 23 | 24 | &:last-child { 25 | border-bottom: none; 26 | } 27 | 28 | &:hover { 29 | background-color: transparent; 30 | 31 | :root[data-theme="dark"] & { 32 | background-color: transparent; 33 | } 34 | } 35 | } 36 | } 37 | 38 | .article-card__link { 39 | color: var(--color-text); 40 | 41 | &:hover { 42 | color: var(--color-text); 43 | } 44 | 45 | .article-card--tags & { 46 | &:hover { 47 | color: var(--color-secondary); 48 | } 49 | } 50 | } 51 | 52 | .article-card__title { 53 | margin: 5px 0; 54 | } 55 | 56 | .article-card__summary { 57 | margin-bottom: 0; 58 | color: var(--color-text-gray); 59 | 60 | :root[data-theme="dark"] & { 61 | color: var(--color-text-dark); 62 | } 63 | } 64 | 65 | /* Tags */ 66 | .tag-list { 67 | display: flex; 68 | flex-wrap: wrap; 69 | } 70 | 71 | .tag-list--no-wrap { 72 | display: flex; 73 | flex-wrap: wrap; 74 | align-items: center; 75 | padding: 40px 0 30px; 76 | border-bottom: 2px solid var(--color-border); 77 | 78 | @media (min-width: 1024px) { 79 | flex-wrap: nowrap; 80 | justify-content: space-between; 81 | } 82 | } 83 | 84 | .tag-list__title { 85 | width: 100%; 86 | margin-top: 0; 87 | margin-right: 20px; 88 | margin-bottom: 20px; 89 | line-height: 1.4em; 90 | 91 | @media (min-width: 1024px) { 92 | margin-bottom: 0; 93 | } 94 | } 95 | 96 | .tag { 97 | margin: 10px 10px 0 0; 98 | padding: 6px 8px; 99 | background-color: var(--color-bg-secondary); 100 | border-radius: var(--border-radius-sm); 101 | color: var(--color-secondary); 102 | white-space: nowrap; 103 | font-size: 13px; 104 | font-weight: 500; 105 | 106 | &:hover { 107 | border: none; 108 | } 109 | 110 | &:first-child { 111 | margin-left: 0; 112 | } 113 | 114 | :root[data-theme="dark"] & { 115 | background-color: var(--color-bg-secondary-dark); 116 | color: var(--color-link-dark); 117 | } 118 | } 119 | 120 | /* Pagination */ 121 | .pagination__list { 122 | display: flex; 123 | align-items: center; 124 | justify-content: center; 125 | margin: 0; 126 | list-style: none; 127 | gap: 25px; 128 | } 129 | 130 | .pagination__link { 131 | color: var(--color-text); 132 | 133 | &[aria-current] { 134 | color: var(--color-secondary); 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/assets/scss/layouts/contact.scss: -------------------------------------------------------------------------------- 1 | .contact-form__submit { 2 | display: block; 3 | } 4 | -------------------------------------------------------------------------------- /src/assets/scss/layouts/home.scss: -------------------------------------------------------------------------------- 1 | .home-page { 2 | max-width: var(--site-width); 3 | margin: 0 auto 60px; 4 | padding: 0 20px 20px; 5 | 6 | @media (min-width: 1024px) { 7 | padding: 20px 40px; 8 | } 9 | } 10 | 11 | .home-page__bg-square { 12 | display: none; 13 | 14 | @media (min-width: 1024px) { 15 | display: block; 16 | position: absolute; 17 | left: auto; 18 | top: 95px; 19 | right: 0; 20 | bottom: auto; 21 | width: 100%; 22 | height: 100%; 23 | max-height: 60%; 24 | max-width: 35%; 25 | min-width: 300px; 26 | border-radius: var(--border-radius); 27 | background-color: var(--color-bg-accent); 28 | z-index: -1; 29 | 30 | :root[data-theme="dark"] & { 31 | background-color: var(--color-bg-accent-dark); 32 | } 33 | } 34 | } 35 | 36 | .hero { 37 | display: grid; 38 | grid-auto-columns: 1fr; 39 | grid-template-rows: auto auto; 40 | column-gap: 70px; 41 | max-width: 1100px; 42 | margin: 0 auto; 43 | 44 | @media (min-width: 1024px) { 45 | grid-template-columns: 3fr 2fr; 46 | row-gap: 20px; 47 | margin: 50px auto; 48 | } 49 | } 50 | 51 | .hero__col { 52 | display: flex; 53 | flex-direction: column; 54 | justify-content: center; 55 | } 56 | 57 | .hero__image { 58 | max-width: 300px; 59 | border-radius: var(--border-radius); 60 | transition: filter 0.4s ease-out; 61 | 62 | @media (min-width: 1024px) { 63 | margin-bottom: 0; 64 | } 65 | } 66 | 67 | .hero__title { 68 | @media (min-width: 1024px) { 69 | margin-top: 0; 70 | font-size: 3em; 71 | } 72 | } 73 | 74 | .hero__body { 75 | margin-top: 0; 76 | margin-bottom: 1rem; 77 | font-size: 1.2em; 78 | 79 | > *:first-child { 80 | margin-top: 0; 81 | } 82 | } 83 | 84 | .list-header { 85 | display: flex; 86 | align-items: center; 87 | justify-content: space-between; 88 | gap: 20px; 89 | margin-bottom: 20px; 90 | padding-top: 20px; 91 | border-top: 2px solid (var(--color-border)); 92 | 93 | :root[data-theme="dark"] & { 94 | border-top: 1px solid (var(--color-border-dark)); 95 | } 96 | } 97 | 98 | .list-header__title { 99 | margin: 0; 100 | } 101 | -------------------------------------------------------------------------------- /src/assets/scss/layouts/projects.scss: -------------------------------------------------------------------------------- 1 | .project-grid { 2 | display: grid; 3 | grid-template-columns: repeat(1, 1fr); 4 | gap: 40px; 5 | margin: 40px 0; 6 | 7 | @media (min-width: 1024px) { 8 | grid-template-columns: repeat(2, 1fr); 9 | } 10 | } 11 | 12 | .project-card { 13 | display: block; 14 | display: flex; 15 | flex-direction: column; 16 | justify-content: center; 17 | padding: 15px 25px; 18 | border: 1px solid var(--color-border); 19 | border-radius: var(--border-radius); 20 | background-color: var(--color-bg); 21 | box-shadow: var(--shadow); 22 | 23 | :root[data-theme="dark"] & { 24 | background-color: var(--color-bg-secondary-dark); 25 | } 26 | } 27 | 28 | .project-card__title { 29 | color: var(--color-text); 30 | 31 | :root[data-theme="dark"] & { 32 | color: var(--color-title-dark); 33 | 34 | &:hover { 35 | color: var(--color-link-dark); 36 | border-color: var(--color-link-dark); 37 | } 38 | } 39 | } 40 | 41 | .project-card__tag { 42 | background-color: var(--color-secondary); 43 | color: var(--color-text-alt); 44 | 45 | :root[data-theme="dark"] & { 46 | background-color: var(--color-secondary-dark); 47 | color: var(--color-text-alt); 48 | } 49 | 50 | &:hover { 51 | color: var(--color-text-alt); 52 | filter: brightness(90%); 53 | 54 | :root[data-theme="dark"] & { 55 | color: var(--color-text-alt); 56 | } 57 | } 58 | } 59 | 60 | .project-card__emoji { 61 | margin-right: 8px; 62 | } 63 | -------------------------------------------------------------------------------- /src/assets/scss/main.scss: -------------------------------------------------------------------------------- 1 | 2 | /* Third Party */ 3 | @use 'third-party/normalize.scss'; 4 | @use 'third-party/prisma-theme.scss'; 5 | 6 | /* Global */ 7 | @use 'global/variables.scss'; 8 | @use 'global/base-styles.scss'; 9 | @use 'global/utilites.scss'; 10 | 11 | /* Layout */ 12 | @use 'layouts/home.scss'; 13 | @use 'layouts/blog.scss'; 14 | @use 'layouts/contact.scss'; 15 | @use 'layouts/projects.scss'; 16 | 17 | /* Components */ 18 | @use 'components/site-header.scss'; 19 | @use 'components/site-footer.scss'; 20 | @use 'components/drawer.scss'; 21 | -------------------------------------------------------------------------------- /src/assets/scss/third-party/normalize.scss: -------------------------------------------------------------------------------- 1 | /*! normalize.css v5.0.0 | MIT License | github.com/necolas/normalize.css */ 2 | 3 | /** 4 | * 1. Change the default font family in all browsers (opinionated). 5 | * 2. Correct the line height in all browsers. 6 | * 3. Prevent adjustments of font size after orientation changes in 7 | * IE on Windows Phone and in iOS. 8 | */ 9 | 10 | /* Document 11 | ========================================================================== */ 12 | 13 | html { 14 | font-family: sans-serif; /* 1 */ 15 | line-height: 1.15; /* 2 */ 16 | text-size-adjust: 100%; /* 3 */ 17 | text-size-adjust: 100%; /* 3 */ 18 | } 19 | 20 | /* Sections 21 | ========================================================================== */ 22 | 23 | /** 24 | * Remove the margin in all browsers (opinionated). 25 | */ 26 | 27 | body { 28 | margin: 0; 29 | } 30 | 31 | /** 32 | * Add the correct display in IE 9-. 33 | */ 34 | 35 | article, 36 | aside, 37 | footer, 38 | header, 39 | nav, 40 | section { 41 | display: block; 42 | } 43 | 44 | /** 45 | * Correct the font size and margin on `h1` elements within `section` and 46 | * `article` contexts in Chrome, Firefox, and Safari. 47 | */ 48 | 49 | h1 { 50 | font-size: 2em; 51 | margin: 0.67em 0; 52 | } 53 | 54 | /* Grouping content 55 | ========================================================================== */ 56 | 57 | /** 58 | * Add the correct display in IE 9-. 59 | * 1. Add the correct display in IE. 60 | */ 61 | 62 | figcaption, 63 | figure, 64 | main { 65 | /* 1 */ 66 | display: block; 67 | } 68 | 69 | /** 70 | * Add the correct margin in IE 8. 71 | */ 72 | 73 | figure { 74 | margin: 1em 40px; 75 | } 76 | 77 | /** 78 | * 1. Add the correct box sizing in Firefox. 79 | * 2. Show the overflow in Edge and IE. 80 | */ 81 | 82 | hr { 83 | box-sizing: content-box; /* 1 */ 84 | height: 0; /* 1 */ 85 | overflow: visible; /* 2 */ 86 | } 87 | 88 | /** 89 | * 1. Correct the inheritance and scaling of font size in all browsers. 90 | * 2. Correct the odd `em` font sizing in all browsers. 91 | */ 92 | 93 | pre { 94 | font-family: monospace, monospace; /* 1 */ 95 | font-size: 1em; /* 2 */ 96 | } 97 | 98 | /* Text-level semantics 99 | ========================================================================== */ 100 | 101 | /** 102 | * 1. Remove the gray background on active links in IE 10. 103 | * 2. Remove gaps in links underline in iOS 8+ and Safari 8+. 104 | */ 105 | 106 | a { 107 | background-color: transparent; /* 1 */ 108 | text-decoration-skip: objects; /* 2 */ 109 | } 110 | 111 | /** 112 | * Remove the outline on focused links when they are also active or hovered 113 | * in all browsers (opinionated). 114 | */ 115 | 116 | a:active, 117 | a:hover { 118 | outline-width: 0; 119 | } 120 | 121 | /** 122 | * 1. Remove the bottom border in Firefox 39-. 123 | * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari. 124 | */ 125 | 126 | abbr[title] { 127 | border-bottom: none; /* 1 */ 128 | text-decoration: underline; /* 2 */ 129 | text-decoration: underline dotted; /* 2 */ 130 | } 131 | 132 | /** 133 | * Prevent the duplicate application of `bolder` by the next rule in Safari 6. 134 | */ 135 | 136 | b, 137 | strong { 138 | font-weight: inherit; 139 | } 140 | 141 | /** 142 | * Add the correct font weight in Chrome, Edge, and Safari. 143 | */ 144 | 145 | b, 146 | strong { 147 | font-weight: bolder; 148 | } 149 | 150 | /** 151 | * 1. Correct the inheritance and scaling of font size in all browsers. 152 | * 2. Correct the odd `em` font sizing in all browsers. 153 | */ 154 | 155 | code, 156 | kbd, 157 | samp { 158 | font-family: monospace, monospace; /* 1 */ 159 | font-size: 1em; /* 2 */ 160 | } 161 | 162 | /** 163 | * Add the correct font style in Android 4.3-. 164 | */ 165 | 166 | dfn { 167 | font-style: italic; 168 | } 169 | 170 | /** 171 | * Add the correct background and color in IE 9-. 172 | */ 173 | 174 | mark { 175 | background-color: #ff0; 176 | color: #000; 177 | } 178 | 179 | /** 180 | * Add the correct font size in all browsers. 181 | */ 182 | 183 | small { 184 | font-size: 80%; 185 | } 186 | 187 | /** 188 | * Prevent `sub` and `sup` elements from affecting the line height in 189 | * all browsers. 190 | */ 191 | 192 | sub, 193 | sup { 194 | font-size: 75%; 195 | line-height: 0; 196 | position: relative; 197 | vertical-align: baseline; 198 | } 199 | 200 | sub { 201 | bottom: -0.25em; 202 | } 203 | 204 | sup { 205 | top: -0.5em; 206 | } 207 | 208 | /* Embedded content 209 | ========================================================================== */ 210 | 211 | /** 212 | * Add the correct display in IE 9-. 213 | */ 214 | 215 | audio, 216 | video { 217 | display: inline-block; 218 | } 219 | 220 | /** 221 | * Add the correct display in iOS 4-7. 222 | */ 223 | 224 | audio:not([controls]) { 225 | display: none; 226 | height: 0; 227 | } 228 | 229 | /** 230 | * Remove the border on images inside links in IE 10-. 231 | */ 232 | 233 | img { 234 | border-style: none; 235 | } 236 | 237 | /** 238 | * Hide the overflow in IE. 239 | */ 240 | 241 | svg:not(:root) { 242 | overflow: hidden; 243 | } 244 | 245 | /* Forms 246 | ========================================================================== */ 247 | 248 | /** 249 | * 1. Change the font styles in all browsers (opinionated). 250 | * 2. Remove the margin in Firefox and Safari. 251 | */ 252 | 253 | button, 254 | input, 255 | optgroup, 256 | select, 257 | textarea { 258 | font-family: sans-serif; /* 1 */ 259 | font-size: 100%; /* 1 */ 260 | line-height: 1.15; /* 1 */ 261 | margin: 0; /* 2 */ 262 | } 263 | 264 | /** 265 | * Show the overflow in IE. 266 | * 1. Show the overflow in Edge. 267 | */ 268 | 269 | button, 270 | input { 271 | /* 1 */ 272 | overflow: visible; 273 | } 274 | 275 | /** 276 | * Remove the inheritance of text transform in Edge, Firefox, and IE. 277 | * 1. Remove the inheritance of text transform in Firefox. 278 | */ 279 | 280 | button, 281 | select { 282 | /* 1 */ 283 | text-transform: none; 284 | } 285 | 286 | /** 287 | * 1. Prevent a WebKit bug where (2) destroys native `audio` and `video` 288 | * controls in Android 4. 289 | * 2. Correct the inability to style clickable types in iOS and Safari. 290 | */ 291 | 292 | button, 293 | html [type="button"], /* 1 */ 294 | [type="reset"], 295 | [type="submit"] { 296 | appearance: button; /* 2 */ 297 | } 298 | 299 | /** 300 | * Remove the inner border and padding in Firefox. 301 | */ 302 | 303 | button::-moz-focus-inner, 304 | [type='button']::-moz-focus-inner, 305 | [type='reset']::-moz-focus-inner, 306 | [type='submit']::-moz-focus-inner { 307 | border-style: none; 308 | padding: 0; 309 | } 310 | 311 | /** 312 | * Restore the focus styles unset by the previous rule. 313 | */ 314 | 315 | button:-moz-focusring, 316 | [type='button']:-moz-focusring, 317 | [type='reset']:-moz-focusring, 318 | [type='submit']:-moz-focusring { 319 | outline: 1px dotted ButtonText; 320 | } 321 | 322 | /** 323 | * Change the border, margin, and padding in all browsers (opinionated). 324 | */ 325 | 326 | fieldset { 327 | border: 1px solid #c0c0c0; 328 | margin: 0 2px; 329 | padding: 0.35em 0.625em 0.75em; 330 | } 331 | 332 | /** 333 | * 1. Correct the text wrapping in Edge and IE. 334 | * 2. Correct the color inheritance from `fieldset` elements in IE. 335 | * 3. Remove the padding so developers are not caught out when they zero out 336 | * `fieldset` elements in all browsers. 337 | */ 338 | 339 | legend { 340 | box-sizing: border-box; /* 1 */ 341 | color: inherit; /* 2 */ 342 | display: table; /* 1 */ 343 | max-width: 100%; /* 1 */ 344 | padding: 0; /* 3 */ 345 | white-space: normal; /* 1 */ 346 | } 347 | 348 | /** 349 | * 1. Add the correct display in IE 9-. 350 | * 2. Add the correct vertical alignment in Chrome, Firefox, and Opera. 351 | */ 352 | 353 | progress { 354 | display: inline-block; /* 1 */ 355 | vertical-align: baseline; /* 2 */ 356 | } 357 | 358 | /** 359 | * Remove the default vertical scrollbar in IE. 360 | */ 361 | 362 | textarea { 363 | overflow: auto; 364 | } 365 | 366 | /** 367 | * 1. Add the correct box sizing in IE 10-. 368 | * 2. Remove the padding in IE 10-. 369 | */ 370 | 371 | [type='checkbox'], 372 | [type='radio'] { 373 | box-sizing: border-box; /* 1 */ 374 | padding: 0; /* 2 */ 375 | } 376 | 377 | /** 378 | * Correct the cursor style of increment and decrement buttons in Chrome. 379 | */ 380 | 381 | [type='number']::-webkit-inner-spin-button, 382 | [type='number']::-webkit-outer-spin-button { 383 | height: auto; 384 | } 385 | 386 | /** 387 | * 1. Correct the odd appearance in Chrome and Safari. 388 | * 2. Correct the outline style in Safari. 389 | */ 390 | 391 | [type='search'] { 392 | appearance: textfield; /* 1 */ 393 | outline-offset: -2px; /* 2 */ 394 | } 395 | 396 | /** 397 | * Remove the inner padding and cancel buttons in Chrome and Safari on macOS. 398 | */ 399 | 400 | [type='search']::-webkit-search-cancel-button, 401 | [type='search']::-webkit-search-decoration { 402 | appearance: none; 403 | } 404 | 405 | /** 406 | * 1. Correct the inability to style clickable types in iOS and Safari. 407 | * 2. Change font properties to `inherit` in Safari. 408 | */ 409 | 410 | ::-webkit-file-upload-button { 411 | appearance: button; /* 1 */ 412 | font: inherit; /* 2 */ 413 | } 414 | 415 | /* Interactive 416 | ========================================================================== */ 417 | 418 | /* 419 | * Add the correct display in IE 9-. 420 | * 1. Add the correct display in Edge, IE, and Firefox. 421 | */ 422 | 423 | details, /* 1 */ 424 | menu { 425 | display: block; 426 | } 427 | 428 | /* 429 | * Add the correct display in all browsers. 430 | */ 431 | 432 | summary { 433 | display: list-item; 434 | } 435 | 436 | /* Scripting 437 | ========================================================================== */ 438 | 439 | /** 440 | * Add the correct display in IE 9-. 441 | */ 442 | 443 | canvas { 444 | display: inline-block; 445 | } 446 | 447 | /** 448 | * Add the correct display in IE. 449 | */ 450 | 451 | template { 452 | display: none; 453 | } 454 | 455 | /* Hidden 456 | ========================================================================== */ 457 | 458 | /** 459 | * Add the correct display in IE 10-. 460 | */ 461 | 462 | [hidden] { 463 | display: none; 464 | } 465 | -------------------------------------------------------------------------------- /src/assets/scss/third-party/prisma-theme.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * xonokai theme for JavaScript, CSS and HTML 3 | * based on: https://github.com/MoOx/sass-prism-theme-base by Maxime Thirouin ~ MoOx --> http://moox.fr/ , which is Loosely based on Monokai textmate theme by http://www.monokai.nl/ 4 | * license: MIT; http://moox.mit-license.org/ 5 | */ 6 | code[class*="language-"], 7 | pre[class*="language-"] { 8 | -moz-tab-size: 2; 9 | -o-tab-size: 2; 10 | tab-size: 2; 11 | -webkit-hyphens: none; 12 | -moz-hyphens: none; 13 | -ms-hyphens: none; 14 | hyphens: none; 15 | white-space: pre; 16 | white-space: pre-wrap; 17 | word-wrap: normal; 18 | font-family: Menlo, Monaco, "Courier New", monospace; 19 | font-size: 16px; 20 | color: #76d9e6; 21 | text-shadow: none; 22 | } 23 | 24 | pre > code[class*="language-"] { 25 | font-size: 1em; 26 | } 27 | 28 | pre[class*="language-"], 29 | :not(pre) > code[class*="language-"] { 30 | background: #2a2a2a; 31 | } 32 | 33 | pre[class*="language-"] { 34 | padding: 15px; 35 | border-radius: var(--border-radius); 36 | border: 1px solid #e1e1e8; 37 | overflow: auto; 38 | position: relative; 39 | } 40 | 41 | pre[class*="language-"] code { 42 | white-space: pre; 43 | display: block; 44 | } 45 | 46 | :not(pre) > code[class*="language-"] { 47 | padding: 0.15em 0.2em 0.05em; 48 | border-radius: .3em; 49 | border: 0.13em solid #7a6652; 50 | box-shadow: 1px 1px 0.3em -0.1em #000 inset; 51 | } 52 | 53 | .token.namespace { 54 | opacity: .7; 55 | } 56 | 57 | .token.comment, 58 | .token.prolog, 59 | .token.doctype, 60 | .token.cdata { 61 | color: #6f705e; 62 | } 63 | 64 | .token.operator, 65 | .token.boolean, 66 | .token.number { 67 | color: #a77afe; 68 | } 69 | 70 | .token.attr-name, 71 | .token.string { 72 | color: #e6d06c; 73 | } 74 | 75 | .token.entity, 76 | .token.url, 77 | .language-css .token.string, 78 | .style .token.string { 79 | color: #e6d06c; 80 | } 81 | 82 | .token.selector, 83 | .token.inserted { 84 | color: #a6e22d; 85 | } 86 | 87 | .token.atrule, 88 | .token.attr-value, 89 | .token.keyword, 90 | .token.important, 91 | .token.deleted { 92 | color: #ef3b7d; 93 | } 94 | 95 | .token.regex, 96 | .token.statement { 97 | color: #76d9e6; 98 | } 99 | 100 | .token.placeholder, 101 | .token.variable { 102 | color: #fff; 103 | } 104 | 105 | .token.important, 106 | .token.statement, 107 | .token.bold { 108 | font-weight: bold; 109 | } 110 | 111 | .token.punctuation { 112 | color: #bebec5; 113 | } 114 | 115 | .token.entity { 116 | cursor: help; 117 | } 118 | 119 | .token.italic { 120 | font-style: italic; 121 | } 122 | 123 | code.language-markup { 124 | color: #f9f9f9; 125 | } 126 | 127 | code.language-markup .token.tag { 128 | color: #ef3b7d; 129 | } 130 | 131 | code.language-markup .token.attr-name { 132 | color: #a6e22d; 133 | } 134 | 135 | code.language-markup .token.attr-value { 136 | color: #e6d06c; 137 | } 138 | 139 | code.language-markup .token.style, 140 | code.language-markup .token.script { 141 | color: #76d9e6; 142 | } 143 | 144 | code.language-markup .token.script .token.keyword { 145 | color: #76d9e6; 146 | } 147 | 148 | /* Line highlight plugin */ 149 | .line-highlight.line-highlight { 150 | padding: 0; 151 | background: rgba(255, 255, 255, 0.08); 152 | } 153 | 154 | .line-highlight.line-highlight:before, 155 | .line-highlight.line-highlight[data-end]:after { 156 | padding: 0.2em 0.5em; 157 | background-color: rgba(255, 255, 255, 0.4); 158 | color: black; 159 | height: 1em; 160 | line-height: 1em; 161 | box-shadow: 0 1px 1px rgba(255, 255, 255, 0.7); 162 | } -------------------------------------------------------------------------------- /src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TylerMRoderick/fernfolio-11ty-template/af491fe90e3030f8a9c08f13f9f808d7b4af2bb3/src/favicon.ico -------------------------------------------------------------------------------- /src/index.njk: -------------------------------------------------------------------------------- 1 | --- 2 | layout: base.njk 3 | section: home 4 | --- 5 |
6 |
7 | 8 | {# Hero #} 9 |
10 |
11 |

{{ home.title }}

12 | {% if home.body %} 13 |
{{ home.body | markdown | safe }}
14 | {% endif %} 15 |
16 |
17 | {% image 18 | home.image, 19 | home.image_alt, 20 | "(min-width: 1024px) 50vw, 100vw", 21 | "hero__image shadow", 22 | "eager" 23 | %} 24 |
25 |
26 | 27 | {# Projects #} 28 |
29 |

Projects

30 | View All 31 |
32 | {% from "macros/projectlist.njk" import projectlist %} 33 | {{ projectlist(projects = collections.project | reverse) }} 34 | 35 | {# Articles #} 36 |
37 |

Articles

38 | View All 39 |
40 | {% from "macros/articlelist.njk" import articlelist %} 41 | {{ articlelist( 42 | articles = collections.post | reverse, 43 | show_tags = false, 44 | limit = 3 45 | ) }} 46 |
47 | -------------------------------------------------------------------------------- /src/pages/about.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: About Me 3 | subtitle: "" 4 | metaDescription: This page is all about me 5 | date: 2017-01-01T00:00:00.000Z 6 | permalink: /about/index.html 7 | eleventyNavigation: 8 | key: About 9 | order: 1 10 | --- 11 | 12 | ### What is Fernfolio? 13 | An [Eleventy](https://www.11ty.io/) theme designed to simplify the process of creating a beautiful portfolio and blog. Tightly integrated with [Netlify CMS](https://www.netlifycms.org/) for flexible, Git-powered content management. 14 | 15 | ### Why create this? 16 | 17 | **Reason #1:** I wanted a portfolio that was simple, fast, accessible, and integrated with a git-powered cms. Existing templates didn't perfectly fit my needs, so I decided to build my own template. 18 | 19 | **Reason #2:** I wanted to learn more about [Eleventy](https://www.11ty.dev/) and static site generators in general. Building Fernfolio was a good excuse to dive deeper on that topic. 20 | 21 | ### How do I edit content? 22 | Once your site is deployed and configured, add `/admin` to the end of the url (not using localhost) and hit refresh. From there, you should be able to login and see see the content management dashboard. You should be able to change most content here (e.g. page text, images, logo, articles, projects, etc.). 23 | 24 | ### Further customizations 25 | If you want to take your customizations further, you will need to modify the project source code. Some customizations, like changing theme colors and fonts, are straightforward (those can be modified in the `variables.scss` file). Other customizations will require more in-depth solutions. 26 | 27 | If you get stuck or just have a question, feel free to create a [Github issue](https://github.com/TylerMRoderick/fernfolio-11ty-template/issues) and I will try to point you in the right direction. 28 | 29 | Thanks for checking out Fernfolio 👋 - [Tyler M. Roderick](https://www.tylerroderick.com/) 30 | 31 | ![Fern in Hand](/src/assets/img/fern-forest.jpeg "Fern in Hand") -------------------------------------------------------------------------------- /src/pages/blog.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: blog.njk 3 | title: Articles 4 | date: 2017-01-01 5 | pagination: 6 | data: collections.post 7 | size: 20 8 | permalink: "blog{% if pagination.pageNumber > 0 %}/page/{{ pagination.pageNumber }}{% endif %}/index.html" 9 | metaDescription: A sample Blog page listing various posts. 10 | subtitle: A collection of technical blog posts and random thoughts 11 | eleventyNavigation: 12 | key: Blog 13 | order: 2 14 | --- 15 | -------------------------------------------------------------------------------- /src/pages/contact.md: -------------------------------------------------------------------------------- 1 | --- 2 | section: contact 3 | layout: contact.njk 4 | title: Get in touch 5 | date: 2018-01-01 6 | permalink: /contact/index.html 7 | metaDescription: This is a sample meta description. If one is not present in 8 | your page/post's front matter, the default metadata.desciption will be used 9 | instead. 10 | subtitle: Contact Subtitle 11 | eleventyNavigation: 12 | key: Contact 13 | order: 4 14 | --- 15 | 16 | The contact form on this page uses [Netlify Forms](https://www.netlify.com/docs/form-handling/) to process submissions, 17 | and saves them in the connected Netlify account where notifications can 18 | optionally be configured. Each submission is passed through a spam filter and 19 | if flagged, will display a CAPTCHA challenge to the user. 20 | -------------------------------------------------------------------------------- /src/pages/pages.json: -------------------------------------------------------------------------------- 1 | { 2 | "layout": "page.njk" 3 | } 4 | -------------------------------------------------------------------------------- /src/pages/projects.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: projects.njk 3 | title: Projects 4 | date: 2021-01-01 5 | permalink: /projects/index.html 6 | metaDescription: A sample Projects page 7 | subtitle: This is the page where all projects will live 8 | emoji: 💻 9 | eleventyNavigation: 10 | key: Projects 11 | order: 3 12 | --- 13 | -------------------------------------------------------------------------------- /src/posts/code-sample.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: This post contains a code sample 3 | metaDescription: Add code samples to your markdown files 4 | date: 2019-01-01T00:00:00.000Z 5 | summary: Add code samples to your markdown files 6 | tags: 7 | - tech 8 | - environment 9 | - politics 10 | - sport 11 | --- 12 | Leverage agile frameworks to provide a robust synopsis for high level overviews. Iterative approaches to corporate strategy foster collaborative thinking to further the overall value proposition. Organically grow the holistic world view of disruptive innovation via workplace diversity and empowerment. 13 | 14 | Bring to the table win-win survival strategies to ensure proactive domination. At the end of the day, going forward, a new normal that has evolved from generation X is on the runway heading towards a streamlined cloud solution. User generated content in real-time will have multiple touchpoints for offshoring. 15 | 16 | ## Section Header 17 | 18 | Capitalize on low hanging fruit to identify a ballpark value added activity to beta test. Override the digital divide with additional clickthroughs from DevOps. Nanotechnology immersion along the information highway will close the loop on focusing solely on the bottom line. 19 | 20 | ```js 21 | // this is a command 22 | function myCommand() { 23 | let counter = 0; 24 | counter++; 25 | } 26 | ``` 27 | Organically grow the holistic world view of disruptive innovation via workplace diversity and empowerment. 28 | -------------------------------------------------------------------------------- /src/posts/customizations.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Theme customizations 3 | date: 2020-10-15T12:23:39.598Z 4 | summary: Modify Fernfolio to meet your needs 5 | tags: 6 | - environment 7 | - sport 8 | --- 9 | 10 | ### How do I edit content? 11 | Once your site is deployed and configured, add `/admin` to the end of the url (not using localhost) and hit refresh. From there, you should be able to login and see see the content management dashboard. You should be able to change most content here (e.g. page text, images, logo, articles, projects, etc.). 12 | 13 | ### Further customizations 14 | If you want to take your customizations further, you will need to modify the project source code. Some customizations, like changing theme colors and fonts, are straightforward (those can be modified in the `variables.scss` file). Other customizations will require more in-depth solutions. 15 | 16 | If you get stuck or just have a question, feel free to create a [Github issue](https://github.com/TylerMRoderick/fernfolio-11ty-template/issues) and I will try to point you in the right direction. 17 | -------------------------------------------------------------------------------- /src/posts/fourthpost.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Images can be added to posts 3 | date: 2020-02-03T08:00:00.000Z 4 | summary: Add an image to your post 5 | tags: 6 | - environment 7 | - politics 8 | --- 9 | The below image was added using Netlify CMS and is stored in your git repo. 10 | 11 | ![Fern](/src/assets/img/fern-in-hand.jpeg "Fern") 12 | 13 | Leverage agile frameworks to provide a robust synopsis for high level overviews. Iterative approaches to corporate strategy foster collaborative thinking to further the overall value proposition. Organically grow the holistic world view of disruptive innovation via workplace diversity and empowerment. -------------------------------------------------------------------------------- /src/posts/posts.json: -------------------------------------------------------------------------------- 1 | { 2 | "layout": "post.njk", 3 | "permalink": "posts/{{ urlPath or title | slug }}/index.html", 4 | "tags": ["post"] 5 | } 6 | -------------------------------------------------------------------------------- /src/posts/secondpost.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: This is the second example post 3 | date: 2020-01-01T08:00:00.000Z 4 | summary: Bring to the table win-win survival strategies to ensure proactive domination. 5 | tags: 6 | - sport 7 | --- 8 | Leverage agile frameworks to provide a robust synopsis for high level overviews. Iterative approaches to corporate strategy foster collaborative thinking to further the overall value proposition. Organically grow the holistic world view of disruptive innovation via workplace diversity and empowerment. 9 | 10 | ## Section Header 11 | 12 | Bring to the table win-win survival strategies to ensure proactive domination. At the end of the day, going forward, a new normal that has evolved from generation X is on the runway heading towards a streamlined cloud solution. User generated content in real-time will have multiple touchpoints for offshoring. 13 | 14 | Capitalize on low hanging fruit to identify a ballpark value added activity to beta test. Override the digital divide with additional clickthroughs from DevOps. Nanotechnology immersion along the information highway will close the loop on focusing solely on the bottom line. 15 | -------------------------------------------------------------------------------- /src/posts/thirdpost.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: This is the third example post which has a slightly longer title than the 3 | others 4 | date: 2020-01-01T08:00:00.000Z 5 | summary: Organically grow the holistic world view of disruptive innovation 6 | tags: 7 | - tech 8 | - politics 9 | --- 10 | Leverage agile frameworks to provide a robust synopsis for high level overviews. Iterative approaches to corporate strategy foster collaborative thinking to further the overall value proposition. Organically grow the holistic world view of disruptive innovation via workplace diversity and empowerment. 11 | 12 | ```css 13 | pre, 14 | code { 15 | line-height: 1.5; 16 | } 17 | ``` 18 | 19 | Bring to the table win-win survival strategies to ensure proactive domination. At the end of the day, going forward, a new normal that has evolved from generation X is on the runway heading towards a streamlined cloud solution. User generated content in real-time will have multiple touchpoints for offshoring. 20 | 21 | ## Section Header 22 | 23 | Capitalize on low hanging fruit to identify a ballpark value added activity to beta test. Override the digital divide with additional clickthroughs from DevOps. Nanotechnology immersion along the information highway will close the loop on focusing solely on the bottom line. 24 | -------------------------------------------------------------------------------- /src/projects/first-project.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Cat Translation API 3 | emoji: 😺 4 | date: 2019-01-01T00:00:00.000Z 5 | summary: API to translate cat speech to english 6 | metaDescription: This is a sample meta description. If one is not present in 7 | your page/project's front matter, the default metadata.desciption will be used 8 | instead. 9 | tags: 10 | - golang 11 | - graphQL 12 | - aws 13 | --- 14 | 15 | ### Task 16 | 17 | Leverage agile frameworks to provide a robust synopsis for high level overviews. Iterative approaches to corporate strategy foster collaborative thinking to further the overall value proposition. Organically grow the holistic world view of disruptive innovation via workplace diversity and empowerment. 18 | 19 | ### Solution 20 | 21 | Bring to the table win-win survival strategies to ensure proactive domination. At the end of the day, going forward, a new normal that has evolved from generation X is on the runway heading towards a streamlined cloud solution. User generated content in real-time will have multiple touchpoints for offshoring. 22 | 23 | #### Image Uploaded to CMS: 24 | ![cat relaxing](/src/assets/img/1177px-cat_august_2010-4.jpg) 25 | 26 | #### Remote Image: 27 | ![cat in snow](https://upload.wikimedia.org/wikipedia/commons/b/b6/Felis_catus-cat_on_snow.jpg) 28 | 29 | -------------------------------------------------------------------------------- /src/projects/projects.json: -------------------------------------------------------------------------------- 1 | { 2 | "layout": "project.njk", 3 | "permalink": "projects/{{ title | slug }}/index.html", 4 | "tags": ["project"] 5 | } 6 | -------------------------------------------------------------------------------- /src/projects/second-project.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Another NPM Package 3 | emoji: 💾 4 | metaDescription: This is a sample meta description. If one is not present in your page/project's front matter, the default metadata.desciption will be used instead. 5 | date: 2019-01-01T00:00:00.000Z 6 | summary: This is an NPM package I made 7 | tags: 8 | - javascript 9 | - node 10 | --- 11 | 12 | ### Task 13 | 14 | Leverage agile frameworks to provide a robust synopsis for high level overviews. Iterative approaches to corporate strategy foster collaborative thinking to further the overall value proposition. Organically grow the holistic world view of disruptive innovation via workplace diversity and empowerment. 15 | 16 | ### Solution 17 | 18 | Bring to the table win-win survival strategies to ensure proactive domination. At the end of the day, going forward, a new normal that has evolved from generation X is on the runway heading towards a streamlined cloud solution. User generated content in real-time will have multiple touchpoints for offshoring. 19 | -------------------------------------------------------------------------------- /src/projects/vue-component.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Vue Date Picker 3 | emoji: 🗓 4 | metaDescription: This is a sample meta description. If one is not present in your page/project's front matter, the default metadata.desciption will be used instead. 5 | date: 2019-01-01T00:00:00.000Z 6 | summary: This is a Vue component I made 7 | tags: 8 | - javascript 9 | - vue 10 | - aws 11 | --- 12 | 13 | ### Task 14 | 15 | Leverage agile frameworks to provide a robust synopsis for high level overviews. Iterative approaches to corporate strategy foster collaborative thinking to further the overall value proposition. Organically grow the holistic world view of disruptive innovation via workplace diversity and empowerment. 16 | 17 | ### Solution 18 | 19 | Bring to the table win-win survival strategies to ensure proactive domination. At the end of the day, going forward, a new normal that has evolved from generation X is on the runway heading towards a streamlined cloud solution. User generated content in real-time will have multiple touchpoints for offshoring. 20 | -------------------------------------------------------------------------------- /src/tags/project-tags.njk: -------------------------------------------------------------------------------- 1 | --- 2 | pagination: 3 | data: collections 4 | size: 1 5 | alias: tag 6 | permalink: /project-tags/{{ tag }}/ 7 | layout: base.njk 8 | renderData: 9 | title: "Projects tagged “{{ tag }}”" 10 | metaDescription: "All projects tagged with “{{ tag }}”" 11 | --- 12 |
13 | {# Header #} 14 |
15 |

Projects Tagged: "{{ tag | capitalize }}"

16 | Remove Tag 17 |
18 | 19 | {# Projects #} 20 | {% from "macros/projectlist.njk" import projectlist %} 21 | {{ projectlist(projects = collections[tag] | reverse) }} 22 | 23 | {# Footer #} 24 | 27 |
28 | -------------------------------------------------------------------------------- /src/tags/tags.njk: -------------------------------------------------------------------------------- 1 | --- 2 | pagination: 3 | data: collections 4 | size: 1 5 | alias: tag 6 | permalink: /tags/{{ tag }}/ 7 | layout: base.njk 8 | renderData: 9 | title: "Posts tagged “{{ tag }}”" 10 | metaDescription: "All posts from the Blog tagged with “{{ tag }}”" 11 | --- 12 |
13 |
14 |

Articles Tagged: "{{ tag | capitalize }}"

15 | Remove Tag 16 |
17 | 18 | {% from "macros/articlelist.njk" import articlelist %} 19 | {{ articlelist( 20 | articles = collections[tag] | reverse, 21 | show_tags = true 22 | ) }} 23 | 24 | 27 |
28 | --------------------------------------------------------------------------------