├── .dependabot └── config.yml ├── .env.development ├── .env.example ├── .github └── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── custom.md │ └── feature_request.md ├── .gitignore ├── .npmrc ├── .nvmrc ├── .prettierrc ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── PULL_REQUEST_TEMPLATE.md ├── README.md ├── _headers ├── cms ├── blocks │ └── index.js ├── cms.js ├── collections │ ├── authors.js │ ├── forms.js │ ├── pages.js │ ├── posts.js │ └── settings.js ├── fields │ ├── index.js │ ├── navigation-field.js │ ├── permalink-field.js │ └── seo.js └── previews │ ├── FormPreview.js │ └── Page.js ├── content ├── authors │ └── wojciech-kaluzny.md ├── blog │ └── hello-world.md ├── forms │ └── example.md └── pages │ ├── about.md │ ├── blog.md │ └── home-page.md ├── gatsby-browser.js ├── gatsby-config.js ├── gatsby-node.js ├── gatsby-ssr.js ├── jsconfig.json ├── netlify.toml ├── package.json ├── postcss.config.js ├── renovate.json ├── src ├── api │ └── .gitkeep ├── blocks │ ├── Content.js │ ├── ContentImage.js │ ├── Hero.js │ ├── Perks.js │ └── RecentArticles.js ├── components │ ├── Article │ │ ├── ArticleCard.js │ │ └── Recent.js │ ├── DarkmodeToggle.js │ ├── Footer.js │ ├── Form │ │ ├── Form.js │ │ ├── FormWrapper.js │ │ └── partials │ │ │ ├── ButtonField.js │ │ │ ├── Checkbox.js │ │ │ ├── ContentSection.js │ │ │ ├── Input.js │ │ │ ├── Submit.js │ │ │ └── TextArea.js │ ├── Head │ │ └── DefaultHead.js │ ├── Header.js │ ├── Layout.js │ ├── PageBuilder.js │ └── UI │ │ ├── Button.js │ │ ├── Buttons.js │ │ ├── Container.js │ │ ├── Label.js │ │ ├── Section.js │ │ ├── Text.js │ │ └── Title.js ├── hooks │ ├── useForms.js │ └── useRecentArticles.js ├── lib │ └── helper.js ├── pages │ └── 404.js ├── resolvers │ ├── Image.js │ └── Link.js ├── settings │ ├── main.json │ └── seo.json ├── styles │ └── main.css └── templates │ ├── page-builder.js │ └── post.js ├── static ├── fonts │ └── .gitkeep └── img │ ├── favicons │ ├── android-icon-144x144.png │ ├── android-icon-192x192.png │ ├── android-icon-36x36.png │ ├── android-icon-48x48.png │ ├── android-icon-72x72.png │ ├── android-icon-96x96.png │ ├── apple-icon-114x114.png │ ├── apple-icon-120x120.png │ ├── apple-icon-144x144.png │ ├── apple-icon-152x152.png │ ├── apple-icon-180x180.png │ ├── apple-icon-57x57.png │ ├── apple-icon-60x60.png │ ├── apple-icon-72x72.png │ ├── apple-icon-76x76.png │ ├── apple-icon-precomposed.png │ ├── apple-icon.png │ ├── browserconfig.xml │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── favicon-96x96.png │ ├── favicon.ico │ ├── manifest.json │ ├── ms-icon-144x144.png │ ├── ms-icon-150x150.png │ ├── ms-icon-310x310.png │ └── ms-icon-70x70.png │ ├── frame-69.png │ ├── frame-72.png │ ├── frame-77.png │ ├── henlo-cover.png │ └── wojciech-kaluzny-20-312x312.jpg └── yarn.lock /.dependabot/config.yml: -------------------------------------------------------------------------------- 1 | version: 1 2 | 3 | update_configs: 4 | - package_manager: javascript 5 | directory: / 6 | update_schedule: live 7 | allowed_updates: 8 | - match: 9 | update_type: security 10 | automerged_updates: 11 | - match: 12 | dependency_type: all 13 | update_type: in_range 14 | version_requirement_updates: widen_ranges -------------------------------------------------------------------------------- /.env.development: -------------------------------------------------------------------------------- 1 | GATSBY_APP_URL="http://localhost:8000" -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | GATSBY_APP_URL="" -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | --- 11 | name: Bug report 12 | about: Create a report to help us improve 13 | --- 14 | 15 | 16 | 17 | 18 | # Bug report 19 | 20 | 21 | 22 | 23 | 24 | 25 | **What is the current behavior?** 26 | 27 | 28 | **If the current behavior is a bug, please provide the steps to reproduce.** 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | **What is the expected behavior?** 38 | 39 | 40 | 41 | 42 | 43 | **Other relevant information:** 44 | 45 | 46 | 47 | Node.js version: 48 | NPM/Yarn version 49 | Operating System: 50 | Additional tools: 51 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/custom.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Custom issue template 3 | about: Describe this issue template's purpose here. 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | --- 11 | name: Other 12 | about: Something else 13 | 14 | --- 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | --- 11 | name: Feature request 12 | about: Suggest an idea for this project 13 | 14 | --- 15 | 16 | 17 | 18 | ## Feature request 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | **What is the expected behavior?** 27 | 28 | 29 | **What is motivation or use case for adding/changing the behavior?** 30 | 31 | 32 | **How should this be implemented in your opinion?** 33 | 34 | 35 | **Are you willing to work on this yourself?** 36 | yes 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Project dependencies 2 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 3 | node_modules 4 | .cache/ 5 | # Build directory 6 | public/ 7 | static/admin/*.bundle.* 8 | .DS_Store 9 | yarn-error.log 10 | .env.* 11 | !.env.example 12 | !.env.development 13 | 14 | # Local Netlify folder 15 | .netlify -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | scripts-prepend-node-path=true 2 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v20.11.0 -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "useTabs": false, 4 | "singleQuote": true, 5 | "semi": false, 6 | "importOrder": [ 7 | "", 8 | "react", 9 | "", 10 | "^[./]", 11 | "^(.*components.*)$", 12 | "^(.*lib.*)$", 13 | "^(.*resolvers.*)$", 14 | "^(.*hooks.*)$", 15 | "^(.*settings.*)$" 16 | ], 17 | "tailwindFunctions": [ 18 | "cn" 19 | ], 20 | "tailwindStylesheet": "./src/styles/main.css", 21 | "plugins": [ 22 | "@trivago/prettier-plugin-sort-imports", 23 | "prettier-plugin-tailwindcss" 24 | ] 25 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 1.3.0 - April 9, 2025 4 | 5 | - Support for Tailwind V4 added 6 | - Packages updates 7 | 8 | ## 1.2.1 - December 3, 2024 9 | 10 | - Gatsby bumped to 5.14.0 11 | - Tailwindcss updated 12 | - Minor packages updates 13 | 14 | ## 1.2.0 - April 18, 2024 15 | 16 | **BREAKING CHANGES** 17 | 18 | - VariantField renamed to SelectField 19 | 20 | ** Fixes ** 21 | 22 | - Adding Forms components 23 | - Forms preview is now working 24 | - Forms collection added to the CMS 25 | 26 | **Improvments** 27 | 28 | - Updated packages to Decap CMS 29 | - Updated Readme urls to reflect the changes 30 | - Node version changed to v20.11.0 31 | - Section component added to support custom spacing for sections 32 | - Added support for tailwind-merge with new `cn()` helper function 33 | - Prettier config added for imports ordering and tailwind classes ordering 34 | - GraphQL Schema Customization definitions added 35 | 36 | **Minor changes** 37 | 38 | - Bumped Gatsby to 5.13.4 39 | 40 | ## 1.1.0 - January 4, 2022 41 | 42 | **BREAKING CHANGES** 43 | 44 | - Support for Gatsby v5 added 45 | - `react-helmet` removed, SEO partials moved to Head API 46 | - `yarn start` renamed to `yarn dev` 47 | 48 | **Minor changes** 49 | 50 | - `classnames` removed, `clsx` added 51 | - Language definition added to `gatsby-ssr.js` 52 | - Removed unnused packages 53 | 54 | **Bug Fixes** 55 | 56 | - Improved darkmode toggle component - issue with hydration 57 | - Resolved minor warnings issues 58 | 59 | ## 1.0.1 - October 22, 2022 60 | 61 | **Minor changes** 62 | 63 | - Packages bumped 64 | - Dependabot changes 65 | 66 | ## 1.0.0 - August 12, 2022 67 | 68 | Complete overhaul of the previous starters. 69 | 70 | New features: 71 | 72 | - 💪 Battle-tested starting point for small & large web projects 73 | - 📄 Form Builder that enables Admins to create multiple forms with ease & Netlify Forms integration. 74 | - 🌗 Darkmode support 75 | - 🗺 Sitemaps using `gatsby-plugin-sitemap` 76 | - 💇‍♀️ TailwindCSS support with PostCSS processing & PurgeCSS 77 | - 🔌 Support for Gatsby API functions 78 | - 🕵️‍♂️ Complete SEO configuration with graphql fragment and reusable components 79 | ..and more 80 | 81 | ## 0.6.0 - March 7, 2021 82 | 83 | **BREAKING CHANGES** 84 | 85 | - Support for Gatsby v4 added 86 | - Tailwind updated to v3 87 | - Node changed to v16.13.0 88 | - Sitemap generation switched to `gatsby-plugin-sitemap` 89 | 90 | **Minor changes** 91 | 92 | - Removed unused styles 93 | 94 | **Features** 95 | 96 | - tailwind/typography plugin added 97 | - tailwind/forms plugin added 98 | - added support for darkmode 99 | 100 | ## 0.5.1 - March 18, 2021 101 | 102 | **FIX** 103 | 104 | - Added gatsby-plugin-image to gatsby-config.js (issues with loading images) 105 | 106 | ## 0.5.0 - March 14, 2021 107 | 108 | **BREAKING CHANGES** 109 | 110 | - Support for Gatsby v3 added 111 | - PurgeCSS switched to v4 112 | - Removed support for `react-svg`, inlined svg are used instead. 113 | - Node changed to v14.15.5 114 | 115 | **Features** 116 | 117 | - SEO component updated for usage with `gatsby-plugin-image` added 118 | - `gatsby-node.js` and `gatsby-config.js` updated to support Gatsby v3 119 | 120 | **Minor changes** 121 | 122 | - Index page updated, version added to index page 123 | - Changelog created 124 | 125 | ## 0.4.1 - March 4, 2021 126 | 127 | - Added support for Twitter cards, sitemeta changed 128 | - All routes use trailing slashes to remove 301 redirects from non slash to slash versions 129 | 130 | ## 0.4.0 - December 31, 2020 131 | 132 | - SEO implementation strategy changed - components instead of helpers 133 | - Brotli compression is now enabled by default 134 | - Cleared unnecessary Gatsby Plugins 135 | - CMS configuration setup udpdated 136 | 137 | ## 0.3.1 - September 21, 2020 138 | 139 | - Standardized favicon generation procedure 140 | 141 | ## 0.3.1 - September 8, 2020 142 | 143 | - Support for Tailwind 1.3.1 added 144 | - Netlify CMS switched to Manual Initialization 145 | - Support for better SEO optimization 146 | - Dynamic layout selection based on keys added 147 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at david@netlify.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # CONTRIBUTING 2 | 3 | Contributions are always welcome, no matter how large or small. Before contributing, 4 | please read the [code of conduct](CODE_OF_CONDUCT.md). 5 | 6 | ## Setup 7 | 8 | > Install yarn on your system: [https://yarnpkg.com/en/docs/install](https://yarnpkg.com/en/docs/install) 9 | 10 | ### Install dependencies 11 | 12 | > Only required on the first run, subsequent runs can use `yarn` to both 13 | bootstrap and run the development server using `yarn develop`. 14 | Since this starter using the [netlify-dev](https://www.netlify.com/products/dev/#how-it-works), there could be further issues you, please check the [netlify-dev](https://github.com/netlify/netlify-dev) repository for further information and set up questions. 15 | 16 | ```sh 17 | $ git clone https://github.com/netlify-templates/gatsby-starter-netlify-cms 18 | $ yarn 19 | ``` 20 | 21 | ## Available scripts 22 | 23 | 24 | ### `build` 25 | 26 | Build the static files into the `public` folder, turns lambda functions into a deployable form. 27 | 28 | #### Usage 29 | 30 | ```sh 31 | $ yarn build 32 | ``` 33 | 34 | ### `clean` 35 | 36 | Runs `gatsby clean` command. 37 | 38 | #### Usage 39 | 40 | ```sh 41 | yarn clean 42 | ``` 43 | 44 | ### `netlify dev` 45 | 46 | Starts the netlify dev environment, including the gatsby dev environment. 47 | For more infor check the [Netlify Dev Docs](https://github.com/netlify/cli/blob/master/docs/netlify-dev.md) 48 | 49 | ```sh 50 | netlify dev 51 | ``` 52 | 53 | ### `develop` or `start` 54 | 55 | Runs the `clean` script and starts the gatsby develop server using the command `gatsby develop`. We recomend using this command when you don't need Netlify specific features 56 | 57 | #### Usage 58 | 59 | ```sh 60 | yarn develop 61 | ``` 62 | ### `test` 63 | 64 | Not implmented yet 65 | 66 | #### Usage 67 | 68 | ```sh 69 | yarn test 70 | ``` 71 | 72 | ### `format` 73 | 74 | Formats code and docs according to our style guidelines using `prettier` 75 | 76 | #### Usage 77 | 78 | ```sh 79 | yarn format 80 | ``` 81 | 82 | 83 | ## Pull Requests 84 | 85 | We actively welcome your pull requests! 86 | 87 | If you need help with Git or our workflow, please ask on [Gitter.im](https://gitter.im/netlify/NetlifyCMS). We want your contributions even if you're just learning Git. Our maintainers are happy to help! 88 | 89 | Netlify CMS uses the [Forking Workflow](https://www.atlassian.com/git/tutorials/comparing-workflows/forking-workflow) + [Feature Branches](https://www.atlassian.com/git/tutorials/comparing-workflows/feature-branch-workflow). Additionally, PR's should be [rebased](https://www.atlassian.com/git/tutorials/merging-vs-rebasing) on master when opened, and again before merging. 90 | 91 | 1. Fork the repo. 92 | 2. Create a branch from `master`. If you're addressing a specific issue, prefix your branch name with the issue number. 93 | 2. If you've added code that should be tested, add tests. 94 | 3. If you've changed APIs, update the documentation. 95 | 4. Run `yarn test` and ensure the test suite passes. (Not applicable yet) 96 | 5. Use `yarn format` to format and lint your code. 97 | 6. PR's must be rebased before merge (feel free to ask for help). 98 | 7. PR should be reviewed by two maintainers prior to merging. 99 | 100 | ## License 101 | 102 | By contributing to the Gatsby - Netlify CMS starter, you agree that your contributions will be licensed 103 | under its [MIT license](LICENSE). 104 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 gatsbyjs 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 | 23 | -------------------------------------------------------------------------------- /PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | **What kind of change does this PR introduce?** 9 | 10 | 11 | 12 | **Does this PR introduce a breaking change?** 13 | 14 | 15 | 16 | **What needs to be documented once your changes are merged?** 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Henlo Starter](https://repository-images.githubusercontent.com/270961687/4085d990-9083-451d-b39b-5316579adf09) 2 | 3 | # Gatsby Starter Henlo (v1.3.0) 4 | 5 | [![Netlify Status](https://api.netlify.com/api/v1/badges/43532afb-3488-432b-8185-a745645a90d8/deploy-status)](https://app.netlify.com/sites/henlo/deploys) 6 | 7 | [Official Website / Demo](http://henlo.cleancommit.io) 8 | 9 | Gatsby Starter Henlo is the most advanced Decap CMS starter for Gatsby.js. We built it with Page Builder setup in mind. All pages are created out of programmable blocks, aiming to provide the best DX & admin UX possible. 10 | 11 | This repo contains an example website that is built with [Gatsby](https://www.gatsbyjs.com/docs/), and [Decap CMS](https://decapcms.org/docs/intro/). 12 | 13 | It follows the [JAMstack architecture](https://jamstack.org) by using Git as a single source of truth, and [Netlify](https://www.netlify.com) for continuous deployment, and CDN distribution. 14 | 15 | ## Features 16 | 17 | - 💇‍♀️ TailwindCSS V4 support with PostCSS & Tailwind Merge 18 | - Support for Gatsby v5 19 | - 💪 Battle-tested starting point for small & large web projects 20 | - 📄 Form Builder that enables Admins to create multiple forms with ease & Netlify Forms integration. 21 | - 🌗 Darkmode support 22 | - 🗺 Sitemaps using `gatsby-plugin-sitemap` 23 | - 🔥 Perfect score on Lighthouse for SEO, Accessibility and Performance 24 | - 🔌 Support for Gatsby API functions 25 | - 🎇 Crazy fast images with `gatsby-plugin-image` 26 | - 🕵️‍♂️ Complete SEO configuration with graphql fragment and reusable components based on Head API 27 | - Netlify deploy configuration 28 | - Example pages, collections, CMS configuration with Decap CMS & hooks 29 | - Readme template for custom projects 30 | - Easy Decap CMS configuration using [Manual Initialization](https://decapcms.org/docs/manual-initialization/#gatsby-focus-wrapper) 31 | - ..and more 32 | 33 | ## Prerequisites 34 | 35 | - Node 20 36 | - [Gatsby CLI](https://www.gatsbyjs.org/docs/) 37 | - [Netlify CLI](https://github.com/netlify/cli) 38 | 39 | ## Getting Started (Recommended) 40 | 41 | Decap CMS can run in any frontend web environment, but the quickest way to try it out is by running it on a pre-configured starter site with Netlify. Use the button below to build and deploy your own copy of the repository: 42 | 43 | Deploy to Netlify 44 | 45 | After clicking that button, you’ll authenticate with GitHub and choose a repository name. Netlify will then automatically create a repository in your GitHub account with a copy of the files from the template. Next, it will build and deploy the new site on Netlify, bringing you to the site dashboard when the build is complete. Next, you’ll need to set up Netlify’s Identity service to authorize users to log in to the CMS. 46 | 47 | ## Getting Started (Without Netlify) 48 | 49 | ``` 50 | $ gatsby new [SITE_DIRECTORY_NAME] https://github.com/clean-commit/gatsby-starter-henlo 51 | $ cd [SITE_DIRECTORY_NAME] 52 | $ yarn dev 53 | ``` 54 | 55 | ### Access Locally 56 | 57 | Pulldown a local copy of the Github repository Netlify created for you, with the name you specified in the previous step 58 | 59 | ``` 60 | $ git clone https://github.com/[GITHUB_USERNAME]/[REPO_NAME].git 61 | $ cd [REPO_NAME] 62 | $ yarn && yarn dev 63 | ``` 64 | 65 | To test the CMS locally, you'll need to start your local development server & [run local instance of Decap CMS](https://decapcms.org/docs/working-with-a-local-git-repository) 66 | 67 | ``` 68 | $ yarn dev 69 | $ yarn cms 70 | // or 71 | $ npx decap-server 72 | ``` 73 | 74 | Your admin configuration will be available at http://localhost:8000/admin 75 | 76 | ### Deployment 77 | 78 | We've added additional commands for quick deployments with Netlify CLI. To deploy the website to Netlify simply run. 79 | 80 | ``` 81 | $ yarn deploy:prod 82 | ``` 83 | 84 | The website will build locally and then deploy to production. 85 | 86 | Before deploying to Netlify, you need to: 87 | 88 | - Define GATSBY_APP_URL to your domain 89 | - Update redirects in netlify.toml to reflect your website (example is set up) 90 | 91 | ### Folder structure 92 | 93 | ``` 94 | ├── cms # Decap CMS configuration 95 | │ ├── blocks 96 | │ ├── collections 97 | │ ├── fields 98 | │ ├── previews 99 | │ └── cms.js 100 | ├── content # Your content lives here 101 | │ ├── authors 102 | │ ├── blog 103 | │ ├── forms 104 | │ └── pages 105 | ├── public 106 | ├── src 107 | │ ├── api # Gatsby functions should be placed here 108 | │ ├── blocks # Blocks that create sections 109 | │ ├── components # Reusable components 110 | │ │ └── UI # UI specialized components 111 | │ ├── hooks # Hooks used in the project 112 | │ ├── lib # misc 113 | │ ├── pages 114 | │ ├── resolvers 115 | │ │ ├── Image.js # Required for previews 116 | │ │ └── Link.js # Resolves links to gatsby and outside links 117 | │ ├── settings # Place for theme settings 118 | │ ├── styles 119 | │ └── templates # Templates used to render page types 120 | │ ├── page-builder.js 121 | │ └── post.js 122 | ├── static 123 | ├── _headers 124 | ├── .env.example # Example env -> GATSBY_APP_URL is required to run the app 125 | ├── gatsby-config.js # Config files for gatsby 126 | ├── gatsby-node.js # Page generation setup & types interferrence 127 | └── tailwind.config.js # Tailwind configuration 128 | ``` 129 | 130 | ### Setting up the CMS 131 | 132 | Follow the [Decap CMS Quick Start Guide](https://decapcms.org/docs/gatsby/#enable-identity-and-git-gateway) to set up authentication, and hosting. 133 | 134 | **Important** 135 | This template can be mostly changed by the user within the CMS itself (Settings type). For sitemaps to work correctly, you'll need to provide ENV variable `GATSBY_APP_URL` which defaults to https://example.com, this url will be used in setting up meta values in the head of the documents and links URL in the CMS. 136 | 137 | CMS configuration was placed within `cms` directory in the root of the project. This allows us to work efficiently on fields and collections without mixing CMS config with Gatsby code. 138 | 139 | Henlo uses Manual Initialization to take advantage of componetized approach to managing configuration for Decap CMS. Thanks to that you don't have to control the CMS from centralized YAML file. 140 | 141 | To ensure best experience we use 2 custom widgets that are maintained by us -> [ID Widget](https://github.com/clean-commit/netlify-cms-widget-id) that provides unmutable IDs for content items and [Permalink Widget](https://github.com/clean-commit/netlify-cms-widget-permalink) that enables you to create custom permalinks with ease. 142 | 143 | ```javascript 144 | import CMS from 'decap-cms-app' 145 | import { Widget as UuidWidget } from 'netlify-cms-widget-id' 146 | import { Widget as PermalinkWidget } from 'netlify-cms-widget-permalink' 147 | import authors from './collections/authors' 148 | import pages from './collections/pages' 149 | import posts from './collections/posts' 150 | import settings from './collections/settings' 151 | import PagePreview from './previews/Page' 152 | 153 | // Preview for all PageBuilder based pages 154 | 155 | const config = { 156 | config: { 157 | load_config_file: false, 158 | display_url: process.env.GATSBY_APP_URL, // Enables urls based on env variable 159 | local_backend: true, 160 | backend: { 161 | name: 'git-gateway', 162 | }, 163 | slug: { 164 | encoding: 'ascii', 165 | clean_accents: true, 166 | }, 167 | media_folder: '/static/img', 168 | public_folder: '/img', 169 | collections: [pages, posts, authors, settings], 170 | }, 171 | } 172 | 173 | CMS.registerPreviewStyle('../commons.css') 174 | CMS.registerPreviewTemplate('pages', PagePreview) 175 | 176 | CMS.registerWidget(UuidWidget) 177 | CMS.registerWidget(PermalinkWidget) 178 | 179 | CMS.init(config) 180 | ``` 181 | 182 | #### Adding blocks 183 | 184 | Blocks are defined in `cms/blocks/index.js` file. We're leveraging use of exported functions and variables from other fields to avoid repetition within the code. 185 | 186 | This is extremely important due to the way GraphQL works with Markdown based files. Each field will have to be present on all page queries -> we can't differetiate between different sections. 187 | 188 | That's why it's important to reuse names of fields, hence usage of imports. 189 | 190 | ```javascript 191 | import { Buttons, Title, Content, SelectField, ImageField } from '../fields'; 192 | 193 | const Config = { 194 | label: 'Blocks', 195 | name: 'blocks', 196 | widget: 'list', 197 | types: [ 198 | { 199 | label: 'Hero', 200 | name: 'hero', 201 | widget: 'object', 202 | fields: [ 203 | Title, 204 | Content, 205 | Buttons, 206 | SelectField('default', ['default', 'centered', 'full']), 207 | ], 208 | }, 209 | ... 210 | ``` 211 | 212 | To add new block you have tp add new `Type` to `cms/blocks/index.js` file, and modify `Blocks` fragment located in `src/components/PageBuilder.js` 213 | 214 | As you can see below we're adding all fields used by all sections. This causes issues with Gatsby's [Schema Inference](https://www.gatsbyjs.com/docs/schema-inference/). Gatsby needs an example of the field to know what type of field it is. 215 | 216 | That's why we rely on `dont-remove.md` this file contains all possible fields used in the starter, so you're never encounter a problem with types inference! 217 | 218 | ``` 219 | export const query = graphql` 220 | fragment Blocks on MarkdownRemarkFrontmatter { 221 | blocks { 222 | type 223 | title 224 | content 225 | columns { 226 | title 227 | content 228 | } 229 | photo { 230 | image { 231 | childImageSharp { 232 | gatsbyImageData( 233 | width: 800 234 | quality: 72 235 | placeholder: DOMINANT_COLOR 236 | formats: [AUTO, WEBP, AVIF] 237 | ) 238 | } 239 | } 240 | alt 241 | } 242 | variant 243 | buttons { 244 | button { 245 | content 246 | url 247 | variant 248 | } 249 | } 250 | } 251 | } 252 | ` 253 | ``` 254 | 255 | To keep the GraphQL query small, we opt to reuse the field names across blocks, but if new type is added it has to be defined in graphql using createSchemaCustomization function. **Without the definition you may encounter errors** during build process. 256 | 257 | ### Adding Favicons 258 | 259 | Favicons can be generated using this [Favicon Generator](https://www.favicon-generator.org/) After generating the icons, drop the contents of downloaded file into `static/img/favicons` directory 260 | 261 | ### Preloading fonts 262 | 263 | Since 0.4.0 Henlo supports [`gatsby-plugin-preload-fonts`](https://www.gatsbyjs.com/plugins/gatsby-plugin-preload-fonts/) plugin out of the box. To create the preload cache you need to start development server and then run `preload-fonts` command. This will generate the `font-preload-cache.json` file in the root of your project. When your projects builds fonts will be added automatically to head of the document. 264 | 265 | ``` 266 | yarn dev 267 | yarn preload-fonts 268 | ``` 269 | 270 | ## Browser support 271 | 272 | Gatsby tends to add a lot of polyfills to support older browser versions. In package.json file you can adjust which sites your project should support. As default Henlo will use `defaults` setting. If you want to learn more about the browser support visit official [Gatsby How-To Guide on this subject](https://www.gatsbyjs.com/docs/how-to/custom-configuration/browser-support/) 273 | 274 | # CONTRIBUTING 275 | 276 | Contributions are always welcome, no matter how large or small. Before contributing, 277 | please read the [code of conduct](CODE_OF_CONDUCT.md). 278 | 279 | # Additional guides 280 | 281 | Here's a list of helpful articles that will help you with your first steps using Henlo! 282 | 283 | - [Efficient Decap CMS config with Manual Initialization](https://mrkaluzny.com/blog/dry-netlify-cms-config-with-manual-initialization?utm_source=GitHub&utm_medium=henlo-gatsby) 284 | - [How to optimize SEO with Gatsby & Netlify](https://mrkaluzny.com/blog/how-to-optimize-seo-with-gatsby-netlify?utm_source=GitHub&utm_medium=henlo-gatsby) 285 | - [Full Text Search with Gatsby & Decap CMS](https://mrkaluzny.com/blog/full-text-search-with-gatsby-and-netlify-cms?utm_source=GitHub&utm_medium=henlo-gatsby) 286 | -------------------------------------------------------------------------------- /_headers: -------------------------------------------------------------------------------- 1 | [[headers]] 2 | for = "/static/*" 3 | [headers.values] 4 | Cache-Control = "public, max-age=360000" -------------------------------------------------------------------------------- /cms/blocks/index.js: -------------------------------------------------------------------------------- 1 | import { 2 | Buttons, 3 | Title, 4 | Content, 5 | SelectField, 6 | ImageField, 7 | SettingsGroup, 8 | } from '../fields'; 9 | 10 | const Config = { 11 | label: 'Blocks', 12 | name: 'blocks', 13 | widget: 'list', 14 | types: [ 15 | { 16 | label: 'Hero', 17 | name: 'hero', 18 | widget: 'object', 19 | fields: [ 20 | Title, 21 | Content, 22 | Buttons, 23 | SelectField('default', ['default', 'centered', 'full']), 24 | SettingsGroup, 25 | ], 26 | }, 27 | { 28 | label: 'Content', 29 | name: 'content', 30 | widget: 'object', 31 | fields: [Content], 32 | }, 33 | { 34 | label: 'Content with Image', 35 | name: 'content_image', 36 | widget: 'object', 37 | summary: '{{fields.title}}', 38 | fields: [ 39 | ImageField(), 40 | Title, 41 | Content, 42 | Buttons, 43 | SelectField('default', ['default', 'reversed']), 44 | SettingsGroup, 45 | ], 46 | }, 47 | { 48 | label: 'Perks', 49 | name: 'perks', 50 | summary: '{{fields.title}}', 51 | widget: 'object', 52 | fields: [ 53 | Title, 54 | Content, 55 | { 56 | label: 'Columns', 57 | name: 'columns', 58 | widget: 'list', 59 | summary: '{{fields.title}}', 60 | fields: [Title, Content], 61 | }, 62 | SettingsGroup, 63 | ], 64 | }, 65 | { 66 | label: 'Recent Articles', 67 | name: 'recentArticles', 68 | widget: 'object', 69 | fields: [Title, SettingsGroup], 70 | }, 71 | { 72 | label: 'Form', 73 | name: 'form', 74 | widget: 'object', 75 | fields: [ 76 | { 77 | label: 'Form', 78 | name: 'form', 79 | widget: 'relation', 80 | collection: 'forms', 81 | search_fields: ['title'], 82 | display_fields: ['{{id}} - {{title}}'], 83 | value_field: 'id', 84 | required: false, 85 | }, 86 | SettingsGroup, 87 | ], 88 | }, 89 | ], 90 | }; 91 | 92 | export default Config; 93 | -------------------------------------------------------------------------------- /cms/cms.js: -------------------------------------------------------------------------------- 1 | import CMS from 'decap-cms-app' 2 | import { Widget as UuidWidget } from 'netlify-cms-widget-id' 3 | import { Widget as PermalinkWidget } from 'netlify-cms-widget-permalink' 4 | import authors from './collections/authors' 5 | import forms from './collections/forms' 6 | import pages from './collections/pages' 7 | import posts from './collections/posts' 8 | import settings from './collections/settings' 9 | import FormPreview from './previews/FormPreview' 10 | import PagePreview from './previews/Page' 11 | 12 | const config = { 13 | config: { 14 | load_config_file: false, 15 | display_url: process.env.GATSBY_APP_URL, 16 | local_backend: true, 17 | backend: { 18 | name: 'git-gateway', 19 | // branch: 'next', 20 | }, 21 | slug: { 22 | encoding: 'ascii', 23 | clean_accents: true, 24 | }, 25 | media_folder: '/static/img', 26 | public_folder: '/img', 27 | collections: [pages, posts, authors, forms, settings], 28 | }, 29 | } 30 | 31 | 32 | CMS.registerPreviewStyle('../commons.css') 33 | CMS.registerPreviewTemplate('pages', PagePreview) 34 | CMS.registerPreviewTemplate('forms', FormPreview) 35 | 36 | const injectCustomStyle = () => { 37 | const style = document.createElement('style') 38 | style.innerHTML = ` 39 | div[data-slate-editor] { 40 | -webkit-user-modify: read-write !important; 41 | } 42 | ` 43 | document.head.appendChild(style) 44 | } 45 | 46 | injectCustomStyle() 47 | 48 | CMS.registerWidget(UuidWidget) 49 | CMS.registerWidget(PermalinkWidget) 50 | 51 | CMS.init(config) 52 | -------------------------------------------------------------------------------- /cms/collections/authors.js: -------------------------------------------------------------------------------- 1 | import { ID } from '../fields'; 2 | 3 | const collection = { 4 | name: 'authors', 5 | label: 'Authors', 6 | editor: { 7 | preview: false, 8 | }, 9 | description: 'Blog Authors', 10 | folder: 'content/authors', 11 | slug: '{{slug}}', 12 | summary: '{{title}}', 13 | create: true, 14 | fields: [ 15 | ID, 16 | { 17 | label: 'Type', 18 | name: 'type', 19 | widget: 'hidden', 20 | default: 'author', 21 | }, 22 | { 23 | label: 'Name', 24 | name: 'title', 25 | widget: 'string', 26 | default: '', 27 | }, 28 | { 29 | label: 'Featured Image', 30 | name: 'thumbnail', 31 | widget: 'image', 32 | default: '', 33 | required: false, 34 | }, 35 | { 36 | label: 'Description', 37 | name: 'body', 38 | widget: 'markdown', 39 | default: '', 40 | required: false, 41 | }, 42 | ], 43 | }; 44 | 45 | export default collection; 46 | -------------------------------------------------------------------------------- /cms/collections/forms.js: -------------------------------------------------------------------------------- 1 | import { ID } from '../fields'; 2 | import { Button } from '../fields'; 3 | 4 | const collection = { 5 | name: 'forms', 6 | label: 'Forms', 7 | description: 'Forms', 8 | folder: 'content/forms', 9 | create: true, 10 | fields: [ 11 | ID, 12 | { 13 | label: 'Type', 14 | name: 'type', 15 | widget: 'hidden', 16 | default: 'form', 17 | }, 18 | { 19 | label: 'Layout', 20 | name: 'layout', 21 | widget: 'hidden', 22 | default: 'hidden', 23 | }, 24 | { 25 | label: 'Name', 26 | name: 'title', 27 | widget: 'string', 28 | }, 29 | { 30 | label: 'Settings', 31 | name: 'settings', 32 | widget: 'object', 33 | fields: [ 34 | { 35 | label: 'Resolver', 36 | name: 'resolver', 37 | widget: 'select', 38 | default: 'Form', 39 | options: ['Form'], 40 | }, 41 | { 42 | label: 'Success Message', 43 | name: 'success_msg', 44 | widget: 'string', 45 | default: 'Thank you for reaching out!', 46 | required: false, 47 | }, 48 | { 49 | label: 'Event ID', 50 | name: 'event_id', 51 | widget: 'string', 52 | required: false, 53 | }, 54 | ], 55 | }, 56 | { 57 | label: 'Rows', 58 | name: 'rows', 59 | widget: 'list', 60 | fields: [ 61 | { 62 | label: 'Position', 63 | name: 'position', 64 | widget: 'select', 65 | default: 'bottom', 66 | options: ['bottom', 'center'], 67 | }, 68 | { 69 | label: 'Fields', 70 | name: 'fields', 71 | widget: 'list', 72 | types: [ 73 | { 74 | label: 'Input', 75 | name: 'input', 76 | widget: 'object', 77 | summary: '{{label}}', 78 | fields: [ 79 | { 80 | label: 'Label', 81 | name: 'label', 82 | widget: 'string', 83 | required: false, 84 | }, 85 | { 86 | label: 'Input Type', 87 | name: 'input_type', 88 | widget: 'select', 89 | default: 'text', 90 | options: ['text', 'email', 'tel', 'hidden', 'time'], 91 | }, 92 | { 93 | label: 'Autocomplete', 94 | name: 'autocomplete', 95 | widget: 'select', 96 | default: 'on', 97 | options: [ 98 | 'on', 99 | 'off', 100 | 'name', 101 | 'tel', 102 | 'email', 103 | 'organization', 104 | ], 105 | }, 106 | { 107 | label: 'Placeholder', 108 | name: 'placeholder', 109 | widget: 'string', 110 | required: false, 111 | }, 112 | { 113 | label: 'Value', 114 | name: 'value', 115 | widget: 'string', 116 | required: false, 117 | }, 118 | { 119 | label: 'Required', 120 | name: 'required', 121 | widget: 'boolean', 122 | default: false, 123 | required: false, 124 | }, 125 | ], 126 | }, 127 | { 128 | label: 'Textarea', 129 | name: 'textarea', 130 | widget: 'object', 131 | summary: '{{label}}', 132 | fields: [ 133 | { 134 | label: 'Label', 135 | name: 'label', 136 | widget: 'string', 137 | required: false, 138 | }, 139 | { 140 | label: 'Placeholder', 141 | name: 'placeholder', 142 | widget: 'string', 143 | required: false, 144 | }, 145 | { 146 | label: 'Required', 147 | name: 'required', 148 | widget: 'boolean', 149 | default: false, 150 | required: false, 151 | }, 152 | ], 153 | }, 154 | { 155 | label: 'Checkbox', 156 | name: 'checkbox', 157 | widget: 'object', 158 | summary: '{{nam}}', 159 | fields: [ 160 | { 161 | label: 'Label', 162 | name: 'label', 163 | widget: 'string', 164 | required: false, 165 | }, 166 | { 167 | label: 'Name', 168 | name: 'name', 169 | widget: 'string', 170 | required: true, 171 | }, 172 | { 173 | label: 'Required', 174 | name: 'required', 175 | widget: 'boolean', 176 | default: false, 177 | required: false, 178 | }, 179 | ], 180 | }, 181 | { 182 | label: 'Button', 183 | name: 'button', 184 | widget: 'object', 185 | fields: [Button], 186 | }, 187 | { 188 | label: 'Text', 189 | name: 'text', 190 | widget: 'object', 191 | fields: [ 192 | { 193 | label: 'Content', 194 | name: 'content', 195 | widget: 'markdown', 196 | required: false, 197 | }, 198 | ], 199 | }, 200 | { 201 | label: 'Submit', 202 | name: 'submit', 203 | widget: 'object', 204 | fields: [ 205 | { 206 | label: 'Label', 207 | name: 'label', 208 | widget: 'string', 209 | required: false, 210 | }, 211 | ], 212 | }, 213 | ], 214 | }, 215 | ], 216 | }, 217 | ], 218 | }; 219 | 220 | export default collection; 221 | -------------------------------------------------------------------------------- /cms/collections/pages.js: -------------------------------------------------------------------------------- 1 | import seo from '../fields/seo'; 2 | import { ID } from '../fields'; 3 | import Blocks from '../blocks'; 4 | import { PermalinkField } from '../fields/permalink-field'; 5 | 6 | const collection = { 7 | name: 'pages', 8 | label: 'Page', 9 | description: 'Custom pages', 10 | folder: 'content/pages', 11 | create: true, 12 | fields: [ 13 | ID, 14 | { 15 | label: 'Type', 16 | name: 'type', 17 | widget: 'hidden', 18 | default: 'page', 19 | }, 20 | { 21 | label: 'Layout', 22 | name: 'layout', 23 | widget: 'hidden', 24 | default: 'page-builder', 25 | }, 26 | { 27 | label: 'Title', 28 | name: 'title', 29 | widget: 'string', 30 | }, 31 | PermalinkField(), 32 | Blocks, 33 | seo, 34 | ], 35 | }; 36 | 37 | export default collection; 38 | -------------------------------------------------------------------------------- /cms/collections/posts.js: -------------------------------------------------------------------------------- 1 | import { PermalinkField } from '../fields/permalink-field'; 2 | import seo from '../fields/seo'; 3 | import { ID } from '../fields'; 4 | 5 | const collection = { 6 | name: 'blog', 7 | label: 'Posts', 8 | editor: { 9 | preview: false, 10 | }, 11 | description: 'Blog posts collection', 12 | folder: 'content/blog', 13 | slug: '{{slug}}', 14 | summary: 15 | "{{title}} - {{date | date('YYYY-MM-DD')}} – {{body | truncate(40, '***')}}", 16 | create: true, 17 | fields: [ 18 | ID, 19 | { 20 | label: 'Type', 21 | name: 'type', 22 | widget: 'hidden', 23 | default: 'post', 24 | }, 25 | { 26 | label: 'Layout', 27 | name: 'layout', 28 | widget: 'hidden', 29 | default: 'post', 30 | }, 31 | { 32 | label: 'Title', 33 | name: 'title', 34 | widget: 'string', 35 | default: '', 36 | }, 37 | PermalinkField('blog'), 38 | { 39 | label: 'Featured Image', 40 | name: 'thumbnail', 41 | widget: 'image', 42 | default: '', 43 | required: false, 44 | }, 45 | { 46 | label: 'Date', 47 | name: 'date', 48 | widget: 'datetime', 49 | default: '', 50 | required: false, 51 | }, 52 | { 53 | label: 'Author', 54 | name: 'author', 55 | widget: 'relation', 56 | collection: 'authors', 57 | default: '', 58 | search_fields: ['title'], 59 | display_fields: ['title'], 60 | value_field: 'id', 61 | required: false, 62 | }, 63 | { 64 | label: 'Excerpt', 65 | name: 'excerpt', 66 | widget: 'markdown', 67 | default: '', 68 | required: false, 69 | }, 70 | { 71 | label: 'Body', 72 | name: 'body', 73 | widget: 'markdown', 74 | default: '', 75 | required: false, 76 | }, 77 | seo, 78 | ], 79 | }; 80 | 81 | export default collection; 82 | -------------------------------------------------------------------------------- /cms/collections/settings.js: -------------------------------------------------------------------------------- 1 | import navigationField from '../fields/navigation-field'; 2 | 3 | const collection = { 4 | name: 'settings', 5 | label: 'Settings', 6 | description: 'Settings for theme', 7 | files: [ 8 | { 9 | label: 'Main Navigation', 10 | name: 'nav', 11 | file: 'src/settings/main.json', 12 | editor: { 13 | preview: false, 14 | }, 15 | fields: [navigationField()], 16 | }, 17 | { 18 | label: 'Footer Navigation', 19 | name: 'footer', 20 | file: 'src/settings/footer.json', 21 | editor: { 22 | preview: false, 23 | }, 24 | fields: [navigationField()], 25 | }, 26 | { 27 | label: 'Site Metadata & SEO Settings', 28 | name: 'seo', 29 | file: 'src/settings/seo.json', 30 | summary: 'Change basic SEO configuration and site meta like URL', 31 | editor: { 32 | preview: false, 33 | }, 34 | fields: [ 35 | { 36 | label: 'Base title', 37 | name: 'baseTitle', 38 | widget: 'string', 39 | required: false, 40 | }, 41 | { 42 | label: 'Separator', 43 | name: 'separator', 44 | widget: 'string', 45 | required: false, 46 | }, 47 | { 48 | label: 'Title', 49 | name: 'title', 50 | widget: 'string', 51 | required: false, 52 | }, 53 | { 54 | label: 'Description', 55 | name: 'description', 56 | widget: 'string', 57 | required: false, 58 | }, 59 | { 60 | label: 'Language Code', 61 | name: 'lang', 62 | widget: 'string', 63 | required: false, 64 | }, 65 | { 66 | label: 'Keywords', 67 | name: 'keyword', 68 | widget: 'string', 69 | required: false, 70 | }, 71 | { 72 | label: 'Image', 73 | name: 'image', 74 | widget: 'image', 75 | required: false, 76 | }, 77 | { 78 | label: 'Twitter Handle', 79 | name: 'twitterHandle', 80 | widget: 'string', 81 | required: false, 82 | }, 83 | { 84 | label: 'Theme Color', 85 | name: 'themeColor', 86 | widget: 'color', 87 | required: false, 88 | }, 89 | ], 90 | }, 91 | ], 92 | }; 93 | 94 | export default collection; 95 | -------------------------------------------------------------------------------- /cms/fields/index.js: -------------------------------------------------------------------------------- 1 | export const ID = { label: 'ID', name: 'id', widget: 'uuid' }; 2 | 3 | export const SelectField = ( 4 | initial, 5 | options = [], 6 | label = 'Variant', 7 | name = 'variant', 8 | ) => ({ 9 | label, 10 | name, 11 | widget: 'select', 12 | options, 13 | ...(initial ? { default: initial } : null), 14 | }); 15 | 16 | export const ImageField = (name = 'image', fieldName = 'photo') => ({ 17 | label: 'Image', 18 | name: fieldName, 19 | widget: 'object', 20 | fields: [ 21 | { label: 'Image', name, widget: 'image', required: false }, 22 | { label: 'Alt', name: 'alt', widget: 'string', required: false }, 23 | ], 24 | }); 25 | 26 | export const Button = { 27 | label: 'Button', 28 | name: 'button', 29 | widget: 'object', 30 | collapsed: true, 31 | fields: [ 32 | { 33 | label: 'Content', 34 | name: 'content', 35 | widget: 'string', 36 | required: false, 37 | }, 38 | { 39 | label: 'URL', 40 | name: 'url', 41 | widget: 'string', 42 | required: false, 43 | }, 44 | SelectField('default', ['default', 'arrow', 'button', 'outlined']), 45 | ], 46 | }; 47 | 48 | export const Buttons = { 49 | label: 'Buttons', 50 | name: 'buttons', 51 | widget: 'list', 52 | fields: [Button], 53 | }; 54 | 55 | export const Title = { 56 | label: 'Title', 57 | name: 'title', 58 | widget: 'string', 59 | required: false, 60 | }; 61 | 62 | export const Content = { 63 | label: 'Content', 64 | name: 'content', 65 | widget: 'markdown', 66 | required: false, 67 | }; 68 | 69 | export const SettingsGroup = { 70 | label: 'Settings', 71 | name: 'settings', 72 | widget: 'object', 73 | collapsed: true, 74 | fields: [ 75 | SelectField('default', ['default', 'dark', 'gray']), 76 | SelectField( 77 | 'md', 78 | ['none', 'sm', 'md', 'lg', 'xl'], 79 | 'Padding Top', 80 | 'padding_top', 81 | ), 82 | SelectField( 83 | 'md', 84 | ['none', 'sm', 'md', 'lg', 'xl'], 85 | 'Padding Bottom', 86 | 'padding_bottom', 87 | ), 88 | SelectField( 89 | 'none', 90 | ['none', 'sm', 'md', 'lg', 'xl'], 91 | 'Margin Top', 92 | 'margin_top', 93 | ), 94 | SelectField( 95 | 'none', 96 | ['none', 'sm', 'md', 'lg', 'xl'], 97 | 'Margin Bottom', 98 | 'margin_bottom', 99 | ), 100 | ], 101 | }; 102 | -------------------------------------------------------------------------------- /cms/fields/navigation-field.js: -------------------------------------------------------------------------------- 1 | export function Field(label = 'Navigation', name = 'nav') { 2 | return { 3 | label: label, 4 | name: name, 5 | widget: 'list', 6 | summary: '{{fields.name}} {{fields.permalink}}', 7 | fields: [ 8 | { 9 | label: 'Name', 10 | name: 'name', 11 | widget: 'string', 12 | }, 13 | { 14 | label: 'Permalink', 15 | name: 'permalink', 16 | widget: 'string', 17 | }, 18 | { 19 | label: 'Children', 20 | name: 'children', 21 | widget: 'list', 22 | summary: '{{fields.name}} {{fields.permalink}}', 23 | fields: [ 24 | { 25 | label: 'Name', 26 | name: 'name', 27 | widget: 'string', 28 | }, 29 | { 30 | label: 'Permalink', 31 | name: 'permalink', 32 | widget: 'string', 33 | }, 34 | ], 35 | }, 36 | ], 37 | }; 38 | } 39 | 40 | export default Field; 41 | -------------------------------------------------------------------------------- /cms/fields/permalink-field.js: -------------------------------------------------------------------------------- 1 | export function PermalinkField(prefix = '') { 2 | return { 3 | label: 'Permalink', 4 | name: 'permalink', 5 | widget: 'permalink', 6 | required: true, 7 | url: process.env.GATSBY_APP_URL, 8 | prefix: prefix, 9 | hint: 'The post URL (do not include folder or file extension)', 10 | }; 11 | } 12 | 13 | export default PermalinkField; 14 | -------------------------------------------------------------------------------- /cms/fields/seo.js: -------------------------------------------------------------------------------- 1 | import metadata from '../../src/settings/seo.json'; 2 | 3 | const partial = { 4 | label: 'SEO Settings', 5 | name: 'seo', 6 | widget: 'object', 7 | collapsed: true, 8 | fields: [ 9 | { 10 | label: 'Title', 11 | name: 'title', 12 | widget: 'string', 13 | required: false, 14 | hint: `Default title: ${metadata.title}, title template is '{title} ${metadata.separator} ${metadata.baseTitle}'`, 15 | }, 16 | { 17 | label: 'Meta Description', 18 | name: 'description', 19 | widget: 'text', 20 | required: false, 21 | hint: `Default description: ${metadata.description}`, 22 | }, 23 | { 24 | label: 'Image', 25 | name: 'ogimage', 26 | widget: 'image', 27 | required: true, 28 | default: metadata.image, 29 | }, 30 | ], 31 | }; 32 | 33 | export default partial; 34 | -------------------------------------------------------------------------------- /cms/previews/FormPreview.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Form from '@/components/Form/Form' 3 | 4 | export default class PagePreview extends React.Component { 5 | render() { 6 | const blocks = this.props.widgetsFor('rows').toJS() 7 | let blocksUpdated = [] 8 | let hasBlocks = Array.isArray(blocks) 9 | if (hasBlocks) { 10 | blocksUpdated = blocks.map((block) => block.data) 11 | } 12 | 13 | return ( 14 |
15 | {hasBlocks ? ( 16 |
17 |
28 |
29 | ) : ( 30 |
31 |

Add first block to start creating your form

32 |
33 | )} 34 |
35 | ) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /cms/previews/Page.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PageBuilder from '../../src/components/PageBuilder'; 3 | 4 | export default class PagePreview extends React.Component { 5 | render() { 6 | const blocks = this.props.widgetsFor('blocks').toJS(); 7 | let blocksUpdated = []; 8 | let hasBlocks = Array.isArray(blocks); 9 | if (hasBlocks) { 10 | blocksUpdated = blocks.map((block) => block.data); 11 | } 12 | 13 | return ( 14 |
15 | {hasBlocks ? ( 16 | 17 | ) : ( 18 |
19 |

Add first block to start creating your website

20 |
21 | )} 22 |
23 | ); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /content/authors/wojciech-kaluzny.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: a1d1172a-8736-47c1-831d-3e508729fee2 3 | type: author 4 | title: Wojciech Kałużny 5 | thumbnail: /img/wojciech-kaluzny-20-312x312.jpg 6 | --- 7 | WK is the technical wizard behind the technology delivered by Clean Commit. -------------------------------------------------------------------------------- /content/blog/hello-world.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: 44d0d78e-1f11-4f92-85ac-d144d60bb573 3 | type: post 4 | layout: post 5 | title: Philosophy behind Henlo. 6 | permalink: /blog/hello-world/ 7 | thumbnail: /img/henlo-cover.png 8 | date: 2022-05-11T19:16:41.067Z 9 | author: a1d1172a-8736-47c1-831d-3e508729fee2 10 | excerpt: If you’re a WordPress developer then you must have heard about a plugin 11 | called Advanced Custom Fields and a Flexible Content field that allows editors 12 | to generate new pages easily. 13 | --- 14 | 15 | If you’re a WordPress developer then you must have heard about a plugin called Advanced Custom Fields and a Flexible Content field that allows editors to generate new pages easily. 16 | 17 | When I started to move more into JAMStack I wanted to recreate ACF’s Flexible Content field in Gatsby. It's possible to use WordPress as a headless CMS and some headless CMS have implemented some sort of an alternative. Prismic has Slices (unfortunately you can’t create multiple repeatable fields within fields). 18 | 19 | For smaller projects WordPress or Prismic may be too complex. In such cases, I usually go with my favorite flat-file CMS - Decap CMS. 20 | 21 | Decap CMS offers everything you need, it’s open-source and free to use. The only thing missing? Flexible Content field. Fortunately, with beta features - Manual Initialization and Variable Types for List fields we can easily create a solution that copies ACF's Flexible Content. 22 | 23 | ## Why using flexible content is a great idea? 24 | 25 | Advanced Custom Fields' Flexible Content allows editors to quickly make significant changes without engaging developers. Creating new pages is a breeze, and optimizing for conversions is easier. 26 | 27 | Using a singular template may not be the best way to organize your content, especially if you want to quickly test new changes. That's why component-based, modular design gives you much more flexibility. 28 | 29 | It lowers development and maintenance costs. Websites are tools that have to generate business value. The better system you build the longer it’ll last without any code changes. 30 | -------------------------------------------------------------------------------- /content/forms/example.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: T9c8SkeW4 3 | type: form 4 | layout: hidden 5 | title: Contact Form 6 | settings: 7 | resolver: Form 8 | success_msg: Thank you for reaching out! 9 | event_id: XJHYJIYC 10 | rows: 11 | - fields: 12 | - type: input 13 | input_type: text 14 | required: true 15 | label: Full name 16 | autocomplete: name 17 | - type: input 18 | input_type: text 19 | required: false 20 | label: Company Name 21 | autocomplete: organization 22 | position: bottom 23 | - fields: 24 | - type: input 25 | input_type: email 26 | required: true 27 | label: Email Address 28 | autocomplete: email 29 | - type: input 30 | input_type: tel 31 | required: false 32 | label: Phone Number 33 | autocomplete: tel 34 | - type: input 35 | input_type: hidden 36 | value: hidden field 37 | required: false 38 | autocomplete: off 39 | position: bottom 40 | - fields: 41 | - type: textarea 42 | required: true 43 | label: What’s the main issue we’ll focus on? 44 | position: bottom 45 | - fields: 46 | - type: checkbox 47 | required: true 48 | label: I agree to the terms and conditions 49 | name: terms 50 | position: bottom 51 | - fields: 52 | - type: submit 53 | label: Take the first step 54 | position: center 55 | --- 56 | -------------------------------------------------------------------------------- /content/pages/about.md: -------------------------------------------------------------------------------- 1 | --- 2 | excerpt: '' 3 | id: 857ff8b7-56fe-4089-9dfe-ed8dafb19daa 4 | type: page 5 | layout: page-builder 6 | title: About 7 | permalink: /about/ 8 | blocks: 9 | - type: hero 10 | title: About Henlo. 11 | content: >- 12 | ## Wilkommen meine hero! 13 | 14 | This is an amazing opportuinity for you to If you’re a WordPress developer then you must have heard about a plugin called Advanced Custom Fields and a Flexible Content field that allows editors to generate new pages easily. 15 | 16 | 17 | When I started to move more into JAMStack I wanted to recreate ACF’s Flexible Content field in Gatsby. It's possible to use WordPress as a headless CMS and some headless CMS have implemented some sort of an alternative. Prismic has Slices (unfortunately you can’t create multiple repeatable fields within fields). 18 | 19 | 20 | For smaller projects WordPress or Prismic may be too complex. In such cases, I usually go with my favorite flat-file CMS - Netlify CMS. 21 | 22 | 23 | Netlify CMS offers everything you need, it’s open-source and free to use. The only thing missing? Flexible Content field. Fortunately, with beta features - Manual Initialization and Variable Types for List fields we can easily create a solution that copies ACF's Flexible Content. 24 | 25 | 26 | ### Why using flexible content is a great idea? 27 | 28 | Advanced Custom Fields' Flexible Content allows editors to quickly make significant changes without engaging developers. Creating new pages is a breeze, and optimizing for conversions is easier. 29 | 30 | 31 | Using a singular template may not be the best way to organize your content, especially if you want to quickly test new changes. That's why component-based, modular design gives you much more flexibility. 32 | 33 | 34 | It lowers development and maintenance costs. Websites are tools that have to generate business value. The better system you build the longer it’ll last without any code changes. 35 | variant: default 36 | thumbnail: '' 37 | date: 2022-04-11T22:00:00.000Z 38 | --- 39 | -------------------------------------------------------------------------------- /content/pages/blog.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: cf54cef7-dddb-48cc-835e-ce80ed5f9122 3 | type: page 4 | layout: page-builder 5 | title: Blog 6 | permalink: /blog/ 7 | blocks: 8 | - type: hero 9 | title: '' 10 | content: |- 11 | # Blog 12 | Read a bit more about henlo 13 | variant: default 14 | - type: recentArticles 15 | title: Recent Articles 16 | --- 17 | -------------------------------------------------------------------------------- /content/pages/home-page.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: JUJDCFVbWC 3 | type: page 4 | layout: page-builder 5 | title: Home Page 6 | permalink: / 7 | thumbnail: '' 8 | blocks: 9 | - type: hero 10 | title: Henlo. 11 | content: |- 12 | ## The most advanced starter for Gatsby & Netlify CMS. 13 | 14 | Extensible, block based starter for Netlify CMS.\ 15 | Built with performance in mind, styled with TailwindCSS. 16 | 17 | **Think lightweight WordPress.** 18 | variant: centered 19 | buttons: 20 | - button: 21 | variant: default 22 | content: Documentation 23 | url: https://github.com/clean-commit/gatsby-starter-henlo 24 | - type: perks 25 | title: Features 26 | columns: 27 | - title: Manual Initialization 28 | content: Ditch yaml for CMS configuration, use JS instead 29 | - title: Support For Local Development 30 | content: Develop & test Netlify CMS configuration locally. 31 | - title: Integrated Forms 32 | content: Create forms with Netlify CMS, powered by 33 | [react-hook-form](https://react-hook-form.com/). 34 | - title: Integrated Previews 35 | content: 36 | Blocks created by developers are instantly available for previews from 37 | Netlify CMS layout 38 | - title: Permalink-based page generation 39 | content: 'Automatically generated pages based on permalinks. ' 40 | - title: Modify Navigation with ease 41 | content: 42 | Navigation can be modified with ease, without a need for changing the 43 | code base 44 | content: '' 45 | - type: content_image 46 | variant: reversed 47 | title: Block-based page creator with Previews 48 | content: 49 | Create blocks and reuse them across different pages that can be created 50 | using Netlify CMS UI. Previews will be generated automatically for all 51 | blocks! 52 | buttons: 53 | - button: 54 | variant: default 55 | content: Get it now 56 | url: https://github.com/clean-commit/gatsby-starter-henlo 57 | photo: 58 | image: /img/frame-69.png 59 | alt: Block based page creator 60 | - type: content_image 61 | variant: default 62 | photo: 63 | alt: Premade components schema 64 | image: /img/frame-77.png 65 | title: Premade components & Netlify CMS settings 66 | content: >- 67 | We've created a collection of basic UI elements, so you can quickly create 68 | new blocks and style them from a single component. 69 | 70 | 71 | We did the same for Netlify CMS configuration. You can use basic fields to create new blocks quicker then ever! 72 | buttons: 73 | - button: 74 | variant: default 75 | content: Get it now 76 | url: https://github.com/clean-commit/gatsby-starter-henlo 77 | - type: content_image 78 | variant: reversed 79 | title: Configuration exposed through Netlify CMS 80 | content: >- 81 | No need to change configuration using `gatsby-node.js` or `.env` files! 82 | 83 | 84 | Admins of the site can setup options using Netlify CMS's UI instead. This allows for reusable themes & templates 85 | photo: 86 | image: /img/frame-72.png 87 | alt: Netlify CMS configuration with Henlo 88 | --- 89 | -------------------------------------------------------------------------------- /gatsby-browser.js: -------------------------------------------------------------------------------- 1 | import '@/styles/main.css' 2 | -------------------------------------------------------------------------------- /gatsby-config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | require('dotenv').config({ 3 | path: `.env.${process.env.NODE_ENV}`, 4 | }) 5 | 6 | module.exports = { 7 | siteMetadata: { 8 | siteUrl: process.env.GATSBY_APP_URL || 'http://localhost:8000', 9 | }, 10 | flags: { 11 | DEV_SSR: true, 12 | }, 13 | plugins: [ 14 | 'gatsby-plugin-sharp', 15 | 'gatsby-transformer-sharp', 16 | 'gatsby-plugin-preload-fonts', 17 | 'gatsby-plugin-image', 18 | 'gatsby-plugin-dark-mode', 19 | 'gatsby-plugin-postcss', 20 | { 21 | resolve: 'gatsby-plugin-brotli', 22 | }, 23 | { 24 | resolve: 'gatsby-transformer-remark', 25 | options: { 26 | plugins: [ 27 | { 28 | resolve: `gatsby-remark-relative-images`, 29 | options: { 30 | name: 'uploads', 31 | }, 32 | }, 33 | { 34 | resolve: `gatsby-remark-images`, 35 | options: { 36 | maxWidth: 1000, 37 | quality: 72, 38 | withWebp: true, 39 | withAvif: true, 40 | }, 41 | }, 42 | ], 43 | }, 44 | }, 45 | { 46 | resolve: 'gatsby-plugin-sitemap', 47 | options: { 48 | resolveSiteUrl: () => 49 | process.env.GATSBY_APP_URL || 'https://www.example.com', 50 | }, 51 | }, 52 | { 53 | resolve: 'gatsby-source-filesystem', 54 | options: { 55 | path: `${__dirname}/static/img`, 56 | name: 'uploads', 57 | }, 58 | }, 59 | { 60 | resolve: 'gatsby-source-filesystem', 61 | options: { 62 | path: `${__dirname}/content`, 63 | name: 'pages', 64 | }, 65 | }, 66 | { 67 | resolve: 'gatsby-plugin-root-import', 68 | options: { 69 | '@': path.join(__dirname, 'src'), 70 | '~': path.join(__dirname, ''), 71 | styles: path.join(__dirname, 'src/styles'), 72 | img: path.join(__dirname, 'static/img'), 73 | }, 74 | }, 75 | { 76 | resolve: 'gatsby-plugin-decap-cms', 77 | options: { 78 | manualInit: true, 79 | modulePath: `${__dirname}/cms/cms.js`, 80 | }, 81 | }, 82 | 'gatsby-plugin-netlify', // make sure to keep it last in the array 83 | ], 84 | } 85 | -------------------------------------------------------------------------------- /gatsby-node.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash') 2 | const path = require('path') 3 | const fs = require('fs') 4 | const { createFilePath } = require('gatsby-source-filesystem') 5 | 6 | exports.createSchemaCustomization = ({ actions }) => { 7 | const { createTypes } = actions 8 | const defs = ` 9 | type MarkdownRemark implements Node { 10 | frontmatter: MarkdownRemarkFrontmatter 11 | } 12 | 13 | type MarkdownRemarkFrontmatter { 14 | id: String 15 | title: String 16 | author: String 17 | thumbnail: File @fileByRelativePath @dontInfer 18 | seo: MarkdownRemarkFrontmatterSeo 19 | rows: [MarkdownRemarkFrontmatterRows] 20 | blocks: [Blocks] 21 | } 22 | 23 | type MarkdownRemarkFrontmatterRows { 24 | position: String 25 | fields: [MarkdownRemarkFrontmatterRowsFields] 26 | } 27 | 28 | type MarkdownRemarkFrontmatterRowsFields { 29 | type: String 30 | input_type: String 31 | required: Boolean 32 | label: String 33 | autocomplete: String 34 | content: String 35 | value: String 36 | placeholder: String 37 | button: MarkdownRemarkFrontmatterRowsFieldsButton 38 | } 39 | 40 | type MarkdownRemarkFrontmatterRowsFieldsButton { 41 | variant: String 42 | content: String 43 | url: String 44 | } 45 | 46 | type Blocks { 47 | type: String 48 | photo: Photo 49 | settings: Settings 50 | } 51 | 52 | type Settings { 53 | variant: String 54 | padding_top: String 55 | padding_bottom: String 56 | margin_top: String 57 | margin_bottom: String 58 | } 59 | 60 | type Photo @dontInfer { 61 | alt: String 62 | image: File @fileByRelativePath 63 | } 64 | 65 | type MarkdownRemarkFrontmatterSeo { 66 | title: String 67 | description: String 68 | ogimage: File @fileByRelativePath 69 | }` 70 | createTypes(defs) 71 | } 72 | 73 | exports.createPages = ({ actions, graphql }) => { 74 | const { createPage } = actions 75 | 76 | return graphql(` 77 | { 78 | allMarkdownRemark( 79 | limit: 3000 80 | filter: { frontmatter: { layout: { nin: ["hidden", null] } } } 81 | ) { 82 | edges { 83 | node { 84 | id 85 | fields { 86 | slug 87 | } 88 | frontmatter { 89 | id 90 | layout 91 | permalink 92 | type 93 | } 94 | } 95 | } 96 | } 97 | } 98 | `).then((result) => { 99 | if (result.errors) { 100 | console.log('errors', results.errors) 101 | result.errors.forEach((e) => console.error(e.toString())) 102 | return Promise.reject(result.errors) 103 | } 104 | 105 | const postOrPage = result.data.allMarkdownRemark.edges.filter((edge) => { 106 | let layout = edge.node.frontmatter.layout 107 | const excludes = [null, 'hidden', 'Category'] 108 | return excludes.indexOf(layout) === -1 ? true : false 109 | }) 110 | 111 | postOrPage.forEach((edge) => { 112 | const id = edge.node.id 113 | let pathName = edge.node.frontmatter.permalink || edge.node.fields.slug 114 | let component = path.resolve( 115 | `src/templates/${String(edge.node.frontmatter.layout)}.js`, 116 | ) 117 | 118 | if (fs.existsSync(component)) { 119 | createPage({ 120 | path: pathName, 121 | component, 122 | context: { 123 | id, 124 | }, 125 | }) 126 | } 127 | }) 128 | }) 129 | } 130 | 131 | exports.onCreatePage = ({ page, actions }) => { 132 | const { createPage, deletePage } = actions 133 | const oldPage = Object.assign({}, page) 134 | if (page.path !== oldPage.path) { 135 | deletePage(oldPage) 136 | createPage(page) 137 | } 138 | } 139 | 140 | exports.onCreateNode = ({ node, actions, getNode }) => { 141 | const { createNodeField } = actions 142 | 143 | if (node.internal.type === `MarkdownRemark`) { 144 | const value = createFilePath({ node, getNode }) 145 | createNodeField({ 146 | name: `slug`, 147 | node, 148 | value, 149 | }) 150 | } 151 | } 152 | 153 | exports.onCreateWebpackConfig = ({ actions }) => { 154 | actions.setWebpackConfig({ 155 | resolve: { 156 | alias: { 157 | path: require.resolve('path-browserify'), 158 | }, 159 | fallback: { 160 | fs: false, 161 | }, 162 | }, 163 | }) 164 | } 165 | -------------------------------------------------------------------------------- /gatsby-ssr.js: -------------------------------------------------------------------------------- 1 | const seoData = require('./src/settings/seo.json') 2 | 3 | exports.onRenderBody = ({ setHtmlAttributes }) => { 4 | setHtmlAttributes({ lang: seoData?.lang || 'en' }) 5 | } 6 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "paths": { 5 | "@/*": ["./src/*"], 6 | "~/*": ["./*"], 7 | "img/*": ["./static/img/*"] 8 | } 9 | }, 10 | "exclude": ["node_modules"] 11 | } 12 | -------------------------------------------------------------------------------- /netlify.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | publish = "public" 3 | command = "npm run build" 4 | functions = "lambda" 5 | [build.environment] 6 | YARN_VERSION = "1.9.4" 7 | YARN_FLAGS = "--no-ignore-optional" 8 | 9 | [[plugins]] 10 | package = "@netlify/plugin-gatsby" 11 | 12 | [[redirects]] 13 | from = "https://henlo.netlify.app" 14 | to = "https://henlo.cleancommit.io" 15 | status = 301 16 | force = true 17 | 18 | [[redirects]] 19 | from = "https://henlo.netlify.app/*" 20 | to = "https://henlo.cleancommit.io/:splat" 21 | status = 301 22 | force = true -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gatsby-starter-henlo", 3 | "description": "Minimalistic starter for Gatsby v5, optimized for SEO & Performance.", 4 | "version": "1.3.0", 5 | "author": "Wojciech Kałużny ", 6 | "browserslist": [ 7 | "defaults" 8 | ], 9 | "dependencies": { 10 | "clsx": "^2.1.1", 11 | "decap-cms-app": "^3.6.2", 12 | "gatsby": "^5.14.3", 13 | "gatsby-plugin-brotli": "^2.1.0", 14 | "gatsby-plugin-dark-mode": "^1.1.2", 15 | "gatsby-plugin-decap-cms": "^4.0.4", 16 | "gatsby-plugin-image": "^3.3.2", 17 | "gatsby-plugin-netlify": "^5.1.0", 18 | "gatsby-plugin-postcss": "^6.13.1", 19 | "gatsby-plugin-preload-fonts": "^4.3.1", 20 | "gatsby-plugin-root-import": "^2.0.6", 21 | "gatsby-plugin-sharp": "^5.8.1", 22 | "gatsby-plugin-sitemap": "^6.3.1", 23 | "gatsby-plugin-svgr-loader": "^0.1.0", 24 | "gatsby-remark-copy-linked-files": "^6.3.0", 25 | "gatsby-remark-images": "^7.3.1", 26 | "gatsby-remark-relative-images": "^2.0.2", 27 | "gatsby-source-filesystem": "^5.3.1", 28 | "gatsby-transformer-remark": "^6.3.2", 29 | "gatsby-transformer-sharp": "^5.3.1", 30 | "netlify-cms-widget-id": "^1.0.1", 31 | "netlify-cms-widget-permalink": "^1.0.2", 32 | "parcel-bundler": "^1.9.4", 33 | "path-browserify": "^1.0.1", 34 | "prop-types": "^15.6.0", 35 | "react": "^18.2.0", 36 | "react-dom": "^18.2.0", 37 | "react-helmet": "^6.1.0", 38 | "react-hook-form": "^7.41.3", 39 | "react-markdown": "^8.0.4", 40 | "tailwind-merge": "^3.2.0" 41 | }, 42 | "keywords": [ 43 | "gatsby" 44 | ], 45 | "license": "MIT", 46 | "main": "n/a", 47 | "scripts": { 48 | "start": "gatsby develop", 49 | "dev": "npm run develop", 50 | "develop": "npm run clean && gatsby develop", 51 | "build": "npm run clean && gatsby build", 52 | "cms": "npx decap-server", 53 | "serve": "gatsby serve", 54 | "serve:prod": "npm run clean && gatsby build && gatsby serve", 55 | "build:prod": "netlify build", 56 | "init": "netlify init", 57 | "link": "netlify link", 58 | "clean": "gatsby clean", 59 | "deploy": "npm run clean && netlify build && netlify deploy", 60 | "deploy:prod": "npm run clean && netlify build && netlify deploy --prod", 61 | "format": "prettier --trailing-comma es5 --no-semi --single-quote --write \"{gatsby-*.js,src/**/*.js}\"", 62 | "test": "echo \"Error: no test specified\" && exit 1", 63 | "preload-fonts": "gatsby-preload-fonts" 64 | }, 65 | "devDependencies": { 66 | "@tailwindcss/forms": "^0.5.10", 67 | "@tailwindcss/postcss": "^4.1.3", 68 | "@tailwindcss/typography": "^0.5.16", 69 | "@trivago/prettier-plugin-sort-imports": "^5.2.2", 70 | "postcss": "^8.5.3", 71 | "postcss-nested": "^7.0.2", 72 | "prettier": "^3.5.3", 73 | "prettier-plugin-organize-imports": "^4.1.0", 74 | "prettier-plugin-tailwindcss": "^0.6.11", 75 | "tailwindcss": "^4.1.3", 76 | "typescript": "^5.8.3" 77 | } 78 | } -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | 'postcss-nested': {}, 4 | '@tailwindcss/postcss': {}, 5 | ...(process.env.NODE_ENV === 'production' ? { cssnano: {} } : {}), 6 | }, 7 | }; 8 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["github>netlify/renovate-config:netlify-cms-starter"] 3 | } 4 | -------------------------------------------------------------------------------- /src/api/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clean-commit/gatsby-starter-henlo/e8443f9816c0924b1ecb7a99d5efdfc2e1134741/src/api/.gitkeep -------------------------------------------------------------------------------- /src/blocks/Content.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Container from '@/components/UI/Container' 3 | import Section from '@/components/UI/Section' 4 | import Text from '@/components/UI/Text' 5 | 6 | export default function Content({ data }) { 7 | return ( 8 |
9 | 10 | {data?.content && ( 11 | {data?.content} 12 | )} 13 | 14 |
15 | ) 16 | } 17 | -------------------------------------------------------------------------------- /src/blocks/ContentImage.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Buttons from '@/components/UI/Buttons' 3 | import Container from '@/components/UI/Container' 4 | import Section from '@/components/UI/Section' 5 | import Text from '@/components/UI/Text' 6 | import Title from '@/components/UI/Title' 7 | import { cn } from '@/lib/helper' 8 | import Image from '@/resolvers/Image' 9 | 10 | export default function ContentImage({ data }) { 11 | const isReversed = data?.variant === 'reversed' 12 | return ( 13 |
14 | 15 |
22 |
23 | {data?.photo?.image && ( 24 | {data?.photo?.alt} 29 | )} 30 |
31 |
32 |
33 | {data?.title && ( 34 | 35 | {data?.title} 36 | 37 | )} 38 | {data?.content && ( 39 | 40 | {data?.content} 41 | 42 | )} 43 | {data?.buttons && ( 44 | 45 | )} 46 |
47 |
48 |
49 |
50 |
51 | ) 52 | } 53 | -------------------------------------------------------------------------------- /src/blocks/Hero.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Buttons from '@/components/UI/Buttons' 3 | import Container from '@/components/UI/Container' 4 | import Section from '@/components/UI/Section' 5 | import Text from '@/components/UI/Text' 6 | import { cn } from '@/lib/helper' 7 | 8 | export default function Hero({ data }) { 9 | const isCentered = data?.variant === 'centered' 10 | const isFull = data?.variant === 'full' 11 | return ( 12 |
24 | 25 | {data?.title && ( 26 |

31 | {data?.title} 32 |

33 | )} 34 | 35 | {data?.content} 36 | 37 | {data?.buttons && ( 38 | 42 | )} 43 |
44 |
45 | ) 46 | } 47 | -------------------------------------------------------------------------------- /src/blocks/Perks.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Container from '@/components/UI/Container' 3 | import Section from '@/components/UI/Section' 4 | import Text from '@/components/UI/Text' 5 | import Title from '@/components/UI/Title' 6 | 7 | export default function Perks({ data }) { 8 | return ( 9 |
13 | 14 | {data?.title && ( 15 | 20 | {data?.title} 21 | 22 | )} 23 | {data?.content && ( 24 | 25 | {data?.content} 26 | 27 | )} 28 |
31 | {data?.columns && 32 | data?.columns.map((col, i) => ( 33 |
34 |
43 | {col?.title && ( 44 | 45 | {col.title} 46 | 47 | )} 48 | {col?.content && ( 49 | {col?.content} 50 | )} 51 |
52 |
53 | ))} 54 |
55 |
56 |
57 | ) 58 | } 59 | -------------------------------------------------------------------------------- /src/blocks/RecentArticles.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Recent from '@/components/Article/Recent' 3 | import Container from '@/components/UI/Container' 4 | import Section from '@/components/UI/Section' 5 | 6 | export default function RecentArticles({ data, preview }) { 7 | return ( 8 |
12 | 13 |

14 | {data?.title} 15 |

16 | {preview ? 'Articles will show up here' : } 17 |
18 |
19 | ) 20 | } 21 | -------------------------------------------------------------------------------- /src/components/Article/ArticleCard.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Link from '@/resolvers/Link' 3 | 4 | export default function ArticleCard({ data }) { 5 | const { node: post } = data 6 | return ( 7 |
8 |

9 | {post.frontmatter.title} 10 |

11 |
{post.excerpt}
12 | 13 | Read more
about {post.frontmatter.title}
14 | 15 |
16 | ) 17 | } 18 | -------------------------------------------------------------------------------- /src/components/Article/Recent.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ArticleCard from '@/components/Article/ArticleCard' 3 | import { useRecentArticles } from '@/hooks/useRecentArticles' 4 | 5 | export default function Recent() { 6 | const posts = useRecentArticles() 7 | 8 | return ( 9 |
10 | {posts && posts.map((item, i) => )} 11 |
12 | ) 13 | } 14 | -------------------------------------------------------------------------------- /src/components/DarkmodeToggle.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { ThemeToggler } from 'gatsby-plugin-dark-mode' 3 | 4 | export default function DarkmodeToggle() { 5 | return ( 6 | 7 | {({ theme, toggleTheme }) => { 8 | return ( 9 | 42 | ) 43 | }} 44 | 45 | ) 46 | } 47 | -------------------------------------------------------------------------------- /src/components/Footer.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export default function Footer() { 4 | return ( 5 | 15 | ) 16 | } 17 | -------------------------------------------------------------------------------- /src/components/Form/Form.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | import { useForm } from 'react-hook-form' 3 | import ButtonField from './partials/ButtonField' 4 | import Checkbox from './partials/Checkbox' 5 | import ContentSection from './partials/ContentSection' 6 | import Input from './partials/Input' 7 | import Submit from './partials/Submit' 8 | import TextArea from './partials/TextArea' 9 | import { handleGoal, cn } from '@/lib/helper' 10 | 11 | const encode = (data) => { 12 | return Object.keys(data) 13 | .map((key) => encodeURIComponent(key) + '=' + encodeURIComponent(data[key])) 14 | .join('&') 15 | } 16 | 17 | export default function Form({ data, white }) { 18 | const [isSend, setIsSend] = useState(false) 19 | const [isSending, setIsSending] = useState(false) 20 | const { 21 | register, 22 | handleSubmit, 23 | setValue, 24 | reset, 25 | formState: { errors: fieldErrors }, 26 | } = useForm() 27 | 28 | const onSubmit = async (formData) => { 29 | setIsSending(true) 30 | if (data.settings.resolver === 'Form') { 31 | fetch('/', { 32 | method: 'POST', 33 | headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, 34 | body: encode({ 'form-name': data.title, ...formData }), 35 | }) 36 | .then(() => { 37 | setIsSend(true) 38 | handleGoal(data.settings.event_id) 39 | setTimeout(() => { 40 | reset() 41 | setIsSending(false) 42 | }, 200) 43 | }) 44 | .catch((error) => setIsSending(false)) 45 | } 46 | 47 | if (data.settings.resolver === 'ConvertKit') { 48 | const res = await fetch('/api/subscribe', { 49 | method: 'POST', 50 | body: JSON.stringify(formData), 51 | }) 52 | const returned = JSON.parse(await res.json()) 53 | if (returned.success === true) { 54 | setIsSend(true) 55 | handleGoal(data.settings.event_id) 56 | setTimeout(() => { 57 | reset() 58 | }, 200) 59 | } 60 | setIsSending(false) 61 | } 62 | } 63 | 64 | return ( 65 |
66 |
74 |
75 | {data?.settings?.success_msg} 76 |
77 |
78 | 86 | {data.rows && 87 | data.rows.map((row, i) => ( 88 |
100 | {row.fields && 101 | row.fields.map((field, i) => { 102 | switch (field.type) { 103 | case 'input': 104 | return ( 105 | 113 | ) 114 | case 'textarea': 115 | return ( 116 | 25 |
26 | {error?.message} 27 |
28 |
29 | ) 30 | } 31 | -------------------------------------------------------------------------------- /src/components/Head/DefaultHead.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { useLocation } from '@reach/router' 3 | import { graphql } from 'gatsby' 4 | import { getSrc } from 'gatsby-plugin-image' 5 | import * as seoData from '@/settings/seo.json' 6 | 7 | export default function DefaultHead({ data, children }) { 8 | const { pathname } = useLocation() 9 | const metadata = { ...seoData, siteUrl: process.env.GATSBY_APP_URL } 10 | const metaDescription = data?.description || metadata?.description 11 | const title = data?.title || metadata?.title 12 | const image = data?.ogimage?.childImageSharp 13 | ? `${metadata.siteUrl}${getSrc(data?.ogimage)}` 14 | : `${metadata.siteUrl}${metadata.image}` 15 | 16 | const fullTitle = `${title} ${metadata.separator} ${metadata.baseTitle}` 17 | 18 | return ( 19 | <> 20 | {fullTitle} 21 | 22 | 23 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | {children} 35 | 36 | 37 | 38 | 39 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 91 | 97 | 103 | 109 | 113 | 114 | ) 115 | } 116 | 117 | export const query = graphql` 118 | fragment Seo on MarkdownRemarkFrontmatter { 119 | seo { 120 | title 121 | description 122 | ogimage { 123 | childImageSharp { 124 | gatsbyImageData( 125 | width: 1200 126 | quality: 100 127 | formats: [AUTO] 128 | placeholder: NONE 129 | ) 130 | } 131 | } 132 | } 133 | } 134 | ` 135 | -------------------------------------------------------------------------------- /src/components/Header.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import DarkmodeToggle from './DarkmodeToggle' 3 | import Container from './UI/Container' 4 | import Link from '@/resolvers/Link' 5 | import nav from '@/settings/main.json' 6 | 7 | export default function Header() { 8 | return ( 9 |
10 | 11 | 12 | Henlo. 13 | 14 |
15 | 22 | 23 | 47 |
48 |
49 |
50 | ) 51 | } 52 | -------------------------------------------------------------------------------- /src/components/Layout.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Footer from '@/components/Footer' 3 | import Header from '@/components/Header' 4 | 5 | const Layout = ({ nav = false, children }) => { 6 | return ( 7 | <> 8 |
9 | {nav &&
} 10 |
{children}
11 |
12 |
13 | 14 | ) 15 | } 16 | 17 | export default Layout 18 | -------------------------------------------------------------------------------- /src/components/PageBuilder.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Content from '@/blocks/Content' 3 | import ContentImage from '@/blocks/ContentImage' 4 | import Hero from '@/blocks/Hero' 5 | import Perks from '@/blocks/Perks' 6 | import RecentArticles from '@/blocks/RecentArticles' 7 | import { graphql } from 'gatsby' 8 | 9 | export default function PageBuilder({ blocks, preview = false }) { 10 | return ( 11 | <> 12 | {blocks && 13 | blocks.map((block, i) => { 14 | switch (block.type) { 15 | case 'hero': 16 | return 17 | case 'recentArticles': 18 | return 19 | case 'content_image': 20 | return 21 | case 'perks': 22 | return 23 | case 'content': 24 | return 25 | default: 26 | return ( 27 |
28 |
29 | Missing Section {block.type} 30 |
31 |
32 | ) 33 | } 34 | })} 35 | 36 | ) 37 | } 38 | 39 | export const query = graphql` 40 | fragment Blocks on MarkdownRemarkFrontmatter { 41 | blocks { 42 | type 43 | title 44 | content 45 | columns { 46 | title 47 | content 48 | } 49 | photo { 50 | image { 51 | childImageSharp { 52 | gatsbyImageData( 53 | width: 800 54 | quality: 72 55 | placeholder: DOMINANT_COLOR 56 | formats: [AUTO, WEBP, AVIF] 57 | ) 58 | } 59 | } 60 | alt 61 | } 62 | variant 63 | buttons { 64 | button { 65 | content 66 | url 67 | variant 68 | } 69 | } 70 | settings { 71 | variant 72 | padding_top 73 | padding_bottom 74 | margin_top 75 | margin_bottom 76 | } 77 | } 78 | } 79 | ` 80 | -------------------------------------------------------------------------------- /src/components/UI/Button.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { cn } from '@/lib/helper' 3 | import Link from '@/resolvers/Link' 4 | 5 | export default function Button({ className, button, children, ...props }) { 6 | let buttonStyle = 'group inline-block font-bold text-dark-500 dark:text-white' 7 | switch (button?.variant) { 8 | case 'button': 9 | buttonStyle = `${buttonStyle} border-dark-500 border dark:border-white bg-dark-500 dark:bg-white text-black dark:text-black py-2 px-6 text-center dark:hover:bg-transparent hover:bg-transparent hover:text-dark-500 dark:hover:text-white transition-colors` 10 | break 11 | case 'outlined': 12 | buttonStyle = `${buttonStyle} border-dark-500 border dark:border-white py-2 px-6 text-center dark:hover:bg-white hover:bg-dark-500 hover:text-white dark:hover:text-black transition-colors` 13 | break 14 | default: 15 | buttonStyle = `${buttonStyle} link dark:link-dark` 16 | } 17 | 18 | return ( 19 | <> 20 | {button?.url ? ( 21 | 26 | {children} 27 | 28 | ) : ( 29 | 32 | )} 33 | 34 | ) 35 | } 36 | -------------------------------------------------------------------------------- /src/components/UI/Buttons.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Button from '@/components/UI/Button' 3 | import { cn } from '@/lib/helper' 4 | 5 | export default function Buttons({ buttons, className }) { 6 | return ( 7 |
8 | {buttons.length > 0 && 9 | buttons.map((item, i) => ( 10 | 13 | ))} 14 |
15 | ) 16 | } 17 | -------------------------------------------------------------------------------- /src/components/UI/Container.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { cn } from '@/lib/helper' 3 | 4 | export default function Container({ children, className }) { 5 | return ( 6 |
7 | {children} 8 |
9 | ) 10 | } 11 | -------------------------------------------------------------------------------- /src/components/UI/Label.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export default function Label({ htmlFor, props, children }) { 4 | return ( 5 | 12 | ) 13 | } 14 | -------------------------------------------------------------------------------- /src/components/UI/Section.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { cn } from '@/lib/helper' 3 | 4 | function Section({ settings, className, children }) { 5 | return ( 6 |
7 | {children} 8 |
9 | ) 10 | } 11 | 12 | export default Section 13 | 14 | function resolveSettings(settings) { 15 | const settingsMap = { 16 | padding_top: { 17 | sm: 'pt-10', 18 | md: 'pt-16', 19 | lg: 'pt-24', 20 | xl: 'pt-32', 21 | }, 22 | padding_bottom: { 23 | sm: 'pb-10', 24 | md: 'pb-16', 25 | lg: 'pb-24', 26 | xl: 'pb-32', 27 | }, 28 | margin_top: { 29 | sm: 'mt-5', 30 | md: 'mt-10', 31 | lg: 'mt-16', 32 | xl: 'mt-24', 33 | }, 34 | margin_bottom: { 35 | sm: 'mb-5', 36 | md: 'mb-10', 37 | lg: 'mb-16', 38 | xl: 'mb-24', 39 | }, 40 | variant: { 41 | gray: 'bg-gray-50', 42 | dark: 'bg-gray-950 text-white', 43 | }, 44 | } 45 | 46 | if (!settings) { 47 | return [] 48 | } 49 | 50 | return Object.entries(settings).reduce((classes, [key, value]) => { 51 | const settingMap = settingsMap[key] 52 | if (settingsMap[key]) { 53 | const cssClass = settingMap[value] 54 | if (cssClass) { 55 | classes.push(cssClass) 56 | } 57 | } 58 | return classes 59 | }, []) 60 | } 61 | -------------------------------------------------------------------------------- /src/components/UI/Text.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactMarkdown from 'react-markdown' 3 | import { cn } from '@/lib/helper' 4 | 5 | export default function Text({ children, className, ...props }) { 6 | return ( 7 | ( 10 | 11 | {props.children} 12 | 13 | ), 14 | }} 15 | className={cn('prose dark:prose-invert ', className)} 16 | {...props} 17 | > 18 | {children} 19 | 20 | ) 21 | } 22 | -------------------------------------------------------------------------------- /src/components/UI/Title.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { cn } from '@/lib/helper' 3 | 4 | export default function Title({ 5 | children, 6 | variant = 'base', 7 | Tag = 'h2', 8 | className, 9 | ...props 10 | }) { 11 | let style = 'dark:text-white font-semibold' 12 | switch (variant) { 13 | case 'hero': 14 | style = `${style} text-5xl lg:text-7xl max-w-5xl mb-4 md:mb-8 hero-title` 15 | break 16 | case 'xl': 17 | style = `${style} text-4xl md:text-5xl` 18 | break 19 | case 'lg': 20 | style = `${style} text-3xl md:text-4xl` 21 | break 22 | case 'base': 23 | default: 24 | style = `${style} text-2xl md:text-3xl` 25 | break 26 | case 'sm': 27 | style = `${style} text-2xl` 28 | break 29 | case 'xs': 30 | style = `${style} text-xl` 31 | break 32 | } 33 | return ( 34 | <> 35 | {children && ( 36 | 41 | )} 42 | 43 | ) 44 | } 45 | -------------------------------------------------------------------------------- /src/hooks/useForms.js: -------------------------------------------------------------------------------- 1 | import { useStaticQuery, graphql } from 'gatsby' 2 | 3 | export const useForms = () => { 4 | const { 5 | allMarkdownRemark: { edges: forms }, 6 | } = useStaticQuery(graphql` 7 | query FormsQuery { 8 | allMarkdownRemark(filter: { frontmatter: { type: { eq: "form" } } }) { 9 | edges { 10 | node { 11 | id 12 | frontmatter { 13 | id 14 | title 15 | settings { 16 | resolver 17 | success_msg 18 | event_id 19 | } 20 | rows { 21 | position 22 | fields { 23 | type 24 | input_type 25 | name 26 | value 27 | autocomplete 28 | label 29 | required 30 | content 31 | button { 32 | content 33 | url 34 | variant 35 | } 36 | } 37 | } 38 | } 39 | } 40 | } 41 | } 42 | } 43 | `) 44 | 45 | return forms 46 | } 47 | -------------------------------------------------------------------------------- /src/hooks/useRecentArticles.js: -------------------------------------------------------------------------------- 1 | import { useStaticQuery, graphql } from 'gatsby' 2 | 3 | export const useRecentArticles = () => { 4 | const { 5 | allMarkdownRemark: { edges: posts }, 6 | } = useStaticQuery(graphql` 7 | query RecentArticlesQuery { 8 | allMarkdownRemark( 9 | sort: { frontmatter: { date: DESC } } 10 | filter: { frontmatter: { type: { eq: "post" } } } 11 | limit: 3 12 | ) { 13 | edges { 14 | node { 15 | id 16 | excerpt(pruneLength: 120) 17 | fields { 18 | slug 19 | } 20 | frontmatter { 21 | title 22 | date(formatString: "MMMM DD, YYYY") 23 | author 24 | thumbnail { 25 | childImageSharp { 26 | gatsbyImageData( 27 | width: 690 28 | quality: 72 29 | layout: FULL_WIDTH 30 | placeholder: DOMINANT_COLOR 31 | formats: [AUTO, WEBP, AVIF] 32 | ) 33 | } 34 | } 35 | } 36 | } 37 | } 38 | } 39 | } 40 | `) 41 | return posts 42 | } 43 | -------------------------------------------------------------------------------- /src/lib/helper.js: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx' 2 | import { twMerge } from 'tailwind-merge' 3 | 4 | export const cn = (...classes) => twMerge(clsx(...classes)) 5 | 6 | export function slugify(str, replace = '-') { 7 | return str 8 | .normalize('NFD') 9 | .replace(/[\u0300-\u036f]/g, '') 10 | .toLowerCase() 11 | .trim() 12 | .replace(/[^\w^/\s-]/g, '') 13 | .replace(/[\s_-]+/g, replace) 14 | .replace(/^-+|-+$/g, '') 15 | } 16 | 17 | export function handleGoal(id) { 18 | if (!id) return false 19 | let val = 0 20 | if (typeof window === 'undefined' || typeof window.fathom === 'undefined') { 21 | return 22 | } 23 | 24 | if (typeof fathom === 'object') { 25 | window.fathom.trackGoal(id, val) 26 | } else { 27 | window.fathom('trackGoal', id, val) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/pages/404.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Layout from '@/components/Layout' 3 | import Container from '@/components/UI/Container' 4 | import Link from '@/resolvers/Link' 5 | 6 | const NotFoundPage = () => ( 7 | 8 |
9 | 10 |
11 |

404.

12 |

Page Not Found

13 |
14 | We can’t seem to find the page you’re looking for. Try going back to 15 | the previous page. 16 |
17 | 21 | Back to Home 22 | 23 |
24 |
25 |
26 |
27 | ) 28 | 29 | export default NotFoundPage 30 | -------------------------------------------------------------------------------- /src/resolvers/Image.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { GatsbyImage, getImage } from 'gatsby-plugin-image' 3 | 4 | export default function Image({ src, alt = '', ...props }) { 5 | const isRemote = typeof src === 'string' 6 | const image = !isRemote ? getImage(src) : [] 7 | return ( 8 | <> 9 | {isRemote ? ( 10 | {alt} 11 | ) : ( 12 | 13 | )} 14 | 15 | ) 16 | } 17 | -------------------------------------------------------------------------------- /src/resolvers/Link.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Link as GatsbyLink } from 'gatsby' 3 | 4 | export default function Link({ to, className, children, ...props }) { 5 | const internal = /^\/(?!\/)/.test(to) 6 | return ( 7 | <> 8 | {!internal ? ( 9 | 16 | {children} 17 | 18 | ) : ( 19 | 20 | {children} 21 | 22 | )} 23 | 24 | ) 25 | } 26 | -------------------------------------------------------------------------------- /src/settings/main.json: -------------------------------------------------------------------------------- 1 | { 2 | "nav": [ 3 | { 4 | "name": "Example Blog", 5 | "permalink": "/blog/" 6 | }, 7 | { 8 | "name": "Documentation", 9 | "permalink": "https://github.com/clean-commit/gatsby-starter-henlo" 10 | } 11 | ] 12 | } -------------------------------------------------------------------------------- /src/settings/seo.json: -------------------------------------------------------------------------------- 1 | { 2 | "separator": "|", 3 | "baseTitle": "Henlo.", 4 | "keyword": "gatsby-starter, blazing fast static site", 5 | "lang": "en", 6 | "title": "The most advanced starter for Gatsby & Netlify CMS.", 7 | "themeColor": "#000", 8 | "image": "/img/henlo-cover.png", 9 | "twitterHandle": "@cleancommit", 10 | "description": "Think lightweight WordPress. Extensible, block based starter for Netlify CMS. Built with performance in mind, styled with TailwindCSS. " 11 | } 12 | -------------------------------------------------------------------------------- /src/styles/main.css: -------------------------------------------------------------------------------- 1 | @import 'tailwindcss'; 2 | 3 | @plugin '@tailwindcss/typography'; 4 | @plugin '@tailwindcss/forms'; 5 | 6 | @custom-variant dark (&:is(.dark *)); 7 | 8 | /* 9 | The default border color has changed to `currentcolor` in Tailwind CSS v4, 10 | so we've added these compatibility styles to make sure everything still 11 | looks the same as it did with Tailwind CSS v3. 12 | 13 | If we ever want to remove these styles, we need to add an explicit border 14 | color utility to any element that depends on these defaults. 15 | */ 16 | @layer base { 17 | *, 18 | ::after, 19 | ::before, 20 | ::backdrop, 21 | ::file-selector-button { 22 | border-color: var(--color-gray-200, currentcolor); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/templates/page-builder.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { graphql } from 'gatsby' 3 | import PropTypes from 'prop-types' 4 | import DefaultHead from '@/components/Head/DefaultHead' 5 | import Layout from '@/components/Layout' 6 | import PageBuilder from '@/components/PageBuilder' 7 | 8 | const Page = ({ data }) => { 9 | return ( 10 | 11 | 12 | 13 | ) 14 | } 15 | 16 | Page.propTypes = { 17 | data: PropTypes.shape({ 18 | markdownRemark: PropTypes.shape({ 19 | frontmatter: PropTypes.object, 20 | }), 21 | }), 22 | } 23 | 24 | export default Page 25 | 26 | export const Head = ({ data }) => ( 27 | 28 | 29 | 30 | ) 31 | 32 | export const basicPageQuery = graphql` 33 | query BasicPage($id: String!) { 34 | page: markdownRemark(id: { eq: $id }) { 35 | id 36 | fields { 37 | slug 38 | } 39 | html 40 | frontmatter { 41 | id 42 | title 43 | ...Blocks 44 | ...Seo 45 | } 46 | } 47 | } 48 | ` 49 | -------------------------------------------------------------------------------- /src/templates/post.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { graphql } from 'gatsby' 3 | import PropTypes from 'prop-types' 4 | import DefaultHead from '@/components/Head/DefaultHead' 5 | import Layout from '@/components/Layout' 6 | 7 | const Post = ({ data }) => { 8 | return ( 9 | 10 |
11 |

12 | {data.post.frontmatter.title} 13 |

14 |
18 |
19 |
20 | ) 21 | } 22 | 23 | Post.propTypes = { 24 | data: PropTypes.shape({ 25 | markdownRemark: PropTypes.shape({ 26 | frontmatter: PropTypes.object, 27 | }), 28 | }), 29 | } 30 | 31 | export const Head = ({ data }) => ( 32 | 33 | {/* Additonal values here */} 34 | 35 | 36 | ) 37 | 38 | export default Post 39 | 40 | export const basicPageQuery = graphql` 41 | query PostQuery($id: String!) { 42 | post: markdownRemark(id: { eq: $id }) { 43 | id 44 | html 45 | frontmatter { 46 | id 47 | title 48 | author 49 | ...Seo 50 | } 51 | } 52 | } 53 | ` 54 | -------------------------------------------------------------------------------- /static/fonts/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clean-commit/gatsby-starter-henlo/e8443f9816c0924b1ecb7a99d5efdfc2e1134741/static/fonts/.gitkeep -------------------------------------------------------------------------------- /static/img/favicons/android-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clean-commit/gatsby-starter-henlo/e8443f9816c0924b1ecb7a99d5efdfc2e1134741/static/img/favicons/android-icon-144x144.png -------------------------------------------------------------------------------- /static/img/favicons/android-icon-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clean-commit/gatsby-starter-henlo/e8443f9816c0924b1ecb7a99d5efdfc2e1134741/static/img/favicons/android-icon-192x192.png -------------------------------------------------------------------------------- /static/img/favicons/android-icon-36x36.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clean-commit/gatsby-starter-henlo/e8443f9816c0924b1ecb7a99d5efdfc2e1134741/static/img/favicons/android-icon-36x36.png -------------------------------------------------------------------------------- /static/img/favicons/android-icon-48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clean-commit/gatsby-starter-henlo/e8443f9816c0924b1ecb7a99d5efdfc2e1134741/static/img/favicons/android-icon-48x48.png -------------------------------------------------------------------------------- /static/img/favicons/android-icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clean-commit/gatsby-starter-henlo/e8443f9816c0924b1ecb7a99d5efdfc2e1134741/static/img/favicons/android-icon-72x72.png -------------------------------------------------------------------------------- /static/img/favicons/android-icon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clean-commit/gatsby-starter-henlo/e8443f9816c0924b1ecb7a99d5efdfc2e1134741/static/img/favicons/android-icon-96x96.png -------------------------------------------------------------------------------- /static/img/favicons/apple-icon-114x114.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clean-commit/gatsby-starter-henlo/e8443f9816c0924b1ecb7a99d5efdfc2e1134741/static/img/favicons/apple-icon-114x114.png -------------------------------------------------------------------------------- /static/img/favicons/apple-icon-120x120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clean-commit/gatsby-starter-henlo/e8443f9816c0924b1ecb7a99d5efdfc2e1134741/static/img/favicons/apple-icon-120x120.png -------------------------------------------------------------------------------- /static/img/favicons/apple-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clean-commit/gatsby-starter-henlo/e8443f9816c0924b1ecb7a99d5efdfc2e1134741/static/img/favicons/apple-icon-144x144.png -------------------------------------------------------------------------------- /static/img/favicons/apple-icon-152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clean-commit/gatsby-starter-henlo/e8443f9816c0924b1ecb7a99d5efdfc2e1134741/static/img/favicons/apple-icon-152x152.png -------------------------------------------------------------------------------- /static/img/favicons/apple-icon-180x180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clean-commit/gatsby-starter-henlo/e8443f9816c0924b1ecb7a99d5efdfc2e1134741/static/img/favicons/apple-icon-180x180.png -------------------------------------------------------------------------------- /static/img/favicons/apple-icon-57x57.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clean-commit/gatsby-starter-henlo/e8443f9816c0924b1ecb7a99d5efdfc2e1134741/static/img/favicons/apple-icon-57x57.png -------------------------------------------------------------------------------- /static/img/favicons/apple-icon-60x60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clean-commit/gatsby-starter-henlo/e8443f9816c0924b1ecb7a99d5efdfc2e1134741/static/img/favicons/apple-icon-60x60.png -------------------------------------------------------------------------------- /static/img/favicons/apple-icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clean-commit/gatsby-starter-henlo/e8443f9816c0924b1ecb7a99d5efdfc2e1134741/static/img/favicons/apple-icon-72x72.png -------------------------------------------------------------------------------- /static/img/favicons/apple-icon-76x76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clean-commit/gatsby-starter-henlo/e8443f9816c0924b1ecb7a99d5efdfc2e1134741/static/img/favicons/apple-icon-76x76.png -------------------------------------------------------------------------------- /static/img/favicons/apple-icon-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clean-commit/gatsby-starter-henlo/e8443f9816c0924b1ecb7a99d5efdfc2e1134741/static/img/favicons/apple-icon-precomposed.png -------------------------------------------------------------------------------- /static/img/favicons/apple-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clean-commit/gatsby-starter-henlo/e8443f9816c0924b1ecb7a99d5efdfc2e1134741/static/img/favicons/apple-icon.png -------------------------------------------------------------------------------- /static/img/favicons/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | #ffffff -------------------------------------------------------------------------------- /static/img/favicons/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clean-commit/gatsby-starter-henlo/e8443f9816c0924b1ecb7a99d5efdfc2e1134741/static/img/favicons/favicon-16x16.png -------------------------------------------------------------------------------- /static/img/favicons/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clean-commit/gatsby-starter-henlo/e8443f9816c0924b1ecb7a99d5efdfc2e1134741/static/img/favicons/favicon-32x32.png -------------------------------------------------------------------------------- /static/img/favicons/favicon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clean-commit/gatsby-starter-henlo/e8443f9816c0924b1ecb7a99d5efdfc2e1134741/static/img/favicons/favicon-96x96.png -------------------------------------------------------------------------------- /static/img/favicons/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clean-commit/gatsby-starter-henlo/e8443f9816c0924b1ecb7a99d5efdfc2e1134741/static/img/favicons/favicon.ico -------------------------------------------------------------------------------- /static/img/favicons/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "App", 3 | "icons": [ 4 | { 5 | "src": "\/android-icon-36x36.png", 6 | "sizes": "36x36", 7 | "type": "image\/png", 8 | "density": "0.75" 9 | }, 10 | { 11 | "src": "\/android-icon-48x48.png", 12 | "sizes": "48x48", 13 | "type": "image\/png", 14 | "density": "1.0" 15 | }, 16 | { 17 | "src": "\/android-icon-72x72.png", 18 | "sizes": "72x72", 19 | "type": "image\/png", 20 | "density": "1.5" 21 | }, 22 | { 23 | "src": "\/android-icon-96x96.png", 24 | "sizes": "96x96", 25 | "type": "image\/png", 26 | "density": "2.0" 27 | }, 28 | { 29 | "src": "\/android-icon-144x144.png", 30 | "sizes": "144x144", 31 | "type": "image\/png", 32 | "density": "3.0" 33 | }, 34 | { 35 | "src": "\/android-icon-192x192.png", 36 | "sizes": "192x192", 37 | "type": "image\/png", 38 | "density": "4.0" 39 | } 40 | ] 41 | } -------------------------------------------------------------------------------- /static/img/favicons/ms-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clean-commit/gatsby-starter-henlo/e8443f9816c0924b1ecb7a99d5efdfc2e1134741/static/img/favicons/ms-icon-144x144.png -------------------------------------------------------------------------------- /static/img/favicons/ms-icon-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clean-commit/gatsby-starter-henlo/e8443f9816c0924b1ecb7a99d5efdfc2e1134741/static/img/favicons/ms-icon-150x150.png -------------------------------------------------------------------------------- /static/img/favicons/ms-icon-310x310.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clean-commit/gatsby-starter-henlo/e8443f9816c0924b1ecb7a99d5efdfc2e1134741/static/img/favicons/ms-icon-310x310.png -------------------------------------------------------------------------------- /static/img/favicons/ms-icon-70x70.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clean-commit/gatsby-starter-henlo/e8443f9816c0924b1ecb7a99d5efdfc2e1134741/static/img/favicons/ms-icon-70x70.png -------------------------------------------------------------------------------- /static/img/frame-69.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clean-commit/gatsby-starter-henlo/e8443f9816c0924b1ecb7a99d5efdfc2e1134741/static/img/frame-69.png -------------------------------------------------------------------------------- /static/img/frame-72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clean-commit/gatsby-starter-henlo/e8443f9816c0924b1ecb7a99d5efdfc2e1134741/static/img/frame-72.png -------------------------------------------------------------------------------- /static/img/frame-77.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clean-commit/gatsby-starter-henlo/e8443f9816c0924b1ecb7a99d5efdfc2e1134741/static/img/frame-77.png -------------------------------------------------------------------------------- /static/img/henlo-cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clean-commit/gatsby-starter-henlo/e8443f9816c0924b1ecb7a99d5efdfc2e1134741/static/img/henlo-cover.png -------------------------------------------------------------------------------- /static/img/wojciech-kaluzny-20-312x312.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clean-commit/gatsby-starter-henlo/e8443f9816c0924b1ecb7a99d5efdfc2e1134741/static/img/wojciech-kaluzny-20-312x312.jpg --------------------------------------------------------------------------------